Skip to content

fix(server): reject WITHOUT ROWID tables in assertSyncCompatible#23

Merged
grrowl merged 2 commits into
mainfrom
fix/reject-without-rowid
Jul 2, 2026
Merged

fix(server): reject WITHOUT ROWID tables in assertSyncCompatible#23
grrowl merged 2 commits into
mainfrom
fix/reject-without-rowid

Conversation

@grrowl

@grrowl grrowl commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Why

Follow-up from the #22 review. #22 made cold-snapshot/fetch reads deterministic by defaulting to ORDER BY rowid when the client sends no orderBy (ADR-0015). That default assumes a real rowid — but assertSyncCompatible only rejected an INTEGER PRIMARY KEY pk, not a WITHOUT ROWID table.

A synced table declared CREATE TABLE t (id TEXT PRIMARY KEY, …) WITHOUT ROWID clears every D9 check and registers fine, then the first orderBy-less subscribe/fetch runs ORDER BY rowid and throws no such column: rowid. That error isn't on the caught path, so it propagates out of webSocketMessage and the subscriber just hangs (never receives snap-end) — a silent failure, not a loud one.

Narrow: only a table deliberately declared WITHOUT ROWID is affected. The documented id TEXT PRIMARY KEY pattern is a rowid table and is untouched. Nothing in the wild breaks; this closes a latent gap.

What changed

  • assertSyncCompatible now probes SELECT rowid FROM "<tbl>" LIMIT 0 and rejects the table with a clear message if there's no rowid. It probes the exact property the reader depends on rather than parsing the DDL; an unrelated exec failure rethrows unchanged (guarded on the error message mentioning rowid).
  • Corrects the now-inaccurate claim in ADR-0015 and the sql-compiler comment that the rowid is "always intact" under D9 — D9 forbade only the INTEGER-alias pk.
  • Adds a reason-pinning test in the existing assert-sync-compatible.test.ts style.

Result

Fail-loud at registerSync instead of a silent hang at read time, matching the repo's "reject; don't degrade" rule and leveraging the existing assertSyncCompatible structure.

  • Typecheck clean; full suite 192 green (191 + the new case).

🤖 Generated with Claude Code

grrowl and others added 2 commits July 2, 2026 22:57
…-0015)

The cold-snapshot/fetch reader defaults to `ORDER BY rowid` when the client
sends no orderBy (ADR-0015). A WITHOUT ROWID table has no rowid, so the read
threw `no such column: rowid` and hung the subscriber — after the table had
already passed registration. assertSyncCompatible now probes for a usable
rowid and rejects such a table loudly at registerSync, alongside the existing
INTEGER PRIMARY KEY guard. Probes the exact property the reader depends on
rather than parsing the DDL; an unrelated exec failure rethrows unchanged.

Corrects the ADR-0015 / sql-compiler claim that the rowid is "always intact"
under D9 (D9 forbade only the INTEGER-alias pk, not WITHOUT ROWID). Ordinary
rowid tables (id TEXT PRIMARY KEY) are unaffected. Adds a reason-pinning test;
full suite 192 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review surfaced a shadow the SELECT-rowid probe alone misses: a
user column named `rowid` resolves the probe (SQLite binds the name to the
column), passes validation, then the compiler's default `ORDER BY rowid`
silently sorts by that arbitrary column instead of insertion order. Reject a
declared `rowid` column via the introspected pragma_table_info columns before
the WITHOUT ROWID probe, which makes the probe unambiguous. Adds a test; docs
and CHANGELOG updated to cover all three no-usable-rowid cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant