@voltro/database 0.73.0 → 0.75.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
@@ -9,6 +9,48 @@ import { Statement } from '@effect/sql';
9
9
 
10
10
  export declare const acquireMigrationLock: (sql: SqlClient.SqlClient, scope?: MigrationLockScope) => Effect.Effect<HeldMigrationLock, SqlError_2>;
11
11
 
12
+ export declare const addColumnDdl: (op: Extract<MigrationOperation, {
13
+ kind: "add-column";
14
+ }>, planned: PlannedOperation, backfillFromTable: (table: string, column: string) => {
15
+ sql?: Statement.Fragment;
16
+ js?: (row: Record<string, unknown>) => unknown | Promise<unknown>;
17
+ batchSize?: number;
18
+ sleepMs?: number;
19
+ } | undefined, dialect: "mariadb" | "postgres" | "sqlite" | "mssql") => {
20
+ pre: ReadonlyArray<string>;
21
+ backfillKind?: "sql" | "js";
22
+ backfillSql?: Statement.Fragment;
23
+ backfillJs?: (row: Record<string, unknown>) => unknown | Promise<unknown>;
24
+ batchSize?: number;
25
+ sleepMs?: number;
26
+ post: ReadonlyArray<string>;
27
+ /**
28
+ * The column declares `.unique()` and THIS emitter did not satisfy it.
29
+ *
30
+ * A column-level `.unique()` is a COLUMN FLAG, not a snapshot index — the
31
+ * planner says so itself, which is why `classifyColumnChange` has to catch a
32
+ * unique that appears on an EXISTING column. For a column the same plan
33
+ * CREATES there is no live side to compare against, so no `add-unique` op is
34
+ * ever emitted and this emitter is the only thing that can carry the flag.
35
+ *
36
+ * It carried it on exactly one of its three branches. `safe` (a nullable add)
37
+ * renders `UNIQUE` inline; `needs-default` and `needs-backfill` dropped it
38
+ * silently, so a new NOT NULL unique column arrived without its constraint,
39
+ * the re-plan then saw `declared.unique !== live.unique` and proposed
40
+ * `add-unique` — and `applyPlan` failed its own convergence proof on a plan
41
+ * that had executed without error. Reported from a MariaDB deploy where the
42
+ * first apply after an upgrade aborted and the second succeeded, because by
43
+ * then the column existed and the comparison branch could run.
44
+ *
45
+ * The flag rather than the DDL, because the statement is dialect-specific
46
+ * (mysql needs the key-prefix resolution, mssql bounds the text column first,
47
+ * sqlite rebuilds) and those emitters already exist at the call site, where
48
+ * `ctx.declared` is in scope. A second spelling here would be a second
49
+ * decision to keep in step.
50
+ */
51
+ uniquePending?: true;
52
+ };
53
+
12
54
  declare interface AggregateColumn<TResult = number | Date | null> {
13
55
  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';
14
56
  readonly column?: string;
@@ -119,13 +161,21 @@ declare type AnyView = View<string, Record<string, ColumnDefinition<unknown>>>;
119
161
  */
120
162
  export declare const appliedAtValue: (sql: SqlClient.SqlClient, iso: string) => Effect.Effect<string>;
121
163
 
164
+ export declare const appliedFileMigrationIds: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<string>, SqlError_2>;
165
+
122
166
  /**
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.
167
+ * The applied file migrations WITH the body hash recorded at the time.
168
+ *
169
+ * An empty `hash` means the row predates hash recording — the answer there is
170
+ * "cannot compare", never "the body changed". Getting that backwards would
171
+ * report drift for every migration an existing database has ever applied, on
172
+ * the first run after upgrading, which is the loudest possible way to make a
173
+ * true warning ignorable.
127
174
  */
128
- export declare const appliedFileMigrationIds: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<string>, never>;
175
+ export declare const appliedFileMigrations: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<{
176
+ readonly id: string;
177
+ readonly hash: string;
178
+ }>, SqlError_2>;
129
179
 
130
180
  /**
131
181
  * The operations the resume ledger says RAN — `started`, `added` or `applied` —
@@ -2564,6 +2614,21 @@ export declare const ensureDeployLedgerTable: (sql: SqlClient.SqlClient, dialect
2564
2614
  */
2565
2615
  export declare const ensureLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId) => Effect.Effect<void, SqlError_2>;
2566
2616
 
2617
+ /**
2618
+ * The PLAN ledger (`_voltro_migration_plans`) — same bootstrap, one caller.
2619
+ *
2620
+ * A file migration records itself here, and the file pass runs BEFORE the
2621
+ * planner that would otherwise create this table. On a database that does not
2622
+ * have it yet, `up()` therefore succeeded and the recording INSERT failed —
2623
+ * `FileMigrationLedgerError`, the one outcome whose whole message is "do not
2624
+ * re-run this". Same reason the resume ledger above is created by the applier
2625
+ * rather than planned: a run needs it before any DDL of its own has happened.
2626
+ *
2627
+ * Index-free like the others, so the planner still owns the table's shape and a
2628
+ * re-plan converges by adding the indexes it declares.
2629
+ */
2630
+ export declare const ensurePlanLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId) => Effect.Effect<void, SqlError_2>;
2631
+
2567
2632
  /** Exported for the same reason as {@link enumTypeBlocks}, and it MUST run
2568
2633
  * before it — see that function's note on ordering. */
