@voltro/database 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ConfigError } from 'effect';
2
+ import { Context } from 'effect';
2
3
  import { Effect } from 'effect';
3
4
  import { Exit } from 'effect';
4
5
  import { Layer } from 'effect';
@@ -8,6 +9,7 @@ import { SqlClient } from '@effect/sql';
8
9
  import { SqlError } from '@effect/sql';
9
10
  import { Statement } from '@effect/sql';
10
11
  import { Stream } from 'effect';
12
+ import { TransactionConnection } from '@effect/sql/SqlClient';
11
13
  import { VoidIfEmpty } from 'effect/Types';
12
14
  import { YieldableError } from 'effect/Cause';
13
15
 
@@ -182,6 +184,7 @@ export declare const attributionFields: (explicit?: WriteAttribution | undefined
182
184
  traceId?: string;
183
185
  subjectId?: string | null;
184
186
  procedure?: string;
187
+ via?: "agent";
185
188
  };
186
189
 
187
190
  /** The write's identity, as both sides can compute it. NOT the row image: the
@@ -316,6 +319,13 @@ export declare interface BranchExecutor {
316
319
  readonly createTable: (namespace: string, table: string) => Promise<void>;
317
320
  /** Copy one table's rows from the parent namespace into the branch (`INSERT … SELECT`). */
318
321
  readonly copyTable: (fromNamespace: string, toNamespace: string, table: string) => Promise<void>;
322
+ /** Replay one of the parent's foreign keys into the branch, re-pointed at the
323
+ * branch's own copy of the target table. `LIKE … INCLUDING ALL` does not copy
324
+ * foreign keys — see {@link BranchForeignKey} for why that is not optional. */
325
+ readonly addForeignKey: (namespace: string, fk: BranchForeignKey) => Promise<void>;
326
+ /** Restore an index's NAME, which the `LIKE` copy re-derived. Idempotent + a
327
+ * no-op when the name already matches — see {@link BranchIndexName}. */
328
+ readonly restoreIndexName: (namespace: string, index: BranchIndexName) => Promise<void>;
319
329
  /** Drop the whole branch namespace (teardown). */
320
330
  readonly dropNamespace: (namespace: string) => Promise<void>;
