Repository navigation
fix(driver-sql,cli): os migrate plan previews a _id-keyed table as apply treats it; one refused table no longer stops apply - #30750
Conversation
…ply treats it; one refused table no longer stops apply Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cy3CHSvvtQP754FaLB
… refused Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cy3CHSvvtQP754FaLB
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cy3CHSvvtQP754FaLB
📓 Docs Drift CheckThis PR changes 2 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 43 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 c9ee75951004aa0a66a96327e0c2dd9cb8d04dd1 && git checkout c9ee75951004aa0a66a96327e0c2dd9cb8d04dd1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11 77b3053a09f62c1c4d5774960d7dd06e8f8149f6 && git checkout -B drift-repro 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11 && git merge --no-ff 77b3053a09f62c1c4d5774960d7dd06e8f8149f6
node scripts/docs-audit/affected-docs.mjs --json 40a9265ff3c8c3e96b1e2c13ea5edd1fb32e6d11
|
… for a flushDeferredSchemaDdl caller Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cy3CHSvvtQP754FaLB
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 Review owed by the Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4 维护者速读
席位意见:ACCEPT(#30636)。 席位核对Head-sha:
|
Requested by Elon · project thread
Fixes #30636
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
os migrate plan遇到用_id当主键、没有id列的已有表,不再预报「补列」,而是照 apply 的真实做法预报:有数据就列为「拒绝」(带表名、_id、行数和拒绝原文),空表就列为「重建」。os migrate apply遇到这种被拒的表,不再整批停下:其余表照常建表补列,被拒的表单独点名,原样不动,最后 PARTIAL、退出码 1。_id表上抛错退出,另外 80 张该建的表一张没建、该补的列一列没补,已完成清单也随异常丢了。id)的预览与执行一字未变。另加一条保护:被拒表上的 drift(其_id列看起来是「孤儿列」)不再下发,即使带--allow-destructive也不会删它。回滚即撤本 PR。sql-driver-deferred-ddl.ts(按预览把被拒表排除出冲刷);sql-driver-schema-sync.ts的previewDeferredSchemaWork新分支;apply.ts冲刷之后那段(拆分被拒、扣下被拒表的 drift、PARTIAL)。pnpm --filter @objectstack/driver-sql exec vitest run src/sql-driver-schema.test.ts,预期 sqlite 格 7 个schema sync never drops a table holding rows用例全绿(其中 4 个是本 PR 新增)。What changes
An existing table keyed by
_idwith noidcolumn is the one shape the schema sync does not adopt by adding columns:initObjectsrebuilds it while it is empty and, since PR #30632, refuses it (ExistingTableAdoptionRefusal) while it holds rows.previewDeferredSchemaWork,sql-driver-schema-sync.ts): that shape is now planned as the sync treats it, with the same two probes and the same row count. Holding rows: arefuse_tableentry,columns: ['_id'],rows: N, andrefusal, the text rendered byrenderExistingTableAdoptionRefusal, the renderer the sync throws through. Empty: arebuild_tableentry (rows: 0, the declared columns). Neveradd_columns. A rotation-declared object never reaches that branch of the sync, so it does not reach this one.schema-drift.ts):PendingSchemaWorkKindgainsrebuild_tableandrefuse_table;PendingSchemaWorkgains the optionalrefusal;isRefusedSchemaWork(kind)is exported besideisInPlaceSchemaWork, which now answerstrueforrebuild_table(it is not additive) andfalseforrefuse_table(it is no work at all).flushDeferredSchemaDdl,sql-driver-deferred-ddl.ts): the flush already captures the preview before running the DDL; a table the preview refused is now left out of the oneinitObjects(pending)re-entry, so it stops nothing. Every other deferred object gets its DDL and the refusal comes back as its entry in the returned list instead of a throw that took that list with it. The sync's own refusal still guards the rows if the table fills between preview and DDL.apply.ts,schema-migrate.ts):splitRefusedSchemaWorksplits what the flush returned;createdlists performed work only, each refused table is printed with its refusal text, and the run endsPARTIALwith exit 1 (--json:refusedbesidecreated,message: "partial"). Drift entries on a refused table are withheld from the driver and reported inskipped: the table's_idcolumn reads as an orphaned column, and with--allow-destructivethe driver would otherwise drop it from the table the sync just left alone (onmainthe throw stopped the run before that call, so this path is new with "carry on").renderPendingSchemaWorklists a refused table under its own headingRefused (left exactly as it is when you apply)with the refusal text, and a rebuild under the in-place heading;summarizePendingSchemaWorkaddsN empty table(s) to rebuild/N table(s) REFUSEDwhen there are some and no longer counts them as temporal columns.addRefusedTableLimitsgives each drift entry on a refused table that refusal as its apply limit, so neitherplannor the listingapplyprints before its prompt offers--allow-destructivefor it.Changed lines (
git diff origin/main --stat): 12 files, +553 / −18 (driver-sql src +65/−4, CLI src +131/−12, tests +344/−2, changeset +13).Tests
packages/drivers/driver-sql/src/sql-driver-schema.test.ts, theschema sync never drops a table holding rowsmatrix (every dialect cell): the plan pin on both arms (rows: exactly onerefuse_tableentry whoserefusalequals the renderer's output AND the messagesyncSchemathrows for the same table; empty:rebuild_table, then the flush performs it), the CONTROL (id-keyed table:add_columns ['qty']), and the scope addition (a refused table between two good ones in deferred order: both get their DDL, the returned list carries bothcreate_tableentries and therefuse_tableentry, the refused table is byte-for-byte as before, the deferral is disarmed).packages/cli/src/utils/schema-migrate.pending-render.test.ts: the Refused heading and text, the rebuild line, the summary, the split.packages/cli/test/migrate-plan-refused-table.e2e.test.ts(nightly e2e tier): the CLI measurement below as a pin,plan --jsonthenapply --yes --allow-destructive --jsonon a seeded SQLite file.Measured through the CLI (fixture: `legacy_full` _id + 3 rows, `legacy_empty` _id empty, `plain_ok` id-keyed missing `note`, `zz_fresh` absent)
origin/main(driver-sql dist and CLI src from 89dbc74):os migrate plan --json:legacy_fullandlegacy_emptybothkind: "add_columns"(8 columns each), exit 0.os migrate apply --yes: listslegacy_full [add_columns: …], then✗ Schema sync REFUSED for the existing table 'legacy_full' …, exit 1. Afterwards:legacy_full3 rows untouched, butlegacy_emptynot rebuilt,plain_okwithoutnote,zz_freshabsent: none of the 80 creates / 25 columns listed was performed.This branch:
os migrate plan:+ plain_ok [add_columns: …, note],~ legacy_empty [rebuild_table: empty table dropped and recreated, 9 column(s)],✗ legacy_full [refuse_table: _id — 3 row(s)]+ the refusal text; summary80 table(s) to create, 9 column(s) to add, 1 empty table(s) to rebuild, 1 table(s) REFUSED;legacy_full._id [drop_column]says"os migrate apply" cannot apply it on this database: the schema sync refuses the table 'legacy_full' ….os migrate apply --yes:✓ Created/extended 82 table(s),✗ REFUSED legacy_full [refuse_table: _id — 3 row(s)]+ text,✗ PARTIAL apply: schema sync REFUSED 1 existing table(s) …, exit 1. Afterwardslegacy_full["_id","title"]3 rows;legacy_emptyrebuilt withid;plain_okhasnote;zz_freshcreated.os migrate apply --yes --json:created82 entries,refused[legacy_full, 3 rows, refusal text],notApplied: [],message: "partial", exit 1.os migrate apply --yes --allow-destructive: same, andlegacy_fullstill["_id","title"]with 3 rows.Ablation (commit aa54f87 on the fixed tree, driver tests read src/)
initObjects(refused.size === 0 ? pending : pending.filter(…))→initObjects(pending)viascripts/ablation-replace.mjs(anchor 1 → 0, blob aa5c540e → 5d90f111):Tests 1 failed | 26 passed(the refused-between-two test). Restored,git diff HEADempty.false &&(blob 2de56b06 → c89e4d34):Tests 3 failed | 24 passed(both plan arms and the flush test). Restored: blob == HEAD,git diff HEADempty.Acceptance notes
PendingSchemaWorkKindis exported by@objectstack/driver-sql(and re-exported asPendingSchemaWorkfrom the CLI'sschema-migrate.ts); it gains two members,PendingSchemaWorkgains the optionalrefusal, andisRefusedSchemaWorkis a new export. Theos migrate plan --jsonpendingarray and theos migrate apply --jsondocument (newrefusedkey) carry them. The seat re-derived Clause-② for this asyes (widening)(claim amendment 6673377065); the changeset carries the same two lines, both packages staypatch(Metadata-protocol: none), and its body names the widening for a driver-sql consumer: a caller offlushDeferredSchemaDdl()must treat a returnedrefuse_tableentry as refused, not performed._idtable used to stop with the refusal and do nothing else; it now performs every other table's DDL and the drift, and still ends exit 1. Nothing in the tree depended on the stop (the only caller offlushDeferredSchemaDdlisSchemaStack.flushSchemaDdl).legacy_empty._id [drop_column]for an empty_idtable the flush rebuilds; once rebuilt the column is gone, and--allow-destructivereports the entry as applied. Pre-existing onmainfor an empty-only database; not this card's surface.Verification
Head
77b3053aadds one changeset-only commit over87ec2b98(the Clause-② line and one sentence); the changeset gates were re-run on it (see the end of this section). Everything else below is on head87ec2b98(this branch merged withorigin/main4564d6f), afterpnpm install --frozen-lockfileand a whole-treeturbo run build(lock VERDICT command-exit 0).pnpm --filter @objectstack/driver-sql typecheckexit 0;pnpm --filter @objectstack/cli typecheckexit 0 (on aa54f87, before the merge).pnpm --filter @objectstack/driver-sql test:Test Files 238 passed | 12 skipped (250),Tests 3866 passed | 218 skipped, exit 0 (aa54f87).pnpm --filter @objectstack/cli exec vitest run --project unit:Test Files 389 passed (389),Tests 5435 passed (5435), exit 0 (aa54f87). The integration tier is declared to CI; the new e2e file ran by hand:OS_TEST_TIERS=nightly … --project integration test/migrate-plan-refused-table.e2e.test.ts→Tests 5 passed (5).sql-driver-schema.test.tsagainst a local PostgreSQL 16 (OS_TEST_POSTGRES_URL):Tests 34 passed | 1 skipped(the live postgres cell of the matrix, all 7 cases green). MySQL not provisioned here; theTemporal Conformance (live PG + MySQL)job runs the cell.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 66 families, each run and exit 0;--ran→66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED.The 66 gate families (each `:: exit 0`)
On head
77b3053a(the changeset-only commit), with this body as thepull_requestevent payload: each:: exit 0.check-changeset-no-majorLEVEL AXIS line: "this PR declares clause-② yes (widening) and metadata-protocol none, and it introduces no changeset entry graded minor or above" (pass).🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cy3CHSvvtQP754FaLB