@voltro/database 0.71.1 → 0.72.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 +383 -0
- package/THIRD-PARTY-NOTICES.md +29 -1
- package/dist/frameworkLiveTables-C1WjQ_uI.js +1641 -0
- package/dist/index.d.ts +661 -30
- package/dist/index.js +831 -1060
- package/dist/sql.d.ts +3027 -711
- package/dist/sql.js +2011 -1237
- package/package.json +3 -2
- package/dist/frameworkLiveTables-D53uncZH.js +0 -1308
package/dist/sql.d.ts
CHANGED
|
@@ -22,6 +22,19 @@ declare interface AggregateColumn<TResult = number | Date | null> {
|
|
|
22
22
|
readonly _result?: TResult;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
declare interface AggregateColumn_2<TResult = number | Date | null> {
|
|
26
|
+
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';
|
|
27
|
+
readonly column?: string;
|
|
28
|
+
readonly alias: string;
|
|
29
|
+
readonly window?: WindowSpec_2;
|
|
30
|
+
/** Used by lag/lead — offset rows (default 1). */
|
|
31
|
+
readonly offset?: number;
|
|
32
|
+
/** Phantom — carries the projected/aggregated result type so
|
|
33
|
+
* `aggregate<Spec>()` can infer a precise result row. Never set at
|
|
34
|
+
* runtime; exists purely for `AggregateColumn<infer T>` inference. */
|
|
35
|
+
readonly _result?: TResult;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
/** One condition under which a failed DDL statement means "already done". */
|
|
26
39
|
export declare interface AlreadySatisfied {
|
|
27
40
|
/** Why this is safe to ignore. Read by a human, not by code. */
|
|
@@ -36,6 +49,10 @@ declare interface AndPredicate {
|
|
|
36
49
|
readonly and: ReadonlyArray<Predicate>;
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
declare interface AndPredicate_2 {
|
|
53
|
+
readonly and: ReadonlyArray<Predicate_2>;
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
/**
|
|
40
57
|
* Approximate-nearest-neighbour search clause carried on a
|
|
41
58
|
* {@link QueryDescriptor}. See {@link QueryDescriptor.annClause}.
|
|
@@ -62,6 +79,32 @@ declare interface AnnClause {
|
|
|
62
79
|
readonly alpha?: number;
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Approximate-nearest-neighbour search clause carried on a
|
|
84
|
+
* {@link QueryDescriptor}. See {@link QueryDescriptor.annClause}.
|
|
85
|
+
*/
|
|
86
|
+
declare interface AnnClause_2 {
|
|
87
|
+
/** Vector column to search. */
|
|
88
|
+
readonly column: string;
|
|
89
|
+
/** Literal query vector (array overload). Mutually exclusive with `queryText`. */
|
|
90
|
+
readonly queryVector?: ReadonlyArray<number>;
|
|
91
|
+
/** Raw query string (string overload) — embedded by the runtime before
|
|
92
|
+
* compile. Mutually exclusive with `queryVector`. */
|
|
93
|
+
readonly queryText?: string;
|
|
94
|
+
/** Distance metric — MUST match the index opclass to accelerate. Default 'cosine'. */
|
|
95
|
+
readonly distance: VectorDistance_2;
|
|
96
|
+
/** Per-query HNSW search effort (pgvector `hnsw.ef_search`). Set by `.efSearch(n)`. */
|
|
97
|
+
readonly ef?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Hybrid-search fusion weight in `[0, 1]`. Set by `hybridSearch(...)`
|
|
100
|
+
* when both an ANN ranking and an FTS filter are present: `0` = pure
|
|
101
|
+
* FTS, `1` = pure vector, `0.5` = balanced. The runtime fuses the two
|
|
102
|
+
* rank signals (Reciprocal-Rank-Fusion) weighted by this. Absent for a
|
|
103
|
+
* plain `nearestNeighbours(...)` query.
|
|
104
|
+
*/
|
|
105
|
+
readonly alpha?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
65
108
|
declare type AnyMixin = MixinDefinition<Record<string, ColumnDefinition<unknown>>>;
|
|
66
109
|
|
|
67
110
|
declare type AnyTable = Table<string, Record<string, ColumnDefinition<unknown>>, boolean, string>;
|
|
@@ -76,6 +119,24 @@ declare type AnyView = View<string, Record<string, ColumnDefinition<unknown>>>;
|
|
|
76
119
|
*/
|
|
77
120
|
export declare const appliedAtValue: (sql: SqlClient.SqlClient, iso: string) => Effect.Effect<string>;
|
|
78
121
|
|
|
122
|
+
/**
|
|
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.
|
|
127
|
+
*/
|
|
128
|
+
export declare const appliedFileMigrationIds: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<string>, never>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The operations the resume ledger says RAN — `started`, `added` or `applied` —
|
|
132
|
+
* decoded. The deploy compensation reads them for the one thing a schema diff
|
|
133
|
+
* cannot see: a rename. A column renamed a→b and then reverted by a diff alone
|
|
134
|
+
* is DROP b + ADD a, which loses the column's values; knowing the rename was
|
|
135
|
+
* applied lets the undo declare `a` as renamed from `b` and the planner emit
|
|
136
|
+
* the rename back. An absent ledger table means nothing ran.
|
|
137
|
+
*/
|
|
138
|
+
export declare const appliedLedgerOperations: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<PlannedOperation>, never>;
|
|
139
|
+
|
|
79
140
|
/**
|
|
80
141
|
* One row of `_voltro_migrations` — what the applier writes after a
|
|
81
142
|
* successful plan execution. Same shape exposed via inspect RPC for
|
|
@@ -241,6 +302,9 @@ export declare const assessRollingDeployGate: (input: {
|
|
|
241
302
|
/** Per-row backfill body executed by the migration runner. */
|
|
242
303
|
declare type BackfillFunction<TsType = unknown> = (row: Readonly<Record<string, unknown>>) => TsType | Promise<TsType>;
|
|
243
304
|
|
|
305
|
+
/** Per-row backfill body executed by the migration runner. */
|
|
306
|
+
declare type BackfillFunction_2<TsType = unknown> = (row: Readonly<Record<string, unknown>>) => TsType | Promise<TsType>;
|
|
307
|
+
|
|
244
308
|
declare interface BackfillSpec<TsType = unknown> {
|
|
245
309
|
/**
|
|
246
310
|
* `sql`: server-side expression evaluated by `UPDATE ... SET col = <expr>`.
|
|
@@ -259,6 +323,24 @@ declare interface BackfillSpec<TsType = unknown> {
|
|
|
259
323
|
readonly sleepMs?: number;
|
|
260
324
|
}
|
|
261
325
|
|
|
326
|
+
declare interface BackfillSpec_2<TsType = unknown> {
|
|
327
|
+
/**
|
|
328
|
+
* `sql`: server-side expression evaluated by `UPDATE ... SET col = <expr>`.
|
|
329
|
+
* Cheapest path — single DB round-trip, transactional, scales to
|
|
330
|
+
* millions of rows.
|
|
331
|
+
* `js`: per-row JS function. Pulled in batches, transformed locally,
|
|
332
|
+
* pushed back. Use when the value can't be expressed in SQL (calls
|
|
333
|
+
* to an embedding model, third-party API, library-only helpers).
|
|
334
|
+
* 10-100× slower than SQL for the same data — the planner warns
|
|
335
|
+
* above ~10k rows.
|
|
336
|
+
*/
|
|
337
|
+
readonly kind: 'sql' | 'js';
|
|
338
|
+
readonly sql?: Statement.Fragment;
|
|
339
|
+
readonly fn?: BackfillFunction_2<TsType>;
|
|
340
|
+
readonly batchSize?: number;
|
|
341
|
+
readonly sleepMs?: number;
|
|
342
|
+
}
|
|
343
|
+
|
|
262
344
|
export declare type BootEnvironment = 'dev' | 'staging' | 'prod';
|
|
263
345
|
|
|
264
346
|
export declare type BootMigrationOutcome = {
|
|
@@ -352,6 +434,125 @@ export declare interface BranchRehearsalReport {
|
|
|
352
434
|
readonly error?: string;
|
|
353
435
|
}
|
|
354
436
|
|
|
437
|
+
/** What a `refreshChangeCaptureExclusions()` pass changed. */
|
|
438
|
+
declare interface ChangeCaptureExclusionRefresh {
|
|
439
|
+
/** Excluded before and after — the condition is real and still there. */
|
|
440
|
+
readonly stillExcluded: ReadonlyArray<string>;
|
|
441
|
+
/** Excluded before, decodable now — the schema work removed the cause. */
|
|
442
|
+
readonly readmitted: ReadonlyArray<string>;
|
|
443
|
+
/** Decodable before, excluded now — the schema work INTRODUCED the cause. */
|
|
444
|
+
readonly newlyExcluded: ReadonlyArray<string>;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* A row-change event emitted by the data store (in-memory) or the CDC
|
|
449
|
+
* consumer (Postgres logical replication). Drives the reactive engine.
|
|
450
|
+
*/
|
|
451
|
+
declare type ChangeEvent = {
|
|
452
|
+
readonly table: string;
|
|
453
|
+
readonly op: 'insert' | 'update' | 'delete';
|
|
454
|
+
readonly old: Row_2 | null;
|
|
455
|
+
readonly new: Row_2 | null;
|
|
456
|
+
/**
|
|
457
|
+
* Set when the TRANSPORT could not carry this change's row images and they
|
|
458
|
+
* had to be RECONSTRUCTED. Absent on every ordinary event, which is the only
|
|
459
|
+
* case where `old`/`new` are the images as they were at commit.
|
|
460
|
+
*
|
|
461
|
+
* Postgres caps a `pg_notify` payload at 8000 bytes, and under
|
|
462
|
+
* `changeStrategy: 'cdc'` the NOTIFY trigger is the SOLE emitter — so a row
|
|
463
|
+
* that does not fit used to arrive with both images null and every tap
|
|
464
|
+
* (search index, analytics mirror, cdc-out, row history) dropped it as a
|
|
465
|
+
* non-event, permanently, with nothing logged. The CDC consumer re-reads the
|
|
466
|
+
* row by primary key before the event reaches anyone; this field says what
|
|
467
|
+
* that recovered:
|
|
468
|
+
*
|
|
469
|
+
* - `'rehydrated'` — insert/update. `new` is the row RE-READ from the
|
|
470
|
+
* database, so it is the row as it is NOW, not necessarily the image the
|
|
471
|
+
* write that fired this event produced (a later write to the same row can
|
|
472
|
+
* already have landed). `old` is null — postgres keeps no pre-image once
|
|
473
|
+
* the transaction is gone.
|
|
474
|
+
* - `'tombstone'` — delete. `old` carries the PRIMARY KEY AND NOTHING ELSE:
|
|
475
|
+
* the row is gone, so the pre-image is unrecoverable and the key is the
|
|
476
|
+
* whole truth. Enough to REMOVE the row downstream; not a snapshot of what
|
|
477
|
+
* was deleted, and anything recording history must not treat it as one.
|
|
478
|
+
* - `'unrecovered'` — both images are null. The change is known to have
|
|
479
|
+
* happened and its content is not available (no key in the payload, the
|
|
480
|
+
* re-read failed, or the row was already gone). Re-read or resync.
|
|
481
|
+
*/
|
|
482
|
+
readonly oversized?: 'rehydrated' | 'tombstone' | 'unrecovered';
|
|
483
|
+
/**
|
|
484
|
+
* How the event reached this process. Absent/'inline' = emitted by this
|
|
485
|
+
* process's own write path; 'injected' = delivered over a cross-instance
|
|
486
|
+
* transport (`injectExternalChange` — the broadcast bus or a CDC
|
|
487
|
+
* consumer). A deployment that must act exactly once per change
|
|
488
|
+
* FLEET-WIDE combines this with `DataStore.changeScope`: on a
|
|
489
|
+
* `'local'`-scope store, acting on non-injected events covers each
|
|
490
|
+
* change exactly once (on its writer); on a `'fleet'`-scope store every
|
|
491
|
+
* replica sees the full stream (own writes included, all 'injected'),
|
|
492
|
+
* so the consumer must elect a single worker instead.
|
|
493
|
+
*/
|
|
494
|
+
readonly origin?: 'inline' | 'injected';
|
|
495
|
+
/**
|
|
496
|
+
* The trace this write happened under — the SAME id `auditPlugin` records as
|
|
497
|
+
* `AuditEvent.traceId` and `voltro logs --trace` resolves.
|
|
498
|
+
*
|
|
499
|
+
* This is the correlation bridge. Without it `plugin-row-history` knows WHAT
|
|
500
|
+
* changed and the audit sink knows WHO called and whether it was refused, and
|
|
501
|
+
* nothing joins the two. With it, "what did this call touch" is one query.
|
|
502
|
+
*
|
|
503
|
+
* Absent means the write had no request behind it: a seed, a `*.startup.tsx`,
|
|
504
|
+
* a schedule, a workflow step — or an event injected from another replica.
|
|
505
|
+
* For `origin: 'injected'` the honest answer is the ORIGINATING replica's
|
|
506
|
+
* trace or nothing at all; stamping the receiving replica's ambient trace
|
|
507
|
+
* would attribute a remote write to a local request, which is worse than
|
|
508
|
+
* silence.
|
|
509
|
+
*/
|
|
510
|
+
readonly traceId?: string;
|
|
511
|
+
/**
|
|
512
|
+
* The acting identity behind the write — the same one `audit()` stamps into
|
|
513
|
+
* `createdBy`/`updatedBy`, so a row's stamp and its change event never
|
|
514
|
+
* disagree. For an API-key subject that is the person, not the credential;
|
|
515
|
+
* which key was used is recoverable from the audit row sharing `traceId`.
|
|
516
|
+
*
|
|
517
|
+
* `null` = a resolved subject with no acting user (anonymous / system).
|
|
518
|
+
* `undefined` = no request context at all. The two are different facts.
|
|
519
|
+
*/
|
|
520
|
+
readonly subjectId?: string | null;
|
|
521
|
+
/**
|
|
522
|
+
* The rpc tag of the call that caused this write (`teams.removeSubTeamMember`).
|
|
523
|
+
*
|
|
524
|
+
* `traceId` says WHICH call; this says WHAT CALL IT WAS. The difference
|
|
525
|
+
* matters to anything rendering history for a human: a row diff carries no
|
|
526
|
+
* intent, and the same delete on a join table is a member removal, a cascade,
|
|
527
|
+
* or an expiry depending only on who asked. Absent for a write with no
|
|
528
|
+
* procedure behind it (seed, startup, migration, schedule) — same meaning as
|
|
529
|
+
* an absent `traceId`.
|
|
530
|
+
*/
|
|
531
|
+
readonly procedure?: string;
|
|
532
|
+
/**
|
|
533
|
+
* The write was made BY AN AGENT acting as `subjectId`, not by that subject
|
|
534
|
+
* directly. Mirrors `WriteAttribution.via` — see it for why the identity is
|
|
535
|
+
* NOT changed instead. Absent = a direct call, which is a fact, not a gap.
|
|
536
|
+
*/
|
|
537
|
+
readonly via?: 'agent';
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* A proven interruption in a store's change stream.
|
|
542
|
+
*
|
|
543
|
+
* Deliberately NOT a list of what was missed. A pub/sub transport keeps no log,
|
|
544
|
+
* so the missed changes are gone — the only honest recovery is to re-derive
|
|
545
|
+
* everything downstream, and a consumer that were handed a partial list would be
|
|
546
|
+
* tempted to patch instead. `reason` is for the log line; the instruction is the
|
|
547
|
+
* event itself.
|
|
548
|
+
*/
|
|
549
|
+
declare interface ChangeStreamGap {
|
|
550
|
+
/** Human-readable cause — goes into the refresh's log line. */
|
|
551
|
+
readonly reason: string;
|
|
552
|
+
/** Wall-clock ms the stream was known to be down, when it is known. */
|
|
553
|
+
readonly downForMs?: number;
|
|
554
|
+
}
|
|
555
|
+
|
|
355
556
|
/** Partition into ≤`n`-sized chunks (disjoint, complete). Exported for the
|
|
356
557
|
* batching regression test — the introspection's correctness reduces to this
|
|
357
558
|
* producing a complete, non-overlapping partition of the table list. */
|
|
@@ -379,6 +580,17 @@ export declare const chunkTables: <T>(xs: ReadonlyArray<T>, n: number) => T[][];
|
|
|
379
580
|
*/
|
|
380
581
|
export declare const classifyRollingDeploySafety: (op: MigrationOperation) => RollingDeploySafety;
|
|
381
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Every resume-ledger row, gone — for a caller that has just UNDONE the run
|
|
585
|
+
* the rows describe. A ledger that outlives the work it recorded would make
|
|
586
|
+
* the next apply resume a sequence whose effects were reverted. An absent
|
|
587
|
+
* ledger table is nothing to clear.
|
|
588
|
+
*/
|
|
589
|
+
export declare const clearResumeLedger: (sql: SqlClient.SqlClient) => Effect.Effect<void, never>;
|
|
590
|
+
|
|
591
|
+
/** A finished deploy — applied, or undone completely — leaves no row. */
|
|
592
|
+
export declare const closeDeploy: (sql: SqlClient.SqlClient, id: string) => Effect.Effect<void, unknown>;
|
|
593
|
+
|
|
382
594
|
declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType, HasDefault extends boolean = boolean> {
|
|
383
595
|
readonly type: Type;
|
|
384
596
|
readonly nullable: boolean;
|
|
@@ -801,351 +1013,1423 @@ declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType,
|
|
|
801
1013
|
readonly __tsType?: TsType;
|
|
802
1014
|
}
|
|
803
1015
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
* from `ColumnDefinition`) or live in the DB (extracted via introspection).
|
|
807
|
-
*
|
|
808
|
-
* The shape is intentionally minimal — only the bits that affect diff
|
|
809
|
-
* classification land here. Things like `idScheme`, `computed`,
|
|
810
|
-
* `defaultFactory` etc. live in the runtime layer and don't survive
|
|
811
|
-
* the round-trip to `information_schema`.
|
|
812
|
-
*/
|
|
813
|
-
export declare interface ColumnSnapshot {
|
|
814
|
-
readonly name: string;
|
|
815
|
-
readonly type: ColumnType;
|
|
1016
|
+
declare interface ColumnDefinition_2<TsType, Type extends ColumnType_2 = ColumnType_2, HasDefault extends boolean = boolean> {
|
|
1017
|
+
readonly type: Type;
|
|
816
1018
|
readonly nullable: boolean;
|
|
817
1019
|
readonly unique: boolean;
|
|
818
1020
|
readonly hasDefault: boolean;
|
|
1021
|
+
/* Excluded from this release type: __hasDefault */
|
|
819
1022
|
readonly defaultValue?: unknown;
|
|
820
1023
|
/**
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
* `
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
readonly using?: string;
|
|
830
|
-
};
|
|
1024
|
+
* Application-side default factory. When set, the MutationStore
|
|
1025
|
+
* wrapper evaluates this at insert-time IF the caller omits the
|
|
1026
|
+
* column from the row. Distinct from `defaultValue` (which is a
|
|
1027
|
+
* literal landed in the DDL DEFAULT clause). Functions can do
|
|
1028
|
+
* things DDL can't — `() => crypto.randomUUID()`,
|
|
1029
|
+
* `() => process.env.REGION`. NOT emitted in DDL.
|
|
1030
|
+
*/
|
|
1031
|
+
readonly defaultFactory?: () => unknown;
|
|
831
1032
|
/**
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
* reporting the type's theoretical maximum — mysql answers 4294967295 for
|
|
838
|
-
* `longtext`, and comparing that against a declared `undefined` would re-emit an
|
|
839
|
-
* ALTER on every boot forever.
|
|
1033
|
+
* Application-side computed value. Re-evaluated on INSERT and on
|
|
1034
|
+
* UPDATE from the rest of the row (after merging defaults on insert;
|
|
1035
|
+
* after merging the patch over the existing row on update). The
|
|
1036
|
+
* return value overwrites whatever the caller passed for this column.
|
|
1037
|
+
* NOT emitted in DDL.
|
|
840
1038
|
*
|
|
841
|
-
*
|
|
842
|
-
* lived on the column definition, was read only when rendering CREATE DDL, and
|
|
843
|
-
* never reached the comparison. So the differ was not comparing lengths wrongly
|
|
844
|
-
* — it could not see them. Adding `.maxLength(n)` to an EXISTING column planned
|
|
845
|
-
* zero operations and reported "schema is up to date" while the live column
|
|
846
|
-
* stayed `longtext`. A deployment hit that trying to apply the documented remedy
|
|
847
|
-
* for MariaDB's hash long-unique, and their contrast is what pinned it: a column
|
|
848
|
-
* bounded AT CREATION was `varchar(64)` (DDL path, fine), one bounded afterwards
|
|
849
|
-
* stayed `longtext` (diff path, blind).
|
|
1039
|
+
* Example: `slug: text().computed(row => slugify(row.title))`.
|
|
850
1040
|
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
* re-emit forever. See `sameColumnShape`.
|
|
1041
|
+
* If the computed function reads a column the row doesn't carry,
|
|
1042
|
+
* it gets `undefined` — handle that in the function.
|
|
854
1043
|
*/
|
|
855
|
-
readonly
|
|
856
|
-
readonly
|
|
857
|
-
|
|
858
|
-
readonly column: string;
|
|
859
|
-
readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
860
|
-
readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
861
|
-
/**
|
|
862
|
-
* Orphan-handling policy for adding this FK to a POPULATED column
|
|
863
|
-
* (`reference(() => t, { orphanPolicy })`). Only ever set on the
|
|
864
|
-
* DECLARED side; introspection leaves it undefined (the live DB has
|
|
865
|
-
* no notion of it). `normRef` strips it before comparison so it never
|
|
866
|
-
* causes FK churn — the planner reads it directly to classify +
|
|
867
|
-
* drive the applier's orphan pre-step. See the references-diff in
|
|
868
|
-
* `classifyColumnChange`.
|
|
869
|
-
*/
|
|
870
|
-
readonly orphanPolicy?: 'fail' | 'null' | 'delete';
|
|
871
|
-
};
|
|
872
|
-
/** Physical representation for `reference()`; defaults to `text`. */
|
|
873
|
-
readonly referenceStorage?: 'text' | 'numeric';
|
|
874
|
-
/** `oneOf(['a','b'])` → CHECK constraint values. */
|
|
875
|
-
readonly oneOf?: ReadonlyArray<string>;
|
|
876
|
-
/** Required/observed JSON validity on SQL Server. Unverified means no
|
|
877
|
-
* recognized enabled and trusted check proves the existing data valid. */
|
|
878
|
-
readonly jsonValidation?: 'value' | 'container' | 'unverified';
|
|
879
|
-
/** Live SQL Server JSON checks recognized by complete predicate shape.
|
|
880
|
-
* Names are catalog identities for DDL, not portable schema fingerprints. */
|
|
881
|
-
readonly jsonChecks?: ReadonlyArray<{
|
|
882
|
-
readonly name: string;
|
|
883
|
-
readonly validation: 'value' | 'container';
|
|
884
|
-
readonly enabled: boolean;
|
|
885
|
-
readonly trusted: boolean;
|
|
886
|
-
}>;
|
|
1044
|
+
readonly computed?: (row: Readonly<Record<string, unknown>>) => unknown;
|
|
1045
|
+
readonly onUpdate?: 'now' | (() => unknown);
|
|
1046
|
+
readonly references?: () => TableLike_2;
|
|
887
1047
|
/**
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
* `
|
|
891
|
-
*
|
|
892
|
-
* exact precision — an ALTER of precision alone is not auto-classified, same
|
|
893
|
-
* posture as `generatedAs`). See `snapshotPgType` / `snapshotMysqlType`.
|
|
1048
|
+
* Physical/TypeScript representation of a foreign key. The default is
|
|
1049
|
+
* `text`, matching Voltro's default TypeID primary key. `numeric` is for a
|
|
1050
|
+
* target declared with `id({ scheme: 'numeric' })`; it renders as the
|
|
1051
|
+
* dialect's 64-bit integer type and decodes as a JavaScript number.
|
|
894
1052
|
*/
|
|
895
|
-
readonly
|
|
896
|
-
|
|
1053
|
+
readonly referenceStorage?: 'text' | 'numeric';
|
|
1054
|
+
/** Target key; defaults to the framework convention `id`. */
|
|
1055
|
+
readonly referenceColumn?: string;
|
|
897
1056
|
/**
|
|
898
|
-
*
|
|
899
|
-
* `
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
* integer type + auto_increment, which the diff doesn't need).
|
|
1057
|
+
* Cascade-on-delete behavior for a `reference()` column. Default
|
|
1058
|
+
* `'restrict'` — Postgres / MySQL / MariaDB / MSSQL / SQLite all block
|
|
1059
|
+
* the parent delete when child rows still point at it. Pre-release
|
|
1060
|
+
* choice: `'restrict'` is safer than `'cascade'` (deleting a user
|
|
1061
|
+
* shouldn't silently nuke 100k posts) and the explicit `'cascade'`
|
|
1062
|
+
* opt-in is one keystroke when the parent-child relationship genuinely
|
|
1063
|
+
* is owned-lifecycle.
|
|
906
1064
|
*/
|
|
907
|
-
readonly
|
|
908
|
-
readonly kind: string;
|
|
909
|
-
};
|
|
1065
|
+
readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
910
1066
|
/**
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
* original fragment, so introspection leaves it undefined.
|
|
1067
|
+
* Cascade-on-update — default `'noAction'`. Rarely used in practice.
|
|
1068
|
+
* Named `refOnUpdate` (not `onUpdate`) to avoid colliding with the
|
|
1069
|
+
* existing column-level `onUpdate('now' | …)` trigger field which
|
|
1070
|
+
* has nothing to do with FK semantics.
|
|
916
1071
|
*/
|
|
917
|
-
readonly
|
|
1072
|
+
readonly refOnUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
918
1073
|
/**
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
* introspection does NOT read generation expressions, so the live snapshot
|
|
925
|
-
* leaves it undefined. The planner's `sameColumnShape` deliberately does
|
|
926
|
-
* NOT compare this field, so a declared-generated column never diffs
|
|
927
|
-
* against its introspected-as-plain live form (no churn). Consequence:
|
|
928
|
-
* the declarative engine creates generated columns correctly but does NOT
|
|
929
|
-
* detect a CHANGE to the expression after the fact — recreate the table
|
|
930
|
-
* (drop + re-migrate) to alter a generation expression.
|
|
1074
|
+
* What the RUNTIME does to this column's rows when the referenced row is
|
|
1075
|
+
* SOFT-deleted (`deletedAt` set) — set by `reference(…, { onSoftDelete })`.
|
|
1076
|
+
* A foreign key never sees a soft delete (it is an UPDATE), so `onDelete`
|
|
1077
|
+
* cannot express it; the store's post-commit change channel enforces this
|
|
1078
|
+
* one instead, the way `pluginRef` orphan rules are enforced.
|
|
931
1079
|
*/
|
|
932
|
-
readonly
|
|
933
|
-
readonly expr: string;
|
|
934
|
-
readonly stored: boolean;
|
|
935
|
-
};
|
|
1080
|
+
readonly refOnSoftDelete?: 'cascade' | 'setNull';
|
|
936
1081
|
/**
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
1082
|
+
* Auto-create a B-tree index on this FK column. Default `true` — FK
|
|
1083
|
+
* columns are nearly always read-by, and the lookup cost of a missing
|
|
1084
|
+
* index dwarfs the write cost of an unnecessary one. Opt-out
|
|
1085
|
+
* (`{ index: false }`) for the rare table where the parent-child set
|
|
1086
|
+
* is small enough that a full-scan beats the index-maintenance
|
|
1087
|
+
* overhead.
|
|
941
1088
|
*/
|
|
942
|
-
readonly
|
|
943
|
-
readonly class: string;
|
|
944
|
-
};
|
|
1089
|
+
readonly fkAutoIndex?: boolean;
|
|
945
1090
|
/**
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
1091
|
+
* This id-shaped scalar names something outside the local relational graph.
|
|
1092
|
+
* Set by `.externalId()` so schema audits do not guess a local FK from the
|
|
1093
|
+
* column name. Metadata only; it emits no constraint or DDL.
|
|
949
1094
|
*/
|
|
950
|
-
readonly
|
|
1095
|
+
readonly externalId?: boolean;
|
|
1096
|
+
readonly jsonType?: unknown;
|
|
951
1097
|
/**
|
|
952
|
-
*
|
|
953
|
-
* the type tag alone — `vector(n)`, `array(of)`, `enum(name, values)`.
|
|
954
|
-
* DECLARED-side only (introspection reports a concrete SQL type, not the
|
|
955
|
-
* declaration that produced it).
|
|
1098
|
+
* Closed value set, set by `text().oneOf([...])`. When present:
|
|
956
1099
|
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
* changed nothing. The plan re-emitted it on every boot, the applier reported
|
|
964
|
-
* success every time, and the schema never converged. (Reported from a live
|
|
965
|
-
* pod: "applied 31 op(s)" with none of the 31 present in the database.)
|
|
1100
|
+
* - The row type narrows from `string` to the union of literals.
|
|
1101
|
+
* - The Postgres migration emitter adds a `CHECK (col IN (...))`
|
|
1102
|
+
* constraint so a bad value is rejected by the DB regardless
|
|
1103
|
+
* of which client wrote it.
|
|
1104
|
+
* - The in-memory data store can validate at insert time
|
|
1105
|
+
* (helps catch test-data drift early).
|
|
966
1106
|
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
1107
|
+
* For Postgres-native ENUM types: not used here. We treat the
|
|
1108
|
+
* underlying column as `text` and lean on CHECK — that keeps adding /
|
|
1109
|
+
* removing values a one-statement migration instead of the
|
|
1110
|
+
* three-statement dance Postgres ENUMs need (`ALTER TYPE … ADD VALUE`
|
|
1111
|
+
* + rewrite, or `ALTER TYPE … RENAME VALUE`).
|
|
970
1112
|
*/
|
|
971
|
-
readonly
|
|
972
|
-
readonly vectorPrecision?: 'float32' | 'half';
|
|
973
|
-
readonly arrayElement?: ColumnType;
|
|
974
|
-
readonly enumName?: string;
|
|
975
|
-
readonly enumValues?: ReadonlyArray<string>;
|
|
1113
|
+
readonly oneOf?: ReadonlyArray<string>;
|
|
976
1114
|
/**
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
*
|
|
1115
|
+
* Bounded text length, set by `text().maxLength(n)`. When present the
|
|
1116
|
+
* migrator emits `VARCHAR(n)` (postgres / mysql / mariadb) / `NVARCHAR(n)`
|
|
1117
|
+
* (mssql) instead of the unbounded `TEXT` / `LONGTEXT` default. The point
|
|
1118
|
+
* is index-ability: a bounded column can back a plain BTREE UNIQUE or
|
|
1119
|
+
* composite index, where an unbounded text column can only take a HASH
|
|
1120
|
+
* long-unique constraint or a prefix index. `n` is a character count.
|
|
982
1121
|
*/
|
|
983
|
-
readonly
|
|
984
|
-
readonly kind: 'geography' | 'geometry';
|
|
985
|
-
readonly geomKind: string;
|
|
986
|
-
readonly srid: number;
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
|
|
991
|
-
|
|
992
|
-
/**
|
|
993
|
-
* Build a `SchemaSnapshot` from an array of `TableLike` declarations.
|
|
994
|
-
* Caller is responsible for passing the FULL set of tables (user-
|
|
995
|
-
* declared + framework). The fingerprint over this snapshot is what the
|
|
996
|
-
* boot-time prod check compares against `_voltro_migration_plans`.
|
|
997
|
-
*/
|
|
998
|
-
export declare const declaredSnapshot: (tables: ReadonlyArray<TableLike>, dialect?: DialectId) => SchemaSnapshot;
|
|
999
|
-
|
|
1000
|
-
/** The NOTIFY channel a schema emits on when none is configured. */
|
|
1001
|
-
export declare const DEFAULT_CDC_CHANNEL = "framework_changes";
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* `DEFAULT <expr>` for an array-valued default.
|
|
1005
|
-
*
|
|
1006
|
-
* Postgres is the only dialect with native arrays, so a `array()` column there
|
|
1007
|
-
* takes an array literal (`'{a,b}'::text[]`); every other dialect stores the
|
|
1008
|
-
* value as JSON/TEXT and takes the json form. A default on a `json()` column
|
|
1009
|
-
* always takes the json form — the array is the VALUE, not the storage.
|
|
1010
|
-
*/
|
|
1011
|
-
export declare const defaultArrayClause: (value: ReadonlyArray<unknown>, column: ColumnDefinition<unknown>, dialect: DialectId) => string;
|
|
1012
|
-
|
|
1013
|
-
export declare const defaultClause: (column: ColumnDefinition<unknown>, dialect: DialectId) => string | null;
|
|
1014
|
-
|
|
1015
|
-
/**
|
|
1016
|
-
* `DEFAULT <expr>` for a plain-object json default, per dialect's json-column
|
|
1017
|
-
* type. Single source for the auto-migrate (`defaultClause`) AND declarative-
|
|
1018
|
-
* applier paths so the two can't drift (the introspected form must round-trip
|
|
1019
|
-
* against `normalizeDefault`, so the emit shape matters on both sides).
|
|
1020
|
-
*/
|
|
1021
|
-
export declare const defaultJsonClause: (value: object, dialect: DialectId) => string;
|
|
1022
|
-
|
|
1023
|
-
export declare const describeOutcome: (outcome: BootMigrationOutcome, dialectId: string) => string;
|
|
1024
|
-
|
|
1025
|
-
/**
|
|
1026
|
-
* Parse `VOLTRO_DESTRUCTIVE_OK`.
|
|
1027
|
-
*
|
|
1028
|
-
* `1` / `true` acknowledges every lossy op in the run. A comma-separated list
|
|
1029
|
-
* acknowledges only those TABLES — which is the form somebody wants far more
|
|
1030
|
-
* often, and its absence was a real complaint: a plan carrying one intended drop
|
|
1031
|
-
* plus three other lossy ops had no way to say yes to the one. `1` said yes to
|
|
1032
|
-
* all four, and the alternative was a hand-written DROP migration, which the
|
|
1033
|
-
* framework's own upgrade note warns against.
|
|
1034
|
-
*
|
|
1035
|
-
* `undefined` = not set at all.
|
|
1036
|
-
*/
|
|
1037
|
-
export declare const destructiveScope: (raw: string | undefined) => {
|
|
1038
|
-
readonly kind: "all";
|
|
1039
|
-
} | {
|
|
1040
|
-
readonly kind: "tables";
|
|
1041
|
-
readonly tables: ReadonlySet<string>;
|
|
1042
|
-
} | undefined;
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* Compare declared reactivity against the triggers actually installed.
|
|
1046
|
-
*
|
|
1047
|
-
* Postgres only — it is the one dialect where reactivity is carried by DDL. The
|
|
1048
|
-
* binlog and Change Tracking readers are configured at runtime from the same
|
|
1049
|
-
* in-memory table list the schema produced, so they cannot drift from it.
|
|
1050
|
-
*/
|
|
1051
|
-
export declare const detectReactiveTriggerDrift: (sql: SqlClient.SqlClient, tables: ReadonlyArray<{
|
|
1052
|
-
readonly tableName: string;
|
|
1053
|
-
readonly isReactive?: boolean;
|
|
1054
|
-
}>,
|
|
1055
|
-
/** The app's `cdcChannel`. Part of the trigger NAME on any non-default
|
|
1056
|
-
* channel, so omitting it here reported every table as missing on a database
|
|
1057
|
-
* where every trigger was present. */
|
|
1058
|
-
channel?: string) => Effect.Effect<ReactiveTriggerDrift, SqlError_2>;
|
|
1059
|
-
|
|
1060
|
-
/** Dialect IDs the framework supports. */
|
|
1061
|
-
declare type DialectId = 'postgres' | 'mysql' | 'mariadb' | 'mssql' | 'sqlite' | 'turso';
|
|
1062
|
-
|
|
1063
|
-
/**
|
|
1064
|
-
* Output of `voltro db drift`: compares the live DB's introspected
|
|
1065
|
-
* schema against the last applied fingerprint in `_voltro_migrations`.
|
|
1066
|
-
*
|
|
1067
|
-
* `isDrifted: false` is the happy path. A drift report names the
|
|
1068
|
-
* concrete divergences so the operator can either reconcile in code or
|
|
1069
|
-
* via a corrective migration.
|
|
1070
|
-
*/
|
|
1071
|
-
export declare interface DriftReport {
|
|
1072
|
-
readonly isDrifted: boolean;
|
|
1073
|
-
readonly lastAppliedFingerprint?: string;
|
|
1074
|
-
readonly liveFingerprint: string;
|
|
1075
|
-
readonly divergences: ReadonlyArray<{
|
|
1076
|
-
readonly kind: 'unexpected' | 'missing';
|
|
1077
|
-
readonly description: string;
|
|
1078
|
-
}>;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
/**
|
|
1082
|
-
* Per-branch refinement passed to `.with({ relation: { ... } })`.
|
|
1083
|
-
*
|
|
1084
|
-
* `true` is the zero-config shorthand — equivalent to `{}`. Otherwise
|
|
1085
|
-
* each branch can carry its own `where` / `orderBy` / `limit` /
|
|
1086
|
-
* `offset` / nested `with`, so `users.with({ posts: { limit: 10,
|
|
1087
|
-
* orderBy: 'createdAt' } })` works without dropping into a sub-query
|
|
1088
|
-
* builder.
|
|
1089
|
-
*
|
|
1090
|
-
* Untyped at runtime: the QueryDescriptor is the boundary between the
|
|
1091
|
-
* typed builder API and the dialect-agnostic JoinCompiler. End-user
|
|
1092
|
-
* types live one layer up in the typed `.with()` method on `Query`.
|
|
1093
|
-
*/
|
|
1094
|
-
declare interface EagerLoadSpec {
|
|
1095
|
-
readonly where?: Predicate;
|
|
1096
|
-
readonly orderBy?: ReadonlyArray<OrderClause>;
|
|
1122
|
+
readonly maxLength?: number;
|
|
1097
1123
|
/**
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1104
|
-
* correlated subquery (postgres / sqlite / mysql / mariadb) or
|
|
1105
|
-
* via `TOP N` inside `FOR JSON PATH` (mssql) when the JSON-agg path
|
|
1106
|
-
* is active. The walker fallback post-buckets the batch fetch and
|
|
1107
|
-
* slices each bucket.
|
|
1124
|
+
* Raw SQL `CHECK` expressions, set by `column.check(expr)` (accumulates
|
|
1125
|
+
* — call it more than once for multiple checks). Each emits an inline
|
|
1126
|
+
* `CHECK (<expr>)` on the column. DB-enforced regardless of client,
|
|
1127
|
+
* unlike app-level `table().validate(Schema)`. The developer owns the
|
|
1128
|
+
* expr's cross-dialect portability. Distinct from `oneOf` (which emits
|
|
1129
|
+
* its own `CHECK (col IN (...))`).
|
|
1108
1130
|
*/
|
|
1109
|
-
readonly
|
|
1110
|
-
/** Per-parent offset. Only meaningful with `limit`. */
|
|
1111
|
-
readonly offset?: number;
|
|
1112
|
-
readonly with?: WithSpec;
|
|
1131
|
+
readonly checks?: ReadonlyArray<string>;
|
|
1113
1132
|
/**
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
* `true` takes every junction column; an array takes only those named. The
|
|
1117
|
-
* values land under `_junction` on each target row:
|
|
1133
|
+
* DB-level generated column. Set by `.generatedAs(expr, { stored })`.
|
|
1118
1134
|
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1135
|
+
* Distinct from `.computed(row => ...)` (application-time, eval'd
|
|
1136
|
+
* at insert) — generated columns are computed by the DB engine
|
|
1137
|
+
* itself. Visible to raw SQL, kept consistent on UPDATE without
|
|
1138
|
+
* re-stamping, can be indexed (combine with `.expressionIndex`).
|
|
1121
1139
|
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1140
|
+
* `stored: true` → `GENERATED ALWAYS AS (expr) STORED` (Postgres/
|
|
1141
|
+
* MySQL/MariaDB/SQLite; PERSISTED on MSSQL).
|
|
1142
|
+
* Disk-resident, indexable.
|
|
1143
|
+
* `stored: false` → VIRTUAL on MySQL/MariaDB/SQLite. Recomputed on
|
|
1144
|
+
* read, no disk cost, NOT indexable. Postgres
|
|
1145
|
+
* has no VIRTUAL — falls back to STORED with a
|
|
1146
|
+
* boot warning.
|
|
1126
1147
|
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
* (
|
|
1131
|
-
* data had to stay a hand-written join. The junction rows were already being
|
|
1132
|
-
* fetched in full to resolve the target ids; this stops throwing them away.
|
|
1148
|
+
* The expression is emitted verbatim (caller-quoted). MUST
|
|
1149
|
+
* reference only OTHER columns of the same row — there's no way
|
|
1150
|
+
* to reference the subject/tenant context from a generated column.
|
|
1151
|
+
* Use `.computed(row => ...)` for context-aware values.
|
|
1133
1152
|
*/
|
|
1134
|
-
readonly
|
|
1153
|
+
readonly generatedAs?: {
|
|
1154
|
+
readonly expr: string;
|
|
1155
|
+
readonly stored: boolean;
|
|
1156
|
+
};
|
|
1135
1157
|
/**
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
* row — e.g. for `users.with({ organizations: { onJunction: eq('role',
|
|
1139
|
-
* 'admin') } })` it keeps only the memberships where `role = 'admin'`,
|
|
1140
|
-
* so the loaded `organizations` are exactly the orgs the user is an
|
|
1141
|
-
* admin of.
|
|
1158
|
+
* Postgres-native ENUM type binding, populated when the column is
|
|
1159
|
+
* built from `dbEnum(...).column()`. Distinct from `oneOf`:
|
|
1142
1160
|
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1161
|
+
* - On Postgres, this column's SQL type is the user-defined ENUM
|
|
1162
|
+
* (`status order_status` not `status text CHECK (...)`).
|
|
1163
|
+
* - On MySQL / MariaDB, emits a native `ENUM('a','b','c')` column.
|
|
1164
|
+
* - On MSSQL / SQLite, falls back to TEXT + CHECK (same shape as
|
|
1165
|
+
* `oneOf` — distinct constraint name so the diff can tell them
|
|
1166
|
+
* apart).
|
|
1167
|
+
*
|
|
1168
|
+
* Migration ops `add-enum-value` / `rename-enum-value` apply natively
|
|
1169
|
+
* on Postgres (cheap `ALTER TYPE … ADD VALUE`); MySQL needs a column
|
|
1170
|
+
* rewrite (`MODIFY col ENUM(...)`); MSSQL / SQLite recreate the CHECK.
|
|
1171
|
+
*/
|
|
1172
|
+
readonly enumName?: string;
|
|
1173
|
+
readonly enumValues?: ReadonlyArray<string>;
|
|
1174
|
+
/**
|
|
1175
|
+
* Element type for `array(...)` columns. Populated only when
|
|
1176
|
+
* `type === 'array'`. The framework emits the DB-native array
|
|
1177
|
+
* type on postgres (`text[]`, `integer[]`, etc.) and falls back
|
|
1178
|
+
* to a JSON column on other dialects.
|
|
1179
|
+
*/
|
|
1180
|
+
readonly arrayElement?: ColumnType_2;
|
|
1181
|
+
/**
|
|
1182
|
+
* Field-level encryption flag, set by `.encrypted()`. When `true`
|
|
1183
|
+
* the runtime's store middleware encrypts the value on write and
|
|
1184
|
+
* decrypts on read via the registered field cipher (AES-256-GCM, keyed
|
|
1185
|
+
* from `VOLTRO_FIELD_ENCRYPTION_KEY` through the Secrets-Resolver — the
|
|
1186
|
+
* framework registers it on both boot paths; `voltro dev` mints the key).
|
|
1187
|
+
* The stored column is an opaque `enc:v2:…` string — bound by GCM's
|
|
1188
|
+
* additional authenticated data to its table, column and row
|
|
1189
|
+
* (`FieldBinding`), so a ciphertext copied into another row or column
|
|
1190
|
+
* does not decrypt there — and the DB type stays `text`. A row written
|
|
1191
|
+
* while encryption is active can't be
|
|
1192
|
+
* read back without the key — losing the key loses the data, by
|
|
1193
|
+
* design. Boot refuses if an `.encrypted()` column is declared but
|
|
1194
|
+
* no cipher is registered.
|
|
1195
|
+
*/
|
|
1196
|
+
readonly encrypted?: boolean;
|
|
1197
|
+
/**
|
|
1198
|
+
* Wire-exposure flag, set by `.serverOnly()`. When `true` the column is
|
|
1199
|
+
* SERVER-INTERNAL: server code reads it normally (an auth middleware verifying
|
|
1200
|
+
* a `keyHash`, say), but it must NEVER be serialized to a client. This is the
|
|
1201
|
+
* EXPOSURE axis, orthogonal to `.encrypted()` (which is storage-at-rest): a
|
|
1202
|
+
* column can be either, both, or neither — a hash you never ship is serverOnly
|
|
1203
|
+
* but not encrypted; a private note the owner may read is encrypted but not
|
|
1204
|
+
* serverOnly. The default is exposed to BOTH server and client; `.serverOnly()`
|
|
1205
|
+
* opts a column OUT of the client. The table-aware read paths (`crud.*`) strip
|
|
1206
|
+
* it automatically, and an audit flags a wire-reachable query whose `source`
|
|
1207
|
+
* table carries a serverOnly column that its output declares. What that audit
|
|
1208
|
+
* COSTS is per command — see `serverOnly()` below for the matrix.
|
|
1209
|
+
*/
|
|
1210
|
+
readonly serverOnly?: boolean;
|
|
1211
|
+
/**
|
|
1212
|
+
* Field-level read scopes, set by `.readableBy(...scopes)`. A NON-EMPTY list
|
|
1213
|
+
* of scope strings; the column is stripped from wire OUTPUT for any subject
|
|
1214
|
+
* that holds NONE of them, and present for a subject holding at least ONE.
|
|
1215
|
+
* This is the graded middle of the exposure axis: `.serverOnly()` hides a
|
|
1216
|
+
* column from EVERYONE on the wire, a plain column is visible to everyone,
|
|
1217
|
+
* and `.readableBy('billing:read')` is visible only to callers scoped for it.
|
|
1218
|
+
*
|
|
1219
|
+
* The scope strings are the SAME vocabulary as `guards:` / `ctx.access` — they
|
|
1220
|
+
* are checked against the subject's EFFECTIVE scope set (raw subject scopes ∪
|
|
1221
|
+
* any rbac role-derived scopes), and the `admin:full` bypass sees every
|
|
1222
|
+
* `.readableBy()` column, exactly as it satisfies every guard. Empty here is a
|
|
1223
|
+
* declaration error (`.readableBy()` with no scope would mean "nobody" — use
|
|
1224
|
+
* `.serverOnly()` for that), so the field is never present as `[]`.
|
|
1225
|
+
*
|
|
1226
|
+
* Orthogonal to `.encrypted()` (storage-at-rest) and to `.sensitive()` /
|
|
1227
|
+
* `.safe()` (export-time masking): this is purely the WIRE-exposure axis.
|
|
1228
|
+
*/
|
|
1229
|
+
readonly readableBy?: ReadonlyArray<string>;
|
|
1230
|
+
/**
|
|
1231
|
+
* CRDT-managed marker, set by the `crdtText()` column constructor. When `true`
|
|
1232
|
+
* the column holds an ENCODED CRDT state (opaque bytes — a Yjs update blob
|
|
1233
|
+
* behind `@voltro/local-first`'s swappable backend) rather than a plain value.
|
|
1234
|
+
*
|
|
1235
|
+
* It changes NOTHING about the column's DDL or how the declarative differ
|
|
1236
|
+
* treats it: the storage type is `bytes` (BYTEA / BLOB / LONGBLOB /
|
|
1237
|
+
* VARBINARY), a normal storable type, and the planner compares it structurally
|
|
1238
|
+
* like any other `bytes` column — introspection never reports this flag, and
|
|
1239
|
+
* `sameColumnShape` never reads it, so a `crdtText()` column round-trips
|
|
1240
|
+
* through a plan with zero churn (no phantom re-plan).
|
|
1241
|
+
*
|
|
1242
|
+
* What it DOES drive lives above the DDL, in the runtime write path: the
|
|
1243
|
+
* MutationStore reads it (via the SchemaRegistry) to run the AUTHORITATIVE
|
|
1244
|
+
* server-side merge on write — an incoming encoded update is folded into the
|
|
1245
|
+
* stored state with `mergeCrdtStates` before the row is written, so two
|
|
1246
|
+
* concurrent clients converge. Orthogonal to every other flag here; a CRDT
|
|
1247
|
+
* column is just `bytes` with server-merge semantics.
|
|
1248
|
+
*/
|
|
1249
|
+
readonly crdtManaged?: boolean;
|
|
1250
|
+
/**
|
|
1251
|
+
* Optimistic-concurrency marker, set by `.version()`.
|
|
1252
|
+
*
|
|
1253
|
+
* The column the store INCREMENTS on every update, and the one an update may
|
|
1254
|
+
* carry an expectation of. Two clients that read the same row and both write
|
|
1255
|
+
* it are the normal case, not an exotic one — and without this the second
|
|
1256
|
+
* write silently wins, which is the shape of every "my change disappeared"
|
|
1257
|
+
* report ever filed.
|
|
1258
|
+
*
|
|
1259
|
+
* A TIMESTAMP cannot do this job. Two writes in the same millisecond are
|
|
1260
|
+
* indistinguishable, and across replicas the clocks disagree; this repo has
|
|
1261
|
+
* already lost rows to exactly that (an analytics sink dropped 7 of 40 events
|
|
1262
|
+
* written in the same millisecond as the query bounding them). A monotonic
|
|
1263
|
+
* integer has neither problem, and it also answers the other half of the
|
|
1264
|
+
* question — which of two copies is NEWER — without consulting a clock.
|
|
1265
|
+
*/
|
|
1266
|
+
readonly versionColumn?: boolean;
|
|
1267
|
+
/**
|
|
1268
|
+
* Data-sensitivity classification, set by `.sensitive(class)`. Declares that
|
|
1269
|
+
* this column holds personal / sensitive data of a given CLASS (`'email'`,
|
|
1270
|
+
* `'fullName'`, `'phone'`, `'secret'`, …). It changes nothing at runtime or
|
|
1271
|
+
* in the DDL — it is metadata read by the export layer (`voltro data export
|
|
1272
|
+
* --profile …`) to decide how to pseudonymise / anonymise the column when
|
|
1273
|
+
* copying prod data to a lower environment. The class picks the
|
|
1274
|
+
* format-preserving fake transform. Orthogonal to `.encrypted()` (which
|
|
1275
|
+
* protects at rest): an encrypted column is decrypted on read, so it is
|
|
1276
|
+
* PLAINTEXT at export time and is treated as sensitive automatically. See
|
|
1277
|
+
* `.safe()` for the other half of the fail-closed classification.
|
|
1278
|
+
*/
|
|
1279
|
+
readonly sensitive?: {
|
|
1280
|
+
readonly class: string;
|
|
1281
|
+
};
|
|
1282
|
+
/**
|
|
1283
|
+
* Reviewed-safe marker, set by `.safe()`. Declares that this column holds NO
|
|
1284
|
+
* personal / sensitive data and may be copied verbatim to a lower
|
|
1285
|
+
* environment. It exists so masking can be FAIL-CLOSED: under a masking
|
|
1286
|
+
* profile, a column that is neither `.sensitive()` nor `.safe()` blocks the
|
|
1287
|
+
* export until it is consciously classified — so a newly-added column can
|
|
1288
|
+
* never silently leak. Metadata only; no runtime/DDL effect.
|
|
1289
|
+
*/
|
|
1290
|
+
readonly safe?: boolean;
|
|
1291
|
+
/**
|
|
1292
|
+
* Fixed-point precision (total significant digits), set by
|
|
1293
|
+
* `decimal(precision, scale)` / `numeric(precision, scale)`. Populated
|
|
1294
|
+
* only when `type === 'decimal'`. Drives the DDL size —
|
|
1295
|
+
* `NUMERIC(p,s)` (postgres) / `DECIMAL(p,s)` (mysql/mariadb/mssql) —
|
|
1296
|
+
* so a money column round-trips at full precision. On sqlite/turso
|
|
1297
|
+
* (no fixed-point type) the column is `NUMERIC` affinity and the value
|
|
1298
|
+
* carries as a TEXT string, so `p`/`s` are informational only there.
|
|
1299
|
+
*/
|
|
1300
|
+
readonly precision?: number;
|
|
1301
|
+
/**
|
|
1302
|
+
* Fixed-point scale (digits after the decimal point), set by
|
|
1303
|
+
* `decimal(precision, scale)`. Populated only when `type === 'decimal'`.
|
|
1304
|
+
* MUST be `<= precision`. See {@link ColumnDefinition.precision}.
|
|
1305
|
+
*/
|
|
1306
|
+
readonly scale?: number;
|
|
1307
|
+
/**
|
|
1308
|
+
* Vector dimensionality. Populated only when `type === 'vector'`.
|
|
1309
|
+
* The migration emitter uses it to size the column DDL —
|
|
1310
|
+
* `VECTOR(n)` on postgres (pgvector) + MariaDB 11.7+ / MySQL 9,
|
|
1311
|
+
* and a sized fallback (`VARBINARY(MAX)` / `BLOB`) elsewhere. The
|
|
1312
|
+
* HNSW index emitter + the `vectorEmbedding()` mixin also read it.
|
|
1313
|
+
*/
|
|
1314
|
+
readonly vectorDim?: number;
|
|
1315
|
+
/**
|
|
1316
|
+
* Vector storage precision. Populated only when `type === 'vector'`.
|
|
1317
|
+
* `'float32'` (default) emits `VECTOR(n)`; `'half'` emits pgvector's
|
|
1318
|
+
* `HALFVEC(n)` (half the bytes, marginally lower recall). MariaDB /
|
|
1319
|
+
* MySQL have no half type — the emitter upcasts to `VECTOR(n)` there.
|
|
1320
|
+
*/
|
|
1321
|
+
readonly vectorPrecision?: 'float32' | 'half';
|
|
1322
|
+
/**
|
|
1323
|
+
* Verbatim DDL fragment for a `raw(ddl)` escape-hatch column.
|
|
1324
|
+
* Populated only when `type === 'raw'`. The migration emitter returns
|
|
1325
|
+
* this string unchanged as the column's SQL type on EVERY dialect —
|
|
1326
|
+
* the fragment is the single source of truth for the column's type,
|
|
1327
|
+
* nullability, and default. Because of that, `.nullable()`,
|
|
1328
|
+
* `.default()`, and `.unique()` are rejected at declaration time on a
|
|
1329
|
+
* `raw` column (two sources of truth would drift).
|
|
1330
|
+
*/
|
|
1331
|
+
readonly rawDdl?: string;
|
|
1332
|
+
/**
|
|
1333
|
+
* Spatial column metadata (`@voltro/plugin-postgis`'s `geography()` /
|
|
1334
|
+
* `geometry()`). The row TYPE stays `text` (handlers read/write WKT/EWKT
|
|
1335
|
+
* strings); this drives the migration emitter to produce real
|
|
1336
|
+
* `geography(Point,4326)` / `geometry(Polygon,4326)` DDL on postgres +
|
|
1337
|
+
* `CREATE EXTENSION IF NOT EXISTS postgis`. PostGIS is postgres-only, so
|
|
1338
|
+
* the emitter THROWS on any other dialect (the documented fail-loud guard)
|
|
1339
|
+
* rather than silently emitting a broken TEXT column. Generic on purpose —
|
|
1340
|
+
* the postgis plugin is the only producer, but core owns the DDL hook (the
|
|
1341
|
+
* same pattern as `vectorDim` / `enumName`) so the column type is real, not
|
|
1342
|
+
* a fabricated marker nothing consumes.
|
|
1343
|
+
*/
|
|
1344
|
+
readonly spatial?: {
|
|
1345
|
+
/** PostGIS base type. */
|
|
1346
|
+
readonly kind: 'geography' | 'geometry';
|
|
1347
|
+
/** Geometry subtype: `Point` / `LineString` / `Polygon` / … */
|
|
1348
|
+
readonly geomKind: string;
|
|
1349
|
+
/** Spatial reference id (e.g. 4326 = WGS-84 lat/lon). */
|
|
1350
|
+
readonly srid: number;
|
|
1351
|
+
};
|
|
1352
|
+
/**
|
|
1353
|
+
* Captured `id()` options. Populated only on the primary-key column
|
|
1354
|
+
* (`type: 'id'`). At column-construction time the `prefix` for typeid
|
|
1355
|
+
* may still be missing — `table()` resolves it against the table name
|
|
1356
|
+
* and the resolved scheme lands on `idScheme` below.
|
|
1357
|
+
*/
|
|
1358
|
+
readonly idSchemeInput?: IdSchemeInput_2;
|
|
1359
|
+
/**
|
|
1360
|
+
* Fully-resolved id scheme for the primary-key column. Set by
|
|
1361
|
+
* `table()` after walking the field map; downstream code (DDL emit,
|
|
1362
|
+
* SchemaRegistry, MutationStore auto-inject) reads this exclusively.
|
|
1363
|
+
*/
|
|
1364
|
+
readonly idScheme?: IdScheme_2;
|
|
1365
|
+
/**
|
|
1366
|
+
* Migration backfill expression. Populated by `.backfill(sqlOrFn, opts?)`.
|
|
1367
|
+
*
|
|
1368
|
+
* The migration planner reads this when ADD COLUMN + NOT NULL on a
|
|
1369
|
+
* populated table would otherwise be classified `needs-backfill` →
|
|
1370
|
+
* with a backfill declared, the planner emits a three-step plan
|
|
1371
|
+
* (ADD COLUMN nullable → UPDATE via expression → SET NOT NULL) inside
|
|
1372
|
+
* one transaction (or one forward-roll group on mysql/mariadb).
|
|
1373
|
+
*
|
|
1374
|
+
* Has zero effect at insert/query time — pure planner metadata.
|
|
1375
|
+
*/
|
|
1376
|
+
readonly backfill?: BackfillSpec_2<TsType>;
|
|
1377
|
+
/**
|
|
1378
|
+
* Rename marker. Populated by `.renamedFrom('oldName')`.
|
|
1379
|
+
*
|
|
1380
|
+
* The migration planner sees `oldName` missing + this column present
|
|
1381
|
+
* + the `renamedFrom` annotation → classifies as RENAME instead of
|
|
1382
|
+
* DROP+ADD (which would lose data). The annotation stays in the code
|
|
1383
|
+
* until `_voltro_migrations` records that the rename was applied in
|
|
1384
|
+
* the env(s) the developer cares about; removing it earlier yields
|
|
1385
|
+
* a clear refuse-to-plan message rather than a silent drop.
|
|
1386
|
+
*/
|
|
1387
|
+
readonly renamedFrom?: string;
|
|
1388
|
+
/**
|
|
1389
|
+
* Dropped marker. Set by the standalone `dropped()` helper at the
|
|
1390
|
+
* column's field-map slot. The planner treats the column as
|
|
1391
|
+
* intentionally going away — emits a `lossy` op that's auto-allowed
|
|
1392
|
+
* (no `VOLTRO_DESTRUCTIVE_OK=1` required) because the developer
|
|
1393
|
+
* declared intent explicitly in the schema diff.
|
|
1394
|
+
*
|
|
1395
|
+
* The drop-marker stays for one applied migration cycle, then the
|
|
1396
|
+
* developer removes the field map entry entirely.
|
|
1397
|
+
*/
|
|
1398
|
+
readonly dropped?: boolean;
|
|
1399
|
+
/**
|
|
1400
|
+
* Type-narrowing marker. Populated by `.narrowedFrom(from, { using })`.
|
|
1401
|
+
*
|
|
1402
|
+
* The planner uses `from` to know what the live-DB column type was
|
|
1403
|
+
* before this migration, and `using` as the dialect-cast expression
|
|
1404
|
+
* when the conversion isn't implicit (e.g. `text → integer` on
|
|
1405
|
+
* postgres requires `USING col::integer`).
|
|
1406
|
+
*/
|
|
1407
|
+
readonly narrowedFrom?: NarrowedFromSpec_2;
|
|
1408
|
+
/**
|
|
1409
|
+
* Extended unique-constraint metadata. Populated by
|
|
1410
|
+
* `.unique({ dedup })`. The legacy boolean `unique` flag above
|
|
1411
|
+
* stays as the canonical "is this column unique?" signal — `uniqueSpec`
|
|
1412
|
+
* carries the planner-only metadata (dedup strategy) so existing
|
|
1413
|
+
* runtime consumers don't need to learn the new shape.
|
|
1414
|
+
*/
|
|
1415
|
+
readonly uniqueSpec?: UniqueSpec_2;
|
|
1416
|
+
/**
|
|
1417
|
+
* Orphan-handling policy used when adding a foreign key to a
|
|
1418
|
+
* populated column. Mirrors the `ReferenceOptions.orphanPolicy` API:
|
|
1419
|
+
*
|
|
1420
|
+
* - `'fail'` (default) → bare ADD CONSTRAINT (`needs-backfill`, applies);
|
|
1421
|
+
* the DB rejects it if an existing row is an orphan — exactly
|
|
1422
|
+
* like tightening a column to NOT NULL. NOT blocked: the
|
|
1423
|
+
* planner is pure (no row counts), so it can't tell a clean /
|
|
1424
|
+
* empty table from one with orphans.
|
|
1425
|
+
* - `'null'` → planner emits `UPDATE … SET col = NULL` before adding FK
|
|
1426
|
+
* - `'delete'` → planner emits `DELETE FROM child WHERE …` before adding FK
|
|
1427
|
+
*
|
|
1428
|
+
* Has no runtime semantics — purely planner metadata.
|
|
1429
|
+
*/
|
|
1430
|
+
readonly orphanPolicy?: 'fail' | 'null' | 'delete';
|
|
1431
|
+
/**
|
|
1432
|
+
* Phantom field so TypeScript can infer the row type at the column level.
|
|
1433
|
+
* Never read at runtime.
|
|
1434
|
+
*/
|
|
1435
|
+
readonly __tsType?: TsType;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/** A column as the DSL builds it — `text().nullable()`, `integer().default(0)` — or its definition. */
|
|
1439
|
+
export declare type ColumnInput = {
|
|
1440
|
+
__definition(): ColumnDefinition<unknown>;
|
|
1441
|
+
} | ColumnDefinition<unknown>;
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* A column as seen by the planner: either declared in code (extracted
|
|
1445
|
+
* from `ColumnDefinition`) or live in the DB (extracted via introspection).
|
|
1446
|
+
*
|
|
1447
|
+
* The shape is intentionally minimal — only the bits that affect diff
|
|
1448
|
+
* classification land here. Things like `idScheme`, `computed`,
|
|
1449
|
+
* `defaultFactory` etc. live in the runtime layer and don't survive
|
|
1450
|
+
* the round-trip to `information_schema`.
|
|
1451
|
+
*/
|
|
1452
|
+
export declare interface ColumnSnapshot {
|
|
1453
|
+
readonly name: string;
|
|
1454
|
+
readonly type: ColumnType;
|
|
1455
|
+
readonly nullable: boolean;
|
|
1456
|
+
readonly unique: boolean;
|
|
1457
|
+
readonly hasDefault: boolean;
|
|
1458
|
+
readonly defaultValue?: unknown;
|
|
1459
|
+
/**
|
|
1460
|
+
* Type-narrowing acknowledgement from `.narrowedFrom(from, { using })`.
|
|
1461
|
+
* Only ever set on the DECLARED side; introspection leaves it
|
|
1462
|
+
* undefined (the live DB has no notion of a prior type). When
|
|
1463
|
+
* `from` equals the live column type the planner downgrades the
|
|
1464
|
+
* otherwise-blocked type change to `needs-backfill` and threads
|
|
1465
|
+
* `using` into the `ALTER COLUMN … TYPE … USING <using>` cast. */
|
|
1466
|
+
readonly narrowedFrom?: {
|
|
1467
|
+
readonly from: ColumnType;
|
|
1468
|
+
readonly using?: string;
|
|
1469
|
+
};
|
|
1470
|
+
/**
|
|
1471
|
+
* Bounded text length from `text().maxLength(n)` — carried on BOTH sides.
|
|
1472
|
+
*
|
|
1473
|
+
* DECLARED: from the column definition. LIVE: from introspection, and ONLY when
|
|
1474
|
+
* the dialect materialises a length. An unbounded text family member
|
|
1475
|
+
* (`text`/`longtext`/`mediumtext`/`clob`) leaves this `undefined` rather than
|
|
1476
|
+
* reporting the type's theoretical maximum — mysql answers 4294967295 for
|
|
1477
|
+
* `longtext`, and comparing that against a declared `undefined` would re-emit an
|
|
1478
|
+
* ALTER on every boot forever.
|
|
1479
|
+
*
|
|
1480
|
+
* WHY IT IS HERE AT ALL. It was absent from the snapshot entirely: the value
|
|
1481
|
+
* lived on the column definition, was read only when rendering CREATE DDL, and
|
|
1482
|
+
* never reached the comparison. So the differ was not comparing lengths wrongly
|
|
1483
|
+
* — it could not see them. Adding `.maxLength(n)` to an EXISTING column planned
|
|
1484
|
+
* zero operations and reported "schema is up to date" while the live column
|
|
1485
|
+
* stayed `longtext`. A deployment hit that trying to apply the documented remedy
|
|
1486
|
+
* for MariaDB's hash long-unique, and their contrast is what pinned it: a column
|
|
1487
|
+
* bounded AT CREATION was `varchar(64)` (DDL path, fine), one bounded afterwards
|
|
1488
|
+
* stayed `longtext` (diff path, blind).
|
|
1489
|
+
*
|
|
1490
|
+
* NOT compared on sqlite: it has no length-enforced type, so the declared value
|
|
1491
|
+
* is real and the live side can never report one. Comparing them there would
|
|
1492
|
+
* re-emit forever. See `sameColumnShape`.
|
|
1493
|
+
*/
|
|
1494
|
+
readonly maxLength?: number;
|
|
1495
|
+
readonly references?: {
|
|
1496
|
+
readonly table: string;
|
|
1497
|
+
readonly column: string;
|
|
1498
|
+
readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
1499
|
+
readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
1500
|
+
/**
|
|
1501
|
+
* Orphan-handling policy for adding this FK to a POPULATED column
|
|
1502
|
+
* (`reference(() => t, { orphanPolicy })`). Only ever set on the
|
|
1503
|
+
* DECLARED side; introspection leaves it undefined (the live DB has
|
|
1504
|
+
* no notion of it). `normRef` strips it before comparison so it never
|
|
1505
|
+
* causes FK churn — the planner reads it directly to classify +
|
|
1506
|
+
* drive the applier's orphan pre-step. See the references-diff in
|
|
1507
|
+
* `classifyColumnChange`.
|
|
1508
|
+
*/
|
|
1509
|
+
readonly orphanPolicy?: 'fail' | 'null' | 'delete';
|
|
1510
|
+
/**
|
|
1511
|
+
* The constraint's catalog name, on the LIVE side only. A reader reports
|
|
1512
|
+
* it (postgres, the mysql family, mssql; sqlite names nothing) so a drop
|
|
1513
|
+
* can name the constraint that is actually there rather than assume the
|
|
1514
|
+
* `<table>_<column>_fkey` convention the framework uses when IT creates
|
|
1515
|
+
* one. `normRef` strips it before comparison and before hashing: a name
|
|
1516
|
+
* is a catalog identity, not schema.
|
|
1517
|
+
*/
|
|
1518
|
+
readonly constraintName?: string;
|
|
1519
|
+
};
|
|
1520
|
+
/** Physical representation for `reference()`; defaults to `text`. */
|
|
1521
|
+
readonly referenceStorage?: 'text' | 'numeric';
|
|
1522
|
+
/** `oneOf(['a','b'])` → CHECK constraint values. */
|
|
1523
|
+
readonly oneOf?: ReadonlyArray<string>;
|
|
1524
|
+
/** Required/observed JSON validity on SQL Server. Unverified means no
|
|
1525
|
+
* recognized enabled and trusted check proves the existing data valid. */
|
|
1526
|
+
readonly jsonValidation?: 'value' | 'container' | 'unverified';
|
|
1527
|
+
/** Live SQL Server JSON checks recognized by complete predicate shape.
|
|
1528
|
+
* Names are catalog identities for DDL, not portable schema fingerprints. */
|
|
1529
|
+
readonly jsonChecks?: ReadonlyArray<{
|
|
1530
|
+
readonly name: string;
|
|
1531
|
+
readonly validation: 'value' | 'container';
|
|
1532
|
+
readonly enabled: boolean;
|
|
1533
|
+
readonly trusted: boolean;
|
|
1534
|
+
}>;
|
|
1535
|
+
/**
|
|
1536
|
+
* Fixed-point precision + scale for a `decimal(p,s)` / `numeric(p,s)`
|
|
1537
|
+
* column (`type === 'decimal'`). Drives the emitted `NUMERIC(p,s)` /
|
|
1538
|
+
* `DECIMAL(p,s)` DDL. Set on the DECLARED side; introspection leaves them
|
|
1539
|
+
* undefined (the planner compares the `decimal` type structurally, not the
|
|
1540
|
+
* exact precision — an ALTER of precision alone is not auto-classified, same
|
|
1541
|
+
* posture as `generatedAs`). See `snapshotPgType` / `snapshotMysqlType`.
|
|
1542
|
+
*/
|
|
1543
|
+
readonly precision?: number;
|
|
1544
|
+
readonly scale?: number;
|
|
1545
|
+
/**
|
|
1546
|
+
* Id-generation scheme for `type: 'id'` columns. Mirrors
|
|
1547
|
+
* `ColumnDefinition.idScheme.kind` so the applier can tell a numeric
|
|
1548
|
+
* auto-increment PK (`kind: 'numeric'`) from a typeid/string PK and
|
|
1549
|
+
* emit the dialect-correct auto-increment DDL (BIGSERIAL / BIGINT
|
|
1550
|
+
* AUTO_INCREMENT / IDENTITY / INTEGER AUTOINCREMENT). Only ever set on
|
|
1551
|
+
* the DECLARED side; introspection leaves it undefined (the live DB
|
|
1552
|
+
* doesn't report a scheme — a numeric PK is detectable there from the
|
|
1553
|
+
* integer type + auto_increment, which the diff doesn't need).
|
|
1554
|
+
*/
|
|
1555
|
+
readonly idScheme?: {
|
|
1556
|
+
readonly kind: string;
|
|
1557
|
+
};
|
|
1558
|
+
/**
|
|
1559
|
+
* What the `id` KIND is made of, on the LIVE side.
|
|
1560
|
+
*
|
|
1561
|
+
* Introspection cannot see a DSL spelling, only a column, and it reports the
|
|
1562
|
+
* `id` kind for ANY sole primary key named `id` — a NAME-based inference. So
|
|
1563
|
+
* the kind alone erases the one distinction that matters when the two sides
|
|
1564
|
+
* disagree: a text key and a sequence are both reported as `id`, and a
|
|
1565
|
+
* declaration that changed from one to the other compared EQUAL. That
|
|
1566
|
+
* approximation predates the `text()` spelling and got wider with it.
|
|
1567
|
+
*
|
|
1568
|
+
* This carries the storage the kind was inferred over, derived from the same
|
|
1569
|
+
* raw type the introspector already read. Set by all four dialect readers
|
|
1570
|
+
* wherever they apply the inference, and derived for the DECLARED side from
|
|
1571
|
+
* `idScheme` — so the comparison pairs like with like instead of pairing on
|
|
1572
|
+
* the name.
|
|
1573
|
+
*/
|
|
1574
|
+
readonly idStorage?: 'text' | 'numeric';
|
|
1575
|
+
/**
|
|
1576
|
+
* What this column's declared KIND is stored AS, when the two differ and no
|
|
1577
|
+
* reader can report the kind back.
|
|
1578
|
+
*
|
|
1579
|
+
* An `array()` is `<element>[]` on postgres and JSON / NVARCHAR(MAX) / TEXT
|
|
1580
|
+
* elsewhere, and every reader answers with the STORAGE — so the declared
|
|
1581
|
+
* kind and the live one never matched and the table hashed apart from its own
|
|
1582
|
+
* database forever. The planner already collapses this per dialect; the
|
|
1583
|
+
* fingerprint has no dialect, so the DECLARED side states the answer here at
|
|
1584
|
+
* snapshot time (`declaredSnapshot` takes the dialect) and the hash reads it.
|
|
1585
|
+
* Set on the declared side only; a live snapshot needs none, because its type
|
|
1586
|
+
* IS the storage.
|
|
1587
|
+
*/
|
|
1588
|
+
readonly storageType?: ColumnType;
|
|
1589
|
+
/**
|
|
1590
|
+
* Verbatim DDL fragment for a `raw(ddl)` column (`type === 'raw'`).
|
|
1591
|
+
* Emitted unchanged as the column's SQL type on every dialect — the
|
|
1592
|
+
* applier and the CREATE-TABLE emitter both defer to it. Only ever set
|
|
1593
|
+
* on the declared side; the live DB reports a concrete type, not the
|
|
1594
|
+
* original fragment, so introspection leaves it undefined.
|
|
1595
|
+
*/
|
|
1596
|
+
readonly rawDdl?: string;
|
|
1597
|
+
/**
|
|
1598
|
+
* `.generatedAs(expr, { stored })` — a DB-computed column. When set the
|
|
1599
|
+
* applier emits `GENERATED ALWAYS AS (<expr>) STORED|VIRTUAL` (mssql:
|
|
1600
|
+
* `AS (<expr>) PERSISTED`) and SUPPRESSES NOT NULL / DEFAULT / inline
|
|
1601
|
+
* UNIQUE / REFERENCES (the value is computed; the DB rejects those
|
|
1602
|
+
* alongside a generation clause). Only ever set on the DECLARED side —
|
|
1603
|
+
* introspection does NOT read generation expressions, so the live snapshot
|
|
1604
|
+
* leaves it undefined. The planner's `sameColumnShape` deliberately does
|
|
1605
|
+
* NOT compare this field, so a declared-generated column never diffs
|
|
1606
|
+
* against its introspected-as-plain live form (no churn). Consequence:
|
|
1607
|
+
* the declarative engine creates generated columns correctly but does NOT
|
|
1608
|
+
* detect a CHANGE to the expression after the fact — recreate the table
|
|
1609
|
+
* (drop + re-migrate) to alter a generation expression.
|
|
1610
|
+
*/
|
|
1611
|
+
readonly generatedAs?: {
|
|
1612
|
+
readonly expr: string;
|
|
1613
|
+
readonly stored: boolean;
|
|
1614
|
+
};
|
|
1615
|
+
/**
|
|
1616
|
+
* Data-sensitivity classification from `.sensitive(class)` (or implied by
|
|
1617
|
+
* `.encrypted()`). DECLARED-side only — introspection never reports it. Read
|
|
1618
|
+
* by the export masker to pseudonymise/anonymise the column when copying to a
|
|
1619
|
+
* lower environment; the planner ignores it (no DDL, no churn). See `safe`.
|
|
1620
|
+
*/
|
|
1621
|
+
readonly sensitive?: {
|
|
1622
|
+
readonly class: string;
|
|
1623
|
+
};
|
|
1624
|
+
/**
|
|
1625
|
+
* Reviewed-safe marker from `.safe()`. DECLARED-side only. Lets the export
|
|
1626
|
+
* masker be fail-closed: a column that is neither `sensitive` nor `safe`
|
|
1627
|
+
* blocks a masking export until classified.
|
|
1628
|
+
*/
|
|
1629
|
+
readonly safe?: boolean;
|
|
1630
|
+
/**
|
|
1631
|
+
* Type PARAMETERS for the three `ColumnType`s whose DDL is not determined by
|
|
1632
|
+
* the type tag alone — `vector(n)`, `array(of)`, `enum(name, values)`.
|
|
1633
|
+
* DECLARED-side only (introspection reports a concrete SQL type, not the
|
|
1634
|
+
* declaration that produced it).
|
|
1635
|
+
*
|
|
1636
|
+
* These exist because the applier renders its DDL from a ColumnSnapshot, not
|
|
1637
|
+
* from the `ColumnDefinition` that `migrate.ts`'s canonical `sqlType` reads.
|
|
1638
|
+
* Without them the snapshot renderers had nothing to render and collapsed all
|
|
1639
|
+
* three to `text` — which is not a smaller mistake than it looks. A declared
|
|
1640
|
+
* `vector(1536)` against a live `text` column planned an `alter-column-type`
|
|
1641
|
+
* that emitted `ALTER COLUMN … TYPE text`: valid SQL, applied successfully,
|
|
1642
|
+
* changed nothing. The plan re-emitted it on every boot, the applier reported
|
|
1643
|
+
* success every time, and the schema never converged. (Reported from a live
|
|
1644
|
+
* pod: "applied 31 op(s)" with none of the 31 present in the database.)
|
|
1645
|
+
*
|
|
1646
|
+
* The rule for anything added later: if the applier has to RENDER it, the
|
|
1647
|
+
* snapshot has to CARRY it — a snapshot renderer must never invent a type it
|
|
1648
|
+
* wasn't given.
|
|
1649
|
+
*/
|
|
1650
|
+
readonly vectorDim?: number;
|
|
1651
|
+
readonly vectorPrecision?: 'float32' | 'half';
|
|
1652
|
+
readonly arrayElement?: ColumnType;
|
|
1653
|
+
readonly enumName?: string;
|
|
1654
|
+
readonly enumValues?: ReadonlyArray<string>;
|
|
1655
|
+
/**
|
|
1656
|
+
* PostGIS `geography(kind, srid)` / `geometry(kind, srid)` parameters. Same
|
|
1657
|
+
* reason as the three above: a spatial column declares `type: 'text'`, so
|
|
1658
|
+
* WITHOUT this the applier's `add-column` created a plain `text` column and
|
|
1659
|
+
* introspection (`USER-DEFINED` → `text`) agreed with it — no churn, no
|
|
1660
|
+
* error, and no spatial column. DECLARED-side only.
|
|
1661
|
+
*/
|
|
1662
|
+
readonly spatial?: {
|
|
1663
|
+
readonly kind: 'geography' | 'geometry';
|
|
1664
|
+
readonly geomKind: string;
|
|
1665
|
+
readonly srid: number;
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
|
|
1670
|
+
|
|
1671
|
+
declare type ColumnType_2 = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
|
|
1672
|
+
|
|
1673
|
+
declare interface DataStore {
|
|
1674
|
+
/**
|
|
1675
|
+
* Execute a typed query descriptor and return matching rows.
|
|
1676
|
+
*
|
|
1677
|
+
* Deliberately NOT generic: `DataStore` is the driver SPI, implemented by
|
|
1678
|
+
* every dialect store and its transactional / namespace views. Threading a
|
|
1679
|
+
* phantom row type through all of them would be pure churn — they genuinely
|
|
1680
|
+
* do return untyped rows off the wire. The row type is re-applied one layer
|
|
1681
|
+
* up, on `FluentStore.query`, which is what handlers actually call.
|
|
1682
|
+
*/
|
|
1683
|
+
query(descriptor: QueryDescriptor_2): Promise<ReadonlyArray<Row_2>>;
|
|
1684
|
+
/** Insert a row. Emits an insert ChangeEvent (deferred if inside `transactional()`). */
|
|
1685
|
+
insert(table: string, row: Row_2): Promise<Row_2>;
|
|
1686
|
+
/**
|
|
1687
|
+
* Insert many rows in ONE statement and return the inserted rows.
|
|
1688
|
+
*
|
|
1689
|
+
* A single multi-row `INSERT … VALUES (…),(…)` per dialect — one
|
|
1690
|
+
* round-trip regardless of row count, instead of N `insert()` calls.
|
|
1691
|
+
*
|
|
1692
|
+
* Emits ONE `insert` ChangeEvent PER row (deferred if inside
|
|
1693
|
+
* `transactional()`), so reactive subscribers see each new row's
|
|
1694
|
+
* delta exactly as they would from N single inserts.
|
|
1695
|
+
*
|
|
1696
|
+
* Cross-dialect:
|
|
1697
|
+
* - postgres / sqlite / mariadb 10.5+: `INSERT … VALUES (…),(…) RETURNING *`
|
|
1698
|
+
* - mssql: `INSERT … OUTPUT INSERTED.* VALUES (…),(…)`
|
|
1699
|
+
* - mysql: multi-row `INSERT`, then re-select by id
|
|
1700
|
+
* - memory store: loop + per-row emit
|
|
1701
|
+
*
|
|
1702
|
+
* An empty `rows` array is a no-op that returns `[]` (no statement, no events).
|
|
1703
|
+
*/
|
|
1704
|
+
insertMany(table: string, rows: ReadonlyArray<Row_2>): Promise<ReadonlyArray<Row_2>>;
|
|
1705
|
+
/**
|
|
1706
|
+
* In-place JSON merge: set `value` at `path` inside the JSON column
|
|
1707
|
+
* (`path` is the column name, optionally with a nested dot-path, e.g.
|
|
1708
|
+
* `'metadata'` or `'metadata.theme'`). Reads-modifies-writes the
|
|
1709
|
+
* single row identified by `pk` and returns the post-image (or `null`
|
|
1710
|
+
* when the row doesn't exist). Emits an `update` ChangeEvent (deferred
|
|
1711
|
+
* if inside `transactional()`).
|
|
1712
|
+
*
|
|
1713
|
+
* Cross-dialect (server-side merge, no round-trip read):
|
|
1714
|
+
* - postgres: `jsonb_set(col, '{path}', value)` (top-level: `col || value`)
|
|
1715
|
+
* - mysql / mariadb: `JSON_SET(col, '$.path', value)`
|
|
1716
|
+
* - mssql: `JSON_MODIFY(col, '$.path', value)`
|
|
1717
|
+
* - sqlite: `json_set(col, '$.path', value)`
|
|
1718
|
+
* - memory store: read-modify-write the JS object in place
|
|
1719
|
+
*
|
|
1720
|
+
* `additionalPatch` assigns other columns in the SAME row update (e.g. an
|
|
1721
|
+
* automatic timestamp). Undefined values are omitted; null is explicit.
|
|
1722
|
+
* Cannot rewrite the primary key or replace the JSON column being patched.
|
|
1723
|
+
*/
|
|
1724
|
+
patchJson(table: string, primaryKey: RowKey, path: string, value: unknown, additionalPatch?: Readonly<Record<string, unknown>>): Promise<Row_2 | null>;
|
|
1725
|
+
/** Patch a row by primary key. Emits an update ChangeEvent (deferred if inside `transactional()`).
|
|
1726
|
+
* Returns the post-image. */
|
|
1727
|
+
update(table: string, primaryKey: RowKey, patch: Readonly<Record<string, unknown>>): Promise<Row_2 | null>;
|
|
1728
|
+
/**
|
|
1729
|
+
* Bulk update — apply `patch` to every row in `table` matching the
|
|
1730
|
+
* `where` predicate (Q9). The predicate is a `Predicate` AST (the
|
|
1731
|
+
* same shape `.where()` uses on queries) and CAN carry sub-queries
|
|
1732
|
+
* via inSubquery / exists.
|
|
1733
|
+
*
|
|
1734
|
+
* Returns the number of rows updated. Emits one `update`
|
|
1735
|
+
* ChangeEvent per affected row (deferred if inside `transactional()`),
|
|
1736
|
+
* so reactive subscribers see each row's delta — at the cost of N
|
|
1737
|
+
* events instead of one.
|
|
1738
|
+
*
|
|
1739
|
+
* Cross-dialect:
|
|
1740
|
+
* - SQL stores: `UPDATE <table> SET <patch> WHERE <pred>` in one
|
|
1741
|
+
* statement; the per-row events come from a fan-out the store
|
|
1742
|
+
* handles (postgres uses RETURNING; the others read-then-update).
|
|
1743
|
+
* - memory store: linear filter + per-row update + emit
|
|
1744
|
+
*/
|
|
1745
|
+
updateMany(table: string, patch: Readonly<Record<string, unknown>>, options: {
|
|
1746
|
+
where: Predicate;
|
|
1747
|
+
}): Promise<number>;
|
|
1748
|
+
/** Delete a row by primary key. Emits a delete ChangeEvent (deferred if inside `transactional()`). */
|
|
1749
|
+
delete(table: string, primaryKey: RowKey): Promise<boolean>;
|
|
1750
|
+
/**
|
|
1751
|
+
* Bulk delete — remove every row in `table` matching the `where`
|
|
1752
|
+
* predicate. The predicate is a `Predicate` AST (the same shape
|
|
1753
|
+
* `.where()` uses on queries) and CAN carry sub-queries via
|
|
1754
|
+
* inSubquery / exists. The set-based counterpart to `updateMany`.
|
|
1755
|
+
*
|
|
1756
|
+
* This is a HARD delete: it removes the row regardless of whether the
|
|
1757
|
+
* table carries `softDelete()`. (Soft-delete is a write-time concern
|
|
1758
|
+
* the `ctx.store` middleware layers on the keyed `delete()` / the
|
|
1759
|
+
* fluent `delete(...).soft()` builder — `deleteMany` is the low-level
|
|
1760
|
+
* physical remove, mirroring `updateMany`'s lack of mixin handling.)
|
|
1761
|
+
*
|
|
1762
|
+
* Returns the number of rows deleted. Emits one `delete` ChangeEvent
|
|
1763
|
+
* per removed row (deferred if inside `transactional()`), each
|
|
1764
|
+
* carrying the removed row's old-image as `old` — so reactive
|
|
1765
|
+
* subscribers see each row leave, at the cost of N events instead of
|
|
1766
|
+
* one.
|
|
1767
|
+
*
|
|
1768
|
+
* Cross-dialect:
|
|
1769
|
+
* - postgres / sqlite / mariadb 10.0+: `DELETE FROM <table> WHERE
|
|
1770
|
+
* <pred> RETURNING *` in one statement; the per-row old-image
|
|
1771
|
+
* events come from the RETURNING rows.
|
|
1772
|
+
* - mssql: `DELETE FROM <table> OUTPUT DELETED.* WHERE <pred>` —
|
|
1773
|
+
* OUTPUT DELETED.* gives the old-images in one statement.
|
|
1774
|
+
* - mysql (8.x): no `RETURNING` on DELETE — SELECT the matching
|
|
1775
|
+
* rows FIRST (for the old-image fan-out), then one DELETE.
|
|
1776
|
+
* - memory store: linear filter + per-row delete + emit.
|
|
1777
|
+
*/
|
|
1778
|
+
deleteMany(table: string, options: {
|
|
1779
|
+
where: Predicate;
|
|
1780
|
+
}): Promise<number>;
|
|
1781
|
+
/**
|
|
1782
|
+
* Upsert: INSERT a row, and on conflict over `conflictColumns`,
|
|
1783
|
+
* UPDATE the conflicting row instead.
|
|
1784
|
+
*
|
|
1785
|
+
* `conflictColumns` declares which column set defines a conflict.
|
|
1786
|
+
* Most often a single column with a UNIQUE constraint (`['email']`)
|
|
1787
|
+
* or a composite UNIQUE (`['orgId', 'slug']`).
|
|
1788
|
+
*
|
|
1789
|
+
* `update` controls what happens on conflict:
|
|
1790
|
+
* - omitted / `undefined` → update every column from the input row
|
|
1791
|
+
* (matches Drizzle's `onConflictDoUpdate({ target, set: input })` shorthand)
|
|
1792
|
+
* - `ReadonlyArray<string>` → update only these columns from the input row
|
|
1793
|
+
* - `(existing) => Patch` → compute a patch from the conflicting row;
|
|
1794
|
+
* useful for "increment counter" semantics
|
|
1795
|
+
*
|
|
1796
|
+
* `updateValues` adds conflict-only assignments without changing the insert
|
|
1797
|
+
* row. See `UpsertOptions` for precedence and omission rules.
|
|
1798
|
+
*
|
|
1799
|
+
* Returns the FINAL row (post-insert OR post-update) so callers can
|
|
1800
|
+
* read the assigned PK without a separate query.
|
|
1801
|
+
*
|
|
1802
|
+
* Emits an `insert` ChangeEvent on the insert path and an `update`
|
|
1803
|
+
* ChangeEvent on the conflict path, so reactive subscribers see a
|
|
1804
|
+
* single delta regardless of which branch ran.
|
|
1805
|
+
*
|
|
1806
|
+
* Cross-dialect implementation:
|
|
1807
|
+
* - postgres / sqlite: `INSERT … ON CONFLICT (cols) DO UPDATE SET …`
|
|
1808
|
+
* - mariadb 10.5+: `INSERT … ON DUPLICATE KEY UPDATE … RETURNING *`
|
|
1809
|
+
* - mysql: SELECT-then-INSERT-or-UPDATE fallback (no RETURNING)
|
|
1810
|
+
* - mssql: SELECT-then-INSERT-or-UPDATE fallback
|
|
1811
|
+
* - memory store: linear scan + match-by-conflict-columns
|
|
1812
|
+
*/
|
|
1813
|
+
upsert(table: string, row: Row_2, options: UpsertOptions): Promise<Row_2>;
|
|
1814
|
+
/**
|
|
1815
|
+
* `upsert`, plus the one thing it cannot report: whether the row was INSERTED
|
|
1816
|
+
* or UPDATED.
|
|
1817
|
+
*
|
|
1818
|
+
* An app that needs to know does the read itself today — look for the row,
|
|
1819
|
+
* then upsert — which costs a round trip AND opens a window in which the
|
|
1820
|
+
* answer stops being true. It does not have to: the framework's store wrapper
|
|
1821
|
+
* mints the id before the statement, and on the conflict path every dialect
|
|
1822
|
+
* hands back the EXISTING row's id. Two ids that differ ARE the answer, for
|
|
1823
|
+
* free.
|
|
1824
|
+
*
|
|
1825
|
+
* Optional, and provided by the framework's wrapper rather than by a dialect
|
|
1826
|
+
* store — the outcome is decided from what the wrapper already knows, so no
|
|
1827
|
+
* dialect implements anything. It is present on `ctx.store` in a handler; a
|
|
1828
|
+
* hand-written `DataStore` need not have it. The typed helper is
|
|
1829
|
+
* `upsertRowOutcome`.
|
|
1830
|
+
*
|
|
1831
|
+
* `'inserted'` is exact, never a default for "could not tell": where the free
|
|
1832
|
+
* comparison cannot answer (the caller supplied the id, or the table's id is
|
|
1833
|
+
* database-assigned) the destination is read first.
|
|
1834
|
+
*/
|
|
1835
|
+
upsertWithOutcome?(table: string, row: Row_2, options: UpsertOptions): Promise<UpsertResult>;
|
|
1836
|
+
/**
|
|
1837
|
+
* Insert-or-ignore: INSERT a row, but if it conflicts on
|
|
1838
|
+
* `conflictColumns`, leave the existing row UNCHANGED and skip the
|
|
1839
|
+
* write. Returns the existing row (when ignored) or the inserted
|
|
1840
|
+
* row.
|
|
1841
|
+
*
|
|
1842
|
+
* Equivalent to `upsert(..., { update: 'nothing' })` but typed as
|
|
1843
|
+
* a separate method for clarity at the call site. The signal is "I
|
|
1844
|
+
* want this row to exist; if it already does, don't touch it" —
|
|
1845
|
+
* common for dedup tables, idempotent event sinks, audit-log
|
|
1846
|
+
* write-once patterns.
|
|
1847
|
+
*
|
|
1848
|
+
* Cross-dialect:
|
|
1849
|
+
* - postgres / sqlite: `INSERT … ON CONFLICT (cols) DO NOTHING RETURNING *`
|
|
1850
|
+
* - mariadb 10.5+: `INSERT IGNORE … RETURNING *`, lookup on the ignore path
|
|
1851
|
+
* - mysql: SELECT-then-skip-or-INSERT fallback
|
|
1852
|
+
* - mssql: SELECT-then-skip-or-INSERT fallback
|
|
1853
|
+
* - memory store: lookup-then-skip
|
|
1854
|
+
*
|
|
1855
|
+
* Emits an `insert` ChangeEvent ONLY when the insert actually
|
|
1856
|
+
* happened; the ignore path emits nothing (no row changed).
|
|
1857
|
+
*/
|
|
1858
|
+
insertIgnore(table: string, row: Row_2, options: {
|
|
1859
|
+
conflictColumns: ReadonlyArray<string>;
|
|
1860
|
+
}): Promise<Row_2>;
|
|
1861
|
+
/**
|
|
1862
|
+
* `insertIgnore`, plus the one thing it cannot report: whether the row was
|
|
1863
|
+
* INSERTED or already there.
|
|
1864
|
+
*
|
|
1865
|
+
* The store's actual insert decision supplies the outcome, including for
|
|
1866
|
+
* caller-supplied and database-generated IDs. A pre-read or comparison of
|
|
1867
|
+
* IDs cannot establish which concurrent caller inserted. Every raw store,
|
|
1868
|
+
* namespace view and transaction view implements this method; wrappers
|
|
1869
|
+
* preserve its outcome while encoding/decoding the row. Inside a transaction
|
|
1870
|
+
* the result is provisional until commit. The typed helper is
|
|
1871
|
+
* `insertIgnoreRowOutcome`.
|
|
1872
|
+
*/
|
|
1873
|
+
insertIgnoreWithOutcome(table: string, row: Row_2, options: {
|
|
1874
|
+
conflictColumns: ReadonlyArray<string>;
|
|
1875
|
+
}): Promise<InsertIgnoreResult>;
|
|
1876
|
+
/**
|
|
1877
|
+
* Run `work` inside a transaction. ALL `insert/update/delete` calls
|
|
1878
|
+
* made via `work`'s store reference (passed as argument) are:
|
|
1879
|
+
*
|
|
1880
|
+
* - atomic — the dialect's native BEGIN/COMMIT/ROLLBACK wraps the
|
|
1881
|
+
* writes; memory validates snapshot read/write-table revisions before
|
|
1882
|
+
* committing and retries conflicting attempts with a fresh snapshot.
|
|
1883
|
+
* - delta-deferred — ChangeEvents queue inside the transaction and
|
|
1884
|
+
* are emitted ONLY after a successful commit. A throw triggers
|
|
1885
|
+
* rollback AND drops every queued event so no subscriber ever
|
|
1886
|
+
* sees a partial-update flicker.
|
|
1887
|
+
*
|
|
1888
|
+
* Reads (`query()`) inside the transaction see the in-flight write
|
|
1889
|
+
* set (read-your-own-writes within the transaction).
|
|
1890
|
+
* Transient contention can replay the whole callback (at most four attempts,
|
|
1891
|
+
* exponential delay starting at 10 ms). Do not perform external side effects
|
|
1892
|
+
* in the callback. Memory validation is table-granular and process-local;
|
|
1893
|
+
* `forUpdate` does not acquire a blocking row lock there.
|
|
1894
|
+
*
|
|
1895
|
+
* `transactional()` MUST NOT be nested — nested invocations throw to
|
|
1896
|
+
* surface design mistakes (transactions are atomic units, not
|
|
1897
|
+
* composable scopes). Use a single top-level `transactional()` per
|
|
1898
|
+
* mutation.
|
|
1899
|
+
*/
|
|
1900
|
+
transactional<T>(work: (tx: DataStore) => Promise<T>): Promise<T>;
|
|
1901
|
+
/** Subscribe to row-change events. Returns an unsubscribe function.
|
|
1902
|
+
*
|
|
1903
|
+
* See `CHANGE_LISTENER_CEILING` for why an EventEmitter-backed
|
|
1904
|
+
* implementation must raise its listener limit before binding. */
|
|
1905
|
+
onChange(listener: (event: ChangeEvent) => void): () => void;
|
|
1906
|
+
/**
|
|
1907
|
+
* Inject a ChangeEvent that originated OUTSIDE this process — the
|
|
1908
|
+
* cross-instance reactivity seam. Emits the event to local `onChange`
|
|
1909
|
+
* subscribers (the dispatcher, the cache invalidation bus, …) exactly
|
|
1910
|
+
* as a local write would, WITHOUT re-persisting it (the row already
|
|
1911
|
+
* landed in the shared database on the writer).
|
|
1912
|
+
*
|
|
1913
|
+
* Two consumers route through here:
|
|
1914
|
+
* - Postgres' LISTEN/NOTIFY CDC consumer, translating each NOTIFY
|
|
1915
|
+
* payload back into a ChangeEvent.
|
|
1916
|
+
* - `@voltro/plugin-broadcast`'s bus, injecting remote-replica
|
|
1917
|
+
* ChangeEvents fanned out over Redis / NATS for the non-postgres
|
|
1918
|
+
* dialects that have no native fan-out.
|
|
1919
|
+
*
|
|
1920
|
+
* It is NOT a write method — there is no transaction, no SQL, no return
|
|
1921
|
+
* value. Subscribe-side only.
|
|
1922
|
+
*/
|
|
1923
|
+
injectExternalChange(event: ChangeEvent): void;
|
|
1924
|
+
/**
|
|
1925
|
+
* How this store's ChangeEvents reach the process. `'local'` (the
|
|
1926
|
+
* default when absent): `onChange` carries this process's OWN writes
|
|
1927
|
+
* inline, plus optionally `'injected'`-origin events from a broadcast
|
|
1928
|
+
* transport — a listener that skips `origin === 'injected'` therefore
|
|
1929
|
+
* sees each fleet-wide change exactly once (on its writer). `'fleet'`:
|
|
1930
|
+
* every replica receives the full fleet-wide stream via a CDC consumer
|
|
1931
|
+
* (own writes included, all `'injected'`) — an exactly-once consumer
|
|
1932
|
+
* must elect a single worker instead of filtering by origin.
|
|
1933
|
+
*/
|
|
1934
|
+
readonly changeScope?: 'local' | 'fleet';
|
|
1935
|
+
/**
|
|
1936
|
+
* Subscribe to CHANGE-STREAM GAPS — moments where this process proved it
|
|
1937
|
+
* stopped receiving changes for a while and has now started again.
|
|
1938
|
+
*
|
|
1939
|
+
* A gap is not an error to show a user; it is an instruction to the reactive
|
|
1940
|
+
* layer: everything derived from this store may be stale, so re-run it. A
|
|
1941
|
+
* live query is idempotent, which is what makes "re-run all of them" both
|
|
1942
|
+
* safe and the complete recovery. `voltro dev` and `voltro serve` wire this
|
|
1943
|
+
* to the dispatcher's `refreshAll`, the same recovery the broadcast bus's
|
|
1944
|
+
* peer-serial gap uses.
|
|
1945
|
+
*
|
|
1946
|
+
* Optional because only a store whose changes arrive over a TRANSPORT can
|
|
1947
|
+
* have one. An inline store emits its own writes in-process — there is no
|
|
1948
|
+
* connection to lose — and correctly does not implement this. Absent
|
|
1949
|
+
* therefore means "gaps are impossible here", never "gaps are not reported".
|
|
1950
|
+
*/
|
|
1951
|
+
readonly onChangeStreamGap?: (listener: (gap: ChangeStreamGap) => void) => () => void;
|
|
1952
|
+
/**
|
|
1953
|
+
* Bind a per-request tenant NAMESPACE, returning a store view whose
|
|
1954
|
+
* every read + write is physically isolated to that namespace
|
|
1955
|
+
* (schema / database / attached file per dialect — see
|
|
1956
|
+
* `@voltro/database`'s `namespace.ts`). The returned view shares the
|
|
1957
|
+
* underlying connection pool + change emitter with `this`; only the
|
|
1958
|
+
* namespace qualification differs.
|
|
1959
|
+
*
|
|
1960
|
+
* `namespace` is the already-resolved, already-sanitised identifier
|
|
1961
|
+
* (e.g. `tenant_acme`) from `resolveTenantNamespace(subject.tenantId)`.
|
|
1962
|
+
* Passing `null` returns a view with NO qualification — the
|
|
1963
|
+
* shared-schema default. The runtime resolves the namespace from the
|
|
1964
|
+
* request subject and binds it here; it NEVER passes a raw, unsanitised
|
|
1965
|
+
* tenant id.
|
|
1966
|
+
*
|
|
1967
|
+
* OPTIONAL on the interface: every shipped store implements it, but
|
|
1968
|
+
* callers that never enable namespace isolation can ignore it.
|
|
1969
|
+
* Equivalent to `this` when the store has no physical-isolation
|
|
1970
|
+
* capability (none ship without it, but the optionality keeps
|
|
1971
|
+
* third-party stores from being forced to implement it).
|
|
1972
|
+
*/
|
|
1973
|
+
withNamespace?(namespace: string | null): DataStore;
|
|
1974
|
+
/**
|
|
1975
|
+
* Raw-SQL escape hatch. Execute a {@link RawSqlFragment} captured by
|
|
1976
|
+
* the `sql` template tag (`@voltro/database/sql`) against the store's
|
|
1977
|
+
* live connection and return the decoded rows.
|
|
1978
|
+
*
|
|
1979
|
+
* Values interpolated into the `sql` template are bound as parameters
|
|
1980
|
+
* by the store's dialect-specific `sql` constructor — never
|
|
1981
|
+
* interpolated into the SQL text — so a value containing `'; DROP
|
|
1982
|
+
* TABLE …` round-trips as data. The *static* SQL text is verbatim:
|
|
1983
|
+
* the caller owns its dialect-portability (an escape hatch trades
|
|
1984
|
+
* portability for power).
|
|
1985
|
+
*
|
|
1986
|
+
* `opts.dependsOn` (or `fragment.dependsOn`) declares the tables this
|
|
1987
|
+
* read touches so the reactive layer can recompute it on writes. Raw
|
|
1988
|
+
* reads are otherwise untracked — the planner can't infer table
|
|
1989
|
+
* dependencies from arbitrary SQL. Best-effort: the tables are
|
|
1990
|
+
* recorded, not validated against the fragment.
|
|
1991
|
+
*
|
|
1992
|
+
* OPTIONAL on the interface: SQL stores implement it; the in-memory
|
|
1993
|
+
* store omits it (there is no SQL engine to run raw text against).
|
|
1994
|
+
* Callers must guard with `if (store.raw)` before invoking.
|
|
1995
|
+
*/
|
|
1996
|
+
raw?<T extends object = Row_2>(fragment: RawSqlFragment, opts?: {
|
|
1997
|
+
dependsOn?: ReadonlyArray<string>;
|
|
1998
|
+
}): Promise<ReadonlyArray<T>>;
|
|
1999
|
+
/**
|
|
2000
|
+
* Gracefully dispose the store's connection pool + background consumers
|
|
2001
|
+
* (the CDC/NOTIFY fiber). Waits up to `gracePeriodMs` for in-flight
|
|
2002
|
+
* transactions to settle before tearing the pool down, so a `voltro serve`
|
|
2003
|
+
* SIGTERM doesn't sever a connection mid-COMMIT.
|
|
2004
|
+
*
|
|
2005
|
+
* OPTIONAL on the interface: every SQL-backed store implements it; the
|
|
2006
|
+
* in-memory store has no pool to close. `voltro serve`'s shutdown guards
|
|
2007
|
+
* with `if (store.close)`.
|
|
2008
|
+
*/
|
|
2009
|
+
close?(gracePeriodMs?: number): Promise<void>;
|
|
2010
|
+
/**
|
|
2011
|
+
* Liveness probe for the store's backing connection — runs a trivial
|
|
2012
|
+
* `SELECT 1`. Resolves when the DB answers; REJECTS when it can't (dead
|
|
2013
|
+
* pool, network partition). The serve path's `/internal/readiness` handler
|
|
2014
|
+
* uses it so a pod whose DB connection died reports 503 and stops taking
|
|
2015
|
+
* traffic instead of erroring every request.
|
|
2016
|
+
*
|
|
2017
|
+
* OPTIONAL on the interface: SQL stores implement it; the in-memory store
|
|
2018
|
+
* omits it (always reachable). Callers guard with `if (store.ping)`.
|
|
2019
|
+
*/
|
|
2020
|
+
ping?(): Promise<void>;
|
|
2021
|
+
/**
|
|
2022
|
+
* Empty every one of `tables` as ONE unit, with referential integrity
|
|
2023
|
+
* suspended for the duration.
|
|
2024
|
+
*
|
|
2025
|
+
* Written for `voltro data import --mode replace`, whose whole job is to put
|
|
2026
|
+
* a target into the state a bundle describes. Two properties, and both are
|
|
2027
|
+
* the point:
|
|
2028
|
+
*
|
|
2029
|
+
* - **All or nothing.** A run that cannot empty every table must leave the
|
|
2030
|
+
* target exactly as it found it. The loop this replaces deleted table by
|
|
2031
|
+
* table and stopped at the first refusal, so an operator's *second*
|
|
2032
|
+
* attempt destroyed more than the first — the deletes that had already
|
|
2033
|
+
* succeeded stayed gone and nothing was reloaded.
|
|
2034
|
+
* - **Order-free.** A parent cannot be emptied while a child still points
|
|
2035
|
+
* at it, and no ordering fixes the two shapes that occur in every real
|
|
2036
|
+
* schema: a table that references ITSELF (the framework's own actor
|
|
2037
|
+
* pattern — `createdBy` on `actors` points at `actors`), and a genuine
|
|
2038
|
+
* cycle. On engines that check each row as it is deleted, ordering the
|
|
2039
|
+
* TABLES cannot help, because the conflict is between two ROWS of one of
|
|
2040
|
+
* them.
|
|
2041
|
+
*
|
|
2042
|
+
* What it deliberately does NOT do is reach outside `tables`. A row in some
|
|
2043
|
+
* OTHER table pointing into this set is not ours to delete, and suspending
|
|
2044
|
+
* integrity would strand it. The caller checks for that first and refuses;
|
|
2045
|
+
* see `runImport`'s replace pre-flight.
|
|
2046
|
+
*
|
|
2047
|
+
* Emits no change events, and the consequence is worth stating rather than
|
|
2048
|
+
* implying. One event per deleted row is not affordable at bundle scale — a
|
|
2049
|
+
* replace of a grown target fanned tens of thousands of them out to every live
|
|
2050
|
+
* subscriber — but silence has an edge. In practice the LOAD that follows
|
|
2051
|
+
* covers it: the reactive layer re-reads a query when any row of its table
|
|
2052
|
+
* changes, so the first inserted row refreshes subscribers and the emptied
|
|
2053
|
+
* rows leave their view with it. The exception is a table the bundle carries
|
|
2054
|
+
* EMPTY: nothing is loaded, nothing is emitted, and a subscriber keeps rows
|
|
2055
|
+
* that are gone until it re-subscribes.
|
|
2056
|
+
*
|
|
2057
|
+
* OPTIONAL on the interface: SQL stores implement it per dialect (the
|
|
2058
|
+
* suspension mechanism is engine-specific and session-scoped, so it has to
|
|
2059
|
+
* live next to the connection). Callers guard with `if (store.emptyTables)`
|
|
2060
|
+
* and fall back to deleting inside one transaction — which keeps the
|
|
2061
|
+
* all-or-nothing half on any store, and is all the in-memory store needs.
|
|
2062
|
+
*/
|
|
2063
|
+
emptyTables?(tables: ReadonlyArray<string>): Promise<void>;
|
|
2064
|
+
/**
|
|
2065
|
+
* Foreign keys pointing INTO `tables` from tables that are NOT in the set,
|
|
2066
|
+
* read from the LIVE catalog.
|
|
2067
|
+
*
|
|
2068
|
+
* The question `--mode replace` has to answer before it deletes anything:
|
|
2069
|
+
* emptying a parent whose children live outside the bundle would strand rows
|
|
2070
|
+
* the bundle cannot restore. It was answered from the caller's SNAPSHOT, which
|
|
2071
|
+
* is right for the CLI (it introspects the target) and wrong for the admin
|
|
2072
|
+
* endpoint, which passes the app's DECLARED schema — that cannot show a table
|
|
2073
|
+
* the app has stopped declaring but the database still has, and those are
|
|
2074
|
+
* exactly the rows nobody is watching.
|
|
2075
|
+
*
|
|
2076
|
+
* The catalog is the only authority here, so the store asks it.
|
|
2077
|
+
*
|
|
2078
|
+
* OPTIONAL: SQL stores implement it; the in-memory store has no catalog and no
|
|
2079
|
+
* foreign keys, so it omits it and callers fall back to the snapshot.
|
|
2080
|
+
*/
|
|
2081
|
+
incomingForeignKeys?(tables: ReadonlyArray<string>): Promise<ReadonlyArray<{
|
|
2082
|
+
readonly from: string;
|
|
2083
|
+
readonly column: string;
|
|
2084
|
+
readonly to: string;
|
|
2085
|
+
}>>;
|
|
2086
|
+
/**
|
|
2087
|
+
* Run `fn` inside a nested savepoint, so a failure inside it costs only its
|
|
2088
|
+
* own work instead of the whole transaction.
|
|
2089
|
+
*
|
|
2090
|
+
* Exists for one reason: on postgres (and SQL Server for some errors) a failed
|
|
2091
|
+
* statement ABORTS the enclosing transaction — every statement after it
|
|
2092
|
+
* answers `current transaction is aborted` and nothing can be salvaged. The
|
|
2093
|
+
* importer's deferred-foreign-key repair depends on the opposite: a row whose
|
|
2094
|
+
* parent has not loaded yet is EXPECTED to fail, is held, and is retried once
|
|
2095
|
+
* the rest has streamed. That repair is how a bundle whose row order puts a
|
|
2096
|
+
* child before its parent lands at all — and ids sort that way as often as
|
|
2097
|
+
* not. Without a savepoint the two are incompatible, so `--atomic` simply
|
|
2098
|
+
* could not import such a bundle on those engines.
|
|
2099
|
+
*
|
|
2100
|
+
* Only the TRANSACTIONAL views implement it, and only for the engines that
|
|
2101
|
+
* need it: the mysql family and sqlite leave a transaction usable after a
|
|
2102
|
+
* failed statement, so wrapping every row there would buy nothing and cost a
|
|
2103
|
+
* round trip per row. Callers guard with `if (store.savepoint)` and run `fn`
|
|
2104
|
+
* directly otherwise — which is also correct outside a transaction, where
|
|
2105
|
+
* there is nothing to protect.
|
|
2106
|
+
*
|
|
2107
|
+
* Failures propagate: the savepoint is rolled back and the error rethrown, so
|
|
2108
|
+
* the caller's own error handling is unchanged.
|
|
2109
|
+
*/
|
|
2110
|
+
savepoint?<A>(fn: () => Promise<A>): Promise<A>;
|
|
2111
|
+
/**
|
|
2112
|
+
* Re-decide which tables this store's change-capture reader can decode, and
|
|
2113
|
+
* apply the answer to the LIVE reader.
|
|
2114
|
+
*
|
|
2115
|
+
* Exists because that decision is a reading of the SCHEMA, and a boot that
|
|
2116
|
+
* migrates in the same process moves the schema after the reader attached.
|
|
2117
|
+
* The reader is built before the migration runs (it has consumers — kv,
|
|
2118
|
+
* broadcast, the analytics mirror — that need a store first), so a table
|
|
2119
|
+
* excluded on the pre-migration shape stayed excluded for the life of the
|
|
2120
|
+
* process even when the migration was the very thing that fixed it.
|
|
2121
|
+
*
|
|
2122
|
+
* Call it once every boot, AFTER all schema work. Cheap (one catalog query)
|
|
2123
|
+
* and a no-op on a store whose verdict did not move — including every store
|
|
2124
|
+
* with nothing to exclude.
|
|
2125
|
+
*
|
|
2126
|
+
* OPTIONAL on the interface: only the mysql-family binlog reader has a
|
|
2127
|
+
* physical-layout exclusion at all. Postgres NOTIFY, mssql Change Tracking
|
|
2128
|
+
* and the in-memory store decode whatever they are given, so they omit it and
|
|
2129
|
+
* callers guard with `if (store.refreshChangeCaptureExclusions)`.
|
|
2130
|
+
*/
|
|
2131
|
+
refreshChangeCaptureExclusions?(): Promise<ChangeCaptureExclusionRefresh>;
|
|
2132
|
+
/**
|
|
2133
|
+
* Run an Effect that needs this store's own `SqlClient` — the raw-SQL seam.
|
|
2134
|
+
*
|
|
2135
|
+
* The capability the STAGED SWAP is built on. `--mode replace` loads into
|
|
2136
|
+
* `_voltro_staging_<t>` tables and then swaps the CONTENT in one short
|
|
2137
|
+
* transaction (`DELETE FROM t; INSERT INTO t (…) SELECT … FROM
|
|
2138
|
+
* _voltro_staging_t`), which moves the long, network-bound load OUT of the
|
|
2139
|
+
* destructive transaction. None of that is expressible through the typed row
|
|
2140
|
+
* API: it is DDL plus a server-side `INSERT … SELECT`, and the whole point is
|
|
2141
|
+
* that no row crosses the wire.
|
|
2142
|
+
*
|
|
2143
|
+
* OPTIONAL on the interface, and declared here rather than duck-typed at the
|
|
2144
|
+
* call site for the reason the house rule gives: the four dialect stores are
|
|
2145
|
+
* four hand-written copies of one interface, and a capability nothing declares
|
|
2146
|
+
* is a capability nothing can be red about. A store that loses it now fails to
|
|
2147
|
+
* satisfy this member instead of silently falling out of `canStage()` and
|
|
2148
|
+
* taking the weaker path forever, on one dialect, in silence.
|
|
2149
|
+
*
|
|
2150
|
+
* The DECISION does not live per store — that would be the four-copies trap
|
|
2151
|
+
* wearing a different hat. `@voltro/data-transfer`'s `stagedSwap.ts` writes
|
|
2152
|
+
* the per-dialect SQL ONCE and sends it down this seam; the store contributes
|
|
2153
|
+
* a connection, not a policy.
|
|
2154
|
+
*
|
|
2155
|
+
* Callers guard with `if (store.run)` (or `canStage(store)`), and a store
|
|
2156
|
+
* without it keeps the path it already has: the in-memory store has no SQL
|
|
2157
|
+
* and no catalog, and neither does a bare `DataStore` view.
|
|
2158
|
+
*
|
|
2159
|
+
* `R` is deliberately `SqlClient.SqlClient`: the effect asks for the client
|
|
2160
|
+
* and the store provides its own, which is what makes this a SEAM rather than
|
|
2161
|
+
* a way to hand a store somebody else's connection.
|
|
2162
|
+
*/
|
|
2163
|
+
run?<A, E>(effect: Effect.Effect<A, E, SqlClient.SqlClient>): Promise<A>;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
/**
|
|
2167
|
+
* Build a `SchemaSnapshot` from an array of `TableLike` declarations.
|
|
2168
|
+
* Caller is responsible for passing the FULL set of tables (user-
|
|
2169
|
+
* declared + framework). The fingerprint over this snapshot is what the
|
|
2170
|
+
* boot-time prod check compares against `_voltro_migration_plans`.
|
|
2171
|
+
*/
|
|
2172
|
+
export declare const declaredSnapshot: (tables: ReadonlyArray<TableLike>, dialect?: DialectId) => SchemaSnapshot;
|
|
2173
|
+
|
|
2174
|
+
/** The NOTIFY channel a schema emits on when none is configured. */
|
|
2175
|
+
export declare const DEFAULT_CDC_CHANNEL = "framework_changes";
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* `DEFAULT <expr>` for an array-valued default.
|
|
2179
|
+
*
|
|
2180
|
+
* Postgres is the only dialect with native arrays, so a `array()` column there
|
|
2181
|
+
* takes an array literal (`'{a,b}'::text[]`); every other dialect stores the
|
|
2182
|
+
* value as JSON/TEXT and takes the json form. A default on a `json()` column
|
|
2183
|
+
* always takes the json form — the array is the VALUE, not the storage.
|
|
2184
|
+
*/
|
|
2185
|
+
export declare const defaultArrayClause: (value: ReadonlyArray<unknown>, column: ColumnDefinition<unknown>, dialect: DialectId) => string;
|
|
2186
|
+
|
|
2187
|
+
export declare const defaultClause: (column: ColumnDefinition<unknown>, dialect: DialectId) => string | null;
|
|
2188
|
+
|
|
2189
|
+
/**
|
|
2190
|
+
* `DEFAULT <expr>` for a plain-object json default, per dialect's json-column
|
|
2191
|
+
* type. Single source for the auto-migrate (`defaultClause`) AND declarative-
|
|
2192
|
+
* applier paths so the two can't drift (the introspected form must round-trip
|
|
2193
|
+
* against `normalizeDefault`, so the emit shape matters on both sides).
|
|
2194
|
+
*/
|
|
2195
|
+
export declare const defaultJsonClause: (value: object, dialect: DialectId) => string;
|
|
2196
|
+
|
|
2197
|
+
/**
|
|
2198
|
+
* The DATA half of a three-step `add-column`, handed back by `executeOp`
|
|
2199
|
+
* instead of run in place when the caller collects it. `data` is the backfill
|
|
2200
|
+
* — UPDATEs only; `after` is the DDL that follows it (the NOT NULL
|
|
2201
|
+
* enforcement, or the sqlite rebuild that stands in for it). The two are
|
|
2202
|
+
* separate so the caller can run every collected `data` inside ONE
|
|
2203
|
+
* transaction and every `after` outside it, which is the only order MySQL
|
|
2204
|
+
* and MariaDB can honour: a DDL statement inside the transaction would commit
|
|
2205
|
+
* the backfills before it.
|
|
2206
|
+
*/
|
|
2207
|
+
export declare interface DeferredData {
|
|
2208
|
+
readonly table: string;
|
|
2209
|
+
readonly op: MigrationOperation;
|
|
2210
|
+
readonly data: Effect.Effect<void, SqlError_2>;
|
|
2211
|
+
readonly after: Effect.Effect<void, SqlError_2>;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
export declare const DEPLOY_LEDGER_TABLE = "_voltro_deploys";
|
|
2215
|
+
|
|
2216
|
+
export declare interface DeployLedgerRow {
|
|
2217
|
+
readonly id: string;
|
|
2218
|
+
readonly status: 'running' | 'failed';
|
|
2219
|
+
readonly dialect: string;
|
|
2220
|
+
readonly appliedBy: string;
|
|
2221
|
+
readonly startedAt: string;
|
|
2222
|
+
readonly baseline: string;
|
|
2223
|
+
readonly report: string | null;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
export declare const describeOutcome: (outcome: BootMigrationOutcome, dialectId: string) => string;
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* Parse `VOLTRO_DESTRUCTIVE_OK`.
|
|
2230
|
+
*
|
|
2231
|
+
* `1` / `true` acknowledges every lossy op in the run. A comma-separated list
|
|
2232
|
+
* acknowledges only those TABLES — which is the form somebody wants far more
|
|
2233
|
+
* often, and its absence was a real complaint: a plan carrying one intended drop
|
|
2234
|
+
* plus three other lossy ops had no way to say yes to the one. `1` said yes to
|
|
2235
|
+
* all four, and the alternative was a hand-written DROP migration, which the
|
|
2236
|
+
* framework's own upgrade note warns against.
|
|
2237
|
+
*
|
|
2238
|
+
* `undefined` = not set at all.
|
|
2239
|
+
*/
|
|
2240
|
+
export declare const destructiveScope: (raw: string | undefined) => {
|
|
2241
|
+
readonly kind: "all";
|
|
2242
|
+
} | {
|
|
2243
|
+
readonly kind: "tables";
|
|
2244
|
+
readonly tables: ReadonlySet<string>;
|
|
2245
|
+
} | undefined;
|
|
2246
|
+
|
|
2247
|
+
/**
|
|
2248
|
+
* Compare declared reactivity against the triggers actually installed.
|
|
2249
|
+
*
|
|
2250
|
+
* Postgres only — it is the one dialect where reactivity is carried by DDL. The
|
|
2251
|
+
* binlog and Change Tracking readers are configured at runtime from the same
|
|
2252
|
+
* in-memory table list the schema produced, so they cannot drift from it.
|
|
2253
|
+
*/
|
|
2254
|
+
export declare const detectReactiveTriggerDrift: (sql: SqlClient.SqlClient, tables: ReadonlyArray<{
|
|
2255
|
+
readonly tableName: string;
|
|
2256
|
+
readonly isReactive?: boolean;
|
|
2257
|
+
}>,
|
|
2258
|
+
/** The app's `cdcChannel`. Part of the trigger NAME on any non-default
|
|
2259
|
+
* channel, so omitting it here reported every table as missing on a database
|
|
2260
|
+
* where every trigger was present. */
|
|
2261
|
+
channel?: string) => Effect.Effect<ReactiveTriggerDrift, SqlError_2>;
|
|
2262
|
+
|
|
2263
|
+
/** Dialect IDs the framework supports. */
|
|
2264
|
+
declare type DialectId = 'postgres' | 'mysql' | 'mariadb' | 'mssql' | 'sqlite' | 'turso';
|
|
2265
|
+
|
|
2266
|
+
/**
|
|
2267
|
+
* Output of `voltro db drift`: compares the live DB's introspected
|
|
2268
|
+
* schema against the last applied fingerprint in `_voltro_migrations`.
|
|
2269
|
+
*
|
|
2270
|
+
* `isDrifted: false` is the happy path. A drift report names the
|
|
2271
|
+
* concrete divergences so the operator can either reconcile in code or
|
|
2272
|
+
* via a corrective migration.
|
|
2273
|
+
*/
|
|
2274
|
+
export declare interface DriftReport {
|
|
2275
|
+
readonly isDrifted: boolean;
|
|
2276
|
+
readonly lastAppliedFingerprint?: string;
|
|
2277
|
+
readonly liveFingerprint: string;
|
|
2278
|
+
readonly divergences: ReadonlyArray<{
|
|
2279
|
+
readonly kind: 'unexpected' | 'missing';
|
|
2280
|
+
readonly description: string;
|
|
2281
|
+
}>;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
export declare interface EachOptions {
|
|
2285
|
+
/** Rows per page. Default 1000. Pages are keyed by the `id` column. */
|
|
2286
|
+
readonly batchSize?: number;
|
|
2287
|
+
/** Milliseconds to sleep between pages, to share the database with live traffic. Default 0. */
|
|
2288
|
+
readonly sleepMs?: number;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
export declare interface EachReport {
|
|
2292
|
+
readonly scanned: number;
|
|
2293
|
+
readonly pages: number;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/**
|
|
2297
|
+
* Per-branch refinement passed to `.with({ relation: { ... } })`.
|
|
2298
|
+
*
|
|
2299
|
+
* `true` is the zero-config shorthand — equivalent to `{}`. Otherwise
|
|
2300
|
+
* each branch can carry its own `where` / `orderBy` / `limit` /
|
|
2301
|
+
* `offset` / nested `with`, so `users.with({ posts: { limit: 10,
|
|
2302
|
+
* orderBy: 'createdAt' } })` works without dropping into a sub-query
|
|
2303
|
+
* builder.
|
|
2304
|
+
*
|
|
2305
|
+
* Untyped at runtime: the QueryDescriptor is the boundary between the
|
|
2306
|
+
* typed builder API and the dialect-agnostic JoinCompiler. End-user
|
|
2307
|
+
* types live one layer up in the typed `.with()` method on `Query`.
|
|
2308
|
+
*/
|
|
2309
|
+
declare interface EagerLoadSpec {
|
|
2310
|
+
readonly where?: Predicate;
|
|
2311
|
+
readonly orderBy?: ReadonlyArray<OrderClause>;
|
|
2312
|
+
/**
|
|
2313
|
+
* Per-parent cap on the relation. `{ limit: 10 }` returns up to 10
|
|
2314
|
+
* rows FOR EACH parent — same semantics as Drizzle / Prisma /
|
|
2315
|
+
* Hibernate. This matches what users actually mean when they write
|
|
2316
|
+
* "the 10 latest posts" inside `users.with({ posts: { limit: 10 } })`.
|
|
2317
|
+
*
|
|
2318
|
+
* The SQL stores express this natively as a `LIMIT` clause in a
|
|
2319
|
+
* correlated subquery (postgres / sqlite / mysql / mariadb) or
|
|
2320
|
+
* via `TOP N` inside `FOR JSON PATH` (mssql) when the JSON-agg path
|
|
2321
|
+
* is active. The walker fallback post-buckets the batch fetch and
|
|
2322
|
+
* slices each bucket.
|
|
2323
|
+
*/
|
|
2324
|
+
readonly limit?: number;
|
|
2325
|
+
/** Per-parent offset. Only meaningful with `limit`. */
|
|
2326
|
+
readonly offset?: number;
|
|
2327
|
+
readonly with?: WithSpec;
|
|
2328
|
+
/**
|
|
2329
|
+
* Project the JUNCTION row's own columns into a `manyToMany` eager result.
|
|
2330
|
+
*
|
|
2331
|
+
* `true` takes every junction column; an array takes only those named. The
|
|
2332
|
+
* values land under `_junction` on each target row:
|
|
2333
|
+
*
|
|
2334
|
+
* projects.with({ teams: { junction: ['role', 'addedAt'] } })
|
|
2335
|
+
* // → team._junction.addedAt
|
|
2336
|
+
*
|
|
2337
|
+
* Nested rather than merged onto the target row, deliberately: a junction and
|
|
2338
|
+
* its target routinely share column names (`createdAt` is the obvious one),
|
|
2339
|
+
* and merging would silently overwrite real target data with membership data.
|
|
2340
|
+
* A name collision that corrupts a row is worse than one extra level.
|
|
2341
|
+
*
|
|
2342
|
+
* Why this exists: junction tables carrying meaningful columns — a role, a
|
|
2343
|
+
* joined-at stamp, a permission tier — are the rule rather than the
|
|
2344
|
+
* exception, and without this an eager load could FILTER on them
|
|
2345
|
+
* (`onJunction`) but never return them, so any relation with real membership
|
|
2346
|
+
* data had to stay a hand-written join. The junction rows were already being
|
|
2347
|
+
* fetched in full to resolve the target ids; this stops throwing them away.
|
|
2348
|
+
*/
|
|
2349
|
+
readonly junction?: true | ReadonlyArray<string>;
|
|
2350
|
+
/**
|
|
2351
|
+
* Filter a `manyToMany` eager branch on the THROUGH-table's columns.
|
|
2352
|
+
* The predicate is evaluated against the junction row, not the target
|
|
2353
|
+
* row — e.g. for `users.with({ organizations: { onJunction: eq('role',
|
|
2354
|
+
* 'admin') } })` it keeps only the memberships where `role = 'admin'`,
|
|
2355
|
+
* so the loaded `organizations` are exactly the orgs the user is an
|
|
2356
|
+
* admin of.
|
|
2357
|
+
*
|
|
2358
|
+
* Compiles into the correlated subquery's WHERE alongside the
|
|
2359
|
+
* source-key correlation, qualified to the junction-table alias.
|
|
2360
|
+
* Ignored on `one` / `many` branches (no junction table exists).
|
|
2361
|
+
*/
|
|
2362
|
+
readonly onJunction?: Predicate;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/**
|
|
2366
|
+
* Per-branch refinement passed to `.with({ relation: { ... } })`.
|
|
2367
|
+
*
|
|
2368
|
+
* `true` is the zero-config shorthand — equivalent to `{}`. Otherwise
|
|
2369
|
+
* each branch can carry its own `where` / `orderBy` / `limit` /
|
|
2370
|
+
* `offset` / nested `with`, so `users.with({ posts: { limit: 10,
|
|
2371
|
+
* orderBy: 'createdAt' } })` works without dropping into a sub-query
|
|
2372
|
+
* builder.
|
|
2373
|
+
*
|
|
2374
|
+
* Untyped at runtime: the QueryDescriptor is the boundary between the
|
|
2375
|
+
* typed builder API and the dialect-agnostic JoinCompiler. End-user
|
|
2376
|
+
* types live one layer up in the typed `.with()` method on `Query`.
|
|
2377
|
+
*/
|
|
2378
|
+
declare interface EagerLoadSpec_2 {
|
|
2379
|
+
readonly where?: Predicate_2;
|
|
2380
|
+
readonly orderBy?: ReadonlyArray<OrderClause_2>;
|
|
2381
|
+
/**
|
|
2382
|
+
* Per-parent cap on the relation. `{ limit: 10 }` returns up to 10
|
|
2383
|
+
* rows FOR EACH parent — same semantics as Drizzle / Prisma /
|
|
2384
|
+
* Hibernate. This matches what users actually mean when they write
|
|
2385
|
+
* "the 10 latest posts" inside `users.with({ posts: { limit: 10 } })`.
|
|
2386
|
+
*
|
|
2387
|
+
* The SQL stores express this natively as a `LIMIT` clause in a
|
|
2388
|
+
* correlated subquery (postgres / sqlite / mysql / mariadb) or
|
|
2389
|
+
* via `TOP N` inside `FOR JSON PATH` (mssql) when the JSON-agg path
|
|
2390
|
+
* is active. The walker fallback post-buckets the batch fetch and
|
|
2391
|
+
* slices each bucket.
|
|
2392
|
+
*/
|
|
2393
|
+
readonly limit?: number;
|
|
2394
|
+
/** Per-parent offset. Only meaningful with `limit`. */
|
|
2395
|
+
readonly offset?: number;
|
|
2396
|
+
readonly with?: WithSpec_2;
|
|
2397
|
+
/**
|
|
2398
|
+
* Project the JUNCTION row's own columns into a `manyToMany` eager result.
|
|
2399
|
+
*
|
|
2400
|
+
* `true` takes every junction column; an array takes only those named. The
|
|
2401
|
+
* values land under `_junction` on each target row:
|
|
2402
|
+
*
|
|
2403
|
+
* projects.with({ teams: { junction: ['role', 'addedAt'] } })
|
|
2404
|
+
* // → team._junction.addedAt
|
|
2405
|
+
*
|
|
2406
|
+
* Nested rather than merged onto the target row, deliberately: a junction and
|
|
2407
|
+
* its target routinely share column names (`createdAt` is the obvious one),
|
|
2408
|
+
* and merging would silently overwrite real target data with membership data.
|
|
2409
|
+
* A name collision that corrupts a row is worse than one extra level.
|
|
2410
|
+
*
|
|
2411
|
+
* Why this exists: junction tables carrying meaningful columns — a role, a
|
|
2412
|
+
* joined-at stamp, a permission tier — are the rule rather than the
|
|
2413
|
+
* exception, and without this an eager load could FILTER on them
|
|
2414
|
+
* (`onJunction`) but never return them, so any relation with real membership
|
|
2415
|
+
* data had to stay a hand-written join. The junction rows were already being
|
|
2416
|
+
* fetched in full to resolve the target ids; this stops throwing them away.
|
|
2417
|
+
*/
|
|
2418
|
+
readonly junction?: true | ReadonlyArray<string>;
|
|
2419
|
+
/**
|
|
2420
|
+
* Filter a `manyToMany` eager branch on the THROUGH-table's columns.
|
|
2421
|
+
* The predicate is evaluated against the junction row, not the target
|
|
2422
|
+
* row — e.g. for `users.with({ organizations: { onJunction: eq('role',
|
|
2423
|
+
* 'admin') } })` it keeps only the memberships where `role = 'admin'`,
|
|
2424
|
+
* so the loaded `organizations` are exactly the orgs the user is an
|
|
2425
|
+
* admin of.
|
|
2426
|
+
*
|
|
2427
|
+
* Compiles into the correlated subquery's WHERE alongside the
|
|
2428
|
+
* source-key correlation, qualified to the junction-table alias.
|
|
2429
|
+
* Ignored on `one` / `many` branches (no junction table exists).
|
|
2430
|
+
*/
|
|
2431
|
+
readonly onJunction?: Predicate_2;
|
|
2432
|
+
}
|
|
1149
2433
|
|
|
1150
2434
|
export declare const emitAddIndexMysql: (op: Extract<MigrationOperation, {
|
|
1151
2435
|
kind: "add-index";
|
|
@@ -1262,6 +2546,35 @@ declare type EmptyMerge = unknown;
|
|
|
1262
2546
|
*/
|
|
1263
2547
|
export declare const engineFromVersion: (version: string) => MysqlEngine;
|
|
1264
2548
|
|
|
2549
|
+
/** The deploy ledger (`_voltro_deploys`) — same bootstrap, same reason: it has to exist before the first plan. */
|
|
2550
|
+
export declare const ensureDeployLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId) => Effect.Effect<void, SqlError_2>;
|
|
2551
|
+
|
|
2552
|
+
/**
|
|
2553
|
+
* Create `_voltro_migration_ops` if it isn't there yet.
|
|
2554
|
+
*
|
|
2555
|
+
* Rendered from the SAME per-dialect create-table emitters the planner uses for
|
|
2556
|
+
* user tables, off the declared descriptor — so the ledger cannot acquire a
|
|
2557
|
+
* hand-written second DDL path that disagrees with the first. All four emitters
|
|
2558
|
+
* are already idempotent (`IF NOT EXISTS` / an `IF OBJECT_ID … IS NULL` guard on
|
|
2559
|
+
* mssql), so this is safe to call on every apply.
|
|
2560
|
+
*
|
|
2561
|
+
* The ledger is bootstrapped here rather than registered with the CLI's
|
|
2562
|
+
* framework-table set because it has to exist before the FIRST plan — and the
|
|
2563
|
+
* first plan is the one that creates the framework tables.
|
|
2564
|
+
*/
|
|
2565
|
+
export declare const ensureLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId) => Effect.Effect<void, SqlError_2>;
|
|
2566
|
+
|
|
2567
|
+
/** Exported for the same reason as {@link enumTypeBlocks}, and it MUST run
|
|
2568
|
+
* before it — see that function's note on ordering. */
|
|
2569
|
+
export declare const enumRenameBlocks: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => string;
|
|
2570
|
+
|
|
2571
|
+
/** Exported for the APPLIER: `voltro db apply` builds its DDL from operations,
|
|
2572
|
+
* not from this emitter, and an operation only names a column's type by tag.
|
|
2573
|
+
* A create-table referencing a `dbEnum` therefore ran before anything had
|
|
2574
|
+
* created the type — `type "x" does not exist`, on a fresh database, for every
|
|
2575
|
+
* app declaring one. One emitter, two callers. */
|
|
2576
|
+
export declare const enumTypeBlocks: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => string;
|
|
2577
|
+
|
|
1265
2578
|
/**
|
|
1266
2579
|
* `EXISTS (SELECT 1 FROM ... WHERE ...)` / `NOT EXISTS (...)`.
|
|
1267
2580
|
* Mirrors `SubqueryInPredicate` but doesn't bind to a specific
|
|
@@ -1274,6 +2587,38 @@ declare interface ExistsPredicate {
|
|
|
1274
2587
|
readonly subquery: QueryDescriptor;
|
|
1275
2588
|
}
|
|
1276
2589
|
|
|
2590
|
+
/**
|
|
2591
|
+
* `EXISTS (SELECT 1 FROM ... WHERE ...)` / `NOT EXISTS (...)`.
|
|
2592
|
+
* Mirrors `SubqueryInPredicate` but doesn't bind to a specific
|
|
2593
|
+
* outer column — the predicate is "the sub-query produces at least
|
|
2594
|
+
* one row" (or doesn't). Non-correlated in v1; the sub-query's
|
|
2595
|
+
* WHERE clause references only its own table.
|
|
2596
|
+
*/
|
|
2597
|
+
declare interface ExistsPredicate_2 {
|
|
2598
|
+
readonly op: 'exists' | 'not-exists';
|
|
2599
|
+
readonly subquery: QueryDescriptor_2;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
/**
|
|
2603
|
+
* The postgres extensions the declared tables need, as one DDL block — or
|
|
2604
|
+
* `undefined`. `IF NOT EXISTS`, so every apply can run it. Shared by the
|
|
2605
|
+
* bring-up emitter and the plan applier; a block only one of them emits is a
|
|
2606
|
+
* boot failure waiting for the right database (see the reactive-trigger scar).
|
|
2607
|
+
*/
|
|
2608
|
+
export declare const extensionBlocks: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => string | undefined;
|
|
2609
|
+
|
|
2610
|
+
/** An undo that could not complete: the row stays, with the report, until an operator forgets it. */
|
|
2611
|
+
export declare const failDeploy: (sql: SqlClient.SqlClient, id: string, report: string) => Effect.Effect<void, unknown>;
|
|
2612
|
+
|
|
2613
|
+
/**
|
|
2614
|
+
* Evaluate the post-conditions a body registered. The descriptions of the
|
|
2615
|
+
* ones that did not hold, in registration order.
|
|
2616
|
+
*/
|
|
2617
|
+
export declare const failedExpectations: (expectations: ReadonlyArray<{
|
|
2618
|
+
readonly description: string;
|
|
2619
|
+
readonly check: () => Promise<boolean> | Effect.Effect<boolean, unknown, never>;
|
|
2620
|
+
}>) => Effect.Effect<ReadonlyArray<string>, unknown>;
|
|
2621
|
+
|
|
1277
2622
|
/**
|
|
1278
2623
|
* Filename pattern the discovery walker matches: timestamp-prefixed
|
|
1279
2624
|
* `.ts` files in any depth under the project's `migrations/` dir.
|
|
@@ -1301,26 +2646,72 @@ export declare interface FileMigration {
|
|
|
1301
2646
|
/** Human description that surfaces in `voltro db plans` + the
|
|
1302
2647
|
* history timeline. Keep it short — what the migration does. */
|
|
1303
2648
|
readonly description: string;
|
|
1304
|
-
/**
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
2649
|
+
/**
|
|
2650
|
+
* Run `up` and the ledger row that records it inside ONE transaction, on
|
|
2651
|
+
* every dialect. **The default.** A body that fails or crashes leaves
|
|
2652
|
+
* nothing behind — not half its rows, and not a ledger row for work that did
|
|
2653
|
+
* not finish. `transactional: false` is one unit per statement with the
|
|
2654
|
+
* ledger row written after the body: a crash in the middle leaves the rows
|
|
2655
|
+
* already written AND no record, so the body runs again on the next boot
|
|
2656
|
+
* over half-converted data, and has to be written to survive that.
|
|
2657
|
+
*
|
|
2658
|
+
* The decision is per MIGRATION, not per dialect, because the engine
|
|
2659
|
+
* property that matters is about DDL: MySQL, MariaDB and Turso cannot hold
|
|
2660
|
+
* DDL inside a transaction — the MySQL family commits everything before it
|
|
2661
|
+
* on every `ALTER`. A transactional migration there is data only. The
|
|
2662
|
+
* runner MEASURES this rather than trusting it: a body that broke its own
|
|
2663
|
+
* transaction is recorded, so it does not run twice, and the run then fails
|
|
2664
|
+
* with the migration's name and the fix (`transactional: false`, or the
|
|
2665
|
+
* schema change through `ctx.schema` in a migration of its own).
|
|
2666
|
+
*/
|
|
2667
|
+
readonly transactional?: boolean;
|
|
2668
|
+
/** The forward body. */
|
|
1307
2669
|
readonly up: (ctx: FileMigrationContext) => Promise<void> | Effect.Effect<void, unknown, never>;
|
|
1308
|
-
/**
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
2670
|
+
/**
|
|
2671
|
+
* The reverse body — required, so that the decision is written down. A
|
|
2672
|
+
* migration that repairs a shape has no inverse: say so with
|
|
2673
|
+
* `down: irreversible('…')`, and `voltro db rollback-file` refuses with
|
|
2674
|
+
* that reason instead of running a body that pretends.
|
|
2675
|
+
*/
|
|
1312
2676
|
readonly down: (ctx: FileMigrationContext) => Promise<void> | Effect.Effect<void, unknown, never>;
|
|
1313
2677
|
}
|
|
1314
2678
|
|
|
1315
2679
|
/**
|
|
1316
2680
|
* The runtime context a file-based migration's `up` / `down` body
|
|
1317
2681
|
* receives.
|
|
2682
|
+
*
|
|
2683
|
+
* `store` is the same typed data store the application runs on — the same
|
|
2684
|
+
* column codecs, the same dialect, the same query builder — bound to the
|
|
2685
|
+
* database this migration is running against. A row read through it has its
|
|
2686
|
+
* JSON parsed and its timestamps as `Date`s on every dialect; a row written
|
|
2687
|
+
* through it is encoded the same way. `sql` stays beside it for what the
|
|
2688
|
+
* builder does not express.
|
|
1318
2689
|
*/
|
|
1319
2690
|
export declare interface FileMigrationContext {
|
|
1320
|
-
/**
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
|
|
2691
|
+
/**
|
|
2692
|
+
* The escape hatch, and it asks for a reason. `raw('why')` returns the
|
|
2693
|
+
* `@effect/sql` client — `sql.unsafe(...)`, `sql\`...\``,
|
|
2694
|
+
* `sql.onDialectOrElse({...})` — for the one thing neither `store` nor
|
|
2695
|
+
* `schema` expresses. The reason is required (an empty one throws), it is
|
|
2696
|
+
* what `voltro doctor` lists beside every migration that reaches for raw
|
|
2697
|
+
* SQL, and it is the sentence a reviewer reads first. A migration written
|
|
2698
|
+
* against the raw client is bound to the dialect it was written on; the
|
|
2699
|
+
* rest of this context is not.
|
|
2700
|
+
*/
|
|
2701
|
+
readonly raw: (reason: string) => SqlClient.SqlClient;
|
|
2702
|
+
/**
|
|
2703
|
+
* The application's data store over this database. Typed reads through a
|
|
2704
|
+
* descriptor (`store.query(database.vacations.descriptor)`) are checked
|
|
2705
|
+
* against the CURRENT declaration at compile time — a column renamed since
|
|
2706
|
+
* this migration was written fails the build, not the deploy. Reads by
|
|
2707
|
+
* table name (`store.query({ table: 'vacations', … })`) go to the database
|
|
2708
|
+
* as it is, decoded through whatever the current declaration says about
|
|
2709
|
+
* the columns it still knows; a column it no longer declares passes
|
|
2710
|
+
* through as the driver returned it.
|
|
2711
|
+
*/
|
|
2712
|
+
readonly store: DataStore;
|
|
2713
|
+
/** The dialect this migration is running against. */
|
|
2714
|
+
readonly dialect: DialectId;
|
|
1324
2715
|
/**
|
|
1325
2716
|
* Lightweight log surface scoped to the migration. Lands in
|
|
1326
2717
|
* `voltro logs --filter 'migrations:file:<id>'` for any apply
|
|
@@ -1333,6 +2724,71 @@ export declare interface FileMigrationContext {
|
|
|
1333
2724
|
/** ISO-8601 timestamp when this migration started (deterministic
|
|
1334
2725
|
* across the up/down pair via the applied_at column). */
|
|
1335
2726
|
readonly appliedAt: string;
|
|
2727
|
+
/** Schema questions and dialect-aware schema operations. */
|
|
2728
|
+
readonly schema: MigrationSchemaOps;
|
|
2729
|
+
/**
|
|
2730
|
+
* Visit every row of `table` in pages keyed by `id`. `fn` may return a
|
|
2731
|
+
* Promise or an Effect. The table needs an `id` column, which every
|
|
2732
|
+
* `table()` with `id()` has.
|
|
2733
|
+
*/
|
|
2734
|
+
readonly each: <R extends Row = Row>(table: string, fn: (row: R) => Promise<void> | Effect.Effect<void, unknown, never>, options?: EachOptions) => Effect.Effect<EachReport, unknown>;
|
|
2735
|
+
/**
|
|
2736
|
+
* Read rows in pages, compute a patch per row, write it back under an
|
|
2737
|
+
* optimistic guard, and repeat until a pass changes nothing.
|
|
2738
|
+
*
|
|
2739
|
+
* The guard is the row's own values: the UPDATE carries `WHERE id = ? AND
|
|
2740
|
+
* <each patched column> = <the value just read>` (plus `options.guard`), so
|
|
2741
|
+
* a row another process changed between the read and the write is not
|
|
2742
|
+
* overwritten — it is counted as contended and read again on the next
|
|
2743
|
+
* pass. That is the guard a migration needs while the previous deploy is
|
|
2744
|
+
* still writing, without a hashing function a dialect may not have.
|
|
2745
|
+
*
|
|
2746
|
+
* `fn` returns the patch, or `undefined` for a row that is already in
|
|
2747
|
+
* shape. A pass in which every row returns `undefined` and nothing was
|
|
2748
|
+
* contended is the stable state that ends the rewrite.
|
|
2749
|
+
*/
|
|
2750
|
+
readonly rewrite: <R extends Row = Row>(table: string, fn: (row: R) => Partial<R> | undefined | Promise<Partial<R> | undefined>, options?: RewriteOptions) => Effect.Effect<RewriteReport, unknown>;
|
|
2751
|
+
/**
|
|
2752
|
+
* A post-condition. Registered during `up`, evaluated after it — every one
|
|
2753
|
+
* must hold, or the migration FAILS and is not recorded, so the next run
|
|
2754
|
+
* tries again. This is the "read it back and check" every data migration
|
|
2755
|
+
* ends with, as a contract the runner enforces instead of a pattern each
|
|
2756
|
+
* body repeats.
|
|
2757
|
+
*/
|
|
2758
|
+
readonly expect: (description: string, check: () => Promise<boolean> | Effect.Effect<boolean, unknown, never>) => void;
|
|
2759
|
+
/**
|
|
2760
|
+
* A precondition. When `condition` is false the body stops HERE, the
|
|
2761
|
+
* migration is recorded as applied with `skipped: <reason>` in its notes,
|
|
2762
|
+
* and it never runs again on this database — the same answer Liquibase's
|
|
2763
|
+
* `MARK_RAN` gives. For a migration that must not be recorded when its
|
|
2764
|
+
* precondition fails, fail it instead.
|
|
2765
|
+
*
|
|
2766
|
+
* yield* ctx.skipUnless(ctx.schema.tableExists('vacations'), 'no vacations table on this database')
|
|
2767
|
+
*/
|
|
2768
|
+
readonly skipUnless: (condition: boolean | Promise<boolean> | Effect.Effect<boolean, unknown, never>, reason: string) => Effect.Effect<void, unknown>;
|
|
2769
|
+
/**
|
|
2770
|
+
* Run `work` inside ONE database transaction: the `store` it receives is
|
|
2771
|
+
* the transaction's view, and an Effect it returns is executed on the
|
|
2772
|
+
* transaction's connection, so `sql` statements yielded inside it are part
|
|
2773
|
+
* of the same unit. A migration that is transactional as a whole declares
|
|
2774
|
+
* `transactional: true` instead and gets the ledger row in the same unit.
|
|
2775
|
+
*
|
|
2776
|
+
* What the engine cannot do, this cannot do either: on MySQL and MariaDB a
|
|
2777
|
+
* DDL statement inside the unit commits everything before it. A
|
|
2778
|
+
* transactional scope is for data.
|
|
2779
|
+
*/
|
|
2780
|
+
readonly transactional: <T>(work: (tx: {
|
|
2781
|
+
readonly store: DataStore;
|
|
2782
|
+
readonly raw: (reason: string) => SqlClient.SqlClient;
|
|
2783
|
+
}) => Promise<T> | Effect.Effect<T, unknown, SqlClient.SqlClient>) => Effect.Effect<T, unknown>;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
export declare interface FileMigrationContextInput {
|
|
2787
|
+
readonly sql: SqlClient.SqlClient;
|
|
2788
|
+
readonly store: DataStore;
|
|
2789
|
+
readonly dialect: DialectId;
|
|
2790
|
+
readonly migrationId: string;
|
|
2791
|
+
readonly appliedAt: string;
|
|
1336
2792
|
}
|
|
1337
2793
|
|
|
1338
2794
|
/**
|
|
@@ -1410,6 +2866,25 @@ declare type IdScheme = {
|
|
|
1410
2866
|
readonly generate: (tableName: string) => string;
|
|
1411
2867
|
};
|
|
1412
2868
|
|
|
2869
|
+
/**
|
|
2870
|
+
* Fully-resolved scheme stored on the `id` column definition AFTER
|
|
2871
|
+
* `table()` walks the fields. For `typeid`, the prefix is always
|
|
2872
|
+
* populated here — either user-supplied or derived from the table name.
|
|
2873
|
+
*/
|
|
2874
|
+
declare type IdScheme_2 = {
|
|
2875
|
+
readonly kind: 'typeid';
|
|
2876
|
+
readonly prefix: string;
|
|
2877
|
+
} | {
|
|
2878
|
+
readonly kind: 'ulid';
|
|
2879
|
+
} | {
|
|
2880
|
+
readonly kind: 'numeric';
|
|
2881
|
+
} | {
|
|
2882
|
+
readonly kind: 'snowflake';
|
|
2883
|
+
} | {
|
|
2884
|
+
readonly kind: 'custom';
|
|
2885
|
+
readonly generate: (tableName: string) => string;
|
|
2886
|
+
};
|
|
2887
|
+
|
|
1413
2888
|
/**
|
|
1414
2889
|
* User-facing input to `id()`. Some shapes leave the prefix to be
|
|
1415
2890
|
* derived from the table name at register-time.
|
|
@@ -1436,6 +2911,32 @@ declare type IdSchemeInput = undefined | {
|
|
|
1436
2911
|
readonly generate: (tableName: string) => string;
|
|
1437
2912
|
};
|
|
1438
2913
|
|
|
2914
|
+
/**
|
|
2915
|
+
* User-facing input to `id()`. Some shapes leave the prefix to be
|
|
2916
|
+
* derived from the table name at register-time.
|
|
2917
|
+
*
|
|
2918
|
+
* Defaults:
|
|
2919
|
+
* - `undefined` → `{ scheme: 'typeid' }` with derived prefix
|
|
2920
|
+
* - `{ prefix }` → typeid with that prefix
|
|
2921
|
+
* - `{ scheme: 'typeid' }` → derived prefix
|
|
2922
|
+
* - `{ scheme: 'typeid', prefix }` → explicit prefix
|
|
2923
|
+
*/
|
|
2924
|
+
declare type IdSchemeInput_2 = undefined | {
|
|
2925
|
+
readonly prefix: string;
|
|
2926
|
+
} | {
|
|
2927
|
+
readonly scheme: 'typeid';
|
|
2928
|
+
readonly prefix?: string;
|
|
2929
|
+
} | {
|
|
2930
|
+
readonly scheme: 'ulid';
|
|
2931
|
+
} | {
|
|
2932
|
+
readonly scheme: 'numeric';
|
|
2933
|
+
} | {
|
|
2934
|
+
readonly scheme: 'snowflake';
|
|
2935
|
+
} | {
|
|
2936
|
+
readonly scheme: 'custom';
|
|
2937
|
+
readonly generate: (tableName: string) => string;
|
|
2938
|
+
};
|
|
2939
|
+
|
|
1439
2940
|
/**
|
|
1440
2941
|
* Live tables the planner must NOT drop, parsed from `VOLTRO_DB_IGNORE_TABLES`
|
|
1441
2942
|
* (comma-separated). The framework already self-excludes its own `_voltro_*`
|
|
@@ -1559,8 +3060,35 @@ export declare interface IndexSnapshot {
|
|
|
1559
3060
|
readonly expression?: boolean;
|
|
1560
3061
|
}
|
|
1561
3062
|
|
|
3063
|
+
/** What an `insertIgnoreWithOutcome` did, beside the row it hands back. */
|
|
3064
|
+
declare interface InsertIgnoreResult {
|
|
3065
|
+
readonly row: Row_2;
|
|
3066
|
+
/** `'ignored'` means a row already existed for the conflict key; it is the
|
|
3067
|
+
* row returned, untouched. */
|
|
3068
|
+
readonly outcome: 'inserted' | 'ignored';
|
|
3069
|
+
}
|
|
3070
|
+
|
|
1562
3071
|
export declare const introspectSchema: (sql: SqlClient.SqlClient) => Effect.Effect<SchemaSnapshot, SqlError_2>;
|
|
1563
3072
|
|
|
3073
|
+
/**
|
|
3074
|
+
* A `down` that says there is no inverse — with the reason, which is what a
|
|
3075
|
+
* rollback prints. A data migration that repairs a broken shape HAS no
|
|
3076
|
+
* inverse (its inverse would be the defect), and a `down` that only logs
|
|
3077
|
+
* "nothing to undo" and returns success lets `voltro db rollback-file`
|
|
3078
|
+
* delete the ledger row for a change that is still in the database.
|
|
3079
|
+
*/
|
|
3080
|
+
export declare const irreversible: (reason: string) => FileMigration["down"];
|
|
3081
|
+
|
|
3082
|
+
export declare class IrreversibleMigrationError extends Error {
|
|
3083
|
+
readonly migrationId: string;
|
|
3084
|
+
readonly reason: string;
|
|
3085
|
+
readonly _tag = "IrreversibleMigrationError";
|
|
3086
|
+
constructor(migrationId: string, reason: string);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
/** The reason a `down` was declared irreversible with, or `undefined` for an ordinary body. */
|
|
3090
|
+
export declare const irreversibleReason: (down: FileMigration["down"]) => string | undefined;
|
|
3091
|
+
|
|
1564
3092
|
export declare const isAlreadySatisfied: (err: unknown, rules: ReadonlyArray<AlreadySatisfied>) => boolean;
|
|
1565
3093
|
|
|
1566
3094
|
/** Type-guard for the discovery walker. */
|
|
@@ -1601,8 +3129,31 @@ declare interface JsonIndexPath {
|
|
|
1601
3129
|
|
|
1602
3130
|
declare type LeafOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'startsWith' | 'arrayContains' | 'arrayOverlaps' | 'arrayHas' | 'isNull' | 'isNotNull' | 'spatial';
|
|
1603
3131
|
|
|
3132
|
+
declare type LeafOperator_2 = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'startsWith' | 'arrayContains' | 'arrayOverlaps' | 'arrayHas' | 'isNull' | 'isNotNull' | 'spatial';
|
|
3133
|
+
|
|
3134
|
+
/**
|
|
3135
|
+
* Build the context for one migration body. Returns the context and the
|
|
3136
|
+
* post-conditions it collects, so the runner can evaluate them after `up`.
|
|
3137
|
+
*/
|
|
3138
|
+
export declare const makeFileMigrationContext: (input: FileMigrationContextInput) => {
|
|
3139
|
+
ctx: FileMigrationContext;
|
|
3140
|
+
expectations: {
|
|
3141
|
+
readonly description: string;
|
|
3142
|
+
readonly check: () => Promise<boolean> | Effect.Effect<boolean, unknown, never>;
|
|
3143
|
+
}[];
|
|
3144
|
+
};
|
|
3145
|
+
|
|
1604
3146
|
export declare const mapPgType: (dataType: string, udtName?: string) => ColumnType;
|
|
1605
3147
|
|
|
3148
|
+
/**
|
|
3149
|
+
* A whole live snapshot as DECLARATIONS, each table's references resolved
|
|
3150
|
+
* against the others — what the planner needs on its declared side to bring a
|
|
3151
|
+
* database back to the shape this snapshot describes. Used by the deploy
|
|
3152
|
+
* compensation: the pre-deploy snapshot is the target, the current live schema
|
|
3153
|
+
* the source, and the plan between them is the undo.
|
|
3154
|
+
*/
|
|
3155
|
+
export declare const materialiseSnapshot: (snap: SchemaSnapshot) => ReadonlyArray<TableLike>;
|
|
3156
|
+
|
|
1606
3157
|
declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = Mixins extends readonly [] ? EmptyMerge : Mixins extends readonly [infer Head, ...infer Rest] ? (Head extends MixinDefinition<infer F> ? F : EmptyMerge) & (Rest extends ReadonlyArray<AnyMixin> ? MergeMixinFields<Rest> : EmptyMerge) : EmptyMerge;
|
|
1607
3158
|
|
|
1608
3159
|
/**
|
|
@@ -1616,20 +3167,51 @@ declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = Mixins e
|
|
|
1616
3167
|
* export default migration({
|
|
1617
3168
|
* id: '20260415_120000_split_address_out',
|
|
1618
3169
|
* description: 'Move users.address* into a separate addresses table.',
|
|
1619
|
-
* up:
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
* log.info('migrated address fields to new table')
|
|
1624
|
-
* },
|
|
1625
|
-
* down:
|
|
1626
|
-
* await sql.unsafe(`ALTER TABLE users ADD COLUMN addressStreet text`)
|
|
1627
|
-
* // ... etc — see file-based docs for the full pattern
|
|
1628
|
-
* },
|
|
3170
|
+
* up: ({ sql, store, log }) => Effect.gen(function* () {
|
|
3171
|
+
* yield* sql.unsafe(`CREATE TABLE addresses ( ... )`)
|
|
3172
|
+
* yield* sql.unsafe(`INSERT INTO addresses (...) SELECT (...) FROM users`)
|
|
3173
|
+
* yield* sql.unsafe(`ALTER TABLE users DROP COLUMN addressStreet`)
|
|
3174
|
+
* yield* log.info('migrated address fields to new table')
|
|
3175
|
+
* }),
|
|
3176
|
+
* down: irreversible('the address rows have no column to go back to'),
|
|
1629
3177
|
* })
|
|
1630
3178
|
*/
|
|
1631
3179
|
export declare const migration: (spec: FileMigration) => FileMigration;
|
|
1632
3180
|
|
|
3181
|
+
export declare const MIGRATION_ATOMICITY: Readonly<Record<DialectId, MigrationAtomicity>>;
|
|
3182
|
+
|
|
3183
|
+
/**
|
|
3184
|
+
* The lock HOLDS a connection for the whole apply, so the work needs a second
|
|
3185
|
+
* one — a pool of one waits out its acquire timeout and reports a connection
|
|
3186
|
+
* problem that is really a sizing one. Said here, at the failure, in the words
|
|
3187
|
+
* an operator behind a pooler can act on.
|
|
3188
|
+
*/
|
|
3189
|
+
export declare const MIGRATION_NEEDS_TWO_CONNECTIONS: string;
|
|
3190
|
+
|
|
3191
|
+
/**
|
|
3192
|
+
* How much of a plan a dialect can apply atomically.
|
|
3193
|
+
*
|
|
3194
|
+
* - `plan-transactional` — the applier wraps the plan in one transaction; a
|
|
3195
|
+
* mid-apply failure rolls the WHOLE plan back and there is nothing to resume.
|
|
3196
|
+
* - `per-operation` — each statement is its own unit; a mid-apply failure
|
|
3197
|
+
* leaves earlier operations applied, so the resume ledger carries the run.
|
|
3198
|
+
*
|
|
3199
|
+
* Keyed by `DialectId`, not by the four driver tokens, so `mariadb` and `turso`
|
|
3200
|
+
* are stated rather than inferred from a sibling. `resumeLedger.test.ts`
|
|
3201
|
+
* ("covers every dialect in the union") asserts every member has an entry.
|
|
3202
|
+
*/
|
|
3203
|
+
export declare type MigrationAtomicity = 'plan-transactional' | 'per-operation';
|
|
3204
|
+
|
|
3205
|
+
export declare const migrationAtomicityFor: (dialect: DialectId) => MigrationAtomicity;
|
|
3206
|
+
|
|
3207
|
+
/** One or more `ctx.expect` post-conditions did not hold after `up`. */
|
|
3208
|
+
export declare class MigrationExpectationFailed extends Error {
|
|
3209
|
+
readonly migrationId: string;
|
|
3210
|
+
readonly failed: ReadonlyArray<string>;
|
|
3211
|
+
readonly _tag = "MigrationExpectationFailed";
|
|
3212
|
+
constructor(migrationId: string, failed: ReadonlyArray<string>);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
1633
3215
|
/**
|
|
1634
3216
|
* The postgres advisory-lock key for a schema. Default schema (undefined /
|
|
1635
3217
|
* `'public'`) → the EXACT legacy constant (rolling-deploy pin, see above).
|
|
@@ -1739,6 +3321,19 @@ export declare type MigrationOperation = {
|
|
|
1739
3321
|
readonly kind: 'drop-unique';
|
|
1740
3322
|
readonly table: string;
|
|
1741
3323
|
readonly column: string;
|
|
3324
|
+
} | {
|
|
3325
|
+
readonly kind: 'add-enum-value';
|
|
3326
|
+
readonly table: string;
|
|
3327
|
+
readonly column: string;
|
|
3328
|
+
/** The postgres type the value is added to — several columns may share it. */
|
|
3329
|
+
/**
|
|
3330
|
+
* The postgres TYPE name. ABSENT on every other dialect, and that is a
|
|
3331
|
+
* fact about the database rather than a missing field: the mysql family's
|
|
3332
|
+
* `ENUM(...)` is anonymous and mssql/sqlite render a CHECK, so there is no
|
|
3333
|
+
* named object to alter. The applier redeclares the column there instead.
|
|
3334
|
+
*/
|
|
3335
|
+
readonly enumName?: string;
|
|
3336
|
+
readonly value: string;
|
|
1742
3337
|
} | {
|
|
1743
3338
|
readonly kind: 'add-unique-composite';
|
|
1744
3339
|
readonly table: string;
|
|
@@ -1755,413 +3350,884 @@ export declare type MigrationOperation = {
|
|
|
1755
3350
|
readonly kind: 'add-foreign-key';
|
|
1756
3351
|
readonly table: string;
|
|
1757
3352
|
readonly column: string;
|
|
1758
|
-
readonly targetTable: string;
|
|
1759
|
-
readonly targetColumn: string;
|
|
1760
|
-
readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
1761
|
-
readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
1762
|
-
readonly orphanPolicy?: 'null' | 'delete';
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
|
|
3353
|
+
readonly targetTable: string;
|
|
3354
|
+
readonly targetColumn: string;
|
|
3355
|
+
readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
3356
|
+
readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
3357
|
+
readonly orphanPolicy?: 'null' | 'delete';
|
|
3358
|
+
}
|
|
3359
|
+
/** `name` is the live constraint's catalog name when the reader reported
|
|
3360
|
+
* one; absent, the applier falls back to the `<table>_<column>_fkey`
|
|
3361
|
+
* convention it uses when creating. */
|
|
3362
|
+
| {
|
|
3363
|
+
readonly kind: 'drop-foreign-key';
|
|
3364
|
+
readonly table: string;
|
|
3365
|
+
readonly column: string;
|
|
3366
|
+
readonly name?: string;
|
|
3367
|
+
} | {
|
|
3368
|
+
readonly kind: 'add-check';
|
|
3369
|
+
readonly table: string;
|
|
3370
|
+
readonly column: string;
|
|
3371
|
+
readonly values: ReadonlyArray<string>;
|
|
3372
|
+
} | {
|
|
3373
|
+
readonly kind: 'drop-check';
|
|
3374
|
+
readonly table: string;
|
|
3375
|
+
readonly column: string;
|
|
3376
|
+
} | {
|
|
3377
|
+
readonly kind: 'reconcile-json-check';
|
|
3378
|
+
readonly table: string;
|
|
3379
|
+
readonly column: string;
|
|
3380
|
+
readonly validation: 'value' | 'container';
|
|
3381
|
+
readonly previousValidation: 'value' | 'container' | 'unverified' | 'absent';
|
|
3382
|
+
readonly dropConstraints: ReadonlyArray<string>;
|
|
3383
|
+
};
|
|
3384
|
+
|
|
3385
|
+
export declare interface MigrationPlan {
|
|
3386
|
+
readonly operations: ReadonlyArray<PlannedOperation>;
|
|
3387
|
+
/** Pre-state fingerprint (live DB at plan time). */
|
|
3388
|
+
readonly fromFingerprint: string;
|
|
3389
|
+
/** Post-state fingerprint (what the DB will look like after apply). */
|
|
3390
|
+
readonly toFingerprint: string;
|
|
3391
|
+
/**
|
|
3392
|
+
* Where the two fingerprints part when the plan is EMPTY. `fromFingerprint`
|
|
3393
|
+
* hashes the whole live introspection; `toFingerprint` hashes the declaration;
|
|
3394
|
+
* a plan of zero operations says the schemas agree — and the hashes can still
|
|
3395
|
+
* differ. Then this names the tables on which they do:
|
|
3396
|
+
*
|
|
3397
|
+
* `outsideDiff` — live tables the diff never compares: framework-owned
|
|
3398
|
+
* (`cluster_*`, an undeclared `_voltro_*`), soft-drop snapshots, a name in
|
|
3399
|
+
* `VOLTRO_DB_IGNORE_TABLES`, the old name of a rename. In the live hash, in
|
|
3400
|
+
* no declaration.
|
|
3401
|
+
* `diffEquivalent` — tables on both sides the diff planned nothing for whose
|
|
3402
|
+
* live and declared snapshots hash apart anyway: a detail introspection
|
|
3403
|
+
* reports differently from how it was declared.
|
|
3404
|
+
*
|
|
3405
|
+
* Both empty means the hashes agree table for table. A reader who meets two
|
|
3406
|
+
* different hashes over `0 operations` is otherwise left to guess what moved.
|
|
3407
|
+
*/
|
|
3408
|
+
readonly fingerprintDivergence: {
|
|
3409
|
+
readonly outsideDiff: ReadonlyArray<string>;
|
|
3410
|
+
readonly diffEquivalent: ReadonlyArray<string>;
|
|
3411
|
+
};
|
|
3412
|
+
/** Count summaries by class — what `voltro db plan` prints. */
|
|
3413
|
+
readonly summary: {
|
|
3414
|
+
readonly safe: number;
|
|
3415
|
+
readonly needsDefault: number;
|
|
3416
|
+
readonly needsBackfill: number;
|
|
3417
|
+
readonly needsRenameAnnotation: number;
|
|
3418
|
+
readonly lossy: number;
|
|
3419
|
+
readonly onlineRequired: number;
|
|
3420
|
+
readonly multiStep: number;
|
|
3421
|
+
readonly blocked: number;
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
/** The schema operations a file migration's context can ask for by name. */
|
|
3426
|
+
export declare type MigrationSchemaOp = {
|
|
3427
|
+
readonly kind: 'drop-foreign-key';
|
|
3428
|
+
readonly table: string;
|
|
3429
|
+
readonly column: string;
|
|
3430
|
+
readonly name?: string;
|
|
3431
|
+
} | {
|
|
3432
|
+
readonly kind: 'drop-index';
|
|
3433
|
+
readonly table: string;
|
|
3434
|
+
readonly index: string;
|
|
3435
|
+
} | {
|
|
3436
|
+
readonly kind: 'drop-column';
|
|
3437
|
+
readonly table: string;
|
|
3438
|
+
readonly column: string;
|
|
3439
|
+
} | {
|
|
3440
|
+
readonly kind: 'rename-column';
|
|
3441
|
+
readonly table: string;
|
|
3442
|
+
readonly from: string;
|
|
3443
|
+
readonly to: string;
|
|
3444
|
+
};
|
|
3445
|
+
|
|
3446
|
+
/**
|
|
3447
|
+
* Schema questions and schema operations a migration body can ask of the
|
|
3448
|
+
* database it is running against, spelled once for every dialect.
|
|
3449
|
+
*
|
|
3450
|
+
* Every operation is idempotent by INSPECTION, not by error tolerance: it
|
|
3451
|
+
* reads the live schema first and answers `already-absent` /
|
|
3452
|
+
* `already-present` when there is nothing to do. That is the portable
|
|
3453
|
+
* `IF EXISTS` — MySQL has no such clause for a foreign key, and its two
|
|
3454
|
+
* "already gone" error codes are not the kind of thing a migration should
|
|
3455
|
+
* be matching on.
|
|
3456
|
+
*
|
|
3457
|
+
* On SQLite the dropping operations are table rebuilds (the dialect has no
|
|
3458
|
+
* `DROP CONSTRAINT`), built from the live table minus the change — the same
|
|
3459
|
+
* rebuild the planner's applier performs, with foreign-key enforcement off
|
|
3460
|
+
* for its duration and checked before the connection is handed back.
|
|
3461
|
+
*/
|
|
3462
|
+
export declare interface MigrationSchemaOps {
|
|
3463
|
+
/** The live schema, read fresh on every call — an operation above may have changed it. */
|
|
3464
|
+
readonly snapshot: () => Effect.Effect<SchemaSnapshot, unknown>;
|
|
3465
|
+
readonly tableExists: (table: string) => Effect.Effect<boolean, unknown>;
|
|
3466
|
+
readonly columnExists: (table: string, column: string) => Effect.Effect<boolean, unknown>;
|
|
3467
|
+
readonly indexExists: (table: string, index: string) => Effect.Effect<boolean, unknown>;
|
|
3468
|
+
/** Whether `table.column` currently carries a FOREIGN KEY. */
|
|
3469
|
+
readonly foreignKeyExists: (table: string, column: string) => Effect.Effect<boolean, unknown>;
|
|
3470
|
+
/** Drop the FOREIGN KEY on `table.column`, under the name the catalog reports for it. */
|
|
3471
|
+
readonly dropForeignKey: (table: string, column: string) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3472
|
+
readonly dropIndex: (table: string, index: string) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3473
|
+
/** Drop a column — and first the FOREIGN KEY it carries, if any, on the
|
|
3474
|
+
* dialects that refuse to drop a column a constraint still holds. */
|
|
3475
|
+
readonly dropColumn: (table: string, column: string) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3476
|
+
/** Rename a column. `already-present` when `to` exists and `from` does not:
|
|
3477
|
+
* a re-run after a crash that landed the rename. */
|
|
3478
|
+
readonly renameColumn: (table: string, from: string, to: string) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3479
|
+
/** Create a table from a declaration. `already-present` when a table of that name exists. */
|
|
3480
|
+
readonly createTable: (table: TableLike) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3481
|
+
/** Drop a table and everything in it. Data loss is the caller's explicit intent here. */
|
|
3482
|
+
readonly dropTable: (table: string) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3483
|
+
/** Rename a table. `already-present` when `to` exists and `from` does not. */
|
|
3484
|
+
readonly renameTable: (from: string, to: string) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3485
|
+
/**
|
|
3486
|
+
* Add a column. A NOT NULL column without a default cannot be added to a
|
|
3487
|
+
* table that has rows on any dialect: add it nullable or with a default,
|
|
3488
|
+
* fill it (`rewrite`), then `alterColumn(table, name, { nullable: false })`.
|
|
3489
|
+
*/
|
|
3490
|
+
readonly addColumn: (table: string, name: string, column: ColumnInput) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3491
|
+
/**
|
|
3492
|
+
* Change a column's nullability and/or default. `default: null` removes the
|
|
3493
|
+
* default; a type change goes through `evolve`, where the planner decides
|
|
3494
|
+
* whether it is lossy.
|
|
3495
|
+
*/
|
|
3496
|
+
readonly alterColumn: (table: string, name: string, change: {
|
|
3497
|
+
readonly nullable?: boolean;
|
|
3498
|
+
readonly default?: unknown;
|
|
3499
|
+
}) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3500
|
+
/** Add an index over `columns`; the name defaults to `<table>_<cols>_idx`. */
|
|
3501
|
+
readonly addIndex: (table: string, columns: ReadonlyArray<string>, options?: {
|
|
3502
|
+
readonly unique?: boolean;
|
|
3503
|
+
readonly name?: string;
|
|
3504
|
+
}) => Effect.Effect<SchemaOpOutcome, unknown>;
|
|
3505
|
+
/** Add a FOREIGN KEY from `table.column` to `target.table`'s `target.column` (default `id`). */
|
|
3506
|
+
readonly addForeignKey: (table: string, column: string, target: {
|
|
3507
|
+
readonly table: string;
|
|
3508
|
+
readonly column?: string;
|
|
3509
|
+
}, options?: {
|
|
3510
|
+
readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
3511
|
+
readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
3512
|
+
}) => 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>;
|
|
3515
|
+
/**
|
|
3516
|
+
* The Voltro way to change a table's shape in a migration: DECLARE the
|
|
3517
|
+
* table as it should be from here on, and the same planner that drives
|
|
3518
|
+
* `voltro db apply` diffs it against the live table and applies the
|
|
3519
|
+
* difference — add, drop, rename, retype, index, constraint — with the same
|
|
3520
|
+
* per-dialect statements, the same three-step backfills, and the same
|
|
3521
|
+
* convergence proof (an `evolve` whose re-plan is not empty fails). A drop
|
|
3522
|
+
* is allowed without a `dropped()` marker: calling `evolve` from a
|
|
3523
|
+
* migration IS the explicit intent the marker stands for elsewhere.
|
|
3524
|
+
*/
|
|
3525
|
+
readonly evolve: (table: TableLike) => Effect.Effect<{
|
|
3526
|
+
readonly applied: number;
|
|
3527
|
+
}, unknown>;
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
/**
|
|
3531
|
+
* The signal `ctx.skipUnless` raises. The runner treats it as an OUTCOME,
|
|
3532
|
+
* not a failure: the migration is recorded with the reason and never runs
|
|
3533
|
+
* again on this database.
|
|
3534
|
+
*/
|
|
3535
|
+
export declare class MigrationSkipped {
|
|
3536
|
+
readonly reason: string;
|
|
3537
|
+
readonly _tag = "MigrationSkipped";
|
|
3538
|
+
constructor(reason: string);
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
declare interface MixinDefinition<F extends Record<string, ColumnDefinition<unknown>>> {
|
|
3542
|
+
/**
|
|
3543
|
+
* Stable dedup key. Two mixins with the same `id` reaching a table via
|
|
3544
|
+
* different paths (direct + transitive) are materialised once. Convention:
|
|
3545
|
+
* `<pluginNamespace>/<mixinName>` — e.g. `voltro/audit`, `voltro/tenant`,
|
|
3546
|
+
* `acmeBilling/invoiceable`.
|
|
3547
|
+
*/
|
|
3548
|
+
readonly id?: string;
|
|
3549
|
+
readonly fields: F;
|
|
3550
|
+
/**
|
|
3551
|
+
* Other mixins this one needs in place. Resolved depth-first before the
|
|
3552
|
+
* mixin itself is applied, so its `beforeInsert` / `defaultWhere` can rely
|
|
3553
|
+
* on the columns added by its dependencies.
|
|
3554
|
+
*/
|
|
3555
|
+
readonly requires?: ReadonlyArray<AnyMixin>;
|
|
3556
|
+
/**
|
|
3557
|
+
* Indexes the mixin contributes when applied to a table. Used by the
|
|
3558
|
+
* built-in `tenant()` mixin to auto-index `tenantId` without
|
|
3559
|
+
* resurrecting a column-level `.index()` modifier (which we
|
|
3560
|
+
* deliberately collapsed into the single table-level surface). The
|
|
3561
|
+
* table-builder merges these into its `appliedIndexes` and
|
|
3562
|
+
* auto-names anonymous entries per-table.
|
|
3563
|
+
*/
|
|
3564
|
+
readonly indexes?: ReadonlyArray<MixinIndex>;
|
|
3565
|
+
readonly behaviors?: {
|
|
3566
|
+
readonly beforeInsert?: (...args: unknown[]) => unknown;
|
|
3567
|
+
readonly beforeUpdate?: (...args: unknown[]) => unknown;
|
|
3568
|
+
readonly onDelete?: (...args: unknown[]) => unknown;
|
|
3569
|
+
readonly defaultWhere?: (...args: unknown[]) => unknown;
|
|
3570
|
+
};
|
|
3571
|
+
readonly policies?: ReadonlyArray<{
|
|
3572
|
+
readonly name: string;
|
|
3573
|
+
readonly predicate: (...args: unknown[]) => boolean;
|
|
3574
|
+
}>;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
/**
|
|
3578
|
+
* Index contribution from a mixin. The fields are column names that
|
|
3579
|
+
* MUST exist either on the host table or on the mixin's own `fields`
|
|
3580
|
+
* map. The optional `name` lets the mixin pin the literal index
|
|
3581
|
+
* identifier; omitted, the table-builder auto-generates
|
|
3582
|
+
* `<tableName>_<col1>_<col2>_idx` so the same mixin applied to
|
|
3583
|
+
* different tables produces distinct DDL names.
|
|
3584
|
+
*/
|
|
3585
|
+
declare interface MixinIndex {
|
|
3586
|
+
readonly fields: ReadonlyArray<string>;
|
|
3587
|
+
readonly name?: string;
|
|
3588
|
+
/**
|
|
3589
|
+
* Index access method. Default `'btree'`. The `vectorEmbedding()` mixin
|
|
3590
|
+
* sets `kind: 'hnsw'` so the vector column it contributes also gets its
|
|
3591
|
+
* ANN index — without resurrecting a column-level `.index()` modifier.
|
|
3592
|
+
* The table-builder forwards `kind` + `kindOptions` into the
|
|
3593
|
+
* `TableIndex` it materialises from this contribution.
|
|
3594
|
+
*/
|
|
3595
|
+
readonly kind?: IndexKind;
|
|
3596
|
+
/** Per-kind tuning (e.g. `kindOptions.hnsw.{m, efConstruction, distance}`). */
|
|
3597
|
+
readonly kindOptions?: IndexKindOptions;
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
/** Which engine a `mysql`-dialect connection is actually talking to. */
|
|
3601
|
+
export declare type MysqlEngine = 'mysql' | 'mariadb';
|
|
3602
|
+
|
|
3603
|
+
/**
|
|
3604
|
+
* How long a prefix a MySQL/MariaDB index key may take on this column.
|
|
3605
|
+
*
|
|
3606
|
+
* `undefined` when the column cannot be found — an introspected-only column
|
|
3607
|
+
* with no live declaration. The caller then emits an unprefixed key, which is
|
|
3608
|
+
* right for every non-text type and only risks "key too long" for an undeclared
|
|
3609
|
+
* text one.
|
|
3610
|
+
*
|
|
3611
|
+
* A second guard, deliberately, and not because the first one is doubted: the
|
|
3612
|
+
* defect above was a renderer losing the column's parameters on the way to a
|
|
3613
|
+
* type decision, and that is a mistake a future path can make again. This one
|
|
3614
|
+
* cannot emit a prefix longer than the column, whatever concluded it needed
|
|
3615
|
+
* one — `min(191, declaredLength)`, which is what the reporter proposed.
|
|
3616
|
+
*
|
|
3617
|
+
* `undefined` = no prefix clause at all (the column is not text-like, or is
|
|
3618
|
+
* unbounded and takes the full 191).
|
|
3619
|
+
*/
|
|
3620
|
+
export declare const mysqlIndexPrefixFor: (col: ColumnSnapshot | undefined) => number | undefined;
|
|
3621
|
+
|
|
3622
|
+
/**
|
|
3623
|
+
* Type-narrowing migration marker. Tells the planner that this column
|
|
3624
|
+
* previously had type `from` and is now being narrowed/converted to its
|
|
3625
|
+
* current type. Without `using`, narrowing fails for rows whose value
|
|
3626
|
+
* can't fit the new type. The `using` fragment is the dialect-specific
|
|
3627
|
+
* cast expression (Postgres `... USING col::int`, MSSQL CONVERT/CAST,
|
|
3628
|
+
* MySQL CAST).
|
|
3629
|
+
*/
|
|
3630
|
+
declare interface NarrowedFromSpec {
|
|
3631
|
+
readonly from: ColumnType;
|
|
3632
|
+
/**
|
|
3633
|
+
* Raw SQL cast expression spliced into the planner's
|
|
3634
|
+
* `ALTER COLUMN … TYPE … USING <using>` (postgres) / shadow-copy
|
|
3635
|
+
* (`shadow := <using>(old)`). Authored by the developer in the
|
|
3636
|
+
* migration — e.g. `'role::role_enum'`, `'flag::int::text'`. Omit when
|
|
3637
|
+
* the conversion is implicit on the dialect (e.g. `varchar → text`).
|
|
3638
|
+
*/
|
|
3639
|
+
readonly using?: string;
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
/**
|
|
3643
|
+
* Type-narrowing migration marker. Tells the planner that this column
|
|
3644
|
+
* previously had type `from` and is now being narrowed/converted to its
|
|
3645
|
+
* current type. Without `using`, narrowing fails for rows whose value
|
|
3646
|
+
* can't fit the new type. The `using` fragment is the dialect-specific
|
|
3647
|
+
* cast expression (Postgres `... USING col::int`, MSSQL CONVERT/CAST,
|
|
3648
|
+
* MySQL CAST).
|
|
3649
|
+
*/
|
|
3650
|
+
declare interface NarrowedFromSpec_2 {
|
|
3651
|
+
readonly from: ColumnType_2;
|
|
3652
|
+
/**
|
|
3653
|
+
* Raw SQL cast expression spliced into the planner's
|
|
3654
|
+
* `ALTER COLUMN … TYPE … USING <using>` (postgres) / shadow-copy
|
|
3655
|
+
* (`shadow := <using>(old)`). Authored by the developer in the
|
|
3656
|
+
* migration — e.g. `'role::role_enum'`, `'flag::int::text'`. Omit when
|
|
3657
|
+
* the conversion is implicit on the dialect (e.g. `varchar → text`).
|
|
3658
|
+
*/
|
|
3659
|
+
readonly using?: string;
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
/**
|
|
3663
|
+
* Postgres identifier for the trigger function backing a given channel.
|
|
3664
|
+
*
|
|
3665
|
+
* Derived from the channel, NOT a single global name, and that is the whole
|
|
3666
|
+
* point: `CREATE OR REPLACE FUNCTION framework_notify_change()` is one row in
|
|
3667
|
+
* `pg_proc` for the entire database, so two schemas applied with different
|
|
3668
|
+
* channels overwrote each other and the last one won. Everything applied
|
|
3669
|
+
* earlier then emitted on somebody else's channel — silently, because a NOTIFY
|
|
3670
|
+
* nobody listens to is not an error.
|
|
3671
|
+
*/
|
|
3672
|
+
export declare const notifyFunctionName: (channel: string) => string;
|
|
3673
|
+
|
|
3674
|
+
/** Negation of an arbitrary sub-predicate → `NOT (...)`. Wraps any
|
|
3675
|
+
* leaf/and/or/subquery subtree (distinct from the per-column `neq` and
|
|
3676
|
+
* the narrow `notInSubquery`/`notExists`). */
|
|
3677
|
+
declare interface NotPredicate {
|
|
3678
|
+
readonly not: Predicate;
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
/** Negation of an arbitrary sub-predicate → `NOT (...)`. Wraps any
|
|
3682
|
+
* leaf/and/or/subquery subtree (distinct from the per-column `neq` and
|
|
3683
|
+
* the narrow `notInSubquery`/`notExists`). */
|
|
3684
|
+
declare interface NotPredicate_2 {
|
|
3685
|
+
readonly not: Predicate_2;
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
/**
|
|
3689
|
+
* DDL fragment for a numeric primary key column. Each dialect has its
|
|
3690
|
+
* own auto-increment idiom — there's no portable shorthand, so we
|
|
3691
|
+
* dispatch per dialect explicitly. The fragment includes type + PK +
|
|
3692
|
+
* auto-increment in one statement; the caller skips the generic
|
|
3693
|
+
* `NOT NULL` / `PRIMARY KEY` additions for numeric ids.
|
|
3694
|
+
*/
|
|
3695
|
+
export declare const numericIdSql: (name: string, dialect: DialectId) => string;
|
|
3696
|
+
|
|
3697
|
+
export declare const openDeploy: (sql: SqlClient.SqlClient, row: {
|
|
3698
|
+
readonly id: string;
|
|
3699
|
+
readonly dialect: string;
|
|
3700
|
+
readonly appliedBy: string;
|
|
3701
|
+
readonly baseline: string;
|
|
3702
|
+
}) => Effect.Effect<void, unknown>;
|
|
3703
|
+
|
|
3704
|
+
/**
|
|
3705
|
+
* The seven classes of schema operations the planner recognises. The
|
|
3706
|
+
* default policy for each is encoded by the planner; the applier
|
|
3707
|
+
* obeys it. Keep this enum in lockstep with the AGENTS.md template's
|
|
3708
|
+
* "Schema migrations" section — agents grep for these strings.
|
|
3709
|
+
*/
|
|
3710
|
+
export declare type OperationClass =
|
|
3711
|
+
/** Reversible, side-effect-free for existing data. Auto-applied. */
|
|
3712
|
+
'safe'
|
|
3713
|
+
/** ADD COLUMN + NOT NULL with `.default(value)` declared.
|
|
3714
|
+
* Applied as `ADD COLUMN ... NOT NULL DEFAULT <value>`. */
|
|
3715
|
+
| 'needs-default'
|
|
3716
|
+
/** ADD COLUMN + NOT NULL on a populated table, ADD UNIQUE / FK / CHECK
|
|
3717
|
+
* with existing offenders. Requires a deliberate declaration
|
|
3718
|
+
* (`.backfill()`, `.unique({dedup})`, `reference({orphanPolicy})`).
|
|
3719
|
+
* Applied as a three-step plan (ADD nullable → run backfill → SET
|
|
3720
|
+
* NOT NULL). */
|
|
3721
|
+
| 'needs-backfill'
|
|
3722
|
+
/** Column appears removed + a new one appeared. Looks like DROP+ADD
|
|
3723
|
+
* but is probably a rename. Refuse-to-plan without an explicit
|
|
3724
|
+
* `.renamedFrom('oldName')` marker — silent renames lose data. */
|
|
3725
|
+
| 'needs-rename-annotation'
|
|
3726
|
+
/** DROP COLUMN, DROP TABLE, narrow type with existing offenders.
|
|
3727
|
+
* Refuse-to-plan unless `dropped()` / `.narrowedFrom()` declared
|
|
3728
|
+
* OR `VOLTRO_DESTRUCTIVE_OK=1` set. */
|
|
3729
|
+
| 'lossy'
|
|
3730
|
+
/** Large-table operation that should run online (CREATE INDEX
|
|
3731
|
+
* CONCURRENTLY, batched backfill, shadow-column swap). The applier
|
|
3732
|
+
* auto-rewrites these once the row count crosses the threshold
|
|
3733
|
+
* (`online-after`, default 50k). */
|
|
3734
|
+
| 'online-required'
|
|
3735
|
+
/** Operations the planner can't infer from a diff (table split /
|
|
3736
|
+
* merge, custom data move, type-change with USING). Requires a
|
|
3737
|
+
* file-based migration in `migrations/<id>.ts`. */
|
|
3738
|
+
| 'multi-step';
|
|
3739
|
+
|
|
3740
|
+
/**
|
|
3741
|
+
* The vocabulary of concrete DDL-level operations the planner emits.
|
|
3742
|
+
* Each `MigrationOperation` carries one of these kinds plus the data
|
|
3743
|
+
* the applier needs to execute it.
|
|
3744
|
+
*
|
|
3745
|
+
* `kind` strings are short + lowercase-kebab so they round-trip
|
|
3746
|
+
* cleanly through JSON (the `operations` column of `_voltro_migrations`)
|
|
3747
|
+
* and can be grepped in `voltro logs` output.
|
|
3748
|
+
*/
|
|
3749
|
+
export declare type OperationKind = 'create-table' | 'drop-table' | 'add-column' | 'drop-column' | 'rename-column' | 'alter-column-nullability' | 'alter-column-type' | 'alter-column-default' | 'add-index' | 'drop-index' | 'add-unique' | 'drop-unique' | 'add-enum-value' | 'add-unique-composite' | 'drop-unique-composite' | 'add-foreign-key' | 'drop-foreign-key' | 'add-check' | 'drop-check' | 'reconcile-json-check';
|
|
3750
|
+
|
|
3751
|
+
declare interface OrderClause {
|
|
3752
|
+
readonly column: string;
|
|
3753
|
+
readonly direction: 'asc' | 'desc';
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
declare interface OrderClause_2 {
|
|
1766
3757
|
readonly column: string;
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
3758
|
+
readonly direction: 'asc' | 'desc';
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
declare interface OrPredicate {
|
|
3762
|
+
readonly or: ReadonlyArray<Predicate>;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
declare interface OrPredicate_2 {
|
|
3766
|
+
readonly or: ReadonlyArray<Predicate_2>;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
export declare const parseEnumCheck: (clause: string) => {
|
|
1770
3770
|
readonly column: string;
|
|
1771
3771
|
readonly values: ReadonlyArray<string>;
|
|
1772
|
-
} |
|
|
1773
|
-
readonly kind: 'drop-check';
|
|
1774
|
-
readonly table: string;
|
|
1775
|
-
readonly column: string;
|
|
1776
|
-
} | {
|
|
1777
|
-
readonly kind: 'reconcile-json-check';
|
|
1778
|
-
readonly table: string;
|
|
1779
|
-
readonly column: string;
|
|
1780
|
-
readonly validation: 'value' | 'container';
|
|
1781
|
-
readonly previousValidation: 'value' | 'container' | 'unverified' | 'absent';
|
|
1782
|
-
readonly dropConstraints: ReadonlyArray<string>;
|
|
1783
|
-
};
|
|
3772
|
+
} | null;
|
|
1784
3773
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
* differ. Then this names the tables on which they do:
|
|
1796
|
-
*
|
|
1797
|
-
* `outsideDiff` — live tables the diff never compares: framework-owned
|
|
1798
|
-
* (`cluster_*`, an undeclared `_voltro_*`), soft-drop snapshots, a name in
|
|
1799
|
-
* `VOLTRO_DB_IGNORE_TABLES`, the old name of a rename. In the live hash, in
|
|
1800
|
-
* no declaration.
|
|
1801
|
-
* `diffEquivalent` — tables on both sides the diff planned nothing for whose
|
|
1802
|
-
* live and declared snapshots hash apart anyway: a detail introspection
|
|
1803
|
-
* reports differently from how it was declared.
|
|
1804
|
-
*
|
|
1805
|
-
* Both empty means the hashes agree table for table. A reader who meets two
|
|
1806
|
-
* different hashes over `0 operations` is otherwise left to guess what moved.
|
|
1807
|
-
*/
|
|
1808
|
-
readonly fingerprintDivergence: {
|
|
1809
|
-
readonly outsideDiff: ReadonlyArray<string>;
|
|
1810
|
-
readonly diffEquivalent: ReadonlyArray<string>;
|
|
1811
|
-
};
|
|
1812
|
-
/** Count summaries by class — what `voltro db plan` prints. */
|
|
1813
|
-
readonly summary: {
|
|
1814
|
-
readonly safe: number;
|
|
1815
|
-
readonly needsDefault: number;
|
|
1816
|
-
readonly needsBackfill: number;
|
|
1817
|
-
readonly needsRenameAnnotation: number;
|
|
1818
|
-
readonly lossy: number;
|
|
1819
|
-
readonly onlineRequired: number;
|
|
1820
|
-
readonly multiStep: number;
|
|
1821
|
-
readonly blocked: number;
|
|
1822
|
-
};
|
|
1823
|
-
}
|
|
3774
|
+
/**
|
|
3775
|
+
* The ids of migration files present on disk but NOT yet recorded against THIS
|
|
3776
|
+
* database — i.e. exactly what `runFileBasedMigrations` would execute next.
|
|
3777
|
+
*
|
|
3778
|
+
* Exists so a caller can decide WHETHER to apply before applying. `voltro dev`
|
|
3779
|
+
* uses it to refuse unattended file migrations against a remote database: that
|
|
3780
|
+
* decision needs the list (to name it in the refusal) and must not have run
|
|
3781
|
+
* anything to get it.
|
|
3782
|
+
*/
|
|
3783
|
+
export declare const pendingFileMigrationIds: (sql: SqlClient.SqlClient, projectRoot: string) => Effect.Effect<ReadonlyArray<string>, unknown>;
|
|
1824
3784
|
|
|
1825
|
-
|
|
3785
|
+
/**
|
|
3786
|
+
* What `runFileBasedMigrations` would execute next, with the one fact a deploy
|
|
3787
|
+
* has to know BEFORE it starts: whether the migration's `down` is declared
|
|
3788
|
+
* `irreversible()`. A deploy that undoes its own work on failure can undo
|
|
3789
|
+
* every pending migration except those, so they are named in the question
|
|
3790
|
+
* the operator answers, not discovered in the report afterwards.
|
|
3791
|
+
*/
|
|
3792
|
+
export declare const pendingFileMigrationSummary: (sql: SqlClient.SqlClient, projectRoot: string) => Effect.Effect<ReadonlyArray<{
|
|
3793
|
+
readonly id: string;
|
|
3794
|
+
readonly irreversible: string | undefined;
|
|
3795
|
+
}>, unknown>;
|
|
3796
|
+
|
|
3797
|
+
export declare interface PlanInput {
|
|
3798
|
+
readonly declared: ReadonlyArray<TableLike>;
|
|
3799
|
+
readonly live: SchemaSnapshot;
|
|
1826
3800
|
/**
|
|
1827
|
-
*
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
* `acmeBilling/invoiceable`.
|
|
3801
|
+
* Threshold above which safe DDL ops on a populated table get
|
|
3802
|
+
* promoted to `online-required` (CONCURRENTLY / batched / shadow-
|
|
3803
|
+
* column). Pass undefined to use the framework default (50_000).
|
|
1831
3804
|
*/
|
|
1832
|
-
readonly
|
|
1833
|
-
readonly fields: F;
|
|
3805
|
+
readonly onlineAfter?: number;
|
|
1834
3806
|
/**
|
|
1835
|
-
*
|
|
1836
|
-
*
|
|
1837
|
-
*
|
|
3807
|
+
* The target dialect. Drives storage-collapse type equivalence (sqlite
|
|
3808
|
+
* stores timestamp/json as TEXT, mssql stores json as NVARCHAR) so the plan
|
|
3809
|
+
* doesn't churn phantom alter-column-type ops. Omit → no collapse (postgres
|
|
3810
|
+
* / mysql / mariadb distinguish these natively anyway).
|
|
1838
3811
|
*/
|
|
1839
|
-
readonly
|
|
3812
|
+
readonly dialect?: DialectId;
|
|
1840
3813
|
/**
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
3814
|
+
* Live tables to LEAVE ALONE — never planned as a drop even though no
|
|
3815
|
+
* declared entity matches them. The framework already self-excludes its own
|
|
3816
|
+
* runtime tables (`_voltro_*` / `_cloud_*` / `cluster_*`, see
|
|
3817
|
+
* `isFrameworkOwnedLiveTable`); this is the USER escape hatch for their own
|
|
3818
|
+
* unmanaged infra tables (e.g. a Strapi→Voltro `_strapi_id_map`), so the diff
|
|
3819
|
+
* doesn't try to drop a table the migration tooling itself depends on. The
|
|
3820
|
+
* CLI populates it from `VOLTRO_DB_IGNORE_TABLES` (comma-separated names).
|
|
1847
3821
|
*/
|
|
1848
|
-
readonly
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
3822
|
+
readonly ignoreTables?: ReadonlyArray<string>;
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
/**
|
|
3826
|
+
* Compute the migration plan between a declared schema (with
|
|
3827
|
+
* annotations) and a live snapshot.
|
|
3828
|
+
*
|
|
3829
|
+
* The output is deterministic: same inputs → same `MigrationPlan`
|
|
3830
|
+
* (modulo op insertion order, which follows table-then-kind sort
|
|
3831
|
+
* order). Lets the applier replay the plan from `_voltro_migration_plans`
|
|
3832
|
+
* without surprises.
|
|
3833
|
+
*
|
|
3834
|
+
* The planner does NOT execute pre-check queries — that's the
|
|
3835
|
+
* applier's job (it needs live SQL access). The classifications here
|
|
3836
|
+
* are based purely on the snapshot shapes + the annotations on
|
|
3837
|
+
* `ColumnDefinition`. The applier may downgrade `safe` → `online-required`
|
|
3838
|
+
* once it knows the row count exceeds the threshold.
|
|
3839
|
+
*/
|
|
3840
|
+
export declare const planMigrations: (input: PlanInput) => MigrationPlan;
|
|
3841
|
+
|
|
3842
|
+
/**
|
|
3843
|
+
* One row in a `MigrationPlan` — a concrete operation paired with the
|
|
3844
|
+
* planner's classification + any blocking reason.
|
|
3845
|
+
*
|
|
3846
|
+
* `blocked` is set when the classification refuses-to-plan (lossy
|
|
3847
|
+
* without `dropped()`, rename without `.renamedFrom()`, etc.). The
|
|
3848
|
+
* applier checks `blocked` before executing; the CLI's `voltro db plan`
|
|
3849
|
+
* output highlights it red.
|
|
3850
|
+
*
|
|
3851
|
+
* `backfill` / `dedup` / `orphanPolicy` mirror the user's annotations
|
|
3852
|
+
* straight from `ColumnDefinition` so the applier doesn't need to
|
|
3853
|
+
* re-introspect them.
|
|
3854
|
+
*/
|
|
3855
|
+
export declare interface PlannedOperation {
|
|
3856
|
+
readonly op: MigrationOperation;
|
|
3857
|
+
readonly classification: OperationClass;
|
|
3858
|
+
/** Why the op was classified this way (also surfaces in the CLI). */
|
|
3859
|
+
readonly reason?: string;
|
|
3860
|
+
/** Set on lossy / needs-rename / multi-step ops with no resolution. */
|
|
3861
|
+
readonly blocked?: {
|
|
3862
|
+
readonly fix: string;
|
|
3863
|
+
};
|
|
3864
|
+
/** Pre-check estimate the planner ran (row counts, offender count). */
|
|
3865
|
+
readonly estimate?: {
|
|
3866
|
+
readonly affectedRows?: number;
|
|
3867
|
+
readonly offenderCount?: number;
|
|
3868
|
+
};
|
|
3869
|
+
/** Whether the op is atomic for each supported dialect. Postgres,
|
|
3870
|
+
* mssql, sqlite are atomic for all DDL; mysql/mariadb implicit-commit
|
|
3871
|
+
* every statement. */
|
|
3872
|
+
readonly atomicByDialect: {
|
|
3873
|
+
readonly postgres: boolean;
|
|
3874
|
+
readonly mysql: boolean;
|
|
3875
|
+
readonly mariadb: boolean;
|
|
3876
|
+
readonly mssql: boolean;
|
|
3877
|
+
readonly sqlite: boolean;
|
|
1854
3878
|
};
|
|
1855
|
-
readonly policies?: ReadonlyArray<{
|
|
1856
|
-
readonly name: string;
|
|
1857
|
-
readonly predicate: (...args: unknown[]) => boolean;
|
|
1858
|
-
}>;
|
|
1859
3879
|
}
|
|
1860
3880
|
|
|
1861
|
-
/**
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
*/
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
3881
|
+
/** What happens to the referencing row when the target row goes away. */
|
|
3882
|
+
declare type PluginRefOrphanPolicy =
|
|
3883
|
+
/** Delete the referencing row. For a row that only exists to point at it —
|
|
3884
|
+
* a favourite, a pin, a share. */
|
|
3885
|
+
'delete'
|
|
3886
|
+
/** Null the column, keeping the row. Requires `.nullable()`; declaring it on
|
|
3887
|
+
* a non-nullable column is refused at declaration rather than failing at the
|
|
3888
|
+
* first delete, months later. */
|
|
3889
|
+
| 'null'
|
|
3890
|
+
/** Do nothing. The explicit "I know, and I handle it myself" — different from
|
|
3891
|
+
* omitting the option, which is the same behaviour arrived at by accident. */
|
|
3892
|
+
| 'keep';
|
|
3893
|
+
|
|
3894
|
+
declare type Predicate = PredicateLeaf | AndPredicate | OrPredicate | NotPredicate | SubqueryInPredicate | ExistsPredicate;
|
|
3895
|
+
|
|
3896
|
+
declare type Predicate_2 = PredicateLeaf_2 | AndPredicate_2 | OrPredicate_2 | NotPredicate_2 | SubqueryInPredicate_2 | ExistsPredicate_2;
|
|
3897
|
+
|
|
3898
|
+
declare interface PredicateLeaf {
|
|
3899
|
+
readonly column: string;
|
|
3900
|
+
readonly op: LeafOperator;
|
|
3901
|
+
readonly value?: unknown;
|
|
1872
3902
|
/**
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1875
|
-
*
|
|
1876
|
-
*
|
|
1877
|
-
*
|
|
3903
|
+
* JSON-path segments INTO a `json()` column (object keys / array indices).
|
|
3904
|
+
* When set, the leaf filters on the extracted value, not the whole column —
|
|
3905
|
+
* built via {@link jsonField}. The sql compiler lowers it to the dialect's
|
|
3906
|
+
* json-extract expression; the reactive matcher treats it as non-indexable
|
|
3907
|
+
* (rides the unindexed bucket → re-checked on any change to the table, like
|
|
3908
|
+
* `contains`). Absent for a normal column leaf.
|
|
1878
3909
|
*/
|
|
1879
|
-
readonly
|
|
1880
|
-
/** Per-kind tuning (e.g. `kindOptions.hnsw.{m, efConstruction, distance}`). */
|
|
1881
|
-
readonly kindOptions?: IndexKindOptions;
|
|
3910
|
+
readonly path?: ReadonlyArray<string | number>;
|
|
1882
3911
|
}
|
|
1883
3912
|
|
|
1884
|
-
|
|
1885
|
-
|
|
3913
|
+
declare interface PredicateLeaf_2 {
|
|
3914
|
+
readonly column: string;
|
|
3915
|
+
readonly op: LeafOperator_2;
|
|
3916
|
+
readonly value?: unknown;
|
|
3917
|
+
/**
|
|
3918
|
+
* JSON-path segments INTO a `json()` column (object keys / array indices).
|
|
3919
|
+
* When set, the leaf filters on the extracted value, not the whole column —
|
|
3920
|
+
* built via {@link jsonField}. The sql compiler lowers it to the dialect's
|
|
3921
|
+
* json-extract expression; the reactive matcher treats it as non-indexable
|
|
3922
|
+
* (rides the unindexed bucket → re-checked on any change to the table, like
|
|
3923
|
+
* `contains`). Absent for a normal column leaf.
|
|
3924
|
+
*/
|
|
3925
|
+
readonly path?: ReadonlyArray<string | number>;
|
|
3926
|
+
}
|
|
1886
3927
|
|
|
1887
3928
|
/**
|
|
1888
|
-
*
|
|
3929
|
+
* Imperative `applyNamespacedSchema` against a fresh SqlClient — the
|
|
3930
|
+
* entry point the CLI / runtime use to provision a tenant's namespace
|
|
3931
|
+
* (eager at migrate time, or lazily on first use).
|
|
1889
3932
|
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
1893
|
-
*
|
|
3933
|
+
* **This is where `lifecycle: 'onTenantCreate'` seeds fire**, after the DDL
|
|
3934
|
+
* lands and before this resolves — so a caller that awaits provisioning gets
|
|
3935
|
+
* a tenant whose tables AND reference data exist, or an error. It was the
|
|
3936
|
+
* missing half: the lifecycle validated, registered, and never ran, so every
|
|
3937
|
+
* tenant came up empty and nothing said so.
|
|
1894
3938
|
*
|
|
1895
|
-
*
|
|
1896
|
-
*
|
|
1897
|
-
*
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
3939
|
+
* The seeds run through `setTenantCreateSeedHook` — the hook itself IS
|
|
3940
|
+
* installed at boot (`seedRunner.ts`); with no seeds declared this costs one
|
|
3941
|
+
* null check. A seed failure REJECTS — see `fireTenantCreateSeeds` for why,
|
|
3942
|
+
* and note that re-calling this function is safe (the DDL is `IF NOT EXISTS`
|
|
3943
|
+
* and seed steps are idempotent by contract).
|
|
1900
3944
|
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
3945
|
+
* Wired by BOTH boot paths through `makeTenantProvisioner` (@voltro/cli):
|
|
3946
|
+
* under `tenantIsolation: 'namespace'` on a SQL dialect, every namespace store
|
|
3947
|
+
* view provisions lazily on first use — memoised per namespace per process.
|
|
3948
|
+
* Eager provisioning is the APP's move (call this from a seed/startup over its
|
|
3949
|
+
* own tenant table); the framework has no tenant registry to enumerate.
|
|
1903
3950
|
*/
|
|
1904
|
-
export declare const
|
|
3951
|
+
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>;
|
|
1905
3952
|
|
|
1906
3953
|
/**
|
|
1907
|
-
*
|
|
1908
|
-
*
|
|
1909
|
-
*
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1912
|
-
*
|
|
3954
|
+
* The runtime-facing materialized query — a plain bag of strings the store and
|
|
3955
|
+
* the matcher consume.
|
|
3956
|
+
*
|
|
3957
|
+
* `R` is a PHANTOM row type. It exists so the row shape the typed builder
|
|
3958
|
+
* already knows survives the trip through `.descriptor` into `store.query()`,
|
|
3959
|
+
* which is where it used to be thrown away: `store.query(database.notes...)`
|
|
3960
|
+
* returned `Row` (`Record<string, unknown>`), so every field read needed a
|
|
3961
|
+
* hand-written cast. One downstream app wrote 2,032 of them. The type was
|
|
3962
|
+
* always available — it was just dropped at this boundary.
|
|
3963
|
+
*
|
|
3964
|
+
* It is an OPTIONAL field, never set at runtime. That matters for two reasons:
|
|
3965
|
+
* a plain object literal still satisfies `QueryDescriptor` (dispatcher, matcher
|
|
3966
|
+
* and store code build and forward these structurally), and `R` stays
|
|
3967
|
+
* covariant, so a `QueryDescriptor<Note>` is still assignable to the bare
|
|
3968
|
+
* `QueryDescriptor` that generic infrastructure passes around. Defaulting to
|
|
3969
|
+
* `Row` keeps every existing annotation meaning exactly what it meant before.
|
|
1913
3970
|
*/
|
|
1914
|
-
declare interface
|
|
1915
|
-
readonly
|
|
3971
|
+
declare interface QueryDescriptor<R = Row> {
|
|
3972
|
+
readonly table: string;
|
|
3973
|
+
readonly predicate: Predicate | undefined;
|
|
3974
|
+
/** Phantom — carries the row type through to `store.query()`. Never set at
|
|
3975
|
+
* runtime; erased entirely by the compiler. */
|
|
3976
|
+
readonly __row?: R;
|
|
3977
|
+
/**
|
|
3978
|
+
* Opt OUT of the runtime's automatic `deletedAt IS NULL` scope on
|
|
3979
|
+
* `softDelete()` tables. Set by `.withDeleted()`. The query builder is
|
|
3980
|
+
* browser-safe and mixin-unaware, so this is just a flag the RUNTIME
|
|
3981
|
+
* (storeMiddleware read path + reactive-query finalize) honors.
|
|
3982
|
+
*/
|
|
3983
|
+
readonly includeDeleted?: boolean;
|
|
3984
|
+
/** Opt OUT of the `expires()` read filter — a deliberate read of expired
|
|
3985
|
+
* rows (an admin view, a grace-period check). Same shape and same posture as
|
|
3986
|
+
* `includeDeleted`. */
|
|
3987
|
+
readonly includeExpired?: boolean;
|
|
3988
|
+
/**
|
|
3989
|
+
* Lock the matched rows for the rest of the enclosing transaction — set by
|
|
3990
|
+
* `.forUpdate()`. `SELECT … FOR UPDATE` on postgres / mysql / mariadb,
|
|
3991
|
+
* `WITH (UPDLOCK, ROWLOCK)` on mssql; sqlite serialises writers already and
|
|
3992
|
+
* emits nothing. Outside a transaction the lock is released as the statement
|
|
3993
|
+
* ends, which is harmless and useless — the point is a read-modify-write
|
|
3994
|
+
* whose read and write share one transaction.
|
|
3995
|
+
*/
|
|
3996
|
+
readonly lock?: 'update';
|
|
3997
|
+
/**
|
|
3998
|
+
* Opt OUT of the runtime's automatic tenant scope on `tenant()` tables.
|
|
3999
|
+
* Set by `.unscoped()`. Honored by the runtime, same as `includeDeleted`.
|
|
4000
|
+
*/
|
|
4001
|
+
readonly crossTenant?: boolean;
|
|
4002
|
+
readonly order: ReadonlyArray<OrderClause>;
|
|
4003
|
+
readonly take: number | undefined;
|
|
4004
|
+
readonly skip: number | undefined;
|
|
4005
|
+
readonly projection: ReadonlyArray<string> | undefined;
|
|
4006
|
+
/**
|
|
4007
|
+
* Aggregate projection. When set, `SELECT` emits the aggregates
|
|
4008
|
+
* instead of row columns; the result is always exactly ONE row
|
|
4009
|
+
* (per group when combined with `groupBy`, ONE row total when not).
|
|
4010
|
+
*
|
|
4011
|
+
* Cannot coexist with `projection` (the two are mutually exclusive
|
|
4012
|
+
* select clauses). The builder enforces this at chain time.
|
|
4013
|
+
*/
|
|
4014
|
+
readonly aggregations?: ReadonlyArray<AggregateColumn<unknown>>;
|
|
4015
|
+
/**
|
|
4016
|
+
* `GROUP BY` columns. When set, the query returns ONE row per
|
|
4017
|
+
* distinct combination of these columns + the aggregations.
|
|
4018
|
+
* Requires `aggregations` to be set; ungrouped aggregates always
|
|
4019
|
+
* produce exactly one row.
|
|
4020
|
+
*/
|
|
4021
|
+
readonly groupBy?: ReadonlyArray<string>;
|
|
4022
|
+
/**
|
|
4023
|
+
* `HAVING` clause — predicate evaluated AFTER GROUP BY. Distinct
|
|
4024
|
+
* from `predicate` (which becomes `WHERE` and runs BEFORE grouping).
|
|
4025
|
+
* Use HAVING to filter groups based on their aggregate values
|
|
4026
|
+
* (e.g. "users with more than 10 posts").
|
|
4027
|
+
*
|
|
4028
|
+
* Stored as a `Predicate` so the same predicate compiler that
|
|
4029
|
+
* handles WHERE handles HAVING — but the column names on the LHS
|
|
4030
|
+
* of HAVING refer to aggregate aliases, not raw rows.
|
|
4031
|
+
*/
|
|
4032
|
+
readonly having?: Predicate;
|
|
4033
|
+
/**
|
|
4034
|
+
* Full-text-search predicate (S7). Set by `.matching('indexName',
|
|
4035
|
+
* 'query')`. `.matching()` resolves the index back to its declared
|
|
4036
|
+
* columns + config + weights from the table registry at call time and
|
|
4037
|
+
* stores them here, so the SQL compiler can emit the right per-dialect
|
|
4038
|
+
* predicate (and an optional relevance-ranking expression) without
|
|
4039
|
+
* re-consulting the registry.
|
|
4040
|
+
*
|
|
4041
|
+
* Per-dialect emission:
|
|
4042
|
+
* - postgres → `<tsvCol> @@ plainto_tsquery('<cfg>', ?)`
|
|
4043
|
+
* - mysql / mariadb → `MATCH(col1, col2) AGAINST (? IN NATURAL LANGUAGE MODE)`
|
|
4044
|
+
* - sqlite → `<table>_<idx>_fts MATCH ?` against the FTS5 table
|
|
4045
|
+
* - mssql → ranked LOWER(...) LIKE fallback (no server FTS catalog)
|
|
4046
|
+
*
|
|
4047
|
+
* `rank` carries an optional relevance-ranking request. When set, the
|
|
4048
|
+
* compiler also surfaces a synthetic relevance column aliased `rank.alias`
|
|
4049
|
+
* and (when `rank.order` is set) orders by it DESC. The score expression
|
|
4050
|
+
* is per-dialect: postgres `ts_rank`, mysql/mariadb `MATCH ... AGAINST`
|
|
4051
|
+
* score, sqlite FTS5 `bm25`, mssql a LIKE-hit count.
|
|
4052
|
+
*/
|
|
4053
|
+
/**
|
|
4054
|
+
* Full-text indexes declared on the FROM table — captured by
|
|
4055
|
+
* `queryFor()` so `.matching()` can resolve an index's columns/config
|
|
4056
|
+
* without a global table-registry lookup. Pure builder metadata; the
|
|
4057
|
+
* SQL compiler never reads it (it reads the resolved `fullTextSearch`).
|
|
4058
|
+
*/
|
|
4059
|
+
readonly fullTextIndexes?: ReadonlyArray<{
|
|
4060
|
+
readonly name: string;
|
|
4061
|
+
readonly columns: ReadonlyArray<string>;
|
|
4062
|
+
readonly config?: string;
|
|
4063
|
+
readonly weights?: Readonly<Record<string, 'A' | 'B' | 'C' | 'D'>>;
|
|
4064
|
+
}>;
|
|
4065
|
+
/**
|
|
4066
|
+
* The SOURCE table object this query was built from (`queryFor(t)` / a
|
|
4067
|
+
* `databaseHandle` entry sets it). The eager-load resolver (`.with(...)`)
|
|
4068
|
+
* needs the source table's shape + relations; it used to re-resolve it
|
|
4069
|
+
* BY NAME via `requireTable(descriptor.table)`, which forces every queried
|
|
4070
|
+
* table to be pre-registered (`databaseHandle(...)` / discovery). A direct
|
|
4071
|
+
* query needs no such registration — it carries the object — so an eager
|
|
4072
|
+
* query that threw "table 'X' is not registered" for a table you can query
|
|
4073
|
+
* directly was an asymmetry. Carrying the object here (same precedent as
|
|
4074
|
+
* `fullTextIndexes`) lets the store use it directly, with `requireTable` as
|
|
4075
|
+
* the fallback for reconstructed descriptors (e.g. `paginateById`). NOT
|
|
4076
|
+
* serialized/fingerprinted — the reactive engine keys on table+predicate.
|
|
4077
|
+
*/
|
|
4078
|
+
readonly sourceTable?: TableLike;
|
|
4079
|
+
readonly fullTextSearch?: {
|
|
4080
|
+
readonly indexName: string;
|
|
4081
|
+
readonly query: string;
|
|
4082
|
+
/** Columns the index covers — resolved from the table registry. */
|
|
4083
|
+
readonly columns: ReadonlyArray<string>;
|
|
4084
|
+
/** Postgres tsvector config (default 'english'). */
|
|
4085
|
+
readonly config?: string;
|
|
4086
|
+
/** Per-column weight letters (postgres ts_rank). */
|
|
4087
|
+
readonly weights?: Readonly<Record<string, 'A' | 'B' | 'C' | 'D'>>;
|
|
4088
|
+
/** Relevance-ranking request — set by `.rankBy(alias?)`. */
|
|
4089
|
+
readonly rank?: {
|
|
4090
|
+
readonly alias: string;
|
|
4091
|
+
readonly order: boolean;
|
|
4092
|
+
};
|
|
4093
|
+
};
|
|
4094
|
+
/**
|
|
4095
|
+
* Common-Table-Expression definitions (Q4) — `WITH <name> AS (...)`.
|
|
4096
|
+
* Each entry binds a `name` to a sub-query descriptor; the outer
|
|
4097
|
+
* SELECT can reference the CTE by name in `from`, `inSubquery`,
|
|
4098
|
+
* `exists`, etc.
|
|
4099
|
+
*
|
|
4100
|
+
* `.withCte()` adds a non-recursive CTE. For tree-walks use
|
|
4101
|
+
* `.recursiveCte()`, which sets `recursive: true` and emits
|
|
4102
|
+
* `WITH RECURSIVE` (the inner descriptor self-references the CTE
|
|
4103
|
+
* name). Both are reactive — see `runtime/dependencyGraph.ts`.
|
|
4104
|
+
*/
|
|
4105
|
+
readonly ctes?: ReadonlyArray<{
|
|
4106
|
+
readonly name: string;
|
|
4107
|
+
readonly descriptor: QueryDescriptor;
|
|
4108
|
+
/**
|
|
4109
|
+
* Recursive CTE marker (Q4 v2). When true, the compiler emits
|
|
4110
|
+
* `WITH RECURSIVE name AS (...)`. The inner descriptor must be a
|
|
4111
|
+
* UNION (via the `union` / `unionAll` helpers) whose first arm is
|
|
4112
|
+
* the anchor and subsequent arms reference the CTE by name (the
|
|
4113
|
+
* recursion). Cycle-detection per-dialect: postgres uses
|
|
4114
|
+
* `UNION` semantics (implicit dedup); other dialects mirror.
|
|
4115
|
+
*/
|
|
4116
|
+
readonly recursive?: boolean;
|
|
4117
|
+
}>;
|
|
1916
4118
|
/**
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
1919
|
-
* (`shadow := <using>(old)`). Authored by the developer in the
|
|
1920
|
-
* migration — e.g. `'role::role_enum'`, `'flag::int::text'`. Omit when
|
|
1921
|
-
* the conversion is implicit on the dialect (e.g. `varchar → text`).
|
|
4119
|
+
* SELECT DISTINCT (Q7). When true, dedup rows by the full result
|
|
4120
|
+
* shape. Cross-dialect.
|
|
1922
4121
|
*/
|
|
1923
|
-
readonly
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
/**
|
|
1927
|
-
* Postgres identifier for the trigger function backing a given channel.
|
|
1928
|
-
*
|
|
1929
|
-
* Derived from the channel, NOT a single global name, and that is the whole
|
|
1930
|
-
* point: `CREATE OR REPLACE FUNCTION framework_notify_change()` is one row in
|
|
1931
|
-
* `pg_proc` for the entire database, so two schemas applied with different
|
|
1932
|
-
* channels overwrote each other and the last one won. Everything applied
|
|
1933
|
-
* earlier then emitted on somebody else's channel — silently, because a NOTIFY
|
|
1934
|
-
* nobody listens to is not an error.
|
|
1935
|
-
*/
|
|
1936
|
-
export declare const notifyFunctionName: (channel: string) => string;
|
|
1937
|
-
|
|
1938
|
-
/** Negation of an arbitrary sub-predicate → `NOT (...)`. Wraps any
|
|
1939
|
-
* leaf/and/or/subquery subtree (distinct from the per-column `neq` and
|
|
1940
|
-
* the narrow `notInSubquery`/`notExists`). */
|
|
1941
|
-
declare interface NotPredicate {
|
|
1942
|
-
readonly not: Predicate;
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
/**
|
|
1946
|
-
* DDL fragment for a numeric primary key column. Each dialect has its
|
|
1947
|
-
* own auto-increment idiom — there's no portable shorthand, so we
|
|
1948
|
-
* dispatch per dialect explicitly. The fragment includes type + PK +
|
|
1949
|
-
* auto-increment in one statement; the caller skips the generic
|
|
1950
|
-
* `NOT NULL` / `PRIMARY KEY` additions for numeric ids.
|
|
1951
|
-
*/
|
|
1952
|
-
export declare const numericIdSql: (name: string, dialect: DialectId) => string;
|
|
1953
|
-
|
|
1954
|
-
/**
|
|
1955
|
-
* The seven classes of schema operations the planner recognises. The
|
|
1956
|
-
* default policy for each is encoded by the planner; the applier
|
|
1957
|
-
* obeys it. Keep this enum in lockstep with the AGENTS.md template's
|
|
1958
|
-
* "Schema migrations" section — agents grep for these strings.
|
|
1959
|
-
*/
|
|
1960
|
-
export declare type OperationClass =
|
|
1961
|
-
/** Reversible, side-effect-free for existing data. Auto-applied. */
|
|
1962
|
-
'safe'
|
|
1963
|
-
/** ADD COLUMN + NOT NULL with `.default(value)` declared.
|
|
1964
|
-
* Applied as `ADD COLUMN ... NOT NULL DEFAULT <value>`. */
|
|
1965
|
-
| 'needs-default'
|
|
1966
|
-
/** ADD COLUMN + NOT NULL on a populated table, ADD UNIQUE / FK / CHECK
|
|
1967
|
-
* with existing offenders. Requires a deliberate declaration
|
|
1968
|
-
* (`.backfill()`, `.unique({dedup})`, `reference({orphanPolicy})`).
|
|
1969
|
-
* Applied as a three-step plan (ADD nullable → run backfill → SET
|
|
1970
|
-
* NOT NULL). */
|
|
1971
|
-
| 'needs-backfill'
|
|
1972
|
-
/** Column appears removed + a new one appeared. Looks like DROP+ADD
|
|
1973
|
-
* but is probably a rename. Refuse-to-plan without an explicit
|
|
1974
|
-
* `.renamedFrom('oldName')` marker — silent renames lose data. */
|
|
1975
|
-
| 'needs-rename-annotation'
|
|
1976
|
-
/** DROP COLUMN, DROP TABLE, narrow type with existing offenders.
|
|
1977
|
-
* Refuse-to-plan unless `dropped()` / `.narrowedFrom()` declared
|
|
1978
|
-
* OR `VOLTRO_DESTRUCTIVE_OK=1` set. */
|
|
1979
|
-
| 'lossy'
|
|
1980
|
-
/** Large-table operation that should run online (CREATE INDEX
|
|
1981
|
-
* CONCURRENTLY, batched backfill, shadow-column swap). The applier
|
|
1982
|
-
* auto-rewrites these once the row count crosses the threshold
|
|
1983
|
-
* (`online-after`, default 50k). */
|
|
1984
|
-
| 'online-required'
|
|
1985
|
-
/** Operations the planner can't infer from a diff (table split /
|
|
1986
|
-
* merge, custom data move, type-change with USING). Requires a
|
|
1987
|
-
* file-based migration in `migrations/<id>.ts`. */
|
|
1988
|
-
| 'multi-step';
|
|
1989
|
-
|
|
1990
|
-
/**
|
|
1991
|
-
* The vocabulary of concrete DDL-level operations the planner emits.
|
|
1992
|
-
* Each `MigrationOperation` carries one of these kinds plus the data
|
|
1993
|
-
* the applier needs to execute it.
|
|
1994
|
-
*
|
|
1995
|
-
* `kind` strings are short + lowercase-kebab so they round-trip
|
|
1996
|
-
* cleanly through JSON (the `operations` column of `_voltro_migrations`)
|
|
1997
|
-
* and can be grepped in `voltro logs` output.
|
|
1998
|
-
*/
|
|
1999
|
-
export declare type OperationKind = 'create-table' | 'drop-table' | 'add-column' | 'drop-column' | 'rename-column' | 'alter-column-nullability' | 'alter-column-type' | 'alter-column-default' | 'add-index' | 'drop-index' | 'add-unique' | 'drop-unique' | 'add-unique-composite' | 'drop-unique-composite' | 'add-foreign-key' | 'drop-foreign-key' | 'add-check' | 'drop-check' | 'reconcile-json-check';
|
|
2000
|
-
|
|
2001
|
-
declare interface OrderClause {
|
|
2002
|
-
readonly column: string;
|
|
2003
|
-
readonly direction: 'asc' | 'desc';
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
declare interface OrPredicate {
|
|
2007
|
-
readonly or: ReadonlyArray<Predicate>;
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
export declare const parseEnumCheck: (clause: string) => {
|
|
2011
|
-
readonly column: string;
|
|
2012
|
-
readonly values: ReadonlyArray<string>;
|
|
2013
|
-
} | null;
|
|
2014
|
-
|
|
2015
|
-
/**
|
|
2016
|
-
* The ids of migration files present on disk but NOT yet recorded against THIS
|
|
2017
|
-
* database — i.e. exactly what `runFileBasedMigrations` would execute next.
|
|
2018
|
-
*
|
|
2019
|
-
* Exists so a caller can decide WHETHER to apply before applying. `voltro dev`
|
|
2020
|
-
* uses it to refuse unattended file migrations against a remote database: that
|
|
2021
|
-
* decision needs the list (to name it in the refusal) and must not have run
|
|
2022
|
-
* anything to get it.
|
|
2023
|
-
*/
|
|
2024
|
-
export declare const pendingFileMigrationIds: (sql: SqlClient.SqlClient, projectRoot: string) => Effect.Effect<ReadonlyArray<string>, unknown>;
|
|
2025
|
-
|
|
2026
|
-
export declare interface PlanInput {
|
|
2027
|
-
readonly declared: ReadonlyArray<TableLike>;
|
|
2028
|
-
readonly live: SchemaSnapshot;
|
|
4122
|
+
readonly distinct?: boolean;
|
|
2029
4123
|
/**
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
4124
|
+
* SELECT DISTINCT ON (cols) — postgres-only (Q7). Picks ONE row
|
|
4125
|
+
* per distinct combination of `distinctOn` columns; the row picked
|
|
4126
|
+
* is the FIRST per the descriptor's `order`. The compiler emits a
|
|
4127
|
+
* windowed-rownumber fallback on other dialects.
|
|
4128
|
+
*
|
|
4129
|
+
* Common use: "latest message per channel".
|
|
2033
4130
|
*/
|
|
2034
|
-
readonly
|
|
4131
|
+
readonly distinctOn?: ReadonlyArray<string>;
|
|
2035
4132
|
/**
|
|
2036
|
-
*
|
|
2037
|
-
*
|
|
2038
|
-
*
|
|
2039
|
-
*
|
|
4133
|
+
* Alias for the FROM table — `FROM <table> AS <alias>` (Q8).
|
|
4134
|
+
* Needed for self-joins where the same table appears multiple
|
|
4135
|
+
* times with different aliases. When unset, the FROM clause is
|
|
4136
|
+
* the bare table name.
|
|
2040
4137
|
*/
|
|
2041
|
-
readonly
|
|
4138
|
+
readonly alias?: string;
|
|
2042
4139
|
/**
|
|
2043
|
-
*
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2046
|
-
*
|
|
2047
|
-
*
|
|
2048
|
-
*
|
|
2049
|
-
*
|
|
4140
|
+
* Joined tables (Q8). Each entry adds an INNER JOIN or LEFT JOIN
|
|
4141
|
+
* with an explicit ON predicate. Self-joins re-use the same
|
|
4142
|
+
* source table with a different alias.
|
|
4143
|
+
*
|
|
4144
|
+
* `on` is a regular `Predicate` — the leaf columns may carry
|
|
4145
|
+
* `alias.column` style qualifiers since the compiler emits the
|
|
4146
|
+
* column string verbatim.
|
|
2050
4147
|
*/
|
|
2051
|
-
readonly
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
export declare const planMigrations: (input: PlanInput) => MigrationPlan;
|
|
2070
|
-
|
|
2071
|
-
/**
|
|
2072
|
-
* One row in a `MigrationPlan` — a concrete operation paired with the
|
|
2073
|
-
* planner's classification + any blocking reason.
|
|
2074
|
-
*
|
|
2075
|
-
* `blocked` is set when the classification refuses-to-plan (lossy
|
|
2076
|
-
* without `dropped()`, rename without `.renamedFrom()`, etc.). The
|
|
2077
|
-
* applier checks `blocked` before executing; the CLI's `voltro db plan`
|
|
2078
|
-
* output highlights it red.
|
|
2079
|
-
*
|
|
2080
|
-
* `backfill` / `dedup` / `orphanPolicy` mirror the user's annotations
|
|
2081
|
-
* straight from `ColumnDefinition` so the applier doesn't need to
|
|
2082
|
-
* re-introspect them.
|
|
2083
|
-
*/
|
|
2084
|
-
export declare interface PlannedOperation {
|
|
2085
|
-
readonly op: MigrationOperation;
|
|
2086
|
-
readonly classification: OperationClass;
|
|
2087
|
-
/** Why the op was classified this way (also surfaces in the CLI). */
|
|
2088
|
-
readonly reason?: string;
|
|
2089
|
-
/** Set on lossy / needs-rename / multi-step ops with no resolution. */
|
|
2090
|
-
readonly blocked?: {
|
|
2091
|
-
readonly fix: string;
|
|
2092
|
-
};
|
|
2093
|
-
/** Pre-check estimate the planner ran (row counts, offender count). */
|
|
2094
|
-
readonly estimate?: {
|
|
2095
|
-
readonly affectedRows?: number;
|
|
2096
|
-
readonly offenderCount?: number;
|
|
2097
|
-
};
|
|
2098
|
-
/** Whether the op is atomic for each supported dialect. Postgres,
|
|
2099
|
-
* mssql, sqlite are atomic for all DDL; mysql/mariadb implicit-commit
|
|
2100
|
-
* every statement. */
|
|
2101
|
-
readonly atomicByDialect: {
|
|
2102
|
-
readonly postgres: boolean;
|
|
2103
|
-
readonly mysql: boolean;
|
|
2104
|
-
readonly mariadb: boolean;
|
|
2105
|
-
readonly mssql: boolean;
|
|
2106
|
-
readonly sqlite: boolean;
|
|
2107
|
-
};
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
/** What happens to the referencing row when the target row goes away. */
|
|
2111
|
-
declare type PluginRefOrphanPolicy =
|
|
2112
|
-
/** Delete the referencing row. For a row that only exists to point at it —
|
|
2113
|
-
* a favourite, a pin, a share. */
|
|
2114
|
-
'delete'
|
|
2115
|
-
/** Null the column, keeping the row. Requires `.nullable()`; declaring it on
|
|
2116
|
-
* a non-nullable column is refused at declaration rather than failing at the
|
|
2117
|
-
* first delete, months later. */
|
|
2118
|
-
| 'null'
|
|
2119
|
-
/** Do nothing. The explicit "I know, and I handle it myself" — different from
|
|
2120
|
-
* omitting the option, which is the same behaviour arrived at by accident. */
|
|
2121
|
-
| 'keep';
|
|
2122
|
-
|
|
2123
|
-
declare type Predicate = PredicateLeaf | AndPredicate | OrPredicate | NotPredicate | SubqueryInPredicate | ExistsPredicate;
|
|
2124
|
-
|
|
2125
|
-
declare interface PredicateLeaf {
|
|
2126
|
-
readonly column: string;
|
|
2127
|
-
readonly op: LeafOperator;
|
|
2128
|
-
readonly value?: unknown;
|
|
4148
|
+
readonly joins?: ReadonlyArray<{
|
|
4149
|
+
readonly kind: 'inner' | 'left';
|
|
4150
|
+
readonly table: string;
|
|
4151
|
+
readonly alias: string;
|
|
4152
|
+
readonly on: Predicate;
|
|
4153
|
+
}>;
|
|
4154
|
+
/**
|
|
4155
|
+
* Aliased projection for joined queries (Q8 v2). When set, the
|
|
4156
|
+
* SELECT emits `alias.col AS outputKey` for each entry, producing
|
|
4157
|
+
* a flat row with the user's chosen keys.
|
|
4158
|
+
*
|
|
4159
|
+
* Mutually exclusive with `projection` (regular select-cols).
|
|
4160
|
+
* Bypassed by `aggregations`.
|
|
4161
|
+
*/
|
|
4162
|
+
readonly joinedProjection?: ReadonlyArray<{
|
|
4163
|
+
readonly outputKey: string;
|
|
4164
|
+
readonly source: string;
|
|
4165
|
+
}>;
|
|
2129
4166
|
/**
|
|
2130
|
-
*
|
|
2131
|
-
*
|
|
2132
|
-
*
|
|
2133
|
-
*
|
|
2134
|
-
*
|
|
2135
|
-
* `
|
|
4167
|
+
* Set-operation composition (Q6). When set, the SELECT shape is
|
|
4168
|
+
* replaced by `(SELECT ...) <OP> (SELECT ...) ...`.
|
|
4169
|
+
*
|
|
4170
|
+
* - `'union'` → dedup-merge (`UNION`)
|
|
4171
|
+
* - `'union-all'` → no dedup (`UNION ALL`)
|
|
4172
|
+
* - `'intersect'` → rows in BOTH (`INTERSECT`)
|
|
4173
|
+
* - `'except'` → rows in LHS not in RHS (`EXCEPT`)
|
|
4174
|
+
*
|
|
4175
|
+
* All four are standard SQL + supported on every dialect we ship.
|
|
2136
4176
|
*/
|
|
2137
|
-
readonly
|
|
4177
|
+
readonly setOp?: {
|
|
4178
|
+
readonly kind: 'union' | 'union-all' | 'intersect' | 'except';
|
|
4179
|
+
readonly queries: ReadonlyArray<QueryDescriptor>;
|
|
4180
|
+
};
|
|
4181
|
+
/**
|
|
4182
|
+
* Optional index hint. Set by `.using('indexName')` on the query
|
|
4183
|
+
* builder. The framework stores it on the descriptor for two reasons:
|
|
4184
|
+
*
|
|
4185
|
+
* 1. Suppresses the dev-mode "non-indexed query" warning for this
|
|
4186
|
+
* descriptor (the user explicitly signalled they thought about
|
|
4187
|
+
* the access pattern).
|
|
4188
|
+
* 2. The matcher's tuple-routing path keys subscriptions on the
|
|
4189
|
+
* declared index's column set — see `IndexedMatcher` in
|
|
4190
|
+
* `@voltro/runtime/matcher.ts`. Subscriptions that declare a
|
|
4191
|
+
* covering index get O(1) candidate lookup per event instead
|
|
4192
|
+
* of column-union routing.
|
|
4193
|
+
*/
|
|
4194
|
+
readonly usingIndex?: string;
|
|
4195
|
+
/**
|
|
4196
|
+
* Eager-load tree declared via `.with({ ... })`. Consumed by the
|
|
4197
|
+
* JoinCompiler (`./joinCompiler.ts`) when present — the SQL stores
|
|
4198
|
+
* dispatch nested-JSON-aggregation queries; the in-memory store
|
|
4199
|
+
* walks it via post-hoc lookups.
|
|
4200
|
+
*/
|
|
4201
|
+
readonly eager?: WithSpec;
|
|
4202
|
+
/**
|
|
4203
|
+
* Approximate-nearest-neighbour clause (plan 01). Set by
|
|
4204
|
+
* `.nearestNeighbours(...)`. Drives a synthetic `distance` projection
|
|
4205
|
+
* + `ORDER BY <col> <op> <query>` in the SQL compiler:
|
|
4206
|
+
*
|
|
4207
|
+
* - postgres (pgvector): `<col> <=> $q` (cosine) / `<->` (l2) /
|
|
4208
|
+
* `<#>` (inner).
|
|
4209
|
+
* - mariadb / mysql: `VEC_DISTANCE_COSINE/EUCLIDEAN(<col>, $q)`.
|
|
4210
|
+
* - mssql / sqlite: no portable ANN operator → the compiler warns +
|
|
4211
|
+
* skips the distance ordering (sequential scan; rows still return).
|
|
4212
|
+
*
|
|
4213
|
+
* `queryVector` is the literal float array; `queryText` is the
|
|
4214
|
+
* raw string the runtime embeds to a vector *before* compiling
|
|
4215
|
+
* (only valid when the table carries `vectorEmbedding()`). Exactly
|
|
4216
|
+
* one of the two is set.
|
|
4217
|
+
*/
|
|
4218
|
+
readonly annClause?: AnnClause;
|
|
4219
|
+
/**
|
|
4220
|
+
* PostGIS spatial distance clause (`@voltro/plugin-postgis`). Set by the
|
|
4221
|
+
* `withDistance(...)` / `nearestBy(...)` transforms applied via `.use(...)`.
|
|
4222
|
+
* Postgres-only — the SQL compiler lowers it to a `ST_Distance(...)`
|
|
4223
|
+
* projected column and/or a `<->` KNN `ORDER BY`; on any non-postgres
|
|
4224
|
+
* dialect the compiler THROWS (mirrors the spatial predicate + column
|
|
4225
|
+
* guards). Never evaluated in-memory (no PostGIS in JS). Composes with
|
|
4226
|
+
* `.where(...)`, `.limit(k)`, spatial predicates, etc.
|
|
4227
|
+
*/
|
|
4228
|
+
readonly spatialClause?: SpatialClause;
|
|
2138
4229
|
}
|
|
2139
4230
|
|
|
2140
|
-
/**
|
|
2141
|
-
* Imperative `applyNamespacedSchema` against a fresh SqlClient — the
|
|
2142
|
-
* entry point the CLI / runtime use to provision a tenant's namespace
|
|
2143
|
-
* (eager at migrate time, or lazily on first use).
|
|
2144
|
-
*
|
|
2145
|
-
* **This is where `lifecycle: 'onTenantCreate'` seeds fire**, after the DDL
|
|
2146
|
-
* lands and before this resolves — so a caller that awaits provisioning gets
|
|
2147
|
-
* a tenant whose tables AND reference data exist, or an error. It was the
|
|
2148
|
-
* missing half: the lifecycle validated, registered, and never ran, so every
|
|
2149
|
-
* tenant came up empty and nothing said so.
|
|
2150
|
-
*
|
|
2151
|
-
* The seeds run through `setTenantCreateSeedHook` — the hook itself IS
|
|
2152
|
-
* installed at boot (`seedRunner.ts`); with no seeds declared this costs one
|
|
2153
|
-
* null check. A seed failure REJECTS — see `fireTenantCreateSeeds` for why,
|
|
2154
|
-
* and note that re-calling this function is safe (the DDL is `IF NOT EXISTS`
|
|
2155
|
-
* and seed steps are idempotent by contract).
|
|
2156
|
-
*
|
|
2157
|
-
* Wired by BOTH boot paths through `makeTenantProvisioner` (@voltro/cli):
|
|
2158
|
-
* under `tenantIsolation: 'namespace'` on a SQL dialect, every namespace store
|
|
2159
|
-
* view provisions lazily on first use — memoised per namespace per process.
|
|
2160
|
-
* Eager provisioning is the APP's move (call this from a seed/startup over its
|
|
2161
|
-
* own tenant table); the framework has no tenant registry to enumerate.
|
|
2162
|
-
*/
|
|
2163
|
-
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>;
|
|
2164
|
-
|
|
2165
4231
|
/**
|
|
2166
4232
|
* The runtime-facing materialized query — a plain bag of strings the store and
|
|
2167
4233
|
* the matcher consume.
|
|
@@ -2180,9 +4246,9 @@ export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>,
|
|
|
2180
4246
|
* `QueryDescriptor` that generic infrastructure passes around. Defaulting to
|
|
2181
4247
|
* `Row` keeps every existing annotation meaning exactly what it meant before.
|
|
2182
4248
|
*/
|
|
2183
|
-
declare interface
|
|
4249
|
+
declare interface QueryDescriptor_2<R = Row_2> {
|
|
2184
4250
|
readonly table: string;
|
|
2185
|
-
readonly predicate:
|
|
4251
|
+
readonly predicate: Predicate_2 | undefined;
|
|
2186
4252
|
/** Phantom — carries the row type through to `store.query()`. Never set at
|
|
2187
4253
|
* runtime; erased entirely by the compiler. */
|
|
2188
4254
|
readonly __row?: R;
|
|
@@ -2211,7 +4277,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2211
4277
|
* Set by `.unscoped()`. Honored by the runtime, same as `includeDeleted`.
|
|
2212
4278
|
*/
|
|
2213
4279
|
readonly crossTenant?: boolean;
|
|
2214
|
-
readonly order: ReadonlyArray<
|
|
4280
|
+
readonly order: ReadonlyArray<OrderClause_2>;
|
|
2215
4281
|
readonly take: number | undefined;
|
|
2216
4282
|
readonly skip: number | undefined;
|
|
2217
4283
|
readonly projection: ReadonlyArray<string> | undefined;
|
|
@@ -2223,7 +4289,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2223
4289
|
* Cannot coexist with `projection` (the two are mutually exclusive
|
|
2224
4290
|
* select clauses). The builder enforces this at chain time.
|
|
2225
4291
|
*/
|
|
2226
|
-
readonly aggregations?: ReadonlyArray<
|
|
4292
|
+
readonly aggregations?: ReadonlyArray<AggregateColumn_2<unknown>>;
|
|
2227
4293
|
/**
|
|
2228
4294
|
* `GROUP BY` columns. When set, the query returns ONE row per
|
|
2229
4295
|
* distinct combination of these columns + the aggregations.
|
|
@@ -2241,7 +4307,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2241
4307
|
* handles WHERE handles HAVING — but the column names on the LHS
|
|
2242
4308
|
* of HAVING refer to aggregate aliases, not raw rows.
|
|
2243
4309
|
*/
|
|
2244
|
-
readonly having?:
|
|
4310
|
+
readonly having?: Predicate_2;
|
|
2245
4311
|
/**
|
|
2246
4312
|
* Full-text-search predicate (S7). Set by `.matching('indexName',
|
|
2247
4313
|
* 'query')`. `.matching()` resolves the index back to its declared
|
|
@@ -2287,7 +4353,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2287
4353
|
* the fallback for reconstructed descriptors (e.g. `paginateById`). NOT
|
|
2288
4354
|
* serialized/fingerprinted — the reactive engine keys on table+predicate.
|
|
2289
4355
|
*/
|
|
2290
|
-
readonly sourceTable?:
|
|
4356
|
+
readonly sourceTable?: TableLike_2;
|
|
2291
4357
|
readonly fullTextSearch?: {
|
|
2292
4358
|
readonly indexName: string;
|
|
2293
4359
|
readonly query: string;
|
|
@@ -2316,7 +4382,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2316
4382
|
*/
|
|
2317
4383
|
readonly ctes?: ReadonlyArray<{
|
|
2318
4384
|
readonly name: string;
|
|
2319
|
-
readonly descriptor:
|
|
4385
|
+
readonly descriptor: QueryDescriptor_2;
|
|
2320
4386
|
/**
|
|
2321
4387
|
* Recursive CTE marker (Q4 v2). When true, the compiler emits
|
|
2322
4388
|
* `WITH RECURSIVE name AS (...)`. The inner descriptor must be a
|
|
@@ -2361,7 +4427,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2361
4427
|
readonly kind: 'inner' | 'left';
|
|
2362
4428
|
readonly table: string;
|
|
2363
4429
|
readonly alias: string;
|
|
2364
|
-
readonly on:
|
|
4430
|
+
readonly on: Predicate_2;
|
|
2365
4431
|
}>;
|
|
2366
4432
|
/**
|
|
2367
4433
|
* Aliased projection for joined queries (Q8 v2). When set, the
|
|
@@ -2388,7 +4454,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2388
4454
|
*/
|
|
2389
4455
|
readonly setOp?: {
|
|
2390
4456
|
readonly kind: 'union' | 'union-all' | 'intersect' | 'except';
|
|
2391
|
-
readonly queries: ReadonlyArray<
|
|
4457
|
+
readonly queries: ReadonlyArray<QueryDescriptor_2>;
|
|
2392
4458
|
};
|
|
2393
4459
|
/**
|
|
2394
4460
|
* Optional index hint. Set by `.using('indexName')` on the query
|
|
@@ -2410,7 +4476,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2410
4476
|
* dispatch nested-JSON-aggregation queries; the in-memory store
|
|
2411
4477
|
* walks it via post-hoc lookups.
|
|
2412
4478
|
*/
|
|
2413
|
-
readonly eager?:
|
|
4479
|
+
readonly eager?: WithSpec_2;
|
|
2414
4480
|
/**
|
|
2415
4481
|
* Approximate-nearest-neighbour clause (plan 01). Set by
|
|
2416
4482
|
* `.nearestNeighbours(...)`. Drives a synthetic `distance` projection
|
|
@@ -2427,7 +4493,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2427
4493
|
* (only valid when the table carries `vectorEmbedding()`). Exactly
|
|
2428
4494
|
* one of the two is set.
|
|
2429
4495
|
*/
|
|
2430
|
-
readonly annClause?:
|
|
4496
|
+
readonly annClause?: AnnClause_2;
|
|
2431
4497
|
/**
|
|
2432
4498
|
* PostGIS spatial distance clause (`@voltro/plugin-postgis`). Set by the
|
|
2433
4499
|
* `withDistance(...)` / `nearestBy(...)` transforms applied via `.use(...)`.
|
|
@@ -2437,7 +4503,7 @@ declare interface QueryDescriptor<R = Row> {
|
|
|
2437
4503
|
* guards). Never evaluated in-memory (no PostGIS in JS). Composes with
|
|
2438
4504
|
* `.where(...)`, `.limit(k)`, spatial predicates, etc.
|
|
2439
4505
|
*/
|
|
2440
|
-
readonly spatialClause?:
|
|
4506
|
+
readonly spatialClause?: SpatialClause_2;
|
|
2441
4507
|
}
|
|
2442
4508
|
|
|
2443
4509
|
declare const quote: (name: string) => string;
|
|
@@ -2497,6 +4563,19 @@ export declare interface ReactiveTriggerDrift {
|
|
|
2497
4563
|
* business, not this one's.
|
|
2498
4564
|
*/
|
|
2499
4565
|
readonly functionOutdated: boolean;
|
|
4566
|
+
/**
|
|
4567
|
+
* What the verdict was measured against — so a report that contradicts a
|
|
4568
|
+
* `db apply` that just installed the triggers can be read: the schema the
|
|
4569
|
+
* detector looked in, the channel it derived the names from, and how many
|
|
4570
|
+
* `framework_changes_*` triggers it found there at all. A consumer measured
|
|
4571
|
+
* "71 have NO trigger" eleven seconds after their migrate job installed 71,
|
|
4572
|
+
* and nothing in the line said which schema or channel was compared.
|
|
4573
|
+
*/
|
|
4574
|
+
readonly compared?: {
|
|
4575
|
+
readonly schema: string;
|
|
4576
|
+
readonly channel: string;
|
|
4577
|
+
readonly present: number;
|
|
4578
|
+
};
|
|
2500
4579
|
}
|
|
2501
4580
|
|
|
2502
4581
|
/**
|
|
@@ -2649,6 +4728,39 @@ export declare const resolveMysqlEngine: (sql: SqlClient.SqlClient) => Effect.Ef
|
|
|
2649
4728
|
|
|
2650
4729
|
declare type RetryDecision = 'retry' | 'noRetry';
|
|
2651
4730
|
|
|
4731
|
+
export declare interface RewriteOptions extends EachOptions {
|
|
4732
|
+
/**
|
|
4733
|
+
* Columns whose CURRENT value must still hold when the patch is written, in
|
|
4734
|
+
* addition to the columns the patch itself changes. A JSON column cannot be
|
|
4735
|
+
* compared by value on every dialect, so a rewrite of one guards on what
|
|
4736
|
+
* you name here — an `updatedAt`, a `version` — rather than on the document.
|
|
4737
|
+
*/
|
|
4738
|
+
readonly guard?: ReadonlyArray<string>;
|
|
4739
|
+
/**
|
|
4740
|
+
* How many passes over the table before contention is a failure. A pass
|
|
4741
|
+
* that rewrites nothing and contends on nothing is STABLE and ends the
|
|
4742
|
+
* rewrite; a row that changed under the migration between its read and its
|
|
4743
|
+
* write is contended and is read again on the next pass. Default 10.
|
|
4744
|
+
*/
|
|
4745
|
+
readonly maxPasses?: number;
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
export declare interface RewriteReport {
|
|
4749
|
+
readonly passes: number;
|
|
4750
|
+
readonly scanned: number;
|
|
4751
|
+
readonly rewritten: number;
|
|
4752
|
+
/** Writes lost to a concurrent change and retried on a later pass. */
|
|
4753
|
+
readonly contended: number;
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
/** A `ctx.rewrite` that was still contended after `maxPasses`. */
|
|
4757
|
+
export declare class RewriteUnstableError extends Error {
|
|
4758
|
+
readonly table: string;
|
|
4759
|
+
readonly report: RewriteReport;
|
|
4760
|
+
readonly _tag = "RewriteUnstableError";
|
|
4761
|
+
constructor(table: string, report: RewriteReport);
|
|
4762
|
+
}
|
|
4763
|
+
|
|
2652
4764
|
/**
|
|
2653
4765
|
* Roll back a single file-based migration by id. Looks up the
|
|
2654
4766
|
* migration's `down` body via file discovery (the file must still be
|
|
@@ -2663,6 +4775,7 @@ export declare const rollbackFileBasedMigration: (sql: SqlClient.SqlClient, ctx:
|
|
|
2663
4775
|
projectRoot: string;
|
|
2664
4776
|
id: string;
|
|
2665
4777
|
lockSchema?: string;
|
|
4778
|
+
store: DataStore;
|
|
2666
4779
|
}) => Effect.Effect<{
|
|
2667
4780
|
id: string;
|
|
2668
4781
|
durationMs: number;
|
|
@@ -2700,6 +4813,11 @@ export declare const rollingDeployUnsafeOps: (operations: ReadonlyArray<PlannedO
|
|
|
2700
4813
|
|
|
2701
4814
|
declare type Row = Readonly<Record<string, unknown>>;
|
|
2702
4815
|
|
|
4816
|
+
declare type Row_2 = Readonly<Record<string, unknown>>;
|
|
4817
|
+
|
|
4818
|
+
/** Addresses one row: its `id`, or the columns of an explicit primary key. */
|
|
4819
|
+
declare type RowKey = string | Readonly<Record<string, unknown>>;
|
|
4820
|
+
|
|
2703
4821
|
/** The context a rule predicate is evaluated with. */
|
|
2704
4822
|
declare interface RuleContext {
|
|
2705
4823
|
/** Transactional store — read other tables in the mutation's snapshot. */
|
|
@@ -2750,6 +4868,19 @@ export declare interface RunFileBasedMigrationsCtx {
|
|
|
2750
4868
|
readonly projectRoot: string;
|
|
2751
4869
|
readonly env: 'dev' | 'staging' | 'prod';
|
|
2752
4870
|
readonly appliedBy: string;
|
|
4871
|
+
/** The application's data store over the same database `sql` is connected
|
|
4872
|
+
* to — what a migration body receives as `ctx.store`. Required, not
|
|
4873
|
+
* optional: a runner that can leave it out is a runner that hands one body
|
|
4874
|
+
* a store and the next one an error, depending on which command ran it. */
|
|
4875
|
+
readonly store: DataStore;
|
|
4876
|
+
/**
|
|
4877
|
+
* The caller holds a transaction the whole run is part of — `voltro db apply`
|
|
4878
|
+
* on a dialect with transactional DDL runs the file migrations and the
|
|
4879
|
+
* schema plan as ONE unit. Each migration then runs on the given store and
|
|
4880
|
+
* connection without a transaction of its own (nesting would be a mistake)
|
|
4881
|
+
* and without the commit probe (the caller's unit is the guarantee).
|
|
4882
|
+
*/
|
|
4883
|
+
readonly enclosed?: boolean;
|
|
2753
4884
|
/** Schema scope for the migration advisory lock (see `migrations/lock.ts`).
|
|
2754
4885
|
* Absent → resolved from configuration (`DB_SCHEMA`). */
|
|
2755
4886
|
readonly lockSchema?: string;
|
|
@@ -2775,6 +4906,17 @@ export declare const runFrameworkBootstrap: (tables: ReadonlyArray<AnyTable>, sq
|
|
|
2775
4906
|
*/
|
|
2776
4907
|
export declare const runMigrate: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
|
|
2777
4908
|
|
|
4909
|
+
/**
|
|
4910
|
+
* Execute ONE planned operation with the applier's own per-dialect
|
|
4911
|
+
* statements, outside a plan — for a file migration's schema helpers
|
|
4912
|
+
* (`ctx.schema.addColumn`, `addIndex`, `createTable`, …), which construct the
|
|
4913
|
+
* operation from the live table and hand it here. `declared` is what the
|
|
4914
|
+
* mysql MODIFY, the mssql prefix rules and the sqlite rebuild read the target
|
|
4915
|
+
* shape from: the live table with the change applied, materialised as a
|
|
4916
|
+
* declaration by the caller.
|
|
4917
|
+
*/
|
|
4918
|
+
export declare const runMigrationOp: (sql: SqlClient.SqlClient, planned: PlannedOperation, declared: ReadonlyArray<TableLike>) => Effect.Effect<AppliedOp, SqlError_2>;
|
|
4919
|
+
|
|
2778
4920
|
/**
|
|
2779
4921
|
* Top-level entry point — called once per boot. Returns a
|
|
2780
4922
|
* `BootMigrationOutcome`; the caller logs + acts on it (refuse-to-boot
|
|
@@ -2795,6 +4937,19 @@ export declare interface RunPlannedMigrationsCtx {
|
|
|
2795
4937
|
readonly softDropRetentionDays?: number;
|
|
2796
4938
|
}
|
|
2797
4939
|
|
|
4940
|
+
/**
|
|
4941
|
+
* Execute ONE schema operation against the database as it is, with the same
|
|
4942
|
+
* per-dialect statements the planner's applier uses — so a migration body
|
|
4943
|
+
* that needs to drop a foreign key, an index or a column, or rename a column,
|
|
4944
|
+
* gets the dialect right without spelling it. `live` is the snapshot the
|
|
4945
|
+
* caller just read; on sqlite it is what the rebuild is computed from.
|
|
4946
|
+
*
|
|
4947
|
+
* Existence is the CALLER's question (`fileContext.ts` reads the snapshot and
|
|
4948
|
+
* answers `already-absent` before reaching here). This function assumes the
|
|
4949
|
+
* object is there.
|
|
4950
|
+
*/
|
|
4951
|
+
export declare const runSchemaOp: (sql: SqlClient.SqlClient, op: MigrationSchemaOp, live: SchemaSnapshot) => Effect.Effect<void, SqlError_2>;
|
|
4952
|
+
|
|
2798
4953
|
/**
|
|
2799
4954
|
* Effect that applies the emitted DDL to the provided SqlClient. Splits
|
|
2800
4955
|
* on `;` only outside dollar-quoted blocks (the postgres NOTIFY trigger
|
|
@@ -2825,6 +4980,9 @@ export declare interface SchemaApplyOptions {
|
|
|
2825
4980
|
readonly lockSchema?: string;
|
|
2826
4981
|
}
|
|
2827
4982
|
|
|
4983
|
+
/** What a schema operation on the migration context found to do. */
|
|
4984
|
+
export declare type SchemaOpOutcome = 'applied' | 'already-absent' | 'already-present';
|
|
4985
|
+
|
|
2828
4986
|
/**
|
|
2829
4987
|
* The full schema as the planner sees it on one side of the diff.
|
|
2830
4988
|
* Both the declared side and the introspected side conform to this
|
|
@@ -2847,7 +5005,9 @@ export declare const shortFingerprint: (fp: string) => string;
|
|
|
2847
5005
|
* trip to/from `information_schema`. `idScheme.kind` IS carried — the
|
|
2848
5006
|
* applier needs it to emit auto-increment DDL for numeric ids.
|
|
2849
5007
|
*/
|
|
2850
|
-
export declare const snapshotColumn: (name: string, def: ColumnDefinition<unknown
|
|
5008
|
+
export declare const snapshotColumn: (name: string, def: ColumnDefinition<unknown>,
|
|
5009
|
+
/** Needed only for `storageType` — a caller that renders DDL does not want it. */
|
|
5010
|
+
dialect?: DialectId) => ColumnSnapshot;
|
|
2851
5011
|
|
|
2852
5012
|
/**
|
|
2853
5013
|
* PostGIS spatial-distance clause carried on a {@link QueryDescriptor}.
|
|
@@ -2884,6 +5044,41 @@ declare interface SpatialClause {
|
|
|
2884
5044
|
readonly order?: 'asc' | 'desc';
|
|
2885
5045
|
}
|
|
2886
5046
|
|
|
5047
|
+
/**
|
|
5048
|
+
* PostGIS spatial-distance clause carried on a {@link QueryDescriptor}.
|
|
5049
|
+
* Drives a `ST_Distance` projected column (distance-as-a-value) and/or a
|
|
5050
|
+
* `<->` nearest-neighbour `ORDER BY` (KNN). Built by `@voltro/plugin-postgis`;
|
|
5051
|
+
* postgres-only (the compiler throws on other dialects).
|
|
5052
|
+
*/
|
|
5053
|
+
declare interface SpatialClause_2 {
|
|
5054
|
+
/** The spatial column to measure from. */
|
|
5055
|
+
readonly column: string;
|
|
5056
|
+
/** The comparison geometry as WKT/EWKT (e.g. `SRID=4326;POINT(8 50)`). Bound as a parameter. */
|
|
5057
|
+
readonly geom: string;
|
|
5058
|
+
/**
|
|
5059
|
+
* Measure metric metres via a `::geography` cast on both operands
|
|
5060
|
+
* (great-circle distance). When false, distance is in the SRID's own
|
|
5061
|
+
* planar units (`::geometry`). Default true — the "distance in metres"
|
|
5062
|
+
* users expect for lat/lon data.
|
|
5063
|
+
*/
|
|
5064
|
+
readonly useGeography: boolean;
|
|
5065
|
+
/**
|
|
5066
|
+
* When set, surface the distance as a projected column aliased this name
|
|
5067
|
+
* (`ST_Distance(col, geom) AS "<projectAs>"`). The row type gains a
|
|
5068
|
+
* `number` field. Absent → no distance projection (KNN-order-only).
|
|
5069
|
+
*/
|
|
5070
|
+
readonly projectAs?: string;
|
|
5071
|
+
/**
|
|
5072
|
+
* When set, add a KNN `ORDER BY col <-> geom <dir>` clause (indexed
|
|
5073
|
+
* nearest-neighbour when a GiST index covers the column). Leads any
|
|
5074
|
+
* explicit `.orderBy(...)`. Absent → no spatial ordering. `<->` is always
|
|
5075
|
+
* a planar operator (postgres has no geography `<->`), so KNN ordering
|
|
5076
|
+
* ranks by planar distance even when `useGeography` is true for the
|
|
5077
|
+
* projected metric distance.
|
|
5078
|
+
*/
|
|
5079
|
+
readonly order?: 'asc' | 'desc';
|
|
5080
|
+
}
|
|
5081
|
+
|
|
2887
5082
|
/**
|
|
2888
5083
|
* The `sql` template tag — captures a tagged-template literal into a
|
|
2889
5084
|
* {@link RawSqlFragment} descriptor without binding it to any client.
|
|
@@ -2972,6 +5167,25 @@ declare interface SubqueryInPredicate {
|
|
|
2972
5167
|
readonly subquery: QueryDescriptor;
|
|
2973
5168
|
}
|
|
2974
5169
|
|
|
5170
|
+
/**
|
|
5171
|
+
* `col IN (SELECT col FROM ...)` / `col NOT IN (SELECT ...)` — the
|
|
5172
|
+
* RHS is a sub-query descriptor. The sub-query MUST project a
|
|
5173
|
+
* single column (via `.select('col')`); the SQL compiler emits the
|
|
5174
|
+
* full inner SELECT inline, and the in-memory store evaluates the
|
|
5175
|
+
* sub-query first + then runs the IN check against the materialized
|
|
5176
|
+
* set.
|
|
5177
|
+
*
|
|
5178
|
+
* The sub-query is non-correlated in v1 — it can't reference outer
|
|
5179
|
+
* row columns. Correlated sub-queries (`WHERE EXISTS (... WHERE
|
|
5180
|
+
* inner.userId = outer.id)`) would require a table-alias mechanism;
|
|
5181
|
+
* tracked separately.
|
|
5182
|
+
*/
|
|
5183
|
+
declare interface SubqueryInPredicate_2 {
|
|
5184
|
+
readonly column: string;
|
|
5185
|
+
readonly op: 'subquery-in' | 'subquery-not-in';
|
|
5186
|
+
readonly subquery: QueryDescriptor_2;
|
|
5187
|
+
}
|
|
5188
|
+
|
|
2975
5189
|
declare interface Table<Name extends string, Fields extends Record<string, ColumnDefinition<unknown>>, Reactive extends boolean = true, IxNames extends string = never> extends TableLike {
|
|
2976
5190
|
readonly tableName: Name;
|
|
2977
5191
|
readonly fields: Fields;
|
|
@@ -3507,6 +5721,11 @@ declare interface TableLike {
|
|
|
3507
5721
|
readonly fields: Record<string, ColumnDefinition<unknown>>;
|
|
3508
5722
|
}
|
|
3509
5723
|
|
|
5724
|
+
declare interface TableLike_2 {
|
|
5725
|
+
readonly tableName: string;
|
|
5726
|
+
readonly fields: Record<string, ColumnDefinition_2<unknown>>;
|
|
5727
|
+
}
|
|
5728
|
+
|
|
3510
5729
|
/**
|
|
3511
5730
|
* A named cross-table business rule, attached by `.rule(name, predicate)`.
|
|
3512
5731
|
*
|
|
@@ -3560,8 +5779,26 @@ declare interface TableUnique {
|
|
|
3560
5779
|
};
|
|
3561
5780
|
}
|
|
3562
5781
|
|
|
5782
|
+
/**
|
|
5783
|
+
* A `transactional` migration whose body committed the transaction from
|
|
5784
|
+
* inside — DDL on the MySQL family, or an explicit COMMIT. The body's work
|
|
5785
|
+
* IS in the database and the migration IS recorded (running it again would
|
|
5786
|
+
* repeat the committed half); what failed is the guarantee the migration
|
|
5787
|
+
* asked for, and the run stops so nobody deploys on top of an assumption
|
|
5788
|
+
* that did not hold.
|
|
5789
|
+
*/
|
|
5790
|
+
export declare class TransactionBrokenByBodyError extends Error {
|
|
5791
|
+
readonly migrationId: string;
|
|
5792
|
+
readonly dialect: DialectId;
|
|
5793
|
+
readonly _tag = "TransactionBrokenByBodyError";
|
|
5794
|
+
constructor(migrationId: string, dialect: DialectId);
|
|
5795
|
+
}
|
|
5796
|
+
|
|
3563
5797
|
export declare const unblockLossy: (plan: MigrationPlan, scope?: ReturnType<typeof destructiveScope>) => MigrationPlan;
|
|
3564
5798
|
|
|
5799
|
+
/** Every deploy that did not finish — `running` (the process died) or `failed` (its undo did not complete). An absent table is an empty ledger. */
|
|
5800
|
+
export declare const unfinishedDeploys: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<DeployLedgerRow>, never>;
|
|
5801
|
+
|
|
3565
5802
|
/**
|
|
3566
5803
|
* Unique-constraint metadata. Set by `.unique()` (or `.unique({ dedup })`).
|
|
3567
5804
|
*
|
|
@@ -3579,6 +5816,43 @@ declare interface UniqueSpec {
|
|
|
3579
5816
|
readonly dedup?: 'fail' | 'suffix-counter' | Statement.Fragment;
|
|
3580
5817
|
}
|
|
3581
5818
|
|
|
5819
|
+
/**
|
|
5820
|
+
* Unique-constraint metadata. Set by `.unique()` (or `.unique({ dedup })`).
|
|
5821
|
+
*
|
|
5822
|
+
* The planner reads `dedup` when ADD UNIQUE is applied to a populated
|
|
5823
|
+
* column — without it, the constraint fails for any existing duplicates.
|
|
5824
|
+
*
|
|
5825
|
+
* - `'fail'` — refuse to migrate; surface offending rows in the plan
|
|
5826
|
+
* output (the safe default).
|
|
5827
|
+
* - `'suffix-counter'` — auto-rename duplicates `value`, `value-2`,
|
|
5828
|
+
* `value-3`, … (only sensible for text columns).
|
|
5829
|
+
* - `sql\`...\`` — custom UPDATE expression keyed off `id` /
|
|
5830
|
+
* ROW_NUMBER() to disambiguate.
|
|
5831
|
+
*/
|
|
5832
|
+
declare interface UniqueSpec_2 {
|
|
5833
|
+
readonly dedup?: 'fail' | 'suffix-counter' | Statement.Fragment;
|
|
5834
|
+
}
|
|
5835
|
+
|
|
5836
|
+
declare interface UpsertOptions {
|
|
5837
|
+
conflictColumns: ReadonlyArray<string>;
|
|
5838
|
+
update?: ReadonlyArray<string> | ((existing: Row_2) => Readonly<Record<string, unknown>>);
|
|
5839
|
+
/**
|
|
5840
|
+
* Additional assignments on CONFLICT only; never part of the INSERT row.
|
|
5841
|
+
* Defined values override the `update` projection (including callback results).
|
|
5842
|
+
* `undefined` is omitted; explicit `null`, false, zero and empty strings are
|
|
5843
|
+
* preserved. Cannot rewrite `id` or a conflict column. Column-list/default
|
|
5844
|
+
* upserts keep their dialect's native statement instead of becoming callbacks.
|
|
5845
|
+
*/
|
|
5846
|
+
readonly updateValues?: Readonly<Record<string, unknown>>;
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
/** What an `upsertWithOutcome` did, beside the row it wrote. */
|
|
5850
|
+
declare interface UpsertResult {
|
|
5851
|
+
readonly row: Row_2;
|
|
5852
|
+
/** `'updated'` means the statement landed on a row that already existed. */
|
|
5853
|
+
readonly outcome: 'inserted' | 'updated';
|
|
5854
|
+
}
|
|
5855
|
+
|
|
3582
5856
|
/**
|
|
3583
5857
|
* Vector distance metric. Drives the HNSW opclass at index-build time and
|
|
3584
5858
|
* the distance operator at query time — the two MUST agree or the index
|
|
@@ -3589,6 +5863,16 @@ declare interface UniqueSpec {
|
|
|
3589
5863
|
*/
|
|
3590
5864
|
declare type VectorDistance = 'cosine' | 'l2' | 'inner';
|
|
3591
5865
|
|
|
5866
|
+
/**
|
|
5867
|
+
* Vector distance metric. Drives the HNSW opclass at index-build time and
|
|
5868
|
+
* the distance operator at query time — the two MUST agree or the index
|
|
5869
|
+
* can't accelerate the search.
|
|
5870
|
+
* - `'cosine'` — 1 − cosine similarity (pgvector `<=>`)
|
|
5871
|
+
* - `'l2'` — Euclidean distance (pgvector `<->`)
|
|
5872
|
+
* - `'inner'` — negative inner product (pgvector `<#>`)
|
|
5873
|
+
*/
|
|
5874
|
+
declare type VectorDistance_2 = 'cosine' | 'l2' | 'inner';
|
|
5875
|
+
|
|
3592
5876
|
/**
|
|
3593
5877
|
* A read-only SQL view descriptor. Structurally a `TableLike` (carries
|
|
3594
5878
|
* `tableName` + `fields`) so the query builder + row decoder + by-name
|
|
@@ -3621,6 +5905,9 @@ declare interface View<Name extends string, Fields extends Record<string, Column
|
|
|
3621
5905
|
*/
|
|
3622
5906
|
export declare const VOLTRO_MIGRATION_LOCK_KEY = 6322741009312437n;
|
|
3623
5907
|
|
|
5908
|
+
/** Index-free for the same reason `_voltro_migration_ops` is: at most a row or two, ever. */
|
|
5909
|
+
export declare const _voltroDeploysTable: TableLike;
|
|
5910
|
+
|
|
3624
5911
|
/**
|
|
3625
5912
|
* One aggregate column in a `SELECT` projection — `COUNT(*)`,
|
|
3626
5913
|
* `SUM(amount) AS total`, etc. Built via the `count() / sum() /
|
|
@@ -3647,9 +5934,38 @@ declare interface WindowSpec {
|
|
|
3647
5934
|
}>;
|
|
3648
5935
|
}
|
|
3649
5936
|
|
|
5937
|
+
/**
|
|
5938
|
+
* One aggregate column in a `SELECT` projection — `COUNT(*)`,
|
|
5939
|
+
* `SUM(amount) AS total`, etc. Built via the `count() / sum() /
|
|
5940
|
+
* avg() / min() / max() / countDistinct()` helpers exported from
|
|
5941
|
+
* `@voltro/database`.
|
|
5942
|
+
*
|
|
5943
|
+
* `column` is undefined for `count()` (which compiles to `COUNT(*)`)
|
|
5944
|
+
* and required for every other op. `alias` is the column name on the
|
|
5945
|
+
* result row.
|
|
5946
|
+
*/
|
|
5947
|
+
/**
|
|
5948
|
+
* Window-function spec (Q3). When set on an AggregateColumn, the
|
|
5949
|
+
* compiler emits `<OP>(...) OVER (PARTITION BY ... ORDER BY ...)`
|
|
5950
|
+
* instead of a plain aggregate. The query is reactive coarsely — the
|
|
5951
|
+
* matcher can't bucket on a window-function result, so the engine
|
|
5952
|
+
* widens the dependency to the whole source table and re-queries on
|
|
5953
|
+
* any write to it (see `runtime/relevantFields.ts`).
|
|
5954
|
+
*/
|
|
5955
|
+
declare interface WindowSpec_2 {
|
|
5956
|
+
readonly partitionBy?: ReadonlyArray<string>;
|
|
5957
|
+
readonly orderBy?: ReadonlyArray<{
|
|
5958
|
+
column: string;
|
|
5959
|
+
direction: 'asc' | 'desc';
|
|
5960
|
+
}>;
|
|
5961
|
+
}
|
|
5962
|
+
|
|
3650
5963
|
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>;
|
|
3651
5964
|
|
|
3652
5965
|
/** Tree of relations to eager-load. */
|
|
3653
5966
|
declare type WithSpec = Readonly<Record<string, true | EagerLoadSpec>>;
|
|
3654
5967
|
|
|
5968
|
+
/** Tree of relations to eager-load. */
|
|
5969
|
+
declare type WithSpec_2 = Readonly<Record<string, true | EagerLoadSpec_2>>;
|
|
5970
|
+
|
|
3655
5971
|
export { }
|