@voltro/database 0.23.0 → 0.25.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
@@ -143,6 +143,25 @@ export declare const applySchema: (tables: ReadonlyArray<AnyTable>, dialect?: Di
143
143
  * raised nothing. */
144
144
  cdcChannel?: string) => Effect.Effect<void, never, SqlClient.SqlClient>;
145
145
 
146
+ /**
147
+ * The OPT-IN refuse gate over the advisory. By default (`VOLTRO_ROLLING_DEPLOY`
148
+ * unset) this never fires — the classifier stays a `db plan` advisory, because
149
+ * the framework can't know the deploy strategy and a maintenance-window /
150
+ * scale-to-zero deploy has no overlap window. An operator who DOES rolling
151
+ * deploys opts in with `VOLTRO_ROLLING_DEPLOY=1`, turning an unsafe `db apply`
152
+ * into a hard refusal they must split (expand/contract) or explicitly `--force`.
153
+ *
154
+ * Pure — the caller supplies the resolved flags + the unsafe-op count (the ⚠
155
+ * detail is already printed by the plan renderer), so the decision is testable
156
+ * without a CLI or a DB, and the cloud managed-hosting migration wall can reuse
157
+ * exactly this verdict.
158
+ */
159
+ export declare const assessRollingDeployGate: (input: {
160
+ readonly enabled: boolean;
161
+ readonly forced: boolean;
162
+ readonly unsafeCount: number;
163
+ }) => RollingDeployGateVerdict;
164
+
146
165
  /** Per-row backfill body executed by the migration runner. */
147
166
  declare type BackfillFunction<TsType = unknown> = (row: Readonly<Record<string, unknown>>) => TsType | Promise<TsType>;
148
167
 
