Skip to content

Commit 51cc7f9

Browse files
refactor(driver-sql): move applyIndexDriftOp and restoreBareIndexAfterFailedTighten into sql-driver-schema-sync.ts (#25521 cut 8) (#26169)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XLmmFpmgAHJChMp7idfTGmz)_ Part of #25521 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Ruling-ref: 6348891160 Cut 8 of the `sql-driver.ts` split (#24586 ruling A, recorded in `6270097840`). The claim is `6417688620`; this is the slice cuts 6 and 7 both proposed (Release `6417590425`). The bodies of the protected `SqlDriver#applyIndexDriftOp` (one index drift op, every dialect) and `SqlDriver#restoreBareIndexAfterFailedTighten` (the ADR-0120 restore it runs when a NULL-safe tightening cannot be created after the old index is gone) move unedited into `packages/drivers/driver-sql/src/sql-driver-schema-sync.ts`, beside cut 6's `applyDriftOpInPlace` and `rebuildSqliteTablePatched`. They reach the class through that module's existing host, `SchemaSyncHost`, **widened by exactly the seven members these two bodies read** and nothing else. Both methods keep their name, docblock, signature (less `async`) and position. The ADR-0120 D4 paragraph of the `sql-driver.ts` anchor entry follows the apply-side body into a new anchor entry for `sql-driver-schema-sync.ts` (below). No `docs/adr/**` edit, no `packages/spec/**` edit, no new gate. It is move-only, and the card stays open for the later cuts. Branch `claude/project-thread-wbvc9p` (platform-assigned to this card thread, as the claim records). One commit, `68fc9470`, on `origin/main` `a335f1def`. **Net line count (the seat's rule from ACCEPT `6385306916`).** `sql-driver.ts` goes from 21,431 lines to 21,334: **-97**. The two bodies are 122 lines out (105 + 17); the two stubs are 24 lines in (15 + 9); the schema-sync import comment gains one line (+1). The module grows 227 to 372 lines. **Serial with the file's in-flight PRs.** Re-measured at my start: 32 open PRs, each file list read in full (the two over 100 files paged to the end: #26138, 343 files, none under `packages/drivers/**`; #25640, the Version Packages PR, only `CHANGELOG.md` and `package.json` for the driver packages). None touches `sql-driver.ts`, `sql-driver-schema-sync.ts` or the driver-sql anchor entries. ## The area, measured on `a335f1def` - `applyIndexDriftOp` is lines 14776-14882 of `sql-driver.ts` (105 body lines); `restoreBareIndexAfterFailedTighten` is lines 14893-14914 (17 body lines). Callers: `applyMigrationEntries` (class) and cut 6's `applyDriftOpInPlace` body (module, through its host closure) call `applyIndexDriftOp`; only `applyIndexDriftOp`'s body calls the restore. - Both are protected with annotated return types (a promise of boolean, a promise of void), so the inferred-return limit does not bite. - Neither builds through `getBuilder` (the only knex use is `this.knex(op.table).columnInfo()`), so `check:tenant-chokepoint` rule 1 is untouched, and neither has a `.raw(` / `.where(` site, so rule 2 visits exactly what it visited. - No static read and no cast-wrapped `this` read. The `this` reads are `knex`, `logger`, `syncDeclaredIndexes`, `getExistingIndexNames`, `dropIndexIfExists`, `dropOrphanedHashShadowColumn`, `probeNullSafeUniqueDuplicates`, `logDurabilityFailure` and `restoreBareIndexAfterFailedTighten` (the restore reads four of those). No `this.FIELD.delete/insert/update(` site, so `UNTYPED_RECEIVERS` does not move. - `TursoDriver` and `SqliteWasmDriver` override neither (`turso-driver.ts` names `applyIndexDriftOp` once, in a comment). One test subclass calls `applyIndexDriftOp` through `this` (`sql-driver-13056-orphan-shadow-column-cleanup.test.ts`), which still reaches the class method. No script, doc or ADR names either method by path. ## The shape (#23050 shape B, as cuts 1-7 apply it) - Each class method keeps its docblock and signature, byte-identical less `async`. Its body is one host literal plus `NAME.call(host, ...)`, marked `[#25521 cut 8]`, and it returns the moved function's own promise. - `knex` and `logger` go as GETTERS that read the driver at call time; the seven methods go as closures that call the method on the driver at call time. So a subclass override, an instance spy, or a `knex`/`logger` replaced after construction is still the one the body reaches (probe below). - Each moved function types its `this` as the `Pick` of `SchemaSyncHost` over the members it reads, and each stub annotates its literal as the `ThisParameterType` of the moved function. No cast, and no member becomes public. - `SchemaSyncHost` gains `dropIndexIfExists`, `dropOrphanedHashShadowColumn`, `getExistingIndexNames`, `logDurabilityFailure`, `probeNullSafeUniqueDuplicates`, `restoreBareIndexAfterFailedTighten` and `syncDeclaredIndexes`, each typed as the class member's indexed access, in the interface's alphabetical order. `knex`, `logger` and `applyIndexDriftOp` were already there. - **Module-level declarations.** The bodies read only the `DriftOp` type, which the module already imports from `schema-drift.ts`. No declaration gains an `export`; `index.ts` re-exports nothing from the module, and `sql-driver.ts` drops no import. - The module header gains a `[#25521 cut 8]` paragraph; the `sql-driver.ts` import comment names cut 8. - The move was produced by a script from the source text, not retyped: each body line re-indented two columns. ## The ADR anchor follows the body `check:adr-anchors` counts decision-letter citations on the anchored surface (each anchor's `invariant` plus the full text of each anchored file). The moved bodies carry three `ADR-0120 D4` citations (the ceremony comment and the refusal text in `applyIndexDriftOp`, the restore's error text); the restore's docblock citation stays on the class. - `scripts/adr-anchors/packages__drivers__driver-sql__src__sql-driver.ts.json`: the `ADR-0120 D4: …` sentences leave its `invariant` and `ADR-0120` leaves its `adrs`. The `ADR-0053` paragraph is untouched. - New `scripts/adr-anchors/packages__drivers__driver-sql__src__sql-driver-schema-sync.ts.json`: `adrs: ["ADR-0120"]`, and its `invariant` is those D4 sentences, byte-identical. - Count: **1879 decision-letter citations at base `a335f1def` and at head `68fc9470`**; anchored files 88 to 89; both runs exit 0. - What stays in `sql-driver.ts`: the plan-side duplicate pre-flight (`applyNullSafeUniquePreflight`, `probeNullSafeUniqueDuplicates`) and the `GLOBAL_TENANT` key fold, which that paragraph also describes. The file still cites `ADR-0120` on 29 lines (10 of them `ADR-0120 D4`), but no anchor entry names it for ADR-0120 any more. Raised to the seat in the report as an open question; this PR follows the claim's wording. ## The published `.d.ts` does not change Under the series bar (ruling `6348891160`, letter A). Each end built with `pnpm turbo run build --filter=@objectstack/driver-sql` against the same dependency tree; the base leg restores the base text of the two source files in this worktree after the commit, 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`; the two are identical at both ends. - Raw sha256 of both files at base and at head: `5028e040e1501b0bd72b91fafcf4e1cb372393c59baa5bab8b936b240dd2d3f5` (cut 7's hash), `cmp` identical. - Positive control: the head `index.d.ts` with `protected` dropped from `applyIndexDriftOp` (line 5606) hashes to `e5e7a31c2fd4231d94f0a95af3c6e7e7b49225b6c1538a86528e7c9bc20fefa7`, a 4-line diff, `cmp` exit 1. - Export lists (CJS `require` and ESM `import` of the built dist): 56 / 56 names at both ends, identical. - The head dist carries the move: `.call(host` sites in `index.mjs` 19 at base, 21 at head. ## Move-only proof TypeScript compiler AST leaf tokens (JSDoc nodes skipped), base `a335f1def` against head `68fc9470`, scratch script, not committed: - `applyIndexDriftOp`: body 502 tokens, 0 differ; 46 comments identical; parameters less `this` 3 tokens, 0 differ; return type 4 tokens, 0 differ; `async` on both; 107 body lines, each the base line less two leading spaces. - `restoreBareIndexAfterFailedTighten`: body 132 tokens, 0 differ; 1 comment identical; parameters less `this` 15 tokens, 0 differ; return type 4 tokens, 0 differ; `async` on both; 19 body lines, each the base line less two leading spaces. - Both stubs: leading trivia (docblock) and signature identical to base less `async`. - Class: 348 to 348 members in the same order; the 346 unmoved members byte-identical (full text with trivia). - `sql-driver.ts` top level: 207 to 207 statements, 205 byte-identical; the two that differ are the schema-sync import (with its comment) and the class. - `sql-driver-schema-sync.ts`: the 5 base statements become 7; 3 byte-identical, the 2 that differ are the import carrying the module header and `SchemaSyncHost`; the 2 new ones are the moved functions. - Result `PROOF OK`, exit 0. Negative control (`REFUSING to rebuild` to `REFUSING to rebui1d` in a module copy): 1 token and 1 line differ, `PROOF FAILED`, exit 1. ## Tests Each suite under its own `os-verify-lock` hold, vitest JSON compared per test name and status, base `a335f1def` against head `68fc9470`: | Suite | Base | Head | Lost / changed / added | |:--|:--|:--|:--| | `@objectstack/driver-sql` | 4010 (3796 passed, 214 skipped; 248 files) | 4010 (3796 passed, 214 skipped; 248 files) | 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` | 704 (704 passed; 39 files) | same | 0 / 0 / 0 | - `pnpm exec tsc --noEmit` in driver-sql: exit 0. `pnpm exec eslint --no-inline-config packages/drivers/driver-sql/src`: exit 0, 268 files, 0 errors, 0 warnings. - **Late-binding probe** (scratch, node against each end's built dist on in-memory SQLite, not committed): 13 readings, base and head output identical (`diff` empty). It covers the `unbuildable_index` early return with no `knex` read; `create_index` through a `knex` replaced on the instance after construction; `replace_unique_index` both ways, the skipped create warning on a `logger` replaced after construction; `recreate_index` refused over duplicates through an instance spy on `logDurabilityFailure`, applied on clean data, its create throwing (an instance spy on the restore reached with the cause, the restore's error logged, the rejection rethrown) and its create silently absent (a subclass override of the restore reached with an undefined cause); the restore whose own re-create fails (the NOT enforced message, the cause passed as meta); `drop_index` running the index half before the shadow half; a `null` op answering a `TypeError` rejection, never a synchronous throw, from both methods; and the `default` arm. - No ablation: no tested behaviour changed. `test:repo` not run locally (the diff touches no repo-project test; CI's Test Core runs it). ## Gates Local, head `68fc9470`. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (change set vs merge base `a335f1def`, 4 paths): 65 commands, each exit captured before any pipe. 62 exit 0 on the first run. `check-plugin-teardown-shape.mjs --self-test` exit 3 (shallow clone) then exit 0 after `git fetch origin 621a487` (its positive control). `check:lean-entry-closure` exit 3 (no objectql dist) then exit 0 after `pnpm turbo run build --filter=@objectstack/objectql` under the lock. `check:dual-build-cjs-loads` exit 3, PREREQUISITE NOT MET (needs every package's dist): NOT MEASURED locally, CI runs it. - `--ran`: "65 derived famil(ies) accounted for — 64 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", exit 0. - Card-named, base read in a clean detached worktree at `a335f1def` and head at `68fc9470`, exit 0 at both ends: - `check:adr-anchors`: 88 to 89 anchored files; **1879 decision-letter citations at base and at head**. - `check:adr-symbol-anchors`: 2658 anchors resolve at both. - `check:tenant-chokepoint`: 24 `getBuilder()` bindings across 3 files at both; rule 2 clean at both. - `check:logger-receiver-detach`: 0 detaches at both (3731 non-test TS files walked at base, 3732 at head, where the base worktree lacks the gitignored generated spec registry; the `logger` getter keeps the receiver). - `check:driver-conformance`: 55 covered, 0 DEBT, 0 exempt at both. - `node scripts/check-tenant-audit-census.mjs`: 243 write call sites certified, every `UNTYPED_RECEIVERS` row matched, at both. - Also exit 0: `check:nul-bytes`, `check:commit-card-trailers`. `check-platform-checklist --changed-from` not owed (no exported symbol renamed or deleted). - `origin/main` moved 9 commits (to `d00215f68`) while this ran; none touches `packages/drivers/**`, `scripts/adr-anchors/**` or `check-adr-anchors.mjs`, `git merge-tree` against it is clean, and `dispatch-gates --commands` derives the same 65-command list there. Not merged: CI tests the merge ref. ## Acceptance notes - The ADR-0120 anchor for `sql-driver.ts` is gone with this cut (see above); the residual plan-side mechanism there is named in the report's `open_questions` for the seat. - Proposed next slice: none measured in this cut. The card's remaining `SqlDriver` areas are the seat's to pick. `skip-changeset`: move-only, nothing a published package ships changes (the `.d.ts` is byte-identical; the emitted JS runs the same bodies through the host). --- _Generated by [Claude Code](https://claude.ai/code/session_0155UnTppT9uLobphwpxhiZu)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6b93552 commit 51cc7f9

4 files changed

Lines changed: 183 additions & 129 deletions

File tree

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

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99
* the members the two bodies call (`applyIndexDriftOp`, `buildRebuiltColumn`,
1010
* `syncTableIndexes` and the rest).
1111
*
12+
* [#25521 cut 8] `applyIndexDriftOp`, which applies one index drift op on
13+
* every dialect, and `restoreBareIndexAfterFailedTighten`, the restore it runs
14+
* when a NULL-safe tightening cannot be created after the old index is gone,
15+
* moved here beside them the same way. The class keeps both methods too, and
16+
* `SchemaSyncHost` grew by exactly the members these two bodies read. The
17+
* ADR-0120 tightening ceremony these bodies carry is anchored on this module
18+
* (`scripts/adr-anchors/`).
19+
*
1220
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
1321
* method of the same name as it stood, re-indented one level and otherwise
1422
* unedited: the same dialect statements, the same rebuild procedure, the same
@@ -54,18 +62,155 @@ export interface SchemaSyncHost {
5462
applyIndexDriftOp: SqlDriver['applyIndexDriftOp'];
5563
buildRebuiltColumn: SqlDriver['buildRebuiltColumn'];
5664
readonly dialectName: SqlDriver['dialectName'];
65+
dropIndexIfExists: SqlDriver['dropIndexIfExists'];
66+
dropOrphanedHashShadowColumn: SqlDriver['dropOrphanedHashShadowColumn'];
67+
getExistingIndexNames: SqlDriver['getExistingIndexNames'];
5768
introspectColumns: SqlDriver['introspectColumns'];
5869
readonly isMysql: SqlDriver['isMysql'];
5970
readonly isPostgres: SqlDriver['isPostgres'];
6071
readonly knex: SqlDriver['knex'];
72+
logDurabilityFailure: SqlDriver['logDurabilityFailure'];
6173
readonly logger: SqlDriver['logger'];
6274
readonly managedObjectFields: SqlDriver['managedObjectFields'];
6375
readonly managedObjectIndexes: SqlDriver['managedObjectIndexes'];
6476
nowColumnDefault: SqlDriver['nowColumnDefault'];
77+
probeNullSafeUniqueDuplicates: SqlDriver['probeNullSafeUniqueDuplicates'];
6578
resolveTenantField: SqlDriver['resolveTenantField'];
79+
restoreBareIndexAfterFailedTighten: SqlDriver['restoreBareIndexAfterFailedTighten'];
80+
syncDeclaredIndexes: SqlDriver['syncDeclaredIndexes'];
6681
syncTableIndexes: SqlDriver['syncTableIndexes'];
6782
}
6883

84+
/** The body of `SqlDriver#applyIndexDriftOp`; the method's docblock there is its contract. */
85+
export async function applyIndexDriftOp(this: Pick<SchemaSyncHost, 'dropIndexIfExists' | 'dropOrphanedHashShadowColumn' | 'getExistingIndexNames' | 'knex' | 'logDurabilityFailure' | 'logger' | 'probeNullSafeUniqueDuplicates' | 'restoreBareIndexAfterFailedTighten' | 'syncDeclaredIndexes'>, op: DriftOp): Promise<boolean> {
86+
// REPORT ONLY: there is no column to build the index over, so no DDL can
87+
// apply it and the remedy is a metadata edit. Answered before any read, and
88+
// on every dialect, so `applyMigrationEntries` reports it `skipped`. The
89+
// `default` arm below would say the same; this names the reason.
90+
if (op.type === 'unbuildable_index') return false;
91+
const physicalColumns = new Set(Object.keys(await this.knex(op.table).columnInfo()));
92+
const ensure = (name: string, columns: string[], unique: boolean, nullSafeColumns?: string[]) =>
93+
this.syncDeclaredIndexes(op.table, [{ name, fields: columns, unique, nullSafeColumns }], physicalColumns);
94+
95+
switch (op.type) {
96+
case 'replace_unique_index': {
97+
// CREATE before DROP: the composite and the legacy index have different
98+
// names, so uniqueness is never unenforced in between. If the create
99+
// fails we have not dropped anything yet and the schema is untouched.
100+
await ensure(op.createIndexName, op.createColumns, true, op.nullSafeColumns);
101+
// …and only drop once the replacement is confirmed present. This is a
102+
// relaxation, not a removal: if `syncDeclaredIndexes` skipped the create
103+
// (a column it references is not materialized), dropping the legacy
104+
// index would leave the field with NO uniqueness at all.
105+
if (!(await this.getExistingIndexNames(op.table)).has(op.createIndexName)) {
106+
this.logger.warn(
107+
`[schema-drift] keeping legacy unique index(es) ${op.dropIndexNames.join(', ')} on '${op.table}' — ` +
108+
`the replacement '${op.createIndexName}' could not be created.`,
109+
);
110+
return false;
111+
}
112+
for (const name of op.dropIndexNames) {
113+
await this.dropIndexIfExists(op.table, name);
114+
}
115+
return true;
116+
}
117+
case 'create_index':
118+
await ensure(op.indexName, op.columns, op.unique, op.nullSafeColumns);
119+
// Honest applied-reporting: `syncDeclaredIndexes` degrades some
120+
// failures (a NULL-safe unique over data that still violates it) into
121+
// a loud log instead of a throw, so presence is the only proof.
122+
return (await this.getExistingIndexNames(op.table)).has(op.indexName);
123+
case 'drop_index': {
124+
// #13056: a TERMINAL removal — the name is retired, nothing will
125+
// re-declare it, and the #11627 hash shadow derived from that name is
126+
// dead the moment the index goes. Index FIRST, then the column it
127+
// keyed: MySQL refuses to drop a column a key still uses, and the
128+
// reverse order would take the constraint down with the column while
129+
// the operator was told only that an index was dropped.
130+
//
131+
// ⚠️ Deliberately HERE and not inside `dropIndexIfExists`. That helper
132+
// has two other callers and neither is terminal: `recreate_index`
133+
// drops in order to re-create under the SAME name — routing it through
134+
// this cleanup would destroy the survivor commit cd1348802's `reusable` branch
135+
// exists to re-key in place, turning every rebuild into a full
136+
// regeneration of a STORED generated column — and
137+
// `replace_unique_index` cannot reach a shadow at all (its legacy
138+
// detection excludes `isHashShadowCarrier`, stated outright in
139+
// `diffManagedIndexes` precisely because that op DROPS the legacy
140+
// name). "Which caller" is the wrong axis; "is this name coming back"
141+
// is the right one, and only the op knows.
142+
const dropped = await this.dropIndexIfExists(op.table, op.indexName);
143+
// Applied when EITHER half changed the table. A half-applied migration
144+
// leaves the index gone and the column behind, so an already-absent
145+
// index is exactly the state whose leftover column still needs
146+
// collecting — reporting that as `skipped` would claim the apply did
147+
// nothing to a table it had just rewritten.
148+
const shadowDropped = await this.dropOrphanedHashShadowColumn(op.table, op.indexName);
149+
return dropped || shadowDropped;
150+
}
151+
case 'recreate_index': {
152+
// Same name on both sides — the drop has to come first, and a UNIQUE
153+
// target can fail on existing duplicates. That is why this op is
154+
// categorised destructive when unique (see `diffManagedIndexes`).
155+
//
156+
// ADR-0120 D4: the NULL-safe tightening re-runs the duplicate
157+
// pre-flight HERE, immediately before the drop — the plan-time probe
158+
// may be stale, and at no point may a constraint be dropped without
159+
// its replacement being creatable. Duplicates → refuse, old index
160+
// untouched.
161+
const nullSafe = op.unique && (op.nullSafeColumns?.length ?? 0) > 0;
162+
if (nullSafe) {
163+
const duplicates = await this.probeNullSafeUniqueDuplicates(
164+
op.table,
165+
op.columns,
166+
op.nullSafeColumns!,
167+
);
168+
if (duplicates.length > 0) {
169+
this.logDurabilityFailure(
170+
`[schema-drift] REFUSING to rebuild '${op.indexName}' on '${op.table}' as a NULL-safe unique — ` +
171+
`${duplicates.length} duplicate group(s) violate it (e.g. ${duplicates[0].key} × ${duplicates[0].rows} rows). ` +
172+
`The existing index is left in place; deduplicate and re-run "os migrate plan" (ADR-0120 D4).`,
173+
);
174+
return false;
175+
}
176+
}
177+
await this.dropIndexIfExists(op.table, op.indexName);
178+
try {
179+
await ensure(op.indexName, op.columns, op.unique, op.nullSafeColumns);
180+
} catch (e) {
181+
if (nullSafe) await this.restoreBareIndexAfterFailedTighten(op, e);
182+
throw e;
183+
}
184+
const present = (await this.getExistingIndexNames(op.table)).has(op.indexName);
185+
if (!present && nullSafe) await this.restoreBareIndexAfterFailedTighten(op, undefined);
186+
return present;
187+
}
188+
default:
189+
return false;
190+
}
191+
}
192+
193+
/** The body of `SqlDriver#restoreBareIndexAfterFailedTighten`; the method's docblock there is its contract. */
194+
export async function restoreBareIndexAfterFailedTighten(this: Pick<SchemaSyncHost, 'getExistingIndexNames' | 'knex' | 'logDurabilityFailure' | 'syncDeclaredIndexes'>, op: Extract<DriftOp, { type: 'recreate_index' }>, cause: unknown): Promise<void> {
195+
try {
196+
await this.syncDeclaredIndexes(
197+
op.table,
198+
[{ name: op.indexName, fields: op.columns, unique: op.unique }],
199+
new Set(Object.keys(await this.knex(op.table).columnInfo())),
200+
);
201+
} catch {
202+
/* the error below reports the state either way */
203+
}
204+
const restored = (await this.getExistingIndexNames(op.table)).has(op.indexName);
205+
this.logDurabilityFailure(
206+
`[schema-drift] could not create the NULL-safe unique '${op.indexName}' on '${op.table}' after dropping ` +
207+
`the old index${restored ? ' — restored the previous bare composite' : ' — AND the restore failed, so the ' +
208+
'constraint is currently NOT enforced'}. Rows without an organization are ${restored ? 'still ' : ''}not ` +
209+
`constrained, because SQL UNIQUE is NULL-distinct; re-run "os migrate plan" and apply the reported op (ADR-0120 D4).`,
210+
(cause as any)?.message ?? cause,
211+
);
212+
}
213+
69214
/** The body of `SqlDriver#applyDriftOpInPlace`; the method's docblock there is its contract. */
70215
export async function applyDriftOpInPlace(this: Pick<SchemaSyncHost, 'applyIndexDriftOp' | 'dialectName' | 'isMysql' | 'isPostgres' | 'knex' | 'logger'>, op: DriftOp): Promise<boolean> {
71216
// Index ops need no dialect-specific ALTER — route them to the portable path.

0 commit comments

Comments
 (0)