Skip to content

refactor(driver-sql): move the catalog-introspection helpers out of sql-driver.ts into sql-driver-introspection.ts (#25521 cut 1) - #25614

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-25521-sql-driver-cut-1
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-25521-sql-driver-cut-1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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:
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

…ql-driver.ts into sql-driver-introspection.ts

Move-only: the bodies of introspectColumnOrder, introspectColumns,
introspectForeignKeys, introspectPrimaryKeys and introspectUniqueConstraints
move unedited into a new module. The class keeps each method with its name,
docblock and signature (less `async`); each body is now one host literal of
exactly the IntrospectionHost members it reads plus `fn.call(host, ...)`.
knex and the three dialect getters go as getters; introspectColumns reaches
introspectColumnOrder through a late-bound closure, so TursoDriver's override
and test spies keep working.

Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql, touching 13 documentable anchor(s).

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-2.mdx (via introspectForeignKeys (symbol, a method of class SqlDriver; a top-level function), introspectPrimaryKeys (symbol, a method of class SqlDriver; a top-level function), introspectUniqueConstraints (symbol, a method of class SqlDriver; a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e649c2d91e270e3da26820bcfe54a28445305a75 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9464a3b2c055098a1d391bf6071bc0f4399a54a9 — the merge of head 021043d5785a5a7d0f0978a3302b2e4f84037e91 into base e649c2d91e270e3da26820bcfe54a28445305a75, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9464a3b2c055098a1d391bf6071bc0f4399a54a9 && git checkout 9464a3b2c055098a1d391bf6071bc0f4399a54a9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e649c2d91e270e3da26820bcfe54a28445305a75 021043d5785a5a7d0f0978a3302b2e4f84037e91 && git checkout -B drift-repro e649c2d91e270e3da26820bcfe54a28445305a75 && git merge --no-ff 021043d5785a5a7d0f0978a3302b2e4f84037e91

node scripts/docs-audit/affected-docs.mjs --json e649c2d91e270e3da26820bcfe54a28445305a75

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e649c2d91e270e3da26820bcfe54a28445305a75 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-24T09:35Z · head 021043d578 · claim 6382121925 · report 6382583487

Checklist conclusions (read on GitHub, not from the report):

  • Shape: draft, base main, author objectstack-fleet[bot], assignee sunhaolin. The first line is Part of #25521, and no closing keyword appears anywhere in the body, so the series card stays open. Clause-②: no and Metadata-protocol: none are line-initial.
  • Scope: 2 files, both in packages/drivers/driver-sql/src/ (sql-driver.ts +73/-556, sql-driver-introspection.ts new +665). No content/docs/releases/, no governed path (check-governed-merges --pr 25614: 0 of 2 paths governed; 1,294 changed lines ≤ 3000).
  • Changeset: skip-changeset under ruling 6348891160 (move-only, no observable published change), as on the sibling slices. The claim's "one changeset" was the seat's over-declaration; the dev followed the ruling (deviation 2).
  • Move-only, re-measured by the seat: I extracted the five bodies at base e649c2d91 (sql-driver.ts:21407, :21449, :21513, :21764, :21940) and in the new module (:64, :100, :156, :396, :522). Whitespace-insensitive comparison: all five identical (833 / 1593 / 8782 / 3786 / 4746 significant chars). The head stubs keep the docblocks and signatures less async, and delegate through IntrospectionHost. introspectColumnOrder reaches the host as a late-bound closure, so a subclass override still dispatches.
  • .d.ts bar (ruling 6348891160): the report gives raw sha256 base = head for index.d.ts and index.d.mts, with a positive control in the same block (introspectColumns made public changes the hash), and 56/56 ESM and CJS exports. The ruling's normaliser is identity on single-quoted unions, so raw byte identity carries the proof (deviation 3). Not re-built by the seat.
  • Tests: the counts are identical at base and head for driver-sql (3787 passed / 212 skipped) and driver-turso (2492 / 34), with 0 tests lost, changed or added by name. check:driver-conformance covered 55 / DEBT 0 at both. These are local readings; CI decides.
  • CI on head 021043d578 at this reading: 22 success, 12 skipped, 10 in progress (Build Core, Test Core 1–6/6, Type Check · workspace / debt ledger / consumer gates). None red. The seat lands only when every check is green or an expected skip.
  • Writes: mcp_calls 0. Both strokes went through the relay as the fleet.
  • drivers(sql): upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740: insertOnlyUpsertColumns stays in sql-driver.ts, byte-identical (now :9241). The hold's restart line needs no re-pointing.

Deviations accepted: 1 (commit trailers in the AGENTS.md form), 2, 3, 4 (no anchor entry owed: no anchored paragraph describes a moved body), 5–8 as reported.

Carried to the next slice of #25521 (not filed): a cut in a getBuilder-building area first needs check:tenant-chokepoint's SCAN_FILES to cover the new module, or the gate narrows silently; protected statics hold the hash-shadow and row-size families in the class. The dev proposes Raw execution as cut 2.

Landing: pr_ready + automerge_enable through the relay once every check on this head is green. After it merges, #25521 goes back to pm:queue for cut 2, and the series stays open.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants