Skip to content

Commit 94bd4c0

Browse files
committed
refactor(driver-sql): regenerate the split on origin/main 2ea6042
Output of `node scripts/codemod-split-sql-driver.mjs --write` on sql-driver.ts at origin/main 2ea6042 (input sha256 9abf7867fd98; #29660 touched it), unedited: 54 method bodies move to 18 section modules and 96 of the 129 top-level declarations ahead of the class to sql-driver-top-level.ts; sql-driver.ts 18330 -> 14050 lines. Output tree sha256 ab8990700a0d, the same on two runs. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2gLghEqeDKTbDTC7Zd
1 parent 272bd65 commit 94bd4c0

20 files changed

Lines changed: 6631 additions & 4885 deletions

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

Lines changed: 344 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 354 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#29520 codemod] The "Column creation helper" section of `SqlDriver`: the
5+
* bodies of `decorateHashShadowDuplicate`, `schemaBytesPerChar`,
6+
* `rethrowWithRowSizeExplanation` moved out of `sql-driver.ts` whole by
7+
* `scripts/codemod-split-sql-driver.mjs`.
8+
*
9+
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
10+
* method of the same name exactly as it stood: not re-indented, not
11+
* formatted, not edited. The method stays on the class with its docblock and
12+
* signature, so the published `dist/*.d.ts`, an instance spy and a subclass
13+
* override are what they were; the docblock there is the contract.
14+
*
15+
* The bodies reach the class through {@link ColumnCreationHost}: exactly the
16+
* `SqlDriver` members they read. Each method hands its body one literal of the
17+
* members that body reads, plus a `.call`: methods go as closures that call
18+
* them on the driver at call time (the builder factory as a bound reference),
19+
* so a subclass override or an instance spy is still the one reached; fields
20+
* and accessors go as GETTERS, read when the body reads them, as they were on
21+
* the class.
22+
*
23+
* The module imports `sql-driver.ts` for types only, so the two modules form
24+
* no evaluation cycle. `index.ts` re-exports nothing from this module.
25+
*/
26+
27+
import type { SqlDriver } from './sql-driver.js';
28+
29+
/**
30+
* The `SqlDriver` members the functions below read — every one of them, and
31+
* no other. Each is typed AS the class member (an indexed access on the
32+
* class), so a change to a member's signature on the class is a change here
33+
* too. `readonly` members are the getters.
34+
*/
35+
export interface ColumnCreationHost {
36+
explainHashShadowDuplicate: SqlDriver['explainHashShadowDuplicate'];
37+
explainRowSizeOverflow: SqlDriver['explainRowSizeOverflow'];
38+
readonly knex: SqlDriver['knex'];
39+
logDurabilityFailure: SqlDriver['logDurabilityFailure'];
40+
}
41+
42+
/** The body of `SqlDriver#decorateHashShadowDuplicate`; the method's docblock there is its contract. */
43+
export async function decorateHashShadowDuplicate(this: Pick<ColumnCreationHost, 'explainHashShadowDuplicate' | 'logDurabilityFailure'>, tableName: string, values: Record<string, unknown>, error: unknown): Promise<unknown> {
44+
let explanation: string | null = null;
45+
try {
46+
explanation = await this.explainHashShadowDuplicate(tableName, values, error);
47+
} catch {
48+
// The disambiguation is a diagnostic. If it cannot be made, the original
49+
// failure is still the failure — never mask it with this one.
50+
return error;
51+
}
52+
if (!explanation) return error;
53+
this.logDurabilityFailure(explanation);
54+
return Object.assign(new Error(`${explanation} (server said: ${String((error as { message?: string })?.message ?? error)})`), {
55+
code: (error as { code?: string })?.code,
56+
cause: error,
57+
});
58+
}
59+
60+
/** The body of `SqlDriver#schemaBytesPerChar`; the method's docblock there is its contract. */
61+
export async function schemaBytesPerChar(this: Pick<ColumnCreationHost, 'knex'>): Promise<{ bytesPerChar: number; charset: string } | null> {
62+
try {
63+
const res: any = await this.knex.raw(
64+
'select cs.CHARACTER_SET_NAME as charset, cs.MAXLEN as maxlen ' +
65+
'from information_schema.CHARACTER_SETS cs ' +
66+
'where cs.CHARACTER_SET_NAME = @@character_set_database',
67+
);
68+
const row = (Array.isArray(res) ? res[0]?.[0] : (res?.rows?.[0] ?? res?.[0])) ?? null;
69+
const maxlen = Number(row?.maxlen ?? row?.MAXLEN);
70+
const charset = String(row?.charset ?? row?.CHARSET ?? '');
71+
if (!Number.isInteger(maxlen) || maxlen <= 0 || !charset) return null;
72+
return { bytesPerChar: maxlen, charset };
73+
} catch {
74+
// Introspection is a nicety here; the advice below holds without it.
75+
return null;
76+
}
77+
}
78+
79+
/** The body of `SqlDriver#rethrowWithRowSizeExplanation`; the method's docblock there is its contract. */
80+
export async function rethrowWithRowSizeExplanation(this: Pick<ColumnCreationHost, 'explainRowSizeOverflow' | 'logDurabilityFailure'>, tableName: string, fields: Record<string, any> | undefined, keyedColumns: ReadonlyMap<string, { unique: boolean }>, addedColumns: string[] | null, e: any): Promise<never> {
81+
const overflow = await this.explainRowSizeOverflow(tableName, fields, keyedColumns, addedColumns, e);
82+
if (!overflow) throw e;
83+
const said = String(e?.sqlMessage ?? e?.message ?? e);
84+
this.logDurabilityFailure(overflow, said);
85+
throw Object.assign(new Error(`${overflow} (server said: ${said})`), {
86+
code: (e as { code?: string }).code,
87+
cause: e,
88+
});
89+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#29520 codemod] The "CRUD — IDataDriver core" section of `SqlDriver`: the
5+
* bodies of `create`, `readBackInsertedRows`, `sequenceKeyHash` moved out of
6+
* `sql-driver.ts` whole by `scripts/codemod-split-sql-driver.mjs`.
7+
*
8+
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
9+
* method of the same name exactly as it stood: not re-indented, not
10+
* formatted, not edited. The method stays on the class with its docblock and
11+
* signature, so the published `dist/*.d.ts`, an instance spy and a subclass
12+
* override are what they were; the docblock there is the contract.
13+
*
14+
* The bodies reach the class through {@link CrudHost}: exactly the
15+
* `SqlDriver` members they read. Each method hands its body one literal of the
16+
* members that body reads, plus a `.call`: methods go as closures that call
17+
* them on the driver at call time (the builder factory as a bound reference),
18+
* so a subclass override or an instance spy is still the one reached; fields
19+
* and accessors go as GETTERS, read when the body reads them, as they were on
20+
* the class.
21+
*
22+
* The module imports `sql-driver.ts` for types only, so the two modules form
23+
* no evaluation cycle. `index.ts` re-exports nothing from this module.
24+
*/
25+
26+
import type { DriverOptions } from '@objectstack/spec/data';
27+
import { nanoid } from 'nanoid';
28+
import { createHash } from 'node:crypto';
29+
import {
30+
AUTONUMBER_COLLISION_RETRIES, DEFAULT_ID_LENGTH, insertedRowsNotReadBackError,
31+
} from './sql-driver-top-level.js';
32+
import type { SqlDriver } from './sql-driver.js';
33+
34+
/**
35+
* The `SqlDriver` members the functions below read — every one of them, and
36+
* no other. Each is typed AS the class member (an indexed access on the
37+
* class), so a change to a member's signature on the class is a change here
38+
* too. `readonly` members are the getters.
39+
*/
40+
export interface CrudHost {
41+
applyTenantScope: SqlDriver['applyTenantScope'];
42+
applyWriteColumnMap: SqlDriver['applyWriteColumnMap'];
43+
auditMissingTenant: SqlDriver['auditMissingTenant'];
44+
collidingAutoNumberReservations: SqlDriver['collidingAutoNumberReservations'];
45+
decorateHashShadowDuplicate: SqlDriver['decorateHashShadowDuplicate'];
46+
fillAutoNumberFields: SqlDriver['fillAutoNumberFields'];
47+
formatInput: SqlDriver['formatInput'];
48+
formatOutput: SqlDriver['formatOutput'];
49+
getBuilder: SqlDriver['getBuilder'];
50+
injectTenantOnInsert: SqlDriver['injectTenantOnInsert'];
51+
readonly insertReturnsStoredRows: SqlDriver['insertReturnsStoredRows'];
52+
readBackInsertedRows: SqlDriver['readBackInsertedRows'];
53+
remoteColumn: SqlDriver['remoteColumn'];
54+
resolveTenantField: SqlDriver['resolveTenantField'];
55+
resyncSequenceToDataMax: SqlDriver['resyncSequenceToDataMax'];
56+
rotationWriteTarget: SqlDriver['rotationWriteTarget'];
57+
stampInsertTimestamps: SqlDriver['stampInsertTimestamps'];
58+
}
59+
60+
/** The body of `SqlDriver#create`; the method's docblock there is its contract. */
61+
export async function create(this: Pick<CrudHost, 'applyWriteColumnMap' | 'auditMissingTenant' | 'collidingAutoNumberReservations' | 'decorateHashShadowDuplicate' | 'fillAutoNumberFields' | 'formatInput' | 'formatOutput' | 'getBuilder' | 'injectTenantOnInsert' | 'insertReturnsStoredRows' | 'readBackInsertedRows' | 'resyncSequenceToDataMax' | 'rotationWriteTarget' | 'stampInsertTimestamps'>, object: string, data: Record<string, any>, options?: DriverOptions): Promise<Record<string, unknown>> {
62+
const { _id, ...rest } = data;
63+
const toInsert = { ...rest };
64+
65+
if (_id !== undefined && toInsert.id === undefined) {
66+
toInsert.id = _id;
67+
} else if (toInsert.id === undefined) {
68+
toInsert.id = nanoid(DEFAULT_ID_LENGTH);
69+
}
70+
71+
this.auditMissingTenant(object, 'create', options, [toInsert]);
72+
this.injectTenantOnInsert(object, toInsert, options);
73+
74+
// #5495. The autonumber counter is bootstrapped from the data table exactly
75+
// once, so any row that lands by a path bypassing `fillAutoNumberFields`
76+
// leaves it permanently behind `MAX` — and every create then collided,
77+
// burned a number and failed the request until it had ground past the
78+
// seeded range one 409 at a time. Re-seed and retry instead, but ONLY when
79+
// the collision is provably this counter's; see
80+
// {@link collidingAutoNumberReservations} for how that is decided and why
81+
// it cannot be decided from the error text alone.
82+
//
83+
// Retrying is confined to the no-caller-transaction case on purpose. Inside
84+
// a caller's transaction there is nothing to repair here — the sequence
85+
// UPDATE shares that transaction and rolls back with the refused INSERT, so
86+
// no number is burned (measured) — and there is something to break: on
87+
// Postgres a constraint failure aborts the whole transaction, so a retry
88+
// issued on it could not succeed anyway. The caller owns that retry.
89+
const mayRetry = options?.transaction === undefined;
90+
for (let attempt = 0; ; attempt++) {
91+
const reservations = await this.fillAutoNumberFields(object, toInsert, options);
92+
93+
// Rotation (ADR-0057 P2): the base name is a read-only view — new rows
94+
// land in the current shard.
95+
const builder = this.getBuilder(this.rotationWriteTarget(object) ?? object, options);
96+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toInsert));
97+
this.stampInsertTimestamps(object, formatted);
98+
99+
try {
100+
if (this.insertReturnsStoredRows) {
101+
const result = await builder.insert(formatted).returning('*');
102+
return this.formatOutput(object, result[0]);
103+
}
104+
await builder.insert(formatted);
105+
} catch (error) {
106+
// #11627: on a table whose UNIQUE index is carried by a hash shadow,
107+
// `ER_DUP_ENTRY` quotes a binary digest and names the shadow index, so
108+
// neither the operator nor this driver can read WHICH value conflicted
109+
// — and a genuine duplicate is indistinguishable from a (near-
110+
// impossible) digest collision. Resolve it by reading the source
111+
// columns back, on the failure path only, and re-throw the SAME error
112+
// carrying whichever answer is true. Returns the error untouched when
113+
// this is not a shadow conflict.
114+
const shadowed = await this.decorateHashShadowDuplicate(
115+
this.rotationWriteTarget(object) ?? object,
116+
formatted,
117+
error,
118+
);
119+
if (!mayRetry || attempt >= AUTONUMBER_COLLISION_RETRIES) throw shadowed;
120+
const colliding = await this.collidingAutoNumberReservations(error, reservations, options);
121+
if (colliding.length === 0) throw shadowed;
122+
for (const reservation of colliding) {
123+
await this.resyncSequenceToDataMax(reservation);
124+
// Clear only what collided, so the next pass regenerates exactly
125+
// those fields. A reservation that did NOT collide keeps its value
126+
// rather than burning a second number for nothing.
127+
delete toInsert[reservation.field];
128+
}
129+
continue;
130+
}
131+
132+
// [#21227] Reached only when the INSERT landed and answered no rows. The
133+
// read sits OUTSIDE the try on purpose: a fault in it is not an insert
134+
// failure, so it must never reach the collision re-seed above, whose
135+
// retry would re-issue a write that already landed.
136+
const [stored] = await this.readBackInsertedRows(
137+
object,
138+
this.rotationWriteTarget(object) ?? object,
139+
[toInsert],
140+
options,
141+
);
142+
return stored;
143+
}
144+
}
145+
146+
/** The body of `SqlDriver#readBackInsertedRows`; the method's docblock there is its contract. */
147+
export async function readBackInsertedRows(this: Pick<CrudHost, 'applyTenantScope' | 'formatOutput' | 'getBuilder' | 'remoteColumn' | 'resolveTenantField'>, object: string, writeTable: string, written: Record<string, any>[], options?: DriverOptions): Promise<Record<string, unknown>[]> {
148+
if (written.length === 0) return [];
149+
const idColumn = this.remoteColumn(object, 'id', 'id');
150+
const tenantField = this.resolveTenantField(object);
151+
const writtenTenants = tenantField
152+
? [
153+
...new Set(
154+
written
155+
.map((row) => row[tenantField])
156+
.filter((value) => value !== undefined && value !== null && value !== '')
157+
.map(String),
158+
),
159+
]
160+
: [];
161+
const everyRowCarriesTenant =
162+
tenantField !== null
163+
&& written.every((row) => row[tenantField] !== undefined && row[tenantField] !== null && row[tenantField] !== '');
164+
const scopeOptions: DriverOptions = everyRowCarriesTenant
165+
? { ...options, tenantId: writtenTenants[0], tenantIds: writtenTenants }
166+
: { ...options };
167+
const builder = this.getBuilder(writeTable, options);
168+
this.applyTenantScope(builder, object, scopeOptions);
169+
const stored: Record<string, any>[] = await builder.whereIn(
170+
idColumn,
171+
written.map((row) => row.id),
172+
);
173+
const byId = new Map<string, Record<string, any>>();
174+
for (const row of stored) byId.set(String(row[idColumn]), row);
175+
const missing = written.filter((row) => !byId.has(String(row.id))).map((row) => row.id);
176+
if (missing.length > 0) throw insertedRowsNotReadBackError(object, missing, written.length);
177+
return written.map((row) => this.formatOutput(object, byId.get(String(row.id))));
178+
}
179+
180+
/** The body of `SqlDriver#sequenceKeyHash`; the method's docblock there is its contract. */
181+
export function sequenceKeyHash(object: string, tenantId: string, field: string, scope: string): string {
182+
return createHash('sha256')
183+
.update(`${object}\u001f${tenantId}\u001f${field}\u001f${scope}`)
184+
.digest('hex');
185+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#29520 codemod] The "Database helpers" section of `SqlDriver`: the bodies of
5+
* `sqliteFilename` moved out of `sql-driver.ts` whole by
6+
* `scripts/codemod-split-sql-driver.mjs`.
7+
*
8+
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
9+
* method of the same name exactly as it stood: not re-indented, not
10+
* formatted, not edited. The method stays on the class with its docblock and
11+
* signature, so the published `dist/*.d.ts`, an instance spy and a subclass
12+
* override are what they were; the docblock there is the contract.
13+
*
14+
* The bodies reach the class through {@link DatabaseHelpersHost}: exactly the
15+
* `SqlDriver` members they read. Each method hands its body one literal of the
16+
* members that body reads, plus a `.call`: methods go as closures that call
17+
* them on the driver at call time (the builder factory as a bound reference),
18+
* so a subclass override or an instance spy is still the one reached; fields
19+
* and accessors go as GETTERS, read when the body reads them, as they were on
20+
* the class.
21+
*
22+
* The module imports `sql-driver.ts` for types only, so the two modules form
23+
* no evaluation cycle. `index.ts` re-exports nothing from this module.
24+
*/
25+
26+
import type { SqlDriver } from './sql-driver.js';
27+
28+
/**
29+
* The `SqlDriver` members the functions below read — every one of them, and
30+
* no other. Each is typed AS the class member (an indexed access on the
31+
* class), so a change to a member's signature on the class is a change here
32+
* too. `readonly` members are the getters.
33+
*/
34+
export interface DatabaseHelpersHost {
35+
readonly config: SqlDriver['config'];
36+
readonly isSqlite: SqlDriver['isSqlite'];
37+
readonly openedEmptyInMemory: SqlDriver['openedEmptyInMemory'];
38+
}
39+
40+
/** The body of `SqlDriver#sqliteFilename`; the method's docblock there is its contract. */
41+
export function sqliteFilename(this: Pick<DatabaseHelpersHost, 'config' | 'isSqlite' | 'openedEmptyInMemory'>): string | null {
42+
if (!this.isSqlite) return null;
43+
// Redirected to `:memory:` because the declared file did not exist and the
44+
// host asked not to create one (#6743). `this.config` still names the
45+
// declared target — on purpose, so the CLI can print it — but NO on-disk
46+
// file backs this connection, which is precisely what this method reports.
47+
// Both callers depend on that reading: `ensureDatabaseExists` would
48+
// otherwise `mkdir` the state directory this mode exists to avoid, and
49+
// `applySqliteJournalMode` would try to put an in-memory database in WAL.
50+
if (this.openedEmptyInMemory) return null;
51+
const conn = (this.config as any).connection;
52+
const filename = typeof conn === 'string' ? conn : conn?.filename;
53+
if (typeof filename !== 'string' || filename === '') return null;
54+
return filename.startsWith(':') ? null : filename;
55+
}

0 commit comments

Comments
 (0)