@voltro/database 0.33.0 → 0.35.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/CHANGELOG.md +1968 -0
- package/dist/{frameworkLiveTables-DbAgeVOU.js → frameworkLiveTables-CeuTYhWm.js} +159 -130
- package/dist/index.d.ts +807 -11
- package/dist/index.js +1011 -678
- package/dist/sql.d.ts +397 -28
- package/dist/sql.js +1316 -914
- package/package.json +2 -2
package/dist/sql.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { SqlError } from '@effect/sql';
|
|
|
7
7
|
import { SqlError as SqlError_2 } from '@effect/sql/SqlError';
|
|
8
8
|
import { Statement } from '@effect/sql';
|
|
9
9
|
|
|
10
|
-
export declare const acquireMigrationLock: (sql: SqlClient.SqlClient) => Effect.Effect<void, SqlError_2>;
|
|
10
|
+
export declare const acquireMigrationLock: (sql: SqlClient.SqlClient, scope?: MigrationLockScope) => Effect.Effect<void, SqlError_2>;
|
|
11
11
|
|
|
12
12
|
declare interface AggregateColumn<TResult = number | Date | null> {
|
|
13
13
|
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';
|
|
@@ -58,6 +58,14 @@ declare type AnyTable = Table<string, Record<string, ColumnDefinition<unknown>>,
|
|
|
58
58
|
|
|
59
59
|
declare type AnyView = View<string, Record<string, ColumnDefinition<unknown>>>;
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The dialect-correct value for an `appliedAt` column, given an ISO instant.
|
|
63
|
+
*
|
|
64
|
+
* MySQL-family: UTC wall clock, `YYYY-MM-DD HH:MM:SS.fff`.
|
|
65
|
+
* Everything else: the ISO string, offset included.
|
|
66
|
+
*/
|
|
67
|
+
export declare const appliedAtValue: (sql: SqlClient.SqlClient, iso: string) => Effect.Effect<string>;
|
|
68
|
+
|
|
61
69
|
/**
|
|
62
70
|
* One row of `_voltro_migrations` — what the applier writes after a
|
|
63
71
|
* successful plan execution. Same shape exposed via inspect RPC for
|
|
@@ -106,7 +114,7 @@ export declare interface AppliedOp {
|
|
|
106
114
|
* no-op (except sqlite's ATTACH, which the splitter runs standalone and
|
|
107
115
|
* whose "already attached" error the caller can ignore).
|
|
108
116
|
*/
|
|
109
|
-
export declare const applyNamespacedSchema: (tables: ReadonlyArray<AnyTable>, namespace: string, dialect?: DialectId) => Effect.Effect<void, never, SqlClient.SqlClient>;
|
|
117
|
+
export declare const applyNamespacedSchema: (tables: ReadonlyArray<AnyTable>, namespace: string, dialect?: DialectId, options?: SchemaApplyOptions) => Effect.Effect<void, never, SqlClient.SqlClient>;
|
|
110
118
|
|
|
111
119
|
/**
|
|
112
120
|
* Execute a `MigrationPlan` against the connected database.
|
|
@@ -116,11 +124,15 @@ export declare const applyNamespacedSchema: (tables: ReadonlyArray<AnyTable>, na
|
|
|
116
124
|
* 1. If the plan has ANY blocked operation, throw before touching
|
|
117
125
|
* the DB. Callers should run `voltro db plan` first + resolve.
|
|
118
126
|
* 2. Acquire the advisory lock (per-dialect; postgres v1).
|
|
119
|
-
* 3.
|
|
120
|
-
*
|
|
121
|
-
* 4.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
127
|
+
* 3. Bootstrap the resume ledger and finish any run a crash left behind
|
|
128
|
+
* (see `resolveInterruptedRun`).
|
|
129
|
+
* 4. Record every operation that will run OUTSIDE a transaction, then for
|
|
130
|
+
* each op in plan order: mark it started, emit the DDL, run any backfill,
|
|
131
|
+
* mark it applied.
|
|
132
|
+
* 5. Write a row to `_voltro_migration_plans` with the post-state
|
|
133
|
+
* fingerprint + operations JSON + duration + environment, and clear the
|
|
134
|
+
* ledger.
|
|
135
|
+
* 6. Release the advisory lock.
|
|
124
136
|
*
|
|
125
137
|
* The return value is the `AppliedMigration` row — the same shape
|
|
126
138
|
* used by the inspect RPC + the cloud dashboard timeline.
|
|
@@ -141,6 +153,15 @@ export declare interface ApplyPlanCtx {
|
|
|
141
153
|
readonly source: 'auto-diff' | 'file';
|
|
142
154
|
/** Optional human note via `voltro db apply --note "..."`. */
|
|
143
155
|
readonly notes?: string;
|
|
156
|
+
/**
|
|
157
|
+
* Schema scope for the migration advisory lock (see `migrations/lock.ts`).
|
|
158
|
+
* Absent → resolved from configuration (`DB_SCHEMA`), which is what every
|
|
159
|
+
* CLI path wants; explicit for programmatic callers whose schema does not
|
|
160
|
+
* come from the environment. Every taker of this lock must resolve the
|
|
161
|
+
* scope the same way, or two writers in one schema stop excluding each
|
|
162
|
+
* other — worse than the cluster-wide key this replaced.
|
|
163
|
+
*/
|
|
164
|
+
readonly lockSchema?: string;
|
|
144
165
|
/**
|
|
145
166
|
* Re-plan against the LIVE schema — the convergence proof.
|
|
146
167
|
*
|
|
@@ -168,23 +189,13 @@ export declare interface ApplyPlanCtx {
|
|
|
168
189
|
readonly replan: (sql: SqlClient.SqlClient) => Effect.Effect<MigrationPlan, SqlError_2, SqlClient.SqlClient>;
|
|
169
190
|
}
|
|
170
191
|
|
|
171
|
-
/**
|
|
172
|
-
* Effect that applies the emitted DDL to the provided SqlClient. Splits
|
|
173
|
-
* on `;` only outside dollar-quoted blocks (the postgres NOTIFY trigger
|
|
174
|
-
* function body uses `$$`), so the plpgsql block stays intact as a
|
|
175
|
-
* single statement. SQLite has no `$$` quoting so the splitter is a
|
|
176
|
-
* superset.
|
|
177
|
-
*
|
|
178
|
-
* Caller passes the dialect explicitly. Defaults to `'postgres'` for
|
|
179
|
-
* call-sites that haven't migrated yet.
|
|
180
|
-
*/
|
|
181
192
|
export declare const applySchema: (tables: ReadonlyArray<AnyTable>, dialect?: DialectId,
|
|
182
193
|
/** NOTIFY channel for the reactive triggers. Pass the SAME value the store
|
|
183
194
|
* was built with. They are two halves of one contract and used to be able to
|
|
184
195
|
* disagree silently: the store listened on its `cdcChannel` while the DDL
|
|
185
196
|
* always emitted on the default, so a custom channel received nothing and
|
|
186
197
|
* raised nothing. */
|
|
187
|
-
cdcChannel?: string) => Effect.Effect<void, never, SqlClient.SqlClient>;
|
|
198
|
+
cdcChannel?: string, options?: SchemaApplyOptions) => Effect.Effect<void, never, SqlClient.SqlClient>;
|
|
188
199
|
|
|
189
200
|
/**
|
|
190
201
|
* The OPT-IN refuse gate over the advisory. By default (`VOLTRO_ROLLING_DEPLOY`
|
|
@@ -247,6 +258,78 @@ export declare type BootMigrationOutcome = {
|
|
|
247
258
|
readonly plan: MigrationPlan;
|
|
248
259
|
};
|
|
249
260
|
|
|
261
|
+
/** Which engine-level mechanism stands a branch up. `namespace` = the portable
|
|
262
|
+
* schema/database snapshot (every dialect); `neon-cow` = Neon's instant
|
|
263
|
+
* copy-on-write branch API (zero-copy, Neon only). */
|
|
264
|
+
declare type BranchMechanism = 'namespace' | 'neon-cow';
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The CI exit code.
|
|
268
|
+
*
|
|
269
|
+
* 0 — rehearsed clean and converged.
|
|
270
|
+
* 2 — a REVIEW signal: the plan destroys data, or the planner refuses part of
|
|
271
|
+
* it. The migration may well be correct; a human has to say so.
|
|
272
|
+
* 1 — the rehearsal could not answer: it failed, the branch was not faithful,
|
|
273
|
+
* or the migration did not converge.
|
|
274
|
+
*
|
|
275
|
+
* `lossy` is deliberately NOT an error. A `drop-column` in a PR is a normal,
|
|
276
|
+
* intentional thing; making it exit 1 trains people to pass `--force`, and the
|
|
277
|
+
* next real failure goes with it.
|
|
278
|
+
*/
|
|
279
|
+
export declare const branchRehearsalExitCode: (report: BranchRehearsalReport) => number;
|
|
280
|
+
|
|
281
|
+
export declare interface BranchRehearsalOps {
|
|
282
|
+
readonly branchId: string;
|
|
283
|
+
readonly mechanism: BranchMechanism;
|
|
284
|
+
readonly branchedTables: number;
|
|
285
|
+
readonly replayedForeignKeys: number;
|
|
286
|
+
/** Stand the branch up. Throwing here yields `outcome: 'failed'`. */
|
|
287
|
+
readonly provision: () => Promise<void>;
|
|
288
|
+
/**
|
|
289
|
+
* The FIDELITY check: plan the PARENT's live schema against the BRANCH. Empty
|
|
290
|
+
* ⇒ the branch is a faithful copy. Supplied as a function rather than a
|
|
291
|
+
* boolean so the caller does the introspection it already knows how to do.
|
|
292
|
+
*/
|
|
293
|
+
readonly fidelityPlan: () => Promise<MigrationPlan>;
|
|
294
|
+
/** Plan the DECLARED schema against the branch — the migration under review. */
|
|
295
|
+
readonly plan: () => Promise<MigrationPlan>;
|
|
296
|
+
/** Execute a (lossy-unblocked) plan on the branch. */
|
|
297
|
+
readonly apply: (plan: MigrationPlan) => Promise<void>;
|
|
298
|
+
/** Re-plan after the apply. Empty ⇒ converged. */
|
|
299
|
+
readonly replan: () => Promise<MigrationPlan>;
|
|
300
|
+
/** Drop the branch. Skipped when `keep`. */
|
|
301
|
+
readonly teardown: () => Promise<void>;
|
|
302
|
+
readonly keep?: boolean;
|
|
303
|
+
readonly log?: (message: string) => void;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export declare interface BranchRehearsalReport {
|
|
307
|
+
readonly branchId: string;
|
|
308
|
+
readonly mechanism: BranchMechanism;
|
|
309
|
+
readonly outcome: RehearsalOutcome;
|
|
310
|
+
/** Tables the branch was stood up with. */
|
|
311
|
+
readonly branchedTables: number;
|
|
312
|
+
/** Foreign keys replayed into the branch (`LIKE` does not copy them). */
|
|
313
|
+
readonly replayedForeignKeys: number;
|
|
314
|
+
/** Every operation the plan proposed, classified. */
|
|
315
|
+
readonly operations: ReadonlyArray<RehearsedOperation>;
|
|
316
|
+
/** The subset that destroys data — what a PR comment leads with. */
|
|
317
|
+
readonly lossy: ReadonlyArray<RehearsedOperation>;
|
|
318
|
+
/** Operations still refused after the branch's lossy unblock. */
|
|
319
|
+
readonly blocked: ReadonlyArray<RehearsedOperation>;
|
|
320
|
+
/** Was the plan actually executed on the branch? */
|
|
321
|
+
readonly applied: boolean;
|
|
322
|
+
/** Did the re-plan come back empty? `false` whenever `applied` is false. */
|
|
323
|
+
readonly converged: boolean;
|
|
324
|
+
/** What the re-plan still proposes — empty iff `converged`. */
|
|
325
|
+
readonly residual: ReadonlyArray<RehearsedOperation>;
|
|
326
|
+
/** Non-empty ⇒ the branch diverged from the parent BEFORE the plan ran. */
|
|
327
|
+
readonly infidelity: ReadonlyArray<RehearsedOperation>;
|
|
328
|
+
/** Was the branch torn down (false ⇒ `--keep`, or teardown failed). */
|
|
329
|
+
readonly tornDown: boolean;
|
|
330
|
+
readonly error?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
250
333
|
/** Partition into ≤`n`-sized chunks (disjoint, complete). Exported for the
|
|
251
334
|
* batching regression test — the introspection's correctness reduces to this
|
|
252
335
|
* producing a complete, non-overlapping partition of the table list. */
|
|
@@ -1002,6 +1085,14 @@ declare interface EagerLoadSpec {
|
|
|
1002
1085
|
readonly onJunction?: Predicate;
|
|
1003
1086
|
}
|
|
1004
1087
|
|
|
1088
|
+
export declare const emitAddIndexMysql: (op: Extract<MigrationOperation, {
|
|
1089
|
+
kind: "add-index";
|
|
1090
|
+
}>, declared: ReadonlyArray<TableLike>) => string;
|
|
1091
|
+
|
|
1092
|
+
export declare const emitCreateTableMysql: (op: Extract<MigrationOperation, {
|
|
1093
|
+
kind: "create-table";
|
|
1094
|
+
}>) => ReadonlyArray<string>;
|
|
1095
|
+
|
|
1005
1096
|
export declare const emitDropColumnDdl: (op: Extract<MigrationOperation, {
|
|
1006
1097
|
kind: "drop-column";
|
|
1007
1098
|
}>, q: typeof quote) => string;
|
|
@@ -1191,6 +1282,12 @@ export declare interface FileMigrationRunResult {
|
|
|
1191
1282
|
*/
|
|
1192
1283
|
export declare const fingerprintSchema: (snapshot: SchemaSnapshot) => string;
|
|
1193
1284
|
|
|
1285
|
+
export declare const fnv1a64: (input: string) => bigint;
|
|
1286
|
+
|
|
1287
|
+
/** Human/PR-comment rendering. Markdown-free on purpose — the CI job that posts
|
|
1288
|
+
* it to a PR wraps it, and a terminal reader gets the same text. */
|
|
1289
|
+
export declare const formatBranchRehearsal: (report: BranchRehearsalReport) => string;
|
|
1290
|
+
|
|
1194
1291
|
/** Human report, or `undefined` when the database and the schema agree. */
|
|
1195
1292
|
export declare const formatReactiveTriggerDrift: (drift: ReactiveTriggerDrift) => string | undefined;
|
|
1196
1293
|
|
|
@@ -1362,6 +1459,20 @@ export declare const isFileMigration: (value: unknown) => value is FileMigration
|
|
|
1362
1459
|
/** Type guard — narrows an unknown value to a {@link RawSqlFragment}. */
|
|
1363
1460
|
export declare const isRawSqlFragment: (value: unknown) => value is RawSqlFragment;
|
|
1364
1461
|
|
|
1462
|
+
/**
|
|
1463
|
+
* True when re-RUNNING this exact statement is a no-op if the previous attempt
|
|
1464
|
+
* (or a sibling process) already applied it. This is what makes an IN-PLACE
|
|
1465
|
+
* statement retry sound on the per-operation dialects: the auto-migrate
|
|
1466
|
+
* emitter's DDL is `IF NOT EXISTS`-shaped almost everywhere, and mysql's one
|
|
1467
|
+
* exception (plain `CREATE INDEX` — the dialect cannot spell the conditional)
|
|
1468
|
+
* is tolerated by `isReRunnableDuplicateIndex` above, which is precisely the
|
|
1469
|
+
* re-run contract this predicate leans on. Anything else (sqlite `ATTACH`,
|
|
1470
|
+
* a bare `ADD CONSTRAINT`) is NOT a retry candidate: a half-applied non-
|
|
1471
|
+
* idempotent statement retried blindly turns a transient error into a
|
|
1472
|
+
* confusing permanent one.
|
|
1473
|
+
*/
|
|
1474
|
+
export declare const isReRunSafeDdl: (statement: string, dialect: DialectId) => boolean;
|
|
1475
|
+
|
|
1365
1476
|
/**
|
|
1366
1477
|
* A json-path index entry, produced by {@link jsonIndex}. Unlike a plain
|
|
1367
1478
|
* `{ expr }` (verbatim, dialect-specific) this carries the column + path
|
|
@@ -1409,6 +1520,40 @@ declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = Mixins e
|
|
|
1409
1520
|
*/
|
|
1410
1521
|
export declare const migration: (spec: FileMigration) => FileMigration;
|
|
1411
1522
|
|
|
1523
|
+
/**
|
|
1524
|
+
* The postgres advisory-lock key for a schema. Default schema (undefined /
|
|
1525
|
+
* `'public'`) → the EXACT legacy constant (rolling-deploy pin, see above).
|
|
1526
|
+
* Otherwise FNV-1a 64 of `voltro_migration_lock:<schema>`, with the sign
|
|
1527
|
+
* bit cleared so the value is a positive signed 64-bit int (`pg_advisory_
|
|
1528
|
+
* lock(bigint)`). Collisions across schemas are possible and ACCEPTABLE —
|
|
1529
|
+
* a collision reintroduces serialization between two schemas, not a race.
|
|
1530
|
+
*/
|
|
1531
|
+
export declare const migrationLockKeyForSchema: (schema: string | undefined) => bigint;
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* The mysql/mssql lock NAME for a schema. Default → the EXACT pre-1.5 name
|
|
1535
|
+
* (`voltro_migration_<key36>`). Otherwise the schema is suffixed; MySQL
|
|
1536
|
+
* caps `GET_LOCK` names at 64 chars, so a name that would overflow falls
|
|
1537
|
+
* back to a hashed suffix (`_h<fnv-1a-64 hex>`, 46 chars total) instead of
|
|
1538
|
+
* a server error. Note mysql's `GET_LOCK` scope is the SERVER: on
|
|
1539
|
+
* mysql/mariadb `DB_SCHEMA` is not a connection pin (the database comes
|
|
1540
|
+
* from the URL), but setting it to the database name is how two apps on
|
|
1541
|
+
* one server un-share this lock.
|
|
1542
|
+
*/
|
|
1543
|
+
export declare const migrationLockNameForSchema: (schema: string | undefined) => string;
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* Which schema's migration lock to take. `schema` undefined → resolve from
|
|
1547
|
+
* configuration (`DB_SCHEMA`, the same variable `connectionConfig.ts` pins
|
|
1548
|
+
* the postgres `search_path` from); the empty string and `'public'`
|
|
1549
|
+
* normalize to the default scope. The scope must come from CONFIGURATION —
|
|
1550
|
+
* never from a per-call `current_schema()` probe, whose answer can drift
|
|
1551
|
+
* per session while the configured value cannot.
|
|
1552
|
+
*/
|
|
1553
|
+
export declare interface MigrationLockScope {
|
|
1554
|
+
readonly schema?: string | undefined;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1412
1557
|
/**
|
|
1413
1558
|
* Discriminated union for the concrete DDL operations the planner
|
|
1414
1559
|
* emits. Every variant carries the `table` (most ops act on one
|
|
@@ -1598,6 +1743,25 @@ declare interface MixinIndex {
|
|
|
1598
1743
|
readonly kindOptions?: IndexKindOptions;
|
|
1599
1744
|
}
|
|
1600
1745
|
|
|
1746
|
+
/**
|
|
1747
|
+
* How long a prefix a MySQL/MariaDB index key may take on this column.
|
|
1748
|
+
*
|
|
1749
|
+
* `undefined` when the column cannot be found — an introspected-only column
|
|
1750
|
+
* with no live declaration. The caller then emits an unprefixed key, which is
|
|
1751
|
+
* right for every non-text type and only risks "key too long" for an undeclared
|
|
1752
|
+
* text one.
|
|
1753
|
+
*
|
|
1754
|
+
* A second guard, deliberately, and not because the first one is doubted: the
|
|
1755
|
+
* defect above was a renderer losing the column's parameters on the way to a
|
|
1756
|
+
* type decision, and that is a mistake a future path can make again. This one
|
|
1757
|
+
* cannot emit a prefix longer than the column, whatever concluded it needed
|
|
1758
|
+
* one — `min(191, declaredLength)`, which is what the reporter proposed.
|
|
1759
|
+
*
|
|
1760
|
+
* `undefined` = no prefix clause at all (the column is not text-like, or is
|
|
1761
|
+
* unbounded and takes the full 191).
|
|
1762
|
+
*/
|
|
1763
|
+
export declare const mysqlIndexPrefixFor: (col: ColumnSnapshot | undefined) => number | undefined;
|
|
1764
|
+
|
|
1601
1765
|
/**
|
|
1602
1766
|
* Type-narrowing migration marker. Tells the planner that this column
|
|
1603
1767
|
* previously had type `from` and is now being narrowed/converted to its
|
|
@@ -1618,6 +1782,18 @@ declare interface NarrowedFromSpec {
|
|
|
1618
1782
|
readonly using?: string;
|
|
1619
1783
|
}
|
|
1620
1784
|
|
|
1785
|
+
/**
|
|
1786
|
+
* Postgres identifier for the trigger function backing a given channel.
|
|
1787
|
+
*
|
|
1788
|
+
* Derived from the channel, NOT a single global name, and that is the whole
|
|
1789
|
+
* point: `CREATE OR REPLACE FUNCTION framework_notify_change()` is one row in
|
|
1790
|
+
* `pg_proc` for the entire database, so two schemas applied with different
|
|
1791
|
+
* channels overwrote each other and the last one won. Everything applied
|
|
1792
|
+
* earlier then emitted on somebody else's channel — silently, because a NOTIFY
|
|
1793
|
+
* nobody listens to is not an error.
|
|
1794
|
+
*/
|
|
1795
|
+
export declare const notifyFunctionName: (channel: string) => string;
|
|
1796
|
+
|
|
1621
1797
|
/** Negation of an arbitrary sub-predicate → `NOT (...)`. Wraps any
|
|
1622
1798
|
* leaf/and/or/subquery subtree (distinct from the per-column `neq` and
|
|
1623
1799
|
* the narrow `notInSubquery`/`notExists`). */
|
|
@@ -1824,8 +2000,26 @@ declare interface PredicateLeaf {
|
|
|
1824
2000
|
* Imperative `applyNamespacedSchema` against a fresh SqlClient — the
|
|
1825
2001
|
* entry point the CLI / runtime use to provision a tenant's namespace
|
|
1826
2002
|
* (eager at migrate time, or lazily on first use).
|
|
2003
|
+
*
|
|
2004
|
+
* **This is where `lifecycle: 'onTenantCreate'` seeds fire**, after the DDL
|
|
2005
|
+
* lands and before this resolves — so a caller that awaits provisioning gets
|
|
2006
|
+
* a tenant whose tables AND reference data exist, or an error. It was the
|
|
2007
|
+
* missing half: the lifecycle validated, registered, and never ran, so every
|
|
2008
|
+
* tenant came up empty and nothing said so.
|
|
2009
|
+
*
|
|
2010
|
+
* The seeds run through `setTenantCreateSeedHook` — the hook itself IS
|
|
2011
|
+
* installed at boot (`seedRunner.ts`); with no seeds declared this costs one
|
|
2012
|
+
* null check. A seed failure REJECTS — see `fireTenantCreateSeeds` for why,
|
|
2013
|
+
* and note that re-calling this function is safe (the DDL is `IF NOT EXISTS`
|
|
2014
|
+
* and seed steps are idempotent by contract).
|
|
2015
|
+
*
|
|
2016
|
+
* Wired by BOTH boot paths through `makeTenantProvisioner` (@voltro/cli):
|
|
2017
|
+
* under `tenantIsolation: 'namespace'` on a SQL dialect, every namespace store
|
|
2018
|
+
* view provisions lazily on first use — memoised per namespace per process.
|
|
2019
|
+
* Eager provisioning is the APP's move (call this from a seed/startup over its
|
|
2020
|
+
* own tenant table); the framework has no tenant registry to enumerate.
|
|
1827
2021
|
*/
|
|
1828
|
-
export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>, namespace: string, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId) => Promise<void>;
|
|
2022
|
+
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>;
|
|
1829
2023
|
|
|
1830
2024
|
/**
|
|
1831
2025
|
* The runtime-facing materialized query — a plain bag of strings the store and
|
|
@@ -2110,11 +2304,22 @@ export declare interface RawSqlFragment {
|
|
|
2110
2304
|
readonly strings: ReadonlyArray<string>;
|
|
2111
2305
|
readonly values: ReadonlyArray<unknown>;
|
|
2112
2306
|
/**
|
|
2113
|
-
* Tables this raw read depends on, for reactive invalidation. Raw
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
* `store.raw(fragment, { dependsOn })`)
|
|
2117
|
-
*
|
|
2307
|
+
* Tables this raw read depends on, for reactive invalidation. Raw reads are
|
|
2308
|
+
* otherwise untracked — the query planner cannot infer which tables an
|
|
2309
|
+
* arbitrary SQL string touches. Declare them here (or via
|
|
2310
|
+
* `store.raw(fragment, { dependsOn })`).
|
|
2311
|
+
*
|
|
2312
|
+
* WHAT IT DRIVES, stated narrowly because the previous wording promised more
|
|
2313
|
+
* than any code delivered. For a live query whose handler returns a COMPUTED
|
|
2314
|
+
* VALUE — the shape whose handler is genuinely re-run on a change — these
|
|
2315
|
+
* tables JOIN the query's declared `source:`, so a write to one recomputes the
|
|
2316
|
+
* subscription. That is the only shape where re-running can refresh a raw
|
|
2317
|
+
* result. A handler that returns a query DESCRIPTOR re-runs the DESCRIPTOR on
|
|
2318
|
+
* a change, not the handler, so declaring tables cannot refresh the raw read
|
|
2319
|
+
* there; the runtime warns about that case at subscribe time instead.
|
|
2320
|
+
*
|
|
2321
|
+
* Best-effort in both directions: the tables are recorded as declared and
|
|
2322
|
+
* never validated against the SQL.
|
|
2118
2323
|
*/
|
|
2119
2324
|
readonly dependsOn?: ReadonlyArray<string>;
|
|
2120
2325
|
}
|
|
@@ -2128,8 +2333,31 @@ export declare interface ReactiveTriggerDrift {
|
|
|
2128
2333
|
/** Tables that carry a trigger but are declared `.nonReactive()` — their
|
|
2129
2334
|
* writes still hit the NOTIFY channel and cost WAL for nothing. */
|
|
2130
2335
|
readonly stale: ReadonlyArray<string>;
|
|
2336
|
+
/**
|
|
2337
|
+
* The notify FUNCTION exists but its body predates this build
|
|
2338
|
+
* (`NOTIFY_BODY_MARKER` absent from `pg_proc.prosrc`).
|
|
2339
|
+
*
|
|
2340
|
+
* Every trigger can be present, every table correct, and the PAYLOADS still
|
|
2341
|
+
* be wrong — which is a state no per-table check can see. It matters because
|
|
2342
|
+
* of what the body changed: an oversized payload (a row over pg_notify's
|
|
2343
|
+
* 8000-byte cap) that does not carry the primary key cannot be re-read, so
|
|
2344
|
+
* the change reaches every tap as a non-event and is lost with nothing
|
|
2345
|
+
* logged. False when the function is ABSENT — nothing calls a function that
|
|
2346
|
+
* does not exist, and "no reactivity here" is the missing/stale report's
|
|
2347
|
+
* business, not this one's.
|
|
2348
|
+
*/
|
|
2349
|
+
readonly functionOutdated: boolean;
|
|
2131
2350
|
}
|
|
2132
2351
|
|
|
2352
|
+
/**
|
|
2353
|
+
* Trigger name for one table on one channel.
|
|
2354
|
+
*
|
|
2355
|
+
* The channel is part of the name so a table reachable from two channels keeps
|
|
2356
|
+
* one trigger per channel, instead of the second install's `DROP TRIGGER IF
|
|
2357
|
+
* EXISTS` silently removing the first.
|
|
2358
|
+
*/
|
|
2359
|
+
export declare const reactiveTriggerName: (tableName: string, channel: string) => string;
|
|
2360
|
+
|
|
2133
2361
|
/**
|
|
2134
2362
|
* The statements that bring the DATABASE's change triggers back in line with
|
|
2135
2363
|
* what the schema declares.
|
|
@@ -2160,15 +2388,117 @@ export declare const reactiveTriggerRepairSql: (input: {
|
|
|
2160
2388
|
readonly missing: ReadonlyArray<string>;
|
|
2161
2389
|
/** Table names the detector found triggered-but-declared-nonReactive. */
|
|
2162
2390
|
readonly stale: ReadonlyArray<string>;
|
|
2391
|
+
/**
|
|
2392
|
+
* The installed notify FUNCTION is older than the one this build emits
|
|
2393
|
+
* (`detectReactiveTriggerDrift().functionOutdated`) — every trigger can be
|
|
2394
|
+
* present and the payloads still be wrong. Replaces the body; touches no
|
|
2395
|
+
* table.
|
|
2396
|
+
*/
|
|
2397
|
+
readonly functionOutdated?: boolean;
|
|
2163
2398
|
readonly channel?: string;
|
|
2164
2399
|
}) => ReadonlyArray<string>;
|
|
2165
2400
|
|
|
2166
|
-
|
|
2401
|
+
/**
|
|
2402
|
+
* Record that a schema was applied even though there was nothing to DO.
|
|
2403
|
+
*
|
|
2404
|
+
* ── The deadlock this exists to remove ──────────────────────────────────────
|
|
2405
|
+
*
|
|
2406
|
+
* `applyPlan` is the only writer of `_voltro_migration_plans`, and every caller
|
|
2407
|
+
* short-circuits before it when `operations.length === 0`. So a plan with an
|
|
2408
|
+
* empty diff records nothing — and `checkProdSchema` compares the DECLARED
|
|
2409
|
+
* fingerprint against the last recorded row.
|
|
2410
|
+
*
|
|
2411
|
+
* Those two rules are individually reasonable and together they deadlock. A
|
|
2412
|
+
* consumer walked into it on an upgrade-only deploy and reproduced it in
|
|
2413
|
+
* isolation on a restored database:
|
|
2414
|
+
*
|
|
2415
|
+
* 1. `voltro serve` → refuses: declared=8aaa5c9b live=72c2305a
|
|
2416
|
+
* 2. `db files` / `db plan` / `db apply --plan` → "schema is up to date",
|
|
2417
|
+
* 0 operations, exit 0, nothing recorded
|
|
2418
|
+
* 3. `voltro serve` → the identical refusal
|
|
2419
|
+
* 4. introduce ANY real delta → apply has work, records, and
|
|
2420
|
+
* 5. `voltro serve` → boots
|
|
2421
|
+
*
|
|
2422
|
+
* There is no path from 1 to 5 through the documented commands. The migrate job
|
|
2423
|
+
* is the gate that is supposed to stop a bad release: it passes, and then every
|
|
2424
|
+
* pod CrashLoopBackOffs, pointing at the command that just succeeded. The
|
|
2425
|
+
* remaining exit is the error message's own suggestion — turn the check off —
|
|
2426
|
+
* and a safety gate disabled to work around bookkeeping stays off.
|
|
2427
|
+
*
|
|
2428
|
+
* `db drift --accept` is not the escape hatch and the consumer checked: drift
|
|
2429
|
+
* compares the LIVE side against the recorded `liveFingerprint`, and the live
|
|
2430
|
+
* side had not moved. What moved is the DECLARED side, which drift never reads.
|
|
2431
|
+
*
|
|
2432
|
+
* ── Why this is a recording rule and not a fingerprint question ─────────────
|
|
2433
|
+
*
|
|
2434
|
+
* A framework upgrade is allowed to change what the declared fingerprint hashes.
|
|
2435
|
+
* Their own note says so, and they are right. What is not allowed is for that
|
|
2436
|
+
* change to be uncatchable: an apply that ran and found nothing to do IS a
|
|
2437
|
+
* successful apply of that schema, and recording it is what makes that sentence
|
|
2438
|
+
* true tomorrow.
|
|
2439
|
+
*
|
|
2440
|
+
* ── Idempotent on purpose ──────────────────────────────────────────────────
|
|
2441
|
+
*
|
|
2442
|
+
* It writes only when the fingerprint is not already the latest recorded one.
|
|
2443
|
+
* `voltro dev` re-boots on every file save; a row per boot would put this table
|
|
2444
|
+
* on the list of things that grow without bound, which is a list this release
|
|
2445
|
+
* has spent enough time on.
|
|
2446
|
+
*/
|
|
2447
|
+
export declare const recordUpToDate: (sql: SqlClient.SqlClient, plan: MigrationPlan, ctx: {
|
|
2448
|
+
readonly appliedBy: string;
|
|
2449
|
+
readonly environment: "dev" | "staging" | "prod";
|
|
2450
|
+
readonly source: string;
|
|
2451
|
+
}) => Effect.Effect<"recorded" | "already-current", never>;
|
|
2452
|
+
|
|
2453
|
+
export declare type RehearsalOutcome =
|
|
2454
|
+
/** The plan applied on the branch and the re-plan was empty. Nothing lossy. */
|
|
2455
|
+
'clean'
|
|
2456
|
+
/** Applied + converged, but the plan destroys data. A review signal, not a failure. */
|
|
2457
|
+
| 'lossy'
|
|
2458
|
+
/** The plan carries operations the planner refuses to auto-apply even on a
|
|
2459
|
+
* throwaway branch (`needs-rename-annotation`, `multi-step`). */
|
|
2460
|
+
| 'blocked'
|
|
2461
|
+
/** Applied, and the re-plan STILL proposes work — the migration does not converge. */
|
|
2462
|
+
| 'diverged'
|
|
2463
|
+
/** The branch is not a faithful copy of the parent, so nothing below it can be trusted. */
|
|
2464
|
+
| 'infidelity'
|
|
2465
|
+
/** Provisioning or the apply threw. */
|
|
2466
|
+
| 'failed';
|
|
2467
|
+
|
|
2468
|
+
/** One operation the rehearsal wants a human to look at, flattened for a PR comment. */
|
|
2469
|
+
export declare interface RehearsedOperation {
|
|
2470
|
+
readonly kind: string;
|
|
2471
|
+
readonly table: string | undefined;
|
|
2472
|
+
readonly classification: PlannedOperation['classification'];
|
|
2473
|
+
readonly reason: string | undefined;
|
|
2474
|
+
/** The `fix` the planner would have printed had this run against production. */
|
|
2475
|
+
readonly blockedFix: string | undefined;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
export declare const rehearsedOperation: (planned: PlannedOperation) => RehearsedOperation;
|
|
2479
|
+
|
|
2480
|
+
/**
|
|
2481
|
+
* Run the rehearsal. Never throws for a database-side failure — a rehearsal that
|
|
2482
|
+
* blows up is itself a result the caller has to report, and swallowing the
|
|
2483
|
+
* teardown behind an exception is how branches leak.
|
|
2484
|
+
*/
|
|
2485
|
+
export declare const rehearseMigrationOnBranch: (ops: BranchRehearsalOps) => Promise<BranchRehearsalReport>;
|
|
2486
|
+
|
|
2487
|
+
export declare const releaseMigrationLock: (sql: SqlClient.SqlClient, scope?: MigrationLockScope) => Effect.Effect<void, SqlError_2>;
|
|
2167
2488
|
|
|
2168
2489
|
export declare const renderColumnMysql: (col: ColumnSnapshot) => string;
|
|
2169
2490
|
|
|
2170
2491
|
export declare const renderColumnPg: (col: ColumnSnapshot) => string;
|
|
2171
2492
|
|
|
2493
|
+
/**
|
|
2494
|
+
* The effective lock schema: an explicit scope wins; otherwise `DB_SCHEMA`
|
|
2495
|
+
* (the one env var the CLI's connection resolver reads for the schema pin).
|
|
2496
|
+
* `undefined` = the default schema → the legacy constant key/name.
|
|
2497
|
+
*/
|
|
2498
|
+
export declare const resolveMigrationLockSchema: (scope?: MigrationLockScope) => string | undefined;
|
|
2499
|
+
|
|
2500
|
+
declare type RetryDecision = 'retry' | 'noRetry';
|
|
2501
|
+
|
|
2172
2502
|
/**
|
|
2173
2503
|
* Roll back a single file-based migration by id. Looks up the
|
|
2174
2504
|
* migration's `down` body via file discovery (the file must still be
|
|
@@ -2182,6 +2512,7 @@ export declare const renderColumnPg: (col: ColumnSnapshot) => string;
|
|
|
2182
2512
|
export declare const rollbackFileBasedMigration: (sql: SqlClient.SqlClient, ctx: {
|
|
2183
2513
|
projectRoot: string;
|
|
2184
2514
|
id: string;
|
|
2515
|
+
lockSchema?: string;
|
|
2185
2516
|
}) => Effect.Effect<{
|
|
2186
2517
|
id: string;
|
|
2187
2518
|
durationMs: number;
|
|
@@ -2269,6 +2600,9 @@ export declare interface RunFileBasedMigrationsCtx {
|
|
|
2269
2600
|
readonly projectRoot: string;
|
|
2270
2601
|
readonly env: 'dev' | 'staging' | 'prod';
|
|
2271
2602
|
readonly appliedBy: string;
|
|
2603
|
+
/** Schema scope for the migration advisory lock (see `migrations/lock.ts`).
|
|
2604
|
+
* Absent → resolved from configuration (`DB_SCHEMA`). */
|
|
2605
|
+
readonly lockSchema?: string;
|
|
2272
2606
|
}
|
|
2273
2607
|
|
|
2274
2608
|
/**
|
|
@@ -2279,7 +2613,7 @@ export declare interface RunFileBasedMigrationsCtx {
|
|
|
2279
2613
|
* `voltro_isr_cache` so framework releases that grow new columns
|
|
2280
2614
|
* don't crash old DBs at boot.
|
|
2281
2615
|
*/
|
|
2282
|
-
export declare const runFrameworkBootstrap: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId) => Promise<void>;
|
|
2616
|
+
export declare const runFrameworkBootstrap: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
|
|
2283
2617
|
|
|
2284
2618
|
/**
|
|
2285
2619
|
* Run `applySchema` against a fresh SqlClient from the provided sqlLayer.
|
|
@@ -2289,7 +2623,7 @@ export declare const runFrameworkBootstrap: (tables: ReadonlyArray<AnyTable>, sq
|
|
|
2289
2623
|
* `dialect` is passed through to the DDL emitter so SQL types + reactive
|
|
2290
2624
|
* trigger emission match the underlying engine.
|
|
2291
2625
|
*/
|
|
2292
|
-
export declare const runMigrate: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId) => Promise<void>;
|
|
2626
|
+
export declare const runMigrate: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
|
|
2293
2627
|
|
|
2294
2628
|
/**
|
|
2295
2629
|
* Top-level entry point — called once per boot. Returns a
|
|
@@ -2308,6 +2642,36 @@ export declare interface RunPlannedMigrationsCtx {
|
|
|
2308
2642
|
readonly dialectId: string;
|
|
2309
2643
|
}
|
|
2310
2644
|
|
|
2645
|
+
/**
|
|
2646
|
+
* Effect that applies the emitted DDL to the provided SqlClient. Splits
|
|
2647
|
+
* on `;` only outside dollar-quoted blocks (the postgres NOTIFY trigger
|
|
2648
|
+
* function body uses `$$`), so the plpgsql block stays intact as a
|
|
2649
|
+
* single statement. SQLite has no `$$` quoting so the splitter is a
|
|
2650
|
+
* superset.
|
|
2651
|
+
*
|
|
2652
|
+
* Caller passes the dialect explicitly. Defaults to `'postgres'` for
|
|
2653
|
+
* call-sites that haven't migrated yet.
|
|
2654
|
+
*/
|
|
2655
|
+
/**
|
|
2656
|
+
* Cross-cutting options for the auto-migrate apply entry points.
|
|
2657
|
+
*
|
|
2658
|
+
* `retryFilter` is the DIALECT's own transient-failure predicate
|
|
2659
|
+
* (`SqlDialect.retryFilter`), threaded in by the caller because this package
|
|
2660
|
+
* deliberately has NO dialect registry — the CLI loads the dialect module and
|
|
2661
|
+
* hands its predicate through (optimizations 1.6). Absent → no retry, the
|
|
2662
|
+
* pre-1.6 behavior. Present → a transient DDL failure (SQLITE_BUSY meeting
|
|
2663
|
+
* the schema lock, a deadlock victim) is retried BOUNDED instead of dying on
|
|
2664
|
+
* the first attempt while every equivalent DML statement would be retried.
|
|
2665
|
+
*
|
|
2666
|
+
* `lockSchema` scopes the migration advisory lock (see `migrations/lock.ts`);
|
|
2667
|
+
* absent → `DB_SCHEMA` from the environment, the same configuration the
|
|
2668
|
+
* connection's `search_path` pin reads.
|
|
2669
|
+
*/
|
|
2670
|
+
export declare interface SchemaApplyOptions {
|
|
2671
|
+
readonly retryFilter?: (error: unknown) => RetryDecision;
|
|
2672
|
+
readonly lockSchema?: string;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2311
2675
|
/**
|
|
2312
2676
|
* The full schema as the planner sees it on one side of the diff.
|
|
2313
2677
|
* Both the declared side and the introspected side conform to this
|
|
@@ -3094,6 +3458,11 @@ declare interface View<Name extends string, Fields extends Record<string, Column
|
|
|
3094
3458
|
* `pg_advisory_lock(bigint)` wants a number; we hard-code a constant
|
|
3095
3459
|
* inside the safe-integer range so every consumer races for the same
|
|
3096
3460
|
* lock without doing string hashing at runtime.
|
|
3461
|
+
*
|
|
3462
|
+
* This is the key for the DEFAULT schema, and it must stay EXACTLY this
|
|
3463
|
+
* value: a rolling deploy of a default-schema app has old and new
|
|
3464
|
+
* replicas migrating side by side, and they can only serialize if both
|
|
3465
|
+
* generations compute the SAME key.
|
|
3097
3466
|
*/
|
|
3098
3467
|
export declare const VOLTRO_MIGRATION_LOCK_KEY = 6322741009312437n;
|
|
3099
3468
|
|
|
@@ -3129,7 +3498,7 @@ declare interface WindowSpec {
|
|
|
3129
3498
|
* leaves the postgres lock held until that connection closes (which
|
|
3130
3499
|
* the pooled SqlClient does on Effect scope cleanup).
|
|
3131
3500
|
*/
|
|
3132
|
-
export declare const withMigrationLock: <A, E, R = never>(sql: SqlClient.SqlClient, work: Effect.Effect<A, E, R
|
|
3501
|
+
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>;
|
|
3133
3502
|
|
|
3134
3503
|
/** Tree of relations to eager-load. */
|
|
3135
3504
|
declare type WithSpec = Readonly<Record<string, true | EagerLoadSpec>>;
|