Skip to content

Commit bd28708

Browse files
refactor(driver-sql): move resolveHashShadowKeys' body out of sql-driver.ts into sql-driver-introspection.ts (#25521 cut 7) (#26114)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X6BFuP3Yn7kQNxWcwpUBnAP)_ Part of #25521 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Ruling-ref: 6348891160 Cut 7 of the `sql-driver.ts` split (#24586 ruling A, recorded in `6270097840`). The claim is `6415078284`. The body of the protected `SqlDriver#resolveHashShadowKeys`, the MySQL hash-shadow enrichment of the index read, moves unedited into `packages/drivers/driver-sql/src/sql-driver-introspection.ts`, beside `introspectIndexes`, its only caller (cut 5). It reaches the class through that module's existing host, `IntrospectionHost`, which already lists `knex` and `resolveHashShadowKeys`, so **the host is not widened**. The method keeps its name, docblock, signature (less `async`) and position. It is move-only, and the card stays open for the later cuts. Branch `claude/project-thread-3jc3ll` (platform-assigned to this card thread, as the claim records). The cut commit `0b01e18ba` sits on `origin/main` `b4199c968`; seat 1's #26015 (`SqlDriver.upsert`) then landed, and `origin/main` was merged in (`156e3538f`, merge base `0a6948210`, clean, no conflict). Every reading below was taken on both pairs: base `b4199c968` vs head `0b01e18ba`, and base `0a6948210` vs merged head `156e3538f`. **Net line count (the seat's rule from ACCEPT `6385306916`).** `sql-driver.ts` goes from 21,449 lines to 21,431 on the merged pair (21,398 to 21,380 on the first pair): **-18**. The body is 25 lines out; the stub is 6 lines in; the introspection import gains one name (+1) and its comment one line (+1); `isHashShadowCarrier` leaves the `schema-drift.js` import (-1), since the moved body was its last value read in the file. The module grows 927 to 975 lines. The slice pays alone, so no hash-shadow neighbour was paired with it. **Serial with the file's in-flight PRs.** Re-measured at my start: 40 open PRs, each file list read. The only one on `packages/drivers/driver-sql/**`, `driver-turso/**` or `driver-sqlite-wasm/**` was #26015 (upsert region at lines ~2256 and ~9495-10078, plus one import line after `unencoded-json-text.js`); this cut's region is the introspection stub (~15134) and the import lines 126 and 163-173. `git merge-tree` of the cut against #26015's head `b4fc36f35e` was clean; #26015 then landed first and this branch merged `main` after it, as card ask 4 says. The Version Packages PR #25640's file list could not be paged through the proxy (HTTP 403 on the redirected page); it has carried only `CHANGELOG.md` and `package.json` changes for these packages in every earlier cut. ## The area, measured on `0a6948210` - `resolveHashShadowKeys` is lines 15134-15163 of `sql-driver.ts` (a 28-line docblock above it, 25 body lines). Its only caller is cut 5's `introspectIndexes` body in `sql-driver-introspection.ts`, through the host closure. - Protected, with an annotated return type (a promise of `void`), so the inferred-return limit does not bite. - It does not build through `getBuilder` (it reads the catalog through `this.knex.select(...).from('information_schema.COLUMNS')`), so `check:tenant-chokepoint` rule 1 is untouched. - No static read and no cast-wrapped `this` read (AST screen): the only member it reads is `knex` (twice). It has no `this.FIELD.delete/insert/update(` site. - `TursoDriver` and `SqliteWasmDriver` do not override it (`git grep` over both packages: 0 hits), and no test, script, doc or ADR names it (`git grep` over `docs/`, `content/`, `scripts/`, `.github/`: 0 hits). ## The shape (#23050 shape B, as cuts 1-6 apply it) - The class method keeps its docblock and signature, byte-identical less `async`. Its body is one host literal plus `resolveHashShadowKeys.call(host, tableName, indexes)`, marked `[#25521 cut 7]`, and it returns the moved function's own promise. - `knex` goes as a GETTER that reads the driver at call time (the shape cut 5's stub uses), so a `knex` replaced on the instance after construction is the one the body reads. - The moved function types its `this` as the `Pick` of `IntrospectionHost` over `knex` alone, and the stub annotates its literal as the `ThisParameterType` of the moved function. No cast, and no member becomes public. - `introspectIndexes`' body still calls `this.resolveHashShadowKeys(...)` through its host closure, which calls the class method on the driver. A subclass override or an instance spy is still the one reached (probe below). - **Module-level declarations.** The body reads `isHashShadowCarrier` and `parseHashShadowKeyParts`, both exported by `schema-drift.ts`, so the module imports them from there (not a copy). `sql-driver.ts` still reads `parseHashShadowKeyParts` (two other call sites) and keeps it; it no longer reads `isHashShadowCarrier` as a value (two prose mentions in comments remain) and drops it from its import. No declaration gains an `export`; `index.ts` re-exports nothing from the module. - The module header gains a `[#25521 cut 7]` paragraph; the `sql-driver.ts` import comment names cut 7. - The move was produced by a script from the source text, not retyped: each body line re-indented two columns. The body holds no template literal. ## The published `.d.ts` does not change Under the series bar (ruling `6348891160`, letter A). Each end built with `pnpm --filter @objectstack/driver-sql build` against the same dependency tree; the base legs restore the base text of the two files in this worktree after the commits, under an EXIT/INT/TERM trap that restores HEAD (proved by HEAD blob hashes, an empty `git diff HEAD` and 0 status entries). - This package emits `dist/index.d.ts` and `dist/index.d.mts` (no `.d.cts`); the two are identical at every end. - First pair: raw sha256 of both files at base `b4199c968` and head `0b01e18ba` is `93da45168e82136b7613944525e47402d5dc6f9f8daa9efa4bdceeaa11b52a89` (cuts 1-6's hash); `cmp` identical. - Merged pair: #26015 changed the published surface, so the hash moved for both ends together: base `0a6948210` and merged head `156e3538f` are both `5028e040e1501b0bd72b91fafcf4e1cb372393c59baa5bab8b936b240dd2d3f5`; `cmp` identical. - Positive control on a copy of the first-pair head `dist/index.d.ts`: line 5750, the `protected resolveHashShadowKeys(tableName: string, indexes: PhysicalIndex[])` declaration, with `protected` removed, gives `766c5f27828eae19be82213dcfedb1fd5f4e52239fb3d6435fc9e5fa0c2f121f` and a 4-line diff; `cmp` exits 1. - Export tables: the ESM and CJS bundles each export the same 56 names at every end (sorted lists `diff`-identical). - The head build carries the move: `.call(host` sites in `dist/index.mjs` and `dist/index.js` go 18 to 19. No changeset: `skip-changeset`, per ruling `6348891160` item 4 (move-only, no product behaviour change). ## Proof of move-only TypeScript AST leaves (recursive `getChildren`, JSDoc skipped), base `0a6948210` vs merged head `156e3538f` (and the same on the first pair): - **Body**: 220 tokens, 0 differ; 1 comment, identical; parameters (less the added `this`) 8 tokens, 0 differ; return type 4 tokens, 0 differ; `async` matches. - **Lines**: 27 body lines (braces included), each the base line with two leading spaces removed. - **Negative control**: in a copy of the module, `'GENERATION_EXPRESSION'` in the `.select(...)` becomes `'GENERATION_EXPRESSI0N'`. The check reports 1 token and 1 line different and exits `PROOF FAILED`, exit 1. - **Class side**: 348 members at both ends, same order; the 347 that did not move are byte-identical, leading trivia included; the stub's leading trivia and signature equal the base's, less `async`. - **Top level of `sql-driver.ts`**: 207 statements at both ends; 204 byte-identical. The three that differ are the `schema-drift.js` import (`isHashShadowCarrier` out), the `sql-driver-introspection.js` import (`resolveHashShadowKeys` in, its comment rewrapped) and the class (the one member above). - **Top level of the module**: 11 to 12 statements; the first import differs only by the header paragraph in its leading trivia, the `schema-drift.js` import gains two names, and the new statement is the moved function. ## Readers of `sql-driver.ts` by path, checked against the moved text None needed an edit: - **ADR anchors.** `scripts/adr-anchors/packages__drivers__driver-sql__src__sql-driver.ts.json` carries ADR-0053 and ADR-0120 D4; neither paragraph describes this body, and the body cites no ADR, so no entry is owed. `check:adr-anchors`: 88 anchored files at both ends; decision-letter citations on the anchored surface **1,878 at base (`0a6948210`, clean detached worktree) and 1,878 at head (`156e3538f`)**. - **Symbol anchors.** No anchor names `resolveHashShadowKeys`. `check:adr-symbol-anchors`: 2,658 anchors resolve at both ends. - **`check:tenant-chokepoint`.** 24 `getBuilder` bindings across 3 files at both ends, rule 2 clean at both. Rule 2's `SCAN_FILES` names `sql-driver.ts` and not the seam modules, so it no longer visits the body's one `.where({ TABLE_SCHEMA, TABLE_NAME })` and one `.whereIn('COLUMN_NAME', ...)` (catalog filters, no null comparand, no tenant column); `where`/`whereIn`/`whereRaw`/`raw` call lines in `sql-driver.ts` go 100 to 98. The verdict and the population the gate prints are unchanged, so it does not go empty. - **Logger receivers.** The body does not log. `check:logger-receiver-detach` exits 0 at both ends. - **Text readers.** The CLI `*.pin.test.ts` files, `logger-receiver-detach.test.ts`, `sql-driver-compile-refusal-seam.test.ts`, the spec liveness test and the scripts that read `sql-driver.ts` as text match none of `isHashShadowCarrier`, `parseHashShadowKeyParts`, `HashShadow` or `shadowKey` (`git grep`: 0 hits). - `check:driver-conformance` at both ends: **55 covered, 0 in the DEBT ledger, 0 exempt.** ## Verification - `tsc --noEmit` (driver-sql) exit 0. `eslint --no-inline-config packages/drivers/driver-sql/src` exit 0 (driver-sql declares no `lint` script; the repo-wide `pnpm lint` is CI's). - Each suite under its own `os-verify-lock` call, exit 0 at every end, compared per test name and status from vitest's JSON reporter: - `driver-sql`: first pair 3,999 tests (3,787 passed, 212 skipped; 247 files); merged pair 4,010 (3,796 passed, 214 skipped; 248 files, #26015's new file). 0 lost, 0 changed, 0 added on each pair. - `driver-turso` (against that end's built driver-sql `dist`): 2,526 tests (2,492 passed, 34 skipped; 95 files) at every end; 0 / 0 / 0 (2,513 distinct names). - `driver-sqlite-wasm`: 704 tests (704 passed; 39 files) at every end; 0 / 0 / 0. - **Late-binding probe** (scratch, node against the built `dist`, not committed): 17 readings, **identical at base and head on both pairs** (`diff` empty). It covered: a direct call with a fake `knex` set on the instance after construction (the same builder chain, `information_schema.COLUMNS`, the `database()` read, the carrier columns in `whereIn`, and the parsed `shadowKey` per carrier, NULL-safe and raw, plus an unresolved carrier and a plain index left alone); no carrier (returns without touching `knex`); a synchronous throw in the builder and an async rejection (both swallowed, resolves `undefined`); a non-array argument (a rejection with a `TypeError`, never a synchronous throw, at both ends); a subclass override of `resolveHashShadowKeys` reached from `introspectIndexes`' MySQL arm (via subclass dialect getters and a `knex.raw` stub); the real method through that arm; and an instance spy on it, reached once. - **Live MySQL-dialect cell**: the four hash-shadow live files (`11627`, `12998`, `13015`, `15479`) ran against a local MariaDB 10.11 at base `0a6948210` and merged head `156e3538f`: 31 tests at both ends (12 passed, 18 failed, 1 skipped), **0 status differences**. MariaDB does not take the shadow path these files assert on (the failures name a missing shadow column or a missing shadow-path log line, identically at both ends), so this cell does not reach the moved body; it is MariaDB, not MySQL 8, and only shows the diff changes nothing there. CI's live MySQL lane, where it runs, is the authority. - `dispatch-gates --commands` on this diff at the merged head `156e3538f` (merge base `0a6948210`, no STALE TREE): 55 derived commands, each run with its exit captured before any pipe. `--ran`: 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`), which CI runs. `check-plugin-teardown-shape --self-test` first exited 3 (shallow clone) and exits 0 after fetching its positive-control commit; `check:lean-entry-closure` first exited 3 (no `objectql` dist) and exits 0 after building it. - Also run, outside the derivation: `check:adr-anchors`, `check:adr-symbol-anchors`, `check:commit-card-trailers` and its `--range origin/main..HEAD` (2 commits, clean): all exit 0. - No reverse verification or ablation: the diff changes no tested behaviour. ## Acceptance notes - **`check:tenant-chokepoint` rule 2 visits two fewer catalog filters in `sql-driver.ts`** (above). Widening `SCAN_FILES` to the seam modules is a gate edit and the open precondition the series carries for the `getBuilder` areas; it is not made here. - The class method now returns the moved function's promise instead of being an `async` method; a caller awaits the same settlement, and a synchronous throw inside the body still arrives as a rejection (probe reading above). - **Proposed next slice** (unchanged from cut 6's report): `applyIndexDriftOp` and `restoreBareIndexAfterFailedTighten` into `sql-driver-schema-sync.ts` beside cut 6's pair. Neither builds through `getBuilder`; screen statics and price at dispatch; `applyIndexDriftOp` carries the ADR-0120 D4 ceremony, so the anchor entry's D4 paragraph would need a new entry for that module. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AuyWbsvLe5peToiMx3LiTd Co-authored-by: Claude <noreply@anthropic.com>
1 parent d1ea236 commit bd28708

