Skip to content

Commit b694e56

Browse files
refactor(driver-sql): move the raw-execution path out of sql-driver.ts into sql-driver-raw-execution.ts (#25521 cut 2) (#25630)
Part of #25521 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Ruling-ref: 6348891160 Cut 2 of the `sql-driver.ts` split (#24586 ruling A, recorded in `6270097840`). The claim is `6383192755`. Three `SqlDriver` method bodies move unedited into a new module, `packages/drivers/driver-sql/src/sql-driver-raw-execution.ts`: the public `execute`, its protected refusal terminal `rawStatementFault`, and the protected `noteRuntimeIndexDdl`, which records the index DDL `execute` ran. It is move-only, and the card stays open for the later cuts. Branch `claude/issue-25521-sql-driver-cut-2`, cut from `origin/main` at `f7b029896` (cut 1's merge). **Serial with the file's in-flight PRs.** At claim the PM read no open PR touching `sql-driver.ts` or `driver-turso/src`. Re-measured after my first push (14 open PRs, each PR's file list read in full from its head, merge base to head) and again just before this PR was opened (13 open; #25617 had merged). None touches `packages/drivers/driver-sql/src/**` or `packages/drivers/driver-turso/src/**`. `origin/main` moved three commits past the base meanwhile (#25612, #25615, #25617, now `cc10e74fe`). None of them touches `driver-sql`, `driver-turso`, `driver-sqlite-wasm` or a gate script this diff derives, so `main` is not merged in. ## Seam table (fresh, at `f7b029896`) `sql-driver.ts` is 21,675 lines. 30-day lines are `git blame --line-porcelain` lines whose committer time is on or after 2026-09-24T10:03:29Z, 30 days before the base commit. History was deepened to 2026-09-15 first, and `scripts/pm/git-history.mjs count` proves 68 commits on the file in that window (19 in the last 7 days). In all, 3,855 lines (17.8%) are from the last 30 days. The class `SqlDriver` spans lines 5482-21600 with 347 members: 251 methods (49 public), 32 statics. No public method has an inferred return type; the one inferred public member is the getter `supports`, in the class head. Sections are the class's own banner comments, cut by contiguous line range. "Public" counts public methods, plus public accessors where there are any: | Lines | Section | Lines | 30-day lines | Members (public) | |:--|:--|--:|--:|:--| | 1-5481 | Module preamble: imports, types, top-level helpers (200 statements) | 5,481 | 1,405 | - | | 5482-7073 | Class head: fields, statics, constructor, capability getters (`supports`) | 1,592 | 290 | 88 (1 + 2 accessors) | | 7074-7505 | Lifecycle | 432 | 155 | 15 (3) | | 7506-10184 | CRUD (IDataDriver core) | 2,679 | 691 | 45 (7) | | 10185-10796 | Bulk and batch operations | 612 | 140 | 9 (6) | | **10797-10922** | **Raw execution (this cut)** | **126** | **12** | **4 (1)** | | 10923-11000 | Transactions | 78 | 0 | 7 (5) | | 11001-11532 | Aggregation | 532 | 61 | 5 (1) | | 11533-11820 | Distinct | 288 | 20 | 3 (1) | | 11821-11936 | Window functions | 116 | 4 | 1 (1) | | 11937-12035 | Query plan analysis | 99 | 1 | 2 (2) | | 12036-15790 | Schema sync (syncSchema / init) | 3,755 | 373 | 62 (14 + 3 accessors) | | 15791-15926 | Schema introspection (`listPhysicalColumns`, `introspectSchema`) | 136 | 65 | 2 (2) | | 15927-21600 | Internal helpers, including cut 1's five introspection stubs | 5,674 | 638 | 104 (5) | | 21601-21675 | After the class: `UniqueConstraintMember`, `singleColumnUniqueColumns` | 75 | 0 | - | The limits per area are cut 1's (PR #25614), re-read on this tree: - **`getBuilder` builders.** Aggregation, Distinct, Window functions, Query plan analysis, CRUD and Bulk build through `getBuilder`, so a cut of any of them must first carry the new module into `check:tenant-chokepoint`'s `SCAN_FILES`, or the gate goes silently narrower. None of them is cut here. - **Protected statics.** The hash-shadow family and the row-size explainers (Internal helpers) read protected statics, so those bodies stay. - **`this.FIELD.delete/insert/update(`.** Transactions has one, `releaseTransaction` (`this.openTransactions.delete(key)`), and Schema sync has three. Those bodies stay. Raw execution has none: `noteRuntimeIndexDdl` calls `.delete(` on a local `Set`, and calls `.get(`, `.set(` and `.values()` on the ledger. - `insertOnlyUpsertColumns` stays in `sql-driver.ts`, because #8740's restart line names that symbol in that file. This cut does not touch it. **Proposed cut 3: Transactions less `releaseTransaction`** (10923-11000, 78 lines, 0 from the last 30 days). That is `beginTransaction`, `assertBareKnexSafe`, `commit`, `rollback`, `commitTransaction` and `rollbackTransaction`. Every return type is annotated, and none reads `getBuilder` or a static. `releaseTransaction` stays, for its `this.openTransactions.delete(` site. The bodies call it through a late-bound closure, as `execute` calls `rawStatementFault` here. Schema introspection (136 lines, 65 from 30 days) is the next candidate after that. ## Why this area for cut 2 Cut 1 proposed it, and it re-measures clean: 126 lines, 12 from the last 30 days. Its one public member, `execute`, has an annotated return type (a `Promise` of `unknown`, pinned by `sql-driver-doors-declared-types.test.ts`). It has no static and no `getBuilder`, and no `this.FIELD.delete/insert/update(` site. Between them, the three bodies read only `knex`, `logger`, `runtimeCreatedIndexes` and each other (`execute` calls `rawStatementFault` and `noteRuntimeIndexDdl`). The field `runtimeCreatedIndexes` and its docblock stay on the class: schema sync also reads the ledger (the index-drift diff in the Schema sync section). Both subclasses reach the moved bodies. `TursoDriver` overrides `execute`, calls `super.execute`, and calls `rawStatementFault` at eight sites. `SqliteWasmDriver` inherits all three. ## The shape (#23050 shape B, as cut 1 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 2]`. `execute` drops `async` and returns the moved function's promise. The other two were never `async`. - `RawExecutionHost` 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. The generator lists them, and they were checked against the `this.` reads of each base body. Each stub annotates its literal as the `ThisParameterType` of the moved function. There is no cast, and no member becomes public. - `knex`, `logger` and `runtimeCreatedIndexes` go as getters, so each read happens when the body makes it. `rawStatementFault` and `noteRuntimeIndexDdl` go as late-bound closures. So a subclass override is still the one `execute` reaches: `sql-driver-21385-refusal-log-line-redaction.test.ts` and `turso-driver-16019-remote-raw-statement-fault-envelope.test.ts` override `rawStatementFault` and call `super`. A spy on the instance is reached the same way. - `this.logger.warn(...)` keeps its receiver: the getter hands back the class's logger, and `.warn` is called on it. - **Module-level declarations.** The bodies read seven top-level declarations of `sql-driver.ts`: `OWN_STATEMENT_FAULT`, `INDEX_DDL_PREFIX`, `CREATE_INDEX_DDL`, `DROP_INDEX_DDL`, `unquoteSqlIdentifier`, `lastIdentifierSegment` and `rawStatementFaultError`. All seven stay where they are, each gains a leading `export`, and the new module imports them back. They stay because `OWN_STATEMENT_FAULT` has readers that did not move (four other refusal lines). They also stay because tests in four other packages (`types`, `cli`, `metadata`, `metadata-protocol`) cite `rawStatementFaultError` "verbatim (`sql-driver.ts`)" by path. Every value import is read only inside a function body, so the module cycle is evaluation-safe, as in cut 1. `index.ts` re-exports nothing new, so the published surface does not move (proof below). This is the one place the cut differs from cut 1's shape: cut 1 needed no new `export`. It is recorded as an open question in the report. - The move was produced by a script from the source text, not retyped: each method's parameter list, return type and body lines, re-indented two columns. The three bodies hold no multi-line template literal. ## The published `.d.ts` does not change Under the series bar (ruling `6348891160`, letter A), measured on base `f7b029896` and on head `a92731ec`. Both were built against the same dependency tree: `turbo run build --filter=@objectstack/driver-turso... --filter=@objectstack/driver-sqlite-wasm...` at base, 9/9 tasks, 0 cached (remote caching disabled). 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` (cut 1's head hash, unchanged). The raw diff is 0 lines. - Normalised with the ruling's one-liner verbatim (double-quoted unions), base = head = `93da45168e82136b7613944525e47402d5dc6f9f8daa9efa4bdceeaa11b52a89`. This file has 0 double-quoted unions, so that normaliser rewrites nothing. - Normalised with the same one-liner, with the quote character swapped to a single quote (21 lines of this file carry a single-quoted union), base = head = `118e992ad8aad590044eb31509dca65e6b321e3410ce846e1e5f5dd34b956e95`. With a raw diff of 0 lines, the proof rests on raw identity. The single-quote pass is reported only because the dispatch asked for it. - Positive control on a copy of the head `dist/index.d.ts`: line 4217, `protected rawStatementFault(command: string, error: unknown): Error;`, made public. That gives raw and double-quote-normalised `47c79477965c2473c4a079afd8c4c8f47bc9f36213ac7dcfa2df99be7f081688` and single-quote-normalised `464658c31cd8f7ee5c779054428fe3f8d3c520610061252de5567e05a480ccd2`, each 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 to `d61a9dd3a02661602b1bb2dbe18152ac43986148f36b2746f40c0884bd8ffdec` on all four. - The head build carries the move: `dist/index.mjs` and `dist/index.js` each contain 8 host `.call(` sites (cut 1's 5 plus these 3). 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, and `async` matches on all three. 404 tokens compared, 0 differ. - **Lines**: 52 body lines compared (16 + 21 + 15, each body after its opening brace). Each one is the base line with two leading spaces removed. - **Negative control**: changing one character inside a string literal of `rawStatementFault` in a copy of the module makes the check report 1 token and 1 line different, and it exits `PROOF FAILED`. - **Class side**: 347 members at base and head. The 344 members that did not move are byte-identical to base, leading trivia included. Each of the 3 stubs keeps its docblock and signature byte-identical, less `async` on `execute`. - **Top level of `sql-driver.ts`**: 200 base statements other than the class. 193 are byte-identical. 7 are byte-identical except for a leading `export`. They stay in order. The one new statement is the import of the new module. - **Size**: +166 / -57 over 2 files, far under the 3,000-line manual-merge line. `sql-driver.ts` goes from 21,675 to 21,651 lines, and `sql-driver-raw-execution.ts` has 133. ## Readers of `sql-driver.ts` by path, checked against the moved text None needed an edit. Each was read against the moved region (10797-10922) 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 bodies cite no ADR at all. The docblocks that cite ADR-0048 and ADR-0112 stay on the class. `check:adr-anchors`: 85 anchored files at base and head. Decision-letter citations on the anchored surface: 1,871 at base and 1,871 at head. - **Symbol anchors.** No ADR or platform-checklist anchor names `execute`, `rawStatementFault`, `noteRuntimeIndexDdl` or the seven module-level declarations in `sql-driver.ts`, and the stubs keep every member name anyway. `check:adr-symbol-anchors`: 2,384 anchors resolve at both ends. - **`check:tenant-chokepoint`** reads `sql-driver.ts` for `getBuilder` bindings and for `IS NULL` arms in raw fragments. The moved text has no `getBuilder`. Its one raw call, `this.knex.raw(command, ...)`, passes a variable, which that gate does not read (its header names this case as outside its view). So the gate saw nothing in the moved text, and it still covers the same population: 24 `getBuilder()` bindings across 3 files at both ends. - **Logger receivers.** `logger-receiver-detach.test.ts` ③ scans `sql-driver.ts` for detach-then-call shapes. The moved `this.logger.warn(` is a direct call, not a finding. The repo-wide `check:logger-receiver-detach` walks every `packages/**` source, so it covers the new module: 0 detaches at head. - **Other source scans**, none of whose patterns occurs in the moved text: `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-object-def-param-keys` (no `(obj as any).KEY` read). - **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 other unmoved symbols. - **Comment citations** in other packages ("`rawStatementFaultError` … (`sql-driver.ts`)") stay true, because that function did not move. - **`driver-turso`** and **`driver-sqlite-wasm`**: their readers of moved members reach the stubs unchanged. Both suites ran 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. - `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 there is no difference (2,513 distinct names; 13 names repeat). - `packages/drivers/driver-sqlite-wasm` suite, against the rebuilt head `dist`: base 704 tests (704 passed; 39 files), head the same, per test name no difference. - `dispatch-gates --commands` on this diff, at head `a92731ec`: 55 derived commands, each run 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`), which CI runs. Two gates first answered exit 3 for prerequisites and then exit 0: `check-plugin-teardown-shape --self-test` after its pinned fixture commit `621a4876` was fetched, and `check:lean-entry-closure` after `objectql` was built. - No reverse verification or ablation: the diff changes no tested behaviour. ## Acceptance notes - Seven top-level declarations of `sql-driver.ts` gain `export`, so the moved bodies can import them without a second copy. The package's public surface does not change (raw `.d.ts` identity, export tables). Moving the five declarations only the moved bodies read into the new module was the alternative. It was not taken: other packages cite `rawStatementFaultError` in `sql-driver.ts` by path, and `OWN_STATEMENT_FAULT` would need the export anyway. - `TursoDriver`'s `execute` override still calls `super.execute`, which now returns the moved function's promise instead of an `async` method's promise. A caller awaits the same settlement. --- _Generated by [Claude Code](https://claude.ai/code/session_01RvoeGVyNRC3zkVrNsLbm8M)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent ca80f05 commit b694e56

2 files changed

Lines changed: 166 additions & 57 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25521 cut 2] The raw-execution path of `SqlDriver`: `execute`, its refusal
5+
* terminal `rawStatementFault`, and `noteRuntimeIndexDdl`, which records the
6+
* index DDL `execute` ran in the `runtimeCreatedIndexes` ledger. They moved out
7+
* of `sql-driver.ts` whole.
8+
*
9+
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
10+
* method of the same name as it stood, re-indented one level and otherwise
11+
* unedited: the same statement handling, the same redacted refusal line, the
12+
* same ledger rules.
13+
*
14+
* ## How the moved bodies reach the class
15+
*
16+
* The shape `sql-driver-introspection.ts` (cut 1) applies, after #23050. The
17+
* class keeps every method, because the published `dist/*.d.ts` lists each
18+
* one's name, docblock and position. `TursoDriver` overrides `execute` and
19+
* calls `super.execute`, and calls `rawStatementFault` from its remote
20+
* transport. Each body is now one literal of exactly the
21+
* {@link RawExecutionHost} members that method reads, plus a `.call`. `execute`
22+
* is no longer `async`: it returns the moved function's own promise, so a
23+
* caller awaits the same promise the body settles, with no extra wrapper.
24+
*
25+
* `execute` reads `rawStatementFault` and `noteRuntimeIndexDdl` through
26+
* closures that call them on the driver at call time. So a subclass override
27+
* (a test subclass overrides `rawStatementFault` and calls `super`) or a test
28+
* spy on the instance is still the one the body reaches.
29+
*
30+
* The fields `knex`, `logger` and `runtimeCreatedIndexes` go as GETTERS, so
31+
* each read happens when the body makes it, as it did on the class. The
32+
* ledger itself stays a field of the class: schema sync reads it as well.
33+
*
34+
* The module-level declarations the bodies read stay in `sql-driver.ts`:
35+
* `OWN_STATEMENT_FAULT`, `rawStatementFaultError`, the three index-DDL patterns
36+
* and the two identifier helpers. They are exported from there and imported
37+
* here. Other packages' tests cite `rawStatementFaultError` in `sql-driver.ts`
38+
* by path, and `OWN_STATEMENT_FAULT` has readers that did not move. The module
39+
* cycle is evaluation-safe because each value import is read only inside a
40+
* function body, never while the two modules are first evaluated. `index.ts`
41+
* re-exports nothing from this module, so the published surface does not move.
42+
*/
43+
44+
import type { DriverOptions } from '@objectstack/spec/data';
45+
import { redactStatementFromMessage } from '@objectstack/types';
46+
import type { Knex } from 'knex';
47+
import type { SqlDriver } from './sql-driver.js';
48+
import {
49+
CREATE_INDEX_DDL,
50+
DROP_INDEX_DDL,
51+
INDEX_DDL_PREFIX,
52+
lastIdentifierSegment,
53+
OWN_STATEMENT_FAULT,
54+
rawStatementFaultError,
55+
unquoteSqlIdentifier,
56+
} from './sql-driver.js';
57+
58+
/**
59+
* The `SqlDriver` members the functions below read — every one of them, and
60+
* no other; each function's `this` is the `Pick` of the members IT reads. Each
61+
* is typed AS the class member (an indexed access on the class), so a change
62+
* to a member's signature on the class is a change here too, and naming a
63+
* member the class does not have is a compile error. See the module header
64+
* for which members are getters.
65+
*/
66+
export interface RawExecutionHost {
67+
readonly knex: SqlDriver['knex'];
68+
readonly logger: SqlDriver['logger'];
69+
noteRuntimeIndexDdl: SqlDriver['noteRuntimeIndexDdl'];
70+
rawStatementFault: SqlDriver['rawStatementFault'];
71+
readonly runtimeCreatedIndexes: SqlDriver['runtimeCreatedIndexes'];
72+
}
73+
74+
/** The body of `SqlDriver#rawStatementFault`; the method's docblock there is its contract. */
75+
export function rawStatementFault(this: Pick<RawExecutionHost, 'logger'>, command: string, error: unknown): Error {
76+
const declared = (error as { status?: unknown } | null | undefined)?.status;
77+
if (typeof declared === 'number') return error as Error;
78+
79+
const detail = (error as { message?: unknown } | null | undefined)?.message;
80+
const code = (error as { code?: unknown } | null | undefined)?.code;
81+
// [#21385] ⛔ `command` is not written. It is the text the caller sent, and a
82+
// raw statement may spell its values inline as well as bind them; the
83+
// dialect's text leads with the same statement, compiled, and is cut below.
84+
this.logger.warn(
85+
'[sql-driver] DATABASE_ERROR — the backend refused a raw statement' +
86+
(typeof code === 'string' && code.length > 0 ? ` (${code})` : '') +
87+
'. The dialect diagnostic, with the statement and its bound literals cut: ' +
88+
redactStatementFromMessage(typeof detail === 'string' ? detail : String(error), OWN_STATEMENT_FAULT),
89+
);
90+
return rawStatementFaultError(error);
91+
}
92+
93+
/** The body of `SqlDriver#execute`; the method's docblock there is its contract. */
94+
export async function execute(this: Pick<RawExecutionHost, 'knex' | 'noteRuntimeIndexDdl' | 'rawStatementFault'>, command: any, params?: any[], options?: DriverOptions): Promise<unknown> {
95+
if (typeof command !== 'string') {
96+
return command;
97+
}
98+
99+
const builder =
100+
options?.transaction
101+
? this.knex.raw(command, params || []).transacting(options.transaction as Knex.Transaction)
102+
: this.knex.raw(command, params || []);
103+
104+
let result: unknown;
105+
try {
106+
result = await builder;
107+
} catch (error) {
108+
// [#16019] The raw path's terminal — see {@link SqlDriver.rawStatementFault}.
109+
throw this.rawStatementFault(command, error);
110+
}
111+
// Only after the statement actually succeeded — an index we failed to
112+
// create is not one we own (#4884).
113+
if (INDEX_DDL_PREFIX.test(command)) this.noteRuntimeIndexDdl(command);
114+
return result;
115+
}
116+
117+
/** The body of `SqlDriver#noteRuntimeIndexDdl`; the method's docblock there is its contract. */
118+
export function noteRuntimeIndexDdl(this: Pick<RawExecutionHost, 'runtimeCreatedIndexes'>, sql: string): void {
119+
const created = CREATE_INDEX_DDL.exec(sql);
120+
if (created) {
121+
const table = lastIdentifierSegment(created[2]);
122+
let names = this.runtimeCreatedIndexes.get(table);
123+
if (!names) this.runtimeCreatedIndexes.set(table, (names = new Set<string>()));
124+
names.add(unquoteSqlIdentifier(created[1]));
125+
return;
126+
}
127+
const dropped = DROP_INDEX_DDL.exec(sql);
128+
if (!dropped) return;
129+
// SQLite / Postgres `DROP INDEX` names no table, so forget the name
130+
// wherever it is recorded — the ledger must never outlive the index.
131+
const name = lastIdentifierSegment(dropped[1]);
132+
for (const names of this.runtimeCreatedIndexes.values()) names.delete(name);
133+
}

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

Lines changed: 33 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ import {
170170
introspectPrimaryKeys,
171171
introspectUniqueConstraints,
172172
} from './sql-driver-introspection.js';
173+
// [#25521 cut 2] The raw-execution path (`execute`, its refusal terminal
174+
// `rawStatementFault`, and `noteRuntimeIndexDdl`, which keeps the ledger of the
175+
// index DDL `execute` ran); the methods of the same names below hand each body
176+
// the members it reads (`RawExecutionHost`).
177+
import { execute, noteRuntimeIndexDdl, rawStatementFault } from './sql-driver-raw-execution.js';
173178
import { makeDatabaseDirectory, probeAbsentSqliteDatabaseFile, SqliteDatabaseFileError } from './sqlite-database-file.js';
174179
import { jsonMembershipPredicate } from '@objectstack/core';
175180
import knex, { Knex } from 'knex';
@@ -243,26 +248,26 @@ const DEFAULT_ID_LENGTH = 16;
243248
* text whatever word it opens with. The shared cut then runs without asking the
244249
* leak predicate, exactly as it does at the engine's raw-statement door.
245250
*/
246-
const OWN_STATEMENT_FAULT = { statementSent: true } as const;
251+
export const OWN_STATEMENT_FAULT = { statementSent: true } as const;
247252