2569
2634
  export declare const enumRenameBlocks: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => string;
@@ -2797,6 +2862,9 @@ export declare interface FileMigrationContextInput {
2797
2862
  * Its own error type because the recovery is the opposite of the ordinary one:
2798
2863
  * do NOT re-run. The change has landed; what is missing is the record that says
2799
2864
  * so, and every future invocation will apply it again until that row exists.
2865
+ * Unless the write failed because the row is ALREADY there — another process
2866
+ * recorded the same migration — and then the message says the opposite: the
2867
+ * record is correct and the body ran twice.
2800
2868
  */
2801
2869
  export declare class FileMigrationLedgerError extends Error {
2802
2870
  readonly migrationId: string;
@@ -2814,6 +2882,27 @@ export declare interface FileMigrationRunResult {
2814
2882
  readonly skipped: ReadonlyArray<string>;
2815
2883
  }
2816
2884
 
2885
+ /**
2886
+ * Applied and pending file migrations, for a caller that wants to REPORT rather
2887
+ * than apply.
2888
+ *
2889
+ * `voltro db status` had no way to ask: it walks `*.migration.ts` and reads the
2890
+ * other ledger, so against a project using `migrations/<timestamp>_<slug>.ts` it
2891
+ * answered "no migrations found" while `voltro db files` listed four. Two
2892
+ * commands that both say "migrations" and mean different files is a contradiction
2893
+ * a user is right to report as a bug, whichever one they read first.
2894
+ */
2895
+ export declare const fileMigrationStatus: (sql: SqlClient.SqlClient, projectRoot: string) => Effect.Effect<ReadonlyArray<FileMigrationStatusRow>, unknown>;
2896
+
2897
+ /** One row of `voltro db status` for a `migrations/<timestamp>_<slug>.ts` file. */
2898
+ export declare interface FileMigrationStatusRow {
2899
+ readonly id: string;
2900
+ readonly applied: boolean;
2901
+ /** The file no longer hashes to what was recorded when it ran. Always false
2902
+ * for a row applied before hashes were recorded — see `appliedFileMigrations`. */
2903
+ readonly bodyChanged: boolean;
2904
+ }
2905
+
2817
2906
  /**
2818
2907
  * Stable-hash a SchemaSnapshot. Returns the hex-encoded sha256 of the
2819
2908
  * canonicalised JSON representation. Truncated identifiers are NOT
@@ -3204,6 +3293,22 @@ export declare type MigrationAtomicity = 'plan-transactional' | 'per-operation';
3204
3293
 
3205
3294
  export declare const migrationAtomicityFor: (dialect: DialectId) => MigrationAtomicity;
3206
3295
 
3296
+ /**
3297
+ * The hash of a migration file's SOURCE, recorded beside its ledger row.
3298
+ *
3299
+ * A migration is looked up by id alone, so without this a body could be
3300
+ * rewritten after it was applied and nothing would say so — the file then reads
3301
+ * like something that ran, and is not. That is the check Liquibase and Flyway
3302
+ * make, for the same reason.
3303
+ *
3304
+ * Shared with the `.migration.ts` runner rather than spelled twice: it has
3305
+ * recorded a hash and warned on drift since it shipped, and two definitions of
3306
+ * "the hash of this file" would be two answers to one question the moment one
3307
+ * of them changed. Same algorithm, same truncation — a hash is only comparable
3308
+ * to one computed identically.
3309
+ */
3310
+ export declare const migrationBodyHash: (source: string) => string;
3311
+
3207
3312
  /** One or more `ctx.expect` post-conditions did not hold after `up`. */
3208
3313
  export declare class MigrationExpectationFailed extends Error {
3209
3314
  readonly migrationId: string;
@@ -3510,8 +3615,17 @@ export declare interface MigrationSchemaOps {
3510
3615
  readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
3511
3616
  readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
3512
3617
  }) => 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>;
3618
+ /**
3619
+ * Remove every row. `DELETE FROM` rather than TRUNCATE: it is a data
3620
+ * statement on every dialect, so it stays inside a transactional migration
3621
+ * where TRUNCATE (DDL on the MySQL family and SQL Server) would commit it.
3622
+ *
3623
+ * Answers `already-absent` for a table that is not there, like every
3624
+ * operation above — and deliberately does NOT go through the store, whose
3625
+ * set-based delete returns the deleted rows in order to fan out one change
3626
+ * event per row.
3627
+ */
3628
+ readonly truncate: (table: string) => Effect.Effect<SchemaOpOutcome, unknown>;
3515
3629
  /**
3516
3630
  * The Voltro way to change a table's shape in a migration: DECLARE the
3517
3631
  * table as it should be from here on, and the same planner that drives