Skip to content

fix(driver-sql): SQLite reconcile reports applied only for the ops the rebuild actually honoured - #12127

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-11722-sqlite-rebuild-applied-honesty
Aug 25, 2026
Merged

fix(driver-sql): SQLite reconcile reports applied only for the ops the rebuild actually honoured#12127
os-warren merged 1 commit into
mainfrom
claude/issue-11722-sqlite-rebuild-applied-honesty

Conversation

@os-warren

@os-warrenos-warren commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes#11722

On SQLite, applyMigrationEntries reported an op as applied whenever a table rebuild ran — including the ops the rebuild is documented to ignore. This makes applied mean the same thing on all three dialects: this op happened.

The defect

SqlDriver.applyMigrationEntries splits by dialect, and the two arms disagreed about what applied means. The in-place arm asks per entry and believes the answer — applyDriftOpInPlace returns false for an op its dialect cannot perform, and the entry goes to skipped. The SQLite arm did not ask at all:

if(this.isSqlite){awaitthis.rebuildSqliteTablePatched(table,ents);applied.push(...ents);// every entry, unconditionally}

rebuildSqliteTablePatched honours exactly four op types — relax_not_null, tighten_not_null, drop_column, drop_column_default — and silently ignores everything else; its own docblock already said so for the varchar ops. An ignored op was still pushed into applied.

The failure mode is a false green, not an error. Nothing throws and nothing is skipped, so every consumer announces work that never happened: reconcileAndWarnDrift logs auto-reconciled {op} on {table}, and the artifact boot gate prints ↪ migrated {op}. The finding is still physically present, so the next boot detects it again, reports drift again, and "migrates" it again — a loop with no failing signal anywhere in it.

Why a test that passes on main would have proved nothing

The gap is latent, held closed from two independent directions neither of which knows it is holding it: enforcesVarcharLength excludes SQLite, so the differ never emits widen_varchar/narrow_varchar there; and multiValueColumnTypeIsLoadBearing excludes SQLite for an unrelated measured reason (a stale textual column does not corrupt the value there), so #11535's manual_column_type_change is never emitted there either.

So the suite constructs the reachability instead of waiting for it. It substitutes exactly one thing and nothing else — the differ's dialect guard — by handing entries straight to the public applyMigrationEntries seam that os migrate apply (packages/cli/src/commands/migrate/apply.ts:199) and the artifact boot gate (packages/cli/src/utils/artifact-boot-migration.ts:143) both call with differ output. Driver, dialect and database are real throughout. All five cases fail on the pre-fix tree — see the ablation below.

The fix

rebuildSqliteTablePatched returns the entries it actually acted on, built in the same pass that fills the four column sets it already partitioned into — deliberately not a second list of op types to keep in sync, so the returned set cannot drift from the work done (honoured.push(e) is reachable only after one of the four branches took the entry; everything else hits an else continue). The caller reports those as applied and routes the remainder to skipped, logging it in the same sentence the in-place arm uses for an op its dialect cannot do:

[schema-drift] widen_varchar on biz_unit.name is unsupported on dialect 'sqlite' — skipped

one greppable line across all three dialects. @objectstack/driver-sqlite-wasm and @objectstack/driver-turso extend SqlDriver without overriding either method, so both inherit the correction; neither has an override to update.

What deliberately does NOT change (clause ② answer: still NO)

No op does anything different — this moves only what is reported. In particular the rebuild still runs for the whole table even when it honours nothing. It re-materializes every kept column's default (#11321, #4560) and the full declared index set from metadata (#3696), so it is not a no-op; suppressing it there would change what the reconciler DOES rather than what it says it did, which is a different question from this one. That is stated in the method's docblock so the next reader does not "optimize" it. applied/skipped remains a reported partition consumed by log lines and CLI counts — not an accept/reject door — and no public surface widens.

Declared file surface — three cards live on sql-driver.ts

Every hunk lands inside the declared band 10100–10560 (old-side hunk headers: 10155, 10196, 10403, 10418, 10422, 10428, 10499).

Verification

Gate union derived, not recalled, at the final commit fd1eeeb162:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack → 14 path-matched families + 6 convention-triggered (test file added). Every one run, exit code captured before any pipe. All green; each gate's own verdict line is quoted in the report on the issue. Highlights:

gateits own verdict line
check:driver-conformanceOK — 45 covered cell(s), 0 in the DEBT ledger, 0 exempt.
check:type-check-debt --re-measureOK — 32 ledger entr(ies) re-measured in 227.7s, 1897 raw tsc error(s) total, none above its recorded number.
check:engine-double-contract390 (file, verb) row(s) held by the RETAINED ledger (no new double)
check:where-matcher299 matcher(s) discovered … none new
check:cross-package-test-inputsOK: 16 package(s) read outside themselves, all declared
pnpm lint (whole repo, eslint . --no-inline-config)exit 0 — run in full, no narrowing claimed

Packages: @objectstack/driver-sqltypecheck exit 0; test132 files / 2016 passed, 8+121 skipped. Both inheritors and the CLI consumer green — driver-sqlite-wasm 25 files, driver-turso 39 files, cli 178 files.

Ablation — direction predicted in writing before the run

Prediction (recorded before the run): restoring the caller's SQLite arm to main's exact two lines turns the new suite RED, on the skipped-membership assertions; not "fewer diagnostics", not a reversal, because the partition is the only producer of the membership those assertions read.

Mutation proved on disk by anchored counts of the text actually changed — not git diff --stat, not an editor exit code — before any result was read:

BEFORE fixed-arm marker : 1 (expect 1) main-arm marker : 0 (expect 0)
AFTER fixed-arm marker : 0 (expect 0) main-arm marker : 1 (expect 1)

No dist is in the resolution path: packages/drivers/driver-sql/dist was absent at ablation time (shown in the run) and the suite imports ./sql-driver.js intra-package, which vitest resolves to the TS source — so no rebuild could stand between the mutation and the reading.

Observed: 5 of 5 RED, the predicted direction, including the consumer-level case failing on expected true to be false — the auto-reconciled line being logged for an op that never happened. Restore was armed with trap … EXIT INT TERM running git checkout HEAD -- {the absolute path}, then verified rather than trusted: git hash-object of the working file 18c76de812602304edb9614457060573e0ad399b equals git rev-parse HEAD:packages/drivers/driver-sql/src/sql-driver.ts, and git status --porcelain is empty.

Changeset

.changeset/sqlite-rebuild-applied-honesty.md, graded patch for @objectstack/driver-sql. Justification: it is a reporting-honesty bug fix with no new capability and no removal. The one contract widening — rebuildSqliteTablePatched now resolving to the honoured ManagedDriftEntry[] where it used to resolve to nothing — cannot break a caller (await-and-discard still compiles; the in-repo direct call in sql-driver-unique-tenancy.test.ts:451 is exactly that shape and is unchanged), only an out-of-tree subclass that overrides a protected internal, which is not a published surface. No workspace subclass overrides it.


Generated by Claude Code

…ebuild honoured
`applyMigrationEntries` split by dialect and the two arms disagreed about what
`applied` means. The in-place arm asks per entry and believes the answer; the
SQLite arm called `rebuildSqliteTablePatched` and pushed EVERY entry into
`applied`. That rebuild honours four op types and silently ignores the rest, so
an ignored op was announced as migrated — `auto-reconciled` in the boot log,
`↪ migrated` at the artifact gate — while still physically present. Next boot:
detected again, reported again, "migrated" again. A loop with no failing signal.
The rebuild now returns the entries it actually acted on, built in the same pass
that fills the four sets it already partitioned into, so the report cannot drift
from the work. The caller routes the remainder to `skipped` and logs it in the
same words the in-place arm uses for an unsupported op.
Reporting only — no op does anything different, and the rebuild still runs for
the whole table even when it honours nothing (it re-materializes defaults and
the declared index set from metadata).
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql, touching 4 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via SqlDriver (symbol))
  • content/docs/data-modeling/index.mdx(via SqlDriver (symbol))
  • content/docs/plugins/packages.mdx(via SqlDriver (symbol))
  • content/docs/protocol/kernel/index.mdx(via SqlDriver (symbol))
  • content/docs/protocol/kernel/lifecycle.mdx(via SqlDriver (symbol))
  • content/docs/protocol/objectql/query-syntax.mdx(via SqlDriver (symbol))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SqlDriver (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9799ffe282d9ad3252806e6721e32b2d29ee8f9bpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 7c1f5d87af0030eeeab0979b045ebd74911cbb51 — the merge of head fd1eeeb162e4db7e3afc8dadbcc9c096a78cee37 into base 9799ffe282d9ad3252806e6721e32b2d29ee8f9b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7c1f5d87af0030eeeab0979b045ebd74911cbb51 && git checkout 7c1f5d87af0030eeeab0979b045ebd74911cbb51
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9799ffe282d9ad3252806e6721e32b2d29ee8f9b fd1eeeb162e4db7e3afc8dadbcc9c096a78cee37 && git checkout -B drift-repro 9799ffe282d9ad3252806e6721e32b2d29ee8f9b && git merge --no-ff fd1eeeb162e4db7e3afc8dadbcc9c096a78cee37
node scripts/docs-audit/affected-docs.mjs --json 9799ffe282d9ad3252806e6721e32b2d29ee8f9b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9799ffe282d9ad3252806e6721e32b2d29ee8f9b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] applyMigrationEntries reports every SQLite entry as applied after a table rebuild, including ops the rebuild is documented to ignore

2 participants

@os-warren@claude