@voltro/database 0.12.0 → 0.14.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
@@ -29,6 +29,14 @@ export declare interface AppliedMigration {
29
29
  readonly environment: 'dev' | 'staging' | 'prod';
30
30
  readonly rollbackPlan?: ReadonlyArray<MigrationOperation>;
31
31
  readonly durationMs: number;
32
+ /**
33
+ * How many operations the applier actually EXECUTED, counted from the per-op
34
+ * results — not `operations.length`, which is what the plan asked for. The
35
+ * two can only differ if an op reports `skipped`, but the log lines quote
36
+ * this one on principle: "applied N op(s)" derived from the plan length is a
37
+ * claim about intent dressed up as a claim about the database.
38
+ */
39
+ readonly appliedOps: number;
32
40
  readonly onlineStrategy?: 'inline' | 'concurrent' | 'batched' | 'shadow-column';
33
41
  readonly source: 'auto-diff' | 'file';
34
42
  readonly notes?: string;
@@ -90,6 +98,31 @@ export declare interface ApplyPlanCtx {
90
98
  readonly source: 'auto-diff' | 'file';
91
99
  /** Optional human note via `voltro db apply --note "..."`. */
92
100
  readonly notes?: string;
101
+ /**
102
+ * Re-plan against the LIVE schema — the convergence proof.
103
+ *
104
+ * `applyPlan` calls this once, after the DDL and BEFORE it records the
105
+ * fingerprint. A plan that converged re-plans to zero operations; anything
106
+ * left is DDL that ran without error and did not take effect, and the apply
107
+ * fails rather than recording a fingerprint that describes a database state
108
+ * that does not exist.
109
+ *
110
+ * This exists because "reports success, applies nothing" is not hypothetical.
111
+ * A schema shipped 31 operations that logged `applied 31 op(s)` on every boot
112
+ * for two releases: the statements were real, postgres accepted all of them,
113
+ * and none of them changed anything (`ALTER COLUMN … TYPE text` on a text
114
+ * column; `SET DEFAULT NULL` for a default the renderer couldn't express).
115
+ * The recorded fingerprint then made the next boot short-circuit on
116
+ * "schema up to date". Both underlying defects are fixed — this is the guard
117
+ * that makes the NEXT one loud instead of permanent.
118
+ *
119
+ * It is REQUIRED, not optional, because the caller is the only place that
120
+ * knows the exact planner inputs (which tables were filtered, which were
121
+ * ignored); an applier-side re-plan would compare against a different set and
122
+ * report drift that isn't there. A caller that cannot re-plan cannot prove it
123
+ * applied anything.
124
+ */
125
+ readonly replan: (sql: SqlClient.SqlClient) => Effect.Effect<MigrationPlan, SqlError_2, SqlClient.SqlClient>;
93
126
  }
94
127
 
95
128
  /**
@@ -598,6 +631,43 @@ export declare interface ColumnSnapshot {
598
631
  * blocks a masking export until classified.
599
632
  */
600
633
  readonly safe?: boolean;
634
+ /**
635
+ * Type PARAMETERS for the three `ColumnType`s whose DDL is not determined by
636
+ * the type tag alone — `vector(n)`, `array(of)`, `enum(name, values)`.
637
+ * DECLARED-side only (introspection reports a concrete SQL type, not the
638
+ * declaration that produced it).
639
+ *
640
+ * These exist because the applier renders its DDL from a ColumnSnapshot, not
641
+ * from the `ColumnDefinition` that `migrate.ts`'s canonical `sqlType` reads.
642
+ * Without them the snapshot renderers had nothing to render and collapsed all
643
+ * three to `text` — which is not a smaller mistake than it looks. A declared
644
+ * `vector(1536)` against a live `text` column planned an `alter-column-type`
645
+ * that emitted `ALTER COLUMN … TYPE text`: valid SQL, applied successfully,
646
+ * changed nothing. The plan re-emitted it on every boot, the applier reported
647
+ * success every time, and the schema never converged. (Reported from a live
648
+ * pod: "applied 31 op(s)" with none of the 31 present in the database.)
649
+ *
650
+ * The rule for anything added later: if the applier has to RENDER it, the
651
+ * snapshot has to CARRY it — a snapshot renderer must never invent a type it
652
+ * wasn't given.
653
+ */
654
+ readonly vectorDim?: number;
655
+ readonly vectorPrecision?: 'float32' | 'half';
656
+ readonly arrayElement?: ColumnType;
657
+ readonly enumName?: string;
658
+ readonly enumValues?: ReadonlyArray<string>;
659
+ /**
660
+ * PostGIS `geography(kind, srid)` / `geometry(kind, srid)` parameters. Same
661
+ * reason as the three above: a spatial column declares `type: 'text'`, so
662
+ * WITHOUT this the applier's `add-column` created a plain `text` column and
663
+ * introspection (`USER-DEFINED` → `text`) agreed with it — no churn, no
664
+ * error, and no spatial column. DECLARED-side only.
665
+ */
666
+ readonly spatial?: {
667
+ readonly kind: 'geography' | 'geometry';
668
+ readonly geomKind: string;
669
+ readonly srid: number;
670
+ };
601
671
  }
