Repository navigation
refactor(driver-sql): move the filter compiler's leaf predicate emitters out of sql-driver.ts into sql-driver-predicate-emitters.ts (#25521 cut 16) - #26669
Conversation
…ers out of sql-driver.ts into sql-driver-predicate-emitters.ts (#25521 cut 16) applyNormalizedComparison, applyNullSafeNegative, applyEmptyOperator, applyJsonMembership, applyLike and applyLikePattern, with the module-level dialect constructs only they read, move unedited into a new seam module reached through PredicateEmitterHost. The class keeps every method as a stub. The compile-refusal seam test reads the new module. The four staying links to unresolvableColumnNameOf resolve again through a type-only import. Claude-Session: https://claude.ai/code/session_01FEkQ5ncyQ2HYfpr45hwrPU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 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 d6aaaa4127be877322989646ea30f57063744530 && git checkout d6aaaa4127be877322989646ea30f57063744530
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 86ae47e13acd862527b23ee7e40be3a58a212a3e 048467ad9815f33268c60e7dd7edf10ebc6b426e && git checkout -B drift-repro 86ae47e13acd862527b23ee7e40be3a58a212a3e && git merge --no-ff 048467ad9815f33268c60e7dd7edf10ebc6b426e
node scripts/docs-audit/affected-docs.mjs --json 86ae47e13acd862527b23ee7e40be3a58a212a3e
|
Seat note: Test Core (6/8) red is not this PR's2026-10-27T12:48Z ·
Generated by Claude Code |
席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 40509204270 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Seat note: re-queued after the merge-group red2026-10-27T14:37Z · The merge-group red (run
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里过滤编译器「把一个已校验的比较写成 SQL」的那一层(规范化列比较、$ne/$nin的空值安全包装、$empty、JSON 列上的$contains成员判断、$contains/$startsWith/$endsWith/$icontains与$like/$ilike),连同只被它们用到的十六个模块级声明(各方言的文本匹配构造、转义、ASCII 大小写表、$like模式计划、$empty的空列表判断与两个拒绝),原样搬进新文件sql-driver-predicate-emitters.ts;类上的六个方法全部保留,只把方法体转交过去。顺带修好第 15 刀留下的四处{@link unresolvableColumnNameOf}失效链接(只加一个仅类型导入)。src/sql-driver.ts(22,096 lines, +2,533 in 30 days) along itsSqlDriverseams, one move-only slice per PR #25521 拆分超大文件的第 16 刀(18.2.0 发布后重启);这一层是重新测量后收益最大的可搬区域,sql-driver.ts减少 703 行。sql-driver-predicate-emitters.ts的头注释与PredicateEmitterHost;sql-driver.ts中六个方法的转交桩和新的 import 行;sql-driver-compile-refusal-seam.test.ts新增的扫描文件。pnpm --filter @objectstack/driver-sql exec vitest run src/sql-driver-compile-refusal-seam.test.ts src/sql-driver-like-pattern.test.ts src/sql-driver-20446-empty-flip.test.ts src/sql-driver-17590-json-column-membership.test.ts,预期全部通过。What changes
Cut 16 of the
sql-driver.tssplit (#24586 ruling A, recorded in6270097840), the first after the 18.2.0 restart (6439210093). The claim is6439272472. The cut opens with a fresh seam table (below), taken onorigin/mainf9867020c6, and takes the area that pays best without breaking a series limit: the leaf predicate emitters of the filter compiler, the methods that turn one comparison the compiler has already validated into SQL on the builder. They move unedited into a new seam module,packages/drivers/driver-sql/src/sql-driver-predicate-emitters.ts, through a new hostPredicateEmitterHost:applyNormalizedComparison, a value test against a storage-normalised column expression;applyNullSafeNegative, the 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 wrapper that lets a NULL column satisfy a negative-polarity test;applyEmptyOperator, filter: the engine's compile surfaces answer$emptyby the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444's$empty;applyJsonMembership, the membership reading of$contains/$notContainson a JSON column;applyLike, the$containsfamily and$icontains;applyLikePattern, Filter AST:likeis folded to$containsat the wire — wildcards bind as literals and driver-sql'slike/ilikearm is unreachable #7536's$like/$ilike.With them go the sixteen module-level declarations only they read:
textMatchPredicate,sqliteLengthAwareTextMatchandmysqlAsciiLowerBinary, the comparand escapesescapeLikeComparand,escapeGlobComparandandwrapTextMatchShape, the ASCII case mapASCII_UPPER_LETTERS/ASCII_LOWER_LETTERS, andNUL_CHARACTER;$likepattern plan:likePatternPredicate,sqliteLikePatternPlan,sqliteLikePatternMatchand theSqliteLikePatternPlanshape;$emptyconstructemptyJsonListPredicateand its two refusals,undeclaredEmptyOperatorFieldErrorandemptyListUnsupportedDialectError.The
likePatternToGlobPatternimport loses its last reader insql-driver.ts, so that one name leaves the import there (the line and its comment stay for the two$likegates beside it) and the module imports it.Cut 15's follow-up, folded in. Four links to
{@link unresolvableColumnNameOf}, in two staying docblocks (aggregateBackendFault's anddistinctBackendFault's), named a function that cut 15 moved tosql-driver-read-exit-fault.ts. The existing import of that module gainstype unresolvableColumnNameOf, so all four resolve again. No comment changes. The same spelling keeps this cut from adding new broken links: five moved declarations that staying docblocks link (textMatchPredicate,likePatternPredicate,emptyJsonListPredicate,undeclaredEmptyOperatorFieldError,emptyListUnsupportedDialectError) are exported from the module and imported back as types. A link-only import is a type import, erased from the build.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. One test changes, and only as a path reader (below). Branchclaude/project-thread-ulif5z(the platform assigned it to this card thread, as the claim records). The move is one commit,5ceec116b, onf9867020c6, followed by one merge oforigin/main.Net line count (the seat's rule).
sql-driver.tsgoes from 19,349 lines to 18,646: -703.NUL_CHARACTER2,escapeLikeComparand11,escapeGlobComparand21,wrapTextMatchShape6,sqliteLengthAwareTextMatch85,mysqlAsciiLowerBinary27,textMatchPredicate138,SqliteLikePatternPlan27,sqliteLikePatternPlan32,sqliteLikePatternMatch62,likePatternPredicate73,undeclaredEmptyOperatorFieldError29,emptyListUnsupportedDialectError19,emptyJsonListPredicate47), plus 15 separator lines.Path readers migrated in the same commit.
sql-driver-compile-refusal-seam.test.tsenumerates everyINVALID_FILTERbuilder by parsing a fixed list of source files, and drives every caller ofwithheldFilterErrorthrough its table. The two$emptyrefusals now live in the new module, so the list gains it (one line, plus its comment and the two docblocks that name the list). No assertion and no table row changes.Serial with the file's in-flight PRs. I read the file list of every open PR on
objectstack-ai/objectstackat the start (19) and again before opening this one (14). None touchessql-driver.ts,turso-driver.ts,sqlite-wasm-driver.ts,check-tenant-chokepoint.mjsor a driver-sql anchor entry. #26337 addsdriver-sql/src/sql-driver-external-unprovisioned-sort-anchor.test.ts, which reads none of the moved members; #26556 and #26517 touch only package manifests and changelogs. #26599 (it touchedbuiltin-column-collision.ts) merged intomainduring the cut and is in this branch through the merge.Seam table, fresh, on
f9867020c6sql-driver.tsis 19,349 lines. The 30-day column countsgit blamelines from commits on or after 2026-09-27T09:51:51Z, 30 days before the base commit.scripts/pm/git-history.mjs countproves 75 commits on the file in 30 days and 32 in the last 7. In all, 3,432 lines (17.7%) are from the last 30 days.SqlDriverspans lines 4847-19274 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.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 areas
Eligibility is as cuts 11-15 defined it: an annotated return type or not public; no
getBuilder,this.FIELD.delete/insert/update(, private-static,super, bare-thisorthis-write site; notinsertOnlyUpsertColumns. Each area is priced as its stubs minus its bodies, then net of the module-level declarations only it reads (they move along, byte-identical) and the ones it shares with members that stay (they stay in place, exported; ⛔ no second copy of a helper).applyNormalizedComparison,applyNullSafeNegative,applyEmptyOperator,applyJsonMembership,applyLike,applyLikePattern)ensureRotation,registerExternalObject,warnBuiltinColumnCollisions,rotationShardKey,rotationColumnList)compileFilterswithfilterArrayReachedDriverErrorcompileFiltershas an inferred return type, so the move script refuses it (series limit).Why
Why this area, as one area
applyFilters→compileFilters→applyFilterCondition) validates a condition, resolves its column, and then hands ONE comparison to a leaf emitter, which only writes the predicate onto the builder. The six emitters run no statement and read no schema beyond the two predicates the host carries. They sat inside the 2,150-line Filter helpers sub-banner only because the compiler calls them.$likepattern plan, the$emptyconstruct and its two refusals) have no other reader and move with them.applyFilterConditionis pinned in place by its ADR-0139 pointer, andcompileFiltershas an inferred return type.applyContainsLike,applyTextOperatorOverNonTextColumnandapplyFalseConstantare leaf emitters too, but each reads none of the moving declarations and its stub would cost as many lines as its body.LIKE_ESCAPE_CHARACTERis also bound byscanMaxNumericTail's prefix scan, andTextMatchShapeis named byapplyLike's signature on the class. Each gains a leadingexportand nothing else, and the module imports both back.withheldFilterError, the finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 seam both$emptyrefusals build through, was already exported. The module reads these 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 them leaks.declaredValueShape,isJsonColumnandapplyNullSafeNegativeare read by members that stay too; the moved bodies reach them through the host.getBuilder, reads a static, has athis.FIELD.delete/insert/update(site, usessuperor a barethis, or writes tothis. Every one has an annotated return type and is private.check:tenant-chokepoint's D8 predicates, measured with its ownnullArmsOf: 4 at base and 4 at head, all insql-driver.ts, 0 in the moved code (none reads a tenant field).catchclause.check:durability-log-level --listgives the same seams at both ends, line numbers aside; the gate reads 48 seams (7 propagating) and 82 read seams at head.check:logger-receiver-detach: 0 detaches (3779 non-test files at head).TursoDriverandSqliteWasmDriverextendSqlDriverand override none of the six. Every stub keeps its signature and modifier, and every member a body calls is a late-bound closure on the instance (probe below).sql-driver.ts#…pointer or asql-driver.ts::…key, and noscripts/adr-anchors/**record names them. Of the tests that readsql-driver.tsas text, only the compile-refusal seam test reads a moved declaration (migrated above). Prose mentions elsewhere (service-analytics's text-match and$likemirrors,@objectstack/core's JSON membership helper, the spec's text conformance table, the driver tests) name the functions, and each is still true.The shape (#23050 shape B, as cuts 1-15 apply it)
NAME.call(host, ...), marked[#25521 cut 16].applyNormalizedComparisonandapplyNullSafeNegativeread no member of the driver, so their methods call the function directly.dialectNamegoes as a GETTER that reads the driver at call time.declaredValueShape,isJsonColumnandapplyNullSafeNegativego as closures that call the method on the driver at call time. So a subclass override, an instance spy, or adialectNamereplaced after construction is still the one the body reaches.PredicateEmitterHostlists exactly those four members, each typed as the class member's indexed access. There is no cast, and no member becomes public.jsonMembershipPredicate(@objectstack/core),expandEmptyOperatorandlikePatternToGlobPattern(@objectstack/spec/data),type SqlDialectName(./schema-drift.js), and from./sql-driver.jsLIKE_ESCAPE_CHARACTER,withheldFilterError,type TextMatchShape,type isMultiValuedColumn(named by a moved docblock) andtype SqlDriver.sql-driver.tsimport comment names cut 16 in 3 lines. The header cites noADR-NNNN Dn.Verification
ADR anchors
check:adr-anchorscounts 1881 decision-letter citations at basef9867020c6(a clean detached worktree) and 1881 at head, over 91 anchored files at both ends. Both runs exit 0.ADR-…mention in them stays insql-driver.ts.check:adr-symbol-anchors: 2683 resolve at both ends.mainmoved under the cut, so I measured its tipdbf7e04adfagainst the merge head048467ad98: 1872 letters at both (91 anchored files, exit 0), and 2687 symbol anchors resolve at both. The drop from 1881 comes frommain's own commits, not from this diff.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.tsand the base seam test into 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, 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), on the move commit and again on the merge head. These two are the dist's only declaration files (no chunk), so nothing was re-ordered and the declaration-reorder bar of ruling6441032442does not come into play.privatefromapplyLikein the headindex.d.ts. The result hashes to4450f3699c74b340a3c946f037a353be89d3c21fe80163eed2040a9a86dcb1ca, withcmpexit 1.requireof the head dist gives 59 names (sha105757ce5ae); the ESM export statement of the base and headindex.mjslists the same 59 names (sha105757ce5aeboth). None of the seven newly exported declarations, and nothing from the new module, is among them..call(hostsites inindex.mjsgo from 57 at base to 61 at head (four host stubs; the two direct-call stubs have none).Move-only proof
The proof compares TypeScript compiler AST leaf tokens (JSDoc nodes skipped), base
f9867020c6against head5ceec116b. It is a scratch script and is not committed.thisapplyNormalizedComparisonapplyEmptyOperatorapplyJsonMembershipapplyNullSafeNegativeapplyLikeapplyLikePatternasync, at either end.sql-driver.ts; five of them carry a leadingexportand nothing else.LIKE_ESCAPE_CHARACTERandTextMatchShapediffer from base by a leadingexportand nothing else. Atsql-driver.ts's top level, 174 of 195 base statements are byte-identical; the rest are the sixteen moved declarations, the two exports, the two edited imports and the class. The one head-only statement besides those is the new import.PROOF OK, exit 0.scripts/ablation-replace.mjs, I changed'[$&]'to'[$&]]'in the module'sescapeGlobComparand(1 hit, checked on disk). The proof then reportsescapeGlobComparandas differing, printsPROOF FAILED, and exits 1. The restore was proved (blob equals HEAD, emptygit diff HEAD).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, basef9867020c6against head5ceec116b, and then again on the merge head048467ad98. Every part's exit code was captured on its own (build, probe, three suites: all 0 at every leg).@objectstack/driver-sql@objectstack/driver-turso, against that end's built driver-sql dist@objectstack/driver-sqlite-wasmOn the merge head (it brings in
mainup todbf7e04adf), the three suites give the same totals and the same per-test statuses as base, 0 / 0 / 0 in each.tsc --noEmit -p .in driver-sql exits 0 on both heads.pnpm exec eslint --no-inline-config packages/drivers/driver-sqlexits 0.code,status, own keys and symbol-keyed members, the SQL and bindings left on the builder, and the log lines written:applyNormalizedComparisonover every operator on both joins, with null comparands and empty or malformed lists, on SQLite, Postgres, MySQL and an unknown dialect;applyNullSafeNegativeon both methods;applyEmptyOperatoron three row shapes, both polarities and both joins, an undeclared field and an unknown dialect (both refusals);applyJsonMembershipon a JSON and a scalar column, both polarities, scalar and list values;applyLikeon the three shapes, negated, folded, with metacharacters and a NUL;applyLikePatternplain, with wildcards, escapes, a NUL, a GLOB metacharacter, and folded;declaredValueShape,isJsonColumnandapplyNullSafeNegative, instance spies on each, anddialectNamereplaced on the instance after construction (the new ones are the ones reached);find/countend to end on SQLite through the compiler, reaching each emitter;_SqlDriver.Xat base andObject.Xat head. That is the host-literal receiver of shape B, the same at every earlier cut, and no test or consumer reads it. On the merge head the probe output is byte-identical to the move commit's.test:repo. Not run locally; it is CI's run on this PR.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 54 families for this diff, the same list on the move commit and on the merge head. I ran each one on its own and captured its exit code before any pipe.048467ad98: 54 run, all 54 exit 0.--ranreconciles:54 derived famil(ies) accounted for — 54 run, 0 NOT-MEASURED, exit 0.5ceec116b: 53 exited 0;check:dual-build-cjs-loadsexited 3 there (its prerequisite, a full workspace build, was missing). After building the driver-sql and objectql closures for the merge head it ran and passed (112 require entry points across 69 packages load).check-adr-anchors,check-adr-symbol-anchors,check-tenant-audit-census,check-platform-checklist --changed-from origin/main,check-commit-card-trailers --range origin/main..HEAD,eslint --no-inline-config packages/drivers/driver-sql,check-durability-degradation-log-level --list.check:logger-receiver-detachwalks 3779 files and fires on its control corpus;check:tenant-chokepointreads 24getBuilder()bindings across 3 files).The 54 derived commands:
node scripts/check-closing-keyword-parity.mjs,node scripts/check-closing-keyword-parity.mjs --self-test,node scripts/check-comment-mask-adoption.mjs,node scripts/check-comment-mask-adoption.mjs --self-test,node scripts/check-comment-mask-corpus.mjs,node scripts/check-dts-emitted.mjs --self-test,node scripts/check-issue-citations.mjs,node scripts/check-plugin-teardown-shape.mjs,node scripts/check-plugin-teardown-shape.mjs --self-test,node scripts/ci/assert-turbo-remote-cache.mjs --self-test,node scripts/ci/plan-test-core-shards.mjs --self-test,node scripts/ci/prune-turbo-cache.mjs --self-test,node scripts/docs-audit/check-affected-docs.mjs,node scripts/docs-audit/check-drift-comment.mjs,pnpm --filter @objectstack/spec run check:duration-unit-keys,pnpm check:ci-filter-parity,pnpm check:closed-ceremony-text,pnpm check:cross-package-test-inputs,pnpm check:dispatcher-error-vocabulary,pnpm check:doc-authoring,pnpm check:driver-conformance,pnpm check:driver-memory-census,pnpm check:dts-closure,pnpm check:dual-build-cjs-loads,pnpm check:durability-log-level,pnpm check:engine-double-contract,pnpm check:error-status-conformance,pnpm check:gitlink-declared,pnpm check:issue-citations,pnpm check:keyed-text-bounds,pnpm check:lean-entry-closure,pnpm check:logger-receiver-detach,pnpm check:nul-bytes,pnpm check:object-def-param-keys,pnpm check:objectql-double-limit,pnpm check:org-identifier,pnpm check:page-declaration-shape,pnpm check:platform-object-tenancy-census,pnpm check:published-files,pnpm check:query-options-erasure,pnpm check:refd-timer-probe,pnpm check:registry-log-declared,pnpm check:rest-log-spy-declared,pnpm check:slot-lookup,pnpm check:sourcemap-no-sources-content,pnpm check:system-context-census,pnpm check:tenant-chokepoint,pnpm check:test-source-alias,pnpm check:tier-file-adoption,pnpm check:type-check-coverage,pnpm check:type-check-debt,pnpm check:undeclared-dep-imports,pnpm check:watch-hint-literal,pnpm check:where-matcher.Acceptance notes
unresolvableFilterColumnError(3 links),backendStatementFaultError(3) andunresolvableColumnProvenance(2), which staying docblocks insql-driver.tsstill name. The order folded in only theunresolvableColumnNameOflinks, so those stay as they were. On a scan of{@link X}names that are neither a top-level binding nor aSqlDrivermember,sql-driver.tsgoes from 34 at base to 30 at head (the four fixed); this cut adds none. In the new module, one comment inside the movedapplyNormalizedComparisonbody names the class memberfilterColumnExpr, byte-identical to base. Noted, not filed: the same type-import repair would close them in one later line.check:tenant-chokepoint'sSCAN_FILESdoes not list the new module. The moved bodies hold 0 of the file's 4 tenant predicates and build no query. Noted, not filed: wideningSCAN_FILESwould be a gate change.ensureRotation,registerExternalObject,warnBuiltinColumnCollisions,rotationShardKey,rotationColumnList, about -120), withmigrateMysqlDatetimeColumns/migrateMysqlTimeColumns(durability trap),ensureShardTable(named bycheck-object-def-param-keys.mjs) andcomputeTenantField(named bypackages/spec/liveness/object.json) staying. The series' remaining yield outside the pinned methods (createColumn,applyFilterCondition,formatInput/formatOutput,initObjects) is small bodies of under -120 each.Human Minimum
packages/specchange.PredicateEmitterHost, follows the interface casing.eslint --no-inline-configpasses (repo-widepnpm lintis CI's run).main, one pull request.skip-changeset; both declaration lines above.packages/specnot touched.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5ncyQ2HYfpr45hwrPU