/** * The apply frame's write API (T12344; journal spec §3.2 "The API", §3.3). * * The ONLY calls that write synced rows during apply. Each performs the SQL * and, in the same transaction, records what it wrote in `_sync_apply_intent` * through {@link recordApplyIntents}, from the STORED value: every write uses * `RETURNING` with the same SQL `enc()` the capture triggers use, so an intent * is byte-equal to what the trigger captures after column affinity, defaults, * CHECK coercions and trigger rewrites. The sealer then subtracts each apply * frame's intents from its captures, so an applied remote change is never * echoed back with fresh HLCs (§3.3). * * Rules, all enforced here: * - INSERT, UPDATE and DELETE only. Never `REPLACE` / `INSERT OR REPLACE`: it * deletes the old row first, so its capture is a D + I with no `*D` intent * and the D would seal (gate 28 bans it too). Nothing in the apply module * writes raw SQL outside this file (K11, test-enforced). * - Values are LOCAL values: references are already translated to local keys * (the applier resolves uids before it calls this API). * - An insert records `*I` plus an intent for every non-NULL stored captured * column, read back with `RETURNING`; captures omit NULLs. * - A secret column records ``, which {@link recordApplyIntents} * binds to the capture the write produced. Intents are recorded right * after each write, before the frame writes that column again. * - Without a capture frame (capture off) the writes run and no intent is * recorded: there are no captures to subtract. * * @module store/sync/apply/write-api * @task T12344 */ import type { DatabaseSync, SQLInputValue } from 'node:sqlite'; import type { TableScope } from '@cleocode/contracts'; import type { LedgerWireValue } from '@cleocode/contracts/ledger'; import { type RowMetaRow } from '../row-meta.js'; import { type ChildKey } from './fk.js'; /** A write the API refuses (unknown table or column, a missing row). */ export declare class ApplyWriteError extends Error { readonly code = "E_SYNC_APPLY_WRITE"; constructor(message: string); } /** * A typed wire value as a node:sqlite parameter: `$i` as a BigInt, `$r` as a * number (`Inf`, `+Inf`, `-Inf` accepted), `$b` as a Buffer. * * @param v - The wire value. * @returns The parameter. */ export declare function wireToSql(v: LedgerWireValue): SQLInputValue; /** What one write stored, per column, as `enc()` text. */ export type StoredEncs = Readonly>; /** * What a DELETE of a row would remove outside the sync set through foreign * key actions: the rows a `CASCADE` deletes (parents before children) and the * columns a `SET NULL` / `SET DEFAULT` clears. Capture never records these, * so a rebase rewind snapshots them before deleting a row it will replay * (§3.5 R7-2, T13267). */ export interface LocalDescendants { /** Rows a cascade deletes, each with every column as stored. */ readonly rows: ReadonlyArray<{ readonly table: string; readonly values: Readonly>; }>; /** Child columns an action clears: the child row's key, the column, its value. */ readonly cleared: ReadonlyArray<{ readonly table: string; readonly where: Readonly>; readonly column: string; readonly value: SQLInputValue; }>; } /** The write API bound to one apply frame. */ export interface ApplyWriteApi { /** * UPDATE columns of an existing row; records one intent per column. * * @returns The stored `enc()` per written column. * @throws {ApplyWriteError} When the row does not exist. */ writeFields(table: string, uid: string, values: Readonly>): StoredEncs; /** * INSERT a row with `uid`; records `*I` and one intent per non-NULL stored * captured column. Every identity column but `uid` must be given. * * @returns The stored `enc()` per captured column (NULLs included). * @throws {ApplyWriteError} When an identity column is missing. */ insertRow(table: string, uid: string, values: Readonly>): StoredEncs; /** * DELETE the row; records `*D`, plus an intent for every sync-set child * column the delete's `ON DELETE SET NULL` action clears, so the FK * action is never sealed as a local write (§3.2 FK actions). * * @returns Whether a row was deleted. */ deleteRow(table: string, uid: string): boolean; /** * Give a natural-key row whose uid was never filled its uid (T13273). A * natural row's uid is a function of its key, so the sealer journals it * before the open-time identity fill writes it to the row; until then a * lookup by uid misses the row. Finds the row with no uid whose local key * equals `localKey` and writes `uid` into it, uncaptured (a derived value, * as the fill writes it). * * @returns Whether a row was given the uid. */ adoptNaturalRow(table: string, uid: string, localKey: Readonly>): boolean; /** * Re-key a row (a K op): its uid, and its birth fingerprint when `newBfp` * is given. Records `*K` (enc = the new uid) and moves the row's meta and * typed-rule state to the new uid. * * @returns Whether a row was re-keyed. */ rekeyRow(table: string, uid: string, newUid: string, newBfp: string | null): boolean; /** * The row's local-only columns (every column capture never records: claims, * leases, local keys of other devices), or null when the row is absent. A * rebase snapshots them before it rewinds an insert (§3.5 R7-2). */ readLocalOnly(table: string, uid: string): Record | null; /** * Write back local-only columns a rebase snapshotted (never captured, never * intents). A restored local key moves the sync-set references that point * at the interim key with it. */ writeLocalOnly(table: string, uid: string, values: Readonly>): void; /** * What deleting the row would remove outside the sync set through FK * actions (recursively through cascades), and the uncaptured sync-set * children with no uid it would cascade away (T12914), for a rebase rewind * to put back after the replay (§3.5 R7-2). Empty when the row is absent. */ readLocalDescendants(table: string, uid: string): LocalDescendants; /** Put back what {@link ApplyWriteApi.readLocalDescendants} saw (never captured, never intents). */ writeLocalDescendants(snapshot: LocalDescendants): void; /** The live sync-set children of a row, per child key (for the parent-delete policy). */ childRows(table: string, uid: string): ReadonlyArray<{ readonly key: ChildKey; readonly uid: string; }>; /** The row's captured columns as wire values, or null when it does not exist. */ readRow(table: string, uid: string): Record | null; /** The row's stored replication meta, or undefined when it has none. */ rowMeta(table: string, uid: string): RowMetaRow | undefined; /** * Record the row's replication meta after the merge (§1.6) through the * shared writer (`upsertRowMetaFromFields`, the sealer's own). Pass the * post-merge WINNING field HLCs; the writer keeps the newer of stored and * incoming per field, so a losing field never moves back, and a write in * which no field wins changes nothing. A remote delete names the row's * fields at the delete's HLC with `deleted: true`. The first write of a * row names every non-identity column. * * @returns The row HLC stored. */ setRowMeta(table: string, uid: string, meta: { readonly fieldHlc: Readonly>; readonly origin: string; readonly actor: string | null; readonly deleted: boolean; readonly keyJson?: string | null; readonly chash?: string | null; readonly bfp?: string | null; }): string; /** * Record the row's replication meta EXACTLY as the merge engine decided * it (the applier's writer). Unlike {@link ApplyWriteApi.setRowMeta} it * does not keep the newer of stored and incoming per field: the engine * already did, and some of its decisions move a field HLC back on purpose * (an absorbing write that overrides a newer edit keeps its own HLC), or * pin the tombstone to the delete's HLC although a field was newer. * * - Live row: `fieldHlc` names the merged HLC per field; a column it omits * keeps its stored HLC, and a first write must name every field. * - Tombstone: `tombstone` is the delete's HLC; `fieldHlc` is ignored. * * @returns The row HLC stored. */ setMergedRowMeta(table: string, uid: string, meta: { readonly fieldHlc: Readonly>; readonly tombstone: string | null; readonly origin: string; readonly actor: string | null; readonly keyJson?: string | null; readonly chash?: string | null; readonly bfp?: string | null; }): string; } /** * Bind the write API to a store, scope and apply frame. * * @param db - The store, inside the frame's transaction. * @param scope - The store's scope. * @param frame - The apply frame id, or null when capture is off. * @param assertActive - Throws once the frame has ended. * @returns The API. */ export declare function createApplyWriteApi(db: DatabaseSync, scope: TableScope, frame: string | null, assertActive: () => void): ApplyWriteApi; //# sourceMappingURL=write-api.d.ts.map