602
672
 
603
673
  declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
@@ -610,6 +680,16 @@ declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigi
610
680
  */
611
681
  export declare const declaredSnapshot: (tables: ReadonlyArray<TableLike>, dialect?: DialectId) => SchemaSnapshot;
612
682
 
683
+ /**
684
+ * `DEFAULT <expr>` for an array-valued default.
685
+ *
686
+ * Postgres is the only dialect with native arrays, so a `array()` column there
687
+ * takes an array literal (`'{a,b}'::text[]`); every other dialect stores the
688
+ * value as JSON/TEXT and takes the json form. A default on a `json()` column
689
+ * always takes the json form — the array is the VALUE, not the storage.
690
+ */
691
+ export declare const defaultArrayClause: (value: ReadonlyArray<unknown>, column: ColumnDefinition<unknown>, dialect: DialectId) => string;
692
+
613
693
  export declare const defaultClause: (column: ColumnDefinition<unknown>, dialect: DialectId) => string | null;
614
694
 
615
695
  /**
@@ -622,6 +702,18 @@ export declare const defaultJsonClause: (value: object, dialect: DialectId) => s
622
702
 
623
703
  export declare const describeOutcome: (outcome: BootMigrationOutcome, dialectId: string) => string;
624
704
 
705
+ /**
706
+ * Compare declared reactivity against the triggers actually installed.
707
+ *
708
+ * Postgres only — it is the one dialect where reactivity is carried by DDL. The
709
+ * binlog and Change Tracking readers are configured at runtime from the same
710
+ * in-memory table list the schema produced, so they cannot drift from it.
711
+ */
712
+ export declare const detectReactiveTriggerDrift: (sql: SqlClient.SqlClient, tables: ReadonlyArray<{
713
+ readonly tableName: string;
714
+ readonly isReactive?: boolean;
715
+ }>) => Effect.Effect<ReactiveTriggerDrift, SqlError_2>;
716
+
625
717
  /** Dialect IDs the framework supports. */
626
718
  declare type DialectId = 'postgres' | 'mysql' | 'mariadb' | 'mssql' | 'sqlite' | 'turso';
627
719
 