2 files changed

Lines changed: 60 additions & 30 deletions

File tree

‎packages/drivers/driver-sql/src/sql-driver-introspection.ts‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
* `schema-drift.ts`, where they live; `sql-driver.ts` no longer reads the two
2929
* functions and dropped them from its import.
3030
*
31+
* [#25521 cut 7] `resolveHashShadowKeys` moved here too, beside its only
32+
* caller. `introspectIndexes`' body still reaches it through the host
33+
* closure above, which calls the class method on the driver, which hands this
34+
* body its host; so a subclass override or a test spy on the instance is
35+
* still the one reached. It reads only `knex`. `isHashShadowCarrier` and
36+
* `parseHashShadowKeyParts` are imported from `schema-drift.ts`, where they
37+
* live; `sql-driver.ts` still reads `parseHashShadowKeyParts` and dropped
38+
* `isHashShadowCarrier` from its import.
39+
*
3140
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
3241
* method of the same name as it stood, re-indented one level and otherwise
3342
* unedited: the same catalog statements, the same dialect arms, the same
@@ -81,7 +90,13 @@ import type {
8190
UniqueConstraintMember,
8291
} from './sql-driver.js';
8392
import { postgresTableNameCollisionError, singleColumnUniqueColumns } from './sql-driver.js';
84-
import { applyIndexKeyParts, parseIndexDdl, type PhysicalIndex } from './schema-drift.js';
93+
import {
94+
applyIndexKeyParts,
95+
isHashShadowCarrier,
96+
parseHashShadowKeyParts,
97+
parseIndexDdl,
98+
type PhysicalIndex,
99+
} from './schema-drift.js';
85100

86101
/**
87102
* The `SqlDriver` members the functions below read — every one of them, and
@@ -925,3 +940,36 @@ export async function introspectIndexes(
925940
}
926941
return [...byName.values()];
927942
}
943+
944+
/** The body of `SqlDriver#resolveHashShadowKeys`; the method's docblock there is its contract. */
945+
export async function resolveHashShadowKeys(
946+
this: Pick<IntrospectionHost, 'knex'>,
947+
tableName: string,
948+
indexes: PhysicalIndex[],
949+
): Promise<void> {
950+
const carriers = indexes.filter((i) => isHashShadowCarrier(i));
951+
if (carriers.length === 0) return;
952+
try {
953+
const rows: Array<Record<string, unknown>> = await this.knex
954+
.select('COLUMN_NAME', 'GENERATION_EXPRESSION')
955+
.from('information_schema.COLUMNS')
956+
.where({ TABLE_SCHEMA: this.knex.client.database(), TABLE_NAME: tableName })
957+
.whereIn(
958+
'COLUMN_NAME',
959+
carriers.map((i) => i.columns[0]!),
960+
);
961+
const exprByColumn = new Map<string, string>();
962+
for (const r of rows) {
963+
const column = String(r.COLUMN_NAME ?? r.column_name ?? '');
964+
if (column) {
965+
exprByColumn.set(column, String(r.GENERATION_EXPRESSION ?? r.generation_expression ?? ''));
966+
}
967+
}
968+
for (const i of carriers) {
969+
const parts = parseHashShadowKeyParts(exprByColumn.get(i.columns[0]!) ?? '');
970+
if (parts.length > 0) i.shadowKey = parts;
971+
}
972+
} catch {
973+
/* see the head note: an unresolved carrier degrades to "no finding". */
974+
}
975+
}

‎packages/drivers/driver-sql/src/sql-driver.ts‎

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ import {
123123
ceremonyOwnedDropFinding,
124124
isCeremonyOwnedColumnDrop,
125125
hashShadowColumnFor,
126-
isHashShadowCarrier,
127126
parseHashShadowKeyParts,
128127
indexedKeyColumns,
129128
isIndexDriftOp,
@@ -161,8 +160,9 @@ import { withUpsertConflictClause } from './upsert-conflict-clause.js';
161160
// [#25521 cut 1] The catalog-introspection helpers (`introspectColumnOrder`,
162161
// `introspectColumns` and the foreign-key, primary-key and unique-constraint reads),
163162
// [cut 4] `introspectSchema`, the whole-schema read built on them, and [cut 5]
164-
// `introspectIndexes`, the per-table index read schema sync is built on; the methods
165-
// of the same names below hand each body the members it reads (`IntrospectionHost`).
163+
// `introspectIndexes`, the per-table index read schema sync is built on, and [cut 7]
164+
// `resolveHashShadowKeys`, its MySQL hash-shadow enrichment; the methods of the
165+
// same names below hand each body the members it reads (`IntrospectionHost`).
166166
import {
167167
introspectColumnOrder,
168168
introspectColumns,
@@ -171,6 +171,7 @@ import {
171171
introspectPrimaryKeys,
172172
introspectSchema,
173173
introspectUniqueConstraints,
174+
resolveHashShadowKeys,
174175
} from './sql-driver-introspection.js';
175176
// [#25521 cut 2] The raw-execution path (`execute`, its refusal terminal
176177
// `rawStatementFault`, and `noteRuntimeIndexDdl`, which keeps the ledger of the
@@ -15131,35 +15132,16 @@ export class SqlDriver implements IDataDriver {
1513115132
* rather than proposing a rebuild it cannot reason about. The degraded
1513215133
* outcome is a missing finding, never a dropped constraint.
1513315134
*/
15134-
protected async resolveHashShadowKeys(
15135+
protected resolveHashShadowKeys(
1513515136
tableName: string,
1513615137
indexes: PhysicalIndex[],
1513715138
): Promise<void> {
15138-
const carriers = indexes.filter((i) => isHashShadowCarrier(i));
15139-
if (carriers.length === 0) return;
15140-
try {
15141-
const rows: Array<Record<string, unknown>> = await this.knex
15142-
.select('COLUMN_NAME', 'GENERATION_EXPRESSION')
15143-
.from('information_schema.COLUMNS')
15144-
.where({ TABLE_SCHEMA: this.knex.client.database(), TABLE_NAME: tableName })
15145-
.whereIn(
15146-
'COLUMN_NAME',
15147-
carriers.map((i) => i.columns[0]!),
15148-
);
15149-
const exprByColumn = new Map<string, string>();
15150-
for (const r of rows) {
15151-
const column = String(r.COLUMN_NAME ?? r.column_name ?? '');
15152-
if (column) {
15153-
exprByColumn.set(column, String(r.GENERATION_EXPRESSION ?? r.generation_expression ?? ''));
15154-
}
15155-
}
15156-
for (const i of carriers) {
15157-
const parts = parseHashShadowKeyParts(exprByColumn.get(i.columns[0]!) ?? '');
15158-
if (parts.length > 0) i.shadowKey = parts;
15159-
}
15160-
} catch {
15161-
/* see the head note: an unresolved carrier degrades to "no finding". */
15162-
}
15139+
// [#25521 cut 7] The body moved to `sql-driver-introspection.ts` (see `IntrospectionHost`).
15140+
const readKnex = () => this.knex;
15141+
const host: ThisParameterType<typeof resolveHashShadowKeys> = {
15142+
get knex() { return readKnex(); },
15143+
};
15144+
return resolveHashShadowKeys.call(host, tableName, indexes);
1516315145
}
1516415146

1516515147
/**

0 commit comments

Comments
 (0)