Repository navigation
refactor(driver-sql): move the cross-field and now-anchored comparison compile out of sql-driver.ts into sql-driver-cross-field-comparison.ts (#25521 cut 13) - #26377
Conversation
…n compile out of sql-driver.ts into a seam module The bodies of applyCrossFieldComparison, applyNowAnchorComparison, nowAnchorBaseExpr and crossFieldOffsetExpr move unedited into the new sql-driver-cross-field-comparison.ts, reached through CrossFieldComparisonHost; the class keeps every method as a stub. Five module-level declarations only those bodies read move with them, with the spec import crossFieldComparisonClass delegates to; eight shared ones gain export in place. The compile-refusal seam test's enumeration parses both files, so the two moved refusal builders are still counted. Claude-Session: https://claude.ai/code/session_01DC6m1WLgsQ2NJA2W8mZBQi Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 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 ded50458b0ec319e3833e25d665d7c8b3f8b9809 && git checkout ded50458b0ec319e3833e25d665d7c8b3f8b9809
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 61c079862ac726a1e16fcd73a04122567f475c4a e1c1a2b07c5b26b81a258690010f1d656628d30c && git checkout -B drift-repro 61c079862ac726a1e16fcd73a04122567f475c4a && git merge --no-ff e1c1a2b07c5b26b81a258690010f1d656628d30c
node scripts/docs-audit/affected-docs.mjs --json 61c079862ac726a1e16fcd73a04122567f475c4a
|
席位核对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
维护者速读
sql-driver.ts里「跨字段比较」与「相对当前时间比较」({ $field }、{ $now, addDays }过滤条件)的 SQL 编译代码原样搬进新文件sql-driver-cross-field-comparison.ts;类上的方法全部保留,只把方法体转交过去。src/sql-driver.ts(22,096 lines, +2,533 in 30 days) along itsSqlDriverseams, one move-only slice per PR #25521 拆分超大文件的第 13 刀;这块在剩余可搬区域里收益最大,sql-driver.ts减少 470 行。sql-driver-cross-field-comparison.ts的头注释与 host 接口;sql-driver.ts中四个方法的转交桩;sql-driver-compile-refusal-seam.test.ts的枚举改为同时读两个文件。pnpm --filter @objectstack/driver-sql exec vitest run src/sql-driver-compile-refusal-seam.test.ts,预期 115 个测试全部通过。What changes
Cut 13 of the
sql-driver.tssplit (#24586 ruling A, recorded in6270097840). The claim is6429655341. The cut opens with a fresh seam table (below), taken onorigin/main77a52cfe8(after cut 12's merge8b39b77674), and takes the area that pays best without breaking a series limit: the cross-field and now-anchored comparison compile of the Filter helpers section. It moves unedited into a new seam module,packages/drivers/driver-sql/src/sql-driver-cross-field-comparison.ts, through a new hostCrossFieldComparisonHost:applyCrossFieldComparison, the [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 column-to-column arm and its An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104addDaysoffset;applyNowAnchorComparison, the Analytics datasets: no filter compares a datetime column against now minus an SLA held on a related record #25808{ $now, addDays }comparand;nowAnchorBaseExprandcrossFieldOffsetExpr, the two operand builders both arms read.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, no gate edit and no new gate. Branchclaude/project-thread-5r39ss(the platform assigned it to this card thread, as the claim records). The move is one commit,b1f89c17c, on77a52cfe8; thenorigin/mainwas merged once before opening (e1c1a2b07c), which brought nopackages/drivers/**file.Net line count (the seat's rule).
sql-driver.tsgoes from 20,492 lines to 20,022: -470.CrossFieldOffset(22),fieldReferenceOffsetOf(16),uncompilableNowAnchorError(22),crossFieldComparisonClass(74) anduncompilableFieldReferenceError(38). And the 6-line spec importcrossFieldColumnVerdict, type CrossFieldComparisonClasswith its comment, whose last reader (crossFieldComparisonClass) moved; it moved whole into the module.Serial with the file's in-flight PRs. I read the full file list of every open PR twice: at start, before cut 12 merged (22 PRs), and again after it, before
pr_create(19 PRs). No open PR touchessql-driver.ts,sql-driver-compile-refusal-seam.test.ts,turso-driver.ts,sqlite-wasm-driver.ts,check-tenant-chokepoint.mjsor the driver-sql anchor entries. #26138 touchesdriver-sql/src/builtin-column-collision.tsonly; #26337 addsdriver-sql/src/sql-driver-external-unprovisioned-sort-anchor.test.tsonly.Seam table, fresh, on
77a52cfe8sql-driver.tsis 20,492 lines. The 30-day column countsgit blamelines from commits on or after 2026-09-26T23:48:01Z, 30 days before the base commit. History reaches 2026-09-16.scripts/pm/git-history.mjs countproves 73 commits on the file in 30 days and 31 in the last 7. In all, 3,618 lines (17.7%) are from the last 30 days.SqlDriverspans lines 5452-20417 with 350 members: 243 instance methods, 13 accessors and 32 statics, all 32 private or protected.!andsatisfiesbefore it readsthis. Sections are the class's own banner comments.|:--|:--|:--|--:|--:|:--|--:|--:|:--|
| 1-5451 | Module preamble | - | 5,451 | 855 | - | - | - | - |
| 5452-7043 | Class head: fields, statics, constructor, capability getters |
name…assertDialectEmits| 1,592 | 290 | 8 (1) | 1 | 0 | 18 (18) || 7044-7475 | Lifecycle |
connect…disconnect| 432 | 155 | 8 (3) | 0 | 0 | 6 (6) || 7476-9986 | CRUD — IDataDriver core |
find…delete| 2,511 | 736 | 43 (7) | 0 | 12 | 0 (0) || 9987-10598 | Bulk & Batch Operations |
bulkCreate…count| 612 | 119 | 9 (6) | 0 | 7 | 0 (0) || 10599-10695 | Raw Execution |
rawStatementFault…noteRuntimeIndexDdl| 97 | 28 | 3 (1) | 0 | 0 | 0 (0) || 10696-10788 | Transactions |
beginTransaction…rollbackTransaction| 93 | 46 | 7 (5) | 0 | 0 | 0 (0) || 10789-11320 | Aggregation |
aggregate…unresolvableAggregateColumnRefusal| 532 | 60 | 5 (1) | 0 | 1 | 0 (0) || 11321-11608 | Distinct |
distinct…unresolvableDistinctColumnRefusal| 288 | 17 | 3 (1) | 0 | 1 | 0 (0) || 11609-11724 | Window Functions |
findWithWindowFunctions…findWithWindowFunctions| 116 | 4 | 1 (1) | 0 | 1 | 0 (0) || 11725-11823 | Query Plan Analysis |
explain…analyzeQuery| 99 | 1 | 2 (2) | 0 | 1 | 0 (0) || 11824-11882 | Schema Sync (syncSchema / init) |
syncSchema…reclaimSpace| 59 | 34 | 3 (3) | 0 | 0 | 0 (0) || 11883-13431 | Schema Sync (syncSchema / init) › Data-lifecycle rotation (ADR-0057 P2) |
rotationStateByTable…migrateMysqlTimeColumns| 1,549 | 158 | 26 (4) | 0 | 0 | 0 (0) || 13432-13688 | Schema Sync (syncSchema / init) › Deferred schema DDL (#3917) |
setDeferredDdl…flushDeferredSchemaDdl| 257 | 25 | 6 (4) | 0 | 0 | 0 (0) || 13689-14503 | Schema Sync (syncSchema / init) › Managed-schema drift & reconcile (#2186) |
dialectName…createNullSafeUniqueIndex| 815 | 237 | 23 (3) | 0 | 0 | 0 (0) || 14504-14555 | Schema Introspection |
listPhysicalColumns…introspectSchema| 52 | 46 | 2 (2) | 0 | 0 | 0 (0) || 14556-14826 | Internal helpers |
getKnex…auditMissingTenant| 271 | 73 | 8 (1) | 0 | 1 | 0 (0) || 14827-17267 | Internal helpers › Filter helpers |
tableNameForBuilder…applyFalseConstant| 2,441 | 438 | 44 (2) | 0 | 0 | 0 (0) || 17268-17443 | Internal helpers › Field mapping |
mapSortField…mapAggregateFunc| 176 | 2 | 6 (0) | 0 | 0 | 0 (0) || 17444-17491 | Internal helpers › Window function builder |
buildWindowFunction…buildWindowFunction| 48 | 26 | 1 (0) | 0 | 0 | 0 (0) || 17492-19438 | Internal helpers › Column creation helper |
nowColumnDefault…applyDeclaredColumnDefault| 1,947 | 92 | 19 (0) | 0 | 0 | 8 (8) || 19439-19736 | Internal helpers › Database helpers |
sqliteFilename…isJsonField| 298 | 76 | 9 (2) | 0 | 0 | 0 (0) || 19737-20213 | Internal helpers › SQLite serialisation |
formatInput…formatOutput| 477 | 27 | 2 (0) | 0 | 0 | 0 (0) || 20214-20417 | Internal helpers › Introspection internals |
introspectColumnOrder…introspectUniqueConstraints| 204 | 73 | 5 (0) | 0 | 0 | 0 (0) || 20418-20492 | After the class | - | 75 | 0 | - | - | - | - |
Pricing the areas
Eligibility is as cuts 11-12 defined it: an annotated return type or not public; no
getBuilder,this.FIELD.delete/insert/update(, private-static,super, bare-thisorthis-write site; notinsertOnlyUpsertColumns. 150 members are eligible on this base. Each is priced as its stub minus its body; per section, the sum over eligible bodies with a negative price and nosql-driver.ts#…pointer is: Filter helpers -550 (22 bodies), the Rotation sub-banner -195 (7), CRUD -157 (12), every other section -93 or less. Each candidate area is then priced net of the module-level declarations it shares with members that stay (they stay in place, exported; ⛔ no second copy of a helper); the declarations only the moved bodies read move with them.$nowcomparison (Filter helpers:applyCrossFieldComparison,applyNowAnchorComparison,nowAnchorBaseExpr,crossFieldOffsetExpr)assertConflictTargetHonoured,introspectKeyIndexes,assertConflictTargetBackedWithoutClause,upsertConflictRefusal,upsertTenantGuard)indexCoversConflictTarget,refuseAmbiguousConflictTarget,unbackedConflictTargetCause,ConflictTargetVerdict)refuseUnbackedConflictTargetandrefuseUpsertConflictOutsideWrittenOrganizationstay withupsert, which builds throughgetBuilder. Still the next candidate.compileFilters-75,applyNormalizedComparison-60,applyEmptyOperator-28, …)applyFilterCondition, which an ADR-0139 pointer pins in place.Why this area
applyFilterConditioncalls them.applyFilterCondition, the one caller of both arms, stays with the operator dispatch (ADR-0139 names it by path).declaredFieldsFor,resolveTenantField,mapSortField,remoteColumnandfilterColumnExprare read by members that stay too. The moved bodies reach them through the host.crossFieldComparisonErrorandnowAnchorPositionError(also thrown byassertCompilableComparand,classifyFilterKeyandapplyFilterCondition),NowAnchor(also read bynowAnchorOf),fieldReferenceOf,mysqlDatetimeLiteral(also read bystorageDatetimeValue),withheldFilterError(the [A of #7929] a spec-declared provenance mark set at both read-scope merge boundaries, so the driver can restore the author-facing cross-field diagnostic without re-disclosing policy #8220 seam every compile refusal goes through),JSON_COLUMN_TYPESandNUMERIC_SCALAR_TYPES. Each gains a leadingexportand nothing else, and the module imports them back. The module reads them only inside function bodies, so the import cycle is evaluation-safe.index.tsnames its exports one by one; the export lists below prove none of the eight leaks.getBuilder, reads a static, has athis.FIELD.delete/insert/update(site, usessuperor a barethis, or writes tothis. All four are protected with annotated returns.check:tenant-chokepoint's D8 predicates, measured with its ownnullArmsOf: 4 at base and 4 at head, all insql-driver.ts, 0 in the new module. The two arms name the tenant column only to refuse it as an operand (9 and 8tenantFieldreferences, 0 predicates); disclosed because the new module is outside the gate'sSCAN_FILES.TursoDriverandSqliteWasmDriveroverride none of the four. The stubs keep their signatures, and every member the bodies call is a late-bound closure on the instance.sql-driver.ts#…pointer or asql-driver.ts::…key. Prose mentions (formula'smatches-filter.ts,objectql'shaving-filter.ts,runtime'sexpected-read-refusal-noise.ts,spec'sfilter-cross-field-comparison-class.ts,cross-field-conformance-cases.ts) name the functions orSqlDriver.applyCrossFieldComparison, which are all still true. The one reader by path that depends on where they live issql-driver-compile-refusal-seam.test.ts, migrated below. The five cligenerate-*.pin.test.tsfiles readsql-driver.tstext insidecreateColumn,fieldHasColumnand the width constants, all unchanged; four of them ran green at head (62 tests), the fifth is in the cli integration tier, which CI runs.The path reader migrated in this PR
sql-driver-compile-refusal-seam.test.tsenumerates every function that builds anINVALID_FILTERerror by parsingsql-driver.ts, and pins the set ofwithheldFilterErrorcallers equal to itsDOORSrows. Two of those callers,uncompilableFieldReferenceErroranduncompilableNowAnchorError, moved. The enumeration now parsessql-driver.tsandsql-driver-cross-field-comparison.tswith the same visitor; its three docblock lines say so. Nothing else in the test changed, and theDOORStable is untouched.every builder that goes through the seam is driven by a row below, with the two moved builders missing from the scanned set. So the migration is what keeps the gate from going silently empty.DOORSrow still asserts its builder by the error's own stack (at uncompilableFieldReferenceError), which the move does not change.The shape (#23050 shape B, as cuts 1-12 apply it)
async). Its body is now one host literal plusNAME.call(host, ...), marked[#25521 cut 13], and returns the moved function's own result.isMysql,isSqlite). Methods go as closures that call the method on the driver at call time (crossFieldOffsetExpr,declaredFieldsFor,filterColumnExpr,mapSortField,nowAnchorBaseExpr,remoteColumn,resolveTenantField). So a subclass override, an instance spy, or a field replaced after construction is still the one the body reaches (probe below).CrossFieldComparisonHostlists exactly those nine members, each typed as the class member's indexed access. There is no cast, and no member becomes public.temporalStorageForm(@objectstack/core), the moved spec import,type Knex(knex), and from./sql-driver.jsthe eight shared declarations plustype SqlDriver.sql-driver.tsimport comment names cut 13 in 3 lines. The header cites noADR-NNNN Dn.ADR anchors
check:adr-anchorscounts 1877 decision-letter citations at base77a52cfe8(a clean detached worktree) and 1877 at head, over 90 anchored files at both ends. Both runs exit 0. After the merge oforigin/mainit still reads 1877.crossFieldComparisonClass's docblock namesADR-0104without a letter.sql-driver.tsentry's one paragraph (ADR-0053) describesformatOutputandpresentReadValue, neither of which moved. Noscripts/adr-anchors/**file changed.The published
.d.tsdoes not changeThis is measured under the series bar (ruling
6348891160, item 1).pnpm turbo run build --filter=@objectstack/driver-sql --forceat each end, against the same dependency tree. The base leg writes basesql-driver.tsandsql-driver-compile-refusal-seam.test.tsinto this worktree and deletes the new module, under an EXIT/INT/TERM trap that restores HEAD. The restore is proved by the HEAD blob hashes (ea341d74,e5a80af5,d0fed3d6), an emptygit diff HEADand 0 status entries.dist/index.d.tsanddist/index.d.mtsis the same at base and at head,0f33bf3febaac3c7aa96f68a4226d752766b8203ab7acade523b69f9aaa44d74(unchanged since cut 8).protectedfromapplyCrossFieldComparisonin the headindex.d.ts. The result hashes tode5a5d07d80992753bdcb2632346daf7a4e45ce1c7983135555d824c64ae97fc, with a 1-line diff andcmpexit 1.requireand ESMimportof the built dist give the same 59 names at both ends (sha105757ce5ae). None of the eight newly exported declarations, and nothing from the new module, is among them..call(hostsites inindex.mjsgo from 43 at base to 47 at head.Move-only proof
The proof compares TypeScript compiler AST leaf tokens (JSDoc nodes skipped), base
77a52cfe8against headb1f89c17c. It is a scratch script and is not committed.thisapplyCrossFieldComparisonapplyNowAnchorComparisonnowAnchorBaseExprcrossFieldOffsetExprsql-driver.ts; so is the moved import with its comment. The eight shared ones differ from base by a leadingexportand nothing else. Atsql-driver.ts's top level, 191 of 206 base statements are byte-identical. The rest are the moved import, the five moved declarations, the eight exports and the class.PROOF OK, exit 0.this.nowAnchorBaseExpr(anchor.at, targetClass)tothis.nowAnchorBaseExpr(anchor.at, 'date')in a module copy (1 hit, checked on disk). The proof then reportsapplyNowAnchorComparisonwith its body tokens 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, base77a52cfe8against headb1f89c17c. Every part's exit code was captured on its own (build, probe, three suites: all 0 at both ends).@objectstack/driver-sql@objectstack/driver-turso, against that end's built driver-sql dist@objectstack/driver-sqlite-wasmtsc --noEmit -p .in driver-sql exits 0, also after the merge.pnpm exec eslint --no-inline-config packages/drivers/driver-sqlexits 0.$eq,$ne, an ordering,addDaysliteral,addDayscolumn,addDayswith$ne) in bothwhereandorWhere, and four now-anchored comparisons, each with the compiled SQL and bindings, and with instance spies on all seven host methods showing the calls in order;addDays,addDayson a number or from a text column, the unreachable operator arms, an invalid anchor, an anchor with no offset, an anchor on a number), each with codeINVALID_FILTER, status 400, message head and tail, and the throwing frame (unsupportedFilterErrorat both ends);crossFieldOffsetExpr,nowAnchorBaseExprandresolveTenantField, each reached (the override's SQL appears in the statement, and its tenant column is refused as an offset);isSqliteandisMysqlshadowed on the instance after construction: the MySQL and PostgreSQL spellings of both operand builders, then SQLite's again;find()calls end to end on SQLite, rows answered, including$notover anaddDayscomparison and two refusals throughfind();TypeErrorwith the same message, and only the top stack frame's receiver name differs,_SqlDriver.applyCrossFieldComparisonat base andObject.applyCrossFieldComparisonat head. That is the host-literal receiver of shape B, the same at every earlier cut, and no test or consumer reads it.test:repo. Therepoprojects of types (11), core (83), objectql (5) and rest (280 passed, 18 skipped) pass locally after building rest's closure. Runtime's is NOT MEASURED locally; CI's Test Core runs all of them.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 59 commands for the 3 changed paths (merge base77a52cfe8). Onb1f89c17c, 58 exit 0.check:dual-build-cjs-loadsexits 3,PREREQUISITE NOT MET(no full build here): NOT MEASURED locally, CI runs it. The teardown self-test's pinned commit was fetched first, and objectql was built forcheck:lean-entry-closure.dispatch-gates --ran: 59 derived families accounted for, 58 run, 1 NOT-MEASURED (derived from the recorded exit 3), exit 0. After the merge the derived list is the same 59 commands.77a52cfe8) and at head, all exit 0:check:adr-anchorscheck:adr-symbol-anchorscheck:tenant-chokepointcheck:durability-log-levelcheck:driver-conformancecheck:keyed-text-boundscheck:object-def-param-keyscheck:system-context-censuscheck-tenant-audit-censuscheck:logger-receiver-detachcheck:nul-bytespasses. The pre-pushcheck:commit-card-trailerspassed on push.Acceptance notes
check:tenant-chokepoint's D8 rule does not scan the new module. The two arms read the tenant column only to refuse it as an operand, and hold 0 of the file's 4 tenant predicates (measured with the gate's ownnullArmsOf). The series limit namesgetBuilderareas only, and none of the four builds a query. Noted, not filed: wideningSCAN_FILESwould be a gate change.cross-field-conformance-cases.ts,matches-filter.ts,having-filter.tsandfilter-cross-field-comparison-class.tsnameSqlDriver.applyCrossFieldComparisonor "crossFieldComparisonClassin@objectstack/driver-sql". Each is still true: the method is still a member and the function is still in the package.upsert, exported in place.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01DC6m1WLgsQ2NJA2W8mZBQi