Skip to content

Commit f7b0298

Browse files
refactor(driver-sql): move the catalog-introspection helpers out of sql-driver.ts into sql-driver-introspection.ts (#25521 cut 1) (#25614)
Part of #25521 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Ruling-ref: 6348891160 Cut 1 of the `sql-driver.ts` split (#24586 ruling A, recorded in `6270097840`), with the fresh seam table the card asks for first. The claim is `6382121925`. Five `SqlDriver` method bodies move unedited into a new module, `packages/drivers/driver-sql/src/sql-driver-introspection.ts`: the protected catalog-introspection helpers `introspectColumnOrder`, `introspectColumns`, `introspectForeignKeys`, `introspectPrimaryKeys` and `introspectUniqueConstraints`. It is move-only, and the card stays open for the later cuts. Branch `claude/issue-25521-sql-driver-cut-1`, cut from `origin/main` at `e649c2d91`. **Serial with the file's in-flight PRs.** At claim the PM read 13 open PRs, none touching the file. Re-measured at my start (12 open PRs) and again just before this PR was opened (13 open PRs), each PR's file list read in full: none touches `packages/drivers/driver-sql/src/sql-driver.ts` or `packages/drivers/driver-turso/src/turso-driver.ts`. `origin/main` did not move between the base and this PR. ## Seam table (fresh, at `e649c2d91`) `sql-driver.ts` is 22,158 lines. 30-day lines are `git blame --line-porcelain` lines whose committer time is on or after 2026-09-24T08:02Z, 30 days before the base commit; history was deepened to 2026-09-15 first, and `scripts/pm/git-history.mjs count` proves 67 commits on the file in that window. In all, 3,790 lines (17.1%) are from the last 30 days. The class `SqlDriver` spans lines 5471-22083 with 347 members: 251 methods (49 public, 202 private or protected), 32 statics. One public member has an inferred return type (`supports`, in the class head). Sections are the class's own banner comments, cut by contiguous line range: | Lines | Section | Lines | 30-day lines | Members (public fn) | |:--|:--|--:|--:|:--| | 1-5470 | Module preamble: imports, types, top-level helpers (197 statements) | 5,470 | 1,394 | - | | 5471-7062 | Class head: fields, statics, constructor, capability getters (`supports`) | 1,592 | 290 | 88 (4) | | 7063-7494 | Lifecycle | 432 | 155 | 15 (3) | | 7495-10173 | CRUD (IDataDriver core) | 2,679 | 691 | 45 (7) | | 10174-10785 | Bulk and batch operations | 612 | 140 | 9 (6) | | 10786-10911 | Raw execution | 126 | 12 | 4 (1) | | 10912-10989 | Transactions | 78 | 0 | 7 (5) | | 10990-11521 | Aggregation | 532 | 61 | 5 (1) | | 11522-11809 | Distinct | 288 | 20 | 3 (1) | | 11810-11925 | Window functions | 116 | 4 | 1 (1) | | 11926-12024 | Query plan analysis | 99 | 1 | 2 (2) | | 12025-15779 | Schema sync (syncSchema / init) | 3,755 | 373 | 62 (17) | | 15780-15915 | Schema introspection (`listPhysicalColumns`, `introspectSchema`) | 136 | 65 | 2 (2) | | 15916-21385 | Internal helpers: tenancy, filter compilation, read presentation, DDL emitters, hash shadows, `formatInput` / `formatOutput` | 5,470 | 565 | 99 (5) | | **21386-22083** | **Internal helpers: catalog introspection (this cut)** | **698** | **19** | **5 (0)** | | 22084-22158 | After the class: `UniqueConstraintMember`, `singleColumnUniqueColumns` | 75 | 0 | - | Limits measured per area, for pricing the next cuts: - **`check:tenant-chokepoint` scans `SqlDriver` methods for `this.getBuilder(` read builders.** Aggregation, Distinct, Window functions, Query plan analysis, CRUD and Bulk build through `getBuilder`, so a cut of any of them must also carry the new module into that gate's `SCAN_FILES` (and teach it module-level functions with a `this` parameter), or the gate would go silently narrower. That is a gate edit; it was kept out of cut 1. - **Protected statics.** The hash-shadow family (19116-19628) reads `SqlDriver.hashShadowColumnFor` and `SqlDriver.MAX_KEYABLE_VARCHAR_CHARS`; the row-size explainers (19682-19977) read four protected statics. A module function cannot read a protected static, so those bodies stay unless a ruling says otherwise. - **`this.FIELD.delete/insert/update(`.** One site in the whole class: `releaseTransaction` (`this.activeTransactions.delete(`, a `Map`), in Transactions; three in Schema sync. `driver-sql` is outside the tenant-audit census corpus (`SURFACE_ROOTS` is `packages/services` and `packages/plugins`), so the `UNTYPED_RECEIVERS` limit does not bite here; cut 1 has no such site either way. **Proposed cut 2: Raw execution (10786-10911, 126 lines, 12 from the last 30 days).** `rawStatementFault`, `execute` (public, annotated `unknown`-promise return) and `noteRuntimeIndexDdl`. They read no `getBuilder` and no static; the field `runtimeCreatedIndexes` stays on the class. `TursoDriver` overrides `execute` and calls `rawStatementFault`, which the stub shape keeps working. The bigger cold areas (Distinct, Aggregation, Query plan analysis) need the `check:tenant-chokepoint` question answered first. ## Why this area for cut 1 The five helpers form a closed cluster at the tail of the class: 693 lines of methods, 19 from the last 30 days, no public member, every return type annotated, no static, no `this.FIELD.delete/insert/update(` site, no `getBuilder`. Between them they read only `knex`, `isPostgres`, `isMysql`, `isSqlite` and each other (`introspectColumns` calls `introspectColumnOrder`). Because the cluster ends the class, no line above 21386 moves, so no line reference into the file above that point shifts. They are also among the file's best-covered members: 17 test files in this package name them, most reaching the protected methods through a test subclass, and `driver-turso` overrides `introspectColumns` and calls `super`. ## The shape (#23050 shape B, as #25467 cut 1 / PR #25494 applies it) - The class keeps each method with its name, signature, docblock and position, because tsup emits members with their JSDoc into the `.d.ts`. Each body is now one host literal plus `fn.call(host, ...)`, marked `[#25521 cut 1]`. The methods drop `async` and return the moved function's promise. - `IntrospectionHost` is new. It types every member as the class member itself (an indexed access on `SqlDriver`). Each moved function types its `this` as the `Pick` of the host over exactly the members its body reads (measured from the body, and asserted by the generator), and each stub annotates its literal as the `ThisParameterType` of the moved function. No cast, and no member becomes public. - `knex` (a field) and the three dialect getters go as getters, so each read happens when the body makes it. `introspectColumnOrder` goes as a late-bound closure, so a subclass override or a spy on the instance is still the one `introspectColumns` reaches. - `this.knex(tableName)` now calls knex with the host as its receiver; knex's callable reads its own closure (`createQueryBuilder(knex.context, ...)` in `make-knex.js`), never its receiver. - **Module-level declarations.** The bodies read `UniqueConstraintMember`, `singleColumnUniqueColumns`, `IntrospectedColumn` and `IntrospectedForeignKey`. All four stay in `sql-driver.ts`, which already exports them (a test imports `singleColumnUniqueColumns` from there), and the module imports them back. The one value import is read only inside a function body, so the module cycle is evaluation-safe. No `export` was added anywhere. - The move was produced by a script from the source text, not retyped: each method's text from its name onward, re-indented two columns, except lines that continue a template literal, which keep their bytes because there the indentation is SQL text. ## The published `.d.ts` does not change Under the series bar (ruling `6348891160`, item 1), measured on base `e649c2d91` and on head `021043d57`, both built against the same dependency tree (`turbo run build --filter=@objectstack/driver-turso...` at base, 8/8 tasks, 0 cached; then `pnpm --filter @objectstack/driver-sql build` at head). - This package emits `dist/index.d.ts` and `dist/index.d.mts` (there is no `.d.cts`); the two are identical to each other at both ends. - Raw sha256 of both files, base and head, all four the same: `93da45168e82136b7613944525e47402d5dc6f9f8daa9efa4bdceeaa11b52a89`. The raw diff is 0 lines. - Normalised sha256, base and head the same: `93da45168e82136b7613944525e47402d5dc6f9f8daa9efa4bdceeaa11b52a89`. The normaliser is the ruling's, verbatim, run from `packages/drivers/driver-sql`: ```sh node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8");process.stdout.write(s.replace(/"[^"\n]*"(?:\s*\|\s*"[^"\n]*")+/g,r=>r.split(/\s*\|\s*/).sort().join(" | ")))' dist/index.d.ts | sha256sum ``` - Note: this `.d.ts` spells its string-literal unions with single quotes (31 such unions, 0 double-quoted), so the normaliser rewrites nothing here and normalised equals raw. The proof therefore rests on the raw identity, which is stronger. - Positive control on a copy of the head `dist/index.d.ts`: line 8165, `protected introspectColumns(...)`, made public gives raw and normalised `e85d733d6d77d1a644666cc45167bf25bd4f1e13bef0a80af977a469339b5823`, different from the real hash. - Export tables: the ESM and CJS bundles each export the same 56 names at base and head, loaded at runtime (`import()` and `require` of the built bundles). The sorted list hashes `d61a9dd3a02661602b1bb2dbe18152ac43986148f36b2746f40c0884bd8ffdec` on all four. - The head build carries the move: `dist/index.mjs` and `dist/index.js` each contain the 5 host `.call(` sites. No changeset: `skip-changeset`, per ruling `6348891160` item 4. ## Proof of move-only - **Bodies, by AST token** (recursive `getChildren` leaves): each moved function's parameters (less the added `this`), return type and body have the same token sequence as the base method; `async` matches on all five. 1,546 tokens compared, 0 differ. The only tokens whose text moved are the three parameter-level JSDoc comments on `onFailure`, equal after removing exactly two columns per continuation line. - **Lines**: 591 lines compared; each is the base line with two leading spaces removed, or (86 lines) byte-identical where the base line continues a template literal. - **Negative control**: one changed token in a template literal of `introspectColumnOrder` makes the check report a difference, at token and at line level. - **Class side**: 347 members at base and head. The 342 members that did not move are byte-identical to base, leading trivia included. Each of the 5 stubs keeps its docblock and signature byte-identical, less `async`. - **Top level of `sql-driver.ts`**: all 199 base statements other than the class are byte-identical, in order. The one new statement is the import of the new module. - **Size**: +738 / -556 over 2 files, under the 3,000-line manual-merge line. `sql-driver.ts` goes from 22,158 to 21,675 lines (each stub keeps its docblock and carries its host literal); `sql-driver-introspection.ts` has 665. ## Readers of `sql-driver.ts` by path, checked against the moved text None needed an edit; each was read against the moved region (lines 21386-22083) and run at both ends where it is a gate or test: - **ADR anchors.** `scripts/adr-anchors/packages__drivers__driver-sql__src__sql-driver.ts.json` carries ADR-0053 (`formatOutput`, `presentReadValue`) and ADR-0120 D4; neither paragraph describes a moved body, so per card ask 3 the new module gets no entry of its own. The moved text cites only a bare `ADR-0015` (no decision letter), which is not in the entry. `check:adr-anchors`: 84 anchored files at base and head; decision-letter citations on the anchored surface 1,883 at base and 1,883 at head. - **Symbol anchors.** ADR and platform-checklist anchors name `sql-driver.ts#introspectSchema` (unmoved); no anchor names a moved member, and the stubs keep every name anyway. `check:adr-symbol-anchors`: 2,384 anchors resolve at both ends. - **Source scans**, none of whose patterns occurs in the moved text: `logger-receiver-detach.test.ts` (the moved text has no `logger` read), `sql-driver-compile-refusal-seam.test.ts` (no `INVALID_FILTER` builder), the five `packages/cli` `generate-*.pin.test.ts` files (`createColumn` and the varchar constants), `check-keyed-text-bounds` (`varcharColumnChars`), `check-tenant-chokepoint` (24 `getBuilder()` bindings across 3 files at both ends; none in the moved text), `check-object-def-param-keys` (`TursoDriver`'s override of `introspectColumns` still matches the stub's signature). - **Path lists** that name the file for cache inputs or fixtures (`turbo.json`, `scripts/cross-package-test-inputs.mjs`, `scripts/pm/check-half-states.mjs`, `scripts/check-changeset-no-major.mjs`, `packages/spec/liveness/*.json` evidence): the readers they serve read `createColumn`, the varchar constants or unmoved symbols. - **`driver-turso`**: its only reader of a moved member is `introspectColumns`, an override that calls `super.introspectColumns`; it reaches the stub and is unchanged. Its suite was run at both ends (below). - `check:driver-conformance` at base and head: 55 covered, 0 in the DEBT ledger, 0 exempt, both times. ## Verification - `tsc --noEmit` (driver-sql) exit 0; `eslint --no-inline-config` on both source files exit 0 (base file also exit 0). - `packages/drivers/driver-sql` suite: base 3,999 tests (3,787 passed, 0 failed, 212 skipped; 247 files); head the same. Compared per test name and status from vitest's JSON reporter: no test lost, none changed status, none added. - `packages/drivers/driver-turso` suite (run against the rebuilt head `dist`): base 2,526 tests (2,492 passed, 0 failed, 34 skipped; 95 files); head the same, per test name and status no difference. - `dispatch-gates --commands` on this diff: 55 derived commands, each run at head with its exit code; `--ran` reconciles 55 derived, 54 run, 1 NOT MEASURED, 0 unrun. The NOT MEASURED one is `check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT MET: it needs every package's `dist`); CI runs it. `check-plugin-teardown-shape --self-test` and `check:lean-entry-closure` first answered exit 3 for prerequisites (a pinned fixture commit outside the shallow clone; `objectql`'s `dist`), and exit 0 after fetching that commit and building `objectql`. - No reverse verification or ablation: the diff changes no tested behaviour. ## Acceptance notes - The claim's file surface lists "one changeset"; ruling `6348891160` item 4 says no changeset and `skip-changeset` for a cut that meets item 1, and the dispatch says the same. This PR follows the ruling. - The dispatch's assumption A2 (an `adr-anchors` entry per new module) is the reference PR's shape, not a rule; card ask 3 makes the entry conditional on a paragraph describing a moved body, and none does here. --- _Generated by [Claude Code](https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7265b33 commit f7b0298

2 files changed

Lines changed: 738 additions & 556 deletions

File tree

0 commit comments

Comments
 (0)