248253
// ── Raw index DDL the driver executes on the framework's behalf (#4884) ──────
249254
/** An SQL identifier in any dialect's quoting, or bare. */
250255
const SQL_IDENTIFIER = '(?:`[^`]+`|"[^"]+"|\\[[^\\]]+\\]|[A-Za-z_][A-Za-z0-9_$]*)';
251256
/** Cheap prefilter so ordinary `execute()` traffic never touches the parsers below. */
252-
const INDEX_DDL_PREFIX = /^\s*(?:create|drop)\s+(?:unique\s+)?index\b/i;
253-
const CREATE_INDEX_DDL = new RegExp(
257+
export const INDEX_DDL_PREFIX = /^\s*(?:create|drop)\s+(?:unique\s+)?index\b/i;
258+
export const CREATE_INDEX_DDL = new RegExp(
254259
`^\\s*create\\s+(?:unique\\s+)?index\\s+(?:concurrently\\s+)?(?:if\\s+not\\s+exists\\s+)?` +
255260
`(${SQL_IDENTIFIER})\\s+on\\s+(${SQL_IDENTIFIER}(?:\\.${SQL_IDENTIFIER})?)`,
256261
'i',
257262
);
258-
const DROP_INDEX_DDL = new RegExp(
263+
export const DROP_INDEX_DDL = new RegExp(
259264
`^\\s*drop\\s+index\\s+(?:concurrently\\s+)?(?:if\\s+exists\\s+)?` +
260265
`(${SQL_IDENTIFIER}(?:\\.${SQL_IDENTIFIER})?)`,
261266
'i',
262267
);
263268

264269
/** Peel one layer of `"…"` / `` `…` `` / `[…]` quoting off an identifier. */
265-
function unquoteSqlIdentifier(raw: string): string {
270+
export function unquoteSqlIdentifier(raw: string): string {
266271
const s = raw.trim();
267272
if (s.length >= 2) {
268273
const first = s[0];
@@ -279,7 +284,7 @@ function unquoteSqlIdentifier(raw: string): string {
279284
* `"public"."sys_metadata"` → `sys_metadata`. Tokenised rather than split on
280285
* `.`, so a dot INSIDE a quoted identifier does not become a separator.
281286
*/
282-
function lastIdentifierSegment(raw: string): string {
287+
export function lastIdentifierSegment(raw: string): string {
283288
const parts = raw.match(new RegExp(SQL_IDENTIFIER, 'g'));
284289
return unquoteSqlIdentifier(parts && parts.length > 0 ? parts[parts.length - 1] : raw);
285290
}
@@ -1174,7 +1179,7 @@ function backendStatementFaultError(object: string, cause: unknown, targetedTabl
11741179
* @param cause - the dialect error, kept whole for the log and for
11751180
* cause-following predicates.
11761181
*/
1177-
function rawStatementFaultError(cause: unknown): Error {
1182+
export function rawStatementFaultError(cause: unknown): Error {
11781183
const err = new Error(
11791184
'The database refused to run a raw statement. The driver could not attribute the failure ' +
11801185
'to any part of the request, so no verdict about the statement is claimed here. The ' +
@@ -10820,21 +10825,12 @@ export class SqlDriver implements IDataDriver {
1082010825
* (`TursoDriver` in remote mode) declares through the SAME composition.
1082110826
*/
1082210827
protected rawStatementFault(command: string, error: unknown): Error {
10823-
const declared = (error as { status?: unknown } | null | undefined)?.status;
10824-
if (typeof declared === 'number') return error as Error;
10825-
10826-
const detail = (error as { message?: unknown } | null | undefined)?.message;
10827-
const code = (error as { code?: unknown } | null | undefined)?.code;
10828-
// [#21385] ⛔ `command` is not written. It is the text the caller sent, and a
10829-
// raw statement may spell its values inline as well as bind them; the
10830-
// dialect's text leads with the same statement, compiled, and is cut below.
10831-
this.logger.warn(
10832-
'[sql-driver] DATABASE_ERROR — the backend refused a raw statement' +
10833-
(typeof code === 'string' && code.length > 0 ? ` (${code})` : '') +
10834-
'. The dialect diagnostic, with the statement and its bound literals cut: ' +
10835-
redactStatementFromMessage(typeof detail === 'string' ? detail : String(error), OWN_STATEMENT_FAULT),
10836-
);
10837-
return rawStatementFaultError(error);
10828+
// [#25521 cut 2] The body moved to `sql-driver-raw-execution.ts` (see `RawExecutionHost`).
10829+
const readLogger = () => this.logger;
10830+
const host: ThisParameterType<typeof rawStatementFault> = {
10831+
get logger() { return readLogger(); },
10832+
};
10833+
return rawStatementFault.call(host, command, error);
1083810834
}
1083910835

1084010836
/**
@@ -10860,27 +10856,15 @@ export class SqlDriver implements IDataDriver {
1086010856
* a caller dereference the result unchecked; pinned by
1086110857
* `sql-driver-doors-declared-types.test.ts`.
1086210858
*/
10863-
async execute(command: any, params?: any[], options?: DriverOptions): Promise<unknown> {
10864-
if (typeof command !== 'string') {
10865-
return command;
10866-
}
10867-
10868-
const builder =
10869-
options?.transaction
10870-
? this.knex.raw(command, params || []).transacting(options.transaction as Knex.Transaction)
10871-
: this.knex.raw(command, params || []);
10872-
10873-
let result: unknown;
10874-
try {
10875-
result = await builder;
10876-
} catch (error) {
10877-
// [#16019] The raw path's terminal — see {@link SqlDriver.rawStatementFault}.
10878-
throw this.rawStatementFault(command, error);
10879-
}
10880-
// Only after the statement actually succeeded — an index we failed to
10881-
// create is not one we own (#4884).
10882-
if (INDEX_DDL_PREFIX.test(command)) this.noteRuntimeIndexDdl(command);
10883-
return result;
10859+
execute(command: any, params?: any[], options?: DriverOptions): Promise<unknown> {
10860+
// [#25521 cut 2] The body moved to `sql-driver-raw-execution.ts` (see `RawExecutionHost`).
10861+
const readKnex = () => this.knex;
10862+
const host: ThisParameterType<typeof execute> = {
10863+
get knex() { return readKnex(); },
10864+
noteRuntimeIndexDdl: (...a) => this.noteRuntimeIndexDdl(...a),
10865+
rawStatementFault: (...a) => this.rawStatementFault(...a),
10866+
};
10867+
return execute.call(host, command, params, options);
1088410868
}
1088510869

1088610870
/**
@@ -10904,20 +10888,12 @@ export class SqlDriver implements IDataDriver {
1090410888

1090510889
/** Record (or, on a DROP, forget) an index this driver just created via raw DDL. */
1090610890
protected noteRuntimeIndexDdl(sql: string): void {
10907-
const created = CREATE_INDEX_DDL.exec(sql);
10908-
if (created) {
10909-
const table = lastIdentifierSegment(created[2]);
10910-
let names = this.runtimeCreatedIndexes.get(table);
10911-
if (!names) this.runtimeCreatedIndexes.set(table, (names = new Set<string>()));
10912-
names.add(unquoteSqlIdentifier(created[1]));
10913-
return;
10914-
}
10915-
const dropped = DROP_INDEX_DDL.exec(sql);
10916-
if (!dropped) return;
10917-
// SQLite / Postgres `DROP INDEX` names no table, so forget the name
10918-
// wherever it is recorded — the ledger must never outlive the index.
10919-
const name = lastIdentifierSegment(dropped[1]);
10920-
for (const names of this.runtimeCreatedIndexes.values()) names.delete(name);
10891+
// [#25521 cut 2] The body moved to `sql-driver-raw-execution.ts` (see `RawExecutionHost`).
10892+
const readRuntimeCreatedIndexes = () => this.runtimeCreatedIndexes;
10893+
const host: ThisParameterType<typeof noteRuntimeIndexDdl> = {
10894+
get runtimeCreatedIndexes() { return readRuntimeCreatedIndexes(); },
10895+
};
10896+
return noteRuntimeIndexDdl.call(host, sql);
1092110897
}
1092210898

1092310899
// ===================================

0 commit comments

Comments
 (0)