import type { Database, VirtualTableModule, BaseModuleConfig, TableSchema, TableIndexSchema as IndexSchema, ModuleCapabilities, VirtualTable, BestAccessPlanRequest, BestAccessPlanResult, SchemaChangeInfo, Schema, MappingAdvertisement, LensDeploymentSnapshot, VtabConcurrencyMode, VirtualTableConnection, BackingHost } from '@quereus/quereus'; import type { IsolationModuleConfig } from './isolation-types.js'; import { IsolatedTable } from './isolated-table.js'; /** * Generates a unique overlay ID for each overlay table instance. * Used to avoid name conflicts when multiple overlays exist. */ export declare function generateOverlayId(): number; /** * Returns the weaker (lower-rank) of two concurrency modes. A merged read * through `IsolationModule` touches BOTH the underlying and the overlay table, * so it is only as concurrency-safe as the weaker of the two. */ export declare function weakerMode(a: VtabConcurrencyMode, b: VtabConcurrencyMode): VtabConcurrencyMode; /** * Caps a mode at `'reentrant-reads'`. `IsolationModule`'s own write path * (`IsolatedTable.update` → `ensureOverlay`, `setHasChanges`, the multi-step * merged-conflict checks, the savepoint sets) mutates shared per-connection * state non-atomically, so the wrapper is never `'fully-reentrant'` no matter * how reentrant the underlying/overlay are. This is the single place that * invariant is enforced. */ export declare function clampToReentrantReads(mode: VtabConcurrencyMode): VtabConcurrencyMode; /** * Per-table state tracking the underlying table (shared across all connections). */ export interface UnderlyingTableState { underlyingTable: VirtualTable; } /** * Per-connection overlay state for a specific table. * Each connection gets its own overlay that persists across IsolatedTable instances. */ export interface ConnectionOverlayState { overlayTable: VirtualTable; hasChanges: boolean; /** * Set by a cross-connection ALTER that could not migrate this (foreign) * overlay to the post-alter column layout. The overlay still holds * PRE-alter rows, so it is structurally inconsistent with the now-committed * schema; any data op that would merge or flush it must throw this message. * Undefined = healthy. Cleared only by discarding the overlay (rollback / * commit-failure → rollback). */ poison?: { message: string; }; } /** * A module wrapper that adds transaction isolation to any underlying module. * * The isolation layer intercepts reads and writes: * - Writes go to an overlay table (uncommitted changes, per-connection) * - Reads merge overlay with underlying data * - Commit flushes overlay to underlying * - Rollback discards overlay * * Architecture: * - Underlying tables are shared across all connections (one per table) * - Overlay tables are per-connection per-table (created lazily on first write) * - Each IsolatedTable instance looks up its overlay from connection-scoped storage * * This provides ACID semantics including: * - Read-your-own-writes within a transaction * - Snapshot isolation (reads see consistent state) * - Savepoint support via overlay module's transaction support */ export declare class IsolationModule implements VirtualTableModule { readonly underlying: VirtualTableModule; readonly overlayModule: VirtualTableModule; readonly tombstoneColumn: string; /** Underlying table state per table, keyed by "schemaName.tableName" */ private readonly underlyingTables; /** * Per-connection overlay states, keyed by "connectionId:schemaName.tableName". * The connectionId is derived from the database's transaction context. */ private readonly connectionOverlays; /** * Tracks savepoint depths that were created before the overlay existed, per * connection+table. Keyed identically to connectionOverlays. * When the overlay is created lazily after some savepoints already exist, * its MemoryVirtualTableConnection stack needs to be padded so that * rollbackToSavepoint(depth) looks up the correct stack index. */ private readonly preOverlaySavepoints; /** * In-flight covering-connection builds, keyed identically to * {@link connectionOverlays} (`:.` via * {@link makeConnectionOverlayKey}). Connection registration is a * per-connection (per-db+table) invariant, not a per-wrapper one, so the memo * lives here — at the layer that spans every `IsolatedTable` wrapper for one * (db, table) — rather than on the wrapper instance. * * `IsolatedTable.ensureConnection()` `await`s the overlay `createConnection()` * / the database `registerConnection()` between its covering-reuse lookup and * the `registeredConnection` set. This module forwards `'reentrant-reads'` (see * {@link concurrencyMode}), so the runtime may drive two concurrent * merged-overlay scans of one table — and it connects a FRESH `IsolatedTable` * per scan (see {@link connect}), so the two scans land on DISTINCT wrapper * instances. A per-wrapper memo cannot coalesce them: both see * `registeredConnection === null`, both miss the existing-covering lookup, both * `registerConnection` — double-registering, which makes * `DeferredConstraintQueue.findConnection()` throw on multiple covering * candidates. Keying the memo per (db, table) coalesces across wrappers: the * first scan to enter creates the build promise; concurrent peers `await` it * and resolve to the SAME covering connection. Typed in * `VirtualTableConnection` terms (not `IsolatedConnection`) to keep this module * free of an `isolated-connection` import; the resolved value is an * `IsolatedConnection`. Mirrors `LaminaTable.connectionInFlight`. */ private readonly connectionInFlight; /** * Backing-host capability forward (engine `vtab/backing-host.ts`) — assigned in * the constructor ONLY when the underlying module implements it, so method * PRESENCE mirrors the underlying (presence IS the capability; a wrapper around * a capability-less module must not advertise it). A straight delegate is * correct: every backing write is privileged (`applyMaintenance` / * `replaceContents` bypass user DML entirely), so the per-connection overlay * never holds backing rows and the underlying host's pending state is the only * state there is. Mid-transaction `select`s of the MV reach that pending state * through the merged read (empty overlay → underlying reads-own-writes), and at * commit/rollback the backing's IsolatedConnection flushes a no-op empty overlay * while the host's own connection commits/rolls back the underlying pending — * disjoint state, so ordering between the two is immaterial. */ getBackingHost?: (db: Database, schemaName: string, tableName: string) => BackingHost | undefined; /** * Materialized-view backing-create capability forward * (`SchemaManager.createBackingTable` prefers `createBacking?() ?? create()`) * — assigned in the constructor ONLY when the underlying module implements it, * so method PRESENCE mirrors the underlying, exactly like {@link getBackingHost}. * The two MUST be forwarded together: this forward routes the MV backing into * the underlying's durable store via its `createBacking`, so the subsequent * (forwarded) {@link getBackingHost} resolves a real host. Without it, the * wrapper would have no `createBacking`, `createBackingTable` would fall back to * the wrapper's generic {@link create} (an ordinary underlying table), and the * forwarded `getBackingHost` would find no durable host for it. The body mirrors * {@link create} — wrap the underlying table in an `IsolatedTable` and record * underlying state — but builds the underlying via `createBacking`. Backing * writes are privileged and bypass the per-connection overlay (see * {@link getBackingHost}), so the empty-overlay wrapper is correct here too. */ createBacking?: (db: Database, tableSchema: TableSchema) => Promise; /** Attach-lifecycle seam forwards — assigned only when the underlying implements them, * mirroring presence so the wrapper advertises each capability iff the underlying does. * Backing writes bypass the per-connection overlay (see {@link getBackingHost}), so * these are straight delegates with no overlay bookkeeping. */ ensureBackingForAttach?: (db: Database, schemaName: string, tableName: string, backingSchema: TableSchema) => Promise; retireBackingForAttach?: (db: Database, schemaName: string, tableName: string, plainSchema: TableSchema) => Promise; discardBackingForAttach?: (db: Database, schemaName: string, tableName: string) => Promise; constructor(config: IsolationModuleConfig); /** * Forwards a concurrency-mode hint so a host that wraps a reentrant module * in `IsolationModule` keeps the plan-level `concurrencySafe` it would get * registering the underlying directly (read by * `TableReferenceNode.computePhysical` via `getModuleConcurrencyMode`). * * Merged reads touch BOTH the underlying table and the overlay table (a * `MemoryTable` by default, or a host-injected `config.overlay`), so the * forwarded mode is the {@link weakerMode weaker} of the two — a serial * underlying OR a serial custom overlay degrades the whole wrapper to * `'serial'`. The result is then {@link clampToReentrantReads capped} at * `'reentrant-reads'`: `IsolationModule`'s write path is never reentrant. * * A live getter (not a construction-time snapshot): the underlying's mode is * a static module property today, but mirroring `expectedLatencyMs` — whose * value is learned lazily at connect time — keeps both forwards reading live * each plan. Always returns a concrete value (never `undefined`), satisfying * the optional `concurrencyMode?` under `exactOptionalPropertyTypes`. */ get concurrencyMode(): VtabConcurrencyMode; /** * Forwards the underlying module's first-row-latency planner hint so a cold * `NodeFsProvider` / OPFS install's scan node carries the latency estimate * through the wrapper (read by `TableReferenceNode.computePhysical`, which * only lifts the value when `> 0`). The overlay is an in-memory staging table * with no meaningful latency, so only the underlying contributes. * * Returns `0` (never `undefined`) when the underlying declares none — `0` is * observably identical to omitting the hint, and a concrete value satisfies * the optional `expectedLatencyMs?` under `exactOptionalPropertyTypes`. A * getter, not a stored field: `LaminaModule.expectedLatencyMs` is itself a * getter whose value is learned lazily at connect time, so a construction-time * snapshot would capture a stale `0`. */ get expectedLatencyMs(): number; /** * Gets the underlying table state for a table. */ getUnderlyingState(schemaName: string, tableName: string): UnderlyingTableState | undefined; /** * Sets underlying table state. */ private setUnderlyingState; /** * Removes underlying table state. */ private removeUnderlyingState; /** * Gets the overlay state for a specific connection and table. */ getConnectionOverlay(db: Database, schemaName: string, tableName: string): ConnectionOverlayState | undefined; /** * Sets the overlay state for a specific connection and table. */ setConnectionOverlay(db: Database, schemaName: string, tableName: string, state: ConnectionOverlayState): void; /** * Removes the overlay state for a specific connection and table. * Called after commit/rollback to clean up. */ clearConnectionOverlay(db: Database, schemaName: string, tableName: string): void; /** * Returns (creating if absent) the set of savepoint depths that pre-date the overlay * for this connection+table. Shared across all IsolatedTable instances in the * same connection so that ensureOverlay() on any instance sees the correct set. */ getPreOverlaySavepoints(db: Database, schemaName: string, tableName: string): Set; /** Removes the pre-overlay savepoint set for a connection+table. */ clearPreOverlaySavepoints(db: Database, schemaName: string, tableName: string): void; /** * Coalesces concurrent covering-connection builds for one (db, table) onto a * single in-flight promise, keyed identically to {@link connectionOverlays} * (see {@link connectionInFlight}). * * On a cache hit, returns the existing in-flight build so a concurrent peer * resolves to the SAME covering connection. On a miss, calls `build()` and * stores the returned promise with **no `await` between the `get` and the * `set`** — `build()` runs its synchronous prefix (including the * covering-reuse lookup) and returns at its first `await`, so a second caller * cannot interleave into the synchronous get→set region and always observes * the populated memo. This holds regardless of where the build's internal * `await`s fall or how microtasks order. * * The memo is cleared on settle (fulfil AND reject), identity-guarded so a * later rebuild's promise is never clobbered by an earlier build's clear — a * failed build must let the next read retry. */ coalesceConnectionBuild(db: Database, schemaName: string, tableName: string, build: () => Promise): Promise; /** * Creates a unique key for connection-scoped overlay storage. * Uses the database instance's identity as the connection identifier. */ private makeConnectionOverlayKey; /** WeakMap to assign stable IDs to database instances */ private static dbIdMap; private static nextDbId; private getDbId; /** * Returns capabilities combining underlying module with isolation guarantees. */ getCapabilities(): ModuleCapabilities; /** * Forwards mapping-advertisement discovery to the underlying module. * * The lens compiler's advertisement resolver reaches a basis table's * `vtabModule` — which is this wrapper when a memory/store basis is isolated — * and calls the optional `getMappingAdvertisements` hook. A decomposition's * storage/access shape is a property of the underlying basis relations and is * isolation-transparent (the overlay does not change the decomposition shape), * so a straight delegate is correct. Without this forward, `quereus.lens.decomp.*` * tags on isolation-wrapped basis tables are silently dropped and a logical * table over the decomposition fails body compilation with "no basis backing". */ getMappingAdvertisements(db: Database, basisSchema: Schema): readonly MappingAdvertisement[]; /** * Forwards APPLY SCHEMA's batch-begin signal to the underlying module. * * APPLY SCHEMA's migration loop fires `beginSchemaBatch`/`endSchemaBatch` * on the *registered* module that owns each table — which is this wrapper * when a basis is isolated. A batching-capable underlying module folds the * whole APPLY SCHEMA into a single substrate commit by opening a batch here * that its subsequent create/destroy/alter callbacks (which IsolationModule * forwards to the underlying) join. Without this forward the underlying is * never reached and silently falls back to per-DDL commits. * * This is a straight delegate to the underlying: APPLY SCHEMA migrations are * DDL against the underlying substrate, not staged data writes, so the * per-connection overlays do not participate. Overlays hold uncommitted * *data* writes inside a user transaction; schema DDL does not route through * them, so there is nothing for the overlay/commit lifecycle to flush as * part of the batch. */ beginSchemaBatch(db: Database, schemaName: string): Promise; /** * Forwards APPLY SCHEMA's batch-end signal to the underlying module. * See `beginSchemaBatch` for why a straight delegate is correct. */ endSchemaBatch(db: Database, schemaName: string, error?: unknown): Promise; /** * Forwards APPLY SCHEMA's lens deployment notification to the underlying module. * * A logical `apply schema X` fires `notifyLensDeployment` on the *registered* * module (this wrapper when a basis is isolated), handing it the freshly * deployed `LensDeploymentSnapshot` so a basis-backing module can reconcile its * storage against the new lens. The deployed lens shape is a property of the * declared logical/basis schemas and is isolation-transparent (the overlay does * not change it), so a straight delegate is correct — mirroring the * `getMappingAdvertisements` forward. Without this forward an isolation-wrapped * basis module would silently never hear the deployment. */ notifyLensDeployment(db: Database, logicalSchemaName: string, snapshot: LensDeploymentSnapshot): Promise; /** * Delegates access plan selection to the underlying module. * This ensures the query planner knows about indexes and can generate * appropriate FilterInfo for index scans. */ getBestAccessPlan(db: Database, tableInfo: TableSchema, request: BestAccessPlanRequest): BestAccessPlanResult; /** * Creates a new isolated table wrapping an underlying table. * * The overlay is NOT created here - it's created lazily on first write * by each IsolatedTable instance, and stored in connection-scoped storage. */ create(db: Database, tableSchema: TableSchema): Promise; /** * Connects to an existing isolated table. * * Each connect() call returns a fresh IsolatedTable that shares: * - The underlying table (with all connections) * - The overlay table (with the same connection/transaction context) * * The overlay is created lazily on first write. */ connect(db: Database, pAux: unknown, moduleName: string, schemaName: string, tableName: string, options: BaseModuleConfig, tableSchema?: TableSchema): Promise; /** * Destroys the underlying table. */ destroy(db: Database, pAux: unknown, moduleName: string, schemaName: string, tableName: string): Promise; /** * Closes all resources held by the underlying module (if it supports closeAll). * Also clears connection overlay state. */ closeAll(): Promise; /** * Creates an index on the underlying table. * * Note: Indexes on per-connection overlays are created lazily when the * overlay is created, by copying from the underlying table's schema. * * We use the stored table instance's createIndex() rather than the module-level * method so that the MemoryTable's local tableSchema property stays in sync. * That property is what ensureOverlay() reads when building the overlay schema. */ createIndex(db: Database, schemaName: string, tableName: string, indexSchema: IndexSchema): Promise; /** * Drops an index on the underlying table. * * Mirrors createIndex: when the underlying VirtualTable exposes an * instance-level dropIndex (e.g. MemoryTable, which forwards to its manager * so MemoryTable.tableSchema stays fresh), prefer that. Otherwise fall back * to the module-level dropIndex (e.g. StoreModule, which refreshes the * StoreTable's cached tableSchema and tears down the index store). * * Any per-connection overlay that already exists for this table is * rebuilt under the post-drop schema, preserving staged rows. A bare * forward to `overlay.dropIndex` is insufficient: when the overlay's * MemoryTable has an active write `TransactionLayer`, its * `tableSchemaAtCreation` is frozen at layer-creation time, so the * synthesized UNIQUE constraint keeps firing inside the overlay's * own UC check on the next write even after the manager's schema is * refreshed. Rebuilding gives the new MemoryTable a fresh * transaction layer that captures the post-drop schema. Overlays * created AFTER this point inherit the post-drop schema from the * underlying at ensureOverlay time. */ dropIndex(db: Database, schemaName: string, tableName: string, indexName: string): Promise; /** * Rebuilds an overlay table under the post-drop-index schema, preserving * staged rows (including tombstones). Column layout is unchanged by * DROP INDEX, so rows can be copied verbatim. */ private migrateOverlayForDropIndex; /** * Delegates ALTER TABLE to the underlying module and migrates any per-connection * overlays to the post-alter schema without discarding staged rows. * * ADD COLUMN — appends the new column's value to each overlay row's data columns, * backfilled per row exactly as the committed path does (literal default, * per-row `new.` evaluator, or NULL); tombstone rows get NULL. * DROP COLUMN — removes the dropped column from each overlay row. * RENAME / ALTER COLUMN — data column indices are unchanged; only schema metadata rotates. * * **Atomicity guarantee.** DDL through Quereus is not transaction-scoped and the * underlying (shared, committed) base auto-commits its mutation immediately — * there is no frame to unwind, and `dropColumn` / type-converting `alterColumn` * are lossy and not invertible, so "revert the underlying on overlay-migration * failure" is not viable. Instead this method **pre-validates** every affected * overlay's backfill (the per-row NOT NULL check and the tombstone-present guard) * BEFORE calling `underlying.alterTable`. A rejection therefore fires while the * underlying, the schema catalog, and every overlay are still untouched, so the * ALTER either fails clean or fully applies — base/catalog can no longer diverge. * This mirrors the engine's pre-mutation `validateNotNullBackfill` in * `runtime/emit/alter-table.ts`. */ alterTable(db: Database, schemaName: string, tableName: string, change: SchemaChangeInfo): Promise; /** * Builds the poison message stamped onto a foreign overlay whose backfill could not * satisfy a cross-connection ALTER (see {@link alterTable} tier 3). Names the * schema.table and the offending column so the owning connection's eventual * read/write/commit error is self-explanatory. Poison only arises on the addColumn * NOT NULL path, so the column name is taken from the addColumn change; other change * types never reach here but are handled defensively. */ private buildAlterPoisonMessage; /** * Renames a table through the isolation layer. * * Forwards to the underlying module so it can re-key its handles and move * any physical storage, then re-keys our own tracking maps so subsequent * connect() calls under the new name find the existing underlying state * and any in-flight per-connection overlays. * * Done in this order so a failure in the underlying rename leaves our * internal maps untouched (the engine will not update the schema catalog * if this method throws). */ renameTable(db: Database, schemaName: string, oldName: string, newName: string): Promise; /** * Re-keys all entries of a connection-scoped map (`:.
`) * from oldName to newName, leaving entries for other tables untouched. */ private rekeyConnectionScopedMap; /** * Rebuilds an overlay table under the post-alter schema, translating each * staged row to the new column layout. */ private migrateOverlayForAlter; /** * Precomputes the per-ALTER constants an `addColumn` overlay backfill needs: * the folded literal DEFAULT (the `tryFoldLiteral` of the DEFAULT expr, or `null` * when there is no DEFAULT or it folds to NULL), the engine-supplied per-row * evaluator (present only for a non-foldable `new.` default), and whether * the new column is NOT NULL. Returns undefined for every non-`addColumn` change * so the row loop appends nothing. * * The new column's nullability is resolved exactly as both underlyings resolve it * (`columnDefToSchema(columnDef, default_column_nullability === 'not_null')`) so the * pre-validation cannot drift from what the underlying will enforce. Because it is * derived purely from `change` + the session option — not the post-alter schema, * which does not exist until `underlying.alterTable` runs — this can be built * BEFORE the irreversible underlying mutation and reused by the migration after. */ private deriveAddColumnBackfill; /** * Dry-runs an overlay's ALTER migration-fallible work without mutating anything, * so the caller can run it for every affected overlay BEFORE the irreversible * `underlying.alterTable` (see {@link alterTable}). It exercises the EXACT code * paths the real migration uses — the tombstone-present guard and, for addColumn, * `computeAddColumnValue` per staged row — so a dry-run pass and the subsequent * migrate pass cannot diverge: * * - A clean overlay (`!hasChanges`) stages no rows, so there is nothing to validate. * - A missing tombstone column throws INTERNAL here, before the underlying is touched. * - For addColumn, each staged row runs through `computeAddColumnValue`: tombstone * rows short-circuit to `null` (the evaluator never runs), and a NOT-NULL-violating * evaluated row throws CONSTRAINT here, atomically. Computed values are discarded. * * Non-addColumn changes (`addColumnCtx === undefined`) only run the tombstone guard; * their row translation appends/removes nothing fallible on data grounds. */ private validateOverlayMigration; /** * Computes one staged row's value for a freshly added column, mirroring the * committed-row backfill (see `base.ts` `recreatePrimaryTreeWithNewColumn` and * `store-module.ts` `migrateRows`): * * - Tombstone rows carry NULL placeholders and their appended value is never read, * so append `null` and never run the evaluator against them (it could reference * NULL siblings or spuriously trip the NOT NULL check). * - With a per-row evaluator, derive the value from the existing-columns slice and * enforce NOT NULL on that path only (a literal/NULL default's nullability is gated * up-front by the engine, exactly as `base.ts` does). * - Otherwise use the folded literal default. */ private computeAddColumnValue; /** * Translates a single overlay row from the pre-alter to the post-alter column layout. * The tombstone value is preserved in the last position. * * `addColumnValue` is the per-row value the caller computed for an `addColumn` * (via {@link computeAddColumnValue}); it is `undefined` for every other change * type. Keeping the (async) backfill in the caller's loop lets this stay synchronous. */ private translateOverlayRow; /** Creates a FilterInfo for a full table scan (no constraints). */ private makeFullScanFilterInfo; /** * Creates overlay schema from underlying schema. * Adds tombstone column and uses unique name to avoid conflicts. * * Called by IsolatedTable when lazily creating its overlay. */ createOverlaySchema(baseSchema: TableSchema): TableSchema; } //# sourceMappingURL=isolation-module.d.ts.map