@voltro/database 0.71.1 → 0.73.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/sql.d.ts CHANGED
@@ -22,6 +22,19 @@ declare interface AggregateColumn<TResult = number | Date | null> {
22
22
  readonly _result?: TResult;
23
23
  }
24
24
 
25
+ declare interface AggregateColumn_2<TResult = number | Date | null> {
26
+ readonly op: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count-distinct' | 'window-row-number' | 'window-rank' | 'window-dense-rank' | 'window-lag' | 'window-lead' | 'window-sum-over' | 'window-avg-over' | 'column';
27
+ readonly column?: string;
28
+ readonly alias: string;
29
+ readonly window?: WindowSpec_2;
30
+ /** Used by lag/lead — offset rows (default 1). */
31
+ readonly offset?: number;
32
+ /** Phantom — carries the projected/aggregated result type so
33
+ * `aggregate<Spec>()` can infer a precise result row. Never set at
34
+ * runtime; exists purely for `AggregateColumn<infer T>` inference. */
35
+ readonly _result?: TResult;
36
+ }
37
+
25
38
  /** One condition under which a failed DDL statement means "already done". */
26
39
  export declare interface AlreadySatisfied {
27
40
  /** Why this is safe to ignore. Read by a human, not by code. */
@@ -36,6 +49,10 @@ declare interface AndPredicate {
36
49
  readonly and: ReadonlyArray<Predicate>;
37
50
  }
38
51
 
52
+ declare interface AndPredicate_2 {
53
+ readonly and: ReadonlyArray<Predicate_2>;
54
+ }
55
+
39
56
  /**
40
57
  * Approximate-nearest-neighbour search clause carried on a
41
58
  * {@link QueryDescriptor}. See {@link QueryDescriptor.annClause}.
@@ -62,6 +79,32 @@ declare interface AnnClause {
62
79
  readonly alpha?: number;
63
80
  }
64
81
 
82
+ /**
83
+ * Approximate-nearest-neighbour search clause carried on a
84
+ * {@link QueryDescriptor}. See {@link QueryDescriptor.annClause}.
85
+ */
86
+ declare interface AnnClause_2 {
87
+ /** Vector column to search. */
88
+ readonly column: string;
89
+ /** Literal query vector (array overload). Mutually exclusive with `queryText`. */
90
+ readonly queryVector?: ReadonlyArray<number>;
91
+ /** Raw query string (string overload) — embedded by the runtime before
92
+ * compile. Mutually exclusive with `queryVector`. */
93
+ readonly queryText?: string;
94
+ /** Distance metric — MUST match the index opclass to accelerate. Default 'cosine'. */
95
+ readonly distance: VectorDistance_2;
96
+ /** Per-query HNSW search effort (pgvector `hnsw.ef_search`). Set by `.efSearch(n)`. */
97
+ readonly ef?: number;
98
+ /**
99
+ * Hybrid-search fusion weight in `[0, 1]`. Set by `hybridSearch(...)`
100
+ * when both an ANN ranking and an FTS filter are present: `0` = pure
101
+ * FTS, `1` = pure vector, `0.5` = balanced. The runtime fuses the two
102
+ * rank signals (Reciprocal-Rank-Fusion) weighted by this. Absent for a
103
+ * plain `nearestNeighbours(...)` query.
104
+ */
105
+ readonly alpha?: number;
106
+ }
107
+
65
108
  declare type AnyMixin = MixinDefinition<Record<string, ColumnDefinition<unknown>>>;
66
109
 
67
110
  declare type AnyTable = Table<string, Record<string, ColumnDefinition<unknown>>, boolean, string>;
@@ -76,6 +119,24 @@ declare type AnyView = View<string, Record<string, ColumnDefinition<unknown>>>;
76
119
  */
77
120
  export declare const appliedAtValue: (sql: SqlClient.SqlClient, iso: string) => Effect.Effect<string>;
78
121
 
122
+ /**
123
+ * The ids recorded as applied file migrations on THIS database — exported for
124
+ * the deploy compensation, which compares the set before a run with the set
125
+ * after it to know which `down`s to run. An absent ledger table reads as an
126
+ * empty set: a virgin database has applied nothing.
127
+ */
128
+ export declare const appliedFileMigrationIds: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<string>, never>;
129
+
130
+ /**
131
+ * The operations the resume ledger says RAN — `started`, `added` or `applied` —
132
+ * decoded. The deploy compensation reads them for the one thing a schema diff
133
+ * cannot see: a rename. A column renamed a→b and then reverted by a diff alone
134
+ * is DROP b + ADD a, which loses the column's values; knowing the rename was
135
+ * applied lets the undo declare `a` as renamed from `b` and the planner emit
136
+ * the rename back. An absent ledger table means nothing ran.
137
+ */
138
+ export declare const appliedLedgerOperations: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<PlannedOperation>, never>;
139
+
79
140
  /**
80
141
  * One row of `_voltro_migrations` — what the applier writes after a
81
142
  * successful plan execution. Same shape exposed via inspect RPC for
@@ -241,6 +302,9 @@ export declare const assessRollingDeployGate: (input: {
241
302
  /** Per-row backfill body executed by the migration runner. */
242
303
  declare type BackfillFunction<TsType = unknown> = (row: Readonly<Record<string, unknown>>) => TsType | Promise<TsType>;
243
304
 
305
+ /** Per-row backfill body executed by the migration runner. */
306
+ declare type BackfillFunction_2<TsType = unknown> = (row: Readonly<Record<string, unknown>>) => TsType | Promise<TsType>;
307
+
244
308
  declare interface BackfillSpec<TsType = unknown> {
245
309
  /**
246
310
  * `sql`: server-side expression evaluated by `UPDATE ... SET col = <expr>`.
@@ -259,6 +323,24 @@ declare interface BackfillSpec<TsType = unknown> {
259
323
  readonly sleepMs?: number;
260
324
  }
261
325
 
326
+ declare interface BackfillSpec_2<TsType = unknown> {
327
+ /**
328
+ * `sql`: server-side expression evaluated by `UPDATE ... SET col = <expr>`.
329
+ * Cheapest path — single DB round-trip, transactional, scales to
330
+ * millions of rows.
331
+ * `js`: per-row JS function. Pulled in batches, transformed locally,
332
+ * pushed back. Use when the value can't be expressed in SQL (calls
333
+ * to an embedding model, third-party API, library-only helpers).
334
+ * 10-100× slower than SQL for the same data — the planner warns
335
+ * above ~10k rows.
336
+ */
337
+ readonly kind: 'sql' | 'js';
338
+ readonly sql?: Statement.Fragment;
339
+ readonly fn?: BackfillFunction_2<TsType>;
340
+ readonly batchSize?: number;
341
+ readonly sleepMs?: number;
342
+ }
343
+
262
344
  export declare type BootEnvironment = 'dev' | 'staging' | 'prod';
263
345
 
264
346
  export declare type BootMigrationOutcome = {
@@ -352,6 +434,125 @@ export declare interface BranchRehearsalReport {
352
434
  readonly error?: string;
353
435
  }
354
436
 
437
+ /** What a `refreshChangeCaptureExclusions()` pass changed. */
438
+ declare interface ChangeCaptureExclusionRefresh {
439
+ /** Excluded before and after — the condition is real and still there. */
440
+ readonly stillExcluded: ReadonlyArray<string>;
441
+ /** Excluded before, decodable now — the schema work removed the cause. */
442
+ readonly readmitted: ReadonlyArray<string>;
443
+ /** Decodable before, excluded now — the schema work INTRODUCED the cause. */
444
+ readonly newlyExcluded: ReadonlyArray<string>;
445
+ }
446
+
447
+ /**
448
+ * A row-change event emitted by the data store (in-memory) or the CDC
449
+ * consumer (Postgres logical replication). Drives the reactive engine.
450
+ */
451
+ declare type ChangeEvent = {
452
+ readonly table: string;
453
+ readonly op: 'insert' | 'update' | 'delete';
454
+ readonly old: Row_2 | null;
455
+ readonly new: Row_2 | null;
456
+ /**
457
+ * Set when the TRANSPORT could not carry this change's row images and they
458
+ * had to be RECONSTRUCTED. Absent on every ordinary event, which is the only
459
+ * case where `old`/`new` are the images as they were at commit.
460
+ *
461
+ * Postgres caps a `pg_notify` payload at 8000 bytes, and under
462
+ * `changeStrategy: 'cdc'` the NOTIFY trigger is the SOLE emitter — so a row
463
+ * that does not fit used to arrive with both images null and every tap
464
+ * (search index, analytics mirror, cdc-out, row history) dropped it as a
465
+ * non-event, permanently, with nothing logged. The CDC consumer re-reads the
466
+ * row by primary key before the event reaches anyone; this field says what
467
+ * that recovered:
468
+ *
469
+ * - `'rehydrated'` — insert/update. `new` is the row RE-READ from the
470
+ * database, so it is the row as it is NOW, not necessarily the image the
471
+ * write that fired this event produced (a later write to the same row can
472
+ * already have landed). `old` is null — postgres keeps no pre-image once
473
+ * the transaction is gone.
474
+ * - `'tombstone'` — delete. `old` carries the PRIMARY KEY AND NOTHING ELSE:
475
+ * the row is gone, so the pre-image is unrecoverable and the key is the
476
+ * whole truth. Enough to REMOVE the row downstream; not a snapshot of what
477
+ * was deleted, and anything recording history must not treat it as one.
478
+ * - `'unrecovered'` — both images are null. The change is known to have
479
+ * happened and its content is not available (no key in the payload, the
480
+ * re-read failed, or the row was already gone). Re-read or resync.
481
+ */
482
+ readonly oversized?: 'rehydrated' | 'tombstone' | 'unrecovered';
483
+ /**
484
+ * How the event reached this process. Absent/'inline' = emitted by this
485
+ * process's own write path; 'injected' = delivered over a cross-instance
486
+ * transport (`injectExternalChange` — the broadcast bus or a CDC
487
+ * consumer). A deployment that must act exactly once per change
488
+ * FLEET-WIDE combines this with `DataStore.changeScope`: on a
489
+ * `'local'`-scope store, acting on non-injected events covers each
490
+ * change exactly once (on its writer); on a `'fleet'`-scope store every
491
+ * replica sees the full stream (own writes included, all 'injected'),
492
+ * so the consumer must elect a single worker instead.
493
+ */
494
+ readonly origin?: 'inline' | 'injected';
495
+ /**
496
+ * The trace this write happened under — the SAME id `auditPlugin` records as
497
+ * `AuditEvent.traceId` and `voltro logs --trace` resolves.
498
+ *
499
+ * This is the correlation bridge. Without it `plugin-row-history` knows WHAT
500
+ * changed and the audit sink knows WHO called and whether it was refused, and
501
+ * nothing joins the two. With it, "what did this call touch" is one query.
502
+ *
503
+ * Absent means the write had no request behind it: a seed, a `*.startup.tsx`,
504
+ * a schedule, a workflow step — or an event injected from another replica.
505
+ * For `origin: 'injected'` the honest answer is the ORIGINATING replica's
506
+ * trace or nothing at all; stamping the receiving replica's ambient trace
507
+ * would attribute a remote write to a local request, which is worse than
508
+ * silence.
509
+ */
510
+ readonly traceId?: string;
511
+ /**
512
+ * The acting identity behind the write — the same one `audit()` stamps into
513
+ * `createdBy`/`updatedBy`, so a row's stamp and its change event never
514
+ * disagree. For an API-key subject that is the person, not the credential;
515
+ * which key was used is recoverable from the audit row sharing `traceId`.
516
+ *
517
+ * `null` = a resolved subject with no acting user (anonymous / system).
518
+ * `undefined` = no request context at all. The two are different facts.
519
+ */
520
+ readonly subjectId?: string | null;
521
+ /**
522
+ * The rpc tag of the call that caused this write (`teams.removeSubTeamMember`).
523
+ *
524
+ * `traceId` says WHICH call; this says WHAT CALL IT WAS. The difference
525
+ * matters to anything rendering history for a human: a row diff carries no
526
+ * intent, and the same delete on a join table is a member removal, a cascade,
527
+ * or an expiry depending only on who asked. Absent for a write with no
528
+ * procedure behind it (seed, startup, migration, schedule) — same meaning as
529
+ * an absent `traceId`.
530
+ */
531
+ readonly procedure?: string;
532
+ /**
533
+ * The write was made BY AN AGENT acting as `subjectId`, not by that subject
534
+ * directly. Mirrors `WriteAttribution.via` — see it for why the identity is
535
+ * NOT changed instead. Absent = a direct call, which is a fact, not a gap.
536
+ */
537
+ readonly via?: 'agent';
538
+ };
539
+
540
+ /**
541
+ * A proven interruption in a store's change stream.
542
+ *
543
+ * Deliberately NOT a list of what was missed. A pub/sub transport keeps no log,
544
+ * so the missed changes are gone — the only honest recovery is to re-derive
545
+ * everything downstream, and a consumer that were handed a partial list would be
546
+ * tempted to patch instead. `reason` is for the log line; the instruction is the
547
+ * event itself.
548
+ */
549
+ declare interface ChangeStreamGap {
550
+ /** Human-readable cause — goes into the refresh's log line. */
551
+ readonly reason: string;
552
+ /** Wall-clock ms the stream was known to be down, when it is known. */
553
+ readonly downForMs?: number;
554
+ }
555
+
355
556
  /** Partition into ≤`n`-sized chunks (disjoint, complete). Exported for the
356
557
  * batching regression test — the introspection's correctness reduces to this
357
558
  * producing a complete, non-overlapping partition of the table list. */
@@ -379,6 +580,17 @@ export declare const chunkTables: <T>(xs: ReadonlyArray<T>, n: number) => T[][];
379
580
  */
380
581
  export declare const classifyRollingDeploySafety: (op: MigrationOperation) => RollingDeploySafety;
381
582
 
583
+ /**
584
+ * Every resume-ledger row, gone — for a caller that has just UNDONE the run
585
+ * the rows describe. A ledger that outlives the work it recorded would make
586
+ * the next apply resume a sequence whose effects were reverted. An absent
587
+ * ledger table is nothing to clear.
588
+ */
589
+ export declare const clearResumeLedger: (sql: SqlClient.SqlClient) => Effect.Effect<void, never>;
590
+
591
+ /** A finished deploy — applied, or undone completely — leaves no row. */
592
+ export declare const closeDeploy: (sql: SqlClient.SqlClient, id: string) => Effect.Effect<void, unknown>;
593
+
382
594
  declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType, HasDefault extends boolean = boolean> {
383
595
  readonly type: Type;
384
596
  readonly nullable: boolean;
@@ -801,351 +1013,1423 @@ declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType,
801
1013
  readonly __tsType?: TsType;
802
1014
  }
803
1015
 
804
- /**
805
- * A column as seen by the planner: either declared in code (extracted
806
- * from `ColumnDefinition`) or live in the DB (extracted via introspection).
807
- *
808
- * The shape is intentionally minimal — only the bits that affect diff
809
- * classification land here. Things like `idScheme`, `computed`,
810
- * `defaultFactory` etc. live in the runtime layer and don't survive
811
- * the round-trip to `information_schema`.
812
- */
813
- export declare interface ColumnSnapshot {
814
- readonly name: string;
815
- readonly type: ColumnType;
1016
+ declare interface ColumnDefinition_2<TsType, Type extends ColumnType_2 = ColumnType_2, HasDefault extends boolean = boolean> {
1017
+ readonly type: Type;
816
1018
  readonly nullable: boolean;
817
1019
  readonly unique: boolean;
818
1020
  readonly hasDefault: boolean;
1021
+ /* Excluded from this release type: __hasDefault */
819
1022
  readonly defaultValue?: unknown;
820
1023
  /**
821
- * Type-narrowing acknowledgement from `.narrowedFrom(from, { using })`.
822
- * Only ever set on the DECLARED side; introspection leaves it
823
- * undefined (the live DB has no notion of a prior type). When
824
- * `from` equals the live column type the planner downgrades the
825
- * otherwise-blocked type change to `needs-backfill` and threads
826
- * `using` into the `ALTER COLUMN … TYPE … USING <using>` cast. */
827
- readonly narrowedFrom?: {
828
- readonly from: ColumnType;
829
- readonly using?: string;
830
- };
1024
+ * Application-side default factory. When set, the MutationStore
1025
+ * wrapper evaluates this at insert-time IF the caller omits the
1026
+ * column from the row. Distinct from `defaultValue` (which is a
1027
+ * literal landed in the DDL DEFAULT clause). Functions can do
1028
+ * things DDL can't — `() => crypto.randomUUID()`,
1029
+ * `() => process.env.REGION`. NOT emitted in DDL.
1030
+ */
1031
+ readonly defaultFactory?: () => unknown;
831
1032
  /**
832
- * Bounded text length from `text().maxLength(n)` — carried on BOTH sides.
833
- *
834
- * DECLARED: from the column definition. LIVE: from introspection, and ONLY when
835
- * the dialect materialises a length. An unbounded text family member
836
- * (`text`/`longtext`/`mediumtext`/`clob`) leaves this `undefined` rather than
837
- * reporting the type's theoretical maximum — mysql answers 4294967295 for
838
- * `longtext`, and comparing that against a declared `undefined` would re-emit an
839
- * ALTER on every boot forever.
1033
+ * Application-side computed value. Re-evaluated on INSERT and on
1034
+ * UPDATE from the rest of the row (after merging defaults on insert;
1035
+ * after merging the patch over the existing row on update). The
1036
+ * return value overwrites whatever the caller passed for this column.
1037
+ * NOT emitted in DDL.
840
1038
  *
841
- * WHY IT IS HERE AT ALL. It was absent from the snapshot entirely: the value
842
- * lived on the column definition, was read only when rendering CREATE DDL, and
843
- * never reached the comparison. So the differ was not comparing lengths wrongly
844
- * — it could not see them. Adding `.maxLength(n)` to an EXISTING column planned
845
- * zero operations and reported "schema is up to date" while the live column
846
- * stayed `longtext`. A deployment hit that trying to apply the documented remedy
847
- * for MariaDB's hash long-unique, and their contrast is what pinned it: a column
848
- * bounded AT CREATION was `varchar(64)` (DDL path, fine), one bounded afterwards
849
- * stayed `longtext` (diff path, blind).
1039
+ * Example: `slug: text().computed(row => slugify(row.title))`.
850
1040
  *
851
- * NOT compared on sqlite: it has no length-enforced type, so the declared value
852
- * is real and the live side can never report one. Comparing them there would
853
- * re-emit forever. See `sameColumnShape`.
1041
+ * If the computed function reads a column the row doesn't carry,
1042
+ * it gets `undefined` — handle that in the function.
854
1043
  */
855
- readonly maxLength?: number;
856
- readonly references?: {
857
- readonly table: string;
858
- readonly column: string;
859
- readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
860
- readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
861
- /**
862
- * Orphan-handling policy for adding this FK to a POPULATED column
863
- * (`reference(() => t, { orphanPolicy })`). Only ever set on the
864
- * DECLARED side; introspection leaves it undefined (the live DB has
865
- * no notion of it). `normRef` strips it before comparison so it never
866
- * causes FK churn — the planner reads it directly to classify +
867
- * drive the applier's orphan pre-step. See the references-diff in
868
- * `classifyColumnChange`.
869
- */
870
- readonly orphanPolicy?: 'fail' | 'null' | 'delete';
871
- };
872
- /** Physical representation for `reference()`; defaults to `text`. */
873
- readonly referenceStorage?: 'text' | 'numeric';
874
- /** `oneOf(['a','b'])` → CHECK constraint values. */
875
- readonly oneOf?: ReadonlyArray<string>;
876
- /** Required/observed JSON validity on SQL Server. Unverified means no
877
- * recognized enabled and trusted check proves the existing data valid. */
878
- readonly jsonValidation?: 'value' | 'container' | 'unverified';
879
- /** Live SQL Server JSON checks recognized by complete predicate shape.
880
- * Names are catalog identities for DDL, not portable schema fingerprints. */
881
- readonly jsonChecks?: ReadonlyArray<{
882
- readonly name: string;
883
- readonly validation: 'value' | 'container';
884
- readonly enabled: boolean;
885
- readonly trusted: boolean;
886
- }>;
1044
+ readonly computed?: (row: Readonly<Record<string, unknown>>) => unknown;
1045
+ readonly onUpdate?: 'now' | (() => unknown);
1046
+ readonly references?: () => TableLike_2;
887
1047
  /**
888
- * Fixed-point precision + scale for a `decimal(p,s)` / `numeric(p,s)`
889
- * column (`type === 'decimal'`). Drives the emitted `NUMERIC(p,s)` /
890
- * `DECIMAL(p,s)` DDL. Set on the DECLARED side; introspection leaves them
891
- * undefined (the planner compares the `decimal` type structurally, not the
892
- * exact precision — an ALTER of precision alone is not auto-classified, same
893
- * posture as `generatedAs`). See `snapshotPgType` / `snapshotMysqlType`.
1048
+ * Physical/TypeScript representation of a foreign key. The default is
1049
+ * `text`, matching Voltro's default TypeID primary key. `numeric` is for a
1050
+ * target declared with `id({ scheme: 'numeric' })`; it renders as the
1051
+ * dialect's 64-bit integer type and decodes as a JavaScript number.
894
1052
  */
895
- readonly precision?: number;
896
- readonly scale?: number;
1053
+ readonly referenceStorage?: 'text' | 'numeric';
1054
+ /** Target key; defaults to the framework convention `id`. */
1055
+ readonly referenceColumn?: string;
897
1056
  /**
898
- * Id-generation scheme for `type: 'id'` columns. Mirrors
899
- * `ColumnDefinition.idScheme.kind` so the applier can tell a numeric
900
- * auto-increment PK (`kind: 'numeric'`) from a typeid/string PK and
901
- * emit the dialect-correct auto-increment DDL (BIGSERIAL / BIGINT
902
- * AUTO_INCREMENT / IDENTITY / INTEGER AUTOINCREMENT). Only ever set on
903
- * the DECLARED side; introspection leaves it undefined (the live DB
904
- * doesn't report a scheme — a numeric PK is detectable there from the
905
- * integer type + auto_increment, which the diff doesn't need).
1057
+ * Cascade-on-delete behavior for a `reference()` column. Default
1058
+ * `'restrict'` — Postgres / MySQL / MariaDB / MSSQL / SQLite all block
1059
+ * the parent delete when child rows still point at it. Pre-release
1060
+ * choice: `'restrict'` is safer than `'cascade'` (deleting a user
1061
+ * shouldn't silently nuke 100k posts) and the explicit `'cascade'`
1062
+ * opt-in is one keystroke when the parent-child relationship genuinely
1063
+ * is owned-lifecycle.
906
1064
  */
907
- readonly idScheme?: {
908
- readonly kind: string;
909
- };
1065
+ readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
910
1066
  /**
911
- * Verbatim DDL fragment for a `raw(ddl)` column (`type === 'raw'`).
912
- * Emitted unchanged as the column's SQL type on every dialect — the
913
- * applier and the CREATE-TABLE emitter both defer to it. Only ever set
914
- * on the declared side; the live DB reports a concrete type, not the
915
- * original fragment, so introspection leaves it undefined.
1067
+ * Cascade-on-update — default `'noAction'`. Rarely used in practice.
1068
+ * Named `refOnUpdate` (not `onUpdate`) to avoid colliding with the
1069
+ * existing column-level `onUpdate('now' | …)` trigger field which
1070
+ * has nothing to do with FK semantics.
916
1071
  */
917
- readonly rawDdl?: string;
1072
+ readonly refOnUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
918
1073
  /**
919
- * `.generatedAs(expr, { stored })` — a DB-computed column. When set the
920
- * applier emits `GENERATED ALWAYS AS (<expr>) STORED|VIRTUAL` (mssql:
921
- * `AS (<expr>) PERSISTED`) and SUPPRESSES NOT NULL / DEFAULT / inline
922
- * UNIQUE / REFERENCES (the value is computed; the DB rejects those
923
- * alongside a generation clause). Only ever set on the DECLARED side —
924
- * introspection does NOT read generation expressions, so the live snapshot
925
- * leaves it undefined. The planner's `sameColumnShape` deliberately does
926
- * NOT compare this field, so a declared-generated column never diffs
927
- * against its introspected-as-plain live form (no churn). Consequence:
928
- * the declarative engine creates generated columns correctly but does NOT
929
- * detect a CHANGE to the expression after the fact — recreate the table
930
- * (drop + re-migrate) to alter a generation expression.
1074
+ * What the RUNTIME does to this column's rows when the referenced row is
1075
+ * SOFT-deleted (`deletedAt` set) — set by `reference(…, { onSoftDelete })`.
1076
+ * A foreign key never sees a soft delete (it is an UPDATE), so `onDelete`
1077
+ * cannot express it; the store's post-commit change channel enforces this
1078
+ * one instead, the way `pluginRef` orphan rules are enforced.
931
1079
  */
932
- readonly generatedAs?: {
933
- readonly expr: string;
934
- readonly stored: boolean;
935
- };
1080
+ readonly refOnSoftDelete?: 'cascade' | 'setNull';
936
1081
  /**
937
- * Data-sensitivity classification from `.sensitive(class)` (or implied by
938
- * `.encrypted()`). DECLARED-side only — introspection never reports it. Read
939
- * by the export masker to pseudonymise/anonymise the column when copying to a
940
- * lower environment; the planner ignores it (no DDL, no churn). See `safe`.
1082
+ * Auto-create a B-tree index on this FK column. Default `true` — FK
1083
+ * columns are nearly always read-by, and the lookup cost of a missing
1084
+ * index dwarfs the write cost of an unnecessary one. Opt-out
1085
+ * (`{ index: false }`) for the rare table where the parent-child set
1086
+ * is small enough that a full-scan beats the index-maintenance
1087
+ * overhead.
941
1088
  */
942
- readonly sensitive?: {
943
- readonly class: string;
944
- };
1089
+ readonly fkAutoIndex?: boolean;
945
1090
  /**
946
- * Reviewed-safe marker from `.safe()`. DECLARED-side only. Lets the export
947
- * masker be fail-closed: a column that is neither `sensitive` nor `safe`
948
- * blocks a masking export until classified.
1091
+ * This id-shaped scalar names something outside the local relational graph.
1092
+ * Set by `.externalId()` so schema audits do not guess a local FK from the
1093
+ * column name. Metadata only; it emits no constraint or DDL.
949
1094
  */
950
- readonly safe?: boolean;
1095
+ readonly externalId?: boolean;
1096
+ readonly jsonType?: unknown;
951
1097
  /**
952
- * Type PARAMETERS for the three `ColumnType`s whose DDL is not determined by
953
- * the type tag alone — `vector(n)`, `array(of)`, `enum(name, values)`.
954
- * DECLARED-side only (introspection reports a concrete SQL type, not the
955
- * declaration that produced it).
1098
+ * Closed value set, set by `text().oneOf([...])`. When present:
956
1099
  *
957
- * These exist because the applier renders its DDL from a ColumnSnapshot, not
958
- * from the `ColumnDefinition` that `migrate.ts`'s canonical `sqlType` reads.
959
- * Without them the snapshot renderers had nothing to render and collapsed all
960
- * three to `text` — which is not a smaller mistake than it looks. A declared
961
- * `vector(1536)` against a live `text` column planned an `alter-column-type`
962
- * that emitted `ALTER COLUMN … TYPE text`: valid SQL, applied successfully,
963
- * changed nothing. The plan re-emitted it on every boot, the applier reported
964
- * success every time, and the schema never converged. (Reported from a live
965
- * pod: "applied 31 op(s)" with none of the 31 present in the database.)
1100
+ * - The row type narrows from `string` to the union of literals.
1101
+ * - The Postgres migration emitter adds a `CHECK (col IN (...))`
1102
+ * constraint so a bad value is rejected by the DB regardless
1103
+ * of which client wrote it.
1104
+ * - The in-memory data store can validate at insert time
1105
+ * (helps catch test-data drift early).
966
1106
  *
967
- * The rule for anything added later: if the applier has to RENDER it, the
968
- * snapshot has to CARRY it — a snapshot renderer must never invent a type it
969
- * wasn't given.
1107
+ * For Postgres-native ENUM types: not used here. We treat the
1108
+ * underlying column as `text` and lean on CHECK — that keeps adding /
1109
+ * removing values a one-statement migration instead of the
1110
+ * three-statement dance Postgres ENUMs need (`ALTER TYPE … ADD VALUE`
1111
+ * + rewrite, or `ALTER TYPE … RENAME VALUE`).
970
1112
  */
971
- readonly vectorDim?: number;
972
- readonly vectorPrecision?: 'float32' | 'half';
973
- readonly arrayElement?: ColumnType;
974
- readonly enumName?: string;
975
- readonly enumValues?: ReadonlyArray<string>;
1113
+ readonly oneOf?: ReadonlyArray<string>;
976
1114
  /**
977
- * PostGIS `geography(kind, srid)` / `geometry(kind, srid)` parameters. Same
978
- * reason as the three above: a spatial column declares `type: 'text'`, so
979
- * WITHOUT this the applier's `add-column` created a plain `text` column and
980
- * introspection (`USER-DEFINED` → `text`) agreed with it — no churn, no
981
- * error, and no spatial column. DECLARED-side only.
1115
+ * Bounded text length, set by `text().maxLength(n)`. When present the
1116
+ * migrator emits `VARCHAR(n)` (postgres / mysql / mariadb) / `NVARCHAR(n)`
1117
+ * (mssql) instead of the unbounded `TEXT` / `LONGTEXT` default. The point
1118
+ * is index-ability: a bounded column can back a plain BTREE UNIQUE or
1119
+ * composite index, where an unbounded text column can only take a HASH
1120
+ * long-unique constraint or a prefix index. `n` is a character count.
982
1121
  */
983
- readonly spatial?: {
984
- readonly kind: 'geography' | 'geometry';
985
- readonly geomKind: string;
986
- readonly srid: number;
987
- };
988
- }
989
-
990
- declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
991
-
992
- /**
993
- * Build a `SchemaSnapshot` from an array of `TableLike` declarations.
994
- * Caller is responsible for passing the FULL set of tables (user-
995
- * declared + framework). The fingerprint over this snapshot is what the
996
- * boot-time prod check compares against `_voltro_migration_plans`.
997
- */
998
- export declare const declaredSnapshot: (tables: ReadonlyArray<TableLike>, dialect?: DialectId) => SchemaSnapshot;
999
-
1000
- /** The NOTIFY channel a schema emits on when none is configured. */
1001
- export declare const DEFAULT_CDC_CHANNEL = "framework_changes";
1002
-
1003
- /**
1004
- * `DEFAULT <expr>` for an array-valued default.
1005
- *
1006
- * Postgres is the only dialect with native arrays, so a `array()` column there
1007
- * takes an array literal (`'{a,b}'::text[]`); every other dialect stores the
1008
- * value as JSON/TEXT and takes the json form. A default on a `json()` column
1009
- * always takes the json form — the array is the VALUE, not the storage.
1010
- */
1011
- export declare const defaultArrayClause: (value: ReadonlyArray<unknown>, column: ColumnDefinition<unknown>, dialect: DialectId) => string;
1012
-
1013
- export declare const defaultClause: (column: ColumnDefinition<unknown>, dialect: DialectId) => string | null;
1014
-
1015
- /**
1016
- * `DEFAULT <expr>` for a plain-object json default, per dialect's json-column
1017
- * type. Single source for the auto-migrate (`defaultClause`) AND declarative-
1018
- * applier paths so the two can't drift (the introspected form must round-trip
1019
- * against `normalizeDefault`, so the emit shape matters on both sides).
1020
- */
1021
- export declare const defaultJsonClause: (value: object, dialect: DialectId) => string;
1022
-
1023
- export declare const describeOutcome: (outcome: BootMigrationOutcome, dialectId: string) => string;
1024
-
1025
- /**
1026
- * Parse `VOLTRO_DESTRUCTIVE_OK`.
1027
- *
1028
- * `1` / `true` acknowledges every lossy op in the run. A comma-separated list
1029
- * acknowledges only those TABLES — which is the form somebody wants far more
1030
- * often, and its absence was a real complaint: a plan carrying one intended drop
1031
- * plus three other lossy ops had no way to say yes to the one. `1` said yes to
1032
- * all four, and the alternative was a hand-written DROP migration, which the
1033
- * framework's own upgrade note warns against.
1034
- *
1035
- * `undefined` = not set at all.
1036
- */
1037
- export declare const destructiveScope: (raw: string | undefined) => {
1038
- readonly kind: "all";
1039
- } | {
1040
- readonly kind: "tables";
1041
- readonly tables: ReadonlySet<string>;
1042
- } | undefined;
1043
-
1044
- /**
1045
- * Compare declared reactivity against the triggers actually installed.
1046
- *
1047
- * Postgres only — it is the one dialect where reactivity is carried by DDL. The
1048
- * binlog and Change Tracking readers are configured at runtime from the same
1049
- * in-memory table list the schema produced, so they cannot drift from it.
1050
- */
1051
- export declare const detectReactiveTriggerDrift: (sql: SqlClient.SqlClient, tables: ReadonlyArray<{
1052
- readonly tableName: string;
1053
- readonly isReactive?: boolean;
1054
- }>,
1055
- /** The app's `cdcChannel`. Part of the trigger NAME on any non-default
1056
- * channel, so omitting it here reported every table as missing on a database
1057
- * where every trigger was present. */
1058
- channel?: string) => Effect.Effect<ReactiveTriggerDrift, SqlError_2>;
1059
-
1060
- /** Dialect IDs the framework supports. */
1061
- declare type DialectId = 'postgres' | 'mysql' | 'mariadb' | 'mssql' | 'sqlite' | 'turso';
1062
-
1063
- /**
1064
- * Output of `voltro db drift`: compares the live DB's introspected
1065
- * schema against the last applied fingerprint in `_voltro_migrations`.
1066
- *
1067
- * `isDrifted: false` is the happy path. A drift report names the
1068
- * concrete divergences so the operator can either reconcile in code or
1069
- * via a corrective migration.
1070
- */
1071
- export declare interface DriftReport {
1072
- readonly isDrifted: boolean;
1073
- readonly lastAppliedFingerprint?: string;
1074
- readonly liveFingerprint: string;
1075
- readonly divergences: ReadonlyArray<{
1076
- readonly kind: 'unexpected' | 'missing';
1077
- readonly description: string;
1078
- }>;
1079
- }
1080
-
1081
- /**
1082
- * Per-branch refinement passed to `.with({ relation: { ... } })`.
1083
- *
1084
- * `true` is the zero-config shorthand — equivalent to `{}`. Otherwise
1085
- * each branch can carry its own `where` / `orderBy` / `limit` /
1086
- * `offset` / nested `with`, so `users.with({ posts: { limit: 10,
1087
- * orderBy: 'createdAt' } })` works without dropping into a sub-query
1088
- * builder.
1089
- *
1090
- * Untyped at runtime: the QueryDescriptor is the boundary between the
1091
- * typed builder API and the dialect-agnostic JoinCompiler. End-user
1092
- * types live one layer up in the typed `.with()` method on `Query`.
1093
- */
1094
- declare interface EagerLoadSpec {
1095
- readonly where?: Predicate;
1096
- readonly orderBy?: ReadonlyArray<OrderClause>;
1122
+ readonly maxLength?: number;
1097
1123
  /**
1098
- * Per-parent cap on the relation. `{ limit: 10 }` returns up to 10
1099
- * rows FOR EACH parent — same semantics as Drizzle / Prisma /
1100
- * Hibernate. This matches what users actually mean when they write
1101
- * "the 10 latest posts" inside `users.with({ posts: { limit: 10 } })`.
1102
- *
1103
- * The SQL stores express this natively as a `LIMIT` clause in a
1104
- * correlated subquery (postgres / sqlite / mysql / mariadb) or
1105
- * via `TOP N` inside `FOR JSON PATH` (mssql) when the JSON-agg path
1106
- * is active. The walker fallback post-buckets the batch fetch and
1107
- * slices each bucket.
1124
+ * Raw SQL `CHECK` expressions, set by `column.check(expr)` (accumulates
1125
+ * — call it more than once for multiple checks). Each emits an inline
1126
+ * `CHECK (<expr>)` on the column. DB-enforced regardless of client,
1127
+ * unlike app-level `table().validate(Schema)`. The developer owns the
1128
+ * expr's cross-dialect portability. Distinct from `oneOf` (which emits
1129
+ * its own `CHECK (col IN (...))`).
1108
1130
  */
1109
- readonly limit?: number;
1110
- /** Per-parent offset. Only meaningful with `limit`. */
1111
- readonly offset?: number;
1112
- readonly with?: WithSpec;
1131
+ readonly checks?: ReadonlyArray<string>;
1113
1132
  /**
1114
- * Project the JUNCTION row's own columns into a `manyToMany` eager result.
1115
- *
1116
- * `true` takes every junction column; an array takes only those named. The
1117
- * values land under `_junction` on each target row:
1133
+ * DB-level generated column. Set by `.generatedAs(expr, { stored })`.
1118
1134
  *
1119
- * projects.with({ teams: { junction: ['role', 'addedAt'] } })
1120
- * // → team._junction.addedAt
1135
+ * Distinct from `.computed(row => ...)` (application-time, eval'd
1136
+ * at insert) — generated columns are computed by the DB engine
1137
+ * itself. Visible to raw SQL, kept consistent on UPDATE without
1138
+ * re-stamping, can be indexed (combine with `.expressionIndex`).
1121
1139
  *
1122
- * Nested rather than merged onto the target row, deliberately: a junction and
1123
- * its target routinely share column names (`createdAt` is the obvious one),
1124
- * and merging would silently overwrite real target data with membership data.
1125
- * A name collision that corrupts a row is worse than one extra level.
1140
+ * `stored: true` → `GENERATED ALWAYS AS (expr) STORED` (Postgres/
1141
+ * MySQL/MariaDB/SQLite; PERSISTED on MSSQL).
1142
+ * Disk-resident, indexable.
1143
+ * `stored: false` → VIRTUAL on MySQL/MariaDB/SQLite. Recomputed on
1144
+ * read, no disk cost, NOT indexable. Postgres
1145
+ * has no VIRTUAL — falls back to STORED with a
1146
+ * boot warning.
1126
1147
  *
1127
- * Why this exists: junction tables carrying meaningful columns — a role, a
1128
- * joined-at stamp, a permission tier — are the rule rather than the
1129
- * exception, and without this an eager load could FILTER on them
1130
- * (`onJunction`) but never return them, so any relation with real membership
1131
- * data had to stay a hand-written join. The junction rows were already being
1132
- * fetched in full to resolve the target ids; this stops throwing them away.
1148
+ * The expression is emitted verbatim (caller-quoted). MUST
1149
+ * reference only OTHER columns of the same row — there's no way
1150
+ * to reference the subject/tenant context from a generated column.
1151
+ * Use `.computed(row => ...)` for context-aware values.
1133
1152
  */
1134
- readonly junction?: true | ReadonlyArray<string>;
1153
+ readonly generatedAs?: {
1154
+ readonly expr: string;
1155
+ readonly stored: boolean;
1156
+ };
1135
1157
  /**
1136
- * Filter a `manyToMany` eager branch on the THROUGH-table's columns.
1137
- * The predicate is evaluated against the junction row, not the target
1138
- * row — e.g. for `users.with({ organizations: { onJunction: eq('role',
1139
- * 'admin') } })` it keeps only the memberships where `role = 'admin'`,
1140
- * so the loaded `organizations` are exactly the orgs the user is an
1141
- * admin of.
1158
+ * Postgres-native ENUM type binding, populated when the column is
1159
+ * built from `dbEnum(...).column()`. Distinct from `oneOf`:
1142
1160
  *
1143
- * Compiles into the correlated subquery's WHERE alongside the
1144
- * source-key correlation, qualified to the junction-table alias.
1145
- * Ignored on `one` / `many` branches (no junction table exists).
1146
- */
1147
- readonly onJunction?: Predicate;
1148
- }
1161
+ * - On Postgres, this column's SQL type is the user-defined ENUM
1162
+ * (`status order_status` not `status text CHECK (...)`).
1163
+ * - On MySQL / MariaDB, emits a native `ENUM('a','b','c')` column.
1164
+ * - On MSSQL / SQLite, falls back to TEXT + CHECK (same shape as
1165
+ * `oneOf` — distinct constraint name so the diff can tell them
1166
+ * apart).
1167
+ *
1168
+ * Migration ops `add-enum-value` / `rename-enum-value` apply natively
1169
+ * on Postgres (cheap `ALTER TYPE … ADD VALUE`); MySQL needs a column
1170
+ * rewrite (`MODIFY col ENUM(...)`); MSSQL / SQLite recreate the CHECK.
1171
+ */
1172
+ readonly enumName?: string;
1173
+ readonly enumValues?: ReadonlyArray<string>;
1174
+ /**
1175
+ * Element type for `array(...)` columns. Populated only when
1176
+ * `type === 'array'`. The framework emits the DB-native array
1177
+ * type on postgres (`text[]`, `integer[]`, etc.) and falls back
1178
+ * to a JSON column on other dialects.
1179
+ */
1180
+ readonly arrayElement?: ColumnType_2;
1181
+ /**
1182
+ * Field-level encryption flag, set by `.encrypted()`. When `true`
1183
+ * the runtime's store middleware encrypts the value on write and
1184
+ * decrypts on read via the registered field cipher (AES-256-GCM, keyed
1185
+ * from `VOLTRO_FIELD_ENCRYPTION_KEY` through the Secrets-Resolver — the
1186
+ * framework registers it on both boot paths; `voltro dev` mints the key).
1187
+ * The stored column is an opaque `enc:v2:…` string — bound by GCM's
1188
+ * additional authenticated data to its table, column and row
1189
+ * (`FieldBinding`), so a ciphertext copied into another row or column
1190
+ * does not decrypt there — and the DB type stays `text`. A row written
1191
+ * while encryption is active can't be
1192
+ * read back without the key — losing the key loses the data, by
1193
+ * design. Boot refuses if an `.encrypted()` column is declared but
1194
+ * no cipher is registered.
1195
+ */
1196
+ readonly encrypted?: boolean;
1197
+ /**
1198
+ * Wire-exposure flag, set by `.serverOnly()`. When `true` the column is
1199
+ * SERVER-INTERNAL: server code reads it normally (an auth middleware verifying
1200
+ * a `keyHash`, say), but it must NEVER be serialized to a client. This is the
1201
+ * EXPOSURE axis, orthogonal to `.encrypted()` (which is storage-at-rest): a
1202
+ * column can be either, both, or neither — a hash you never ship is serverOnly
1203
+ * but not encrypted; a private note the owner may read is encrypted but not
1204
+ * serverOnly. The default is exposed to BOTH server and client; `.serverOnly()`
1205
+ * opts a column OUT of the client. The table-aware read paths (`crud.*`) strip
1206
+ * it automatically, and an audit flags a wire-reachable query whose `source`
1207
+ * table carries a serverOnly column that its output declares. What that audit
1208
+ * COSTS is per command — see `serverOnly()` below for the matrix.
1209
+ */
1210
+ readonly serverOnly?: boolean;
1211
+ /**
1212
+ * Field-level read scopes, set by `.readableBy(...scopes)`. A NON-EMPTY list
1213
+ * of scope strings; the column is stripped from wire OUTPUT for any subject
1214
+ * that holds NONE of them, and present for a subject holding at least ONE.
1215
+ * This is the graded middle of the exposure axis: `.serverOnly()` hides a
1216
+ * column from EVERYONE on the wire, a plain column is visible to everyone,
1217
+ * and `.readableBy('billing:read')` is visible only to callers scoped for it.
1218
+ *
1219
+ * The scope strings are the SAME vocabulary as `guards:` / `ctx.access` — they
1220
+ * are checked against the subject's EFFECTIVE scope set (raw subject scopes ∪
1221
+ * any rbac role-derived scopes), and the `admin:full` bypass sees every
1222
+ * `.readableBy()` column, exactly as it satisfies every guard. Empty here is a
1223
+ * declaration error (`.readableBy()` with no scope would mean "nobody" — use
1224
+ * `.serverOnly()` for that), so the field is never present as `[]`.
1225
+ *
1226
+ * Orthogonal to `.encrypted()` (storage-at-rest) and to `.sensitive()` /
1227
+ * `.safe()` (export-time masking): this is purely the WIRE-exposure axis.
1228
+ */
1229
+ readonly readableBy?: ReadonlyArray<string>;
1230
+ /**
1231
+ * CRDT-managed marker, set by the `crdtText()` column constructor. When `true`
1232
+ * the column holds an ENCODED CRDT state (opaque bytes — a Yjs update blob
1233
+ * behind `@voltro/local-first`'s swappable backend) rather than a plain value.
1234
+ *
1235
+ * It changes NOTHING about the column's DDL or how the declarative differ
1236
+ * treats it: the storage type is `bytes` (BYTEA / BLOB / LONGBLOB /
1237
+ * VARBINARY), a normal storable type, and the planner compares it structurally
1238
+ * like any other `bytes` column — introspection never reports this flag, and
1239
+ * `sameColumnShape` never reads it, so a `crdtText()` column round-trips
1240
+ * through a plan with zero churn (no phantom re-plan).
1241
+ *
1242
+ * What it DOES drive lives above the DDL, in the runtime write path: the
1243
+ * MutationStore reads it (via the SchemaRegistry) to run the AUTHORITATIVE
1244
+ * server-side merge on write — an incoming encoded update is folded into the
1245
+ * stored state with `mergeCrdtStates` before the row is written, so two
1246
+ * concurrent clients converge. Orthogonal to every other flag here; a CRDT
1247
+ * column is just `bytes` with server-merge semantics.
1248
+ */
1249
+ readonly crdtManaged?: boolean;
1250
+ /**
1251
+ * Optimistic-concurrency marker, set by `.version()`.
1252
+ *
1253
+ * The column the store INCREMENTS on every update, and the one an update may
1254
+ * carry an expectation of. Two clients that read the same row and both write
1255
+ * it are the normal case, not an exotic one — and without this the second
1256
+ * write silently wins, which is the shape of every "my change disappeared"
1257
+ * report ever filed.
1258
+ *
1259
+ * A TIMESTAMP cannot do this job. Two writes in the same millisecond are
1260
+ * indistinguishable, and across replicas the clocks disagree; this repo has
1261
+ * already lost rows to exactly that (an analytics sink dropped 7 of 40 events
1262
+ * written in the same millisecond as the query bounding them). A monotonic
1263
+ * integer has neither problem, and it also answers the other half of the
1264
+ * question — which of two copies is NEWER — without consulting a clock.
1265
+ */
1266
+ readonly versionColumn?: boolean;
1267
+ /**
1268
+ * Data-sensitivity classification, set by `.sensitive(class)`. Declares that
1269
+ * this column holds personal / sensitive data of a given CLASS (`'email'`,
1270
+ * `'fullName'`, `'phone'`, `'secret'`, …). It changes nothing at runtime or
1271
+ * in the DDL — it is metadata read by the export layer (`voltro data export
1272
+ * --profile …`) to decide how to pseudonymise / anonymise the column when
1273
+ * copying prod data to a lower environment. The class picks the
1274
+ * format-preserving fake transform. Orthogonal to `.encrypted()` (which
1275
+ * protects at rest): an encrypted column is decrypted on read, so it is
1276
+ * PLAINTEXT at export time and is treated as sensitive automatically. See
1277
+ * `.safe()` for the other half of the fail-closed classification.
1278
+ */
1279
+ readonly sensitive?: {
1280
+ readonly class: string;
1281
+ };
1282
+ /**
1283
+ * Reviewed-safe marker, set by `.safe()`. Declares that this column holds NO
1284
+ * personal / sensitive data and may be copied verbatim to a lower
1285
+ * environment. It exists so masking can be FAIL-CLOSED: under a masking
1286
+ * profile, a column that is neither `.sensitive()` nor `.safe()` blocks the
1287
+ * export until it is consciously classified — so a newly-added column can
1288
+ * never silently leak. Metadata only; no runtime/DDL effect.
1289
+ */
1290
+ readonly safe?: boolean;
1291
+ /**
1292
+ * Fixed-point precision (total significant digits), set by
1293
+ * `decimal(precision, scale)` / `numeric(precision, scale)`. Populated
1294
+ * only when `type === 'decimal'`. Drives the DDL size —
1295
+ * `NUMERIC(p,s)` (postgres) / `DECIMAL(p,s)` (mysql/mariadb/mssql) —
1296
+ * so a money column round-trips at full precision. On sqlite/turso
1297
+ * (no fixed-point type) the column is `NUMERIC` affinity and the value
1298
+ * carries as a TEXT string, so `p`/`s` are informational only there.
1299
+ */
1300
+ readonly precision?: number;
1301
+ /**
1302
+ * Fixed-point scale (digits after the decimal point), set by
1303
+ * `decimal(precision, scale)`. Populated only when `type === 'decimal'`.
1304
+ * MUST be `<= precision`. See {@link ColumnDefinition.precision}.
1305
+ */
1306
+ readonly scale?: number;
1307
+ /**
1308
+ * Vector dimensionality. Populated only when `type === 'vector'`.
1309
+ * The migration emitter uses it to size the column DDL —
1310
+ * `VECTOR(n)` on postgres (pgvector) + MariaDB 11.7+ / MySQL 9,
1311
+ * and a sized fallback (`VARBINARY(MAX)` / `BLOB`) elsewhere. The
1312
+ * HNSW index emitter + the `vectorEmbedding()` mixin also read it.
1313
+ */
1314
+ readonly vectorDim?: number;
1315
+ /**
1316
+ * Vector storage precision. Populated only when `type === 'vector'`.
1317
+ * `'float32'` (default) emits `VECTOR(n)`; `'half'` emits pgvector's
1318
+ * `HALFVEC(n)` (half the bytes, marginally lower recall). MariaDB /
1319
+ * MySQL have no half type — the emitter upcasts to `VECTOR(n)` there.
1320
+ */
1321
+ readonly vectorPrecision?: 'float32' | 'half';
1322
+ /**
1323
+ * Verbatim DDL fragment for a `raw(ddl)` escape-hatch column.
1324
+ * Populated only when `type === 'raw'`. The migration emitter returns
1325
+ * this string unchanged as the column's SQL type on EVERY dialect —
1326
+ * the fragment is the single source of truth for the column's type,
1327
+ * nullability, and default. Because of that, `.nullable()`,
1328
+ * `.default()`, and `.unique()` are rejected at declaration time on a
1329
+ * `raw` column (two sources of truth would drift).
1330
+ */
1331
+ readonly rawDdl?: string;
1332
+ /**
1333
+ * Spatial column metadata (`@voltro/plugin-postgis`'s `geography()` /
1334
+ * `geometry()`). The row TYPE stays `text` (handlers read/write WKT/EWKT
1335
+ * strings); this drives the migration emitter to produce real
1336
+ * `geography(Point,4326)` / `geometry(Polygon,4326)` DDL on postgres +
1337
+ * `CREATE EXTENSION IF NOT EXISTS postgis`. PostGIS is postgres-only, so
1338
+ * the emitter THROWS on any other dialect (the documented fail-loud guard)
1339
+ * rather than silently emitting a broken TEXT column. Generic on purpose —
1340
+ * the postgis plugin is the only producer, but core owns the DDL hook (the
1341
+ * same pattern as `vectorDim` / `enumName`) so the column type is real, not
1342
+ * a fabricated marker nothing consumes.
1343
+ */
1344
+ readonly spatial?: {
1345
+ /** PostGIS base type. */
1346
+ readonly kind: 'geography' | 'geometry';
1347
+ /** Geometry subtype: `Point` / `LineString` / `Polygon` / … */
1348
+ readonly geomKind: string;
1349
+ /** Spatial reference id (e.g. 4326 = WGS-84 lat/lon). */
1350
+ readonly srid: number;
1351
+ };
1352
+ /**
1353
+ * Captured `id()` options. Populated only on the primary-key column
1354
+ * (`type: 'id'`). At column-construction time the `prefix` for typeid
1355
+ * may still be missing — `table()` resolves it against the table name
1356
+ * and the resolved scheme lands on `idScheme` below.
1357
+ */
1358
+ readonly idSchemeInput?: IdSchemeInput_2;
1359
+ /**
1360
+ * Fully-resolved id scheme for the primary-key column. Set by
1361
+ * `table()` after walking the field map; downstream code (DDL emit,
1362
+ * SchemaRegistry, MutationStore auto-inject) reads this exclusively.
1363
+ */
1364
+ readonly idScheme?: IdScheme_2;
1365
+ /**
1366
+ * Migration backfill expression. Populated by `.backfill(sqlOrFn, opts?)`.
1367
+ *
1368
+ * The migration planner reads this when ADD COLUMN + NOT NULL on a
1369
+ * populated table would otherwise be classified `needs-backfill` →
1370
+ * with a backfill declared, the planner emits a three-step plan
1371
+ * (ADD COLUMN nullable → UPDATE via expression → SET NOT NULL) inside
1372
+ * one transaction (or one forward-roll group on mysql/mariadb).
1373
+ *
1374
+ * Has zero effect at insert/query time — pure planner metadata.
1375
+ */
1376
+ readonly backfill?: BackfillSpec_2<TsType>;
1377
+ /**
1378
+ * Rename marker. Populated by `.renamedFrom('oldName')`.
1379
+ *
1380
+ * The migration planner sees `oldName` missing + this column present
1381
+ * + the `renamedFrom` annotation → classifies as RENAME instead of
1382
+ * DROP+ADD (which would lose data). The annotation stays in the code
1383
+ * until `_voltro_migrations` records that the rename was applied in
1384
+ * the env(s) the developer cares about; removing it earlier yields
1385
+ * a clear refuse-to-plan message rather than a silent drop.
1386
+ */
1387
+ readonly renamedFrom?: string;
1388
+ /**
1389
+ * Dropped marker. Set by the standalone `dropped()` helper at the
1390
+ * column's field-map slot. The planner treats the column as
1391
+ * intentionally going away — emits a `lossy` op that's auto-allowed
1392
+ * (no `VOLTRO_DESTRUCTIVE_OK=1` required) because the developer
1393
+ * declared intent explicitly in the schema diff.
1394
+ *
1395
+ * The drop-marker stays for one applied migration cycle, then the
1396
+ * developer removes the field map entry entirely.
1397
+ */
1398
+ readonly dropped?: boolean;
1399
+ /**
1400
+ * Type-narrowing marker. Populated by `.narrowedFrom(from, { using })`.
1401
+ *
1402
+ * The planner uses `from` to know what the live-DB column type was
1403
+ * before this migration, and `using` as the dialect-cast expression
1404
+ * when the conversion isn't implicit (e.g. `text → integer` on
1405
+ * postgres requires `USING col::integer`).
1406
+ */
1407
+ readonly narrowedFrom?: NarrowedFromSpec_2;
1408
+ /**
1409
+ * Extended unique-constraint metadata. Populated by
1410
+ * `.unique({ dedup })`. The legacy boolean `unique` flag above
1411
+ * stays as the canonical "is this column unique?" signal — `uniqueSpec`
1412
+ * carries the planner-only metadata (dedup strategy) so existing
1413
+ * runtime consumers don't need to learn the new shape.
1414
+ */
1415
+ readonly uniqueSpec?: UniqueSpec_2;
1416
+ /**
1417
+ * Orphan-handling policy used when adding a foreign key to a
1418
+ * populated column. Mirrors the `ReferenceOptions.orphanPolicy` API:
1419
+ *
1420
+ * - `'fail'` (default) → bare ADD CONSTRAINT (`needs-backfill`, applies);
1421
+ * the DB rejects it if an existing row is an orphan — exactly
1422
+ * like tightening a column to NOT NULL. NOT blocked: the
1423
+ * planner is pure (no row counts), so it can't tell a clean /
1424
+ * empty table from one with orphans.
1425
+ * - `'null'` → planner emits `UPDATE … SET col = NULL` before adding FK
1426
+ * - `'delete'` → planner emits `DELETE FROM child WHERE …` before adding FK
1427
+ *
1428
+ * Has no runtime semantics — purely planner metadata.
1429
+ */
1430
+ readonly orphanPolicy?: 'fail' | 'null' | 'delete';
1431
+ /**
1432
+ * Phantom field so TypeScript can infer the row type at the column level.
1433
+ * Never read at runtime.
1434
+ */
1435
+ readonly __tsType?: TsType;
1436
+ }
1437
+
1438
+ /** A column as the DSL builds it — `text().nullable()`, `integer().default(0)` — or its definition. */
1439
+ export declare type ColumnInput = {
1440
+ __definition(): ColumnDefinition<unknown>;
1441
+ } | ColumnDefinition<unknown>;
1442
+
1443
+ /**
1444
+ * A column as seen by the planner: either declared in code (extracted
1445
+ * from `ColumnDefinition`) or live in the DB (extracted via introspection).
1446
+ *
1447
+ * The shape is intentionally minimal — only the bits that affect diff
1448
+ * classification land here. Things like `idScheme`, `computed`,
1449
+ * `defaultFactory` etc. live in the runtime layer and don't survive
1450
+ * the round-trip to `information_schema`.
1451
+ */
1452
+ export declare interface ColumnSnapshot {
1453
+ readonly name: string;
1454
+ readonly type: ColumnType;
1455
+ readonly nullable: boolean;
1456
+ readonly unique: boolean;
1457
+ readonly hasDefault: boolean;
1458
+ readonly defaultValue?: unknown;
1459
+ /**
1460
+ * Type-narrowing acknowledgement from `.narrowedFrom(from, { using })`.
1461
+ * Only ever set on the DECLARED side; introspection leaves it
1462
+ * undefined (the live DB has no notion of a prior type). When
1463
+ * `from` equals the live column type the planner downgrades the
1464
+ * otherwise-blocked type change to `needs-backfill` and threads
1465
+ * `using` into the `ALTER COLUMN … TYPE … USING <using>` cast. */
1466
+ readonly narrowedFrom?: {
1467
+ readonly from: ColumnType;
1468
+ readonly using?: string;
1469
+ };
1470
+ /**
1471
+ * Bounded text length from `text().maxLength(n)` — carried on BOTH sides.
1472
+ *
1473
+ * DECLARED: from the column definition. LIVE: from introspection, and ONLY when
1474
+ * the dialect materialises a length. An unbounded text family member
1475
+ * (`text`/`longtext`/`mediumtext`/`clob`) leaves this `undefined` rather than
1476
+ * reporting the type's theoretical maximum — mysql answers 4294967295 for
1477
+ * `longtext`, and comparing that against a declared `undefined` would re-emit an
1478
+ * ALTER on every boot forever.
1479
+ *
1480
+ * WHY IT IS HERE AT ALL. It was absent from the snapshot entirely: the value
1481
+ * lived on the column definition, was read only when rendering CREATE DDL, and
1482
+ * never reached the comparison. So the differ was not comparing lengths wrongly
1483
+ * — it could not see them. Adding `.maxLength(n)` to an EXISTING column planned
1484
+ * zero operations and reported "schema is up to date" while the live column
1485
+ * stayed `longtext`. A deployment hit that trying to apply the documented remedy
1486
+ * for MariaDB's hash long-unique, and their contrast is what pinned it: a column
1487
+ * bounded AT CREATION was `varchar(64)` (DDL path, fine), one bounded afterwards
1488
+ * stayed `longtext` (diff path, blind).
1489
+ *
1490
+ * NOT compared on sqlite: it has no length-enforced type, so the declared value
1491
+ * is real and the live side can never report one. Comparing them there would
1492
+ * re-emit forever. See `sameColumnShape`.
1493
+ */
1494
+ readonly maxLength?: number;
1495
+ readonly references?: {
1496
+ readonly table: string;
1497
+ readonly column: string;
1498
+ readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
1499
+ readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
1500
+ /**
1501
+ * Orphan-handling policy for adding this FK to a POPULATED column
1502
+ * (`reference(() => t, { orphanPolicy })`). Only ever set on the
1503
+ * DECLARED side; introspection leaves it undefined (the live DB has
1504
+ * no notion of it). `normRef` strips it before comparison so it never
1505
+ * causes FK churn — the planner reads it directly to classify +
1506
+ * drive the applier's orphan pre-step. See the references-diff in
1507
+ * `classifyColumnChange`.
1508
+ */
1509
+ readonly orphanPolicy?: 'fail' | 'null' | 'delete';
1510
+ /**
1511
+ * The constraint's catalog name, on the LIVE side only. A reader reports
1512
+ * it (postgres, the mysql family, mssql; sqlite names nothing) so a drop
1513
+ * can name the constraint that is actually there rather than assume the
1514
+ * `<table>_<column>_fkey` convention the framework uses when IT creates
1515
+ * one. `normRef` strips it before comparison and before hashing: a name
1516
+ * is a catalog identity, not schema.
1517
+ */
1518
+ readonly constraintName?: string;
1519
+ };
1520
+ /** Physical representation for `reference()`; defaults to `text`. */
1521
+ readonly referenceStorage?: 'text' | 'numeric';
1522
+ /** `oneOf(['a','b'])` → CHECK constraint values. */
1523
+ readonly oneOf?: ReadonlyArray<string>;
1524
+ /** Required/observed JSON validity on SQL Server. Unverified means no
1525
+ * recognized enabled and trusted check proves the existing data valid. */
1526
+ readonly jsonValidation?: 'value' | 'container' | 'unverified';
1527
+ /** Live SQL Server JSON checks recognized by complete predicate shape.
1528
+ * Names are catalog identities for DDL, not portable schema fingerprints. */
1529
+ readonly jsonChecks?: ReadonlyArray<{
1530
+ readonly name: string;
1531
+ readonly validation: 'value' | 'container';
1532
+ readonly enabled: boolean;
1533
+ readonly trusted: boolean;
1534
+ }>;
1535
+ /**
1536
+ * Fixed-point precision + scale for a `decimal(p,s)` / `numeric(p,s)`
1537
+ * column (`type === 'decimal'`). Drives the emitted `NUMERIC(p,s)` /
1538
+ * `DECIMAL(p,s)` DDL. Set on the DECLARED side; introspection leaves them
1539
+ * undefined (the planner compares the `decimal` type structurally, not the
1540
+ * exact precision — an ALTER of precision alone is not auto-classified, same
1541
+ * posture as `generatedAs`). See `snapshotPgType` / `snapshotMysqlType`.
1542
+ */
1543
+ readonly precision?: number;
1544
+ readonly scale?: number;
1545
+ /**
1546
+ * Id-generation scheme for `type: 'id'` columns. Mirrors
1547
+ * `ColumnDefinition.idScheme.kind` so the applier can tell a numeric
1548
+ * auto-increment PK (`kind: 'numeric'`) from a typeid/string PK and
1549
+ * emit the dialect-correct auto-increment DDL (BIGSERIAL / BIGINT
1550
+ * AUTO_INCREMENT / IDENTITY / INTEGER AUTOINCREMENT). Only ever set on
1551
+ * the DECLARED side; introspection leaves it undefined (the live DB
1552
+ * doesn't report a scheme — a numeric PK is detectable there from the
1553
+ * integer type + auto_increment, which the diff doesn't need).
1554
+ */
1555
+ readonly idScheme?: {
1556
+ readonly kind: string;
1557
+ };
1558
+ /**
1559
+ * What the `id` KIND is made of, on the LIVE side.
1560
+ *
1561
+ * Introspection cannot see a DSL spelling, only a column, and it reports the
1562
+ * `id` kind for ANY sole primary key named `id` — a NAME-based inference. So
1563
+ * the kind alone erases the one distinction that matters when the two sides
1564
+ * disagree: a text key and a sequence are both reported as `id`, and a
1565
+ * declaration that changed from one to the other compared EQUAL. That
1566
+ * approximation predates the `text()` spelling and got wider with it.
1567
+ *
1568
+ * This carries the storage the kind was inferred over, derived from the same
1569
+ * raw type the introspector already read. Set by all four dialect readers
1570
+ * wherever they apply the inference, and derived for the DECLARED side from
1571
+ * `idScheme` — so the comparison pairs like with like instead of pairing on
1572
+ * the name.
1573
+ */
1574
+ readonly idStorage?: 'text' | 'numeric';
1575
+ /**
1576
+ * What this column's declared KIND is stored AS, when the two differ and no
1577
+ * reader can report the kind back.
1578
+ *
1579
+ * An `array()` is `<element>[]` on postgres and JSON / NVARCHAR(MAX) / TEXT
1580
+ * elsewhere, and every reader answers with the STORAGE — so the declared
1581
+ * kind and the live one never matched and the table hashed apart from its own
1582
+ * database forever. The planner already collapses this per dialect; the
1583
+ * fingerprint has no dialect, so the DECLARED side states the answer here at
1584
+ * snapshot time (`declaredSnapshot` takes the dialect) and the hash reads it.
1585
+ * Set on the declared side only; a live snapshot needs none, because its type
1586
+ * IS the storage.
1587
+ */
1588
+ readonly storageType?: ColumnType;
1589
+ /**
1590
+ * Verbatim DDL fragment for a `raw(ddl)` column (`type === 'raw'`).
1591
+ * Emitted unchanged as the column's SQL type on every dialect — the
1592
+ * applier and the CREATE-TABLE emitter both defer to it. Only ever set
1593
+ * on the declared side; the live DB reports a concrete type, not the
1594
+ * original fragment, so introspection leaves it undefined.
1595
+ */
1596
+ readonly rawDdl?: string;
1597
+ /**
1598
+ * `.generatedAs(expr, { stored })` — a DB-computed column. When set the
1599
+ * applier emits `GENERATED ALWAYS AS (<expr>) STORED|VIRTUAL` (mssql:
1600
+ * `AS (<expr>) PERSISTED`) and SUPPRESSES NOT NULL / DEFAULT / inline
1601
+ * UNIQUE / REFERENCES (the value is computed; the DB rejects those
1602
+ * alongside a generation clause). Only ever set on the DECLARED side —
1603
+ * introspection does NOT read generation expressions, so the live snapshot
1604
+ * leaves it undefined. The planner's `sameColumnShape` deliberately does
1605
+ * NOT compare this field, so a declared-generated column never diffs
1606
+ * against its introspected-as-plain live form (no churn). Consequence:
1607
+ * the declarative engine creates generated columns correctly but does NOT
1608
+ * detect a CHANGE to the expression after the fact — recreate the table
1609
+ * (drop + re-migrate) to alter a generation expression.
1610
+ */
1611
+ readonly generatedAs?: {
1612
+ readonly expr: string;
1613
+ readonly stored: boolean;
1614
+ };
1615
+ /**
1616
+ * Data-sensitivity classification from `.sensitive(class)` (or implied by
1617
+ * `.encrypted()`). DECLARED-side only — introspection never reports it. Read
1618
+ * by the export masker to pseudonymise/anonymise the column when copying to a
1619
+ * lower environment; the planner ignores it (no DDL, no churn). See `safe`.
1620
+ */
1621
+ readonly sensitive?: {
1622
+ readonly class: string;
1623
+ };
1624
+ /**
1625
+ * Reviewed-safe marker from `.safe()`. DECLARED-side only. Lets the export
1626
+ * masker be fail-closed: a column that is neither `sensitive` nor `safe`
1627
+ * blocks a masking export until classified.
1628
+ */
1629
+ readonly safe?: boolean;
1630
+ /**
1631
+ * Type PARAMETERS for the three `ColumnType`s whose DDL is not determined by
1632
+ * the type tag alone — `vector(n)`, `array(of)`, `enum(name, values)`.
1633
+ * DECLARED-side only (introspection reports a concrete SQL type, not the
1634
+ * declaration that produced it).
1635
+ *
1636
+ * These exist because the applier renders its DDL from a ColumnSnapshot, not
1637
+ * from the `ColumnDefinition` that `migrate.ts`'s canonical `sqlType` reads.
1638
+ * Without them the snapshot renderers had nothing to render and collapsed all
1639
+ * three to `text` — which is not a smaller mistake than it looks. A declared
1640
+ * `vector(1536)` against a live `text` column planned an `alter-column-type`
1641
+ * that emitted `ALTER COLUMN … TYPE text`: valid SQL, applied successfully,
1642
+ * changed nothing. The plan re-emitted it on every boot, the applier reported
1643
+ * success every time, and the schema never converged. (Reported from a live
1644
+ * pod: "applied 31 op(s)" with none of the 31 present in the database.)
1645
+ *
1646
+ * The rule for anything added later: if the applier has to RENDER it, the
1647
+ * snapshot has to CARRY it — a snapshot renderer must never invent a type it
1648
+ * wasn't given.
1649
+ */
1650
+ readonly vectorDim?: number;
1651
+ readonly vectorPrecision?: 'float32' | 'half';
1652
+ readonly arrayElement?: ColumnType;
1653
+ readonly enumName?: string;
1654
+ readonly enumValues?: ReadonlyArray<string>;
1655
+ /**
1656
+ * PostGIS `geography(kind, srid)` / `geometry(kind, srid)` parameters. Same
1657
+ * reason as the three above: a spatial column declares `type: 'text'`, so
1658
+ * WITHOUT this the applier's `add-column` created a plain `text` column and
1659
+ * introspection (`USER-DEFINED` → `text`) agreed with it — no churn, no
1660
+ * error, and no spatial column. DECLARED-side only.
1661
+ */
1662
+ readonly spatial?: {
1663
+ readonly kind: 'geography' | 'geometry';
1664
+ readonly geomKind: string;
1665
+ readonly srid: number;
1666
+ };
1667
+ }
1668
+
1669
+ declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
1670
+
1671
+ declare type ColumnType_2 = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
1672
+
1673
+ declare interface DataStore {
1674
+ /**
1675
+ * Execute a typed query descriptor and return matching rows.
1676
+ *
1677
+ * Deliberately NOT generic: `DataStore` is the driver SPI, implemented by
1678
+ * every dialect store and its transactional / namespace views. Threading a
1679
+ * phantom row type through all of them would be pure churn — they genuinely
1680
+ * do return untyped rows off the wire. The row type is re-applied one layer
1681
+ * up, on `FluentStore.query`, which is what handlers actually call.
1682
+ */
1683
+ query(descriptor: QueryDescriptor_2): Promise<ReadonlyArray<Row_2>>;
1684
+ /** Insert a row. Emits an insert ChangeEvent (deferred if inside `transactional()`). */
1685
+ insert(table: string, row: Row_2): Promise<Row_2>;
1686
+ /**
1687
+ * Insert many rows in ONE statement and return the inserted rows.
1688
+ *
1689
+ * A single multi-row `INSERT … VALUES (…),(…)` per dialect — one
1690
+ * round-trip regardless of row count, instead of N `insert()` calls.
1691
+ *
1692
+ * Emits ONE `insert` ChangeEvent PER row (deferred if inside
1693
+ * `transactional()`), so reactive subscribers see each new row's
1694
+ * delta exactly as they would from N single inserts.
1695
+ *
1696
+ * Cross-dialect:
1697
+ * - postgres / sqlite / mariadb 10.5+: `INSERT … VALUES (…),(…) RETURNING *`
1698
+ * - mssql: `INSERT … OUTPUT INSERTED.* VALUES (…),(…)`
1699
+ * - mysql: multi-row `INSERT`, then re-select by id
1700
+ * - memory store: loop + per-row emit
1701
+ *
1702
+ * An empty `rows` array is a no-op that returns `[]` (no statement, no events).
1703
+ */
1704
+ insertMany(table: string, rows: ReadonlyArray<Row_2>): Promise<ReadonlyArray<Row_2>>;
1705
+ /**
1706
+ * In-place JSON merge: set `value` at `path` inside the JSON column
1707
+ * (`path` is the column name, optionally with a nested dot-path, e.g.
1708
+ * `'metadata'` or `'metadata.theme'`). Reads-modifies-writes the
1709
+ * single row identified by `pk` and returns the post-image (or `null`
1710
+ * when the row doesn't exist). Emits an `update` ChangeEvent (deferred
1711
+ * if inside `transactional()`).
1712
+ *
1713
+ * Cross-dialect (server-side merge, no round-trip read):
1714
+ * - postgres: `jsonb_set(col, '{path}', value)` (top-level: `col || value`)
1715
+ * - mysql / mariadb: `JSON_SET(col, '$.path', value)`
1716
+ * - mssql: `JSON_MODIFY(col, '$.path', value)`
1717
+ * - sqlite: `json_set(col, '$.path', value)`
1718
+ * - memory store: read-modify-write the JS object in place
1719
+ *
1720
+ * `additionalPatch` assigns other columns in the SAME row update (e.g. an
1721
+ * automatic timestamp). Undefined values are omitted; null is explicit.
1722
+ * Cannot rewrite the primary key or replace the JSON column being patched.
1723
+ */
1724
+ patchJson(table: string, primaryKey: RowKey, path: string, value: unknown, additionalPatch?: Readonly<Record<string, unknown>>): Promise<Row_2 | null>;
1725
+ /** Patch a row by primary key. Emits an update ChangeEvent (deferred if inside `transactional()`).
1726
+ * Returns the post-image. */
1727
+ update(table: string, primaryKey: RowKey, patch: Readonly<Record<string, unknown>>): Promise<Row_2 | null>;
1728
+ /**
1729
+ * Bulk update — apply `patch` to every row in `table` matching the
1730
+ * `where` predicate (Q9). The predicate is a `Predicate` AST (the
1731
+ * same shape `.where()` uses on queries) and CAN carry sub-queries
1732
+ * via inSubquery / exists.
1733
+ *
1734
+ * Returns the number of rows updated. Emits one `update`
1735
+ * ChangeEvent per affected row (deferred if inside `transactional()`),
1736
+ * so reactive subscribers see each row's delta — at the cost of N
1737
+ * events instead of one.
1738
+ *
1739
+ * Cross-dialect:
1740
+ * - SQL stores: `UPDATE <table> SET <patch> WHERE <pred>` in one
1741
+ * statement; the per-row events come from a fan-out the store
1742
+ * handles (postgres uses RETURNING; the others read-then-update).
1743
+ * - memory store: linear filter + per-row update + emit
1744
+ */
1745
+ updateMany(table: string, patch: Readonly<Record<string, unknown>>, options: {
1746
+ where: Predicate;
1747
+ }): Promise<number>;
1748
+ /** Delete a row by primary key. Emits a delete ChangeEvent (deferred if inside `transactional()`). */
1749
+ delete(table: string, primaryKey: RowKey): Promise<boolean>;
1750
+ /**
1751
+ * Bulk delete — remove every row in `table` matching the `where`
1752
+ * predicate. The predicate is a `Predicate` AST (the same shape
1753
+ * `.where()` uses on queries) and CAN carry sub-queries via
1754
+ * inSubquery / exists. The set-based counterpart to `updateMany`.
1755
+ *
1756
+ * This is a HARD delete: it removes the row regardless of whether the
1757
+ * table carries `softDelete()`. (Soft-delete is a write-time concern
1758
+ * the `ctx.store` middleware layers on the keyed `delete()` / the
1759
+ * fluent `delete(...).soft()` builder — `deleteMany` is the low-level
1760
+ * physical remove, mirroring `updateMany`'s lack of mixin handling.)
1761
+ *
1762
+ * Returns the number of rows deleted. Emits one `delete` ChangeEvent
1763
+ * per removed row (deferred if inside `transactional()`), each
1764
+ * carrying the removed row's old-image as `old` — so reactive
1765
+ * subscribers see each row leave, at the cost of N events instead of
1766
+ * one.
1767
+ *
1768
+ * Cross-dialect:
1769
+ * - postgres / sqlite / mariadb 10.0+: `DELETE FROM <table> WHERE
1770
+ * <pred> RETURNING *` in one statement; the per-row old-image
1771
+ * events come from the RETURNING rows.
1772
+ * - mssql: `DELETE FROM <table> OUTPUT DELETED.* WHERE <pred>` —
1773
+ * OUTPUT DELETED.* gives the old-images in one statement.
1774
+ * - mysql (8.x): no `RETURNING` on DELETE — SELECT the matching
1775
+ * rows FIRST (for the old-image fan-out), then one DELETE.
1776
+ * - memory store: linear filter + per-row delete + emit.
1777
+ */
1778
+ deleteMany(table: string, options: {
1779
+ where: Predicate;
1780
+ }): Promise<number>;
1781
+ /**
1782
+ * Upsert: INSERT a row, and on conflict over `conflictColumns`,
1783
+ * UPDATE the conflicting row instead.
1784
+ *
1785
+ * `conflictColumns` declares which column set defines a conflict.
1786
+ * Most often a single column with a UNIQUE constraint (`['email']`)
1787
+ * or a composite UNIQUE (`['orgId', 'slug']`).
1788
+ *
1789
+ * `update` controls what happens on conflict:
1790
+ * - omitted / `undefined` → update every column from the input row
1791
+ * (matches Drizzle's `onConflictDoUpdate({ target, set: input })` shorthand)
1792
+ * - `ReadonlyArray<string>` → update only these columns from the input row
1793
+ * - `(existing) => Patch` → compute a patch from the conflicting row;
1794
+ * useful for "increment counter" semantics
1795
+ *
1796
+ * `updateValues` adds conflict-only assignments without changing the insert
1797
+ * row. See `UpsertOptions` for precedence and omission rules.
1798
+ *
1799
+ * Returns the FINAL row (post-insert OR post-update) so callers can
1800
+ * read the assigned PK without a separate query.
1801
+ *
1802
+ * Emits an `insert` ChangeEvent on the insert path and an `update`
1803
+ * ChangeEvent on the conflict path, so reactive subscribers see a
1804
+ * single delta regardless of which branch ran.
1805
+ *
1806
+ * Cross-dialect implementation:
1807
+ * - postgres / sqlite: `INSERT … ON CONFLICT (cols) DO UPDATE SET …`
1808
+ * - mariadb 10.5+: `INSERT … ON DUPLICATE KEY UPDATE … RETURNING *`
1809
+ * - mysql: SELECT-then-INSERT-or-UPDATE fallback (no RETURNING)
1810
+ * - mssql: SELECT-then-INSERT-or-UPDATE fallback
1811
+ * - memory store: linear scan + match-by-conflict-columns
1812
+ */
1813
+ upsert(table: string, row: Row_2, options: UpsertOptions): Promise<Row_2>;
1814
+ /**
1815
+ * `upsert`, plus the one thing it cannot report: whether the row was INSERTED
1816
+ * or UPDATED.
1817
+ *
1818
+ * An app that needs to know does the read itself today — look for the row,
1819
+ * then upsert — which costs a round trip AND opens a window in which the
1820
+ * answer stops being true. It does not have to: the framework's store wrapper
1821
+ * mints the id before the statement, and on the conflict path every dialect
1822
+ * hands back the EXISTING row's id. Two ids that differ ARE the answer, for
1823
+ * free.
1824
+ *
1825
+ * Optional, and provided by the framework's wrapper rather than by a dialect
1826
+ * store — the outcome is decided from what the wrapper already knows, so no
1827
+ * dialect implements anything. It is present on `ctx.store` in a handler; a
1828
+ * hand-written `DataStore` need not have it. The typed helper is
1829
+ * `upsertRowOutcome`.
1830
+ *
1831
+ * `'inserted'` is exact, never a default for "could not tell": where the free
1832
+ * comparison cannot answer (the caller supplied the id, or the table's id is
1833
+ * database-assigned) the destination is read first.
1834
+ */
1835
+ upsertWithOutcome?(table: string, row: Row_2, options: UpsertOptions): Promise<UpsertResult>;
1836
+ /**
1837
+ * Insert-or-ignore: INSERT a row, but if it conflicts on
1838
+ * `conflictColumns`, leave the existing row UNCHANGED and skip the
1839
+ * write. Returns the existing row (when ignored) or the inserted
1840
+ * row.
1841
+ *
1842
+ * Equivalent to `upsert(..., { update: 'nothing' })` but typed as
1843
+ * a separate method for clarity at the call site. The signal is "I
1844
+ * want this row to exist; if it already does, don't touch it" —
1845
+ * common for dedup tables, idempotent event sinks, audit-log
1846
+ * write-once patterns.
1847
+ *
1848
+ * Cross-dialect:
1849
+ * - postgres / sqlite: `INSERT … ON CONFLICT (cols) DO NOTHING RETURNING *`
1850
+ * - mariadb 10.5+: `INSERT IGNORE … RETURNING *`, lookup on the ignore path
1851
+ * - mysql: SELECT-then-skip-or-INSERT fallback
1852
+ * - mssql: SELECT-then-skip-or-INSERT fallback
1853
+ * - memory store: lookup-then-skip
1854
+ *
1855
+ * Emits an `insert` ChangeEvent ONLY when the insert actually
1856
+ * happened; the ignore path emits nothing (no row changed).
1857
+ */
1858
+ insertIgnore(table: string, row: Row_2, options: {
1859
+ conflictColumns: ReadonlyArray<string>;
1860
+ }): Promise<Row_2>;
1861
+ /**
1862
+ * `insertIgnore`, plus the one thing it cannot report: whether the row was
1863
+ * INSERTED or already there.
1864
+ *
1865
+ * The store's actual insert decision supplies the outcome, including for
1866
+ * caller-supplied and database-generated IDs. A pre-read or comparison of
1867
+ * IDs cannot establish which concurrent caller inserted. Every raw store,
1868
+ * namespace view and transaction view implements this method; wrappers
1869
+ * preserve its outcome while encoding/decoding the row. Inside a transaction
1870
+ * the result is provisional until commit. The typed helper is
1871
+ * `insertIgnoreRowOutcome`.
1872
+ */
1873
+ insertIgnoreWithOutcome(table: string, row: Row_2, options: {
1874
+ conflictColumns: ReadonlyArray<string>;
1875
+ }): Promise<InsertIgnoreResult>;
1876
+ /**
1877
+ * Run `work` inside a transaction. ALL `insert/update/delete` calls
1878
+ * made via `work`'s store reference (passed as argument) are:
1879
+ *
1880
+ * - atomic — the dialect's native BEGIN/COMMIT/ROLLBACK wraps the
1881
+ * writes; memory validates snapshot read/write-table revisions before
1882
+ * committing and retries conflicting attempts with a fresh snapshot.
1883
+ * - delta-deferred — ChangeEvents queue inside the transaction and
1884
+ * are emitted ONLY after a successful commit. A throw triggers
1885
+ * rollback AND drops every queued event so no subscriber ever
1886
+ * sees a partial-update flicker.
1887
+ *
1888
+ * Reads (`query()`) inside the transaction see the in-flight write
1889
+ * set (read-your-own-writes within the transaction).
1890
+ * Transient contention can replay the whole callback (at most four attempts,
1891
+ * exponential delay starting at 10 ms). Do not perform external side effects
1892
+ * in the callback. Memory validation is table-granular and process-local;
1893
+ * `forUpdate` does not acquire a blocking row lock there.
1894
+ *
1895
+ * `transactional()` MUST NOT be nested — nested invocations throw to
1896
+ * surface design mistakes (transactions are atomic units, not
1897
+ * composable scopes). Use a single top-level `transactional()` per
1898
+ * mutation.
1899
+ */
1900
+ transactional<T>(work: (tx: DataStore) => Promise<T>): Promise<T>;
1901
+ /** Subscribe to row-change events. Returns an unsubscribe function.
1902
+ *
1903
+ * See `CHANGE_LISTENER_CEILING` for why an EventEmitter-backed
1904
+ * implementation must raise its listener limit before binding. */
1905
+ onChange(listener: (event: ChangeEvent) => void): () => void;
1906
+ /**
1907
+ * Inject a ChangeEvent that originated OUTSIDE this process — the
1908
+ * cross-instance reactivity seam. Emits the event to local `onChange`
1909
+ * subscribers (the dispatcher, the cache invalidation bus, …) exactly
1910
+ * as a local write would, WITHOUT re-persisting it (the row already
1911
+ * landed in the shared database on the writer).
1912
+ *
1913
+ * Two consumers route through here:
1914
+ * - Postgres' LISTEN/NOTIFY CDC consumer, translating each NOTIFY
1915
+ * payload back into a ChangeEvent.
1916
+ * - `@voltro/plugin-broadcast`'s bus, injecting remote-replica
1917
+ * ChangeEvents fanned out over Redis / NATS for the non-postgres
1918
+ * dialects that have no native fan-out.
1919
+ *
1920
+ * It is NOT a write method — there is no transaction, no SQL, no return
1921
+ * value. Subscribe-side only.
1922
+ */
1923
+ injectExternalChange(event: ChangeEvent): void;
1924
+ /**
1925
+ * How this store's ChangeEvents reach the process. `'local'` (the
1926
+ * default when absent): `onChange` carries this process's OWN writes
1927
+ * inline, plus optionally `'injected'`-origin events from a broadcast
1928
+ * transport — a listener that skips `origin === 'injected'` therefore
1929
+ * sees each fleet-wide change exactly once (on its writer). `'fleet'`:
1930
+ * every replica receives the full fleet-wide stream via a CDC consumer
1931
+ * (own writes included, all `'injected'`) — an exactly-once consumer
1932
+ * must elect a single worker instead of filtering by origin.
1933
+ */
1934
+ readonly changeScope?: 'local' | 'fleet';
1935
+ /**
1936
+ * Subscribe to CHANGE-STREAM GAPS — moments where this process proved it
1937
+ * stopped receiving changes for a while and has now started again.
1938
+ *
1939
+ * A gap is not an error to show a user; it is an instruction to the reactive
1940
+ * layer: everything derived from this store may be stale, so re-run it. A
1941
+ * live query is idempotent, which is what makes "re-run all of them" both
1942
+ * safe and the complete recovery. `voltro dev` and `voltro serve` wire this
1943
+ * to the dispatcher's `refreshAll`, the same recovery the broadcast bus's
1944
+ * peer-serial gap uses.
1945
+ *
1946
+ * Optional because only a store whose changes arrive over a TRANSPORT can
1947
+ * have one. An inline store emits its own writes in-process — there is no
1948
+ * connection to lose — and correctly does not implement this. Absent
1949
+ * therefore means "gaps are impossible here", never "gaps are not reported".
1950
+ */
1951
+ readonly onChangeStreamGap?: (listener: (gap: ChangeStreamGap) => void) => () => void;
1952
+ /**
1953
+ * Bind a per-request tenant NAMESPACE, returning a store view whose
1954
+ * every read + write is physically isolated to that namespace
1955
+ * (schema / database / attached file per dialect — see
1956
+ * `@voltro/database`'s `namespace.ts`). The returned view shares the
1957
+ * underlying connection pool + change emitter with `this`; only the
1958
+ * namespace qualification differs.
1959
+ *
1960
+ * `namespace` is the already-resolved, already-sanitised identifier
1961
+ * (e.g. `tenant_acme`) from `resolveTenantNamespace(subject.tenantId)`.
1962
+ * Passing `null` returns a view with NO qualification — the
1963
+ * shared-schema default. The runtime resolves the namespace from the
1964
+ * request subject and binds it here; it NEVER passes a raw, unsanitised
1965
+ * tenant id.
1966
+ *
1967
+ * OPTIONAL on the interface: every shipped store implements it, but
1968
+ * callers that never enable namespace isolation can ignore it.
1969
+ * Equivalent to `this` when the store has no physical-isolation
1970
+ * capability (none ship without it, but the optionality keeps
1971
+ * third-party stores from being forced to implement it).
1972
+ */
1973
+ withNamespace?(namespace: string | null): DataStore;
1974
+ /**
1975
+ * Raw-SQL escape hatch. Execute a {@link RawSqlFragment} captured by
1976
+ * the `sql` template tag (`@voltro/database/sql`) against the store's
1977
+ * live connection and return the decoded rows.
1978
+ *
1979
+ * Values interpolated into the `sql` template are bound as parameters
1980
+ * by the store's dialect-specific `sql` constructor — never
1981
+ * interpolated into the SQL text — so a value containing `'; DROP
1982
+ * TABLE …` round-trips as data. The *static* SQL text is verbatim:
1983
+ * the caller owns its dialect-portability (an escape hatch trades
1984
+ * portability for power).
1985
+ *
1986
+ * `opts.dependsOn` (or `fragment.dependsOn`) declares the tables this
1987
+ * read touches so the reactive layer can recompute it on writes. Raw
1988
+ * reads are otherwise untracked — the planner can't infer table
1989
+ * dependencies from arbitrary SQL. Best-effort: the tables are
1990
+ * recorded, not validated against the fragment.
1991
+ *
1992
+ * OPTIONAL on the interface: SQL stores implement it; the in-memory
1993
+ * store omits it (there is no SQL engine to run raw text against).
1994
+ * Callers must guard with `if (store.raw)` before invoking.
1995
+ */
1996
+ raw?<T extends object = Row_2>(fragment: RawSqlFragment, opts?: {
1997
+ dependsOn?: ReadonlyArray<string>;
1998
+ }): Promise<ReadonlyArray<T>>;
1999
+ /**
2000
+ * Gracefully dispose the store's connection pool + background consumers
2001
+ * (the CDC/NOTIFY fiber). Waits up to `gracePeriodMs` for in-flight
2002
+ * transactions to settle before tearing the pool down, so a `voltro serve`
2003
+ * SIGTERM doesn't sever a connection mid-COMMIT.
2004
+ *
2005
+ * OPTIONAL on the interface: every SQL-backed store implements it; the
2006
+ * in-memory store has no pool to close. `voltro serve`'s shutdown guards
2007
+ * with `if (store.close)`.
2008
+ */
2009
+ close?(gracePeriodMs?: number): Promise<void>;
2010
+ /**
2011
+ * Liveness probe for the store's backing connection — runs a trivial
2012
+ * `SELECT 1`. Resolves when the DB answers; REJECTS when it can't (dead
2013
+ * pool, network partition). The serve path's `/internal/readiness` handler
2014
+ * uses it so a pod whose DB connection died reports 503 and stops taking
2015
+ * traffic instead of erroring every request.
2016
+ *
2017
+ * OPTIONAL on the interface: SQL stores implement it; the in-memory store
2018
+ * omits it (always reachable). Callers guard with `if (store.ping)`.
2019
+ */
2020
+ ping?(): Promise<void>;
2021
+ /**
2022
+ * Empty every one of `tables` as ONE unit, with referential integrity
2023
+ * suspended for the duration.
2024
+ *
2025
+ * Written for `voltro data import --mode replace`, whose whole job is to put
2026
+ * a target into the state a bundle describes. Two properties, and both are
2027
+ * the point:
2028
+ *
2029
+ * - **All or nothing.** A run that cannot empty every table must leave the
2030
+ * target exactly as it found it. The loop this replaces deleted table by
2031
+ * table and stopped at the first refusal, so an operator's *second*
2032
+ * attempt destroyed more than the first — the deletes that had already
2033
+ * succeeded stayed gone and nothing was reloaded.
2034
+ * - **Order-free.** A parent cannot be emptied while a child still points
2035
+ * at it, and no ordering fixes the two shapes that occur in every real
2036
+ * schema: a table that references ITSELF (the framework's own actor
2037
+ * pattern — `createdBy` on `actors` points at `actors`), and a genuine
2038
+ * cycle. On engines that check each row as it is deleted, ordering the
2039
+ * TABLES cannot help, because the conflict is between two ROWS of one of
2040
+ * them.
2041
+ *
2042
+ * What it deliberately does NOT do is reach outside `tables`. A row in some
2043
+ * OTHER table pointing into this set is not ours to delete, and suspending
2044
+ * integrity would strand it. The caller checks for that first and refuses;
2045
+ * see `runImport`'s replace pre-flight.
2046
+ *
2047
+ * Emits no change events, and the consequence is worth stating rather than
2048
+ * implying. One event per deleted row is not affordable at bundle scale — a
2049
+ * replace of a grown target fanned tens of thousands of them out to every live
2050
+ * subscriber — but silence has an edge. In practice the LOAD that follows
2051
+ * covers it: the reactive layer re-reads a query when any row of its table
2052
+ * changes, so the first inserted row refreshes subscribers and the emptied
2053
+ * rows leave their view with it. The exception is a table the bundle carries
2054
+ * EMPTY: nothing is loaded, nothing is emitted, and a subscriber keeps rows
2055
+ * that are gone until it re-subscribes.
2056
+ *
2057
+ * OPTIONAL on the interface: SQL stores implement it per dialect (the
2058
+ * suspension mechanism is engine-specific and session-scoped, so it has to
2059
+ * live next to the connection). Callers guard with `if (store.emptyTables)`
2060
+ * and fall back to deleting inside one transaction — which keeps the
2061
+ * all-or-nothing half on any store, and is all the in-memory store needs.
2062
+ */
2063
+ emptyTables?(tables: ReadonlyArray<string>): Promise<void>;
2064
+ /**
2065
+ * Foreign keys pointing INTO `tables` from tables that are NOT in the set,
2066
+ * read from the LIVE catalog.
2067
+ *
2068
+ * The question `--mode replace` has to answer before it deletes anything:
2069
+ * emptying a parent whose children live outside the bundle would strand rows
2070
+ * the bundle cannot restore. It was answered from the caller's SNAPSHOT, which
2071
+ * is right for the CLI (it introspects the target) and wrong for the admin
2072
+ * endpoint, which passes the app's DECLARED schema — that cannot show a table
2073
+ * the app has stopped declaring but the database still has, and those are
2074
+ * exactly the rows nobody is watching.
2075
+ *
2076
+ * The catalog is the only authority here, so the store asks it.
2077
+ *
2078
+ * OPTIONAL: SQL stores implement it; the in-memory store has no catalog and no
2079
+ * foreign keys, so it omits it and callers fall back to the snapshot.
2080
+ */
2081
+ incomingForeignKeys?(tables: ReadonlyArray<string>): Promise<ReadonlyArray<{
2082
+ readonly from: string;
2083
+ readonly column: string;
2084
+ readonly to: string;
2085
+ }>>;
2086
+ /**
2087
+ * Run `fn` inside a nested savepoint, so a failure inside it costs only its
2088
+ * own work instead of the whole transaction.
2089
+ *
2090
+ * Exists for one reason: on postgres (and SQL Server for some errors) a failed
2091
+ * statement ABORTS the enclosing transaction — every statement after it
2092
+ * answers `current transaction is aborted` and nothing can be salvaged. The
2093
+ * importer's deferred-foreign-key repair depends on the opposite: a row whose
2094
+ * parent has not loaded yet is EXPECTED to fail, is held, and is retried once
2095
+ * the rest has streamed. That repair is how a bundle whose row order puts a
2096
+ * child before its parent lands at all — and ids sort that way as often as
2097
+ * not. Without a savepoint the two are incompatible, so `--atomic` simply
2098
+ * could not import such a bundle on those engines.
2099
+ *
2100
+ * Only the TRANSACTIONAL views implement it, and only for the engines that
2101
+ * need it: the mysql family and sqlite leave a transaction usable after a
2102
+ * failed statement, so wrapping every row there would buy nothing and cost a
2103
+ * round trip per row. Callers guard with `if (store.savepoint)` and run `fn`
2104
+ * directly otherwise — which is also correct outside a transaction, where
2105
+ * there is nothing to protect.
2106
+ *
2107
+ * Failures propagate: the savepoint is rolled back and the error rethrown, so
2108
+ * the caller's own error handling is unchanged.
2109
+ */
2110
+ savepoint?<A>(fn: () => Promise<A>): Promise<A>;
2111
+ /**
2112
+ * Re-decide which tables this store's change-capture reader can decode, and
2113
+ * apply the answer to the LIVE reader.
2114
+ *
2115
+ * Exists because that decision is a reading of the SCHEMA, and a boot that
2116
+ * migrates in the same process moves the schema after the reader attached.
2117
+ * The reader is built before the migration runs (it has consumers — kv,
2118
+ * broadcast, the analytics mirror — that need a store first), so a table
2119
+ * excluded on the pre-migration shape stayed excluded for the life of the
2120
+ * process even when the migration was the very thing that fixed it.
2121
+ *
2122
+ * Call it once every boot, AFTER all schema work. Cheap (one catalog query)
2123
+ * and a no-op on a store whose verdict did not move — including every store
2124
+ * with nothing to exclude.
2125
+ *
2126
+ * OPTIONAL on the interface: only the mysql-family binlog reader has a
2127
+ * physical-layout exclusion at all. Postgres NOTIFY, mssql Change Tracking
2128
+ * and the in-memory store decode whatever they are given, so they omit it and
2129
+ * callers guard with `if (store.refreshChangeCaptureExclusions)`.
2130
+ */
2131
+ refreshChangeCaptureExclusions?(): Promise<ChangeCaptureExclusionRefresh>;
2132
+ /**
2133
+ * Run an Effect that needs this store's own `SqlClient` — the raw-SQL seam.
2134
+ *
2135
+ * The capability the STAGED SWAP is built on. `--mode replace` loads into
2136
+ * `_voltro_staging_<t>` tables and then swaps the CONTENT in one short
2137
+ * transaction (`DELETE FROM t; INSERT INTO t (…) SELECT … FROM
2138
+ * _voltro_staging_t`), which moves the long, network-bound load OUT of the
2139
+ * destructive transaction. None of that is expressible through the typed row
2140
+ * API: it is DDL plus a server-side `INSERT … SELECT`, and the whole point is
2141
+ * that no row crosses the wire.
2142
+ *
2143
+ * OPTIONAL on the interface, and declared here rather than duck-typed at the
2144
+ * call site for the reason the house rule gives: the four dialect stores are
2145
+ * four hand-written copies of one interface, and a capability nothing declares
2146
+ * is a capability nothing can be red about. A store that loses it now fails to
2147
+ * satisfy this member instead of silently falling out of `canStage()` and
2148
+ * taking the weaker path forever, on one dialect, in silence.
2149
+ *
2150
+ * The DECISION does not live per store — that would be the four-copies trap
2151
+ * wearing a different hat. `@voltro/data-transfer`'s `stagedSwap.ts` writes
2152
+ * the per-dialect SQL ONCE and sends it down this seam; the store contributes
2153
+ * a connection, not a policy.
2154
+ *
2155
+ * Callers guard with `if (store.run)` (or `canStage(store)`), and a store
2156
+ * without it keeps the path it already has: the in-memory store has no SQL
2157
+ * and no catalog, and neither does a bare `DataStore` view.
2158
+ *
2159
+ * `R` is deliberately `SqlClient.SqlClient`: the effect asks for the client
2160
+ * and the store provides its own, which is what makes this a SEAM rather than
2161
+ * a way to hand a store somebody else's connection.
2162
+ */
2163
+ run?<A, E>(effect: Effect.Effect<A, E, SqlClient.SqlClient>): Promise<A>;
2164
+ }
2165
+
2166
+ /**
2167
+ * Build a `SchemaSnapshot` from an array of `TableLike` declarations.
2168
+ * Caller is responsible for passing the FULL set of tables (user-
2169
+ * declared + framework). The fingerprint over this snapshot is what the
2170
+ * boot-time prod check compares against `_voltro_migration_plans`.
2171
+ */
2172
+ export declare const declaredSnapshot: (tables: ReadonlyArray<TableLike>, dialect?: DialectId) => SchemaSnapshot;
2173
+
2174
+ /** The NOTIFY channel a schema emits on when none is configured. */
2175
+ export declare const DEFAULT_CDC_CHANNEL = "framework_changes";
2176
+
2177
+ /**
2178
+ * `DEFAULT <expr>` for an array-valued default.
2179
+ *
2180
+ * Postgres is the only dialect with native arrays, so a `array()` column there
2181
+ * takes an array literal (`'{a,b}'::text[]`); every other dialect stores the
2182
+ * value as JSON/TEXT and takes the json form. A default on a `json()` column
2183
+ * always takes the json form — the array is the VALUE, not the storage.
2184
+ */
2185
+ export declare const defaultArrayClause: (value: ReadonlyArray<unknown>, column: ColumnDefinition<unknown>, dialect: DialectId) => string;
2186
+
2187
+ export declare const defaultClause: (column: ColumnDefinition<unknown>, dialect: DialectId) => string | null;
2188
+
2189
+ /**
2190
+ * `DEFAULT <expr>` for a plain-object json default, per dialect's json-column
2191
+ * type. Single source for the auto-migrate (`defaultClause`) AND declarative-
2192
+ * applier paths so the two can't drift (the introspected form must round-trip
2193
+ * against `normalizeDefault`, so the emit shape matters on both sides).
2194
+ */
2195
+ export declare const defaultJsonClause: (value: object, dialect: DialectId) => string;
2196
+
2197
+ /**
2198
+ * The DATA half of a three-step `add-column`, handed back by `executeOp`
2199
+ * instead of run in place when the caller collects it. `data` is the backfill
2200
+ * — UPDATEs only; `after` is the DDL that follows it (the NOT NULL
2201
+ * enforcement, or the sqlite rebuild that stands in for it). The two are
2202
+ * separate so the caller can run every collected `data` inside ONE
2203
+ * transaction and every `after` outside it, which is the only order MySQL
2204
+ * and MariaDB can honour: a DDL statement inside the transaction would commit
2205
+ * the backfills before it.
2206
+ */
2207
+ export declare interface DeferredData {
2208
+ readonly table: string;
2209
+ readonly op: MigrationOperation;
2210
+ readonly data: Effect.Effect<void, SqlError_2>;
2211
+ readonly after: Effect.Effect<void, SqlError_2>;
2212
+ }
2213
+
2214
+ export declare const DEPLOY_LEDGER_TABLE = "_voltro_deploys";
2215
+
2216
+ export declare interface DeployLedgerRow {
2217
+ readonly id: string;
2218
+ readonly status: 'running' | 'failed';
2219
+ readonly dialect: string;
2220
+ readonly appliedBy: string;
2221
+ readonly startedAt: string;
2222
+ readonly baseline: string;
2223
+ readonly report: string | null;
2224
+ }
2225
+
2226
+ export declare const describeOutcome: (outcome: BootMigrationOutcome, dialectId: string) => string;
2227
+
2228
+ /**
2229
+ * Parse `VOLTRO_DESTRUCTIVE_OK`.
2230
+ *
2231
+ * `1` / `true` acknowledges every lossy op in the run. A comma-separated list
2232
+ * acknowledges only those TABLES — which is the form somebody wants far more
2233
+ * often, and its absence was a real complaint: a plan carrying one intended drop
2234
+ * plus three other lossy ops had no way to say yes to the one. `1` said yes to
2235
+ * all four, and the alternative was a hand-written DROP migration, which the
2236
+ * framework's own upgrade note warns against.
2237
+ *
2238
+ * `undefined` = not set at all.
2239
+ */
2240
+ export declare const destructiveScope: (raw: string | undefined) => {
2241
+ readonly kind: "all";
2242
+ } | {
2243
+ readonly kind: "tables";
2244
+ readonly tables: ReadonlySet<string>;
2245
+ } | undefined;
2246
+
2247
+ /**
2248
+ * Compare declared reactivity against the triggers actually installed.
2249
+ *
2250
+ * Postgres only — it is the one dialect where reactivity is carried by DDL. The
2251
+ * binlog and Change Tracking readers are configured at runtime from the same
2252
+ * in-memory table list the schema produced, so they cannot drift from it.
2253
+ */
2254
+ export declare const detectReactiveTriggerDrift: (sql: SqlClient.SqlClient, tables: ReadonlyArray<{
2255
+ readonly tableName: string;
2256
+ readonly isReactive?: boolean;
2257
+ }>,
2258
+ /** The app's `cdcChannel`. Part of the trigger NAME on any non-default
2259
+ * channel, so omitting it here reported every table as missing on a database
2260
+ * where every trigger was present. */
2261
+ channel?: string) => Effect.Effect<ReactiveTriggerDrift, SqlError_2>;
2262
+
2263
+ /** Dialect IDs the framework supports. */
2264
+ declare type DialectId = 'postgres' | 'mysql' | 'mariadb' | 'mssql' | 'sqlite' | 'turso';
2265
+
2266
+ /**
2267
+ * Output of `voltro db drift`: compares the live DB's introspected
2268
+ * schema against the last applied fingerprint in `_voltro_migrations`.
2269
+ *
2270
+ * `isDrifted: false` is the happy path. A drift report names the
2271
+ * concrete divergences so the operator can either reconcile in code or
2272
+ * via a corrective migration.
2273
+ */
2274
+ export declare interface DriftReport {
2275
+ readonly isDrifted: boolean;
2276
+ readonly lastAppliedFingerprint?: string;
2277
+ readonly liveFingerprint: string;
2278
+ readonly divergences: ReadonlyArray<{
2279
+ readonly kind: 'unexpected' | 'missing';
2280
+ readonly description: string;
2281
+ }>;
2282
+ }
2283
+
2284
+ export declare interface EachOptions {
2285
+ /** Rows per page. Default 1000. Pages are keyed by the `id` column. */
2286
+ readonly batchSize?: number;
2287
+ /** Milliseconds to sleep between pages, to share the database with live traffic. Default 0. */
2288
+ readonly sleepMs?: number;
2289
+ }
2290
+
2291
+ export declare interface EachReport {
2292
+ readonly scanned: number;
2293
+ readonly pages: number;
2294
+ }
2295
+
2296
+ /**
2297
+ * Per-branch refinement passed to `.with({ relation: { ... } })`.
2298
+ *
2299
+ * `true` is the zero-config shorthand — equivalent to `{}`. Otherwise
2300
+ * each branch can carry its own `where` / `orderBy` / `limit` /
2301
+ * `offset` / nested `with`, so `users.with({ posts: { limit: 10,
2302
+ * orderBy: 'createdAt' } })` works without dropping into a sub-query
2303
+ * builder.
2304
+ *
2305
+ * Untyped at runtime: the QueryDescriptor is the boundary between the
2306
+ * typed builder API and the dialect-agnostic JoinCompiler. End-user
2307
+ * types live one layer up in the typed `.with()` method on `Query`.
2308
+ */
2309
+ declare interface EagerLoadSpec {
2310
+ readonly where?: Predicate;
2311
+ readonly orderBy?: ReadonlyArray<OrderClause>;
2312
+ /**
2313
+ * Per-parent cap on the relation. `{ limit: 10 }` returns up to 10
2314
+ * rows FOR EACH parent — same semantics as Drizzle / Prisma /
2315
+ * Hibernate. This matches what users actually mean when they write
2316
+ * "the 10 latest posts" inside `users.with({ posts: { limit: 10 } })`.
2317
+ *
2318
+ * The SQL stores express this natively as a `LIMIT` clause in a
2319
+ * correlated subquery (postgres / sqlite / mysql / mariadb) or
2320
+ * via `TOP N` inside `FOR JSON PATH` (mssql) when the JSON-agg path
2321
+ * is active. The walker fallback post-buckets the batch fetch and
2322
+ * slices each bucket.
2323
+ */
2324
+ readonly limit?: number;
2325
+ /** Per-parent offset. Only meaningful with `limit`. */
2326
+ readonly offset?: number;
2327
+ readonly with?: WithSpec;
2328
+ /**
2329
+ * Project the JUNCTION row's own columns into a `manyToMany` eager result.
2330
+ *
2331
+ * `true` takes every junction column; an array takes only those named. The
2332
+ * values land under `_junction` on each target row:
2333
+ *
2334
+ * projects.with({ teams: { junction: ['role', 'addedAt'] } })
2335
+ * // → team._junction.addedAt
2336
+ *
2337
+ * Nested rather than merged onto the target row, deliberately: a junction and
2338
+ * its target routinely share column names (`createdAt` is the obvious one),
2339
+ * and merging would silently overwrite real target data with membership data.
2340
+ * A name collision that corrupts a row is worse than one extra level.
2341
+ *
2342
+ * Why this exists: junction tables carrying meaningful columns — a role, a
2343
+ * joined-at stamp, a permission tier — are the rule rather than the
2344
+ * exception, and without this an eager load could FILTER on them
2345
+ * (`onJunction`) but never return them, so any relation with real membership
2346
+ * data had to stay a hand-written join. The junction rows were already being
2347
+ * fetched in full to resolve the target ids; this stops throwing them away.
2348
+ */
2349
+ readonly junction?: true | ReadonlyArray<string>;
2350
+ /**
2351
+ * Filter a `manyToMany` eager branch on the THROUGH-table's columns.
2352
+ * The predicate is evaluated against the junction row, not the target
2353
+ * row — e.g. for `users.with({ organizations: { onJunction: eq('role',
2354
+ * 'admin') } })` it keeps only the memberships where `role = 'admin'`,
2355
+ * so the loaded `organizations` are exactly the orgs the user is an
2356
+ * admin of.
2357
+ *
2358
+ * Compiles into the correlated subquery's WHERE alongside the
2359
+ * source-key correlation, qualified to the junction-table alias.
2360
+ * Ignored on `one` / `many` branches (no junction table exists).
2361
+ */
2362
+ readonly onJunction?: Predicate;
2363
+ }
2364
+
2365
+ /**
2366
+ * Per-branch refinement passed to `.with({ relation: { ... } })`.
2367
+ *
2368
+ * `true` is the zero-config shorthand — equivalent to `{}`. Otherwise
2369
+ * each branch can carry its own `where` / `orderBy` / `limit` /
2370
+ * `offset` / nested `with`, so `users.with({ posts: { limit: 10,
2371
+ * orderBy: 'createdAt' } })` works without dropping into a sub-query
2372
+ * builder.
2373
+ *
2374
+ * Untyped at runtime: the QueryDescriptor is the boundary between the
2375
+ * typed builder API and the dialect-agnostic JoinCompiler. End-user
2376
+ * types live one layer up in the typed `.with()` method on `Query`.
2377
+ */
2378
+ declare interface EagerLoadSpec_2 {
2379
+ readonly where?: Predicate_2;
2380
+ readonly orderBy?: ReadonlyArray<OrderClause_2>;
2381
+ /**
2382
+ * Per-parent cap on the relation. `{ limit: 10 }` returns up to 10
2383
+ * rows FOR EACH parent — same semantics as Drizzle / Prisma /
2384
+ * Hibernate. This matches what users actually mean when they write
2385
+ * "the 10 latest posts" inside `users.with({ posts: { limit: 10 } })`.
2386
+ *
2387
+ * The SQL stores express this natively as a `LIMIT` clause in a
2388
+ * correlated subquery (postgres / sqlite / mysql / mariadb) or
2389
+ * via `TOP N` inside `FOR JSON PATH` (mssql) when the JSON-agg path
2390
+ * is active. The walker fallback post-buckets the batch fetch and
2391
+ * slices each bucket.
2392
+ */
2393
+ readonly limit?: number;
2394
+ /** Per-parent offset. Only meaningful with `limit`. */
2395
+ readonly offset?: number;
2396
+ readonly with?: WithSpec_2;
2397
+ /**
2398
+ * Project the JUNCTION row's own columns into a `manyToMany` eager result.
2399
+ *
2400
+ * `true` takes every junction column; an array takes only those named. The
2401
+ * values land under `_junction` on each target row:
2402
+ *
2403
+ * projects.with({ teams: { junction: ['role', 'addedAt'] } })
2404
+ * // → team._junction.addedAt
2405
+ *
2406
+ * Nested rather than merged onto the target row, deliberately: a junction and
2407
+ * its target routinely share column names (`createdAt` is the obvious one),
2408
+ * and merging would silently overwrite real target data with membership data.
2409
+ * A name collision that corrupts a row is worse than one extra level.
2410
+ *
2411
+ * Why this exists: junction tables carrying meaningful columns — a role, a
2412
+ * joined-at stamp, a permission tier — are the rule rather than the
2413
+ * exception, and without this an eager load could FILTER on them
2414
+ * (`onJunction`) but never return them, so any relation with real membership
2415
+ * data had to stay a hand-written join. The junction rows were already being
2416
+ * fetched in full to resolve the target ids; this stops throwing them away.
2417
+ */
2418
+ readonly junction?: true | ReadonlyArray<string>;
2419
+ /**
2420
+ * Filter a `manyToMany` eager branch on the THROUGH-table's columns.
2421
+ * The predicate is evaluated against the junction row, not the target
2422
+ * row — e.g. for `users.with({ organizations: { onJunction: eq('role',
2423
+ * 'admin') } })` it keeps only the memberships where `role = 'admin'`,
2424
+ * so the loaded `organizations` are exactly the orgs the user is an
2425
+ * admin of.
2426
+ *
2427
+ * Compiles into the correlated subquery's WHERE alongside the
2428
+ * source-key correlation, qualified to the junction-table alias.
2429
+ * Ignored on `one` / `many` branches (no junction table exists).
2430
+ */
2431
+ readonly onJunction?: Predicate_2;
2432
+ }
1149
2433
 
1150
2434
  export declare const emitAddIndexMysql: (op: Extract<MigrationOperation, {
1151
2435
  kind: "add-index";
@@ -1262,6 +2546,35 @@ declare type EmptyMerge = unknown;
1262
2546
  */
1263
2547
  export declare const engineFromVersion: (version: string) => MysqlEngine;
1264
2548
 
2549
+ /** The deploy ledger (`_voltro_deploys`) — same bootstrap, same reason: it has to exist before the first plan. */
2550
+ export declare const ensureDeployLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId) => Effect.Effect<void, SqlError_2>;
2551
+
2552
+ /**
2553
+ * Create `_voltro_migration_ops` if it isn't there yet.
2554
+ *
2555
+ * Rendered from the SAME per-dialect create-table emitters the planner uses for
2556
+ * user tables, off the declared descriptor — so the ledger cannot acquire a
2557
+ * hand-written second DDL path that disagrees with the first. All four emitters
2558
+ * are already idempotent (`IF NOT EXISTS` / an `IF OBJECT_ID … IS NULL` guard on
2559
+ * mssql), so this is safe to call on every apply.
2560
+ *
2561
+ * The ledger is bootstrapped here rather than registered with the CLI's
2562
+ * framework-table set because it has to exist before the FIRST plan — and the
2563
+ * first plan is the one that creates the framework tables.
2564
+ */
2565
+ export declare const ensureLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId) => Effect.Effect<void, SqlError_2>;
2566
+
2567
+ /** Exported for the same reason as {@link enumTypeBlocks}, and it MUST run
2568
+ * before it — see that function's note on ordering. */
2569
+ export declare const enumRenameBlocks: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => string;
2570
+
2571
+ /** Exported for the APPLIER: `voltro db apply` builds its DDL from operations,
2572
+ * not from this emitter, and an operation only names a column's type by tag.
2573
+ * A create-table referencing a `dbEnum` therefore ran before anything had
2574
+ * created the type — `type "x" does not exist`, on a fresh database, for every
2575
+ * app declaring one. One emitter, two callers. */
2576
+ export declare const enumTypeBlocks: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => string;
2577
+
1265
2578
  /**
1266
2579
  * `EXISTS (SELECT 1 FROM ... WHERE ...)` / `NOT EXISTS (...)`.
1267
2580
  * Mirrors `SubqueryInPredicate` but doesn't bind to a specific
@@ -1274,6 +2587,38 @@ declare interface ExistsPredicate {
1274
2587
  readonly subquery: QueryDescriptor;
1275
2588
  }
1276
2589
 
2590
+ /**
2591
+ * `EXISTS (SELECT 1 FROM ... WHERE ...)` / `NOT EXISTS (...)`.
2592
+ * Mirrors `SubqueryInPredicate` but doesn't bind to a specific
2593
+ * outer column — the predicate is "the sub-query produces at least
2594
+ * one row" (or doesn't). Non-correlated in v1; the sub-query's
2595
+ * WHERE clause references only its own table.
2596
+ */
2597
+ declare interface ExistsPredicate_2 {
2598
+ readonly op: 'exists' | 'not-exists';
2599
+ readonly subquery: QueryDescriptor_2;
2600
+ }
2601
+
2602
+ /**
2603
+ * The postgres extensions the declared tables need, as one DDL block — or
2604
+ * `undefined`. `IF NOT EXISTS`, so every apply can run it. Shared by the
2605
+ * bring-up emitter and the plan applier; a block only one of them emits is a
2606
+ * boot failure waiting for the right database (see the reactive-trigger scar).
2607
+ */
2608
+ export declare const extensionBlocks: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => string | undefined;
2609
+
2610
+ /** An undo that could not complete: the row stays, with the report, until an operator forgets it. */
2611
+ export declare const failDeploy: (sql: SqlClient.SqlClient, id: string, report: string) => Effect.Effect<void, unknown>;
2612
+
2613
+ /**
2614
+ * Evaluate the post-conditions a body registered. The descriptions of the
2615
+ * ones that did not hold, in registration order.
2616
+ */
2617
+ export declare const failedExpectations: (expectations: ReadonlyArray<{
2618
+ readonly description: string;
2619
+ readonly check: () => Promise<boolean> | Effect.Effect<boolean, unknown, never>;
2620
+ }>) => Effect.Effect<ReadonlyArray<string>, unknown>;
2621
+
1277
2622
  /**
1278
2623
  * Filename pattern the discovery walker matches: timestamp-prefixed
1279
2624
  * `.ts` files in any depth under the project's `migrations/` dir.
@@ -1301,26 +2646,72 @@ export declare interface FileMigration {
1301
2646
  /** Human description that surfaces in `voltro db plans` + the
1302
2647
  * history timeline. Keep it short — what the migration does. */
1303
2648
  readonly description: string;
1304
- /** The forward body. Runs inside a transaction on dialects that
1305
- * support transactional DDL (postgres / mssql / sqlite); runs as
1306
- * separate implicit-commit statements on mysql / mariadb. */
2649
+ /**
2650
+ * Run `up` and the ledger row that records it inside ONE transaction, on
2651
+ * every dialect. **The default.** A body that fails or crashes leaves
2652
+ * nothing behind — not half its rows, and not a ledger row for work that did
2653
+ * not finish. `transactional: false` is one unit per statement with the
2654
+ * ledger row written after the body: a crash in the middle leaves the rows
2655
+ * already written AND no record, so the body runs again on the next boot
2656
+ * over half-converted data, and has to be written to survive that.
2657
+ *
2658
+ * The decision is per MIGRATION, not per dialect, because the engine
2659
+ * property that matters is about DDL: MySQL, MariaDB and Turso cannot hold
2660
+ * DDL inside a transaction — the MySQL family commits everything before it
2661
+ * on every `ALTER`. A transactional migration there is data only. The
2662
+ * runner MEASURES this rather than trusting it: a body that broke its own
2663
+ * transaction is recorded, so it does not run twice, and the run then fails
2664
+ * with the migration's name and the fix (`transactional: false`, or the
2665
+ * schema change through `ctx.schema` in a migration of its own).
2666
+ */
2667
+ readonly transactional?: boolean;
2668
+ /** The forward body. */
1307
2669
  readonly up: (ctx: FileMigrationContext) => Promise<void> | Effect.Effect<void, unknown, never>;
1308
- /** The reverse body — required. The runner uses this for
1309
- * `voltro db rollback <id>`. Non-destructive inverses don't
1310
- * bring back data dropped in `up`; design accordingly (see
1311
- * Rollback docs). */
2670
+ /**
2671
+ * The reverse body — required, so that the decision is written down. A
2672
+ * migration that repairs a shape has no inverse: say so with
2673
+ * `down: irreversible('…')`, and `voltro db rollback-file` refuses with
2674
+ * that reason instead of running a body that pretends.
2675
+ */
1312
2676
  readonly down: (ctx: FileMigrationContext) => Promise<void> | Effect.Effect<void, unknown, never>;
1313
2677
  }
1314
2678
 
1315
2679
  /**
1316
2680
  * The runtime context a file-based migration's `up` / `down` body
1317
2681
  * receives.
2682
+ *
2683
+ * `store` is the same typed data store the application runs on — the same
2684
+ * column codecs, the same dialect, the same query builder — bound to the
2685
+ * database this migration is running against. A row read through it has its
2686
+ * JSON parsed and its timestamps as `Date`s on every dialect; a row written
2687
+ * through it is encoded the same way. `sql` stays beside it for what the
2688
+ * builder does not express.
1318
2689
  */
1319
2690
  export declare interface FileMigrationContext {
1320
- /** The live `@effect/sql` SqlClient — issue raw DDL via
1321
- * `sql.unsafe(...)`, tagged-template fragments via `sql\`...\``,
1322
- * per-dialect dispatch via `sql.onDialectOrElse({...})`. */
1323
- readonly sql: SqlClient.SqlClient;
2691
+ /**
2692
+ * The escape hatch, and it asks for a reason. `raw('why')` returns the
2693
+ * `@effect/sql` client — `sql.unsafe(...)`, `sql\`...\``,
2694
+ * `sql.onDialectOrElse({...})` — for the one thing neither `store` nor
2695
+ * `schema` expresses. The reason is required (an empty one throws), it is
2696
+ * what `voltro doctor` lists beside every migration that reaches for raw
2697
+ * SQL, and it is the sentence a reviewer reads first. A migration written
2698
+ * against the raw client is bound to the dialect it was written on; the
2699
+ * rest of this context is not.
2700
+ */
2701
+ readonly raw: (reason: string) => SqlClient.SqlClient;
2702
+ /**
2703
+ * The application's data store over this database. Typed reads through a
2704
+ * descriptor (`store.query(database.vacations.descriptor)`) are checked
2705
+ * against the CURRENT declaration at compile time — a column renamed since
2706
+ * this migration was written fails the build, not the deploy. Reads by
2707
+ * table name (`store.query({ table: 'vacations', … })`) go to the database
2708
+ * as it is, decoded through whatever the current declaration says about
2709
+ * the columns it still knows; a column it no longer declares passes
2710
+ * through as the driver returned it.
2711
+ */
2712
+ readonly store: DataStore;
2713
+ /** The dialect this migration is running against. */
2714
+ readonly dialect: DialectId;
1324
2715
  /**
1325
2716
  * Lightweight log surface scoped to the migration. Lands in
1326
2717
  * `voltro logs --filter 'migrations:file:<id>'` for any apply
@@ -1333,6 +2724,71 @@ export declare interface FileMigrationContext {
1333
2724
  /** ISO-8601 timestamp when this migration started (deterministic
1334
2725
  * across the up/down pair via the applied_at column). */
1335
2726
  readonly appliedAt: string;
2727
+ /** Schema questions and dialect-aware schema operations. */
2728
+ readonly schema: MigrationSchemaOps;
2729
+ /**
2730
+ * Visit every row of `table` in pages keyed by `id`. `fn` may return a
2731
+ * Promise or an Effect. The table needs an `id` column, which every
2732
+ * `table()` with `id()` has.
2733
+ */
2734
+ readonly each: <R extends Row = Row>(table: string, fn: (row: R) => Promise<void> | Effect.Effect<void, unknown, never>, options?: EachOptions) => Effect.Effect<EachReport, unknown>;
2735
+ /**
2736
+ * Read rows in pages, compute a patch per row, write it back under an
2737
+ * optimistic guard, and repeat until a pass changes nothing.
2738
+ *
2739
+ * The guard is the row's own values: the UPDATE carries `WHERE id = ? AND
2740
+ * <each patched column> = <the value just read>` (plus `options.guard`), so
2741
+ * a row another process changed between the read and the write is not
2742
+ * overwritten — it is counted as contended and read again on the next
2743
+ * pass. That is the guard a migration needs while the previous deploy is
2744
+ * still writing, without a hashing function a dialect may not have.
2745
+ *
2746
+ * `fn` returns the patch, or `undefined` for a row that is already in
2747
+ * shape. A pass in which every row returns `undefined` and nothing was
2748
+ * contended is the stable state that ends the rewrite.
2749
+ */
2750
+ readonly rewrite: <R extends Row = Row>(table: string, fn: (row: R) => Partial<R> | undefined | Promise<Partial<R> | undefined>, options?: RewriteOptions) => Effect.Effect<RewriteReport, unknown>;
2751
+ /**
2752
+ * A post-condition. Registered during `up`, evaluated after it — every one
2753
+ * must hold, or the migration FAILS and is not recorded, so the next run
2754
+ * tries again. This is the "read it back and check" every data migration
2755
+ * ends with, as a contract the runner enforces instead of a pattern each
2756
+ * body repeats.
2757
+ */
2758
+ readonly expect: (description: string, check: () => Promise<boolean> | Effect.Effect<boolean, unknown, never>) => void;
2759
+ /**
2760
+ * A precondition. When `condition` is false the body stops HERE, the
2761
+ * migration is recorded as applied with `skipped: <reason>` in its notes,
2762
+ * and it never runs again on this database — the same answer Liquibase's
2763
+ * `MARK_RAN` gives. For a migration that must not be recorded when its
2764
+ * precondition fails, fail it instead.
2765
+ *
2766
+ * yield* ctx.skipUnless(ctx.schema.tableExists('vacations'), 'no vacations table on this database')
2767
+ */
2768
+ readonly skipUnless: (condition: boolean | Promise<boolean> | Effect.Effect<boolean, unknown, never>, reason: string) => Effect.Effect<void, unknown>;
2769
+ /**
2770
+ * Run `work` inside ONE database transaction: the `store` it receives is
2771
+ * the transaction's view, and an Effect it returns is executed on the
2772
+ * transaction's connection, so `sql` statements yielded inside it are part
2773
+ * of the same unit. A migration that is transactional as a whole declares
2774
+ * `transactional: true` instead and gets the ledger row in the same unit.
2775
+ *
2776
+ * What the engine cannot do, this cannot do either: on MySQL and MariaDB a
2777
+ * DDL statement inside the unit commits everything before it. A
2778
+ * transactional scope is for data.
2779
+ */
2780
+ readonly transactional: <T>(work: (tx: {
2781
+ readonly store: DataStore;
2782
+ readonly raw: (reason: string) => SqlClient.SqlClient;
2783
+ }) => Promise<T> | Effect.Effect<T, unknown, SqlClient.SqlClient>) => Effect.Effect<T, unknown>;
2784
+ }
2785
+
2786
+ export declare interface FileMigrationContextInput {
2787
+ readonly sql: SqlClient.SqlClient;
2788
+ readonly store: DataStore;
2789
+ readonly dialect: DialectId;
2790
+ readonly migrationId: string;
2791
+ readonly appliedAt: string;
1336
2792
  }
1337
2793
 
1338
2794
  /**
@@ -1410,6 +2866,25 @@ declare type IdScheme = {
1410
2866
  readonly generate: (tableName: string) => string;
1411
2867
  };
1412
2868
 
2869
+ /**
2870
+ * Fully-resolved scheme stored on the `id` column definition AFTER
2871
+ * `table()` walks the fields. For `typeid`, the prefix is always
2872
+ * populated here — either user-supplied or derived from the table name.
2873
+ */
2874
+ declare type IdScheme_2 = {
2875
+ readonly kind: 'typeid';
2876
+ readonly prefix: string;
2877
+ } | {
2878
+ readonly kind: 'ulid';
2879
+ } | {
2880
+ readonly kind: 'numeric';
2881
+ } | {
2882
+ readonly kind: 'snowflake';
2883
+ } | {
2884
+ readonly kind: 'custom';
2885
+ readonly generate: (tableName: string) => string;
2886
+ };
2887
+
1413
2888
  /**
1414
2889
  * User-facing input to `id()`. Some shapes leave the prefix to be
1415
2890
  * derived from the table name at register-time.
@@ -1436,6 +2911,32 @@ declare type IdSchemeInput = undefined | {
1436
2911
  readonly generate: (tableName: string) => string;
1437
2912
  };
1438
2913
 
2914
+ /**
2915
+ * User-facing input to `id()`. Some shapes leave the prefix to be
2916
+ * derived from the table name at register-time.
2917
+ *
2918
+ * Defaults:
2919
+ * - `undefined` → `{ scheme: 'typeid' }` with derived prefix
2920
+ * - `{ prefix }` → typeid with that prefix
2921
+ * - `{ scheme: 'typeid' }` → derived prefix
2922
+ * - `{ scheme: 'typeid', prefix }` → explicit prefix
2923
+ */
2924
+ declare type IdSchemeInput_2 = undefined | {
2925
+ readonly prefix: string;
2926
+ } | {
2927
+ readonly scheme: 'typeid';
2928
+ readonly prefix?: string;
2929
+ } | {
2930
+ readonly scheme: 'ulid';
2931
+ } | {
2932
+ readonly scheme: 'numeric';
2933
+ } | {
2934
+ readonly scheme: 'snowflake';
2935
+ } | {
2936
+ readonly scheme: 'custom';
2937
+ readonly generate: (tableName: string) => string;
2938
+ };
2939
+
1439
2940
  /**
1440
2941
  * Live tables the planner must NOT drop, parsed from `VOLTRO_DB_IGNORE_TABLES`
1441
2942
  * (comma-separated). The framework already self-excludes its own `_voltro_*`
@@ -1559,8 +3060,35 @@ export declare interface IndexSnapshot {
1559
3060
  readonly expression?: boolean;
1560
3061
  }
1561
3062
 
3063
+ /** What an `insertIgnoreWithOutcome` did, beside the row it hands back. */
3064
+ declare interface InsertIgnoreResult {
3065
+ readonly row: Row_2;
3066
+ /** `'ignored'` means a row already existed for the conflict key; it is the
3067
+ * row returned, untouched. */
3068
+ readonly outcome: 'inserted' | 'ignored';
3069
+ }
3070
+
1562
3071
  export declare const introspectSchema: (sql: SqlClient.SqlClient) => Effect.Effect<SchemaSnapshot, SqlError_2>;
1563
3072
 
3073
+ /**
3074
+ * A `down` that says there is no inverse — with the reason, which is what a
3075
+ * rollback prints. A data migration that repairs a broken shape HAS no
3076
+ * inverse (its inverse would be the defect), and a `down` that only logs
3077
+ * "nothing to undo" and returns success lets `voltro db rollback-file`
3078
+ * delete the ledger row for a change that is still in the database.
3079
+ */
3080
+ export declare const irreversible: (reason: string) => FileMigration["down"];
3081
+
3082
+ export declare class IrreversibleMigrationError extends Error {
3083
+ readonly migrationId: string;
3084
+ readonly reason: string;
3085
+ readonly _tag = "IrreversibleMigrationError";
3086
+ constructor(migrationId: string, reason: string);
3087
+ }
3088
+
3089
+ /** The reason a `down` was declared irreversible with, or `undefined` for an ordinary body. */
3090
+ export declare const irreversibleReason: (down: FileMigration["down"]) => string | undefined;
3091
+
1564
3092
  export declare const isAlreadySatisfied: (err: unknown, rules: ReadonlyArray<AlreadySatisfied>) => boolean;
1565
3093
 
1566
3094
  /** Type-guard for the discovery walker. */
@@ -1601,8 +3129,31 @@ declare interface JsonIndexPath {
1601
3129
 
1602
3130
  declare type LeafOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'startsWith' | 'arrayContains' | 'arrayOverlaps' | 'arrayHas' | 'isNull' | 'isNotNull' | 'spatial';
1603
3131
 
3132
+ declare type LeafOperator_2 = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'startsWith' | 'arrayContains' | 'arrayOverlaps' | 'arrayHas' | 'isNull' | 'isNotNull' | 'spatial';
3133
+
3134
+ /**
3135
+ * Build the context for one migration body. Returns the context and the
3136
+ * post-conditions it collects, so the runner can evaluate them after `up`.
3137
+ */
3138
+ export declare const makeFileMigrationContext: (input: FileMigrationContextInput) => {
3139
+ ctx: FileMigrationContext;
3140
+ expectations: {
3141
+ readonly description: string;
3142
+ readonly check: () => Promise<boolean> | Effect.Effect<boolean, unknown, never>;
3143
+ }[];
3144
+ };
3145
+
1604
3146
  export declare const mapPgType: (dataType: string, udtName?: string) => ColumnType;
1605
3147
 
3148
+ /**
3149
+ * A whole live snapshot as DECLARATIONS, each table's references resolved
3150
+ * against the others — what the planner needs on its declared side to bring a
3151
+ * database back to the shape this snapshot describes. Used by the deploy
3152
+ * compensation: the pre-deploy snapshot is the target, the current live schema
3153
+ * the source, and the plan between them is the undo.
3154
+ */
3155
+ export declare const materialiseSnapshot: (snap: SchemaSnapshot) => ReadonlyArray<TableLike>;
3156
+
1606
3157
  declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = Mixins extends readonly [] ? EmptyMerge : Mixins extends readonly [infer Head, ...infer Rest] ? (Head extends MixinDefinition<infer F> ? F : EmptyMerge) & (Rest extends ReadonlyArray<AnyMixin> ? MergeMixinFields<Rest> : EmptyMerge) : EmptyMerge;
1607
3158
 
1608
3159
  /**
@@ -1616,20 +3167,51 @@ declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = Mixins e
1616
3167
  * export default migration({
1617
3168
  * id: '20260415_120000_split_address_out',
1618
3169
  * description: 'Move users.address* into a separate addresses table.',
1619
- * up: async ({ sql, log }) => {
1620
- * await sql.unsafe(`CREATE TABLE addresses ( ... )`)
1621
- * await sql.unsafe(`INSERT INTO addresses (...) SELECT (...) FROM users`)
1622
- * await sql.unsafe(`ALTER TABLE users DROP COLUMN addressStreet`)
1623
- * log.info('migrated address fields to new table')
1624
- * },
1625
- * down: async ({ sql }) => {
1626
- * await sql.unsafe(`ALTER TABLE users ADD COLUMN addressStreet text`)
1627
- * // ... etc — see file-based docs for the full pattern
1628
- * },
3170
+ * up: ({ sql, store, log }) => Effect.gen(function* () {
3171
+ * yield* sql.unsafe(`CREATE TABLE addresses ( ... )`)
3172
+ * yield* sql.unsafe(`INSERT INTO addresses (...) SELECT (...) FROM users`)
3173
+ * yield* sql.unsafe(`ALTER TABLE users DROP COLUMN addressStreet`)
3174
+ * yield* log.info('migrated address fields to new table')
3175
+ * }),
3176
+ * down: irreversible('the address rows have no column to go back to'),
1629
3177
  * })
1630
3178
  */
1631
3179
  export declare const migration: (spec: FileMigration) => FileMigration;
1632
3180
 
3181
+ export declare const MIGRATION_ATOMICITY: Readonly<Record<DialectId, MigrationAtomicity>>;
3182
+
3183
+ /**
3184
+ * The lock HOLDS a connection for the whole apply, so the work needs a second
3185
+ * one — a pool of one waits out its acquire timeout and reports a connection
3186
+ * problem that is really a sizing one. Said here, at the failure, in the words
3187
+ * an operator behind a pooler can act on.
3188
+ */
3189
+ export declare const MIGRATION_NEEDS_TWO_CONNECTIONS: string;
3190
+
3191
+ /**
3192
+ * How much of a plan a dialect can apply atomically.
3193
+ *
3194
+ * - `plan-transactional` — the applier wraps the plan in one transaction; a
3195
+ * mid-apply failure rolls the WHOLE plan back and there is nothing to resume.
3196
+ * - `per-operation` — each statement is its own unit; a mid-apply failure
3197
+ * leaves earlier operations applied, so the resume ledger carries the run.
3198
+ *
3199
+ * Keyed by `DialectId`, not by the four driver tokens, so `mariadb` and `turso`
3200
+ * are stated rather than inferred from a sibling. `resumeLedger.test.ts`
3201
+ * ("covers every dialect in the union") asserts every member has an entry.
3202
+ */
3203
+ export declare type MigrationAtomicity = 'plan-transactional' | 'per-operation';
3204
+
3205
+ export declare const migrationAtomicityFor: (dialect: DialectId) => MigrationAtomicity;
3206
+
3207
+ /** One or more `ctx.expect` post-conditions did not hold after `up`. */
3208
+ export declare class MigrationExpectationFailed extends Error {
3209
+ readonly migrationId: string;
3210
+ readonly failed: ReadonlyArray<string>;
3211
+ readonly _tag = "MigrationExpectationFailed";
3212
+ constructor(migrationId: string, failed: ReadonlyArray<string>);
3213
+ }
3214
+
1633
3215
  /**
1634
3216
  * The postgres advisory-lock key for a schema. Default schema (undefined /
1635
3217
  * `'public'`) → the EXACT legacy constant (rolling-deploy pin, see above).
@@ -1739,6 +3321,19 @@ export declare type MigrationOperation = {
1739
3321
  readonly kind: 'drop-unique';
1740
3322
  readonly table: string;
1741
3323
  readonly column: string;
3324
+ } | {
3325
+ readonly kind: 'add-enum-value';
3326
+ readonly table: string;
3327
+ readonly column: string;
3328
+ /** The postgres type the value is added to — several columns may share it. */
3329
+ /**
3330
+ * The postgres TYPE name. ABSENT on every other dialect, and that is a
3331
+ * fact about the database rather than a missing field: the mysql family's
3332
+ * `ENUM(...)` is anonymous and mssql/sqlite render a CHECK, so there is no
3333
+ * named object to alter. The applier redeclares the column there instead.
3334
+ */
3335
+ readonly enumName?: string;
3336
+ readonly value: string;
1742
3337
  } | {
1743
3338
  readonly kind: 'add-unique-composite';
1744
3339
  readonly table: string;
@@ -1755,413 +3350,884 @@ export declare type MigrationOperation = {
1755
3350
  readonly kind: 'add-foreign-key';
1756
3351
  readonly table: string;
1757
3352
  readonly column: string;
1758
- readonly targetTable: string;
1759
- readonly targetColumn: string;
1760
- readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
1761
- readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
1762
- readonly orphanPolicy?: 'null' | 'delete';
1763
- } | {
1764
- readonly kind: 'drop-foreign-key';
1765
- readonly table: string;
3353
+ readonly targetTable: string;
3354
+ readonly targetColumn: string;
3355
+ readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
3356
+ readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
3357
+ readonly orphanPolicy?: 'null' | 'delete';
3358
+ }
3359
+ /** `name` is the live constraint's catalog name when the reader reported
3360
+ * one; absent, the applier falls back to the `<table>_<column>_fkey`
3361
+ * convention it uses when creating. */
3362
+ | {
3363
+ readonly kind: 'drop-foreign-key';
3364
+ readonly table: string;
3365
+ readonly column: string;
3366
+ readonly name?: string;
3367
+ } | {
3368
+ readonly kind: 'add-check';
3369
+ readonly table: string;
3370
+ readonly column: string;
3371
+ readonly values: ReadonlyArray<string>;
3372
+ } | {
3373
+ readonly kind: 'drop-check';
3374
+ readonly table: string;
3375
+ readonly column: string;
3376
+ } | {
3377
+ readonly kind: 'reconcile-json-check';
3378
+ readonly table: string;
3379
+ readonly column: string;
3380
+ readonly validation: 'value' | 'container';
3381
+ readonly previousValidation: 'value' | 'container' | 'unverified' | 'absent';
3382
+ readonly dropConstraints: ReadonlyArray<string>;
3383
+ };
3384
+
3385
+ export declare interface MigrationPlan {
3386
+ readonly operations: ReadonlyArray<PlannedOperation>;
3387
+ /** Pre-state fingerprint (live DB at plan time). */
3388
+ readonly fromFingerprint: string;
3389
+ /** Post-state fingerprint (what the DB will look like after apply). */
3390
+ readonly toFingerprint: string;
3391
+ /**
3392
+ * Where the two fingerprints part when the plan is EMPTY. `fromFingerprint`
3393
+ * hashes the whole live introspection; `toFingerprint` hashes the declaration;
3394
+ * a plan of zero operations says the schemas agree — and the hashes can still
3395
+ * differ. Then this names the tables on which they do:
3396
+ *
3397
+ * `outsideDiff` — live tables the diff never compares: framework-owned
3398
+ * (`cluster_*`, an undeclared `_voltro_*`), soft-drop snapshots, a name in
3399
+ * `VOLTRO_DB_IGNORE_TABLES`, the old name of a rename. In the live hash, in
3400
+ * no declaration.
3401
+ * `diffEquivalent` — tables on both sides the diff planned nothing for whose
3402
+ * live and declared snapshots hash apart anyway: a detail introspection
3403
+ * reports differently from how it was declared.
3404
+ *
3405
+ * Both empty means the hashes agree table for table. A reader who meets two
3406
+ * different hashes over `0 operations` is otherwise left to guess what moved.
3407
+ */
3408
+ readonly fingerprintDivergence: {
3409
+ readonly outsideDiff: ReadonlyArray<string>;
3410
+ readonly diffEquivalent: ReadonlyArray<string>;
3411
+ };
3412
+ /** Count summaries by class — what `voltro db plan` prints. */
3413
+ readonly summary: {
3414
+ readonly safe: number;
3415
+ readonly needsDefault: number;
3416
+ readonly needsBackfill: number;
3417
+ readonly needsRenameAnnotation: number;
3418
+ readonly lossy: number;
3419
+ readonly onlineRequired: number;
3420
+ readonly multiStep: number;
3421
+ readonly blocked: number;
3422
+ };
3423
+ }
3424
+
3425
+ /** The schema operations a file migration's context can ask for by name. */
3426
+ export declare type MigrationSchemaOp = {
3427
+ readonly kind: 'drop-foreign-key';
3428
+ readonly table: string;
3429
+ readonly column: string;
3430
+ readonly name?: string;
3431
+ } | {
3432
+ readonly kind: 'drop-index';
3433
+ readonly table: string;
3434
+ readonly index: string;
3435
+ } | {
3436
+ readonly kind: 'drop-column';
3437
+ readonly table: string;
3438
+ readonly column: string;
3439
+ } | {
3440
+ readonly kind: 'rename-column';
3441
+ readonly table: string;
3442
+ readonly from: string;
3443
+ readonly to: string;
3444
+ };
3445
+
3446
+ /**
3447
+ * Schema questions and schema operations a migration body can ask of the
3448
+ * database it is running against, spelled once for every dialect.
3449
+ *
3450
+ * Every operation is idempotent by INSPECTION, not by error tolerance: it
3451
+ * reads the live schema first and answers `already-absent` /
3452
+ * `already-present` when there is nothing to do. That is the portable
3453
+ * `IF EXISTS` — MySQL has no such clause for a foreign key, and its two
3454
+ * "already gone" error codes are not the kind of thing a migration should
3455
+ * be matching on.
3456
+ *
3457
+ * On SQLite the dropping operations are table rebuilds (the dialect has no
3458
+ * `DROP CONSTRAINT`), built from the live table minus the change — the same
3459
+ * rebuild the planner's applier performs, with foreign-key enforcement off
3460
+ * for its duration and checked before the connection is handed back.
3461
+ */
3462
+ export declare interface MigrationSchemaOps {
3463
+ /** The live schema, read fresh on every call — an operation above may have changed it. */
3464
+ readonly snapshot: () => Effect.Effect<SchemaSnapshot, unknown>;
3465
+ readonly tableExists: (table: string) => Effect.Effect<boolean, unknown>;
3466
+ readonly columnExists: (table: string, column: string) => Effect.Effect<boolean, unknown>;
3467
+ readonly indexExists: (table: string, index: string) => Effect.Effect<boolean, unknown>;
3468
+ /** Whether `table.column` currently carries a FOREIGN KEY. */
3469
+ readonly foreignKeyExists: (table: string, column: string) => Effect.Effect<boolean, unknown>;
3470
+ /** Drop the FOREIGN KEY on `table.column`, under the name the catalog reports for it. */
3471
+ readonly dropForeignKey: (table: string, column: string) => Effect.Effect<SchemaOpOutcome, unknown>;
3472
+ readonly dropIndex: (table: string, index: string) => Effect.Effect<SchemaOpOutcome, unknown>;
3473
+ /** Drop a column — and first the FOREIGN KEY it carries, if any, on the
3474
+ * dialects that refuse to drop a column a constraint still holds. */
3475
+ readonly dropColumn: (table: string, column: string) => Effect.Effect<SchemaOpOutcome, unknown>;
3476
+ /** Rename a column. `already-present` when `to` exists and `from` does not:
3477
+ * a re-run after a crash that landed the rename. */
3478
+ readonly renameColumn: (table: string, from: string, to: string) => Effect.Effect<SchemaOpOutcome, unknown>;
3479
+ /** Create a table from a declaration. `already-present` when a table of that name exists. */
3480
+ readonly createTable: (table: TableLike) => Effect.Effect<SchemaOpOutcome, unknown>;
3481
+ /** Drop a table and everything in it. Data loss is the caller's explicit intent here. */
3482
+ readonly dropTable: (table: string) => Effect.Effect<SchemaOpOutcome, unknown>;
3483
+ /** Rename a table. `already-present` when `to` exists and `from` does not. */
3484
+ readonly renameTable: (from: string, to: string) => Effect.Effect<SchemaOpOutcome, unknown>;
3485
+ /**
3486
+ * Add a column. A NOT NULL column without a default cannot be added to a
3487
+ * table that has rows on any dialect: add it nullable or with a default,
3488
+ * fill it (`rewrite`), then `alterColumn(table, name, { nullable: false })`.
3489
+ */
3490
+ readonly addColumn: (table: string, name: string, column: ColumnInput) => Effect.Effect<SchemaOpOutcome, unknown>;
3491
+ /**
3492
+ * Change a column's nullability and/or default. `default: null` removes the
3493
+ * default; a type change goes through `evolve`, where the planner decides
3494
+ * whether it is lossy.
3495
+ */
3496
+ readonly alterColumn: (table: string, name: string, change: {
3497
+ readonly nullable?: boolean;
3498
+ readonly default?: unknown;
3499
+ }) => Effect.Effect<SchemaOpOutcome, unknown>;
3500
+ /** Add an index over `columns`; the name defaults to `<table>_<cols>_idx`. */
3501
+ readonly addIndex: (table: string, columns: ReadonlyArray<string>, options?: {
3502
+ readonly unique?: boolean;
3503
+ readonly name?: string;
3504
+ }) => Effect.Effect<SchemaOpOutcome, unknown>;
3505
+ /** Add a FOREIGN KEY from `table.column` to `target.table`'s `target.column` (default `id`). */
3506
+ readonly addForeignKey: (table: string, column: string, target: {
3507
+ readonly table: string;
3508
+ readonly column?: string;
3509
+ }, options?: {
3510
+ readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
3511
+ readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
3512
+ }) => Effect.Effect<SchemaOpOutcome, unknown>;
3513
+ /** Remove every row. `DELETE FROM` where the dialect has no TRUNCATE inside a unit. */
3514
+ readonly truncate: (table: string) => Effect.Effect<void, unknown>;
3515
+ /**
3516
+ * The Voltro way to change a table's shape in a migration: DECLARE the
3517
+ * table as it should be from here on, and the same planner that drives
3518
+ * `voltro db apply` diffs it against the live table and applies the
3519
+ * difference — add, drop, rename, retype, index, constraint — with the same
3520
+ * per-dialect statements, the same three-step backfills, and the same
3521
+ * convergence proof (an `evolve` whose re-plan is not empty fails). A drop
3522
+ * is allowed without a `dropped()` marker: calling `evolve` from a
3523
+ * migration IS the explicit intent the marker stands for elsewhere.
3524
+ */
3525
+ readonly evolve: (table: TableLike) => Effect.Effect<{
3526
+ readonly applied: number;
3527
+ }, unknown>;
3528
+ }
3529
+
3530
+ /**
3531
+ * The signal `ctx.skipUnless` raises. The runner treats it as an OUTCOME,
3532
+ * not a failure: the migration is recorded with the reason and never runs
3533
+ * again on this database.
3534
+ */
3535
+ export declare class MigrationSkipped {
3536
+ readonly reason: string;
3537
+ readonly _tag = "MigrationSkipped";
3538
+ constructor(reason: string);
3539
+ }
3540
+
3541
+ declare interface MixinDefinition<F extends Record<string, ColumnDefinition<unknown>>> {
3542
+ /**
3543
+ * Stable dedup key. Two mixins with the same `id` reaching a table via
3544
+ * different paths (direct + transitive) are materialised once. Convention:
3545
+ * `<pluginNamespace>/<mixinName>` — e.g. `voltro/audit`, `voltro/tenant`,
3546
+ * `acmeBilling/invoiceable`.
3547
+ */
3548
+ readonly id?: string;
3549
+ readonly fields: F;
3550
+ /**
3551
+ * Other mixins this one needs in place. Resolved depth-first before the
3552
+ * mixin itself is applied, so its `beforeInsert` / `defaultWhere` can rely
3553
+ * on the columns added by its dependencies.
3554
+ */
3555
+ readonly requires?: ReadonlyArray<AnyMixin>;
3556
+ /**
3557
+ * Indexes the mixin contributes when applied to a table. Used by the
3558
+ * built-in `tenant()` mixin to auto-index `tenantId` without
3559
+ * resurrecting a column-level `.index()` modifier (which we
3560
+ * deliberately collapsed into the single table-level surface). The
3561
+ * table-builder merges these into its `appliedIndexes` and
3562
+ * auto-names anonymous entries per-table.
3563
+ */
3564
+ readonly indexes?: ReadonlyArray<MixinIndex>;
3565
+ readonly behaviors?: {
3566
+ readonly beforeInsert?: (...args: unknown[]) => unknown;
3567
+ readonly beforeUpdate?: (...args: unknown[]) => unknown;
3568
+ readonly onDelete?: (...args: unknown[]) => unknown;
3569
+ readonly defaultWhere?: (...args: unknown[]) => unknown;
3570
+ };
3571
+ readonly policies?: ReadonlyArray<{
3572
+ readonly name: string;
3573
+ readonly predicate: (...args: unknown[]) => boolean;
3574
+ }>;
3575
+ }
3576
+
3577
+ /**
3578
+ * Index contribution from a mixin. The fields are column names that
3579
+ * MUST exist either on the host table or on the mixin's own `fields`
3580
+ * map. The optional `name` lets the mixin pin the literal index
3581
+ * identifier; omitted, the table-builder auto-generates
3582
+ * `<tableName>_<col1>_<col2>_idx` so the same mixin applied to
3583
+ * different tables produces distinct DDL names.
3584
+ */
3585
+ declare interface MixinIndex {
3586
+ readonly fields: ReadonlyArray<string>;
3587
+ readonly name?: string;
3588
+ /**
3589
+ * Index access method. Default `'btree'`. The `vectorEmbedding()` mixin
3590
+ * sets `kind: 'hnsw'` so the vector column it contributes also gets its
3591
+ * ANN index — without resurrecting a column-level `.index()` modifier.
3592
+ * The table-builder forwards `kind` + `kindOptions` into the
3593
+ * `TableIndex` it materialises from this contribution.
3594
+ */
3595
+ readonly kind?: IndexKind;
3596
+ /** Per-kind tuning (e.g. `kindOptions.hnsw.{m, efConstruction, distance}`). */
3597
+ readonly kindOptions?: IndexKindOptions;
3598
+ }
3599
+
3600
+ /** Which engine a `mysql`-dialect connection is actually talking to. */
3601
+ export declare type MysqlEngine = 'mysql' | 'mariadb';
3602
+
3603
+ /**
3604
+ * How long a prefix a MySQL/MariaDB index key may take on this column.
3605
+ *
3606
+ * `undefined` when the column cannot be found — an introspected-only column
3607
+ * with no live declaration. The caller then emits an unprefixed key, which is
3608
+ * right for every non-text type and only risks "key too long" for an undeclared
3609
+ * text one.
3610
+ *
3611
+ * A second guard, deliberately, and not because the first one is doubted: the
3612
+ * defect above was a renderer losing the column's parameters on the way to a
3613
+ * type decision, and that is a mistake a future path can make again. This one
3614
+ * cannot emit a prefix longer than the column, whatever concluded it needed
3615
+ * one — `min(191, declaredLength)`, which is what the reporter proposed.
3616
+ *
3617
+ * `undefined` = no prefix clause at all (the column is not text-like, or is
3618
+ * unbounded and takes the full 191).
3619
+ */
3620
+ export declare const mysqlIndexPrefixFor: (col: ColumnSnapshot | undefined) => number | undefined;
3621
+
3622
+ /**
3623
+ * Type-narrowing migration marker. Tells the planner that this column
3624
+ * previously had type `from` and is now being narrowed/converted to its
3625
+ * current type. Without `using`, narrowing fails for rows whose value
3626
+ * can't fit the new type. The `using` fragment is the dialect-specific
3627
+ * cast expression (Postgres `... USING col::int`, MSSQL CONVERT/CAST,
3628
+ * MySQL CAST).
3629
+ */
3630
+ declare interface NarrowedFromSpec {
3631
+ readonly from: ColumnType;
3632
+ /**
3633
+ * Raw SQL cast expression spliced into the planner's
3634
+ * `ALTER COLUMN … TYPE … USING <using>` (postgres) / shadow-copy
3635
+ * (`shadow := <using>(old)`). Authored by the developer in the
3636
+ * migration — e.g. `'role::role_enum'`, `'flag::int::text'`. Omit when
3637
+ * the conversion is implicit on the dialect (e.g. `varchar → text`).
3638
+ */
3639
+ readonly using?: string;
3640
+ }
3641
+
3642
+ /**
3643
+ * Type-narrowing migration marker. Tells the planner that this column
3644
+ * previously had type `from` and is now being narrowed/converted to its
3645
+ * current type. Without `using`, narrowing fails for rows whose value
3646
+ * can't fit the new type. The `using` fragment is the dialect-specific
3647
+ * cast expression (Postgres `... USING col::int`, MSSQL CONVERT/CAST,
3648
+ * MySQL CAST).
3649
+ */
3650
+ declare interface NarrowedFromSpec_2 {
3651
+ readonly from: ColumnType_2;
3652
+ /**
3653
+ * Raw SQL cast expression spliced into the planner's
3654
+ * `ALTER COLUMN … TYPE … USING <using>` (postgres) / shadow-copy
3655
+ * (`shadow := <using>(old)`). Authored by the developer in the
3656
+ * migration — e.g. `'role::role_enum'`, `'flag::int::text'`. Omit when
3657
+ * the conversion is implicit on the dialect (e.g. `varchar → text`).
3658
+ */
3659
+ readonly using?: string;
3660
+ }
3661
+
3662
+ /**
3663
+ * Postgres identifier for the trigger function backing a given channel.
3664
+ *
3665
+ * Derived from the channel, NOT a single global name, and that is the whole
3666
+ * point: `CREATE OR REPLACE FUNCTION framework_notify_change()` is one row in
3667
+ * `pg_proc` for the entire database, so two schemas applied with different
3668
+ * channels overwrote each other and the last one won. Everything applied
3669
+ * earlier then emitted on somebody else's channel — silently, because a NOTIFY
3670
+ * nobody listens to is not an error.
3671
+ */
3672
+ export declare const notifyFunctionName: (channel: string) => string;
3673
+
3674
+ /** Negation of an arbitrary sub-predicate → `NOT (...)`. Wraps any
3675
+ * leaf/and/or/subquery subtree (distinct from the per-column `neq` and
3676
+ * the narrow `notInSubquery`/`notExists`). */
3677
+ declare interface NotPredicate {
3678
+ readonly not: Predicate;
3679
+ }
3680
+
3681
+ /** Negation of an arbitrary sub-predicate → `NOT (...)`. Wraps any
3682
+ * leaf/and/or/subquery subtree (distinct from the per-column `neq` and
3683
+ * the narrow `notInSubquery`/`notExists`). */
3684
+ declare interface NotPredicate_2 {
3685
+ readonly not: Predicate_2;
3686
+ }
3687
+
3688
+ /**
3689
+ * DDL fragment for a numeric primary key column. Each dialect has its
3690
+ * own auto-increment idiom — there's no portable shorthand, so we
3691
+ * dispatch per dialect explicitly. The fragment includes type + PK +
3692
+ * auto-increment in one statement; the caller skips the generic
3693
+ * `NOT NULL` / `PRIMARY KEY` additions for numeric ids.
3694
+ */
3695
+ export declare const numericIdSql: (name: string, dialect: DialectId) => string;
3696
+
3697
+ export declare const openDeploy: (sql: SqlClient.SqlClient, row: {
3698
+ readonly id: string;
3699
+ readonly dialect: string;
3700
+ readonly appliedBy: string;
3701
+ readonly baseline: string;
3702
+ }) => Effect.Effect<void, unknown>;
3703
+
3704
+ /**
3705
+ * The seven classes of schema operations the planner recognises. The
3706
+ * default policy for each is encoded by the planner; the applier
3707
+ * obeys it. Keep this enum in lockstep with the AGENTS.md template's
3708
+ * "Schema migrations" section — agents grep for these strings.
3709
+ */
3710
+ export declare type OperationClass =
3711
+ /** Reversible, side-effect-free for existing data. Auto-applied. */
3712
+ 'safe'
3713
+ /** ADD COLUMN + NOT NULL with `.default(value)` declared.
3714
+ * Applied as `ADD COLUMN ... NOT NULL DEFAULT <value>`. */
3715
+ | 'needs-default'
3716
+ /** ADD COLUMN + NOT NULL on a populated table, ADD UNIQUE / FK / CHECK
3717
+ * with existing offenders. Requires a deliberate declaration
3718
+ * (`.backfill()`, `.unique({dedup})`, `reference({orphanPolicy})`).
3719
+ * Applied as a three-step plan (ADD nullable → run backfill → SET
3720
+ * NOT NULL). */
3721
+ | 'needs-backfill'
3722
+ /** Column appears removed + a new one appeared. Looks like DROP+ADD
3723
+ * but is probably a rename. Refuse-to-plan without an explicit
3724
+ * `.renamedFrom('oldName')` marker — silent renames lose data. */
3725
+ | 'needs-rename-annotation'
3726
+ /** DROP COLUMN, DROP TABLE, narrow type with existing offenders.
3727
+ * Refuse-to-plan unless `dropped()` / `.narrowedFrom()` declared
3728
+ * OR `VOLTRO_DESTRUCTIVE_OK=1` set. */
3729
+ | 'lossy'
3730
+ /** Large-table operation that should run online (CREATE INDEX
3731
+ * CONCURRENTLY, batched backfill, shadow-column swap). The applier
3732
+ * auto-rewrites these once the row count crosses the threshold
3733
+ * (`online-after`, default 50k). */
3734
+ | 'online-required'
3735
+ /** Operations the planner can't infer from a diff (table split /
3736
+ * merge, custom data move, type-change with USING). Requires a
3737
+ * file-based migration in `migrations/<id>.ts`. */
3738
+ | 'multi-step';
3739
+
3740
+ /**
3741
+ * The vocabulary of concrete DDL-level operations the planner emits.
3742
+ * Each `MigrationOperation` carries one of these kinds plus the data
3743
+ * the applier needs to execute it.
3744
+ *
3745
+ * `kind` strings are short + lowercase-kebab so they round-trip
3746
+ * cleanly through JSON (the `operations` column of `_voltro_migrations`)
3747
+ * and can be grepped in `voltro logs` output.
3748
+ */
3749
+ export declare type OperationKind = 'create-table' | 'drop-table' | 'add-column' | 'drop-column' | 'rename-column' | 'alter-column-nullability' | 'alter-column-type' | 'alter-column-default' | 'add-index' | 'drop-index' | 'add-unique' | 'drop-unique' | 'add-enum-value' | 'add-unique-composite' | 'drop-unique-composite' | 'add-foreign-key' | 'drop-foreign-key' | 'add-check' | 'drop-check' | 'reconcile-json-check';
3750
+
3751
+ declare interface OrderClause {
3752
+ readonly column: string;
3753
+ readonly direction: 'asc' | 'desc';
3754
+ }
3755
+
3756
+ declare interface OrderClause_2 {
1766
3757
  readonly column: string;
1767
- } | {
1768
- readonly kind: 'add-check';
1769
- readonly table: string;
3758
+ readonly direction: 'asc' | 'desc';
3759
+ }
3760
+
3761
+ declare interface OrPredicate {
3762
+ readonly or: ReadonlyArray<Predicate>;
3763
+ }
3764
+
3765
+ declare interface OrPredicate_2 {
3766
+ readonly or: ReadonlyArray<Predicate_2>;
3767
+ }
3768
+
3769
+ export declare const parseEnumCheck: (clause: string) => {
1770
3770
  readonly column: string;
1771
3771
  readonly values: ReadonlyArray<string>;
1772
- } | {
1773
- readonly kind: 'drop-check';
1774
- readonly table: string;
1775
- readonly column: string;
1776
- } | {
1777
- readonly kind: 'reconcile-json-check';
1778
- readonly table: string;
1779
- readonly column: string;
1780
- readonly validation: 'value' | 'container';
1781
- readonly previousValidation: 'value' | 'container' | 'unverified' | 'absent';
1782
- readonly dropConstraints: ReadonlyArray<string>;
1783
- };
3772
+ } | null;
1784
3773
 
1785
- export declare interface MigrationPlan {
1786
- readonly operations: ReadonlyArray<PlannedOperation>;
1787
- /** Pre-state fingerprint (live DB at plan time). */
1788
- readonly fromFingerprint: string;
1789
- /** Post-state fingerprint (what the DB will look like after apply). */
1790
- readonly toFingerprint: string;
1791
- /**
1792
- * Where the two fingerprints part when the plan is EMPTY. `fromFingerprint`
1793
- * hashes the whole live introspection; `toFingerprint` hashes the declaration;
1794
- * a plan of zero operations says the schemas agree — and the hashes can still
1795
- * differ. Then this names the tables on which they do:
1796
- *
1797
- * `outsideDiff` — live tables the diff never compares: framework-owned
1798
- * (`cluster_*`, an undeclared `_voltro_*`), soft-drop snapshots, a name in
1799
- * `VOLTRO_DB_IGNORE_TABLES`, the old name of a rename. In the live hash, in
1800
- * no declaration.
1801
- * `diffEquivalent` — tables on both sides the diff planned nothing for whose
1802
- * live and declared snapshots hash apart anyway: a detail introspection
1803
- * reports differently from how it was declared.
1804
- *
1805
- * Both empty means the hashes agree table for table. A reader who meets two
1806
- * different hashes over `0 operations` is otherwise left to guess what moved.
1807
- */
1808
- readonly fingerprintDivergence: {
1809
- readonly outsideDiff: ReadonlyArray<string>;
1810
- readonly diffEquivalent: ReadonlyArray<string>;
1811
- };
1812
- /** Count summaries by class — what `voltro db plan` prints. */
1813
- readonly summary: {
1814
- readonly safe: number;
1815
- readonly needsDefault: number;
1816
- readonly needsBackfill: number;
1817
- readonly needsRenameAnnotation: number;
1818
- readonly lossy: number;
1819
- readonly onlineRequired: number;
1820
- readonly multiStep: number;
1821
- readonly blocked: number;
1822
- };
1823
- }
3774
+ /**
3775
+ * The ids of migration files present on disk but NOT yet recorded against THIS
3776
+ * database — i.e. exactly what `runFileBasedMigrations` would execute next.
3777
+ *
3778
+ * Exists so a caller can decide WHETHER to apply before applying. `voltro dev`
3779
+ * uses it to refuse unattended file migrations against a remote database: that
3780
+ * decision needs the list (to name it in the refusal) and must not have run
3781
+ * anything to get it.
3782
+ */
3783
+ export declare const pendingFileMigrationIds: (sql: SqlClient.SqlClient, projectRoot: string) => Effect.Effect<ReadonlyArray<string>, unknown>;
1824
3784
 
1825
- declare interface MixinDefinition<F extends Record<string, ColumnDefinition<unknown>>> {
3785
+ /**
3786
+ * What `runFileBasedMigrations` would execute next, with the one fact a deploy
3787
+ * has to know BEFORE it starts: whether the migration's `down` is declared
3788
+ * `irreversible()`. A deploy that undoes its own work on failure can undo
3789
+ * every pending migration except those, so they are named in the question
3790
+ * the operator answers, not discovered in the report afterwards.
3791
+ */
3792
+ export declare const pendingFileMigrationSummary: (sql: SqlClient.SqlClient, projectRoot: string) => Effect.Effect<ReadonlyArray<{
3793
+ readonly id: string;
3794
+ readonly irreversible: string | undefined;
3795
+ }>, unknown>;
3796
+
3797
+ export declare interface PlanInput {
3798
+ readonly declared: ReadonlyArray<TableLike>;
3799
+ readonly live: SchemaSnapshot;
1826
3800
  /**
1827
- * Stable dedup key. Two mixins with the same `id` reaching a table via
1828
- * different paths (direct + transitive) are materialised once. Convention:
1829
- * `<pluginNamespace>/<mixinName>` — e.g. `voltro/audit`, `voltro/tenant`,
1830
- * `acmeBilling/invoiceable`.
3801
+ * Threshold above which safe DDL ops on a populated table get
3802
+ * promoted to `online-required` (CONCURRENTLY / batched / shadow-
3803
+ * column). Pass undefined to use the framework default (50_000).
1831
3804
  */
1832
- readonly id?: string;
1833
- readonly fields: F;
3805
+ readonly onlineAfter?: number;
1834
3806
  /**
1835
- * Other mixins this one needs in place. Resolved depth-first before the
1836
- * mixin itself is applied, so its `beforeInsert` / `defaultWhere` can rely
1837
- * on the columns added by its dependencies.
3807
+ * The target dialect. Drives storage-collapse type equivalence (sqlite
3808
+ * stores timestamp/json as TEXT, mssql stores json as NVARCHAR) so the plan
3809
+ * doesn't churn phantom alter-column-type ops. Omit → no collapse (postgres
3810
+ * / mysql / mariadb distinguish these natively anyway).
1838
3811
  */
1839
- readonly requires?: ReadonlyArray<AnyMixin>;
3812
+ readonly dialect?: DialectId;
1840
3813
  /**
1841
- * Indexes the mixin contributes when applied to a table. Used by the
1842
- * built-in `tenant()` mixin to auto-index `tenantId` without
1843
- * resurrecting a column-level `.index()` modifier (which we
1844
- * deliberately collapsed into the single table-level surface). The
1845
- * table-builder merges these into its `appliedIndexes` and
1846
- * auto-names anonymous entries per-table.
3814
+ * Live tables to LEAVE ALONE — never planned as a drop even though no
3815
+ * declared entity matches them. The framework already self-excludes its own
3816
+ * runtime tables (`_voltro_*` / `_cloud_*` / `cluster_*`, see
3817
+ * `isFrameworkOwnedLiveTable`); this is the USER escape hatch for their own
3818
+ * unmanaged infra tables (e.g. a Strapi→Voltro `_strapi_id_map`), so the diff
3819
+ * doesn't try to drop a table the migration tooling itself depends on. The
3820
+ * CLI populates it from `VOLTRO_DB_IGNORE_TABLES` (comma-separated names).
1847
3821
  */
1848
- readonly indexes?: ReadonlyArray<MixinIndex>;
1849
- readonly behaviors?: {
1850
- readonly beforeInsert?: (...args: unknown[]) => unknown;
1851
- readonly beforeUpdate?: (...args: unknown[]) => unknown;
1852
- readonly onDelete?: (...args: unknown[]) => unknown;
1853
- readonly defaultWhere?: (...args: unknown[]) => unknown;
3822
+ readonly ignoreTables?: ReadonlyArray<string>;
3823
+ }
3824
+
3825
+ /**
3826
+ * Compute the migration plan between a declared schema (with
3827
+ * annotations) and a live snapshot.
3828
+ *
3829
+ * The output is deterministic: same inputs → same `MigrationPlan`
3830
+ * (modulo op insertion order, which follows table-then-kind sort
3831
+ * order). Lets the applier replay the plan from `_voltro_migration_plans`
3832
+ * without surprises.
3833
+ *
3834
+ * The planner does NOT execute pre-check queries — that's the
3835
+ * applier's job (it needs live SQL access). The classifications here
3836
+ * are based purely on the snapshot shapes + the annotations on
3837
+ * `ColumnDefinition`. The applier may downgrade `safe` → `online-required`
3838
+ * once it knows the row count exceeds the threshold.
3839
+ */
3840
+ export declare const planMigrations: (input: PlanInput) => MigrationPlan;
3841
+
3842
+ /**
3843
+ * One row in a `MigrationPlan` — a concrete operation paired with the
3844
+ * planner's classification + any blocking reason.
3845
+ *
3846
+ * `blocked` is set when the classification refuses-to-plan (lossy
3847
+ * without `dropped()`, rename without `.renamedFrom()`, etc.). The
3848
+ * applier checks `blocked` before executing; the CLI's `voltro db plan`
3849
+ * output highlights it red.
3850
+ *
3851
+ * `backfill` / `dedup` / `orphanPolicy` mirror the user's annotations
3852
+ * straight from `ColumnDefinition` so the applier doesn't need to
3853
+ * re-introspect them.
3854
+ */
3855
+ export declare interface PlannedOperation {
3856
+ readonly op: MigrationOperation;
3857
+ readonly classification: OperationClass;
3858
+ /** Why the op was classified this way (also surfaces in the CLI). */
3859
+ readonly reason?: string;
3860
+ /** Set on lossy / needs-rename / multi-step ops with no resolution. */
3861
+ readonly blocked?: {
3862
+ readonly fix: string;
3863
+ };
3864
+ /** Pre-check estimate the planner ran (row counts, offender count). */
3865
+ readonly estimate?: {
3866
+ readonly affectedRows?: number;
3867
+ readonly offenderCount?: number;
3868
+ };
3869
+ /** Whether the op is atomic for each supported dialect. Postgres,
3870
+ * mssql, sqlite are atomic for all DDL; mysql/mariadb implicit-commit
3871
+ * every statement. */
3872
+ readonly atomicByDialect: {
3873
+ readonly postgres: boolean;
3874
+ readonly mysql: boolean;
3875
+ readonly mariadb: boolean;
3876
+ readonly mssql: boolean;
3877
+ readonly sqlite: boolean;
1854
3878
  };
1855
- readonly policies?: ReadonlyArray<{
1856
- readonly name: string;
1857
- readonly predicate: (...args: unknown[]) => boolean;
1858
- }>;
1859
3879
  }
1860
3880
 
1861
- /**
1862
- * Index contribution from a mixin. The fields are column names that
1863
- * MUST exist either on the host table or on the mixin's own `fields`
1864
- * map. The optional `name` lets the mixin pin the literal index
1865
- * identifier; omitted, the table-builder auto-generates
1866
- * `<tableName>_<col1>_<col2>_idx` so the same mixin applied to
1867
- * different tables produces distinct DDL names.
1868
- */
1869
- declare interface MixinIndex {
1870
- readonly fields: ReadonlyArray<string>;
1871
- readonly name?: string;
3881
+ /** What happens to the referencing row when the target row goes away. */
3882
+ declare type PluginRefOrphanPolicy =
3883
+ /** Delete the referencing row. For a row that only exists to point at it —
3884
+ * a favourite, a pin, a share. */
3885
+ 'delete'
3886
+ /** Null the column, keeping the row. Requires `.nullable()`; declaring it on
3887
+ * a non-nullable column is refused at declaration rather than failing at the
3888
+ * first delete, months later. */
3889
+ | 'null'
3890
+ /** Do nothing. The explicit "I know, and I handle it myself" — different from
3891
+ * omitting the option, which is the same behaviour arrived at by accident. */
3892
+ | 'keep';
3893
+
3894
+ declare type Predicate = PredicateLeaf | AndPredicate | OrPredicate | NotPredicate | SubqueryInPredicate | ExistsPredicate;
3895
+
3896
+ declare type Predicate_2 = PredicateLeaf_2 | AndPredicate_2 | OrPredicate_2 | NotPredicate_2 | SubqueryInPredicate_2 | ExistsPredicate_2;
3897
+
3898
+ declare interface PredicateLeaf {
3899
+ readonly column: string;
3900
+ readonly op: LeafOperator;
3901
+ readonly value?: unknown;
1872
3902
  /**
1873
- * Index access method. Default `'btree'`. The `vectorEmbedding()` mixin
1874
- * sets `kind: 'hnsw'` so the vector column it contributes also gets its
1875
- * ANN index — without resurrecting a column-level `.index()` modifier.
1876
- * The table-builder forwards `kind` + `kindOptions` into the
1877
- * `TableIndex` it materialises from this contribution.
3903
+ * JSON-path segments INTO a `json()` column (object keys / array indices).
3904
+ * When set, the leaf filters on the extracted value, not the whole column —
3905
+ * built via {@link jsonField}. The sql compiler lowers it to the dialect's
3906
+ * json-extract expression; the reactive matcher treats it as non-indexable
3907
+ * (rides the unindexed bucket → re-checked on any change to the table, like
3908
+ * `contains`). Absent for a normal column leaf.
1878
3909
  */
1879
- readonly kind?: IndexKind;
1880
- /** Per-kind tuning (e.g. `kindOptions.hnsw.{m, efConstruction, distance}`). */
1881
- readonly kindOptions?: IndexKindOptions;
3910
+ readonly path?: ReadonlyArray<string | number>;
1882
3911
  }
1883
3912
 
1884
- /** Which engine a `mysql`-dialect connection is actually talking to. */
1885
- export declare type MysqlEngine = 'mysql' | 'mariadb';
3913
+ declare interface PredicateLeaf_2 {
3914
+ readonly column: string;
3915
+ readonly op: LeafOperator_2;
3916
+ readonly value?: unknown;
3917
+ /**
3918
+ * JSON-path segments INTO a `json()` column (object keys / array indices).
3919
+ * When set, the leaf filters on the extracted value, not the whole column —
3920
+ * built via {@link jsonField}. The sql compiler lowers it to the dialect's
3921
+ * json-extract expression; the reactive matcher treats it as non-indexable
3922
+ * (rides the unindexed bucket → re-checked on any change to the table, like
3923
+ * `contains`). Absent for a normal column leaf.
3924
+ */
3925
+ readonly path?: ReadonlyArray<string | number>;
3926
+ }
1886
3927
 
1887
3928
  /**
1888
- * How long a prefix a MySQL/MariaDB index key may take on this column.
3929
+ * Imperative `applyNamespacedSchema` against a fresh SqlClient — the
3930
+ * entry point the CLI / runtime use to provision a tenant's namespace
3931
+ * (eager at migrate time, or lazily on first use).
1889
3932
  *
1890
- * `undefined` when the column cannot be found — an introspected-only column
1891
- * with no live declaration. The caller then emits an unprefixed key, which is
1892
- * right for every non-text type and only risks "key too long" for an undeclared
1893
- * text one.
3933
+ * **This is where `lifecycle: 'onTenantCreate'` seeds fire**, after the DDL
3934
+ * lands and before this resolves — so a caller that awaits provisioning gets
3935
+ * a tenant whose tables AND reference data exist, or an error. It was the
3936
+ * missing half: the lifecycle validated, registered, and never ran, so every
3937
+ * tenant came up empty and nothing said so.
1894
3938
  *
1895
- * A second guard, deliberately, and not because the first one is doubted: the
1896
- * defect above was a renderer losing the column's parameters on the way to a
1897
- * type decision, and that is a mistake a future path can make again. This one
1898
- * cannot emit a prefix longer than the column, whatever concluded it needed
1899
- * one — `min(191, declaredLength)`, which is what the reporter proposed.
3939
+ * The seeds run through `setTenantCreateSeedHook` — the hook itself IS
3940
+ * installed at boot (`seedRunner.ts`); with no seeds declared this costs one
3941
+ * null check. A seed failure REJECTS — see `fireTenantCreateSeeds` for why,
3942
+ * and note that re-calling this function is safe (the DDL is `IF NOT EXISTS`
3943
+ * and seed steps are idempotent by contract).
1900
3944
  *
1901
- * `undefined` = no prefix clause at all (the column is not text-like, or is
1902
- * unbounded and takes the full 191).
3945
+ * Wired by BOTH boot paths through `makeTenantProvisioner` (@voltro/cli):
3946
+ * under `tenantIsolation: 'namespace'` on a SQL dialect, every namespace store
3947
+ * view provisions lazily on first use — memoised per namespace per process.
3948
+ * Eager provisioning is the APP's move (call this from a seed/startup over its
3949
+ * own tenant table); the framework has no tenant registry to enumerate.
1903
3950
  */
1904
- export declare const mysqlIndexPrefixFor: (col: ColumnSnapshot | undefined) => number | undefined;
3951
+ export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>, namespace: string, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
1905
3952
 
1906
3953
  /**
1907
- * Type-narrowing migration marker. Tells the planner that this column
1908
- * previously had type `from` and is now being narrowed/converted to its
1909
- * current type. Without `using`, narrowing fails for rows whose value
1910
- * can't fit the new type. The `using` fragment is the dialect-specific
1911
- * cast expression (Postgres `... USING col::int`, MSSQL CONVERT/CAST,
1912
- * MySQL CAST).
3954
+ * The runtime-facing materialized query — a plain bag of strings the store and
3955
+ * the matcher consume.
3956
+ *
3957
+ * `R` is a PHANTOM row type. It exists so the row shape the typed builder
3958
+ * already knows survives the trip through `.descriptor` into `store.query()`,
3959
+ * which is where it used to be thrown away: `store.query(database.notes...)`
3960
+ * returned `Row` (`Record<string, unknown>`), so every field read needed a
3961
+ * hand-written cast. One downstream app wrote 2,032 of them. The type was
3962
+ * always available — it was just dropped at this boundary.
3963
+ *
3964
+ * It is an OPTIONAL field, never set at runtime. That matters for two reasons:
3965
+ * a plain object literal still satisfies `QueryDescriptor` (dispatcher, matcher
3966
+ * and store code build and forward these structurally), and `R` stays
3967
+ * covariant, so a `QueryDescriptor<Note>` is still assignable to the bare
3968
+ * `QueryDescriptor` that generic infrastructure passes around. Defaulting to
3969
+ * `Row` keeps every existing annotation meaning exactly what it meant before.
1913
3970
  */
1914
- declare interface NarrowedFromSpec {
1915
- readonly from: ColumnType;
3971
+ declare interface QueryDescriptor<R = Row> {
3972
+ readonly table: string;
3973
+ readonly predicate: Predicate | undefined;
3974
+ /** Phantom — carries the row type through to `store.query()`. Never set at
3975
+ * runtime; erased entirely by the compiler. */
3976
+ readonly __row?: R;
3977
+ /**
3978
+ * Opt OUT of the runtime's automatic `deletedAt IS NULL` scope on
3979
+ * `softDelete()` tables. Set by `.withDeleted()`. The query builder is
3980
+ * browser-safe and mixin-unaware, so this is just a flag the RUNTIME
3981
+ * (storeMiddleware read path + reactive-query finalize) honors.
3982
+ */
3983
+ readonly includeDeleted?: boolean;
3984
+ /** Opt OUT of the `expires()` read filter — a deliberate read of expired
3985
+ * rows (an admin view, a grace-period check). Same shape and same posture as
3986
+ * `includeDeleted`. */
3987
+ readonly includeExpired?: boolean;
3988
+ /**
3989
+ * Lock the matched rows for the rest of the enclosing transaction — set by
3990
+ * `.forUpdate()`. `SELECT … FOR UPDATE` on postgres / mysql / mariadb,
3991
+ * `WITH (UPDLOCK, ROWLOCK)` on mssql; sqlite serialises writers already and
3992
+ * emits nothing. Outside a transaction the lock is released as the statement
3993
+ * ends, which is harmless and useless — the point is a read-modify-write
3994
+ * whose read and write share one transaction.
3995
+ */
3996
+ readonly lock?: 'update';
3997
+ /**
3998
+ * Opt OUT of the runtime's automatic tenant scope on `tenant()` tables.
3999
+ * Set by `.unscoped()`. Honored by the runtime, same as `includeDeleted`.
4000
+ */
4001
+ readonly crossTenant?: boolean;
4002
+ readonly order: ReadonlyArray<OrderClause>;
4003
+ readonly take: number | undefined;
4004
+ readonly skip: number | undefined;
4005
+ readonly projection: ReadonlyArray<string> | undefined;
4006
+ /**
4007
+ * Aggregate projection. When set, `SELECT` emits the aggregates
4008
+ * instead of row columns; the result is always exactly ONE row
4009
+ * (per group when combined with `groupBy`, ONE row total when not).
4010
+ *
4011
+ * Cannot coexist with `projection` (the two are mutually exclusive
4012
+ * select clauses). The builder enforces this at chain time.
4013
+ */
4014
+ readonly aggregations?: ReadonlyArray<AggregateColumn<unknown>>;
4015
+ /**
4016
+ * `GROUP BY` columns. When set, the query returns ONE row per
4017
+ * distinct combination of these columns + the aggregations.
4018
+ * Requires `aggregations` to be set; ungrouped aggregates always
4019
+ * produce exactly one row.
4020
+ */
4021
+ readonly groupBy?: ReadonlyArray<string>;
4022
+ /**
4023
+ * `HAVING` clause — predicate evaluated AFTER GROUP BY. Distinct
4024
+ * from `predicate` (which becomes `WHERE` and runs BEFORE grouping).
4025
+ * Use HAVING to filter groups based on their aggregate values
4026
+ * (e.g. "users with more than 10 posts").
4027
+ *
4028
+ * Stored as a `Predicate` so the same predicate compiler that
4029
+ * handles WHERE handles HAVING — but the column names on the LHS
4030
+ * of HAVING refer to aggregate aliases, not raw rows.
4031
+ */
4032
+ readonly having?: Predicate;
4033
+ /**
4034
+ * Full-text-search predicate (S7). Set by `.matching('indexName',
4035
+ * 'query')`. `.matching()` resolves the index back to its declared
4036
+ * columns + config + weights from the table registry at call time and
4037
+ * stores them here, so the SQL compiler can emit the right per-dialect
4038
+ * predicate (and an optional relevance-ranking expression) without
4039
+ * re-consulting the registry.
4040
+ *
4041
+ * Per-dialect emission:
4042
+ * - postgres → `<tsvCol> @@ plainto_tsquery('<cfg>', ?)`
4043
+ * - mysql / mariadb → `MATCH(col1, col2) AGAINST (? IN NATURAL LANGUAGE MODE)`
4044
+ * - sqlite → `<table>_<idx>_fts MATCH ?` against the FTS5 table
4045
+ * - mssql → ranked LOWER(...) LIKE fallback (no server FTS catalog)
4046
+ *
4047
+ * `rank` carries an optional relevance-ranking request. When set, the
4048
+ * compiler also surfaces a synthetic relevance column aliased `rank.alias`
4049
+ * and (when `rank.order` is set) orders by it DESC. The score expression
4050
+ * is per-dialect: postgres `ts_rank`, mysql/mariadb `MATCH ... AGAINST`
4051
+ * score, sqlite FTS5 `bm25`, mssql a LIKE-hit count.
4052
+ */
4053
+ /**
4054
+ * Full-text indexes declared on the FROM table — captured by
4055
+ * `queryFor()` so `.matching()` can resolve an index's columns/config
4056
+ * without a global table-registry lookup. Pure builder metadata; the
4057
+ * SQL compiler never reads it (it reads the resolved `fullTextSearch`).
4058
+ */
4059
+ readonly fullTextIndexes?: ReadonlyArray<{
4060
+ readonly name: string;
4061
+ readonly columns: ReadonlyArray<string>;
4062
+ readonly config?: string;
4063
+ readonly weights?: Readonly<Record<string, 'A' | 'B' | 'C' | 'D'>>;
4064
+ }>;
4065
+ /**
4066
+ * The SOURCE table object this query was built from (`queryFor(t)` / a
4067
+ * `databaseHandle` entry sets it). The eager-load resolver (`.with(...)`)
4068
+ * needs the source table's shape + relations; it used to re-resolve it
4069
+ * BY NAME via `requireTable(descriptor.table)`, which forces every queried
4070
+ * table to be pre-registered (`databaseHandle(...)` / discovery). A direct
4071
+ * query needs no such registration — it carries the object — so an eager
4072
+ * query that threw "table 'X' is not registered" for a table you can query
4073
+ * directly was an asymmetry. Carrying the object here (same precedent as
4074
+ * `fullTextIndexes`) lets the store use it directly, with `requireTable` as
4075
+ * the fallback for reconstructed descriptors (e.g. `paginateById`). NOT
4076
+ * serialized/fingerprinted — the reactive engine keys on table+predicate.
4077
+ */
4078
+ readonly sourceTable?: TableLike;
4079
+ readonly fullTextSearch?: {
4080
+ readonly indexName: string;
4081
+ readonly query: string;
4082
+ /** Columns the index covers — resolved from the table registry. */
4083
+ readonly columns: ReadonlyArray<string>;
4084
+ /** Postgres tsvector config (default 'english'). */
4085
+ readonly config?: string;
4086
+ /** Per-column weight letters (postgres ts_rank). */
4087
+ readonly weights?: Readonly<Record<string, 'A' | 'B' | 'C' | 'D'>>;
4088
+ /** Relevance-ranking request — set by `.rankBy(alias?)`. */
4089
+ readonly rank?: {
4090
+ readonly alias: string;
4091
+ readonly order: boolean;
4092
+ };
4093
+ };
4094
+ /**
4095
+ * Common-Table-Expression definitions (Q4) — `WITH <name> AS (...)`.
4096
+ * Each entry binds a `name` to a sub-query descriptor; the outer
4097
+ * SELECT can reference the CTE by name in `from`, `inSubquery`,
4098
+ * `exists`, etc.
4099
+ *
4100
+ * `.withCte()` adds a non-recursive CTE. For tree-walks use
4101
+ * `.recursiveCte()`, which sets `recursive: true` and emits
4102
+ * `WITH RECURSIVE` (the inner descriptor self-references the CTE
4103
+ * name). Both are reactive — see `runtime/dependencyGraph.ts`.
4104
+ */
4105
+ readonly ctes?: ReadonlyArray<{
4106
+ readonly name: string;
4107
+ readonly descriptor: QueryDescriptor;
4108
+ /**
4109
+ * Recursive CTE marker (Q4 v2). When true, the compiler emits
4110
+ * `WITH RECURSIVE name AS (...)`. The inner descriptor must be a
4111
+ * UNION (via the `union` / `unionAll` helpers) whose first arm is
4112
+ * the anchor and subsequent arms reference the CTE by name (the
4113
+ * recursion). Cycle-detection per-dialect: postgres uses
4114
+ * `UNION` semantics (implicit dedup); other dialects mirror.
4115
+ */
4116
+ readonly recursive?: boolean;
4117
+ }>;
1916
4118
  /**
1917
- * Raw SQL cast expression spliced into the planner's
1918
- * `ALTER COLUMN … TYPE … USING <using>` (postgres) / shadow-copy
1919
- * (`shadow := <using>(old)`). Authored by the developer in the
1920
- * migration — e.g. `'role::role_enum'`, `'flag::int::text'`. Omit when
1921
- * the conversion is implicit on the dialect (e.g. `varchar → text`).
4119
+ * SELECT DISTINCT (Q7). When true, dedup rows by the full result
4120
+ * shape. Cross-dialect.
1922
4121
  */
1923
- readonly using?: string;
1924
- }
1925
-
1926
- /**
1927
- * Postgres identifier for the trigger function backing a given channel.
1928
- *
1929
- * Derived from the channel, NOT a single global name, and that is the whole
1930
- * point: `CREATE OR REPLACE FUNCTION framework_notify_change()` is one row in
1931
- * `pg_proc` for the entire database, so two schemas applied with different
1932
- * channels overwrote each other and the last one won. Everything applied
1933
- * earlier then emitted on somebody else's channel — silently, because a NOTIFY
1934
- * nobody listens to is not an error.
1935
- */
1936
- export declare const notifyFunctionName: (channel: string) => string;
1937
-
1938
- /** Negation of an arbitrary sub-predicate → `NOT (...)`. Wraps any
1939
- * leaf/and/or/subquery subtree (distinct from the per-column `neq` and
1940
- * the narrow `notInSubquery`/`notExists`). */
1941
- declare interface NotPredicate {
1942
- readonly not: Predicate;
1943
- }
1944
-
1945
- /**
1946
- * DDL fragment for a numeric primary key column. Each dialect has its
1947
- * own auto-increment idiom — there's no portable shorthand, so we
1948
- * dispatch per dialect explicitly. The fragment includes type + PK +
1949
- * auto-increment in one statement; the caller skips the generic
1950
- * `NOT NULL` / `PRIMARY KEY` additions for numeric ids.
1951
- */
1952
- export declare const numericIdSql: (name: string, dialect: DialectId) => string;
1953
-
1954
- /**
1955
- * The seven classes of schema operations the planner recognises. The
1956
- * default policy for each is encoded by the planner; the applier
1957
- * obeys it. Keep this enum in lockstep with the AGENTS.md template's
1958
- * "Schema migrations" section — agents grep for these strings.
1959
- */
1960
- export declare type OperationClass =
1961
- /** Reversible, side-effect-free for existing data. Auto-applied. */
1962
- 'safe'
1963
- /** ADD COLUMN + NOT NULL with `.default(value)` declared.
1964
- * Applied as `ADD COLUMN ... NOT NULL DEFAULT <value>`. */
1965
- | 'needs-default'
1966
- /** ADD COLUMN + NOT NULL on a populated table, ADD UNIQUE / FK / CHECK
1967
- * with existing offenders. Requires a deliberate declaration
1968
- * (`.backfill()`, `.unique({dedup})`, `reference({orphanPolicy})`).
1969
- * Applied as a three-step plan (ADD nullable → run backfill → SET
1970
- * NOT NULL). */
1971
- | 'needs-backfill'
1972
- /** Column appears removed + a new one appeared. Looks like DROP+ADD
1973
- * but is probably a rename. Refuse-to-plan without an explicit
1974
- * `.renamedFrom('oldName')` marker — silent renames lose data. */
1975
- | 'needs-rename-annotation'
1976
- /** DROP COLUMN, DROP TABLE, narrow type with existing offenders.
1977
- * Refuse-to-plan unless `dropped()` / `.narrowedFrom()` declared
1978
- * OR `VOLTRO_DESTRUCTIVE_OK=1` set. */
1979
- | 'lossy'
1980
- /** Large-table operation that should run online (CREATE INDEX
1981
- * CONCURRENTLY, batched backfill, shadow-column swap). The applier
1982
- * auto-rewrites these once the row count crosses the threshold
1983
- * (`online-after`, default 50k). */
1984
- | 'online-required'
1985
- /** Operations the planner can't infer from a diff (table split /
1986
- * merge, custom data move, type-change with USING). Requires a
1987
- * file-based migration in `migrations/<id>.ts`. */
1988
- | 'multi-step';
1989
-
1990
- /**
1991
- * The vocabulary of concrete DDL-level operations the planner emits.
1992
- * Each `MigrationOperation` carries one of these kinds plus the data
1993
- * the applier needs to execute it.
1994
- *
1995
- * `kind` strings are short + lowercase-kebab so they round-trip
1996
- * cleanly through JSON (the `operations` column of `_voltro_migrations`)
1997
- * and can be grepped in `voltro logs` output.
1998
- */
1999
- export declare type OperationKind = 'create-table' | 'drop-table' | 'add-column' | 'drop-column' | 'rename-column' | 'alter-column-nullability' | 'alter-column-type' | 'alter-column-default' | 'add-index' | 'drop-index' | 'add-unique' | 'drop-unique' | 'add-unique-composite' | 'drop-unique-composite' | 'add-foreign-key' | 'drop-foreign-key' | 'add-check' | 'drop-check' | 'reconcile-json-check';
2000
-
2001
- declare interface OrderClause {
2002
- readonly column: string;
2003
- readonly direction: 'asc' | 'desc';
2004
- }
2005
-
2006
- declare interface OrPredicate {
2007
- readonly or: ReadonlyArray<Predicate>;
2008
- }
2009
-
2010
- export declare const parseEnumCheck: (clause: string) => {
2011
- readonly column: string;
2012
- readonly values: ReadonlyArray<string>;
2013
- } | null;
2014
-
2015
- /**
2016
- * The ids of migration files present on disk but NOT yet recorded against THIS
2017
- * database — i.e. exactly what `runFileBasedMigrations` would execute next.
2018
- *
2019
- * Exists so a caller can decide WHETHER to apply before applying. `voltro dev`
2020
- * uses it to refuse unattended file migrations against a remote database: that
2021
- * decision needs the list (to name it in the refusal) and must not have run
2022
- * anything to get it.
2023
- */
2024
- export declare const pendingFileMigrationIds: (sql: SqlClient.SqlClient, projectRoot: string) => Effect.Effect<ReadonlyArray<string>, unknown>;
2025
-
2026
- export declare interface PlanInput {
2027
- readonly declared: ReadonlyArray<TableLike>;
2028
- readonly live: SchemaSnapshot;
4122
+ readonly distinct?: boolean;
2029
4123
  /**
2030
- * Threshold above which safe DDL ops on a populated table get
2031
- * promoted to `online-required` (CONCURRENTLY / batched / shadow-
2032
- * column). Pass undefined to use the framework default (50_000).
4124
+ * SELECT DISTINCT ON (cols) — postgres-only (Q7). Picks ONE row
4125
+ * per distinct combination of `distinctOn` columns; the row picked
4126
+ * is the FIRST per the descriptor's `order`. The compiler emits a
4127
+ * windowed-rownumber fallback on other dialects.
4128
+ *
4129
+ * Common use: "latest message per channel".
2033
4130
  */
2034
- readonly onlineAfter?: number;
4131
+ readonly distinctOn?: ReadonlyArray<string>;
2035
4132
  /**
2036
- * The target dialect. Drives storage-collapse type equivalence (sqlite
2037
- * stores timestamp/json as TEXT, mssql stores json as NVARCHAR) so the plan
2038
- * doesn't churn phantom alter-column-type ops. Omit → no collapse (postgres
2039
- * / mysql / mariadb distinguish these natively anyway).
4133
+ * Alias for the FROM table — `FROM <table> AS <alias>` (Q8).
4134
+ * Needed for self-joins where the same table appears multiple
4135
+ * times with different aliases. When unset, the FROM clause is
4136
+ * the bare table name.
2040
4137
  */
2041
- readonly dialect?: DialectId;
4138
+ readonly alias?: string;
2042
4139
  /**
2043
- * Live tables to LEAVE ALONE — never planned as a drop even though no
2044
- * declared entity matches them. The framework already self-excludes its own
2045
- * runtime tables (`_voltro_*` / `_cloud_*` / `cluster_*`, see
2046
- * `isFrameworkOwnedLiveTable`); this is the USER escape hatch for their own
2047
- * unmanaged infra tables (e.g. a Strapi→Voltro `_strapi_id_map`), so the diff
2048
- * doesn't try to drop a table the migration tooling itself depends on. The
2049
- * CLI populates it from `VOLTRO_DB_IGNORE_TABLES` (comma-separated names).
4140
+ * Joined tables (Q8). Each entry adds an INNER JOIN or LEFT JOIN
4141
+ * with an explicit ON predicate. Self-joins re-use the same
4142
+ * source table with a different alias.
4143
+ *
4144
+ * `on` is a regular `Predicate` — the leaf columns may carry
4145
+ * `alias.column` style qualifiers since the compiler emits the
4146
+ * column string verbatim.
2050
4147
  */
2051
- readonly ignoreTables?: ReadonlyArray<string>;
2052
- }
2053
-
2054
- /**
2055
- * Compute the migration plan between a declared schema (with
2056
- * annotations) and a live snapshot.
2057
- *
2058
- * The output is deterministic: same inputs → same `MigrationPlan`
2059
- * (modulo op insertion order, which follows table-then-kind sort
2060
- * order). Lets the applier replay the plan from `_voltro_migration_plans`
2061
- * without surprises.
2062
- *
2063
- * The planner does NOT execute pre-check queries — that's the
2064
- * applier's job (it needs live SQL access). The classifications here
2065
- * are based purely on the snapshot shapes + the annotations on
2066
- * `ColumnDefinition`. The applier may downgrade `safe` → `online-required`
2067
- * once it knows the row count exceeds the threshold.
2068
- */
2069
- export declare const planMigrations: (input: PlanInput) => MigrationPlan;
2070
-
2071
- /**
2072
- * One row in a `MigrationPlan` — a concrete operation paired with the
2073
- * planner's classification + any blocking reason.
2074
- *
2075
- * `blocked` is set when the classification refuses-to-plan (lossy
2076
- * without `dropped()`, rename without `.renamedFrom()`, etc.). The
2077
- * applier checks `blocked` before executing; the CLI's `voltro db plan`
2078
- * output highlights it red.
2079
- *
2080
- * `backfill` / `dedup` / `orphanPolicy` mirror the user's annotations
2081
- * straight from `ColumnDefinition` so the applier doesn't need to
2082
- * re-introspect them.
2083
- */
2084
- export declare interface PlannedOperation {
2085
- readonly op: MigrationOperation;
2086
- readonly classification: OperationClass;
2087
- /** Why the op was classified this way (also surfaces in the CLI). */
2088
- readonly reason?: string;
2089
- /** Set on lossy / needs-rename / multi-step ops with no resolution. */
2090
- readonly blocked?: {
2091
- readonly fix: string;
2092
- };
2093
- /** Pre-check estimate the planner ran (row counts, offender count). */
2094
- readonly estimate?: {
2095
- readonly affectedRows?: number;
2096
- readonly offenderCount?: number;
2097
- };
2098
- /** Whether the op is atomic for each supported dialect. Postgres,
2099
- * mssql, sqlite are atomic for all DDL; mysql/mariadb implicit-commit
2100
- * every statement. */
2101
- readonly atomicByDialect: {
2102
- readonly postgres: boolean;
2103
- readonly mysql: boolean;
2104
- readonly mariadb: boolean;
2105
- readonly mssql: boolean;
2106
- readonly sqlite: boolean;
2107
- };
2108
- }
2109
-
2110
- /** What happens to the referencing row when the target row goes away. */
2111
- declare type PluginRefOrphanPolicy =
2112
- /** Delete the referencing row. For a row that only exists to point at it —
2113
- * a favourite, a pin, a share. */
2114
- 'delete'
2115
- /** Null the column, keeping the row. Requires `.nullable()`; declaring it on
2116
- * a non-nullable column is refused at declaration rather than failing at the
2117
- * first delete, months later. */
2118
- | 'null'
2119
- /** Do nothing. The explicit "I know, and I handle it myself" — different from
2120
- * omitting the option, which is the same behaviour arrived at by accident. */
2121
- | 'keep';
2122
-
2123
- declare type Predicate = PredicateLeaf | AndPredicate | OrPredicate | NotPredicate | SubqueryInPredicate | ExistsPredicate;
2124
-
2125
- declare interface PredicateLeaf {
2126
- readonly column: string;
2127
- readonly op: LeafOperator;
2128
- readonly value?: unknown;
4148
+ readonly joins?: ReadonlyArray<{
4149
+ readonly kind: 'inner' | 'left';
4150
+ readonly table: string;
4151
+ readonly alias: string;
4152
+ readonly on: Predicate;
4153
+ }>;
4154
+ /**
4155
+ * Aliased projection for joined queries (Q8 v2). When set, the
4156
+ * SELECT emits `alias.col AS outputKey` for each entry, producing
4157
+ * a flat row with the user's chosen keys.
4158
+ *
4159
+ * Mutually exclusive with `projection` (regular select-cols).
4160
+ * Bypassed by `aggregations`.
4161
+ */
4162
+ readonly joinedProjection?: ReadonlyArray<{
4163
+ readonly outputKey: string;
4164
+ readonly source: string;
4165
+ }>;
2129
4166
  /**
2130
- * JSON-path segments INTO a `json()` column (object keys / array indices).
2131
- * When set, the leaf filters on the extracted value, not the whole column —
2132
- * built via {@link jsonField}. The sql compiler lowers it to the dialect's
2133
- * json-extract expression; the reactive matcher treats it as non-indexable
2134
- * (rides the unindexed bucket → re-checked on any change to the table, like
2135
- * `contains`). Absent for a normal column leaf.
4167
+ * Set-operation composition (Q6). When set, the SELECT shape is
4168
+ * replaced by `(SELECT ...) <OP> (SELECT ...) ...`.
4169
+ *
4170
+ * - `'union'` → dedup-merge (`UNION`)
4171
+ * - `'union-all'` → no dedup (`UNION ALL`)
4172
+ * - `'intersect'` → rows in BOTH (`INTERSECT`)
4173
+ * - `'except'` → rows in LHS not in RHS (`EXCEPT`)
4174
+ *
4175
+ * All four are standard SQL + supported on every dialect we ship.
2136
4176
  */
2137
- readonly path?: ReadonlyArray<string | number>;
4177
+ readonly setOp?: {
4178
+ readonly kind: 'union' | 'union-all' | 'intersect' | 'except';
4179
+ readonly queries: ReadonlyArray<QueryDescriptor>;
4180
+ };
4181
+ /**
4182
+ * Optional index hint. Set by `.using('indexName')` on the query
4183
+ * builder. The framework stores it on the descriptor for two reasons:
4184
+ *
4185
+ * 1. Suppresses the dev-mode "non-indexed query" warning for this
4186
+ * descriptor (the user explicitly signalled they thought about
4187
+ * the access pattern).
4188
+ * 2. The matcher's tuple-routing path keys subscriptions on the
4189
+ * declared index's column set — see `IndexedMatcher` in
4190
+ * `@voltro/runtime/matcher.ts`. Subscriptions that declare a
4191
+ * covering index get O(1) candidate lookup per event instead
4192
+ * of column-union routing.
4193
+ */
4194
+ readonly usingIndex?: string;
4195
+ /**
4196
+ * Eager-load tree declared via `.with({ ... })`. Consumed by the
4197
+ * JoinCompiler (`./joinCompiler.ts`) when present — the SQL stores
4198
+ * dispatch nested-JSON-aggregation queries; the in-memory store
4199
+ * walks it via post-hoc lookups.
4200
+ */
4201
+ readonly eager?: WithSpec;
4202
+ /**
4203
+ * Approximate-nearest-neighbour clause (plan 01). Set by
4204
+ * `.nearestNeighbours(...)`. Drives a synthetic `distance` projection
4205
+ * + `ORDER BY <col> <op> <query>` in the SQL compiler:
4206
+ *
4207
+ * - postgres (pgvector): `<col> <=> $q` (cosine) / `<->` (l2) /
4208
+ * `<#>` (inner).
4209
+ * - mariadb / mysql: `VEC_DISTANCE_COSINE/EUCLIDEAN(<col>, $q)`.
4210
+ * - mssql / sqlite: no portable ANN operator → the compiler warns +
4211
+ * skips the distance ordering (sequential scan; rows still return).
4212
+ *
4213
+ * `queryVector` is the literal float array; `queryText` is the
4214
+ * raw string the runtime embeds to a vector *before* compiling
4215
+ * (only valid when the table carries `vectorEmbedding()`). Exactly
4216
+ * one of the two is set.
4217
+ */
4218
+ readonly annClause?: AnnClause;
4219
+ /**
4220
+ * PostGIS spatial distance clause (`@voltro/plugin-postgis`). Set by the
4221
+ * `withDistance(...)` / `nearestBy(...)` transforms applied via `.use(...)`.
4222
+ * Postgres-only — the SQL compiler lowers it to a `ST_Distance(...)`
4223
+ * projected column and/or a `<->` KNN `ORDER BY`; on any non-postgres
4224
+ * dialect the compiler THROWS (mirrors the spatial predicate + column
4225
+ * guards). Never evaluated in-memory (no PostGIS in JS). Composes with
4226
+ * `.where(...)`, `.limit(k)`, spatial predicates, etc.
4227
+ */
4228
+ readonly spatialClause?: SpatialClause;
2138
4229
  }
2139
4230
 
2140
- /**
2141
- * Imperative `applyNamespacedSchema` against a fresh SqlClient — the
2142
- * entry point the CLI / runtime use to provision a tenant's namespace
2143
- * (eager at migrate time, or lazily on first use).
2144
- *
2145
- * **This is where `lifecycle: 'onTenantCreate'` seeds fire**, after the DDL
2146
- * lands and before this resolves — so a caller that awaits provisioning gets
2147
- * a tenant whose tables AND reference data exist, or an error. It was the
2148
- * missing half: the lifecycle validated, registered, and never ran, so every
2149
- * tenant came up empty and nothing said so.
2150
- *
2151
- * The seeds run through `setTenantCreateSeedHook` — the hook itself IS
2152
- * installed at boot (`seedRunner.ts`); with no seeds declared this costs one
2153
- * null check. A seed failure REJECTS — see `fireTenantCreateSeeds` for why,
2154
- * and note that re-calling this function is safe (the DDL is `IF NOT EXISTS`
2155
- * and seed steps are idempotent by contract).
2156
- *
2157
- * Wired by BOTH boot paths through `makeTenantProvisioner` (@voltro/cli):
2158
- * under `tenantIsolation: 'namespace'` on a SQL dialect, every namespace store
2159
- * view provisions lazily on first use — memoised per namespace per process.
2160
- * Eager provisioning is the APP's move (call this from a seed/startup over its
2161
- * own tenant table); the framework has no tenant registry to enumerate.
2162
- */
2163
- export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>, namespace: string, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
2164
-
2165
4231
  /**
2166
4232
  * The runtime-facing materialized query — a plain bag of strings the store and
2167
4233
  * the matcher consume.
@@ -2180,9 +4246,9 @@ export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>,
2180
4246
  * `QueryDescriptor` that generic infrastructure passes around. Defaulting to
2181
4247
  * `Row` keeps every existing annotation meaning exactly what it meant before.
2182
4248
  */
2183
- declare interface QueryDescriptor<R = Row> {
4249
+ declare interface QueryDescriptor_2<R = Row_2> {
2184
4250
  readonly table: string;
2185
- readonly predicate: Predicate | undefined;
4251
+ readonly predicate: Predicate_2 | undefined;
2186
4252
  /** Phantom — carries the row type through to `store.query()`. Never set at
2187
4253
  * runtime; erased entirely by the compiler. */
2188
4254
  readonly __row?: R;
@@ -2211,7 +4277,7 @@ declare interface QueryDescriptor<R = Row> {
2211
4277
  * Set by `.unscoped()`. Honored by the runtime, same as `includeDeleted`.
2212
4278
  */
2213
4279
  readonly crossTenant?: boolean;
2214
- readonly order: ReadonlyArray<OrderClause>;
4280
+ readonly order: ReadonlyArray<OrderClause_2>;
2215
4281
  readonly take: number | undefined;
2216
4282
  readonly skip: number | undefined;
2217
4283
  readonly projection: ReadonlyArray<string> | undefined;
@@ -2223,7 +4289,7 @@ declare interface QueryDescriptor<R = Row> {
2223
4289
  * Cannot coexist with `projection` (the two are mutually exclusive
2224
4290
  * select clauses). The builder enforces this at chain time.
2225
4291
  */
2226
- readonly aggregations?: ReadonlyArray<AggregateColumn<unknown>>;
4292
+ readonly aggregations?: ReadonlyArray<AggregateColumn_2<unknown>>;
2227
4293
  /**
2228
4294
  * `GROUP BY` columns. When set, the query returns ONE row per
2229
4295
  * distinct combination of these columns + the aggregations.
@@ -2241,7 +4307,7 @@ declare interface QueryDescriptor<R = Row> {
2241
4307
  * handles WHERE handles HAVING — but the column names on the LHS
2242
4308
  * of HAVING refer to aggregate aliases, not raw rows.
2243
4309
  */
2244
- readonly having?: Predicate;
4310
+ readonly having?: Predicate_2;
2245
4311
  /**
2246
4312
  * Full-text-search predicate (S7). Set by `.matching('indexName',
2247
4313
  * 'query')`. `.matching()` resolves the index back to its declared
@@ -2287,7 +4353,7 @@ declare interface QueryDescriptor<R = Row> {
2287
4353
  * the fallback for reconstructed descriptors (e.g. `paginateById`). NOT
2288
4354
  * serialized/fingerprinted — the reactive engine keys on table+predicate.
2289
4355
  */
2290
- readonly sourceTable?: TableLike;
4356
+ readonly sourceTable?: TableLike_2;
2291
4357
  readonly fullTextSearch?: {
2292
4358
  readonly indexName: string;
2293
4359
  readonly query: string;
@@ -2316,7 +4382,7 @@ declare interface QueryDescriptor<R = Row> {
2316
4382
  */
2317
4383
  readonly ctes?: ReadonlyArray<{
2318
4384
  readonly name: string;
2319
- readonly descriptor: QueryDescriptor;
4385
+ readonly descriptor: QueryDescriptor_2;
2320
4386
  /**
2321
4387
  * Recursive CTE marker (Q4 v2). When true, the compiler emits
2322
4388
  * `WITH RECURSIVE name AS (...)`. The inner descriptor must be a
@@ -2361,7 +4427,7 @@ declare interface QueryDescriptor<R = Row> {
2361
4427
  readonly kind: 'inner' | 'left';
2362
4428
  readonly table: string;
2363
4429
  readonly alias: string;
2364
- readonly on: Predicate;
4430
+ readonly on: Predicate_2;
2365
4431
  }>;
2366
4432
  /**
2367
4433
  * Aliased projection for joined queries (Q8 v2). When set, the
@@ -2388,7 +4454,7 @@ declare interface QueryDescriptor<R = Row> {
2388
4454
  */
2389
4455
  readonly setOp?: {
2390
4456
  readonly kind: 'union' | 'union-all' | 'intersect' | 'except';
2391
- readonly queries: ReadonlyArray<QueryDescriptor>;
4457
+ readonly queries: ReadonlyArray<QueryDescriptor_2>;
2392
4458
  };
2393
4459
  /**
2394
4460
  * Optional index hint. Set by `.using('indexName')` on the query
@@ -2410,7 +4476,7 @@ declare interface QueryDescriptor<R = Row> {
2410
4476
  * dispatch nested-JSON-aggregation queries; the in-memory store
2411
4477
  * walks it via post-hoc lookups.
2412
4478
  */
2413
- readonly eager?: WithSpec;
4479
+ readonly eager?: WithSpec_2;
2414
4480
  /**
2415
4481
  * Approximate-nearest-neighbour clause (plan 01). Set by
2416
4482
  * `.nearestNeighbours(...)`. Drives a synthetic `distance` projection
@@ -2427,7 +4493,7 @@ declare interface QueryDescriptor<R = Row> {
2427
4493
  * (only valid when the table carries `vectorEmbedding()`). Exactly
2428
4494
  * one of the two is set.
2429
4495
  */
2430
- readonly annClause?: AnnClause;
4496
+ readonly annClause?: AnnClause_2;
2431
4497
  /**
2432
4498
  * PostGIS spatial distance clause (`@voltro/plugin-postgis`). Set by the
2433
4499
  * `withDistance(...)` / `nearestBy(...)` transforms applied via `.use(...)`.
@@ -2437,7 +4503,7 @@ declare interface QueryDescriptor<R = Row> {
2437
4503
  * guards). Never evaluated in-memory (no PostGIS in JS). Composes with
2438
4504
  * `.where(...)`, `.limit(k)`, spatial predicates, etc.
2439
4505
  */
2440
- readonly spatialClause?: SpatialClause;
4506
+ readonly spatialClause?: SpatialClause_2;
2441
4507
  }
2442
4508
 
2443
4509
  declare const quote: (name: string) => string;
@@ -2497,6 +4563,19 @@ export declare interface ReactiveTriggerDrift {
2497
4563
  * business, not this one's.
2498
4564
  */
2499
4565
  readonly functionOutdated: boolean;
4566
+ /**
4567
+ * What the verdict was measured against — so a report that contradicts a
4568
+ * `db apply` that just installed the triggers can be read: the schema the
4569
+ * detector looked in, the channel it derived the names from, and how many
4570
+ * `framework_changes_*` triggers it found there at all. A consumer measured
4571
+ * "71 have NO trigger" eleven seconds after their migrate job installed 71,
4572
+ * and nothing in the line said which schema or channel was compared.
4573
+ */
4574
+ readonly compared?: {
4575
+ readonly schema: string;
4576
+ readonly channel: string;
4577
+ readonly present: number;
4578
+ };
2500
4579
  }
2501
4580
 
2502
4581
  /**
@@ -2649,6 +4728,39 @@ export declare const resolveMysqlEngine: (sql: SqlClient.SqlClient) => Effect.Ef
2649
4728
 
2650
4729
  declare type RetryDecision = 'retry' | 'noRetry';
2651
4730
 
4731
+ export declare interface RewriteOptions extends EachOptions {
4732
+ /**
4733
+ * Columns whose CURRENT value must still hold when the patch is written, in
4734
+ * addition to the columns the patch itself changes. A JSON column cannot be
4735
+ * compared by value on every dialect, so a rewrite of one guards on what
4736
+ * you name here — an `updatedAt`, a `version` — rather than on the document.
4737
+ */
4738
+ readonly guard?: ReadonlyArray<string>;
4739
+ /**
4740
+ * How many passes over the table before contention is a failure. A pass
4741
+ * that rewrites nothing and contends on nothing is STABLE and ends the
4742
+ * rewrite; a row that changed under the migration between its read and its
4743
+ * write is contended and is read again on the next pass. Default 10.
4744
+ */
4745
+ readonly maxPasses?: number;
4746
+ }
4747
+
4748
+ export declare interface RewriteReport {
4749
+ readonly passes: number;
4750
+ readonly scanned: number;
4751
+ readonly rewritten: number;
4752
+ /** Writes lost to a concurrent change and retried on a later pass. */
4753
+ readonly contended: number;
4754
+ }
4755
+
4756
+ /** A `ctx.rewrite` that was still contended after `maxPasses`. */
4757
+ export declare class RewriteUnstableError extends Error {
4758
+ readonly table: string;
4759
+ readonly report: RewriteReport;
4760
+ readonly _tag = "RewriteUnstableError";
4761
+ constructor(table: string, report: RewriteReport);
4762
+ }
4763
+
2652
4764
  /**
2653
4765
  * Roll back a single file-based migration by id. Looks up the
2654
4766
  * migration's `down` body via file discovery (the file must still be
@@ -2663,6 +4775,7 @@ export declare const rollbackFileBasedMigration: (sql: SqlClient.SqlClient, ctx:
2663
4775
  projectRoot: string;
2664
4776
  id: string;
2665
4777
  lockSchema?: string;
4778
+ store: DataStore;
2666
4779
  }) => Effect.Effect<{
2667
4780
  id: string;
2668
4781
  durationMs: number;
@@ -2700,6 +4813,11 @@ export declare const rollingDeployUnsafeOps: (operations: ReadonlyArray<PlannedO
2700
4813
 
2701
4814
  declare type Row = Readonly<Record<string, unknown>>;
2702
4815
 
4816
+ declare type Row_2 = Readonly<Record<string, unknown>>;
4817
+
4818
+ /** Addresses one row: its `id`, or the columns of an explicit primary key. */
4819
+ declare type RowKey = string | Readonly<Record<string, unknown>>;
4820
+
2703
4821
  /** The context a rule predicate is evaluated with. */
2704
4822
  declare interface RuleContext {
2705
4823
  /** Transactional store — read other tables in the mutation's snapshot. */
@@ -2750,6 +4868,19 @@ export declare interface RunFileBasedMigrationsCtx {
2750
4868
  readonly projectRoot: string;
2751
4869
  readonly env: 'dev' | 'staging' | 'prod';
2752
4870
  readonly appliedBy: string;
4871
+ /** The application's data store over the same database `sql` is connected
4872
+ * to — what a migration body receives as `ctx.store`. Required, not
4873
+ * optional: a runner that can leave it out is a runner that hands one body
4874
+ * a store and the next one an error, depending on which command ran it. */
4875
+ readonly store: DataStore;
4876
+ /**
4877
+ * The caller holds a transaction the whole run is part of — `voltro db apply`
4878
+ * on a dialect with transactional DDL runs the file migrations and the
4879
+ * schema plan as ONE unit. Each migration then runs on the given store and
4880
+ * connection without a transaction of its own (nesting would be a mistake)
4881
+ * and without the commit probe (the caller's unit is the guarantee).
4882
+ */
4883
+ readonly enclosed?: boolean;
2753
4884
  /** Schema scope for the migration advisory lock (see `migrations/lock.ts`).
2754
4885
  * Absent → resolved from configuration (`DB_SCHEMA`). */
2755
4886
  readonly lockSchema?: string;
@@ -2775,6 +4906,17 @@ export declare const runFrameworkBootstrap: (tables: ReadonlyArray<AnyTable>, sq
2775
4906
  */
2776
4907
  export declare const runMigrate: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
2777
4908
 
4909
+ /**
4910
+ * Execute ONE planned operation with the applier's own per-dialect
4911
+ * statements, outside a plan — for a file migration's schema helpers
4912
+ * (`ctx.schema.addColumn`, `addIndex`, `createTable`, …), which construct the
4913
+ * operation from the live table and hand it here. `declared` is what the
4914
+ * mysql MODIFY, the mssql prefix rules and the sqlite rebuild read the target
4915
+ * shape from: the live table with the change applied, materialised as a
4916
+ * declaration by the caller.
4917
+ */
4918
+ export declare const runMigrationOp: (sql: SqlClient.SqlClient, planned: PlannedOperation, declared: ReadonlyArray<TableLike>) => Effect.Effect<AppliedOp, SqlError_2>;
4919
+
2778
4920
  /**
2779
4921
  * Top-level entry point — called once per boot. Returns a
2780
4922
  * `BootMigrationOutcome`; the caller logs + acts on it (refuse-to-boot
@@ -2795,6 +4937,19 @@ export declare interface RunPlannedMigrationsCtx {
2795
4937
  readonly softDropRetentionDays?: number;
2796
4938
  }
2797
4939
 
4940
+ /**
4941
+ * Execute ONE schema operation against the database as it is, with the same
4942
+ * per-dialect statements the planner's applier uses — so a migration body
4943
+ * that needs to drop a foreign key, an index or a column, or rename a column,
4944
+ * gets the dialect right without spelling it. `live` is the snapshot the
4945
+ * caller just read; on sqlite it is what the rebuild is computed from.
4946
+ *
4947
+ * Existence is the CALLER's question (`fileContext.ts` reads the snapshot and
4948
+ * answers `already-absent` before reaching here). This function assumes the
4949
+ * object is there.
4950
+ */
4951
+ export declare const runSchemaOp: (sql: SqlClient.SqlClient, op: MigrationSchemaOp, live: SchemaSnapshot) => Effect.Effect<void, SqlError_2>;
4952
+
2798
4953
  /**
2799
4954
  * Effect that applies the emitted DDL to the provided SqlClient. Splits
2800
4955
  * on `;` only outside dollar-quoted blocks (the postgres NOTIFY trigger
@@ -2825,6 +4980,9 @@ export declare interface SchemaApplyOptions {
2825
4980
  readonly lockSchema?: string;
2826
4981
  }
2827
4982
 
4983
+ /** What a schema operation on the migration context found to do. */
4984
+ export declare type SchemaOpOutcome = 'applied' | 'already-absent' | 'already-present';
4985
+
2828
4986
  /**
2829
4987
  * The full schema as the planner sees it on one side of the diff.
2830
4988
  * Both the declared side and the introspected side conform to this
@@ -2847,7 +5005,9 @@ export declare const shortFingerprint: (fp: string) => string;
2847
5005
  * trip to/from `information_schema`. `idScheme.kind` IS carried — the
2848
5006
  * applier needs it to emit auto-increment DDL for numeric ids.
2849
5007
  */
2850
- export declare const snapshotColumn: (name: string, def: ColumnDefinition<unknown>) => ColumnSnapshot;
5008
+ export declare const snapshotColumn: (name: string, def: ColumnDefinition<unknown>,
5009
+ /** Needed only for `storageType` — a caller that renders DDL does not want it. */
5010
+ dialect?: DialectId) => ColumnSnapshot;
2851
5011
 
2852
5012
  /**
2853
5013
  * PostGIS spatial-distance clause carried on a {@link QueryDescriptor}.
@@ -2884,6 +5044,41 @@ declare interface SpatialClause {
2884
5044
  readonly order?: 'asc' | 'desc';
2885
5045
  }
2886
5046
 
5047
+ /**
5048
+ * PostGIS spatial-distance clause carried on a {@link QueryDescriptor}.
5049
+ * Drives a `ST_Distance` projected column (distance-as-a-value) and/or a
5050
+ * `<->` nearest-neighbour `ORDER BY` (KNN). Built by `@voltro/plugin-postgis`;
5051
+ * postgres-only (the compiler throws on other dialects).
5052
+ */
5053
+ declare interface SpatialClause_2 {
5054
+ /** The spatial column to measure from. */
5055
+ readonly column: string;
5056
+ /** The comparison geometry as WKT/EWKT (e.g. `SRID=4326;POINT(8 50)`). Bound as a parameter. */
5057
+ readonly geom: string;
5058
+ /**
5059
+ * Measure metric metres via a `::geography` cast on both operands
5060
+ * (great-circle distance). When false, distance is in the SRID's own
5061
+ * planar units (`::geometry`). Default true — the "distance in metres"
5062
+ * users expect for lat/lon data.
5063
+ */
5064
+ readonly useGeography: boolean;
5065
+ /**
5066
+ * When set, surface the distance as a projected column aliased this name
5067
+ * (`ST_Distance(col, geom) AS "<projectAs>"`). The row type gains a
5068
+ * `number` field. Absent → no distance projection (KNN-order-only).
5069
+ */
5070
+ readonly projectAs?: string;
5071
+ /**
5072
+ * When set, add a KNN `ORDER BY col <-> geom <dir>` clause (indexed
5073
+ * nearest-neighbour when a GiST index covers the column). Leads any
5074
+ * explicit `.orderBy(...)`. Absent → no spatial ordering. `<->` is always
5075
+ * a planar operator (postgres has no geography `<->`), so KNN ordering
5076
+ * ranks by planar distance even when `useGeography` is true for the
5077
+ * projected metric distance.
5078
+ */
5079
+ readonly order?: 'asc' | 'desc';
5080
+ }
5081
+
2887
5082
  /**
2888
5083
  * The `sql` template tag — captures a tagged-template literal into a
2889
5084
  * {@link RawSqlFragment} descriptor without binding it to any client.
@@ -2972,6 +5167,25 @@ declare interface SubqueryInPredicate {
2972
5167
  readonly subquery: QueryDescriptor;
2973
5168
  }
2974
5169
 
5170
+ /**
5171
+ * `col IN (SELECT col FROM ...)` / `col NOT IN (SELECT ...)` — the
5172
+ * RHS is a sub-query descriptor. The sub-query MUST project a
5173
+ * single column (via `.select('col')`); the SQL compiler emits the
5174
+ * full inner SELECT inline, and the in-memory store evaluates the
5175
+ * sub-query first + then runs the IN check against the materialized
5176
+ * set.
5177
+ *
5178
+ * The sub-query is non-correlated in v1 — it can't reference outer
5179
+ * row columns. Correlated sub-queries (`WHERE EXISTS (... WHERE
5180
+ * inner.userId = outer.id)`) would require a table-alias mechanism;
5181
+ * tracked separately.
5182
+ */
5183
+ declare interface SubqueryInPredicate_2 {
5184
+ readonly column: string;
5185
+ readonly op: 'subquery-in' | 'subquery-not-in';
5186
+ readonly subquery: QueryDescriptor_2;
5187
+ }
5188
+
2975
5189
  declare interface Table<Name extends string, Fields extends Record<string, ColumnDefinition<unknown>>, Reactive extends boolean = true, IxNames extends string = never> extends TableLike {
2976
5190
  readonly tableName: Name;
2977
5191
  readonly fields: Fields;
@@ -3507,6 +5721,11 @@ declare interface TableLike {
3507
5721
  readonly fields: Record<string, ColumnDefinition<unknown>>;
3508
5722
  }
3509
5723
 
5724
+ declare interface TableLike_2 {
5725
+ readonly tableName: string;
5726
+ readonly fields: Record<string, ColumnDefinition_2<unknown>>;
5727
+ }
5728
+
3510
5729
  /**
3511
5730
  * A named cross-table business rule, attached by `.rule(name, predicate)`.
3512
5731
  *
@@ -3560,8 +5779,26 @@ declare interface TableUnique {
3560
5779
  };
3561
5780
  }
3562
5781
 
5782
+ /**
5783
+ * A `transactional` migration whose body committed the transaction from
5784
+ * inside — DDL on the MySQL family, or an explicit COMMIT. The body's work
5785
+ * IS in the database and the migration IS recorded (running it again would
5786
+ * repeat the committed half); what failed is the guarantee the migration
5787
+ * asked for, and the run stops so nobody deploys on top of an assumption
5788
+ * that did not hold.
5789
+ */
5790
+ export declare class TransactionBrokenByBodyError extends Error {
5791
+ readonly migrationId: string;
5792
+ readonly dialect: DialectId;
5793
+ readonly _tag = "TransactionBrokenByBodyError";
5794
+ constructor(migrationId: string, dialect: DialectId);
5795
+ }
5796
+
3563
5797
  export declare const unblockLossy: (plan: MigrationPlan, scope?: ReturnType<typeof destructiveScope>) => MigrationPlan;
3564
5798
 
5799
+ /** Every deploy that did not finish — `running` (the process died) or `failed` (its undo did not complete). An absent table is an empty ledger. */
5800
+ export declare const unfinishedDeploys: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<DeployLedgerRow>, never>;
5801
+
3565
5802
  /**
3566
5803
  * Unique-constraint metadata. Set by `.unique()` (or `.unique({ dedup })`).
3567
5804
  *
@@ -3579,6 +5816,43 @@ declare interface UniqueSpec {
3579
5816
  readonly dedup?: 'fail' | 'suffix-counter' | Statement.Fragment;
3580
5817
  }
3581
5818
 
5819
+ /**
5820
+ * Unique-constraint metadata. Set by `.unique()` (or `.unique({ dedup })`).
5821
+ *
5822
+ * The planner reads `dedup` when ADD UNIQUE is applied to a populated
5823
+ * column — without it, the constraint fails for any existing duplicates.
5824
+ *
5825
+ * - `'fail'` — refuse to migrate; surface offending rows in the plan
5826
+ * output (the safe default).
5827
+ * - `'suffix-counter'` — auto-rename duplicates `value`, `value-2`,
5828
+ * `value-3`, … (only sensible for text columns).
5829
+ * - `sql\`...\`` — custom UPDATE expression keyed off `id` /
5830
+ * ROW_NUMBER() to disambiguate.
5831
+ */
5832
+ declare interface UniqueSpec_2 {
5833
+ readonly dedup?: 'fail' | 'suffix-counter' | Statement.Fragment;
5834
+ }
5835
+
5836
+ declare interface UpsertOptions {
5837
+ conflictColumns: ReadonlyArray<string>;
5838
+ update?: ReadonlyArray<string> | ((existing: Row_2) => Readonly<Record<string, unknown>>);
5839
+ /**
5840
+ * Additional assignments on CONFLICT only; never part of the INSERT row.
5841
+ * Defined values override the `update` projection (including callback results).
5842
+ * `undefined` is omitted; explicit `null`, false, zero and empty strings are
5843
+ * preserved. Cannot rewrite `id` or a conflict column. Column-list/default
5844
+ * upserts keep their dialect's native statement instead of becoming callbacks.
5845
+ */
5846
+ readonly updateValues?: Readonly<Record<string, unknown>>;
5847
+ }
5848
+
5849
+ /** What an `upsertWithOutcome` did, beside the row it wrote. */
5850
+ declare interface UpsertResult {
5851
+ readonly row: Row_2;
5852
+ /** `'updated'` means the statement landed on a row that already existed. */
5853
+ readonly outcome: 'inserted' | 'updated';
5854
+ }
5855
+
3582
5856
  /**
3583
5857
  * Vector distance metric. Drives the HNSW opclass at index-build time and
3584
5858
  * the distance operator at query time — the two MUST agree or the index
@@ -3589,6 +5863,16 @@ declare interface UniqueSpec {
3589
5863
  */
3590
5864
  declare type VectorDistance = 'cosine' | 'l2' | 'inner';
3591
5865
 
5866
+ /**
5867
+ * Vector distance metric. Drives the HNSW opclass at index-build time and
5868
+ * the distance operator at query time — the two MUST agree or the index
5869
+ * can't accelerate the search.
5870
+ * - `'cosine'` — 1 − cosine similarity (pgvector `<=>`)
5871
+ * - `'l2'` — Euclidean distance (pgvector `<->`)
5872
+ * - `'inner'` — negative inner product (pgvector `<#>`)
5873
+ */
5874
+ declare type VectorDistance_2 = 'cosine' | 'l2' | 'inner';
5875
+
3592
5876
  /**
3593
5877
  * A read-only SQL view descriptor. Structurally a `TableLike` (carries
3594
5878
  * `tableName` + `fields`) so the query builder + row decoder + by-name
@@ -3621,6 +5905,9 @@ declare interface View<Name extends string, Fields extends Record<string, Column
3621
5905
  */
3622
5906
  export declare const VOLTRO_MIGRATION_LOCK_KEY = 6322741009312437n;
3623
5907
 
5908
+ /** Index-free for the same reason `_voltro_migration_ops` is: at most a row or two, ever. */
5909
+ export declare const _voltroDeploysTable: TableLike;
5910
+
3624
5911
  /**
3625
5912
  * One aggregate column in a `SELECT` projection — `COUNT(*)`,
3626
5913
  * `SUM(amount) AS total`, etc. Built via the `count() / sum() /
@@ -3647,9 +5934,38 @@ declare interface WindowSpec {
3647
5934
  }>;
3648
5935
  }
3649
5936
 
5937
+ /**
5938
+ * One aggregate column in a `SELECT` projection — `COUNT(*)`,
5939
+ * `SUM(amount) AS total`, etc. Built via the `count() / sum() /
5940
+ * avg() / min() / max() / countDistinct()` helpers exported from
5941
+ * `@voltro/database`.
5942
+ *
5943
+ * `column` is undefined for `count()` (which compiles to `COUNT(*)`)
5944
+ * and required for every other op. `alias` is the column name on the
5945
+ * result row.
5946
+ */
5947
+ /**
5948
+ * Window-function spec (Q3). When set on an AggregateColumn, the
5949
+ * compiler emits `<OP>(...) OVER (PARTITION BY ... ORDER BY ...)`
5950
+ * instead of a plain aggregate. The query is reactive coarsely — the
5951
+ * matcher can't bucket on a window-function result, so the engine
5952
+ * widens the dependency to the whole source table and re-queries on
5953
+ * any write to it (see `runtime/relevantFields.ts`).
5954
+ */
5955
+ declare interface WindowSpec_2 {
5956
+ readonly partitionBy?: ReadonlyArray<string>;
5957
+ readonly orderBy?: ReadonlyArray<{
5958
+ column: string;
5959
+ direction: 'asc' | 'desc';
5960
+ }>;
5961
+ }
5962
+
3650
5963
  export declare const withMigrationLock: <A, E, R = never>(sql: SqlClient.SqlClient, work: Effect.Effect<A, E, R>, scope?: MigrationLockScope) => Effect.Effect<A, E | SqlError_2, R>;
3651
5964
 
3652
5965
  /** Tree of relations to eager-load. */
3653
5966
  declare type WithSpec = Readonly<Record<string, true | EagerLoadSpec>>;
3654
5967
 
5968
+ /** Tree of relations to eager-load. */
5969
+ declare type WithSpec_2 = Readonly<Record<string, true | EagerLoadSpec_2>>;
5970
+
3655
5971
  export { }