@@ -799,6 +891,9 @@ export declare interface FileMigrationRunResult {
799
891
  */
800
892
  export declare const fingerprintSchema: (snapshot: SchemaSnapshot) => string;
801
893
 
894
+ /** Human report, or `undefined` when the database and the schema agree. */
895
+ export declare const formatReactiveTriggerDrift: (drift: ReactiveTriggerDrift) => string | undefined;
896
+
802
897
  /**
803
898
  * Fully-resolved scheme stored on the `id` column definition AFTER
804
899
  * `table()` walks the fields. For `typeid`, the prefix is always
@@ -1289,6 +1384,17 @@ export declare const parseEnumCheck: (clause: string) => {
1289
1384
  readonly values: ReadonlyArray<string>;
1290
1385
  } | null;
1291
1386
 
1387
+ /**
1388
+ * The ids of migration files present on disk but NOT yet recorded against THIS
1389
+ * database — i.e. exactly what `runFileBasedMigrations` would execute next.
1390
+ *
1391
+ * Exists so a caller can decide WHETHER to apply before applying. `voltro dev`
1392
+ * uses it to refuse unattended file migrations against a remote database: that
1393
+ * decision needs the list (to name it in the refusal) and must not have run
1394
+ * anything to get it.
1395
+ */
1396
+ export declare const pendingFileMigrationIds: (sql: SqlClient.SqlClient, projectRoot: string) => Effect.Effect<ReadonlyArray<string>, unknown>;
1397
+
1292
1398
  export declare interface PlanInput {
1293
1399
  readonly declared: ReadonlyArray<TableLike>;
1294
1400
  readonly live: SchemaSnapshot;
@@ -1403,6 +1509,17 @@ export declare interface RawSqlFragment {
1403
1509
  readonly dependsOn?: ReadonlyArray<string>;
1404
1510
  }
1405
1511
 
1512
+ /** Prefix `reactiveTableTriggerSql` names its triggers with. */
1513
+ export declare const REACTIVE_TRIGGER_PREFIX = "framework_changes_";
1514
+
1515
+ export declare interface ReactiveTriggerDrift {
1516
+ /** Tables the schema says are reactive that carry no trigger in the DB. */
1517
+ readonly missing: ReadonlyArray<string>;
1518
+ /** Tables that carry a trigger but are declared `.nonReactive()` — their
1519
+ * writes still hit the NOTIFY channel and cost WAL for nothing. */
1520
+ readonly stale: ReadonlyArray<string>;
1521
+ }
1522
+
1406
1523
  export declare const releaseMigrationLock: (sql: SqlClient.SqlClient) => Effect.Effect<void, SqlError_2>;
1407
1524
 
1408
1525
  export declare const renderColumnMysql: (col: ColumnSnapshot) => string;
@@ -1427,16 +1544,6 @@ export declare const rollbackFileBasedMigration: (sql: SqlClient.SqlClient, ctx:
1427
1544
  durationMs: number;
1428
1545
  }, unknown>;
1429
1546
 
1430
- /**
1431
- * Discover + apply every pending file-based migration under
1432
- * `projectRoot/migrations/`. Runs under the same advisory lock as
1433
- * the planner-based applier, so concurrent invocations serialise.
1434
- *
1435
- * - Returns `applied` (newly-run ids + durations) + `skipped`
1436
- * (ids that were already applied) for the caller's log line.
1437
- * - Failures abort the run + propagate as the Effect's error
1438
- * channel; the caller (dev.ts boot) is expected to refuse-to-boot.
1439
- */
1440
1547
  export declare const runFileBasedMigrations: (sql: SqlClient.SqlClient, ctx: RunFileBasedMigrationsCtx) => Effect.Effect<FileMigrationRunResult, unknown>;
1441
1548
 
1442
1549
  export declare interface RunFileBasedMigrationsCtx {
@@ -1498,6 +1605,14 @@ export declare interface SchemaSnapshot {
1498
1605
  */
1499
1606
  export declare const shortFingerprint: (fp: string) => string;
1500
1607
 
1608
+ /**
1609
+ * Snapshot a single `TableLike` declaration. Drops the runtime-only
1610
+ * fields (`computed`, `defaultFactory`, `__tsType`) that don't round-
1611
+ * trip to/from `information_schema`. `idScheme.kind` IS carried — the
1612
+ * applier needs it to emit auto-increment DDL for numeric ids.
1613
+ */
1614
+ export declare const snapshotColumn: (name: string, def: ColumnDefinition<unknown>) => ColumnSnapshot;
1615
+
1501
1616
  /**
1502
1617
  * The `sql` template tag — captures a tagged-template literal into a
1503
1618
  * {@link RawSqlFragment} descriptor without binding it to any client.
@@ -1567,7 +1682,7 @@ export declare interface SquashResult {
1567
1682
  readonly snapshotId: string;
1568
1683
  }
1569
1684
 
1570
- declare interface Table<Name extends string, Fields extends Record<string, ColumnDefinition<unknown>>, Reactive extends boolean = false, IxNames extends string = never> extends TableLike {
1685
+ declare interface Table<Name extends string, Fields extends Record<string, ColumnDefinition<unknown>>, Reactive extends boolean = true, IxNames extends string = never> extends TableLike {
1571
1686
  readonly tableName: Name;
1572
1687
  readonly fields: Fields;
1573
1688
  readonly isReactive: Reactive;
@@ -1879,11 +1994,39 @@ declare interface Table<Name extends string, Fields extends Record<string, Colum
1879
1994
  */
1880
1995
  primaryKey: <const F extends readonly [keyof Fields & string, keyof Fields & string, ...Array<keyof Fields & string>]>(fields: F) => Table<Name, Fields, Reactive, IxNames>;
1881
1996
  /**
1882
- * Opt the table into the reactive engine. Applies
1883
- * `alter table <name> replica identity full` at migration time so the WAL
1884
- * stream carries full pre-images on UPDATE/DELETE. See the schema-DSL plan.
1997
+ * Turn reactivity OFF for this table.
1998
+ *
1999
+ * Not "off across instances" off. The table emits no change events at all:
2000
+ * no local subscriber fires, no cross-instance transport carries it. That is
2001
+ * what the name says, and a version of this that only silenced the
2002
+ * cross-instance half would leave every in-process subscription live on a
2003
+ * table declared non-reactive.
2004
+ *
2005
+ * Reactivity is the default because that is what this framework is for. The
2006
+ * inverse (`.reactive()`) was the API for a long time and failed in both
2007
+ * directions silently: written on mariadb it did nothing at all, omitted on
2008
+ * postgres+cdc it meant a write on one instance never reached another
2009
+ * instance's subscribers.
2010
+ *
2011
+ * Implemented in EVERY dialect, at the store's emit:
2012
+ *
2013
+ * memory the store's single emit point drops the event
2014
+ * sqlite / same, and turso reuses that store
2015
+ * turso
2016
+ * postgres also: no `REPLICA IDENTITY FULL`, no `pg_notify` trigger
2017
+ * mysql / also: excluded from the binlog reader's table filter
2018
+ * mariadb
2019
+ * mssql also: excluded from the Change Tracking table set
2020
+ *
2021
+ * The WRITE is unaffected — this is about notification, never persistence.
2022
+ *
2023
+ * Worth using for a genuinely hot table nobody subscribes to: an append-only
2024
+ * event log, a metrics sink. On postgres it also drops `REPLICA IDENTITY
2025
+ * FULL`, which widens every UPDATE/DELETE in the WAL. Do NOT use it on a
2026
+ * table a query reads — that subscription simply never fires. `voltro dev`
2027
+ * says so at boot.
1885
2028
  */
1886
- reactive: () => Table<Name, Fields, true, IxNames>;
2029
+ nonReactive: () => Table<Name, Fields, false, IxNames>;
1887
2030
  }
1888
2031
 
1889
2032
  /**