321
331
  /** Create a Neon copy-on-write branch from `parentBranch`; RESOLVE to the new
@@ -325,6 +335,69 @@ export declare interface BranchExecutor {
325
335
  readonly neonBranchDelete: (branchId: string) => Promise<void>;
326
336
  }
327
337
 
338
+ /**
339
+ * One foreign key to REPLAY into the branch namespace, re-pointed at the
340
+ * branch's own copy of the target table.
341
+ *
342
+ * This exists because of a silent fidelity hole in the namespace mechanism, and
343
+ * the hole is a property of Postgres rather than of our emission:
344
+ * `CREATE TABLE … (LIKE parent INCLUDING ALL)` copies columns, defaults, CHECK
345
+ * constraints, identity and indexes — and does NOT copy FOREIGN KEYS. There is
346
+ * no `INCLUDING` clause that does. So a namespace branch of a schema with
347
+ * referential integrity came up WITHOUT any of it: a PR preview bound to that
348
+ * branch accepts writes production rejects, and a migration rehearsed on it is
349
+ * rehearsing a different schema from the one it claims to.
350
+ *
351
+ * The caller supplies them (introspection already carries them per column, as
352
+ * `ColumnSnapshot.references`), because the branch primitive deliberately does
353
+ * no I/O of its own.
354
+ */
355
+ export declare interface BranchForeignKey {
356
+ /** Table (in the branch) that carries the FK column. */
357
+ readonly table: string;
358
+ readonly column: string;
359
+ /** Referenced table — resolved to the BRANCH's copy, never the parent's. */
360
+ readonly targetTable: string;
361
+ readonly targetColumn: string;
362
+ readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
363
+ readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
364
+ }
365
+
366
+ /**
367
+ * One index whose NAME must be restored on the branch.
368
+ *
369
+ * The second thing `LIKE … INCLUDING ALL` does not carry, found the same way as
370
+ * the foreign keys — by branching a real converged schema and watching the
371
+ * differ propose work. Postgres copies the index and **re-derives its name from
372
+ * the table and columns**; the source name survives only when it happened to
373
+ * equal that derivation. Measured on pg 17:
374
+ *
375
+ * probeparent._voltro_api_keys byApiKeyTenant
376
+ * br_pr9_probe._voltro_api_keys _voltro_api_keys_tenantId_idx
377
+ * probeparent._voltro_idempotency _voltro_idempotency_scope_key_uq
378
+ * br_pr9_probe._voltro_idempotency _voltro_idempotency_scope_key_idx
379
+ *
380
+ * The planner compares indexes BY NAME, so every custom-named index on the
381
+ * parent reads as a different index on the branch: a rehearsal proposed
382
+ * `rename-index` plus a spurious `drop-unique-composite`/`add-unique-composite`
383
+ * pair for a schema that was already converged. Which is worse than noise — the
384
+ * rehearsal is supposed to be the thing that tells you what your migration does.
385
+ *
386
+ * PRIMARY KEYS are excluded: `<table>_pkey` is postgres's own derivation AND
387
+ * what four of the five introspectors fabricate, so the copy is already right.
388
+ * Expression indexes are excluded too — their key list carries no column to
389
+ * match on.
390
+ */
391
+ export declare interface BranchIndexName {
392
+ readonly table: string;
393
+ /** Key columns IN INDEX ORDER — how the branch's copy is identified, since its
394
+ * re-derived name is exactly what we do not know. */
395
+ readonly columns: ReadonlyArray<string>;
396
+ readonly unique: boolean;
397
+ /** The name the PARENT's index carries, and the one to restore. */
398
+ readonly name: string;
399
+ }
400
+
328
401
  export declare interface BranchLimitDecision {
329
402
  /** Namespaces to tear down now (past TTL). */
330
403
  readonly evict: ReadonlyArray<string>;
@@ -375,6 +448,14 @@ export declare interface BranchPlanInput {
375
448
  readonly seed: BranchSeed;
376
449
  /** namespace only: the source schema to snapshot from (required for `'copy'`). */
377
450
  readonly parentNamespace?: string;
451
+ /** namespace only: the parent's foreign keys, replayed into the branch —
452
+ * `LIKE … INCLUDING ALL` does not copy them. Ignored by `neon-cow`, whose
453
+ * copy-on-write branch carries the parent's catalog verbatim. */
454
+ readonly foreignKeys?: ReadonlyArray<BranchForeignKey>;
455
+ /** namespace only: composite UNIQUE constraints whose NAME the `LIKE` copy
456
+ * re-derived by the `LIKE` copy. Same reason as `foreignKeys` — a branch that
457
+ * differs from the parent is not a copy of it. See {@link BranchIndexName}. */
458
+ readonly indexNames?: ReadonlyArray<BranchIndexName>;
378
459
  /** neon-cow only: the base Neon branch to copy-on-write from (default `'main'`). */
379
460
  readonly neonParentBranch?: string;
380
461
  }
@@ -393,6 +474,14 @@ export declare interface BranchProvisionRequest {
393
474
  readonly tableNames: ReadonlyArray<string>;
394
475
  /** namespace mechanism, `seed: 'copy'`: the source schema to snapshot from. */
395
476
  readonly parentNamespace?: string;
477
+ /** namespace mechanism: the parent's foreign keys, replayed into the branch
478
+ * after the tables exist (and after the rows, for a `copy` seed). Omitting
479
+ * them yields a branch with NO referential integrity — see
480
+ * {@link BranchForeignKey}. */
481
+ readonly foreignKeys?: ReadonlyArray<BranchForeignKey>;
482
+ /** namespace mechanism: the parent's index names, restored on the branch after
483
+ * the `LIKE` copy re-derived them — see {@link BranchIndexName}. */
484
+ readonly indexNames?: ReadonlyArray<BranchIndexName>;
396
485
  /** The owned-DB connection string (Neon detection drives the fast-path). */
397
486
  readonly dbUrl?: string;
398
487
  /** neon mechanism: the base branch to copy-on-write from (default `'main'`). */
@@ -431,7 +520,7 @@ export declare interface BranchSqlRunner {
431
520
  export declare type BranchState = 'requested' | 'provisioning' | 'ready' | 'destroying' | 'destroyed' | 'failed';
432
521
 
433
522
  export declare interface BranchStep {
434
- readonly kind: 'create-namespace' | 'create-table' | 'copy-table' | 'seed' | 'drop-namespace' | 'neon-branch-create' | 'neon-branch-delete';
523
+ readonly kind: 'create-namespace' | 'create-table' | 'copy-table' | 'copy-foreign-key' | 'restore-index-name' | 'seed' | 'drop-namespace' | 'neon-branch-create' | 'neon-branch-delete';
435
524
  readonly detail: string;
436
525
  readonly table?: string;
437
526
  }
@@ -443,6 +532,37 @@ export declare class BranchTransitionInvalid extends Error {
443
532
  constructor(from: BranchState, event: BranchEvent);
444
533
  }
445
534
 
535
+ /**
536
+ * Per-dialect ceilings, keyed by the same ids `DialectId` uses.
537
+ *
538
+ * Deliberately NOT a `Record<DialectId, …>` typed against the dialect union:
539
+ * `@voltro/database` owns both, so the union would compile-check the map — but
540
+ * the stores look their entry up by a string they hold anyway (mysql's store
541
+ * carries `variant: 'mysql' | 'mariadb'`), and an unknown key must degrade to
542
+ * "no chunking" rather than to `undefined.maxBindParameters` at runtime. The
543
+ * parity test asserts every shipped dialect has an entry, which is the check
544
+ * that actually matters.
545
+ */
546
+ export declare const BULK_INSERT_LIMITS: Readonly<Record<string, BulkInsertLimits>>;
547
+
548
+ /**
549
+ * What one `INSERT … VALUES` statement may carry on a dialect.
550
+ *
551
+ * Both bounds are real and independent; see the header for the mssql case where
552
+ * the row bound binds first.
553
+ */
554
+ export declare interface BulkInsertLimits {
555
+ /** Bind parameters (`?` / `$n`) the engine accepts in ONE statement. */
556
+ readonly maxBindParameters: number;
557
+ /** Row constructors the engine accepts in one `VALUES` clause, when the
558
+ * engine caps that separately. */
559
+ readonly maxRowsPerStatement?: number;
560
+ }
561
+
562
+ /** The ceilings for `dialectId`, or `undefined` when we do not know them —
563
+ * which must read as "emit one statement, exactly as before". */
564
+ export declare const bulkInsertLimitsFor: (dialectId: string) => BulkInsertLimits | undefined;
565
+
446
566
  /** Raw binary column — `BYTEA` (postgres) / `BLOB` (sqlite) / `LONGBLOB`
447
567
  * (mysql/mariadb) / `VARBINARY(MAX)` (mssql). Values round-trip as
448
568
  * `Uint8Array`. Use for storing bytes IN the database (e.g. the
@@ -500,6 +620,33 @@ export declare type ChangeEvent = {
500
620
  readonly op: 'insert' | 'update' | 'delete';
501
621
  readonly old: Row | null;
502
622
  readonly new: Row | null;
623
+ /**
624
+ * Set when the TRANSPORT could not carry this change's row images and they
625
+ * had to be RECONSTRUCTED. Absent on every ordinary event, which is the only
626
+ * case where `old`/`new` are the images as they were at commit.
627
+ *
628
+ * Postgres caps a `pg_notify` payload at 8000 bytes, and under
629
+ * `changeStrategy: 'cdc'` the NOTIFY trigger is the SOLE emitter — so a row
630
+ * that does not fit used to arrive with both images null and every tap
631
+ * (search index, analytics mirror, cdc-out, row history) dropped it as a
632
+ * non-event, permanently, with nothing logged. The CDC consumer re-reads the
633
+ * row by primary key before the event reaches anyone; this field says what
634
+ * that recovered:
635
+ *
636
+ * - `'rehydrated'` — insert/update. `new` is the row RE-READ from the
637
+ * database, so it is the row as it is NOW, not necessarily the image the
638
+ * write that fired this event produced (a later write to the same row can
639
+ * already have landed). `old` is null — postgres keeps no pre-image once
640
+ * the transaction is gone.
641
+ * - `'tombstone'` — delete. `old` carries the PRIMARY KEY AND NOTHING ELSE:
642
+ * the row is gone, so the pre-image is unrecoverable and the key is the
643
+ * whole truth. Enough to REMOVE the row downstream; not a snapshot of what
644
+ * was deleted, and anything recording history must not treat it as one.
645
+ * - `'unrecovered'` — both images are null. The change is known to have
646
+ * happened and its content is not available (no key in the payload, the
647
+ * re-read failed, or the row was already gone). Re-read or resync.
648
+ */
649
+ readonly oversized?: 'rehydrated' | 'tombstone' | 'unrecovered';
503
650
  /**
504
651
  * How the event reached this process. Absent/'inline' = emitted by this
505
652
  * process's own write path; 'injected' = delivered over a cross-instance
@@ -549,10 +696,16 @@ export declare type ChangeEvent = {
549
696
  * an absent `traceId`.
550
697
  */
551
698
  readonly procedure?: string;
699
+ /**
700
+ * The write was made BY AN AGENT acting as `subjectId`, not by that subject
701
+ * directly. Mirrors `WriteAttribution.via` — see it for why the identity is
702
+ * NOT changed instead. Absent = a direct call, which is a fact, not a gap.
703
+ */
704
+ readonly via?: 'agent';
552
705
  };
553
706
 
554
707
  /** Attribution and origin, identical on every constructor. */
555
- export declare type ChangeEventMeta = Pick<ChangeEvent, 'origin' | 'traceId' | 'subjectId' | 'procedure'>;
708
+ export declare type ChangeEventMeta = Pick<ChangeEvent, 'origin' | 'traceId' | 'subjectId' | 'procedure' | 'via'>;
556
709
 
557
710
  /** A row was created. `old` is null, always. */
558
711
  export declare const changeInsert: (table: string, row: Row, meta?: ChangeEventMeta) => ChangeEvent;
@@ -589,6 +742,19 @@ export declare type ChangeStrategy = 'inline' | 'cdc';
589
742
  /** A row changed. Both sides are present, because an update has both. */
590
743
  export declare const changeUpdate: (table: string, before: Row, after: Row, meta?: ChangeEventMeta) => ChangeEvent;
591
744
 
745
+ /**
746
+ * Split `rows` into groups that each fit in one statement on this dialect.
747
+ *
748
+ * Returns a single group whenever the whole array already fits — which is the
749
+ * overwhelmingly common case, and it must stay allocation-cheap and, more
750
+ * importantly, must keep emitting the SAME single statement it did before. A
751
+ * chunker that split a 3-row insert into 3 would be a correct implementation of
752
+ * the wrong thing.
753
+ *
754
+ * `undefined` limits (an unknown dialect) → one group, unchanged behaviour.
755
+ */
756
+ export declare const chunkRowsForInsert: <T extends Record<string, unknown>>(rows: ReadonlyArray<T>, limits: BulkInsertLimits | undefined) => ReadonlyArray<ReadonlyArray<T>>;
757
+
592
758
  /**
593
759
  * Claim the attribution for an echo, or `undefined` if this write did not
594
760
  * originate here.
@@ -1468,6 +1634,18 @@ declare type ColumnOptionalForInsert<K extends PropertyKey, D> = K extends AutoF
1468
1634
  */
1469
1635
  export declare const columnSchema: <TsType, C extends ColumnType>(def: ColumnDefinition<TsType, C>) => Schema.Schema.Any;
1470
1636
 
1637
+ /**
1638
+ * How many bind parameters one row costs.
1639
+ *
1640
+ * The MAX across rows, not the first row's count: `sql.insert` builds its
1641
+ * column list from the union of the rows' keys and binds a value (a NULL where
1642
+ * a row omits the column) in every position, so a set whose first row is narrow
1643
+ * and whose tenth is wide costs the WIDE row's count for every row. Counting
1644
+ * the first row would under-count exactly the shape most likely to be near the
1645
+ * ceiling.
1646
+ */
1647
+ export declare const columnsPerRow: (rows: ReadonlyArray<Record<string, unknown>>) => number;
1648
+
1471
1649
  export declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
1472
1650
 
1473
1651
  /**
@@ -1564,6 +1742,75 @@ export declare interface ConnectionConfig {
1564
1742
  */
1565
1743
  readonly ssl?: boolean;
1566
1744
  readonly maxConnections?: number;
1745
+ /**
1746
+ * How long a caller waits for a free pooled connection before the acquire
1747
+ * FAILS (ms). Default {@link DEFAULT_ACQUIRE_TIMEOUT_MS}; `0` restores the
1748
+ * driver's own (unbounded) behaviour. Env `DB_ACQUIRE_TIMEOUT_MS`.
1749
+ *
1750
+ * This is the OTHER half of `statementTimeoutMs`. That one bounds a query the
1751
+ * SERVER is running; nothing bounded a query the client had not sent yet — a
1752
+ * request arriving when every pooled connection is busy did not fail, did not
1753
+ * retry and logged nothing, it simply sat in the driver's queue. A bounded
1754
+ * failure names the pool as the cause at the moment it IS the cause, instead
1755
+ * of surfacing as an unexplained latency spike somewhere with no connection
1756
+ * information in it.
1757
+ *
1758
+ * **What each dialect can actually enforce** — the drivers differ, and the
1759
+ * difference is not papered over:
1760
+ * - **postgres** — exact. Maps to node-postgres' `connectionTimeoutMillis`,
1761
+ * which bounds BOTH establishing a new connection and waiting in the
1762
+ * pool's pending queue for a busy one (`pg-pool`'s `_pendingQueue`
1763
+ * timer). This is the full guarantee.
1764
+ * - **mysql/mariadb** — establishment only, and that is the whole of it.
1765
+ * mysql2's pool has NO time-based acquire bound at all
1766
+ * (`lib/base/pool.js` pushes the callback onto `_connectionQueue` with no
1767
+ * timer). The waiting half is UNBOUNDED by default and deliberately so —
1768
+ * see {@link ConnectionConfig.acquireQueueLimit} for the length bound
1769
+ * that exists, why it is opt-in, and what it costs.
1770
+ * - **mssql** — establishment only. Maps to `@effect/sql-mssql`'s
1771
+ * `connectTimeout` (tedious connect + the boot `SELECT 1` probe); its
1772
+ * pool is an Effect `Pool` whose `get` has no timeout knob.
1773
+ * - **sqlite** — ignored. One in-process connection, no pool to exhaust.
1774
+ */
1775
+ readonly acquireTimeoutMs?: number;
1776
+ /**
1777
+ * **mysql/mariadb only, OPT-IN, and a hazard** — how many callers may be
1778
+ * QUEUED waiting for a free pooled connection before the next one fails
1779
+ * immediately (mysql2's `queueLimit`). Unset by default (mysql2's own `0` =
1780
+ * unbounded queue). Env `DB_ACQUIRE_QUEUE_LIMIT`.
1781
+ *
1782
+ * It looks like the mysql substitute for `acquireTimeoutMs`, which mysql2
1783
+ * cannot express for the waiting phase. **It is not, and a default of 100
1784
+ * shipped for exactly one change set before it wedged a process.** A LENGTH
1785
+ * bound and a TIME bound are not interchangeable, and the difference is the
1786
+ * reason this is opt-in:
1787
+ *
1788
+ * - A time bound self-throttles. The acquire fails only after the wait has
1789
+ * elapsed, so a caller that retries on failure retries no faster than the
1790
+ * timeout — a busy loop is impossible.
1791
+ * - A length bound is free. Past the limit mysql2 rejects the acquire
1792
+ * **synchronously** (`lib/base/pool.js` — the queue-limit branch is the one
1793
+ * error return that does not go through `process.nextTick`), so a caller
1794
+ * that retries on failure retries at CPU speed, in the same tick, forever.
1795
+ * The event loop is never reached again: no timer fires, nothing drains the
1796
+ * queue that caused the rejection, and nothing is logged.
1797
+ *
1798
+ * That caller is not hypothetical and not ours to fix: `@effect/cluster`
1799
+ * releases shards one statement per shard (300 by default) and wraps each in
1800
+ * `Effect.eventually` — retry until success, no schedule, no delay, logged at
1801
+ * debug. On a `shardLockDisableAdvisory` (Galera / PXC) runner those releases
1802
+ * are pooled `DELETE FROM cluster_locks` statements, so every graceful
1803
+ * shutdown queued ~290 acquires against a 10-connection pool, crossed the
1804
+ * limit, and spun the process at 100% CPU with no output.
1805
+ *
1806
+ * So: set it only where you know nothing retries an acquire failure without a
1807
+ * delay, and set it well above the fan-out of anything that might.
1808
+ *
1809
+ * Other dialects ignore it: postgres bounds the wait by TIME (which is
1810
+ * strictly better — it does not care how many are ahead of you), and mssql's
1811
+ * Effect `Pool` exposes neither.
1812
+ */
1813
+ readonly acquireQueueLimit?: number;
1567
1814
  /**
1568
1815
  * Per-statement timeout in ms — a runaway query (missing index, cartesian
1569
1816
  * join) is aborted instead of holding a pooled connection forever, which under
@@ -1602,6 +1849,22 @@ export declare interface ConnectionConfig {
1602
1849
  readonly schema?: string;
1603
1850
  }
1604
1851
 
1852
+ /**
1853
+ * A stable, non-disclosing id for the database a connection points at.
1854
+ *
1855
+ * `undefined` when the inputs do not identify one — which a caller must treat as
1856
+ * "cannot tell", never as "different".
1857
+ */
1858
+ export declare const connectionIdentity: (input: ConnectionIdentityInput) => string | undefined;
1859
+
1860
+ export declare interface ConnectionIdentityInput {
1861
+ readonly dialect?: string | undefined;
1862
+ readonly url?: string | undefined;
1863
+ readonly host?: string | undefined;
1864
+ readonly port?: number | undefined;
1865
+ readonly database?: string | undefined;
1866
+ }
1867
+
1605
1868
  /** Case-insensitive substring match on a text column. Non-indexable —
1606
1869
  * always evaluated as an unindexed leaf (the matcher's pre-filter only
1607
1870
  * recognizes eq/in/range). Lowers to `ILIKE '%…%'` on Postgres. */
@@ -2012,6 +2275,29 @@ export declare interface DbEnumHandle<Values extends ReadonlyArray<string>> {
2012
2275
  readonly column: () => ColumnBuilder<Values[number], 'enum'>;
2013
2276
  }
2014
2277
 
2278
+ export declare const dbMetricTagCacheSizeForTests: () => number;
2279
+
2280
+ /**
2281
+ * What kind of database work is being timed.
2282
+ *
2283
+ * A CLOSED union on purpose — it is a metric label, and a label whose value set
2284
+ * is open is a cardinality incident waiting for the first caller who passes a
2285
+ * table name. Adding a member here is a deliberate act with a matching entry in
2286
+ * the docs' metrics table.
2287
+ */
2288
+ export declare type DbOp = 'select' | 'insert' | 'update' | 'delete' | 'upsert' | 'raw' | 'transaction' | 'ddl';
2289
+
2290
+ /** Current in-flight count for `dialect` — the gauge's value, for tests. */
2291
+ export declare const dbOperationsInFlight: (dialect: string) => number;
2292
+
2293
+ /** One completed database operation. */
2294
+ export declare interface DbOpSample {
2295
+ readonly dialect: DialectId | string;
2296
+ readonly op: DbOp;
2297
+ readonly durationMs: number;
2298
+ readonly status: 'ok' | 'error';
2299
+ }
2300
+
2015
2301
  /**
2016
2302
  * Fixed-point exact-decimal column — money, tax rates, anything where
2017
2303
  * floating-point rounding is unacceptable. Emits `NUMERIC(precision, scale)`
@@ -2089,6 +2375,34 @@ export declare const decryptFieldsOnRead: (rows: ReadonlyArray<Row>, table: Tabl
2089
2375
  }) => void;
2090
2376
  }) => ReadonlyArray<Row>;
2091
2377
 
2378
+ /**
2379
+ * 10 s — the framework's default `ConnectionConfig.acquireTimeoutMs`.
2380
+ *
2381
+ * Long enough that a healthy pool under a burst never trips it (a connection is
2382
+ * normally handed over in single-digit milliseconds) and short enough that "the
2383
+ * pool is exhausted" reaches a log line while it is still the answer to a
2384
+ * question somebody is asking. Every dialect that can bound an acquire reads
2385
+ * THIS constant, so the number is one decision rather than four.
2386
+ */
2387
+ export declare const DEFAULT_ACQUIRE_TIMEOUT_MS = 10000;
2388
+
2389
+ /**
2390
+ * How often a persistent fallback is allowed to say so.
2391
+ *
2392
+ * A once-per-table warning (the shape `makeOversizedReporter` uses) is right for
2393
+ * a condition that either recovers or does not recur. This one is different: it
2394
+ * is a PERMANENT per-query cliff, and after the single line scrolls out of the
2395
+ * log there is nothing left to find. Re-warning on an interval keeps a
2396
+ * still-degraded query present in a log an operator greps a week later, without
2397
+ * turning a hot path into a log flood.
2398
+ *
2399
+ * Env: `VOLTRO_DB_EAGER_FALLBACK_WARN_INTERVAL_MS`. `0` warns once per
2400
+ * (table, reason) and never again.
2401
+ */
2402
+ export declare const DEFAULT_EAGER_FALLBACK_WARN_INTERVAL_MS = 300000;
2403
+
2404
+ export declare const DEFAULT_SUBJECT_GRAPH_DEPTH = 4;
2405
+
2092
2406
  export declare const defineMigration: (input: MigrationDefinitionInput) => MigrationDefinition;
2093
2407
 
2094
2408
  /**
@@ -2111,6 +2425,42 @@ export declare const defineSeed: (input: SeedDefinitionInput) => SeedDefinition;
2111
2425
 
2112
2426
  export declare const denseRank: (alias?: string) => WindowBuilder;
2113
2427
 
2428
+ /**
2429
+ * Walk the schema outward from `subjectTable` and return every route to a table
2430
+ * holding that subject's rows.
2431
+ *
2432
+ * Breadth-first with a visited set keyed on (table, foreignKey), so a cyclic
2433
+ * schema terminates and a table reachable two ways contributes both routes ONCE
2434
+ * each rather than looping. Two tables can legitimately be reached by different
2435
+ * columns (`createdBy` and `updatedBy` both pointing at `actors`) and both are
2436
+ * kept — dropping one would silently narrow an erasure.
2437
+ */
2438
+ export declare const deriveSubjectGraph: (input: DeriveSubjectGraphInput) => SubjectGraph;
2439
+
2440
+ export declare interface DeriveSubjectGraphInput {
2441
+ /** The table whose primary key IS the subject id (`users`, `actors`, …). */
2442
+ readonly subjectTable: string;
2443
+ /** Every registered table. Defaults to the live table registry at call time —
2444
+ * a default rather than a fixed read, so a test can hand in a schema without
2445
+ * writing to a process-global other suites also use. */
2446
+ readonly tables?: ReadonlyArray<TableLike>;
2447
+ /** Every registered relation, as `allRegisteredRelations()` returns them.
2448
+ * Defaults to the live relations registry. */
2449
+ readonly relations?: ReadonlyArray<{
2450
+ readonly source: string;
2451
+ readonly name: string;
2452
+ readonly relation: Relation;
2453
+ }>;
2454
+ /**
2455
+ * Hop ceiling. 4 is deep enough for `user → org-membership → project → task`
2456
+ * and shallow enough that a densely-linked schema does not fan out into every
2457
+ * table it has. Anything cut is REPORTED as `depth-truncated`.
2458
+ */
2459
+ readonly maxDepth?: number;
2460
+ /** Tables never to walk INTO. */
2461
+ readonly exclude?: ReadonlyArray<string>;
2462
+ }
2463
+
2114
2464
  /**
2115
2465
  * Lower-cases + naively singularizes a table name into a typeid prefix.
2116
2466
  *
@@ -2167,6 +2517,57 @@ export declare interface DialectReplicationAdapter {
2167
2517
  compare(primaryPosition: string, replicaPosition: string): CaughtUpVerdict;
2168
2518
  }
2169
2519
 
2520
+ /**
2521
+ * The parts of a transaction that are genuinely the DIALECT's. Anything not in
2522
+ * here is owned by `runRetryingTransaction` / `runStoreTransaction` and must not
2523
+ * be re-decided per store — see the header.
2524
+ */
2525
+ export declare interface DialectTransactionSpec {
2526
+ /** `<Store>.transactional` / `<Store>.runInNamespace` — prefixes the
2527
+ * missing-connection error so a failure names the entry point it came from. */
2528
+ readonly label: string;
2529
+ /**
2530
+ * Which dialect this transaction belongs to — the `dialect` label on
2531
+ * `voltro_db_queries_total{op="transaction"}` and its duration histogram.
2532
+ *
2533
+ * REQUIRED rather than derived from `span.attributes['db.system']`: that
2534
+ * field carries OpenTelemetry's spelling (`postgresql`, not `postgres`) and
2535
+ * mysql and mariadb share one value there, so a metric derived from it would
2536
+ * silently merge two dialects. Required also means a new dialect store cannot
2537
+ * be written without deciding what its transactions are called.
2538
+ */
2539
+ readonly dialect: DialectId;
2540
+ /** The store's `sql.withTransaction` — issues BEGIN/COMMIT/ROLLBACK and
2541
+ * provides `TransactionConnection`. */
2542
+ readonly withTransaction: <A>(effect: Effect.Effect<A, unknown, never>) => Effect.Effect<A, unknown, never>;
2543
+ /** The store's managed runtime, ALWAYS as `runPromiseExit` — `runPromise` is
2544
+ * the bug this file exists to prevent, so the shared bracket never sees one. */
2545
+ readonly runPromiseExit: <A>(effect: Effect.Effect<A, unknown, never>) => Promise<Exit.Exit<A, unknown>>;
2546
+ /** Transient-contention predicate: pg `40001`/`40P01`, mysql deadlock/lock-wait,
2547
+ * sqlite `SQLITE_BUSY`, turso's MVCC write-write conflict, mssql `1205`. Drives
2548
+ * BOTH the retry schedule and the commit-defect promotion. */
2549
+ readonly isRetryable: (e: unknown) => boolean;
2550
+ /** OpenTelemetry span for the whole boundary (retries included). */
2551
+ readonly span: {
2552
+ readonly name: string;
2553
+ readonly attributes: Readonly<Record<string, string>>;
2554
+ };
2555
+ /**
2556
+ * Runs on the transaction's connection BEFORE the caller's work, once per
2557
+ * attempt. Postgres' namespace binding (`SET LOCAL search_path TO "<ns>"`)
2558
+ * is the only current use; it must be inside the transaction, on this
2559
+ * connection, or a pooled handoff leaks the tenant's schema into the next
2560
+ * request.
2561
+ */
2562
+ readonly prepare?: (conn: TransactionConnectionContext) => Effect.Effect<unknown, unknown, never>;
2563
+ /**
2564
+ * Wraps the finished program (retry + span already applied) immediately
2565
+ * before it runs. Turso sets the fiber flag that upgrades `BEGIN` to
2566
+ * `BEGIN CONCURRENT` here; every other dialect omits it.
2567
+ */
2568
+ readonly wrapProgram?: <A>(effect: Effect.Effect<A, unknown, never>) => Effect.Effect<A, unknown, never>;
2569
+ }
2570
+
2170
2571
  /**
2171
2572
  * Drain accumulated WARN-level identifier messages. Called once by
2172
2573
  * `voltro dev` (and `voltro start`) after schema discovery so the boot
@@ -2216,6 +2617,26 @@ export declare class EagerCardinalityError extends Error {
2216
2617
  constructor(message: string);
2217
2618
  }
2218
2619
 
2620
+ export declare interface EagerFallbackEvent {
2621
+ readonly dialect: DialectId | string;
2622
+ readonly table: string;
2623
+ readonly reason: EagerFallbackReason;
2624
+ /** The error the JSON-agg statement threw. Absent for `not-compilable`. */
2625
+ readonly error?: unknown;
2626
+ }
2627
+
2628
+ /** Why an eager-load query is running on the walker instead of JSON-agg. */
2629
+ export declare type EagerFallbackReason =
2630
+ /** `compileEagerJson` returned `null` — the shape is not compilable (an
2631
+ * unregistered relation, an ambiguous inferred FK, or a namespaced read on a
2632
+ * dialect whose JSON-agg compiler does not qualify identifiers). Steady
2633
+ * state for a given query: it will never take the fast path. */
2634
+ 'not-compilable'
2635
+ /** The compiled JSON-agg statement THREW and the walker re-ran the work. This
2636
+ * is the one to alert on: the fast path exists, is being attempted, and is
2637
+ * failing — so every such query pays BOTH paths. */
2638
+ | 'execute-failed';
2639
+
2219
2640
  /**
2220
2641
  * Compiled JSON-aggregation plan: a `Statement.Statement` ready to
2221
2642
  * execute via `SqlClient.SqlClient`, plus a `decode()` that restores
@@ -2447,6 +2868,13 @@ export declare const expires: () => MixinDefinition<{
2447
2868
  readonly expiresAt: ColumnDefinition<Date | null, "timestamp", boolean>;
2448
2869
  }>;
2449
2870
 
2871
+ /** The subset of `@voltro/logger`'s logger this module needs. Structural so
2872
+ * this package keeps its browser-safe import surface. */
2873
+ export declare interface FallbackLogger {
2874
+ readonly warn: (message: string, fields?: Record<string, unknown>) => void;
2875
+ readonly debug: (message: string, fields?: Record<string, unknown>) => void;
2876
+ }
2877
+
2450
2878
  /** The cipher the store middleware injects. Operates on opaque strings. */
2451
2879
  export declare interface FieldCipher {
2452
2880
  readonly encrypt: (plaintext: string) => string;
@@ -2543,6 +2971,26 @@ export declare interface FileMigrationContext {
2543
2971
  readonly appliedAt: string;
2544
2972
  }
2545
2973
 
2974
+ /**
2975
+ * Fire the `onSchemaChange` seeds for the tables an apply just changed.
2976
+ *
2977
+ * Unlike tenant-create this does NOT propagate: a schema apply that succeeded
2978
+ * must not be reported as failed because a data fixture threw. The runner logs
2979
+ * + records the failure in `_voltro_seeds`.
2980
+ */
2981
+ export declare const fireSchemaChangeSeeds: (event: SchemaChangeSeedEvent) => Promise<void>;
2982
+
2983
+ /**
2984
+ * Fire the `onTenantCreate` seeds for a freshly provisioned namespace.
2985
+ *
2986
+ * Awaited, and a rejection PROPAGATES to the provisioning caller on purpose:
2987
+ * a tenant whose namespace exists but whose seed data does not is a broken
2988
+ * tenant that reports success, which is the exact failure class this seam was
2989
+ * built to remove. `provisionTenantNamespace` is idempotent, so the caller's
2990
+ * retry is safe.
2991
+ */
2992
+ export declare const fireTenantCreateSeeds: (event: TenantCreateSeedEvent) => Promise<void>;
2993
+
2546
2994
  /** Flatten an intersection into a single object type for readable errors/hovers. */
2547
2995
  declare type Flatten<O> = {
2548
2996
  [K in keyof O]: O[K];
@@ -2565,6 +3013,10 @@ declare interface FormatIssueOptions {
2565
3013
  readonly indent?: string;
2566
3014
  }
2567
3015
 
3016
+ /** Human-readable rendering of a derived graph — what `voltro privacy scope`
3017
+ * prints, and what a DPO reads before signing off on a DSAR process. */
3018
+ export declare const formatSubjectGraph: (graph: SubjectGraph) => string;
3019
+
2568
3020
  /**
2569
3021
  * Live tables the framework's own runtime creates under a name that does NOT
2570
3022
  * start with a reserved prefix, so the prefix rule below cannot recognise them.
@@ -3140,6 +3592,9 @@ export declare const lead: (column: string, offset?: number, alias?: string) =>
3140
3592
 
3141
3593
  export declare type LeafOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'startsWith' | 'arrayContains' | 'arrayOverlaps' | 'arrayHas' | 'isNull' | 'isNotNull' | 'spatial';
3142
3594
 
3595
+ /** Every registration conflict since the last {@link clearRetentions}. */
3596
+ export declare const listRetentionConflicts: () => ReadonlyArray<RetentionConflict>;
3597
+
3143
3598
  /** Every currently-registered retention spec. The boot sweep reads this each tick. */
3144
3599
  export declare const listRetentions: () => ReadonlyArray<RetentionSpec>;
3145
3600
 
@@ -3179,14 +3634,40 @@ export declare const lt: <RowOf = Record<string, unknown>, K extends keyof RowOf
3179
3634
 
3180
3635
  export declare const lte: <RowOf = Record<string, unknown>, K extends keyof RowOf & string = keyof RowOf & string>(column: K, value: RowOf[K]) => PredicateLeaf;
3181
3636
 
3637
+ /**
3638
+ * Build the store-scoped fallback reporter: counts EVERY occurrence, logs a
3639
+ * rate-limited line.
3640
+ *
3641
+ * Instance-scoped rather than a module global, for the reason
3642
+ * `makeOversizedReporter` states: the rate limit must be the STORE's, so a
3643
+ * second store in the same process (a read replica, a test harness) cannot
3644
+ * silence the first one's first warning.
3645
+ *
3646
+ * The COUNTER is never rate-limited. That separation is the whole point of
3647
+ * PERF-23 — the log line is for a human reading it at the moment it happens,
3648
+ * and the counter is for the dashboard that has to show a cliff which started
3649
+ * three weeks ago.
3650
+ */
3651
+ export declare const makeEagerFallbackReporter: (log: FallbackLogger, options?: {
3652
+ readonly warnIntervalMs?: number;
3653
+ readonly now?: () => number;
3654
+ }) => ((event: EagerFallbackEvent) => void);
3655
+
3182
3656
  /**
3183
3657
  * A concrete {@link BranchExecutor} for the NAMESPACE mechanism on Postgres — it
3184
3658
  * emits schema DDL + table-copy DML through an injected {@link BranchSqlRunner}.
3185
3659
  * `createTable` clones the parent table's STRUCTURE (`LIKE … INCLUDING ALL`);
3186
- * `copyTable` snapshots its rows (`INSERT … SELECT`); teardown drops the schema.
3660
+ * `copyTable` snapshots its rows (`INSERT … SELECT`); `addForeignKey` replays the
3661
+ * referential integrity `LIKE` does not carry; teardown drops the schema.
3187
3662
  * The Neon ops throw — a copy-on-write branch needs a Neon connection (which
3188
3663
  * resolves to the `neon-cow` mechanism + its own executor).
3189
3664
  *
3665
+ * POSTGRES ONLY, and the name is the whole warning: every statement here is
3666
+ * postgres syntax (`CREATE SCHEMA`, `LIKE … INCLUDING ALL`, `"` quoting). MySQL /
3667
+ * MariaDB spell the clone `CREATE TABLE x LIKE y` and quote with backticks;
3668
+ * sqlite has no schema namespace to create. A caller on another dialect needs
3669
+ * its own executor — the PLAN is dialect-agnostic, this executor is not.
3670
+ *
3190
3671
  * Pure SQL emission: unit-tested with a recording runner (no live DB to BUILD or
3191
3672
  * TEST — only to RUN, like any SQL). The cloud wires `run` to its owned store.
3192
3673
  */
@@ -3482,6 +3963,15 @@ export declare const neq: <RowOf = Record<string, unknown>, K extends keyof RowO
3482
3963
  * double-destroy or a provision-after-destroyed can't corrupt state). */
3483
3964
  export declare const nextBranchState: (current: BranchState, event: BranchEvent) => BranchState;
3484
3965
 
3966
+ /** The normalised `(host, port, database)` a connection points at, before
3967
+ * hashing. Exported for the tests — the digest is not readable, and a
3968
+ * normalisation bug in it would be invisible otherwise. */
3969
+ export declare const normaliseTarget: (input: ConnectionIdentityInput) => {
3970
+ readonly host: string;
3971
+ readonly port: number;
3972
+ readonly database: string;
3973
+ } | undefined;
3974
+
3485
3975
  /** Negate a sub-predicate: `not(eq('archived', true))` → `NOT (...)`.
3486
3976
  * Wraps any subtree, including `and(...)`/`or(...)`. */
3487
3977
  export declare const not: (predicate: Predicate) => NotPredicate;
@@ -3506,6 +3996,16 @@ export declare interface NotPredicate {
3506
3996
  */
3507
3997
  export declare const numeric: (precision: number, scale?: number) => ColumnBuilder<string, "decimal">;
3508
3998
 
3999
+ /**
4000
+ * Time `run`, count it, and hold the in-flight gauge for its duration.
4001
+ *
4002
+ * The `finally` is load-bearing in two directions: a throw must still be TIMED
4003
+ * (a statement that fails after 30 seconds is the interesting one) and must
4004
+ * still release the gauge, or a single failing query leaks a permanent +1 and
4005
+ * the saturation signal drifts upward forever.
4006
+ */
4007
+ export declare const observeDbOp: <T>(dialect: DialectId | string, op: DbOp, run: () => Promise<T>) => Promise<T>;
4008
+
3509
4009
  /**
3510
4010
  * What a decrypt failure does. `'throw'` (default, and the ONLY safe production
3511
4011
  * behaviour) surfaces a typed `FieldDecryptionError`. `'null'` degrades the one
@@ -3650,10 +4150,18 @@ export declare const planBranch: (input: BranchPlanInput) => ReadonlyArray<Branc
3650
4150
  /**
3651
4151
  * The ordered steps to stand a branch up — what the cloud executor runs
3652
4152
  * (create-namespace + create-table via `provisionTenantNamespace`, copy-table
3653
- * via cross-namespace `INSERT … SELECT`, seed via the boot seeds). Pure plan, so
4153
+ * via cross-namespace `INSERT … SELECT`, copy-foreign-key via
4154
+ * `ALTER TABLE … ADD CONSTRAINT`, seed via the boot seeds). Pure plan, so
3654
4155
  * it's unit-testable; the executor is the cloud side.
4156
+ *
4157
+ * The FK replay is sequenced AFTER the row copy on purpose: `INSERT … SELECT`
4158
+ * per table has no topological order, so a child table copied before its parent
4159
+ * would violate a constraint that is only correct once every table is populated.
4160
+ * It is sequenced BEFORE the `fresh` seed for the mirror-image reason — seeds
4161
+ * write through the app's own paths and should meet the same integrity rules
4162
+ * production has.
3655
4163
  */
3656
- export declare const planBranchProvision: (branchNamespace: string, tableNames: ReadonlyArray<string>, seed: BranchSeed, parentNamespace?: string) => ReadonlyArray<BranchStep>;
4164
+ export declare const planBranchProvision: (branchNamespace: string, tableNames: ReadonlyArray<string>, seed: BranchSeed, parentNamespace?: string, foreignKeys?: ReadonlyArray<BranchForeignKey>, indexNames?: ReadonlyArray<BranchIndexName>) => ReadonlyArray<BranchStep>;
3657
4165
 
3658
4166
  /** The teardown plan — drop the whole branch namespace (idempotent on the
3659
4167
  * executor side). */
@@ -4583,11 +5091,22 @@ declare interface RawSqlFragment {
4583
5091
  readonly strings: ReadonlyArray<string>;
4584
5092
  readonly values: ReadonlyArray<unknown>;
4585
5093
  /**
4586
- * Tables this raw read depends on, for reactive invalidation. Raw
4587
- * reads are otherwise untracked — the query planner can't infer which
4588
- * tables an arbitrary SQL string touches. Declare them here (or via
4589
- * `store.raw(fragment, { dependsOn })`) to opt the read into the
4590
- * reactive layer's change-driven recomputation.
5094
+ * Tables this raw read depends on, for reactive invalidation. Raw reads are
5095
+ * otherwise untracked — the query planner cannot infer which tables an
5096
+ * arbitrary SQL string touches. Declare them here (or via
5097
+ * `store.raw(fragment, { dependsOn })`).
5098
+ *
5099
+ * WHAT IT DRIVES, stated narrowly because the previous wording promised more
5100
+ * than any code delivered. For a live query whose handler returns a COMPUTED
5101
+ * VALUE — the shape whose handler is genuinely re-run on a change — these
5102
+ * tables JOIN the query's declared `source:`, so a write to one recomputes the
5103
+ * subscription. That is the only shape where re-running can refresh a raw
5104
+ * result. A handler that returns a query DESCRIPTOR re-runs the DESCRIPTOR on
5105
+ * a change, not the handler, so declaring tables cannot refresh the raw read
5106
+ * there; the runtime warns about that case at subscribe time instead.
5107
+ *
5108
+ * Best-effort in both directions: the tables are recorded as declared and
5109
+ * never validated against the SQL.
4591
5110
  */
4592
5111
  readonly dependsOn?: ReadonlyArray<string>;
4593
5112
  }
@@ -4617,6 +5136,15 @@ export declare const readableByColumns: (table: TableLike) => ReadonlyArray<Read
4617
5136
  * would truncate. */
4618
5137
  export declare const real: () => ColumnBuilder<number, "real", boolean>;
4619
5138
 
5139
+ /**
5140
+ * Record one completed operation: the counter, the duration histogram, and — on
5141
+ * a throw — the error counter.
5142
+ *
5143
+ * Prefer {@link observeDbOp}, which also moves the in-flight gauge and cannot
5144
+ * forget the `error` arm on a throw.
5145
+ */
5146
+ export declare const recordDbOp: (s: DbOpSample) => void;
5147
+
4620
5148
  /** One write, as the store sees it at the moment it happens. */
4621
5149
  export declare interface RecordedWrite {
4622
5150
  readonly table: string;
@@ -4733,7 +5261,33 @@ export declare const registerPendingAttribution: (key: string, attribution: Writ
4733
5261
  */
4734
5262
  export declare const registerRelations: (spec: RelationsSpec) => void;
4735
5263
 
4736
- /** Register (or replace) the retention bound for a table. Idempotent per table. */
5264
+ /**
5265
+ * Register the retention bound for a table.
5266
+ *
5267
+ * ── Two registrations for one table used to be a silent last-write-wins ─────
5268
+ *
5269
+ * A consumer registered `_voltro_schedule_claims` at 1 hour from a startup, and
5270
+ * one second later the framework registered its own default for the same table.
5271
+ * Ours won, nothing said so, and their startup went on logging `bounded to 1h`
5272
+ * at every boot while the table kept everything younger than the framework's
5273
+ * TTL. They found it by counting rows, not by reading a log.
5274
+ *
5275
+ * `registry.set(table, spec)` is what did that, and the comment above it —
5276
+ * "so a double-registration (e.g. plugin re-init) is idempotent" — describes a
5277
+ * case that really exists (a plugin re-registering ITS OWN spec) and silently
5278
+ * covered a different one.
5279
+ *
5280
+ * **Precedence now decides, and a real conflict is always reported.** An app's
5281
+ * registration outranks a plugin's, which outranks a framework default; a tie
5282
+ * keeps the later one, as before. `source` defaults to `'app'`, so an
5283
+ * application does not have to know the field exists to win with it.
5284
+ *
5285
+ * The direction was not obvious and is worth stating: the loser here is chosen
5286
+ * by WHO registered, not by which TTL is narrower. A narrower TTL deletes more,
5287
+ * and picking "narrower wins" would let a framework default we tighten in some
5288
+ * future release silently start deleting an app's data faster than the app
5289
+ * asked for. Whoever owns the data decides; we are the fallback.
5290
+ */
4737
5291
  export declare const registerRetention: (spec: RetentionSpec) => void;
4738
5292
 
4739
5293
  /**
@@ -4808,6 +5362,9 @@ export declare const requireTable: (tableName: string) => TableLike;
4808
5362
 
4809
5363
  export declare const requireTenants: () => (() => TableLike);
4810
5364
 
5365
+ /** Test seams for the cardinality guard — the caches are module-private otherwise. */
5366
+ export declare const resetDbMetricTagCacheForTests: () => void;
5367
+
4811
5368
  /**
4812
5369
  * Re-read SNOWFLAKE_MACHINE_ID from the environment. Use in tests or
4813
5370
  * after a runtime reconfiguration. Production code should set the env
@@ -4874,6 +5431,13 @@ export declare const resolveActorSnapshot: (store: ActorLookupStore, subjectId:
4874
5431
  */
4875
5432
  export declare const resolveBranchMechanism: (opts: BranchMechanismOptions) => BranchMechanism;
4876
5433
 
5434
+ /** Resolve the re-warn interval. Env overrides the caller, like every other
5435
+ * tunable here; an unparseable or negative value falls through to the default
5436
+ * rather than being honoured. */
5437
+ export declare const resolveEagerFallbackWarnIntervalMs: (configured?: number | undefined, env?: {
5438
+ readonly VOLTRO_DB_EAGER_FALLBACK_WARN_INTERVAL_MS?: string;
5439
+ }) => number;
5440
+
4877
5441
  /**
4878
5442
  * Answer a transport echo with the attribution of the local write that produced
4879
5443
  * it, or with `undefined` when the write came from another replica.
@@ -4943,6 +5507,22 @@ export declare const resolveTenantHome: (tenantId: string | null | undefined, co
4943
5507
  */
4944
5508
  export declare const resolveTenantNamespace: (tenantId: string | null | undefined) => string;
4945
5509
 
5510
+ export declare interface RetentionConflict {
5511
+ readonly table: string;
5512
+ readonly kept: {
5513
+ readonly source: RetentionSource;
5514
+ readonly ttlMs: number;
5515
+ };
5516
+ readonly dropped: {
5517
+ readonly source: RetentionSource;
5518
+ readonly ttlMs: number;
5519
+ };
5520
+ }
5521
+
5522
+ /** Who registered a bound. Higher precedence first: an app's deliberate policy
5523
+ * outranks a plugin's, which outranks a framework default. */
5524
+ export declare type RetentionSource = 'app' | 'plugin' | 'framework';
5525
+
4946
5526
  /** A registered bound on one append-only table. */
4947
5527
  export declare interface RetentionSpec {
4948
5528
  /** Physical table name, e.g. `_voltro_schedule_runs`. */
@@ -4961,6 +5541,16 @@ export declare interface RetentionSpec {
4961
5541
  * plain time-only purge.
4962
5542
  */
4963
5543
  readonly where?: Predicate;
5544
+ /**
5545
+ * Who registered this. Decides which registration wins when two arrive for
5546
+ * one table — see {@link registerRetention}.
5547
+ *
5548
+ * DEFAULTS TO `'app'`, and that default is the feature: an application
5549
+ * registering a bound has said something deliberate about ITS data, so it
5550
+ * outranks a framework default without having to know this field exists. The
5551
+ * framework and plugins pass their own value explicitly.
5552
+ */
5553
+ readonly source?: RetentionSource;
4964
5554
  /** Optional human label for logs (defaults to the table name). */
4965
5555
  readonly label?: string;
4966
5556
  /**
@@ -5048,6 +5638,57 @@ export declare interface RuleViolationDetail {
5048
5638
  readonly message?: string;
5049
5639
  }
5050
5640
 
5641
+ /**
5642
+ * Run `body` inside one retrying transaction and settle its Exit.
5643
+ *
5644
+ * Owns, for every dialect:
5645
+ * 1. the connection handshake — `withTransaction` + `TransactionConnection`,
5646
+ * with a loud failure rather than a silent non-transactional run when the
5647
+ * service is somehow absent;
5648
+ * 2. **commit-defect promotion** — `@effect/sql` runs COMMIT as `Effect.orDie`,
5649
+ * so a serialization/deadlock error raised AT COMMIT arrives as a defect and
5650
+ * `Effect.retry` never sees it. A retryable defect is promoted back to a
5651
+ * typed failure so the schedule can replay it; anything else re-dies, so
5652
+ * genuine defects keep crash semantics;
5653
+ * 3. the retry schedule — exponential backoff from 10ms, up to 3 retries,
5654
+ * gated on the dialect's `isRetryable`. Each attempt re-runs `body` from
5655
+ * scratch against a FRESH transaction;
5656
+ * 4. the span, covering all attempts as one logical operation;
5657
+ * 5. settling through `settleTransactionExit`, never `runPromise`.
5658
+ *
5659
+ * `body` receives the attempt number (1-based) so a caller can build per-attempt
5660
+ * state that must not be shared across a retry.
5661
+ */
5662
+ export declare const runRetryingTransaction: <A>(spec: DialectTransactionSpec & {
5663
+ readonly body: (conn: TransactionConnectionContext, attempt: number) => Promise<A>;
5664
+ }) => Promise<A>;
5665
+
5666
+ /**
5667
+ * THE entry point every dialect store's `transactional()` — and postgres'
5668
+ * `runInNamespace()` — routes through.
5669
+ *
5670
+ * On top of `runRetryingTransaction` it owns the two things that are about the
5671
+ * CALLER rather than the engine:
5672
+ *
5673
+ * - **write attribution.** Captured HERE, synchronously, before any await:
5674
+ * `transactional()` is entered from the request's async-local scope but the
5675
+ * callback runs from inside the Effect, where — measured against live
5676
+ * postgres — that scope is EMPTY. The captured value is handed to
5677
+ * `makeView` so every write the view makes carries the identity explicitly
5678
+ * (a pool handoff cannot strand it), and the scope is re-entered around
5679
+ * `work` for anything the view reaches that is not threaded. `undefined`
5680
+ * stays `undefined`: "no request behind this write" and "a request that knew
5681
+ * nothing" are different facts and must not blur.
5682
+ * - **event drain ordering.** A fresh view per attempt, and `commitEvents()`
5683
+ * only after the Exit settled successfully — so a retried attempt's buffered
5684
+ * events are dropped with its unreachable view and subscribers see exactly
5685
+ * the winning attempt, once.
5686
+ */
5687
+ export declare const runStoreTransaction: <T, V extends DataStore & TransactionalViewHandle>(spec: DialectTransactionSpec & {
5688
+ readonly work: (tx: DataStore) => Promise<T>;
5689
+ readonly makeView: (conn: TransactionConnectionContext, attr: WriteAttribution | undefined) => V;
5690
+ }) => Promise<T>;
5691
+
5051
5692
  /**
5052
5693
  * Run `fn` with `attribution` active for its whole (sync + awaited) execution.
5053
5694
  *
@@ -5065,6 +5706,16 @@ export declare const runWithWriteAttribution: <T>(attribution: WriteAttribution,
5065
5706
  */
5066
5707
  export declare const runWriteRecorders: (port: TxnRecorderPort, write: RecordedWrite) => Promise<void>;
5067
5708
 
5709
+ /**
5710
+ * Are two connections pointed at the same database?
5711
+ *
5712
+ * Three answers, and the third is the load-bearing one: `'unknown'` means at
5713
+ * least one side could not be identified, and the caller keeps whatever
5714
+ * conservative behaviour it had. Collapsing that into `false` is how a guard
5715
+ * stops guarding.
5716
+ */
5717
+ export declare const sameDatabase: (a: string | undefined, b: string | undefined) => "same" | "different" | "unknown";
5718
+
5068
5719
  /**
5069
5720
  * Lower a tenant id to the safe identifier fragment used inside a
5070
5721
  * namespace name. ONLY `[a-z0-9_]` survive; every other character —
@@ -5082,6 +5733,15 @@ export declare const runWriteRecorders: (port: TxnRecorderPort, write: RecordedW
5082
5733
  */
5083
5734
  export declare const sanitizeIdentifierFragment: (tenantId: string) => string;
5084
5735
 
5736
+ /** What a schema-change firing knows: which tables the apply actually touched.
5737
+ * A seed watches table NAMES (`watchedTables`), so this is the list the
5738
+ * runner intersects against — an empty list fires nothing. */
5739
+ export declare interface SchemaChangeSeedEvent {
5740
+ readonly changedTables: ReadonlyArray<string>;
5741
+ }
5742
+
5743
+ export declare type SchemaChangeSeedHook = (event: SchemaChangeSeedEvent) => Promise<void>;
5744
+
5085
5745
  /**
5086
5746
  * The PUBLIC structural view of a built table — name, columns, and applied
5087
5747
  * indexes — with NONE of the fluent builder methods.
@@ -5129,6 +5789,7 @@ export declare interface SeedDefinition {
5129
5789
  readonly name: string;
5130
5790
  readonly lifecycle: SeedLifecycle;
5131
5791
  readonly cron?: string;
5792
+ readonly timezone?: string;
5132
5793
  readonly watchedTables?: ReadonlyArray<string>;
5133
5794
  readonly fingerprint?: (ctx: {
5134
5795
  src: string;
@@ -5143,6 +5804,15 @@ export declare interface SeedDefinitionInput {
5143
5804
  readonly lifecycle: SeedLifecycle;
5144
5805
  /** Required for `lifecycle: 'cron'`. Standard cron expression or `@hourly`/`@daily` etc. */
5145
5806
  readonly cron?: string;
5807
+ /**
5808
+ * IANA timezone the `cron` expression is read in. Default `'UTC'`.
5809
+ *
5810
+ * Defaulted rather than required, unlike `defineSchedule`'s: a seed is
5811
+ * reference data, so "02:00 in Europe/Berlin" is a much rarer requirement
5812
+ * than it is for a business schedule — but server-local is never the answer,
5813
+ * so the default is an explicit UTC rather than the container's clock.
5814
+ */
5815
+ readonly timezone?: string;
5146
5816
  /** Required for `lifecycle: 'onSchemaChange'`. Table names to watch. */
5147
5817
  readonly watchedTables?: ReadonlyArray<string>;
5148
5818
  /**
@@ -5268,6 +5938,12 @@ export declare const serverOnlyColumns: (table: TableLike) => ReadonlyArray<stri
5268
5938
  * to the pure resolvers in tests instead. */
5269
5939
  export declare const setResidencyConfig: (config: ResidencyConfig) => ResidencyConfig;
5270
5940
 
5941
+ /** Install (or, with `null`, remove) the `onSchemaChange` runner. */
5942
+ export declare const setSchemaChangeSeedHook: (hook: SchemaChangeSeedHook | null) => void;
5943
+
5944
+ /** Install (or, with `null`, remove) the `onTenantCreate` runner. */
5945
+ export declare const setTenantCreateSeedHook: (hook: TenantCreateSeedHook | null) => void;
5946
+
5271
5947
  /**
5272
5948
  * Unwrap a transaction program's `Exit`: return the value, or throw the
5273
5949
  * ORIGINAL error rather than Effect's `FiberFailure` wrapper.
@@ -5490,6 +6166,51 @@ export declare interface StreamTableOptions {
5490
6166
  readonly retry?: Schedule.Schedule<unknown, TableStreamError>;
5491
6167
  }
5492
6168
 
6169
+ export declare interface SubjectGraph {
6170
+ readonly subjectTable: string;
6171
+ readonly paths: ReadonlyArray<SubjectPath>;
6172
+ readonly limitations: ReadonlyArray<SubjectGraphLimitation>;
6173
+ }
6174
+
6175
+ /** A known blind spot in a derived graph. Always reported. */
6176
+ export declare interface SubjectGraphLimitation {
6177
+ readonly kind:
6178
+ /** BFS hit `maxDepth`; tables beyond `table` were not explored. */
6179
+ 'depth-truncated'
6180
+ /** A table nothing links to the subject — invisible to this derivation. */
6181
+ | 'unreachable'
6182
+ /** Excluded by configuration. */
6183
+ | 'excluded';
6184
+ readonly table: string;
6185
+ readonly detail: string;
6186
+ }
6187
+
6188
+ /** One hop away from the previous table. */
6189
+ export declare interface SubjectHop {
6190
+ /** The table this hop lands on. */
6191
+ readonly table: string;
6192
+ /** Column on `table` holding the PREVIOUS row's key. */
6193
+ readonly foreignKey: string;
6194
+ /** Column on the PREVIOUS table whose value `foreignKey` holds. Almost always
6195
+ * `id`; `sourceKey` on a relation can move it. */
6196
+ readonly parentKey: string;
6197
+ }
6198
+
6199
+ /**
6200
+ * An ordered route from the subject table to a table holding their data.
6201
+ * `hops` is empty for the subject table itself (the row keyed by the subject id).
6202
+ */
6203
+ export declare interface SubjectPath {
6204
+ /** Terminal table — where rows are read/erased. */
6205
+ readonly table: string;
6206
+ readonly hops: ReadonlyArray<SubjectHop>;
6207
+ /** `hops.length`. 0 = the subject row itself, 1 = a direct child. */
6208
+ readonly depth: number;
6209
+ /** Which declaration produced it — a declared `relations()` block, an
6210
+ * inferred `reference()` column, or an app-supplied `subjectScopes` entry. */
6211
+ readonly via: 'relation' | 'reference' | 'declared';
6212
+ }
6213
+
5493
6214
  /**
5494
6215
  * `col IN (SELECT col FROM ...)` / `col NOT IN (SELECT ...)` — the
5495
6216
  * RHS is a sub-query descriptor. The sub-query MUST project a
@@ -6117,6 +6838,17 @@ export declare interface TableUnique {
6117
6838
  */
6118
6839
  export declare const teardownBranch: (branchId: string, mechanism: BranchMechanism, executor: BranchExecutor) => Promise<ReadonlyArray<BranchStep>>;
6119
6840
 
6841
+ /** What a tenant-create firing knows: which namespace was just provisioned. */
6842
+ export declare interface TenantCreateSeedEvent {
6843
+ /** The namespace (postgres schema / mysql database / attached sqlite file)
6844
+ * that was just created. Seeds run scoped to THIS namespace, never the
6845
+ * shared one. */
6846
+ readonly namespace: string;
6847
+ readonly dialect: string;
6848
+ }
6849
+
6850
+ export declare type TenantCreateSeedHook = (event: TenantCreateSeedEvent) => Promise<void>;
6851
+
6120
6852
  /** A tenant's declared home — the region it's pinned to, optionally a named
6121
6853
  * connection/secret key for that region's DB. */
6122
6854
  export declare interface TenantHome {
@@ -6212,6 +6944,22 @@ export declare const timestampMs: Schema.Schema<Date, number>;
6212
6944
  */
6213
6945
  export declare const timestampMsOrNull: Schema.Schema<Date | null, number | null>;
6214
6946
 
6947
+ /**
6948
+ * A store's per-transaction `DataStore` view: buffers the ChangeEvents its
6949
+ * writes produce and drains them to the parent emitter only once the
6950
+ * transaction has actually committed.
6951
+ */
6952
+ export declare interface TransactionalViewHandle {
6953
+ commitEvents(): void;
6954
+ }
6955
+
6956
+ /**
6957
+ * The `[Connection, depth]` tuple `@effect/sql` provides inside
6958
+ * `withTransaction`. Every statement that must run on the transaction's OWN
6959
+ * connection (rather than a fresh one from the pool) is given this explicitly.
6960
+ */
6961
+ export declare type TransactionConnectionContext = Context.Tag.Service<typeof TransactionConnection>;
6962
+
6215
6963
  /** Append one row inside the caller's transaction. Insert-only by design. */
6216
6964
  export declare type TxnAppend = (table: string, row: Row) => Promise<void>;
6217
6965
 
@@ -6730,6 +7478,21 @@ export declare interface WriteAttribution {
6730
7478
  /** `null` is meaningful: a resolved subject with no acting user (an
6731
7479
  * anonymous or system call), as distinct from `undefined` = no request. */
6732
7480
  readonly subjectId?: string | null;
7481
+ /**
7482
+ * The write was made BY AN AGENT on the subject's behalf.
7483
+ *
7484
+ * `subjectId` stays the PERSON — an agent never escalates identity, which is
7485
+ * the whole property `@voltro/ai`'s `agentActor` encodes. So without a second
7486
+ * field, "Anna archived this invoice" and "an agent archived it while acting
7487
+ * as Anna" are the same row, and the difference is the one a reviewer of an
7488
+ * agent rollout actually asks about.
7489
+ *
7490
+ * Set by the transports that KNOW: the app-tool surface reached over MCP
7491
+ * (`@voltro/cli`'s `agentToolSurface`) and any in-process `appTools` caller
7492
+ * that opts in. Absent for an ordinary request — absent means "a human or a
7493
+ * service called this directly", not "unknown".
7494
+ */
7495
+ readonly via?: 'agent';
6733
7496
  }
6734
7497
 
6735
7498
  export declare type WriteRecorder = (port: TxnRecorderPort, write: RecordedWrite) => Promise<void>;