Repository navigation
refactor(driver-sql): move the schema-drift pipeline (detect, reconcile, apply) out of sql-driver.ts into sql-driver-schema-sync.ts (#25521 cut 10) - #26287
Conversation
…driver-schema-sync.ts Cut 10 of the sql-driver.ts split, move-only. The bodies of detectTableDrift, detectTableIndexDrift, detectManagedDrift, planMediaColumnMove, reconcileAndWarnDrift and applyMigrationEntries move unedited into the existing seam module, through SchemaSyncHost. The class keeps every method, its docblock and its signature (less `async`); each body is now one host literal plus a `.call`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTp8gt1Ujr3CiXttYmfFS9
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 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 00af20a99f536c3811ebb802c76450177db11a17 && git checkout 00af20a99f536c3811ebb802c76450177db11a17
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2409c9c8d75c45e118a0706e4b52f4265810aaf7 5fba834b47288c291d287b3623899dc1fa059bad && git checkout -B drift-repro 2409c9c8d75c45e118a0706e4b52f4265810aaf7 && git merge --no-ff 5fba834b47288c291d287b3623899dc1fa059bad
node scripts/docs-audit/affected-docs.mjs --json 2409c9c8d75c45e118a0706e4b52f4265810aaf7
|
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Part of #25521
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6270097840
Ruling-ref: 6348891160
Cut 10 of the
sql-driver.tssplit (#24586 ruling A, recorded in6270097840). The claim is6423425118. The cut opens with a fresh seam table (below) and takes the area it prices best that breaks no series limit. The bodies of sixSqlDrivermethods move unedited into the existing seam modulepackages/drivers/driver-sql/src/sql-driver-schema-sync.ts, through its existing hostSchemaSyncHost. Together they are the managed-schema drift pipeline, the code that sits on top of the reconcilers and the index machinery cuts 6, 8 and 9 already moved there:detectTableDrift, which diffs one table's columns against its physical schema;detectTableIndexDrift, which diffs that table's indexes and runs the ADR-0120 pre-flight on the result;detectManagedDrift(public), which runs the table diff over every managed table;planMediaColumnMove(public), the read-only plan of the ADR-0104 media column move;reconcileAndWarnDrift, the boot-time reconcile-or-warn;applyMigrationEntries(public), which applies a set of drift entries.The change is move-only, and the card stays open for later cuts. There is no
docs/adr/**edit, nopackages/spec/**edit, no anchor entry change and no new gate. Branchclaude/project-thread-5jymjn(the platform assigned it to this card thread, as the claim records). One commit,5fba834b4, onorigin/main48fba2b178.Net line count (the seat's rule).
sql-driver.tsgoes from 20,990 lines to 20,739: -251.Serial with the file's in-flight PRs. I read the full file list of every open PR at start (37 PRs) and again before
pr_create(35 PRs, 17 of them new). No open PR touchessql-driver-schema-sync.ts,turso-driver.ts, the drift section ofsql-driver.tsor the driver-sql anchor entries.{ $now: true, addDays }shifted by days held on the record #26261 ($nowcomparand) is the one that editssql-driver.ts. Its hunks are in the module preamble and in Filter helpers (applyFilterCondition's operator loop and a newapplyNowAnchorComparisonbeside the cross-field comparison), not in the drift section. Whichever of the two lands second mergesmain.driver-sql/src/builtin-column-collision.tsonly, and test(vitest): one shared root preset for the 18 identical package configs (#26092 part 2) #26267 thevitest.config.tsof driver-turso and driver-sqlite-wasm only. The release PR chore: version packages #25640 touches driverpackage.jsonandCHANGELOG.mdfiles only.Seam table, fresh, on
48fba2b178origin/mainis at48fba2b178, after cut 9 (ab112ea8d5).sql-driver.tsis 20,990 lines. 30-day lines aregit blamelines from commits on or after 2026-09-26T17:15:20Z, 30 days before the base commit. I deepened history to 2026-09-18 first, andscripts/pm/git-history.mjs countproves 69 commits on the file in that window (27 in the last 7 days). In all, 3,202 lines (15.3%) are from the last 30 days.SqlDriverspans lines 5513-20915 with 348 members: 241 instance methods (49 public), 13 accessors and 32 statics, all 32 of them private or protected. 24 of the instance methods are already stubs from cuts 1-9.supportsgetter, in the class head.!andsatisfiesbefore it readsthis. Sections are the class's own banner comments (// ===blocks and// ──sub-banners), as cut 9 cut them; cut 9's table also folds the docblock heading "The sequence semantics a second face shares" into CRUD, and so does this one. "Methods" counts instance method declarations, without accessors.name…assertDialectEmitsconnect…disconnectfind…deletebulkCreate…countrawStatementFault…noteRuntimeIndexDdlbeginTransaction…rollbackTransactionaggregate…unresolvableAggregateColumnRefusaldistinct…unresolvableDistinctColumnRefusalfindWithWindowFunctions…findWithWindowFunctionsexplain…analyzeQuerysyncSchema…reclaimSpacerotationStateByTable…migrateMysqlTimeColumnssetDeferredDdl…flushDeferredSchemaDdldialectName…createNullSafeUniqueIndexlistPhysicalColumns…introspectSchemagetKnex…auditMissingTenanttableNameForBuilder…applyFalseConstantmapSortField…mapAggregateFuncbuildWindowFunction…buildWindowFunctionnowColumnDefault…applyDeclaredColumnDefaultsqliteFilename…isJsonFieldformatInput…formatOutputintrospectColumnOrder…introspectUniqueConstraintsPricing the eligible methods
A method is eligible to move when it meets every condition below. 168 of the 241 instance methods do (24 are already stubs, 19 build through
getBuilder, 17 read a private static, 12 write tothis, 3 have athis.FIELD.delete/insert/update(site, and one isinsertOnlyUpsertColumns).getBuildersite, nothis.FIELD.delete/insert/update(site, no private or protected static read, nosuperread, no barethispass and no write tothis.insertOnlyUpsertColumns.The net-line rule. Each candidate is priced as its stub minus its body. A stub costs 4 lines, plus 2 for each field or getter read and 1 for each method read.
The highest-priced candidates, and why each one stays or moves:
createColumn(Column creation helper)sql-driver.ts#createColumn, and fivepackages/cligenerate-*.pin.test.tsfiles read this file's text.applyFilterCondition(Filter helpers)sql-driver.ts#applyFilterCondition.formatOutput(SQLite serialisation)sql-driver.tsanchor entry's ADR-0053 paragraph describes it.initObjects(Rotation)packages/spec/liveness/object.jsoncites it.formatInput(SQLite serialisation)applyCrossFieldComparison(Filter helpers)crossFieldComparisonClass,crossFieldComparisonError,fieldReferenceOffsetOf,uncompilableFieldReferenceError) that other class members also call, so it would need exports or a helper move.assertConflictTargetHonoured(CRUD)backendStatementFault(Bulk & Batch)compileFilters(Filter helpers)getNextSequenceValue(CRUD)applyMigrationEntries(drift)planMediaColumnMove(drift)reconcileAndWarnDrift(drift)detectTableDrift(drift)detectTableIndexDrift(drift)detectManagedDrift(drift)Priced by area (every eligible body with a negative price and no
sql-driver.ts#…pointer, summed per section): Filter helpers -448 (20 bodies), Rotation -388 (15), CRUD -351 (18), Managed-schema drift -275 (10), and every other section -93 or less.Why this area
{ $now: true, addDays }shifted by days held on the record #26261 is editing, so the serial rule takes it out of this cut.SQLITE_NON_TEMPORAL_TEXT_REFS,isMultiValuedColumn,isDeferredDdlLockWaitRefusal,isNowDefaultValue,AUDIT_TIMESTAMP_COLUMNSand more). Moving them needs exports added tosql-driver.tsor a helper moved away from its other callers. It is also three areas under one banner: rotation, the canonical datetime and JSON backfills, and the MySQL column migrations.SEQUENCES_TABLEand five conflict-target helpers are module-local and shared.sql-driver.ts. Every declaration they read is imported from the module that declares it (schema-drift.ts,media-column-move.ts,@objectstack/spec/system) or is the exported typeIntrospectedColumn. Noexportwas added anywhere, and no helper moved.sql-driver-schema-sync.tsalready holds what this pipeline calls:applyDriftOpInPlaceandrebuildSqliteTablePatched(cut 6),applyIndexDriftOp(cut 8),applyNullSafeUniquePreflight(cut 9).SchemaSyncHostalready listed 8 of the 26 members these bodies read.getBuilder;this.FIELD.delete/insert/update(site (this.driftWarned.add(is a Set write, read through the host getter, and the tenant-audit census reads the same 243 sites at both ends);super, a barethisor a cast-wrappedthis, and none writes tothis;detectManagedDrift,planMediaColumnMove,applyMigrationEntries) have annotated return types; the other three are protected.TursoDriveroverridesdetectManagedDrift,applyMigrationEntriesandplanMediaColumnMove, and each callssuperon its local path. The stubs stay on the class with the same signatures, sosuperstill reaches them, and the moved bodies reach every method through a late-bound closure on the instance, so Turso'sintrospectColumns,introspectIndexesandapplyMigrationEntriesoverrides are still the ones that run. One driver-sql test (sql-driver-11722-sqlite-rebuild-applied-honesty.test.ts) overridesdetectTableDriftin a subclass; it passes at both ends, and the probe below covers the same shape.sql-driver.ts#…path, and no test or gate slices their text out ofsql-driver.ts(the files that readsql-driver.tsby path are the five cligenerate-*pins,logger-receiver-detach.test.ts,sql-driver-compile-refusal-seam.test.tsand the gates in the table below; none of them reads these bodies). See Acceptance notes for prose that names them.The shape (#23050 shape B, as cuts 1-9 apply it)
async. Its body is now one host literal plusNAME.call(host, ...), marked[#25521 cut 10], and it returns the moved function's own promise.autoMigrate,dialectName,driftWarned,fileColumnsMoved,isSqlite,knex,logger,managedObjectFields,managedObjectIndexes,mediaFields,runtimeCreatedIndexesandschemaMode. Methods go as closures that call the method on the driver at call time. So a subclass override, an instance spy, or a field replaced after construction is still the one the body reaches (see the probe below).this.logger.info(...)andthis.logger.warn(...)still call through the logger itself, as the [finding] driver-sql has 10 inline(this.logger.error ?? this.logger.warn)(…)detach-then-call sites — the shape #12773 measured crashing, in a file that already contains the correct spelling #12792 comment inreconcileAndWarnDriftrequires.thisas the Pick ofSchemaSyncHostover exactly the members it reads. Each stub annotates its literal as the ThisParameterType of the moved function. There is no cast, and no member becomes public.SchemaSyncHostgains 18 members, each typed as the class member's indexed access, in the interface's alphabetical order:applyDriftOpInPlace,applyMigrationEntries,applyNullSafeUniquePreflight,assertSchemaMutable,autoMigrate,detectTableDrift,detectTableIndexDrift,driftWarned,fileColumnsMoved,introspectIndexes,isProductionEnv,isSqlite,isTenantKeyCertified,mediaFields,rebuildSqliteTablePatched,runtimeCreatedIndexes,schemaModeandvarcharColumnChars.schema-drift.tscarriesNullSafeTenantKeyPart,ceremonyOwnedDropFinding,diffManagedIndexes,diffManagedTable,diffUnbuildableIndexes,driftKey,expectedIndexes,indexedKeyColumns,isCeremonyOwnedColumnDrop,legacyUniqueReplacementsand the typePhysicalColumn; frommedia-column-move.tsisJsonColumnType,mediaColumnMoveDialect,mediaColumnMovePlanand the threeMediaColumnMove…types;StorageNameMappingfrom@objectstack/spec/system; and the typeIntrospectedColumnfromsql-driver.ts(a type-only import, as the module already does forSqlDriver).sql-driver.tsdrops the 15 of those names it no longer reads (all butdiffManagedIndexes,indexedKeyColumns,StorageNameMapping,IntrospectedColumnandMediaColumnMoveScan, which other code there still reads).isIndexDriftOpis one of the 15, becauseapplyMigrationEntrieswas its last reader there.index.tsre-exports nothing from the module, so the published surface does not move.[#25521 cut 10]paragraph that cites no decision letter. Thesql-driver.tsimport comment names cut 10 in 2 lines.ADR anchors
check:adr-anchorscounts decision-letter citations on the anchored surface: each anchor'sinvariant, plus the full text of each anchored file.sql-driver.tshas its ADR-0053 entry, andsql-driver-schema-sync.tshas the ADR-0120 entry cut 8 created.ADR-0131 D1andADR-0131 D10citations from one anchored file to the other, and the new header paragraph cites no letter.48fba2b178and at head5fba834b4, over 90 anchored files at both ends. Both runs exit 0.formatOutputandpresentReadValue, and the ADR-0120 D4 paragraph describes the tightening ceremony that cuts 8 and 9 moved.The published
.d.tsdoes not changeThis is measured under the series bar (ruling
6348891160, item 1).pnpm turbo run build --filter=@objectstack/driver-sql --force, against the same dependency tree. The base leg writes the base text of the two source files into this worktree after the commit, under an EXIT/INT/TERM trap that restores HEAD. The restore is proved by the HEAD blob hashes (d4d08264,90b63682), an emptygit diff HEADand 0 status entries.dist/index.d.tsanddist/index.d.mtsis the same at base and at head:5028e040e1501b0bd72b91fafcf4e1cb372393c59baa5bab8b936b240dd2d3f5(cut 9's hash).cmpfinds them identical.protectedfromdetectTableDriftin the headindex.d.ts. The result hashes toa9d58e424308f85ac7429096c80df2554b41abe53d67410e030799a463072be5, with a 1-line diff andcmpexit 1.requireand ESMimportof the built dist give 56 / 56 names at both ends, identical..call(hostsites inindex.mjsgo from 24 at base to 30 at head.Move-only proof
The proof compares TypeScript compiler AST leaf tokens (JSDoc nodes skipped), base
48fba2b178against head5fba834b4. It is a scratch script and is not committed.thisdetectTableDriftdetectTableIndexDriftdetectManagedDriftplanMediaColumnMovereconcileAndWarnDriftapplyMigrationEntriesasyncis on both ends, and there are no type parameters. Each body line is the base line less two leading spaces.async.sql-driver.tstop level. It has 206 statements at both ends. The only base statements that differ are theschema-drift.js,media-column-move.jsandsql-driver-schema-sync.jsimports, and the class.sql-driver-schema-sync.ts. It goes from 12 to 20 statements. The base statements that differ are the first import (which carries the module header), theschema-drift.jsandsql-driver.jsimports, andSchemaSyncHost. The new statements are the@objectstack/spec/systemandmedia-column-move.jsimports and the six functions.PROOF OK, exit 0.could not introspecttocould not intr0spectin a module copy. The proof then reportsreconcileAndWarnDriftwith its body tokens and lines differing, printsPROOF FAILED, and exits 1.Tests
Each end ran under one
os-verify-lockhold, after building that end's driver-sql dist. I compared the vitest JSON per test name and status, base48fba2b178against head5fba834b4. Every part's exit code was captured on its own (build, three suites: all 0 at both ends).@objectstack/driver-sql@objectstack/driver-turso, against that end's built driver-sql dist@objectstack/driver-sqlite-wasmpnpm exec tsc --noEmit -p .in driver-sql exits 0.pnpm exec eslint --no-inline-configon the two touched files exits 0, with 0 problems.diffis empty). They cover:detectManagedDriftreaching a SUBCLASS override ofdetectTableDrift(from the walk and with explicit objects), andmanagedObjectFieldsreplaced on the instance after construction;detectTableDriftreaching instance spies onintrospectColumnsanddetectTableIndexDrift, with the physical column set and the NOT NULL witness it passes;detectTableIndexDriftreaching instance spies onintrospectIndexesandapplyNullSafeUniquePreflight(twice, around aruntimeCreatedIndexesentry added on the instance; the index result is empty both times at both ends, so that reading proves the calls, not the ledger read);reconcileAndWarnDriftwithautoMigrate: 'safe'on a managed schema, through aloggerand adriftWarnedreplaced after construction: the warn-once lines, the production arm through an instance spy onisProductionEnv, and the introspection-failure warning through an instance override ofdetectTableDrift;applyMigrationEntrieson SQLite: a destructive entry skipped, the column entries through an instance spy onrebuildSqliteTablePatched, an index entry through an instance override ofapplyIndexDriftOp, and the empty set;planMediaColumnMove: a plannedunquote,mediaFieldsreplaced after construction (an absent column refused), and a subclassdialectNamegetter answering the unsupported-dialect refusal;detectTableDriftwithfileColumnsMovedflipped on the instance (an empty result both ways at both ends on SQLite, so it proves only that the read does not throw);test:repowas not run locally: the diff touches no repo-project test, and CI's Test Core runs it.Gates
All local runs are on head
5fba834b4.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 55 commands from the change set against merge base48fba2b17(2 paths, 802 changed lines). I captured each exit code before any pipe.check-plugin-teardown-shape.mjs --self-testexited 3 on the shallow clone, then 0 aftergit fetch origin 621a4876(its positive control).check:lean-entry-closureexited 3 with no objectql dist, then 0 afterpnpm turbo run build --filter=@objectstack/objectqlunder the lock.check:dual-build-cjs-loadsexited 3, PREREQUISITE NOT MET: it needs every package's dist. It is NOT MEASURED locally, and CI runs it.--ranreports "55 derived famil(ies) accounted for — 54 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", exit 0.lintscript, so I ran the root lint's command scoped to the package:pnpm exec eslint --no-inline-config packages/drivers/driver-sql, exit 0.48fba2b178and at head5fba834b4. All exit 0 at both ends, and each prints the same reading at both ends except the one noted.check:adr-anchorscheck:adr-symbol-anchorscheck:tenant-chokepointgetBuilder()bindings across 3 files; rule 2 cleancheck:logger-receiver-detachpackages/spec/src/migrations/registry.ts.check:driver-conformancecheck:keyed-text-boundscheck:object-def-param-keyssql-driver.tsby path.check:system-context-censuscheck-tenant-audit-censuscheck-platform-checklist --changed-fromis not owed: no exported symbol was renamed or deleted. Pre-pushcheck:commit-card-trailersandcheck:nul-bytespass.origin/mainmoved while this ran. It moved 19 commits, to2409c9c8d7. Inpackages/drivers/**,packages/types/src,packages/spec/src/system,scripts/adr-anchors/**,check-adr-anchors.mjsandcheck-tenant-chokepoint.mjs, onlypackages/types/src/node.test.tschanged (one line).git merge-treeagainst it is clean. I did not merge, because CI tests the merge ref.Acceptance notes
check:tenant-chokepointrule 2 visits a little less. Rule 2 (the D8 null-arm rule) scans class members of the three driver files, so it no longer visits what moved: theresolveTenantFieldbindings indetectTableDriftanddetectTableIndexDrift. Neither saysIS NULLagainst a tenant column, and neither is awherearm. The verdict is unchanged, and the gate is not empty (24 bindings, 3 files). This is the same disclosure cuts 5-9 made, and the card already carries theSCAN_FILESprecondition.logger-receiver-detach.test.ts③ readssql-driver.tsonly. Its structural pin no longer seesreconcileAndWarnDrift's receiver-kept log calls, which now live in the module. The repo-wide gatecheck:logger-receiver-detachwalks every non-test TS file, the module included, and reads 0 detaches at head. The runtime case in the same test file (reconcileAndWarnDriftthrough a class-based logger) still runs and passes.docs/qa/platform-checklist/areas/cli.jsoncitessql-driver.ts#SqlDriverConfigwith a parenthetical describingreconcileAndWarnDrift's gate andapplyMigrationEntries'allowDestructive: falsecall, and oneverifyline says "(sql-driver.ts)". The anchor symbolSqlDriverConfigdid not move, and both methods are still on the class, so the symbol anchor resolves (check-platform-checklistjudges the symbol, not the parenthetical); the behaviour described now runs from the module.schema-drift.ts,turso-driver.tsand several tests nameSqlDriver.detectTableDriftand the others in prose; each is still aSqlDrivermethod with the same docblock.sql-driver.tsnow point across files. The docblock ofdriftWarnedsays "keyed by {@link driftKey}", and themedia-column-move.jsimport comment says the column step's statements are "Built here".driftKeyand the statement builders are now imported by the module rather than bysql-driver.ts. I left both comments unedited, since the cut is move-only.getNextSequenceValue-66,fillAutoNumberFields-52,issueAutonumbers-40,maxAutonumberCounter-19,resyncSequenceToDataMax-13 and their small helpers, about -200). It would need a new seam module. Before pricing it at dispatch, screenSEQUENCES_TABLE(module-local, read by other members; the class already has asequencesTableNamegetter), andTursoDriver's overrides ofgetNextSequenceValue,autoNumberValueExistsandresyncSequenceToDataMax, which the "stated once" banner says a second face must call, never copy.skip-changeset: the change is move-only, so nothing a published package ships changes. The.d.tsis byte-identical, and the emitted JS runs the same bodies through the host.Generated by Claude Code