import type { ScopedSql } from './scope-host.js'; /** * Refuse a statement whose write target is a `_substrat_*` table. * * Throws a `forbidden` (`reason: 'spine_write'`) — module code reaching the spine * is a fault in the module, not in the caller's permissions, and the message names * the table so the author sees which line to delete. */ export declare function assertNoSpineWrite(sql: string, statefulTables?: ReadonlySet): void; /** * Refuse runtime DDL that would take a stateful table's guarantees away (#119, Codex r3). * * #1811 made a module's own DDL through `ctx.sql` a supported path, and it stays one. But a table * that carries archive/trash state is guarded by derived triggers ON THAT TABLE, and DDL can move * the rows out from under them: `ALTER TABLE docs RENAME TO x; CREATE TABLE docs AS SELECT …, * NULL AS _substrat_trashed_at FROM x` un-trashes everything with no move and no event. So, for a * stateful table, in any spelling — quoted, any case, `main.`/`temp.`-qualified: * * - `ALTER TABLE ` that renames the table or a column, or drops a column; and `RENAME TO ` * from another table; * - `DROP TABLE `; * - `CREATE [TEMP] TABLE|VIEW ` — a temp object of the same name SHADOWS the real table in * every unqualified reference, the kernel's own included; * - `CREATE [TEMP] TRIGGER … ON `. * * Plus, for any table, a trigger, index or view carrying the reserved prefix (the kernel's derived * objects), and `ATTACH`/`DETACH` outright: nothing in module code needs a second database, and an * attached one is a second place a stateful name can resolve. Ordinary runtime DDL — a module's own * unrelated tables, an index on a stateful table, an added column — is untouched. */ export declare function assertNoStatefulDdl(sql: string, statefulTables: ReadonlySet): void; /** * Whether `pieces` — `splitSqlStatements(sql)` — are the whole of `sql`: each found, in order, as * an unaltered substring, and nothing left between or after them but whitespace, `;` and * comments. The splitter never rewrites text, so this holds by construction; it is the check a * multi-statement call is refused on rather than run in pieces nobody can account for. */ export declare function piecesReproduce(sql: string, pieces: readonly string[]): boolean; /** Does this SQL change the schema — any statement in it a CREATE, ALTER or DROP? */ export declare function changesSchema(sql: string): boolean; /** * Refuse a statement that WRITES a `_substrat_*` column of a module's own table (#119). * * The kernel keeps state on module rows — `_substrat_archived_at`, `_substrat_trashed_at` — * and a row's way into or out of the trash must be `ctx.trash`/`ctx.restore`, which check the * declared key and emit the event. A module that could `UPDATE todo_lists SET * _substrat_trashed_at = NULL` would restore without the key and without a record, which is * the forgery the table rule above exists to stop, one level down. * * Judged by POSITION, so reading the columns stays allowed — in a `SELECT`, and in the `WHERE` * of a write (`UPDATE … SET done = 1 WHERE _substrat_trashed_at IS NULL`). Refused: * * - an assignment target after `SET` — `UPDATE … SET`, an upsert's `DO UPDATE SET`, and a * trigger body's `UPDATE`, row-value targets `SET (a, b) = …` included; * - a name in an `INSERT`/`REPLACE` column list; * - any reserved name in an `ALTER TABLE` (add, rename or drop the column itself); * - into a table that CARRIES the columns (`statefulTables`, lowercased names): an `INSERT` * with no column list — `VALUES (…)` and `SELECT …` fill the columns by position, the * reserved ones included, without ever naming them — and any `REPLACE` / `INSERT OR * REPLACE`, which deletes the existing row and writes a new one with the columns NULL, so * it un-archives and un-trashes without the key. An `INSERT … (columns) SELECT` and an * upsert's `DO UPDATE SET` are judged by the rules above. * * Below this sits a trigger on each such table that refuses a row born archived or trashed * (`entityStateMigrations`), so a form this scan misses still cannot set the state. * * The prefix is reserved whole, not the two names: the next kernel-owned column is covered * without anyone remembering to list it here. */ export declare function assertNoReservedColumnWrite(sql: string, statefulTables?: ReadonlySet): void; /** * Refuse a statement whose `REFERENCES` clause names a `_substrat_*` table (#1898), for the * same reason `CREATE TRIGGER … ON` is refused above: the spine's rows become the parent of a * module's, and with foreign keys enforced the kernel's own writes to that table (a revoke, a * restore's re-point, an outbox prune) then fail on the module's rows. That denies the spine * rather than forging it. `what` names the SQL's source in the message. * * `ctx.sql` passes through here (`assertNoSpineWrite`), and so does every migration a scope * applies, which reaches the database on the kernel's own handle rather than through * `ctx.sql`. A dump's replayed DDL is held to the same rule by `assertReplayableDump`. */ export declare function assertNoSpineReference(sql: string, what: string): void; /** * Wrap a module-facing `ScopedSql` so every statement passes `assertNoSpineWrite` * first. `query` is guarded too: SQLite runs `INSERT … RETURNING` perfectly well * through a `.all()`, so guarding only `exec` would leave the door open. */ export declare function guardSpine(inner: ScopedSql, /** The module tables that carry archive/trash columns (#119), lowercased — see `assertNoReservedColumnWrite`. */ statefulTables?: ReadonlySet, /** * Run after any statement that changed the schema passed (#119, Codex r3): the kernel's own * check that every stateful table still carries what it derived. It throws to fail closed — * the operation, and the DDL with it, roll back. */ afterDdl?: () => void): ScopedSql; //# sourceMappingURL=spine-guard.d.ts.map