Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions packages/drivers/driver-sql/src/sql-driver-raw-execution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* [#25521 cut 2] The raw-execution path of `SqlDriver`: `execute`, its refusal
* terminal `rawStatementFault`, and `noteRuntimeIndexDdl`, which records the
* index DDL `execute` ran in the `runtimeCreatedIndexes` ledger. They moved out
* of `sql-driver.ts` whole.
*
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
* method of the same name as it stood, re-indented one level and otherwise
* unedited: the same statement handling, the same redacted refusal line, the
* same ledger rules.
*
* ## How the moved bodies reach the class
*
* The shape `sql-driver-introspection.ts` (cut 1) applies, after #23050. The
* class keeps every method, because the published `dist/*.d.ts` lists each
* one's name, docblock and position. `TursoDriver` overrides `execute` and
* calls `super.execute`, and calls `rawStatementFault` from its remote
* transport. Each body is now one literal of exactly the
* {@link RawExecutionHost} members that method reads, plus a `.call`. `execute`
* is no longer `async`: it returns the moved function's own promise, so a
* caller awaits the same promise the body settles, with no extra wrapper.
*
* `execute` reads `rawStatementFault` and `noteRuntimeIndexDdl` through
* closures that call them on the driver at call time. So a subclass override
* (a test subclass overrides `rawStatementFault` and calls `super`) or a test
* spy on the instance is still the one the body reaches.
*
* The fields `knex`, `logger` and `runtimeCreatedIndexes` go as GETTERS, so
* each read happens when the body makes it, as it did on the class. The
* ledger itself stays a field of the class: schema sync reads it as well.
*
* The module-level declarations the bodies read stay in `sql-driver.ts`:
* `OWN_STATEMENT_FAULT`, `rawStatementFaultError`, the three index-DDL patterns
* and the two identifier helpers. They are exported from there and imported
* here. Other packages' tests cite `rawStatementFaultError` in `sql-driver.ts`
* by path, and `OWN_STATEMENT_FAULT` has readers that did not move. The module
* cycle is evaluation-safe because each value import is read only inside a
* function body, never while the two modules are first evaluated. `index.ts`
* re-exports nothing from this module, so the published surface does not move.
*/

import type { DriverOptions } from '@objectstack/spec/data';
import { redactStatementFromMessage } from '@objectstack/types';
import type { Knex } from 'knex';
import type { SqlDriver } from './sql-driver.js';
import {
CREATE_INDEX_DDL,
DROP_INDEX_DDL,
INDEX_DDL_PREFIX,
lastIdentifierSegment,
OWN_STATEMENT_FAULT,
rawStatementFaultError,
unquoteSqlIdentifier,
} from './sql-driver.js';

/**
* The `SqlDriver` members the functions below read — every one of them, and
* no other; each function's `this` is the `Pick` of the members IT reads. Each
* is typed AS the class member (an indexed access on the class), so a change
* to a member's signature on the class is a change here too, and naming a
* member the class does not have is a compile error. See the module header
* for which members are getters.
*/
export interface RawExecutionHost {
readonly knex: SqlDriver['knex'];
readonly logger: SqlDriver['logger'];
noteRuntimeIndexDdl: SqlDriver['noteRuntimeIndexDdl'];
rawStatementFault: SqlDriver['rawStatementFault'];
readonly runtimeCreatedIndexes: SqlDriver['runtimeCreatedIndexes'];
}

/** The body of `SqlDriver#rawStatementFault`; the method's docblock there is its contract. */
export function rawStatementFault(this: Pick<RawExecutionHost, 'logger'>, command: string, error: unknown): Error {
const declared = (error as { status?: unknown } | null | undefined)?.status;
if (typeof declared === 'number') return error as Error;

const detail = (error as { message?: unknown } | null | undefined)?.message;
const code = (error as { code?: unknown } | null | undefined)?.code;
// [#21385] ⛔ `command` is not written. It is the text the caller sent, and a
// raw statement may spell its values inline as well as bind them; the
// dialect's text leads with the same statement, compiled, and is cut below.
this.logger.warn(
'[sql-driver] DATABASE_ERROR — the backend refused a raw statement' +
(typeof code === 'string' && code.length > 0 ? ` (${code})` : '') +
'. The dialect diagnostic, with the statement and its bound literals cut: ' +
redactStatementFromMessage(typeof detail === 'string' ? detail : String(error), OWN_STATEMENT_FAULT),
);
return rawStatementFaultError(error);
}

/** The body of `SqlDriver#execute`; the method's docblock there is its contract. */
export async function execute(this: Pick<RawExecutionHost, 'knex' | 'noteRuntimeIndexDdl' | 'rawStatementFault'>, command: any, params?: any[], options?: DriverOptions): Promise<unknown> {
if (typeof command !== 'string') {
return command;
}

const builder =
options?.transaction
? this.knex.raw(command, params || []).transacting(options.transaction as Knex.Transaction)
: this.knex.raw(command, params || []);

let result: unknown;
try {
result = await builder;
} catch (error) {
// [#16019] The raw path's terminal — see {@link SqlDriver.rawStatementFault}.
throw this.rawStatementFault(command, error);
}
// Only after the statement actually succeeded — an index we failed to
// create is not one we own (#4884).
if (INDEX_DDL_PREFIX.test(command)) this.noteRuntimeIndexDdl(command);
return result;
}

/** The body of `SqlDriver#noteRuntimeIndexDdl`; the method's docblock there is its contract. */
export function noteRuntimeIndexDdl(this: Pick<RawExecutionHost, 'runtimeCreatedIndexes'>, sql: string): void {
const created = CREATE_INDEX_DDL.exec(sql);
if (created) {
const table = lastIdentifierSegment(created[2]);
let names = this.runtimeCreatedIndexes.get(table);
if (!names) this.runtimeCreatedIndexes.set(table, (names = new Set<string>()));
names.add(unquoteSqlIdentifier(created[1]));
return;
}
const dropped = DROP_INDEX_DDL.exec(sql);
if (!dropped) return;
// SQLite / Postgres `DROP INDEX` names no table, so forget the name
// wherever it is recorded — the ledger must never outlive the index.
const name = lastIdentifierSegment(dropped[1]);
for (const names of this.runtimeCreatedIndexes.values()) names.delete(name);
}
90 changes: 33 additions & 57 deletions packages/drivers/driver-sql/src/sql-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ import {
introspectPrimaryKeys,
introspectUniqueConstraints,
} from './sql-driver-introspection.js';
// [#25521 cut 2] The raw-execution path (`execute`, its refusal terminal
// `rawStatementFault`, and `noteRuntimeIndexDdl`, which keeps the ledger of the
// index DDL `execute` ran); the methods of the same names below hand each body
// the members it reads (`RawExecutionHost`).
import { execute, noteRuntimeIndexDdl, rawStatementFault } from './sql-driver-raw-execution.js';
import { makeDatabaseDirectory, probeAbsentSqliteDatabaseFile, SqliteDatabaseFileError } from './sqlite-database-file.js';
import { jsonMembershipPredicate } from '@objectstack/core';
import knex, { Knex } from 'knex';
Expand Down Expand Up @@ -243,26 +248,26 @@ const DEFAULT_ID_LENGTH = 16;
* text whatever word it opens with. The shared cut then runs without asking the
* leak predicate, exactly as it does at the engine's raw-statement door.
*/
const OWN_STATEMENT_FAULT = { statementSent: true } as const;
export const OWN_STATEMENT_FAULT = { statementSent: true } as const;

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

/** Peel one layer of `"…"` / `` `…` `` / `[…]` quoting off an identifier. */
function unquoteSqlIdentifier(raw: string): string {
export function unquoteSqlIdentifier(raw: string): string {
const s = raw.trim();
if (s.length >= 2) {
const first = s[0];
Expand All @@ -279,7 +284,7 @@ function unquoteSqlIdentifier(raw: string): string {
* `"public"."sys_metadata"` → `sys_metadata`. Tokenised rather than split on
* `.`, so a dot INSIDE a quoted identifier does not become a separator.
*/
function lastIdentifierSegment(raw: string): string {
export function lastIdentifierSegment(raw: string): string {
const parts = raw.match(new RegExp(SQL_IDENTIFIER, 'g'));
return unquoteSqlIdentifier(parts && parts.length > 0 ? parts[parts.length - 1] : raw);
}
Expand Down Expand Up @@ -1174,7 +1179,7 @@ function backendStatementFaultError(object: string, cause: unknown, targetedTabl
* @param cause - the dialect error, kept whole for the log and for
* cause-following predicates.
*/
function rawStatementFaultError(cause: unknown): Error {
export function rawStatementFaultError(cause: unknown): Error {
const err = new Error(
'The database refused to run a raw statement. The driver could not attribute the failure ' +
'to any part of the request, so no verdict about the statement is claimed here. The ' +
Expand Down Expand Up @@ -10820,21 +10825,12 @@ export class SqlDriver implements IDataDriver {
* (`TursoDriver` in remote mode) declares through the SAME composition.
*/
protected rawStatementFault(command: string, error: unknown): Error {
const declared = (error as { status?: unknown } | null | undefined)?.status;
if (typeof declared === 'number') return error as Error;

const detail = (error as { message?: unknown } | null | undefined)?.message;
const code = (error as { code?: unknown } | null | undefined)?.code;
// [#21385] ⛔ `command` is not written. It is the text the caller sent, and a
// raw statement may spell its values inline as well as bind them; the
// dialect's text leads with the same statement, compiled, and is cut below.
this.logger.warn(
'[sql-driver] DATABASE_ERROR — the backend refused a raw statement' +
(typeof code === 'string' && code.length > 0 ? ` (${code})` : '') +
'. The dialect diagnostic, with the statement and its bound literals cut: ' +
redactStatementFromMessage(typeof detail === 'string' ? detail : String(error), OWN_STATEMENT_FAULT),
);
return rawStatementFaultError(error);
// [#25521 cut 2] The body moved to `sql-driver-raw-execution.ts` (see `RawExecutionHost`).
const readLogger = () => this.logger;
const host: ThisParameterType<typeof rawStatementFault> = {
get logger() { return readLogger(); },
};
return rawStatementFault.call(host, command, error);
}

/**
Expand All @@ -10860,27 +10856,15 @@ export class SqlDriver implements IDataDriver {
* a caller dereference the result unchecked; pinned by
* `sql-driver-doors-declared-types.test.ts`.
*/
async execute(command: any, params?: any[], options?: DriverOptions): Promise<unknown> {
if (typeof command !== 'string') {
return command;
}

const builder =
options?.transaction
? this.knex.raw(command, params || []).transacting(options.transaction as Knex.Transaction)
: this.knex.raw(command, params || []);

let result: unknown;
try {
result = await builder;
} catch (error) {
// [#16019] The raw path's terminal — see {@link SqlDriver.rawStatementFault}.
throw this.rawStatementFault(command, error);
}
// Only after the statement actually succeeded — an index we failed to
// create is not one we own (#4884).
if (INDEX_DDL_PREFIX.test(command)) this.noteRuntimeIndexDdl(command);
return result;
execute(command: any, params?: any[], options?: DriverOptions): Promise<unknown> {
// [#25521 cut 2] The body moved to `sql-driver-raw-execution.ts` (see `RawExecutionHost`).
const readKnex = () => this.knex;
const host: ThisParameterType<typeof execute> = {
get knex() { return readKnex(); },
noteRuntimeIndexDdl: (...a) => this.noteRuntimeIndexDdl(...a),
rawStatementFault: (...a) => this.rawStatementFault(...a),
};
return execute.call(host, command, params, options);
}

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

/** Record (or, on a DROP, forget) an index this driver just created via raw DDL. */
protected noteRuntimeIndexDdl(sql: string): void {
const created = CREATE_INDEX_DDL.exec(sql);
if (created) {
const table = lastIdentifierSegment(created[2]);
let names = this.runtimeCreatedIndexes.get(table);
if (!names) this.runtimeCreatedIndexes.set(table, (names = new Set<string>()));
names.add(unquoteSqlIdentifier(created[1]));
return;
}
const dropped = DROP_INDEX_DDL.exec(sql);
if (!dropped) return;
// SQLite / Postgres `DROP INDEX` names no table, so forget the name
// wherever it is recorded — the ledger must never outlive the index.
const name = lastIdentifierSegment(dropped[1]);
for (const names of this.runtimeCreatedIndexes.values()) names.delete(name);
// [#25521 cut 2] The body moved to `sql-driver-raw-execution.ts` (see `RawExecutionHost`).
const readRuntimeCreatedIndexes = () => this.runtimeCreatedIndexes;
const host: ThisParameterType<typeof noteRuntimeIndexDdl> = {
get runtimeCreatedIndexes() { return readRuntimeCreatedIndexes(); },
};
return noteRuntimeIndexDdl.call(host, sql);
}

// ===================================
Expand Down
Loading