/** * What the kernel derived onto a module's tables, checked and put back (#2090). * * | Object | Is | When it is missing or not what the kernel emits | * |---|---|---| * | `_substrat_archived_at` / `_substrat_trashed_at` (#119) | state | fail closed: it cannot be invented again | * | the `born` / `moved` archive/trash guard triggers (#119) | derived | put back | * | the derived list indexes, partial per view on a stateful table (#811) | derived | put back | | the purge sweep's partial index on a trashable table with a horizon (#119) | derived | put back | * | the search index and the three triggers keeping it in step (#827) | derived | rebuilt from the rows | * * The search triggers are a privacy fact, not only a freshness one: subject erasure rewrites a * row's text and relies on the update and delete triggers to take the old text out of the index. * Without them, erased text stays searchable. * * **An object is judged by its definition, not its name.** `sqlite_master` holds each one's * CREATE statement as it was run, and the kernel knows the statement it emits * (`DerivedObject`), so a same-named trigger that does nothing, an index on the wrong columns or * with the wrong partial predicate, and an object filed under another table are all "not * there". The text is compared exactly: nothing but the kernel writes these objects, and a * normalisation that forgave whitespace would forgive it inside a string literal too — a guard * keyed on a different entity type. An object an older kernel worded differently is re-created * once. * * Four places ask, and all four come here, so "what a table is owed" is written once: * * - **runtime DDL** (`ctx.sql`'s after-DDL hook, #1811) repairs inside the operation's * transaction, whenever the scope has any derived plan. The text guard already refuses the DDL * known to move a stateful table's rows; a search-only or list-only table has no such guard, * and a module may rebuild it — this puts its triggers and indexes back before the operation * commits. A lost state column fails the operation, and the DDL with it. * - **a migration** checks the state columns after its own SQL, inside its own transaction, so a * migration that drops one rolls back with nothing applied. The last migration of a pass then * repairs the derived objects, inside that same transaction: SQLite's ordinary * create-copy-rename rebuild of a table drops its triggers and indexes with it, and the * migrations that made them are journaled, so they never run again. Only after the last, * because a rebuild split over two migrations copies its rows in the second, and a guard put * back in between would refuse the archived ones — which also means such a split must ship * in ONE deploy: split over two, the guard is back before the copy runs. * - **a wake with nothing pending** (and every `migrateScope`) repairs the same way, once per * instance. That is what reaches a scope stripped before this existed, whose migrations are * all journaled and will never run again. * - **a restore** repairs after the rows, inside the load's transaction, which may legitimately * bring them archived or trashed. * * ## What the journal says a table is owed * * Every expectation comes from `_substrat_migrations`, not only from the declarations: a scope * mid-upgrade has declared a trash its column migration has not added yet, and a restored dump * carries the journal of the scope it came from. A column is owed once the migration that added * it is journaled; a derived object once the migration that derived it, for the CURRENT * declaration, is. An object from a withdrawn declaration is not this module's business. */ import { SubstratError } from '@substrat-run/contracts'; import { type EntityStatePlan } from './entity-state.js'; import { type ListIndexPlan } from './list-index.js'; import type { ScopedSql } from './scope-host.js'; import { type SearchIndexPlan } from './search-index.js'; /** A scope's derivation inputs: every registered module's plans, entity type → plan. */ export interface DerivedPlans { readonly state: ReadonlyMap; readonly lists: ReadonlyMap; readonly search: ReadonlyMap; } /** * A table without a state column its journal says it has (`internal`). `migration` is the * journaled `@` that added the column — what a scope failing closed on a wake, * where no migration ran, records as its failure. */ export declare class StateColumnLost extends SubstratError { readonly migration: string; constructor(migration: string, message: string); } /** * After runtime DDL, inside the operation's transaction: the same repair a migration pass makes. * A lost state column throws `internal`, and the operation and its DDL roll back. */ export declare function afterRuntimeDdl(sql: ScopedSql, run: (ddl: string) => void, plans: DerivedPlans): void; /** Whether a scope has anything derived for `afterRuntimeDdl` to keep — the hook is installed only then. */ export declare const derivesAnything: (plans: DerivedPlans) => boolean; /** * A migration's half, after its own SQL and inside its own transaction: the state columns are * still where the journal put them. A migration that dropped one throws here and rolls back — * the rows keep their state, and the scope fails closed naming the migration. */ export declare function assertEntityStateColumns(sql: ScopedSql, plans: DerivedPlans, after: string): void; /** * Check the state columns, then re-create every derived set the journal says a table is owed * and the catalogue does not hold exactly as emitted. Run inside the caller's transaction, so a * failure — a lost column, or DDL that will not apply — rolls the caller back whole. * * `run` executes a multi-statement script (a Durable Object splits it first). `after` names what * left the schema this way, for the message. `absentTable: 'skip'` is a restore's: a table the * dump did not carry is left absent, and owed nothing. Everywhere else an owed table that is * gone has lost its state, and fails closed. */ export declare function repairDerivedObjects(sql: ScopedSql, run: (ddl: string) => void, plans: DerivedPlans, opts: { readonly after: string; readonly absentTable?: 'fail' | 'skip'; }): void; /** * Both adapters' migration pass, after one migration's SQL and inside its transaction: the * column check, and — after the last migration of the pass — the derived objects repaired. * `key` is `@`, which a failure names. A last entry the pass skips (journaled * by a pass that ran first) needs nothing here: that pass repaired them after it. */ export declare function afterMigration(sql: ScopedSql, run: (ddl: string) => void, plans: DerivedPlans, key: string, last: boolean): void; //# sourceMappingURL=derived-objects.d.ts.map