@@ -190,6 +209,28 @@ export declare type BootMigrationOutcome = {
190
209
  * producing a complete, non-overlapping partition of the table list. */
191
210
  export declare const chunkTables: <T>(xs: ReadonlyArray<T>, n: number) => T[][];
192
211
 
212
+ /**
213
+ * Classify a single DDL operation for rolling-deploy safety.
214
+ *
215
+ * UNSAFE (breaks old code during the overlap window):
216
+ * - `drop-column` / `drop-table` — old code reads it → error.
217
+ * - `rename-column` / `rename-table` — to old code the old name vanished.
218
+ * - `alter-column-type` — old reads may fail to decode, old writes may be
219
+ * rejected by the new type; even a "widening" is risky to assume.
220
+ * - `alter-column-nullability` → NOT NULL — old code that inserts without the
221
+ * column (or with NULL) is now rejected.
222
+ * - `add-unique` / `add-unique-composite` / `add-check` / `add-foreign-key`
223
+ * — a constraint old writes may violate the instant it exists.
224
+ *
225
+ * SAFE (old code keeps working):
226
+ * - `create-table` / `add-column` (nullable or defaulted) / `add-index` —
227
+ * additive; old code doesn't see it.
228
+ * - `drop-index` / `drop-unique*` / `drop-check` / `drop-foreign-key` —
229
+ * removing enforcement never makes an old query ERROR.
230
+ * - `alter-column-nullability` → nullable, `alter-column-default` — relaxing.
231
+ */
232
+ export declare const classifyRollingDeploySafety: (op: MigrationOperation) => RollingDeploySafety;
233
+
193
234
  declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType, HasDefault extends boolean = boolean> {
194
235
  readonly type: Type;
195
236
  readonly nullable: boolean;
@@ -358,6 +399,23 @@ declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType,
358
399
  * COSTS is per command — see `serverOnly()` below for the matrix.
359
400
  */
360
401
  readonly serverOnly?: boolean;
402
+ /**
403
+ * Optimistic-concurrency marker, set by `.version()`.
404
+ *
405
+ * The column the store INCREMENTS on every update, and the one an update may
406
+ * carry an expectation of. Two clients that read the same row and both write
407
+ * it are the normal case, not an exotic one — and without this the second
408
+ * write silently wins, which is the shape of every "my change disappeared"
409
+ * report ever filed.
410
+ *
411
+ * A TIMESTAMP cannot do this job. Two writes in the same millisecond are
412
+ * indistinguishable, and across replicas the clocks disagree; this repo has
413
+ * already lost rows to exactly that (an analytics sink dropped 7 of 40 events
414
+ * written in the same millisecond as the query bounding them). A monotonic
415
+ * integer has neither problem, and it also answers the other half of the
416
+ * question — which of two copies is NEWER — without consulting a clock.
417
+ */
418
+ readonly versionColumn?: boolean;
361
419
  /**
362
420
  * Data-sensitivity classification, set by `.sensitive(class)`. Declares that
363
421
  * this column holds personal / sensitive data of a given CLASS (`'email'`,
@@ -712,10 +770,7 @@ declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigi
712
770
  */
713
771
  export declare const declaredSnapshot: (tables: ReadonlyArray<TableLike>, dialect?: DialectId) => SchemaSnapshot;
714
772
 
715
- /** The NOTIFY channel a schema emits on when none is configured. Exported so
716
- * the store and the DDL cannot drift: they must agree, and before this they
717
- * did not — the store read `options.cdcChannel` while the trigger hardcoded
718
- * the default, so any custom channel listened to silence. */
773
+ /** The NOTIFY channel a schema emits on when none is configured. */
719
774
  export declare const DEFAULT_CDC_CHANNEL = "framework_changes";
720
775
 
721
776
  /**
@@ -769,7 +824,11 @@ export declare const destructiveScope: (raw: string | undefined) => {
769
824
  export declare const detectReactiveTriggerDrift: (sql: SqlClient.SqlClient, tables: ReadonlyArray<{
770
825
  readonly tableName: string;
771
826
  readonly isReactive?: boolean;
772
- }>) => Effect.Effect<ReactiveTriggerDrift, SqlError_2>;
827
+ }>,
828
+ /** The app's `cdcChannel`. Part of the trigger NAME on any non-default
829
+ * channel, so omitting it here reported every table as missing on a database
830
+ * where every trigger was present. */
831
+ channel?: string) => Effect.Effect<ReactiveTriggerDrift, SqlError_2>;
773
832
 
774
833
  /** Dialect IDs the framework supports. */
775
834
  declare type DialectId = 'postgres' | 'mysql' | 'mariadb' | 'mssql' | 'sqlite' | 'turso';
@@ -938,6 +997,21 @@ export declare interface FileMigrationContext {
938
997
  readonly appliedAt: string;
939
998
  }
940
999
 
1000
+ /**
1001
+ * A migration whose `up()` SUCCEEDED and whose ledger row could not be written.
1002
+ *
1003
+ * Its own error type because the recovery is the opposite of the ordinary one:
1004
+ * do NOT re-run. The change has landed; what is missing is the record that says
1005
+ * so, and every future invocation will apply it again until that row exists.
1006
+ */
1007
+ export declare class FileMigrationLedgerError extends Error {
1008
+ readonly migrationId: string;
1009
+ readonly file: string;
1010
+ readonly cause: unknown;
1011
+ readonly _tag = "FileMigrationLedgerError";
1012
+ constructor(migrationId: string, file: string, cause: unknown);
1013
+ }
1014
+
941
1015
  export declare interface FileMigrationRunResult {
942
1016
  readonly applied: ReadonlyArray<{
943
1017
  id: string;
@@ -1565,7 +1639,7 @@ export declare interface RawSqlFragment {
1565
1639
  readonly dependsOn?: ReadonlyArray<string>;
1566
1640
  }
1567
1641
 
1568
- /** Prefix `reactiveTableTriggerSql` names its triggers with. */
1642
+ /** Prefix every reactive trigger name starts with, whatever the channel. */
1569
1643
  export declare const REACTIVE_TRIGGER_PREFIX = "framework_changes_";
1570
1644
 
1571
1645
  export declare interface ReactiveTriggerDrift {
@@ -1576,6 +1650,39 @@ export declare interface ReactiveTriggerDrift {
1576
1650
  readonly stale: ReadonlyArray<string>;
1577
1651
  }
1578
1652
 
1653
+ /**
1654
+ * The statements that bring the DATABASE's change triggers back in line with
1655
+ * what the schema declares.
1656
+ *
1657
+ * This exists because the diff could not do it and said it could. Reactive
1658
+ * triggers are emitted only by the two full-schema emitters — the CREATE-
1659
+ * everything path for a fresh database, and the framework bootstrap — so a
1660
+ * table that arrived through the PLANNER (every table an existing app has
1661
+ * added since it was created) never got one. The planner has no trigger
1662
+ * dimension at all, so `db plan` reports 0 operations and `db apply` reports
1663
+ * "schema is up to date" while the drift detector is simultaneously telling
1664
+ * you 500 tables have no trigger, and pointing at `voltro db apply` as the
1665
+ * remedy. Reported by a consumer with 525 tables and 27 triggers, all 27 on
1666
+ * framework tables.
1667
+ *
1668
+ * A single instance is unaffected — its own writes reach its own subscribers
1669
+ * through the in-process path — so this is invisible until you scale out, which
1670
+ * is the worst possible time to find it.
1671
+ *
1672
+ * Postgres only: it is the one dialect where reactivity is carried by DDL. The
1673
+ * binlog / Change Tracking readers are configured at runtime from the same
1674
+ * in-memory table list, so they cannot drift from it.
1675
+ */
1676
+ export declare const reactiveTriggerRepairSql: (input: {
1677
+ readonly tables: ReadonlyArray<AnyTable>;
1678
+ readonly dialect: DialectId;
1679
+ /** Table names the detector found reactive-but-untriggered. */
1680
+ readonly missing: ReadonlyArray<string>;
1681
+ /** Table names the detector found triggered-but-declared-nonReactive. */
1682
+ readonly stale: ReadonlyArray<string>;
1683
+ readonly channel?: string;
1684
+ }) => ReadonlyArray<string>;
1685
+
1579
1686
  export declare const releaseMigrationLock: (sql: SqlClient.SqlClient) => Effect.Effect<void, SqlError_2>;
1580
1687
 
1581
1688
  export declare const renderColumnMysql: (col: ColumnSnapshot) => string;
@@ -1600,6 +1707,36 @@ export declare const rollbackFileBasedMigration: (sql: SqlClient.SqlClient, ctx:
1600
1707
  durationMs: number;
1601
1708
  }, unknown>;
1602
1709
 
1710
+ export declare interface RollingDeployGateVerdict {
1711
+ /** The apply MUST NOT proceed (opt-in gate tripped, not forced). */
1712
+ readonly refuse: boolean;
1713
+ /** The gate tripped but `--force` waved it through — surface a warning. */
1714
+ readonly warnForced: boolean;
1715
+ /** Empty unless refuse or warnForced. */
1716
+ readonly message: string;
1717
+ }
1718
+
1719
+ /** Verdict for one operation under a rolling deploy. */
1720
+ export declare type RollingDeploySafety = {
1721
+ readonly safe: true;
1722
+ } | {
1723
+ readonly safe: false;
1724
+ /** Why an instance on the OLD code breaks against the new schema. */
1725
+ readonly reason: string;
1726
+ /** The expand/contract move that keeps both code versions working. */
1727
+ readonly remedy: string;
1728
+ };
1729
+
1730
+ /**
1731
+ * The rolling-deploy-unsafe operations in a plan, each paired with its verdict.
1732
+ * Empty ⇒ the whole plan is safe to apply while an old deploy is still live.
1733
+ */
1734
+ export declare const rollingDeployUnsafeOps: (operations: ReadonlyArray<PlannedOperation>) => ReadonlyArray<{
1735
+ readonly op: PlannedOperation;
1736
+ readonly reason: string;
1737
+ readonly remedy: string;
1738
+ }>;
1739
+
1603
1740
  export declare const runFileBasedMigrations: (sql: SqlClient.SqlClient, ctx: RunFileBasedMigrationsCtx) => Effect.Effect<FileMigrationRunResult, unknown>;
1604
1741
 
1605
1742
  export declare interface RunFileBasedMigrationsCtx {