Skip to content

Commit a485b5a

Browse files
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)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XWvG89mssHGAkmn1TLwURoV)_ 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}` 失效链接(只加一个仅类型导入)。 - **为什么改**:#25521 拆分超大文件的第 16 刀(18.2.0 发布后重启);这一层是重新测量后收益最大的可搬区域,`sql-driver.ts` 减少 703 行。 - **风险与回滚**:纯搬移,无行为变化;三个 SQL 驱动的测试两端逐条一致,公开类型声明哈希不变,676 条探针读数两端一致(仅 2 条栈顶接收者名不同)。回滚即还原本 PR。 - **先看**:`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.ts` split (#24586 ruling A, recorded in `6270097840`), the first after the 18.2.0 restart (`6439210093`). The claim is `6439272472`. The cut opens with a fresh seam table (below), taken on `origin/main` `f9867020c6`, 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 host `PredicateEmitterHost`: - `applyNormalizedComparison`, a value test against a storage-normalised column expression; - `applyNullSafeNegative`, the #5298 wrapper that lets a NULL column satisfy a negative-polarity test; - `applyEmptyOperator`, #20444's `$empty`; - `applyJsonMembership`, the membership reading of `$contains` / `$notContains` on a JSON column; - `applyLike`, the `$contains` family and `$icontains`; - `applyLikePattern`, #7536's `$like` / `$ilike`. With them go the sixteen module-level declarations only they read: - the text-match constructs `textMatchPredicate`, `sqliteLengthAwareTextMatch` and `mysqlAsciiLowerBinary`, the comparand escapes `escapeLikeComparand`, `escapeGlobComparand` and `wrapTextMatchShape`, the ASCII case map `ASCII_UPPER_LETTERS` / `ASCII_LOWER_LETTERS`, and `NUL_CHARACTER`; - the `$like` pattern plan: `likePatternPredicate`, `sqliteLikePatternPlan`, `sqliteLikePatternMatch` and the `SqliteLikePatternPlan` shape; - the `$empty` construct `emptyJsonListPredicate` and its two refusals, `undeclaredEmptyOperatorFieldError` and `emptyListUnsupportedDialectError`. The `likePatternToGlobPattern` import loses its last reader in `sql-driver.ts`, so that one name leaves the import there (the line and its comment stay for the two `$like` gates 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 and `distinctBackendFault`'s), named a function that cut 15 moved to `sql-driver-read-exit-fault.ts`. The existing import of that module gains `type 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, no `packages/spec/**` edit, no anchor entry change, no gate edit and no new gate. One test changes, and only as a path reader (below). Branch `claude/project-thread-ulif5z` (the platform assigned it to this card thread, as the claim records). The move is one commit, `5ceec116b`, on `f9867020c6`, followed by one merge of `origin/main`. **Net line count (the seat's rule).** `sql-driver.ts` goes from 19,349 lines to 18,646: **-703**. - **Out:** the six bodies, 131 lines (62 + 35 + 24 + 4 + 3 + 3). The sixteen declarations, 592 lines with their own docblocks (ASCII map 13, `NUL_CHARACTER` 2, `escapeLikeComparand` 11, `escapeGlobComparand` 21, `wrapTextMatchShape` 6, `sqliteLengthAwareTextMatch` 85, `mysqlAsciiLowerBinary` 27, `textMatchPredicate` 138, `SqliteLikePatternPlan` 27, `sqliteLikePatternPlan` 32, `sqliteLikePatternMatch` 62, `likePatternPredicate` 73, `undeclaredEmptyOperatorFieldError` 29, `emptyListUnsupportedDialectError` 19, `emptyJsonListPredicate` 47), plus 15 separator lines. - **In:** the six stub bodies, 31 lines (2 + 7 + 8 + 2 + 6 + 6), and a 4-line import of the module with its comment. - The new module is 850 lines. **Path readers migrated in the same commit.** `sql-driver-compile-refusal-seam.test.ts` enumerates every `INVALID_FILTER` builder by parsing a fixed list of source files, and drives every caller of `withheldFilterError` through its table. The two `$empty` refusals 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/objectstack` at the start (19) and again before opening this one (14). None touches `sql-driver.ts`, `turso-driver.ts`, `sqlite-wasm-driver.ts`, `check-tenant-chokepoint.mjs` or a driver-sql anchor entry. #26337 adds `driver-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 touched `builtin-column-collision.ts`) merged into `main` during the cut and is in this branch through the merge. ## Seam table, fresh, on `f9867020c6` - **Size and churn.** `sql-driver.ts` is 19,349 lines. The 30-day column counts `git blame` lines from commits on or after 2026-09-27T09:51:51Z, 30 days before the base commit. `scripts/pm/git-history.mjs count` proves 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. - **Class shape.** `SqlDriver` spans lines 4847-19274 with 350 members: 243 instance methods, 13 accessors and 32 statics, all 32 private or protected. - **How it was measured.** The same TypeScript compiler AST script cuts 9-15 used, which strips casts, parentheses, `!` and `satisfies` before it reads `this`. Sections are the class's own banner comments. | Lines | Section | First … last member | Lines | 30-day | Methods (public) | Inferred public | getBuilder sites | Statics (private/protected) | |:--|:--|:--|--:|--:|:--|--:|--:|:--| | 1-4846 | Module preamble | - | 4,846 | 779 | - | - | - | - | | 4847-6438 | Class head: fields, statics, constructor, capability getters | `name` … `assertDialectEmits` | 1,592 | 290 | 8 (1) | 1 | 0 | 18 (18) | | 6439-6870 | Lifecycle | `connect` … `disconnect` | 432 | 155 | 8 (3) | 0 | 0 | 6 (6) | | 6871-9277 | CRUD — IDataDriver core | `find` … `delete` | 2,407 | 745 | 43 (7) | 0 | 12 | 0 (0) | | 9278-9807 | Bulk & Batch Operations | `bulkCreate` … `count` | 530 | 70 | 9 (6) | 0 | 7 | 0 (0) | | 9808-9904 | Raw Execution | `rawStatementFault` … `noteRuntimeIndexDdl` | 97 | 28 | 3 (1) | 0 | 0 | 0 (0) | | 9905-9997 | Transactions | `beginTransaction` … `rollbackTransaction` | 93 | 46 | 7 (5) | 0 | 0 | 0 (0) | | 9998-10494 | Aggregation | `aggregate` … `unresolvableAggregateColumnRefusal` | 497 | 70 | 5 (1) | 0 | 1 | 0 (0) | | 10495-10756 | Distinct | `distinct` … `unresolvableDistinctColumnRefusal` | 262 | 26 | 3 (1) | 0 | 1 | 0 (0) | | 10757-10872 | Window Functions | `findWithWindowFunctions` … `findWithWindowFunctions` | 116 | 4 | 1 (1) | 0 | 1 | 0 (0) | | 10873-10971 | Query Plan Analysis | `explain` … `analyzeQuery` | 99 | 1 | 2 (2) | 0 | 1 | 0 (0) | | 10972-11030 | Schema Sync (syncSchema / init) | `syncSchema` … `reclaimSpace` | 59 | 34 | 3 (3) | 0 | 0 | 0 (0) | | 11031-12579 | Schema Sync (syncSchema / init) › Data-lifecycle rotation (ADR-0057 P2) | `rotationStateByTable` … `migrateMysqlTimeColumns` | 1,549 | 158 | 26 (4) | 0 | 0 | 0 (0) | | 12580-12836 | Schema Sync (syncSchema / init) › Deferred schema DDL (#3917) | `setDeferredDdl` … `flushDeferredSchemaDdl` | 257 | 25 | 6 (4) | 0 | 0 | 0 (0) | | 12837-13651 | Schema Sync (syncSchema / init) › Managed-schema drift & reconcile (#2186) | `dialectName` … `createNullSafeUniqueIndex` | 815 | 237 | 23 (3) | 0 | 0 | 0 (0) | | 13652-13703 | Schema Introspection | `listPhysicalColumns` … `introspectSchema` | 52 | 46 | 2 (2) | 0 | 0 | 0 (0) | | 13704-13974 | Internal helpers | `getKnex` … `auditMissingTenant` | 271 | 73 | 8 (1) | 0 | 1 | 0 (0) | | 13975-16124 | Internal helpers › Filter helpers | `tableNameForBuilder` … `applyFalseConstant` | 2,150 | 349 | 44 (2) | 0 | 0 | 0 (0) | | 16125-16300 | Internal helpers › Field mapping | `mapSortField` … `mapAggregateFunc` | 176 | 2 | 6 (0) | 0 | 0 | 0 (0) | | 16301-16348 | Internal helpers › Window function builder | `buildWindowFunction` … `buildWindowFunction` | 48 | 26 | 1 (0) | 0 | 0 | 0 (0) | | 16349-18295 | Internal helpers › Column creation helper | `nowColumnDefault` … `applyDeclaredColumnDefault` | 1,947 | 92 | 19 (0) | 0 | 0 | 8 (8) | | 18296-18593 | Internal helpers › Database helpers | `sqliteFilename` … `isJsonField` | 298 | 76 | 9 (2) | 0 | 0 | 0 (0) | | 18594-19070 | Internal helpers › SQLite serialisation | `formatInput` … `formatOutput` | 477 | 27 | 2 (0) | 0 | 0 | 0 (0) | | 19071-19274 | Internal helpers › Introspection internals | `introspectColumnOrder` … `introspectUniqueConstraints` | 204 | 73 | 5 (0) | 0 | 0 | 0 (0) | | 19275-19349 | After the class | - | 75 | 0 | - | - | - | - | ### Pricing 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-`this` or `this`-write site; not `insertOnlyUpsertColumns`. 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). | Area | Bodies | Bodies net | Declarations that move along | Disposition | |:--|--:|--:|--:|:--| | **Leaf predicate emitters** (`applyNormalizedComparison`, `applyNullSafeNegative`, `applyEmptyOperator`, `applyJsonMembership`, `applyLike`, `applyLikePattern`) | 6 | -100 | -607 (sixteen declarations with their docblocks and separators), +4 for the import | **Moves (this cut).** -703 measured. | | Rotation sub-banner (`ensureRotation`, `registerExternalObject`, `warnBuiltinColumnCollisions`, `rotationShardKey`, `rotationColumnList`) | 5 | -124 (dry run) | 0 | Not taken: about -120 with its import, a sixth of this cut. Priced as the next slice (below). | | `compileFilters` with `filterArrayReachedDriverError` | 1 | about -115 | included | Not taken: `compileFilters` has an inferred return type, so the move script refuses it (series limit). | | Deferred-DDL previews | - | about -80 | 0 | Not taken: smaller. | | Withheld-filter log family | - | about -40 | 0 | Not taken: smaller. | ## Why ### Why this area, as one area - **One mechanism.** The filter compiler (`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. - **It pays the most of any area that moves cleanly**, nearly six times the next one, because sixteen module-level declarations (the dialect text-match constructs, the `$like` pattern plan, the `$empty` construct and its two refusals) have no other reader and move with them. - **What stays, and why.** - The compiler itself stays; `applyFilterCondition` is pinned in place by its ADR-0139 pointer, and `compileFilters` has an inferred return type. - `applyContainsLike`, `applyTextOperatorOverNonTextColumn` and `applyFalseConstant` are leaf emitters too, but each reads none of the moving declarations and its stub would cost as many lines as its body. - `LIKE_ESCAPE_CHARACTER` is also bound by `scanMaxNumericTail`'s prefix scan, and `TextMatchShape` is named by `applyLike`'s signature on the class. Each gains a leading `export` and nothing else, and the module imports both back. `withheldFilterError`, the #7929 seam both `$empty` refusals build through, was already exported. The module reads these only inside function bodies, so the import cycle is evaluation-safe. `index.ts` names its exports one by one; the export lists below prove none of them leaks. - `declaredValueShape`, `isJsonColumn` and `applyNullSafeNegative` are read by members that stay too; the moved bodies reach them through the host. - **No series limit is touched.** None of the six builds through `getBuilder`, reads a static, has a `this.FIELD.delete/insert/update(` site, uses `super` or a bare `this`, or writes to `this`. Every one has an annotated return type and is private. `check:tenant-chokepoint`'s D8 predicates, measured with its own `nullArmsOf`: **4 at base and 4 at head**, all in `sql-driver.ts`, 0 in the moved code (none reads a tenant field). - **Durability gate.** The moved code has no `catch` clause. `check:durability-log-level --list` gives the same seams at both ends, line numbers aside; the gate reads 48 seams (7 propagating) and 82 read seams at head. - **Logger.** No moved body writes to a logger. `check:logger-receiver-detach`: 0 detaches (3779 non-test files at head). - **Subclasses.** `TursoDriver` and `SqliteWasmDriver` extend `SqlDriver` and 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). - **Path readers, screened.** No script, test, doc or ADR names any of the six, or the sixteen moved declarations, by a `sql-driver.ts#…` pointer or a `sql-driver.ts::…` key, and no `scripts/adr-anchors/**` record names them. Of the tests that read `sql-driver.ts` as text, only the compile-refusal seam test reads a moved declaration (migrated above). Prose mentions elsewhere (`service-analytics`'s text-match and `$like` mirrors, `@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) - Each class method keeps its docblock and signature, byte-identical. Its body is now one host literal plus `NAME.call(host, ...)`, marked `[#25521 cut 16]`. `applyNormalizedComparison` and `applyNullSafeNegative` read no member of the driver, so their methods call the function directly. - `dialectName` goes as a GETTER that reads the driver at call time. `declaredValueShape`, `isJsonColumn` and `applyNullSafeNegative` go as closures that call the method on the driver at call time. So a subclass override, an instance spy, or a `dialectName` replaced after construction is still the one the body reaches. - `PredicateEmitterHost` lists exactly those four members, each typed as the class member's indexed access. There is no cast, and no member becomes public. - **Module imports.** `jsonMembershipPredicate` (`@objectstack/core`), `expandEmptyOperator` and `likePatternToGlobPattern` (`@objectstack/spec/data`), `type SqlDialectName` (`./schema-drift.js`), and from `./sql-driver.js` `LIKE_ESCAPE_CHARACTER`, `withheldFilterError`, `type TextMatchShape`, `type isMultiValuedColumn` (named by a moved docblock) and `type SqlDriver`. - **Comments.** The module header says what moved, what stayed and why. The `sql-driver.ts` import comment names cut 16 in 3 lines. The header cites no `ADR-NNNN Dn`. - **How the move was made.** A script produced it from the source text; nothing was retyped. Each body line is re-indented two columns; no body line starts inside a template literal. ## Verification ### ADR anchors - **The count.** `check:adr-anchors` counts **1881** decision-letter citations at base `f9867020c6` (a clean detached worktree) and **1881** at head, over 91 anchored files at both ends. Both runs exit 0. - **Why equal.** No comment inside the moved bodies, declarations or import cites a decision letter. The stubs keep their docblocks, so every `ADR-…` mention in them stays in `sql-driver.ts`. - **No entry needs a new home.** No anchor entry names a moved member or declaration. `check:adr-symbol-anchors`: 2683 resolve at both ends. - **After the merge.** `main` moved under the cut, so I measured its tip `dbf7e04adf` against the merge head `048467ad98`: **1872** letters at both (91 anchored files, exit 0), and 2687 symbol anchors resolve at both. The drop from 1881 comes from `main`'s own commits, not from this diff. ### The published `.d.ts` does not change This is measured under the series bar (ruling `6348891160`, item 1). - **How each end was built.** `pnpm turbo run build --filter=@objectstack/driver-sql --force` at each end, against the same dependency tree. The base leg writes base `sql-driver.ts` and 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 empty `git diff HEAD` and 0 status entries. - **The hashes.** The raw sha256 of `dist/index.d.ts` and `dist/index.d.mts` is 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 ruling `6441032442` does not come into play. - **Positive control.** I dropped `private` from `applyLike` in the head `index.d.ts`. The result hashes to `4450f3699c74b340a3c946f037a353be89d3c21fe80163eed2040a9a86dcb1ca`, with `cmp` exit 1. - **Export lists.** CJS `require` of the head dist gives 59 names (sha1 `05757ce5ae`); the ESM export statement of the base and head `index.mjs` lists the same 59 names (sha1 `05757ce5ae` both). None of the seven newly exported declarations, and nothing from the new module, is among them. - **The head dist carries the move.** `.call(host` sites in `index.mjs` go 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 `f9867020c6` against head `5ceec116b`. It is a scratch script and is not committed. | Moved body | Body tokens | Comments | Parameter tokens less `this` | Return type tokens | Body lines | |:--|--:|--:|--:|--:|--:| | `applyNormalizedComparison` | 432 | 5 | 27 | 1 | 62 | | `applyEmptyOperator` | 259 | 3 | 27 | 1 | 35 | | `applyJsonMembership` | 172 | 3 | 25 | 1 | 24 | | `applyNullSafeNegative` | 33 | 0 | 18 | 1 | 4 | | `applyLike` | 50 | 0 | 21 | 1 | 3 | | `applyLikePattern` | 46 | 0 | 15 | 1 | 3 | - **Each moved body.** In every row, 0 tokens differ and every comment is identical. Each body line is the base line less two leading spaces. None is `async`, at either end. - **The six stubs.** Leading trivia (the docblock), modifiers and the signature are identical to base. - **The class.** It has 350 members at both ends, in the same order, and the 344 unmoved members are byte-identical, trivia included. - **Module-level declarations.** The sixteen that moved are byte-identical in the module from their own docblock on, and gone from `sql-driver.ts`; five of them carry a leading `export` and nothing else. `LIKE_ESCAPE_CHARACTER` and `TextMatchShape` differ from base by a leading `export` and nothing else. At `sql-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. - **Result.** `PROOF OK`, exit 0. - **Negative control.** Through `scripts/ablation-replace.mjs`, I changed `'[$&]'` to `'[$&]]'` in the module's `escapeGlobComparand` (1 hit, checked on disk). The proof then reports `escapeGlobComparand` as differing, prints `PROOF FAILED`, and exits 1. The restore was proved (blob equals HEAD, empty `git diff HEAD`). ### Tests Each end ran under one `os-verify-lock` hold, after building that end's driver-sql dist. I compared the vitest JSON per test name and status, base `f9867020c6` against head `5ceec116b`, and then again on the merge head `048467ad98`. Every part's exit code was captured on its own (build, probe, three suites: all 0 at every leg). | Suite | Base | Head | Lost / changed / added | |:--|:--|:--|:--| | `@objectstack/driver-sql` | 4060 (3846 passed, 214 skipped; 248 files) | same | 0 / 0 / 0 | | `@objectstack/driver-turso`, against that end's built driver-sql dist | 2526 (2492 passed, 34 skipped; 95 files) | same | 0 / 0 / 0 | | `@objectstack/driver-sqlite-wasm` | 746 (746 passed; 39 files) | same | 0 / 0 / 0 | On the merge head (it brings in `main` up to `dbf7e04adf`), the three suites give the same totals and the same per-test statuses as base, 0 / 0 / 0 in each. - **Type check and lint.** `tsc --noEmit -p .` in driver-sql exits 0 on both heads. `pnpm exec eslint --no-inline-config packages/drivers/driver-sql` exits 0. - **Late-binding probe.** A scratch script, not committed. It runs node against each end's built dist and prints 676 readings, each with the result or the envelope's message, `code`, `status`, own keys and symbol-keyed members, the SQL and bindings left on the builder, and the log lines written: 1. `applyNormalizedComparison` over every operator on both joins, with null comparands and empty or malformed lists, on SQLite, Postgres, MySQL and an unknown dialect; 2. `applyNullSafeNegative` on both methods; 3. `applyEmptyOperator` on three row shapes, both polarities and both joins, an undeclared field and an unknown dialect (both refusals); 4. `applyJsonMembership` on a JSON and a scalar column, both polarities, scalar and list values; 5. `applyLike` on the three shapes, negated, folded, with metacharacters and a NUL; 6. `applyLikePattern` plain, with wildcards, escapes, a NUL, a GLOB metacharacter, and folded; 7. a SUBCLASS overriding `declaredValueShape`, `isJsonColumn` and `applyNullSafeNegative`, instance spies on each, and `dialectName` replaced on the instance after construction (the new ones are the ones reached); 8. `find`/`count` end to end on SQLite through the compiler, reaching each emitter; 9. bad arguments. - **Probe result.** 674 of the 676 readings are identical, SQL, bindings and envelopes included. The other 2 differ only in the top stack frame's receiver name, where a null builder throws inside a moved body: `_SqlDriver.X` at base and `Object.X` at 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. - **Not run.** No ablation, because no tested behaviour changed. - **`test:repo`.** Not run locally; it is CI's run on this PR. ### Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives **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. - **Merge head `048467ad98`: 54 run, all 54 exit 0.** `--ran` reconciles: `54 derived famil(ies) accounted for — 54 run, 0 NOT-MEASURED`, exit 0. - **Move commit `5ceec116b`:** 53 exited 0; `check:dual-build-cjs-loads` exited 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). - **Hand gates on the merge head, all exit 0:** `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`. - **No gate silently empty.** Each derived gate's log prints a non-zero population (for example `check:logger-receiver-detach` walks 3779 files and fires on its control corpus; `check:tenant-chokepoint` reads 24 `getBuilder()` 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 - **Other stale links from cut 15 are left.** Cut 15 also moved `unresolvableFilterColumnError` (3 links), `backendStatementFaultError` (3) and `unresolvableColumnProvenance` (2), which staying docblocks in `sql-driver.ts` still name. The order folded in only the `unresolvableColumnNameOf` links, so those stay as they were. On a scan of `{@link X}` names that are neither a top-level binding nor a `SqlDriver` member, `sql-driver.ts` goes from 34 at base to 30 at head (the four fixed); this cut adds none. In the new module, one comment inside the moved `applyNormalizedComparison` body names the class member `filterColumnExpr`, byte-identical to base. Noted, not filed: the same type-import repair would close them in one later line. - **`check:tenant-chokepoint`'s `SCAN_FILES` does not list the new module.** The moved bodies hold 0 of the file's 4 tenant predicates and build no query. Noted, not filed: widening `SCAN_FILES` would be a gate change. - **Next slice, priced on this base.** The rotation sub-banner is now the largest clean area (`ensureRotation`, `registerExternalObject`, `warnBuiltinColumnCollisions`, `rotationShardKey`, `rotationColumnList`, about -120), with `migrateMysqlDatetimeColumns`/`migrateMysqlTimeColumns` (durability trap), `ensureShardTable` (named by `check-object-def-param-keys.mjs`) and `computeTenantField` (named by `packages/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 - [x] 1. No `packages/spec` change. - [x] 2. The one new name, `PredicateEmitterHost`, follows the interface casing. - [x] 3. No formatter ran; scoped `eslint --no-inline-config` passes (repo-wide `pnpm lint` is CI's run). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. No publishable behaviour change: `skip-changeset`; both declaration lines above. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to generated docs, changelogs or release notes. - [x] 8. Title, body and comments are in English (the maintainer brief excepted). --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5ncyQ2HYfpr45hwrPU)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FEkQ5ncyQ2HYfpr45hwrPU Co-authored-by: Claude <noreply@anthropic.com>
1 parent 74db43e commit a485b5a

3 files changed

Lines changed: 898 additions & 747 deletions

File tree

‎packages/drivers/driver-sql/src/sql-driver-compile-refusal-seam.test.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929
*
3030
* 1. **The enumeration** reads `sql-driver.ts` and its cross-field seam module
3131
* `sql-driver-cross-field-comparison.ts` (#25521 cut 13), plus the read-exit
32-
* fault module `sql-driver-read-exit-fault.ts` (#25521 cut 15), and lists every
33-
* function that builds an `INVALID_FILTER` error. Only three may call
32+
* fault module `sql-driver-read-exit-fault.ts` (#25521 cut 15) and the
33+
* predicate-emitter module `sql-driver-predicate-emitters.ts` (#25521 cut 16),
34+
* and lists every function that builds an `INVALID_FILTER` error. Only three may call
3435
* `unsupportedFilterError` directly (the seam itself, the `'author'`
3536
* re-issue, and the column refusal raised from a dialect error AFTER the
3637
* statement ran), and every caller of `withheldFilterError` must have a row
@@ -66,7 +67,7 @@ const SECRET_NUM = 7770123;
6667
const UNDECLARED_KEY = '$psecret_combinator';
6768

6869
type Door = {
69-
/** The builder in `sql-driver.ts` or its cross-field seam module this row drives — asserted by the error's stack. */
70+
/** The builder in `sql-driver.ts` or one of its seam modules this row drives — asserted by the error's stack. */
7071
readonly builder: string;
7172
readonly where: () => Record<string, unknown> | unknown[];
7273
readonly secrets: readonly string[];
@@ -273,8 +274,8 @@ const DOORS: readonly Door[] = [
273274
// ── Half 1: the enumeration ───────────────────────────────────────────────────
274275

275276
/**
276-
* Every function in `sql-driver.ts`, `sql-driver-cross-field-comparison.ts` and
277-
* `sql-driver-read-exit-fault.ts` that builds an `INVALID_FILTER` error, by the route it takes. Read with the TypeScript parser, not a regex, so a call
277+
* Every function in `sql-driver.ts`, `sql-driver-cross-field-comparison.ts`,
278+
* `sql-driver-read-exit-fault.ts` and `sql-driver-predicate-emitters.ts` that builds an `INVALID_FILTER` error, by the route it takes. Read with the TypeScript parser, not a regex, so a call
278279
* split across lines, an inline `throw`, or a builder written as a method or an
279280
* arrow cannot slip past the count.
280281
*/
@@ -287,10 +288,13 @@ function enumerateRefusalBuilders(): {
287288
// its own seam module with two of the builders below; the scan reads both.
288289
// [#25521 cut 15] The read exits' fault terminals moved to a third, with
289290
// `unresolvableFilterColumnError`; the scan reads it too.
291+
// [#25521 cut 16] The leaf predicate emitters moved to a fourth, with the two
292+
// `$empty` refusals; the scan reads it too.
290293
const sources = [
291294
['sql-driver.ts', readFileSync(new URL('./sql-driver.ts', import.meta.url), 'utf8')],
292295
['sql-driver-cross-field-comparison.ts', readFileSync(new URL('./sql-driver-cross-field-comparison.ts', import.meta.url), 'utf8')],
293296
['sql-driver-read-exit-fault.ts', readFileSync(new URL('./sql-driver-read-exit-fault.ts', import.meta.url), 'utf8')],
297+
['sql-driver-predicate-emitters.ts', readFileSync(new URL('./sql-driver-predicate-emitters.ts', import.meta.url), 'utf8')],
294298
] as const;
295299
const directCallers = new Set<string>();
296300
const seamCallers = new Set<string>();

0 commit comments

Comments
 (0)