Repository navigation
fix(driver-sql): os migrate plan on a new database prints no DATABASE_ERROR for the tables whose DDL it deferred (#20821) - #21093
Conversation
…t a DATABASE_ERROR `os migrate plan` boots with the SQL driver's DDL deferred, then reads sys_metadata, sys_metadata_activation and sys_migration, which the plan has just listed as pending creates. On a new database each read was refused and each refusal printed a `[sql-driver] DATABASE_ERROR` warn line. `SqlDriver.backendStatementFault` now sends that refusal to `debug` only when all three hold: DDL is deferred on this driver, the targeted table is in its own deferred set, and `isMissingTableError` holds over the envelope. The throw and the envelope are unchanged; every other refusal still warns. Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…helper `src/` is the cli tsconfig's rootDir, so a src test cannot import `test/helpers/serve-process.ts`; the pin names `bin/run-dev.js` itself and strips the same two env families `childEnv()` strips. Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…grate-plan-deferred-reads
…grate-plan-deferred-reads
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 193bc521bad3888b5daaff37eb4bdc391d6f2ad5 && git checkout 193bc521bad3888b5daaff37eb4bdc391d6f2ad5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 63d1a7c3781e41dca16cf0e286ee02224a579e04 ea9309b800acaf173b36d70706453dbc43918721 && git checkout -B drift-repro 63d1a7c3781e41dca16cf0e286ee02224a579e04 && git merge --no-ff ea9309b800acaf173b36d70706453dbc43918721
node scripts/docs-audit/affected-docs.mjs --json 63d1a7c3781e41dca16cf0e286ee02224a579e04 |
Contract reviewServed-tier: Inputs read: card #20821 (body and all 5 comments: triage 5908818119, claim 5924062757, round-0 report 5924224857, seat answer 5924276655, round-1 report 5925703713); PR #21093 body, its 4-file list, and the net diff against the merge-base ① Derived judgmentsAccept-set: none. The one source hunk ( The demotion keys on exactly the seat answer's three conditions (5924276655 Q1 A), judged one by one:
Triage ⛔ 1, no second message regex: the diff contains no regex and no message text comparison — right. Triage ⛔ 2, not every refused read demoted: any condition false reaches the unchanged Log channel: Fenced regions: the hunk is disjoint from the JSON-column gate ( Public surface: none. No export, no new member, no signature change ( Pins. The driver pin Check-runs on the head (snapshot 06:07Z, 31 runs). Success, 15: Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, Type Check · debt ledger, Type Check · source gates, filter. Skipped, 3: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in). In progress, 13, each one NOT a verdict: Dogfood Regression Gate (1/3), (2/3), (3/3); Lint & Repo Gates; Temporal Conformance (live PG + MySQL); Test Core (1/6), (2/6), (3/6), (4/6), (5/6), (6/6); Type Check · consumer gates; Type Check · workspace. Not waited for and not polled. The Vercel status reads "Canceled by Ignored Build Step" and is not a gate. Landing still requires every check green; this record judges the contract. ② Semver level
Clause-②: ③ Boundary flags
Dev flags from report 5925703713 and the PR body, each answered:
Escalated: nothing. Triage's two ⛔ hold (①). The edit stays inside the amended claim and out of the fenced regions, and the single-writer path guard is green. Implemented-by: VERDICT: PASS |
Fixes #20821
Clause-②: no
os migrate planagainst a database that does not exist yet printed six[sql-driver] DATABASE_ERROR … no such tablelines on stderr. The plan boots with the SQL driver's DDL deferred, lists every table as pendingcreate_table, and then the same boot readssys_metadata,sys_metadata_activationandsys_migration. Every one of those reads was refused, and every reader already answered from the refusal. Only the driver's warn line was wrong.This follows the seat answer on the card (5924276655): Q1 A, a driver-side demotion keyed on the driver's own deferral; Q2 A, the pin is scoped to the three deferred tables.
The six readers (traced with a stack per line at
b253fadfb; unchanged at this head)sys_metadataObjectQLPlugin.restoreMetadataFromDb→ObjectStackProtocolImplementation.loadMetaFromDb(isMissingTableError→loaded: 0)ObjectQLPlugin.start(), phase 2sys_metadatareadAuthoredTranslationLayer(core,fallbacks/authored-translation-sync.ts) →nullkernel:readysys_metadataObjectQLPlugin.readAuthoredHookRowsviaresyncAuthoredHooksNow→nullkernel:readysys_metadataObjectQLPlugin.readAuthoredActionRowsviaresyncAuthoredActionsNow→nullkernel:readysys_metadata_activationObjectStoreActionActivationStore.probeviaObjectQLPlugin.hydrateActionActivations→ its own functional warnkernel:readysys_migrationObjectQL.announceOpenMigrationGates→readMigrationFlagVerified→ "not verified, not conclusive"kernel:bootstrappedThe
DATABASE_ERRORline is written insideSqlDriver.backendStatementFault, before any of these readers sees the error. So a reader-side catch cannot remove it. "Not asked" was measured and declined: no reader can see the deferral, because noIDataDrivermember and no kernel key carries it. Deferred is also not the same as absent: on an existing database the deferral records every object before anyhasTable. Making "not asked" honest would need a new contract fact, andClause-②would become yes.The change
packages/drivers/driver-sql/src/sql-driver.ts,SqlDriver.backendStatementFault, in the warn decision only. A refused statement goes tologger.debuginstead oflogger.warnonly when all three conditions hold:deferredDdl);deferredSchemaObjects;isMissingTableError(envelope, object)holds. This is the one shared predicate, asked over the envelope's declared target. No second message regex.The throw and the envelope (
DATABASE_ERROR, status 500) are unchanged. Every other refusal still warns: a malformed statement on a table that exists, a missing table the driver did not defer, and anything afterflushDeferredSchemaDdl(which clears both the flag and the set). The new branch sits beside PR #20818's pre-DDL scope check. It is disjoint from the PR #20988 hunks, the JSON-column gate region and the JSON-membership helpers.Kept on purpose
ObjectQLPlugin.hydrateActionActivations(sys_metadata_activation is registered but could not be read — packaged-ACTION enable/disable is UNAVAILABLE on this deployment …) is kept. It is the reader's own functional line, not the driver's, and the stdout-identity pin requires it. On a dry run against an absent file it is a false alarm, and that is recorded here.examples/app-crmapp-hook lines (sys_position×3,sys_permission_set×3) are kept. They come from the app'sonEnablekernel:bootstrappedhook, which reads tables the plan's composition never declares. That is a different door: [finding]os migrate planon examples/app-crm runs the app'sonEnablehook, which readssys_position/sys_permission_setthe plan never declares: 6 DATABASE_ERROR + 6 WARN lines on every plan #21054 is not addressed here, and the pin does not count those lines.Before and after on
examples/app-crmnode ../../packages/cli/bin/run.js migrate plan [--json] --database-url file:ABSENT.sqlite. The base is576afc17b(before the fix). The head isea9309b80, rebuilt.DATABASE_ERRORlines on stderrsys_metadata/sys_metadata_activation/sys_migrationNmsdurationsdiffexit 0ℹ Database:…Apply with:) md5e102064c7b13bce96303c1e9b698693de102064c7b13bce96303c1e9b698693d--jsonstdout withdurationremovedcmpexit 0)Tests
packages/drivers/driver-sql/src/sql-driver-20821-deferred-ddl-missing-table.test.ts, on a real SQLite file. Every case asserts the envelopecodeandstatus.debug(withno such table), andwarnstays empty. AfterflushDeferredSchemaDdlthe same read answers[]with nothing logged.packages/cli/src/commands/migrate/plan.deferred-reads.integration.test.ts(integration tier: it spawnsbin/run-dev.js). Its fixture is one host config with a lookup field. On the base, that fixture reproduced exactly the card's 6 lines (4 / 1 / 1). In human mode and under--json, it asserts:DATABASE_ERRORlines naming the three tables;[value-shape]gate is announced, and the activation WARN is present;create_table;ea9309b80:pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2: 204 files passed, 11 skipped; 3285 tests passed, 188 skipped. The skips are the live PG and MySQL cells, which are not provisioned here.@objectstack/cli--project unit: 240 files, 3419 tests passed.@objectstack/cli--project integration, run on this PR's file plusschema-migrate.readonly-probeandschema-migrate.deferred-ddl: 3 files, 9 tests passed.typecheckfordriver-sqlandcliis green, includingcli'scheck:test-typecheck(debt unchanged). The new cli pin is insidetsc's program, becausesrcis theinclude.Reverse verification (from the committed fix, through
scripts/ablation-replace.mjs)this.deferredSchemaObjects.has(targetedTable) &&, 1 → 0; blobc626b59d→fecb8704). The driver pin turned red on ③ only (expected [] to have a length of 1 but got +0), and ① and ② stayed green. The driver pin importssrc, so no build was involved. Restore: blobc626b59dequals HEAD, andgit diff HEADis empty.ifblock, 1 → 0; blobc626b59d→e7acc7e7).driver-sqlwas rebuilt.ablation-dist-preflight --absent 'this driver deferred its DDL'found the marker absent from all 6 built files. The CLI pin turned red on both cases (expected [ …(6) ] to deeply equal []). Restore: blob equals HEAD andgit diff HEADis empty. After a rebuild, the preflight found the marker present in 2 built files and the tree clean against HEAD, and both pins were green again. This leg was run again atfd9f151a4, after the pin's spawn harness changed: same result.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatea9309b80derived 66 families. All 66 were run, with exit codes recorded before any pipe, and all exited 0.--ranreports:66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.check:dual-build-cjs-loadsandcheck:i18n-coveragefirst answeredPREREQUISITE NOT MET(exit 3) on the earlier merge head, because nine packages had nodist/. Those packages were built and both gates re-ran green.origin/mainand named two of its inputs as changed:lint.ymlgained timeout keys and comments, andengine-double-contract.pinned.jsongained entries for other files. Neither changes the family set. No commit since the merge touchesdriver-sqlorcli/src/commands/migrate.node scripts/check-driver-conformance.mjs, before and after:OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.eslint --no-inline-config --format jsonover the 3 touched TypeScript files reports 3 files, 0 errors, 0 warnings.eslint --print-configresolves each of them, and none carriesparserOptions.project.eslint.config.mjsenables no type-aware linting, so this diff cannot move the verdict on any file it does not touch. The repo-widepnpm lintis CI's.Changeset
.changeset/20821-plan-deferred-ddl-reads.md: apatchfor@objectstack/driver-sql.@objectstack/cliships onlydist,README.mdandCHANGELOG.md, andtsconfig.build.jsonexcludessrc/**/*.test.ts, so the pin publishes nothing and gets no entry.Acceptance notes
examples/app-crm/dist/objectstack.json(gitignored). With that compiled artifact present,os migrate plantakes the artifact boot path:onEnabledoes not run, and the six [finding]os migrate planon examples/app-crm runs the app'sonEnablehook, which readssys_position/sys_permission_setthe plan never declares: 6 DATABASE_ERROR + 6 WARN lines on every plan #21054 lines disappear too. The after-measurement above was taken with that artifact removed, so it matches the base's no-artifact path. This is noted for whoever takes [finding]os migrate planon examples/app-crm runs the app'sonEnablehook, which readssys_position/sys_permission_setthe plan never declares: 6 DATABASE_ERROR + 6 WARN lines on every plan #21054, since whether the app hook runs depends on that file.Generated by Claude Code