@voltro/database 0.33.0 → 0.34.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 +1801 -0
- package/dist/{frameworkLiveTables-DbAgeVOU.js → frameworkLiveTables-CeuTYhWm.js} +159 -130
- package/dist/index.d.ts +774 -11
- package/dist/index.js +1039 -710
- package/dist/sql.d.ts +247 -10
- package/dist/sql.js +1269 -912
- package/package.json +2 -2
package/dist/sql.d.ts
CHANGED
|
@@ -116,11 +116,15 @@ export declare const applyNamespacedSchema: (tables: ReadonlyArray<AnyTable>, na
|
|
|
116
116
|
* 1. If the plan has ANY blocked operation, throw before touching
|
|
117
117
|
* the DB. Callers should run `voltro db plan` first + resolve.
|
|
118
118
|
* 2. Acquire the advisory lock (per-dialect; postgres v1).
|
|
119
|
-
* 3.
|
|
120
|
-
*
|
|
121
|
-
* 4.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
119
|
+
* 3. Bootstrap the resume ledger and finish any run a crash left behind
|
|
120
|
+
* (see `resolveInterruptedRun`).
|
|
121
|
+
* 4. Record every operation that will run OUTSIDE a transaction, then for
|
|
122
|
+
* each op in plan order: mark it started, emit the DDL, run any backfill,
|
|
123
|
+
* mark it applied.
|
|
124
|
+
* 5. Write a row to `_voltro_migration_plans` with the post-state
|
|
125
|
+
* fingerprint + operations JSON + duration + environment, and clear the
|
|
126
|
+
* ledger.
|
|
127
|
+
* 6. Release the advisory lock.
|
|
124
128
|
*
|
|
125
129
|
* The return value is the `AppliedMigration` row — the same shape
|
|
126
130
|
* used by the inspect RPC + the cloud dashboard timeline.
|
|
@@ -247,6 +251,78 @@ export declare type BootMigrationOutcome = {
|
|
|
247
251
|
readonly plan: MigrationPlan;
|
|
248
252
|
};
|
|
249
253
|
|
|
254
|
+
/** Which engine-level mechanism stands a branch up. `namespace` = the portable
|
|
255
|
+
* schema/database snapshot (every dialect); `neon-cow` = Neon's instant
|
|
256
|
+
* copy-on-write branch API (zero-copy, Neon only). */
|
|
257
|
+
declare type BranchMechanism = 'namespace' | 'neon-cow';
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The CI exit code.
|
|
261
|
+
*
|
|
262
|
+
* 0 — rehearsed clean and converged.
|
|
263
|
+
* 2 — a REVIEW signal: the plan destroys data, or the planner refuses part of
|
|
264
|
+
* it. The migration may well be correct; a human has to say so.
|
|
265
|
+
* 1 — the rehearsal could not answer: it failed, the branch was not faithful,
|
|
266
|
+
* or the migration did not converge.
|
|
267
|
+
*
|
|
268
|
+
* `lossy` is deliberately NOT an error. A `drop-column` in a PR is a normal,
|
|
269
|
+
* intentional thing; making it exit 1 trains people to pass `--force`, and the
|
|
270
|
+
* next real failure goes with it.
|
|
271
|
+
*/
|
|
272
|
+
export declare const branchRehearsalExitCode: (report: BranchRehearsalReport) => number;
|
|
273
|
+
|
|
274
|
+
export declare interface BranchRehearsalOps {
|
|
275
|
+
readonly branchId: string;
|
|
276
|
+
readonly mechanism: BranchMechanism;
|
|
277
|
+
readonly branchedTables: number;
|
|
278
|
+
readonly replayedForeignKeys: number;
|
|
279
|
+
/** Stand the branch up. Throwing here yields `outcome: 'failed'`. */
|
|
280
|
+
readonly provision: () => Promise<void>;
|
|
281
|
+
/**
|
|
282
|
+
* The FIDELITY check: plan the PARENT's live schema against the BRANCH. Empty
|
|
283
|
+
* ⇒ the branch is a faithful copy. Supplied as a function rather than a
|
|
284
|
+
* boolean so the caller does the introspection it already knows how to do.
|
|
285
|
+
*/
|
|
286
|
+
readonly fidelityPlan: () => Promise<MigrationPlan>;
|
|
287
|
+
/** Plan the DECLARED schema against the branch — the migration under review. */
|
|
288
|
+
readonly plan: () => Promise<MigrationPlan>;
|
|
289
|
+
/** Execute a (lossy-unblocked) plan on the branch. */
|
|
290
|
+
readonly apply: (plan: MigrationPlan) => Promise<void>;
|
|
291
|
+
/** Re-plan after the apply. Empty ⇒ converged. */
|
|
292
|
+
readonly replan: () => Promise<MigrationPlan>;
|
|
293
|
+
/** Drop the branch. Skipped when `keep`. */
|
|
294
|
+
readonly teardown: () => Promise<void>;
|
|
295
|
+
readonly keep?: boolean;
|
|
296
|
+
readonly log?: (message: string) => void;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export declare interface BranchRehearsalReport {
|
|
300
|
+
readonly branchId: string;
|
|
301
|
+
readonly mechanism: BranchMechanism;
|
|
302
|
+
readonly outcome: RehearsalOutcome;
|
|
303
|
+
/** Tables the branch was stood up with. */
|
|
304
|
+
readonly branchedTables: number;
|
|
305
|
+
/** Foreign keys replayed into the branch (`LIKE` does not copy them). */
|
|
306
|
+
readonly replayedForeignKeys: number;
|
|
307
|
+
/** Every operation the plan proposed, classified. */
|
|
308
|
+
readonly operations: ReadonlyArray<RehearsedOperation>;
|
|
309
|
+
/** The subset that destroys data — what a PR comment leads with. */
|
|
310
|
+
readonly lossy: ReadonlyArray<RehearsedOperation>;
|
|
311
|
+
/** Operations still refused after the branch's lossy unblock. */
|
|
312
|
+
readonly blocked: ReadonlyArray<RehearsedOperation>;
|
|
313
|
+
/** Was the plan actually executed on the branch? */
|
|
314
|
+
readonly applied: boolean;
|
|
315
|
+
/** Did the re-plan come back empty? `false` whenever `applied` is false. */
|
|
316
|
+
readonly converged: boolean;
|
|
317
|
+
/** What the re-plan still proposes — empty iff `converged`. */
|
|
318
|
+
readonly residual: ReadonlyArray<RehearsedOperation>;
|
|
319
|
+
/** Non-empty ⇒ the branch diverged from the parent BEFORE the plan ran. */
|
|
320
|
+
readonly infidelity: ReadonlyArray<RehearsedOperation>;
|
|
321
|
+
/** Was the branch torn down (false ⇒ `--keep`, or teardown failed). */
|
|
322
|
+
readonly tornDown: boolean;
|
|
323
|
+
readonly error?: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
250
326
|
/** Partition into ≤`n`-sized chunks (disjoint, complete). Exported for the
|
|
251
327
|
* batching regression test — the introspection's correctness reduces to this
|
|
252
328
|
* producing a complete, non-overlapping partition of the table list. */
|
|
@@ -1002,6 +1078,14 @@ declare interface EagerLoadSpec {
|
|
|
1002
1078
|
readonly onJunction?: Predicate;
|
|
1003
1079
|
}
|
|
1004
1080
|
|
|
1081
|
+
export declare const emitAddIndexMysql: (op: Extract<MigrationOperation, {
|
|
1082
|
+
kind: "add-index";
|
|
1083
|
+
}>, declared: ReadonlyArray<TableLike>) => string;
|
|
1084
|
+
|
|
1085
|
+
export declare const emitCreateTableMysql: (op: Extract<MigrationOperation, {
|
|
1086
|
+
kind: "create-table";
|
|
1087
|
+
}>) => ReadonlyArray<string>;
|
|
1088
|
+
|
|
1005
1089
|
export declare const emitDropColumnDdl: (op: Extract<MigrationOperation, {
|
|
1006
1090
|
kind: "drop-column";
|
|
1007
1091
|
}>, q: typeof quote) => string;
|
|
@@ -1191,6 +1275,10 @@ export declare interface FileMigrationRunResult {
|
|
|
1191
1275
|
*/
|
|
1192
1276
|
export declare const fingerprintSchema: (snapshot: SchemaSnapshot) => string;
|
|
1193
1277
|
|
|
1278
|
+
/** Human/PR-comment rendering. Markdown-free on purpose — the CI job that posts
|
|
1279
|
+
* it to a PR wraps it, and a terminal reader gets the same text. */
|
|
1280
|
+
export declare const formatBranchRehearsal: (report: BranchRehearsalReport) => string;
|
|
1281
|
+
|
|
1194
1282
|
/** Human report, or `undefined` when the database and the schema agree. */
|
|
1195
1283
|
export declare const formatReactiveTriggerDrift: (drift: ReactiveTriggerDrift) => string | undefined;
|
|
1196
1284
|
|
|
@@ -1598,6 +1686,25 @@ declare interface MixinIndex {
|
|
|
1598
1686
|
readonly kindOptions?: IndexKindOptions;
|
|
1599
1687
|
}
|
|
1600
1688
|
|
|
1689
|
+
/**
|
|
1690
|
+
* How long a prefix a MySQL/MariaDB index key may take on this column.
|
|
1691
|
+
*
|
|
1692
|
+
* `undefined` when the column cannot be found — an introspected-only column
|
|
1693
|
+
* with no live declaration. The caller then emits an unprefixed key, which is
|
|
1694
|
+
* right for every non-text type and only risks "key too long" for an undeclared
|
|
1695
|
+
* text one.
|
|
1696
|
+
*
|
|
1697
|
+
* A second guard, deliberately, and not because the first one is doubted: the
|
|
1698
|
+
* defect above was a renderer losing the column's parameters on the way to a
|
|
1699
|
+
* type decision, and that is a mistake a future path can make again. This one
|
|
1700
|
+
* cannot emit a prefix longer than the column, whatever concluded it needed
|
|
1701
|
+
* one — `min(191, declaredLength)`, which is what the reporter proposed.
|
|
1702
|
+
*
|
|
1703
|
+
* `undefined` = no prefix clause at all (the column is not text-like, or is
|
|
1704
|
+
* unbounded and takes the full 191).
|
|
1705
|
+
*/
|
|
1706
|
+
export declare const mysqlIndexPrefixFor: (col: ColumnSnapshot | undefined) => number | undefined;
|
|
1707
|
+
|
|
1601
1708
|
/**
|
|
1602
1709
|
* Type-narrowing migration marker. Tells the planner that this column
|
|
1603
1710
|
* previously had type `from` and is now being narrowed/converted to its
|
|
@@ -1824,6 +1931,18 @@ declare interface PredicateLeaf {
|
|
|
1824
1931
|
* Imperative `applyNamespacedSchema` against a fresh SqlClient — the
|
|
1825
1932
|
* entry point the CLI / runtime use to provision a tenant's namespace
|
|
1826
1933
|
* (eager at migrate time, or lazily on first use).
|
|
1934
|
+
*
|
|
1935
|
+
* **This is where `lifecycle: 'onTenantCreate'` seeds fire**, after the DDL
|
|
1936
|
+
* lands and before this resolves — so a caller that awaits provisioning gets
|
|
1937
|
+
* a tenant whose tables AND reference data exist, or an error. It was the
|
|
1938
|
+
* missing half: the lifecycle validated, registered, and never ran, so every
|
|
1939
|
+
* tenant came up empty and nothing said so.
|
|
1940
|
+
*
|
|
1941
|
+
* The seeds run through the hook the CLI installs at boot
|
|
1942
|
+
* (`setTenantCreateSeedHook`); with no seeds declared this costs one null
|
|
1943
|
+
* check. A seed failure REJECTS — see `fireTenantCreateSeeds` for why, and
|
|
1944
|
+
* note that re-calling this function is safe (the DDL is `IF NOT EXISTS` and
|
|
1945
|
+
* seed steps are idempotent by contract).
|
|
1827
1946
|
*/
|
|
1828
1947
|
export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>, namespace: string, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId) => Promise<void>;
|
|
1829
1948
|
|
|
@@ -2110,11 +2229,22 @@ export declare interface RawSqlFragment {
|
|
|
2110
2229
|
readonly strings: ReadonlyArray<string>;
|
|
2111
2230
|
readonly values: ReadonlyArray<unknown>;
|
|
2112
2231
|
/**
|
|
2113
|
-
* Tables this raw read depends on, for reactive invalidation. Raw
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
* `store.raw(fragment, { dependsOn })`)
|
|
2117
|
-
*
|
|
2232
|
+
* Tables this raw read depends on, for reactive invalidation. Raw reads are
|
|
2233
|
+
* otherwise untracked — the query planner cannot infer which tables an
|
|
2234
|
+
* arbitrary SQL string touches. Declare them here (or via
|
|
2235
|
+
* `store.raw(fragment, { dependsOn })`).
|
|
2236
|
+
*
|
|
2237
|
+
* WHAT IT DRIVES, stated narrowly because the previous wording promised more
|
|
2238
|
+
* than any code delivered. For a live query whose handler returns a COMPUTED
|
|
2239
|
+
* VALUE — the shape whose handler is genuinely re-run on a change — these
|
|
2240
|
+
* tables JOIN the query's declared `source:`, so a write to one recomputes the
|
|
2241
|
+
* subscription. That is the only shape where re-running can refresh a raw
|
|
2242
|
+
* result. A handler that returns a query DESCRIPTOR re-runs the DESCRIPTOR on
|
|
2243
|
+
* a change, not the handler, so declaring tables cannot refresh the raw read
|
|
2244
|
+
* there; the runtime warns about that case at subscribe time instead.
|
|
2245
|
+
*
|
|
2246
|
+
* Best-effort in both directions: the tables are recorded as declared and
|
|
2247
|
+
* never validated against the SQL.
|
|
2118
2248
|
*/
|
|
2119
2249
|
readonly dependsOn?: ReadonlyArray<string>;
|
|
2120
2250
|
}
|
|
@@ -2128,6 +2258,20 @@ export declare interface ReactiveTriggerDrift {
|
|
|
2128
2258
|
/** Tables that carry a trigger but are declared `.nonReactive()` — their
|
|
2129
2259
|
* writes still hit the NOTIFY channel and cost WAL for nothing. */
|
|
2130
2260
|
readonly stale: ReadonlyArray<string>;
|
|
2261
|
+
/**
|
|
2262
|
+
* The notify FUNCTION exists but its body predates this build
|
|
2263
|
+
* (`NOTIFY_BODY_MARKER` absent from `pg_proc.prosrc`).
|
|
2264
|
+
*
|
|
2265
|
+
* Every trigger can be present, every table correct, and the PAYLOADS still
|
|
2266
|
+
* be wrong — which is a state no per-table check can see. It matters because
|
|
2267
|
+
* of what the body changed: an oversized payload (a row over pg_notify's
|
|
2268
|
+
* 8000-byte cap) that does not carry the primary key cannot be re-read, so
|
|
2269
|
+
* the change reaches every tap as a non-event and is lost with nothing
|
|
2270
|
+
* logged. False when the function is ABSENT — nothing calls a function that
|
|
2271
|
+
* does not exist, and "no reactivity here" is the missing/stale report's
|
|
2272
|
+
* business, not this one's.
|
|
2273
|
+
*/
|
|
2274
|
+
readonly functionOutdated: boolean;
|
|
2131
2275
|
}
|
|
2132
2276
|
|
|
2133
2277
|
/**
|
|
@@ -2160,9 +2304,102 @@ export declare const reactiveTriggerRepairSql: (input: {
|
|
|
2160
2304
|
readonly missing: ReadonlyArray<string>;
|
|
2161
2305
|
/** Table names the detector found triggered-but-declared-nonReactive. */
|
|
2162
2306
|
readonly stale: ReadonlyArray<string>;
|
|
2307
|
+
/**
|
|
2308
|
+
* The installed notify FUNCTION is older than the one this build emits
|
|
2309
|
+
* (`detectReactiveTriggerDrift().functionOutdated`) — every trigger can be
|
|
2310
|
+
* present and the payloads still be wrong. Replaces the body; touches no
|
|
2311
|
+
* table.
|
|
2312
|
+
*/
|
|
2313
|
+
readonly functionOutdated?: boolean;
|
|
2163
2314
|
readonly channel?: string;
|
|
2164
2315
|
}) => ReadonlyArray<string>;
|
|
2165
2316
|
|
|
2317
|
+
/**
|
|
2318
|
+
* Record that a schema was applied even though there was nothing to DO.
|
|
2319
|
+
*
|
|
2320
|
+
* ── The deadlock this exists to remove ──────────────────────────────────────
|
|
2321
|
+
*
|
|
2322
|
+
* `applyPlan` is the only writer of `_voltro_migration_plans`, and every caller
|
|
2323
|
+
* short-circuits before it when `operations.length === 0`. So a plan with an
|
|
2324
|
+
* empty diff records nothing — and `checkProdSchema` compares the DECLARED
|
|
2325
|
+
* fingerprint against the last recorded row.
|
|
2326
|
+
*
|
|
2327
|
+
* Those two rules are individually reasonable and together they deadlock. A
|
|
2328
|
+
* consumer walked into it on an upgrade-only deploy and reproduced it in
|
|
2329
|
+
* isolation on a restored database:
|
|
2330
|
+
*
|
|
2331
|
+
* 1. `voltro serve` → refuses: declared=8aaa5c9b live=72c2305a
|
|
2332
|
+
* 2. `db files` / `db plan` / `db apply --plan` → "schema is up to date",
|
|
2333
|
+
* 0 operations, exit 0, nothing recorded
|
|
2334
|
+
* 3. `voltro serve` → the identical refusal
|
|
2335
|
+
* 4. introduce ANY real delta → apply has work, records, and
|
|
2336
|
+
* 5. `voltro serve` → boots
|
|
2337
|
+
*
|
|
2338
|
+
* There is no path from 1 to 5 through the documented commands. The migrate job
|
|
2339
|
+
* is the gate that is supposed to stop a bad release: it passes, and then every
|
|
2340
|
+
* pod CrashLoopBackOffs, pointing at the command that just succeeded. The
|
|
2341
|
+
* remaining exit is the error message's own suggestion — turn the check off —
|
|
2342
|
+
* and a safety gate disabled to work around bookkeeping stays off.
|
|
2343
|
+
*
|
|
2344
|
+
* `db drift --accept` is not the escape hatch and the consumer checked: drift
|
|
2345
|
+
* compares the LIVE side against the recorded `liveFingerprint`, and the live
|
|
2346
|
+
* side had not moved. What moved is the DECLARED side, which drift never reads.
|
|
2347
|
+
*
|
|
2348
|
+
* ── Why this is a recording rule and not a fingerprint question ─────────────
|
|
2349
|
+
*
|
|
2350
|
+
* A framework upgrade is allowed to change what the declared fingerprint hashes.
|
|
2351
|
+
* Their own note says so, and they are right. What is not allowed is for that
|
|
2352
|
+
* change to be uncatchable: an apply that ran and found nothing to do IS a
|
|
2353
|
+
* successful apply of that schema, and recording it is what makes that sentence
|
|
2354
|
+
* true tomorrow.
|
|
2355
|
+
*
|
|
2356
|
+
* ── Idempotent on purpose ──────────────────────────────────────────────────
|
|
2357
|
+
*
|
|
2358
|
+
* It writes only when the fingerprint is not already the latest recorded one.
|
|
2359
|
+
* `voltro dev` re-boots on every file save; a row per boot would put this table
|
|
2360
|
+
* on the list of things that grow without bound, which is a list this release
|
|
2361
|
+
* has spent enough time on.
|
|
2362
|
+
*/
|
|
2363
|
+
export declare const recordUpToDate: (sql: SqlClient.SqlClient, plan: MigrationPlan, ctx: {
|
|
2364
|
+
readonly appliedBy: string;
|
|
2365
|
+
readonly environment: "dev" | "staging" | "prod";
|
|
2366
|
+
readonly source: string;
|
|
2367
|
+
}) => Effect.Effect<"recorded" | "already-current", never>;
|
|
2368
|
+
|
|
2369
|
+
export declare type RehearsalOutcome =
|
|
2370
|
+
/** The plan applied on the branch and the re-plan was empty. Nothing lossy. */
|
|
2371
|
+
'clean'
|
|
2372
|
+
/** Applied + converged, but the plan destroys data. A review signal, not a failure. */
|
|
2373
|
+
| 'lossy'
|
|
2374
|
+
/** The plan carries operations the planner refuses to auto-apply even on a
|
|
2375
|
+
* throwaway branch (`needs-rename-annotation`, `multi-step`). */
|
|
2376
|
+
| 'blocked'
|
|
2377
|
+
/** Applied, and the re-plan STILL proposes work — the migration does not converge. */
|
|
2378
|
+
| 'diverged'
|
|
2379
|
+
/** The branch is not a faithful copy of the parent, so nothing below it can be trusted. */
|
|
2380
|
+
| 'infidelity'
|
|
2381
|
+
/** Provisioning or the apply threw. */
|
|
2382
|
+
| 'failed';
|
|
2383
|
+
|
|
2384
|
+
/** One operation the rehearsal wants a human to look at, flattened for a PR comment. */
|
|
2385
|
+
export declare interface RehearsedOperation {
|
|
2386
|
+
readonly kind: string;
|
|
2387
|
+
readonly table: string | undefined;
|
|
2388
|
+
readonly classification: PlannedOperation['classification'];
|
|
2389
|
+
readonly reason: string | undefined;
|
|
2390
|
+
/** The `fix` the planner would have printed had this run against production. */
|
|
2391
|
+
readonly blockedFix: string | undefined;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
export declare const rehearsedOperation: (planned: PlannedOperation) => RehearsedOperation;
|
|
2395
|
+
|
|
2396
|
+
/**
|
|
2397
|
+
* Run the rehearsal. Never throws for a database-side failure — a rehearsal that
|
|
2398
|
+
* blows up is itself a result the caller has to report, and swallowing the
|
|
2399
|
+
* teardown behind an exception is how branches leak.
|
|
2400
|
+
*/
|
|
2401
|
+
export declare const rehearseMigrationOnBranch: (ops: BranchRehearsalOps) => Promise<BranchRehearsalReport>;
|
|
2402
|
+
|
|
2166
2403
|
export declare const releaseMigrationLock: (sql: SqlClient.SqlClient) => Effect.Effect<void, SqlError_2>;
|
|
2167
2404
|
|
|
2168
2405
|
export declare const renderColumnMysql: (col: ColumnSnapshot) => string;
|