Repository navigation
refactor(driver-sql): move the NULL-safe unique pre-flight and the declared-index sync out of sql-driver.ts into sql-driver-schema-sync.ts (#25521 cut 9) - #26245
Conversation
…clared-index sync into sql-driver-schema-sync.ts (cut 9) The bodies of SqlDriver#applyNullSafeUniquePreflight, #syncDeclaredIndexes and #createNullSafeUniqueIndex move unedited into sql-driver-schema-sync.ts through the existing SchemaSyncHost, widened by the three members they read that it did not list (createHashShadowUniqueIndex, createNullSafeUniqueIndex, explainUnkeyableTextColumn). The module-level formatDuplicateGroups moves with its only two callers and stays module-local. Each class method keeps its docblock and signature less async. Claude-Session: https://claude.ai/code/session_01EcqJz48p6ineaiedw6SCaX Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
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 a0eadd95448ddba95649fb84c4f274e74c618e4b && git checkout a0eadd95448ddba95649fb84c4f274e74c618e4b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df8aac41f2d9d14db04afdfe48511216419dafc0 b2b00ab42839c48bc6a1c51007fba8bad8751696 && git checkout -B drift-repro df8aac41f2d9d14db04afdfe48511216419dafc0 && git merge --no-ff b2b00ab42839c48bc6a1c51007fba8bad8751696
node scripts/docs-audit/affected-docs.mjs --json df8aac41f2d9d14db04afdfe48511216419dafc0
|
席位核对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 9 of the
sql-driver.tssplit (#24586 ruling A, recorded in6270097840). The claim is6420465502. Cut 8 measured no next slice, so this cut opens with a fresh seam table (below) and takes the area it prices best that breaks no series limit. The bodies of three protectedSqlDrivermethods move unedited into the existing seam modulepackages/drivers/driver-sql/src/sql-driver-schema-sync.ts, through its existing hostSchemaSyncHost:applyNullSafeUniquePreflight, the plan-side duplicate pre-flight that clears or blocks a unique index op;syncDeclaredIndexes, which materializes a table's declared indexes;createNullSafeUniqueIndex, the raw DDL for an organization-scoped unique's expression key.The module-level helper
formatDuplicateGroupsmoves with them, byte-identical with its docblock. Its only two callers are the pre-flight and the sync, so it stays module-local and unexported, as its docblock requires.These bodies are the rest of the ADR-0120 tightening machinery. Cut 8 already moved its apply side and put the ADR-0120 D4 anchor paragraph on this module's entry. That paragraph describes the duplicate pre-flight, which now lives in the module the paragraph anchors. No anchor entry changes, there is no
docs/adr/**edit and nopackages/spec/**edit, and there is no new gate.The change is move-only, and the card stays open for later cuts. Branch
claude/project-thread-k34erv(the platform assigned it to this card thread, as the claim records). One commit,b2b00ab42, onorigin/maine6dfeb08f.Net line count (the seat's rule).
sql-driver.tsgoes from 21,334 lines to 20,990: -344.formatDuplicateGroupswith its docblock and the blank line before it, 20 lines; and thedescribeMissingIndexColumnsimport name, 1 line, because its last reader was the moved sync body.Serial with the file's in-flight PRs. I measured this twice: at start, and again before
pr_create(41 open PRs). Each time I read every open PR's file list in full. Two touchpackages/drivers/driver-sql/**, and neither touchessrc/: #26203 changes onlytsconfig.jsonand #26198 onlypackage.json. No PR touchessql-driver.ts,sql-driver-schema-sync.ts,turso-driver.tsor the driver-sql anchor entries.Seam table, fresh, on
e6dfeb08forigin/mainis ate6dfeb08f, after cut 8 (51cc7f9603).sql-driver.tsis 21,334 lines. 30-day lines aregit blame --line-porcelainlines whose committer time is on or after 2026-09-26T15:16:49Z, 30 days before the base commit. I deepened history to 2026-09-18 first, andscripts/pm/git-history.mjs countproves 68 commits on the file in that window (26 in the last 7 days). In all, 3,206 lines (15.0%) are from the last 30 days.SqlDriverspans lines 5532-21259 with 348 members: 252 methods (49 of them public instance methods), 13 getters and 32 statics, all 32 of them private or protected.supports, in the class head.!before it readsthis.// ===blocks and// ──sub-banners), cut by contiguous line range.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. 191 of the 241 instance methods do.
getBuildersite.this.FIELD.delete/insert/update(site.superread and no barethispass.this.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#createColumnfor the NOT NULL mechanism in this body, so moving it would need adocs/adr/**edit. Fivepackages/cligenerate-*.pin.test.tsfiles also sliceprotected createColumn(…switch (type)out of this file's text.applyFilterCondition(Filter helpers)sql-driver.ts#applyFilterCondition, and a governed.claude/**reference names it too.sql-driver-compile-refusal-seam.test.tsenumeratesINVALID_FILTERbuilders in this file only.syncDeclaredIndexes(Managed-schema drift & reconcile)formatOutput(SQLite serialisation)sql-driver.tsanchor entry's ADR-0053 paragraph describes this body, ADR-0104 citessql-driver.ts#formatOutput, and the body reads 11 fields.initObjects(Rotation)packages/spec/liveness/object.jsoncitessql-driver.ts#initObjects, which would need a spec edit.formatInput(SQLite serialisation)sql-driver.ts#formatInput.applyCrossFieldComparison(Filter helpers)applyNullSafeUniquePreflight(Managed-schema drift & reconcile)createNullSafeUniqueIndex(Managed-schema drift & reconcile)Why this area
sql-driver-schema-sync.tsalready holds the apply side of the same mechanism (cut 8'sapplyIndexDriftOp, which callssyncDeclaredIndexesandprobeNullSafeUniqueDuplicates).SchemaSyncHostalready listed 7 of the 10 members these bodies read.getBuilder;this.FIELD.delete/insert/update(site;super, a barethisor a cast-wrappedthis;TursoDriveroverridesprobeNullSafeUniqueDuplicatesand callssuper. The bodies reach it through a late-bound closure, so the override is still the one that runs. No subclass overrides any of the three moved methods.sql-driver.ts#…path, and none slices their text out ofsql-driver.ts.packages/cli/src/commands/generate.tsanddriver-turso/src/remote-transport.tsnameSqlDriver.syncDeclaredIndexesin prose. That method is still on the class, with the same docblock, so the prose still holds.formatDuplicateGroupsoutside its callers is one{@link formatDuplicateGroups}in the docblock ofreclaimBetterSqlite3insql-driver.ts. See Acceptance notes.probeNullSafeUniqueDuplicatesstays in the class. Its body reads the module-leveluniqueProbeNotNullClause, so moving it would drag a second helper, and Turso's override already reaches it through the host.The shape (#23050 shape B, as cuts 1-8 apply it)
async. Its body is now one host literal plusNAME.call(host, ...), marked[#25521 cut 9], and it returns the moved function's own promise.knex,loggerandisMysqlgo as GETTERS that read the driver at call time. The methods go as closures that call the method on the driver at call time. So a subclass override, an instance spy, or aknexorloggerreplaced after construction is still the one the body reaches (see the probe below).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.SchemaSyncHostgainscreateHashShadowUniqueIndex,createNullSafeUniqueIndexandexplainUnkeyableTextColumn, each typed as the class member's indexed access, in the interface's alphabetical order. The other members these bodies read were already there:knex,logger,isMysql,getExistingIndexNames,logDurabilityFailure,probeNullSafeUniqueDuplicatesandresolveTenantField.normalizeDeclaredIndex,describeMissingIndexColumns,GLOBAL_TENANTand the typeDeclaredIndexInputfromschema-drift.ts, andisUniqueViolationErrorfrom@objectstack/types, the modules that declare them. Noexportwas added anywhere.sql-driver.tsdrops onlydescribeMissingIndexColumnsfrom its import, because the moved sync body was its last reader. Everything else it imported is still read there.index.tsre-exports nothing from the module, so the published surface does not move.[#25521 cut 9]paragraph that cites no decision letter. Thesql-driver.tsimport comment names cut 9 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-0120 D3andADR-0120 D4citations from one anchored file to the other, and the new header paragraph cites no letter.e6dfeb08fand at headb2b00ab42, over 90 anchored files at both ends. Both runs exit 0.sql-driver-schema-sync.ts's entry. The ADR-0053 paragraph onsql-driver.tsdescribesformatOutputandpresentReadValue, and neither 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 restores the base text of the two source files in this worktree after the commit, under an EXIT/INT/TERM trap that restores HEAD. The restore is proved by the HEAD blob hashes (dbe720a2,86929118), an emptygit diff HEADand 0 status entries.dist/index.d.tsanddist/index.d.mtsis the same at base and at head:5028e040e1501b0bd72b91fafcf4e1cb372393c59baa5bab8b936b240dd2d3f5(cut 8's hash).cmpfinds them identical.protectedfromsyncDeclaredIndexesin the headindex.d.ts. The result hashes tof6a003a07258bfdcd858e4e68504f0960aa01bd85e6e9d4ab387fb26dfa81a2c, with a 4-line diff andcmpexit 1.requireand ESMimportof the built dist give 56 / 56 names at both ends, identical..call(hostsites inindex.mjsgo from 21 at base to 24 at head.Move-only proof
The proof compares TypeScript compiler AST leaf tokens (JSDoc nodes skipped), base
e6dfeb08fagainst headb2b00ab42. It is a scratch script and is not committed.thisapplyNullSafeUniquePreflightsyncDeclaredIndexescreateNullSafeUniqueIndexasyncis on both ends, and there are no type parameters. Each body line is the base line less two leading spaces.async.formatDuplicateGroups. Its 19 lines with the docblock are byte-identical, it is gone fromsql-driver.ts, and it is still unexported.sql-driver.tstop level. It goes from 207 to 206 statements. The only base statements that differ are the two imports, the moved helper and the class.sql-driver-schema-sync.ts. It goes from 7 to 12 statements. The 2 base statements that differ are the first import (which carries the module header) andSchemaSyncHost. The 5 new statements are the@objectstack/typesimport, the helper and the three functions.PROOF OK, exit 0.was NOT created:towas NOT creat3d:in a module copy. The proof then reports 1 token and 1 line differing, printsPROOF FAILED, and exits 1.Tests
Each suite ran under its own
os-verify-lockhold. I compared the vitest JSON per test name and status, basee6dfeb08fagainst headb2b00ab42:@objectstack/driver-sql@objectstack/driver-turso, against that end's built driver-sql dist@objectstack/driver-sqlite-wasmpnpm exec tsc --noEmitin driver-sql exits 0.pnpm exec eslint --no-inline-configon the two touched files exits 0, with 0 problems.diffis empty). They cover:syncDeclaredIndexesthrough aknexreplaced on the instance after construction;logDurabilityFailure, and an instance spy ongetExistingIndexNames;createNullSafeUniqueIndex, with the emittedCOALESCE(organization_id, '__global__')DDL;formatDuplicateGroupsrenders, five groups then "…and 2 more";createNullSafeUniqueIndexthrough a subclassisMysqlgetter, on its functional-key-part fallback to the bare composite;applyNullSafeUniquePreflighton a dirty create (blocked, withapplyLimitand message), a clean tightening (safe), and a probe failure warned through aloggerreplaced after construction;TypeErrorrejection, never a synchronous 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
b2b00ab42.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 55 commands from the change set against merge basee6dfeb08f(2 paths). 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.e6dfeb08fand at headb2b00ab42. All exit 0 at both ends.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-tenant-audit-censuscheck-platform-checklist --changed-fromis not owed: no exported symbol was renamed or deleted, becauseformatDuplicateGroupswas never exported. Pre-pushcheck:commit-card-trailersandcheck:nul-bytespass.origin/mainmoved while this ran. It moved 10 commits, todf8aac41f. None of them touchespackages/drivers/**,scripts/adr-anchors/**,check-adr-anchors.mjsorcheck-tenant-chokepoint.mjs. Two touchpackages/types/src, which driver-sql importsisUniqueViolationErrorfrom, so CI's merge-ref run covers that.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:knex.raw(site,createNullSafeUniqueIndex'sCREATE UNIQUE INDEXwith itsCOALESCE(... '__global__')key part (sql-driver.tsgoes from 57 to 56.raw(sites);syncDeclaredIndexes'tenantFieldparameter and itsresolveTenantFieldbinding.None of these says
IS NULLagainst a tenant column, and none is awherearm. The verdict is unchanged, and the gate is not empty (24 bindings, 3 files). This is the same disclosure cuts 5-7 made, and the card already carries theSCAN_FILESprecondition.A docblock link no longer resolves in its file. The docblock of
reclaimBetterSqlite3insql-driver.tssays "Module-local, like {@link formatDuplicateGroups}", and that helper now lives insql-driver-schema-sync.ts. The sentence is still true, because the helper is still module-local. I left the docblock unedited, since the cut is move-only.Proposed next slice:
applyCrossFieldComparison(Filter helpers, about -179, reads 6 methods and no field). It would need a new seam module. Before pricing it at dispatch, screen it againstsql-driver-compile-refusal-seam.test.ts(whether it buildsINVALID_FILTER) and againstcheck:tenant-chokepointrule 2.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