@voltro/database 0.1.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 +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +319 -0
- package/dist/fileBased-cc9IzIjU.js +1083 -0
- package/dist/index.d.ts +5107 -0
- package/dist/index.js +1505 -0
- package/dist/sql.d.ts +2023 -0
- package/dist/sql.js +2569 -0
- package/package.json +51 -0
package/dist/sql.d.ts
ADDED
|
@@ -0,0 +1,2023 @@
|
|
|
1
|
+
import { ConfigError } from 'effect';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { Layer } from 'effect';
|
|
4
|
+
import { Schema } from 'effect';
|
|
5
|
+
import { SqlClient } from '@effect/sql';
|
|
6
|
+
import { SqlError } from '@effect/sql';
|
|
7
|
+
import { SqlError as SqlError_2 } from '@effect/sql/SqlError';
|
|
8
|
+
import { Statement } from '@effect/sql';
|
|
9
|
+
|
|
10
|
+
export declare const acquireMigrationLock: (sql: SqlClient.SqlClient) => Effect.Effect<void, SqlError_2>;
|
|
11
|
+
|
|
12
|
+
declare type AnyMixin = MixinDefinition<Record<string, ColumnDefinition<unknown>>>;
|
|
13
|
+
|
|
14
|
+
declare type AnyTable = Table<string, Record<string, ColumnDefinition<unknown>>, boolean, string>;
|
|
15
|
+
|
|
16
|
+
declare type AnyView = View<string, Record<string, ColumnDefinition<unknown>>>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One row of `_voltro_migrations` — what the applier writes after a
|
|
20
|
+
* successful plan execution. Same shape exposed via inspect RPC for
|
|
21
|
+
* devtools / cloud dashboards.
|
|
22
|
+
*/
|
|
23
|
+
export declare interface AppliedMigration {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly appliedAt: string;
|
|
26
|
+
readonly fingerprint: string;
|
|
27
|
+
readonly operations: ReadonlyArray<PlannedOperation>;
|
|
28
|
+
readonly appliedBy: string;
|
|
29
|
+
readonly environment: 'dev' | 'staging' | 'prod';
|
|
30
|
+
readonly rollbackPlan?: ReadonlyArray<MigrationOperation>;
|
|
31
|
+
readonly durationMs: number;
|
|
32
|
+
readonly onlineStrategy?: 'inline' | 'concurrent' | 'batched' | 'shadow-column';
|
|
33
|
+
readonly source: 'auto-diff' | 'file';
|
|
34
|
+
readonly notes?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Result of executing one op (per-step instrumentation). */
|
|
38
|
+
export declare interface AppliedOp {
|
|
39
|
+
readonly op: MigrationOperation;
|
|
40
|
+
readonly status: 'applied' | 'skipped';
|
|
41
|
+
readonly durationMs: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Apply the schema DDL for a SINGLE tenant namespace: create the
|
|
46
|
+
* container (schema / database / attached file), then create the tenant's
|
|
47
|
+
* tables inside it. This is the migration-time fan-out for namespace
|
|
48
|
+
* isolation — call it once per active tenant namespace, OR lazily on a
|
|
49
|
+
* tenant's first request (the runtime can call this to provision a new
|
|
50
|
+
* tenant's namespace on demand).
|
|
51
|
+
*
|
|
52
|
+
* Idempotent: the container DDL uses `IF NOT EXISTS` / guards, and the
|
|
53
|
+
* table DDL is the same `CREATE TABLE IF NOT EXISTS` path the shared
|
|
54
|
+
* migrate uses — re-running against an already-provisioned namespace is a
|
|
55
|
+
* no-op (except sqlite's ATTACH, which the splitter runs standalone and
|
|
56
|
+
* whose "already attached" error the caller can ignore).
|
|
57
|
+
*/
|
|
58
|
+
export declare const applyNamespacedSchema: (tables: ReadonlyArray<AnyTable>, namespace: string, dialect?: DialectId) => Effect.Effect<void, never, SqlClient.SqlClient>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Execute a `MigrationPlan` against the connected database.
|
|
62
|
+
*
|
|
63
|
+
* Behaviour:
|
|
64
|
+
*
|
|
65
|
+
* 1. If the plan has ANY blocked operation, throw before touching
|
|
66
|
+
* the DB. Callers should run `voltro db plan` first + resolve.
|
|
67
|
+
* 2. Acquire the advisory lock (per-dialect; postgres v1).
|
|
68
|
+
* 3. For each op in plan order: emit the DDL, run any backfill,
|
|
69
|
+
* mark applied.
|
|
70
|
+
* 4. Write a row to `_voltro_migration_plans` with the post-state
|
|
71
|
+
* fingerprint + operations JSON + duration + environment.
|
|
72
|
+
* 5. Release the advisory lock.
|
|
73
|
+
*
|
|
74
|
+
* The return value is the `AppliedMigration` row — the same shape
|
|
75
|
+
* used by the inspect RPC + the cloud dashboard timeline.
|
|
76
|
+
*/
|
|
77
|
+
export declare const applyPlan: (sql: SqlClient.SqlClient, plan: MigrationPlan, ctx: ApplyPlanCtx) => Effect.Effect<AppliedMigration, SqlError_2, SqlClient.SqlClient>;
|
|
78
|
+
|
|
79
|
+
export declare interface ApplyPlanCtx {
|
|
80
|
+
/** Tables declared by user code — needed to look up
|
|
81
|
+
* `.backfill()` specs for add-column ops, since the snapshot
|
|
82
|
+
* doesn't carry them. */
|
|
83
|
+
readonly declared: ReadonlyArray<TableLike>;
|
|
84
|
+
/** `dev:boot` | `cli:user` | service-principal. Written to
|
|
85
|
+
* `_voltro_migration_plans.appliedBy`. */
|
|
86
|
+
readonly appliedBy: string;
|
|
87
|
+
/** Set from `NODE_ENV` upstream. */
|
|
88
|
+
readonly environment: 'dev' | 'staging' | 'prod';
|
|
89
|
+
/** `auto-diff` (planner) or `file` (file-based migration). */
|
|
90
|
+
readonly source: 'auto-diff' | 'file';
|
|
91
|
+
/** Optional human note via `voltro db apply --note "..."`. */
|
|
92
|
+
readonly notes?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Effect that applies the emitted DDL to the provided SqlClient. Splits
|
|
97
|
+
* on `;` only outside dollar-quoted blocks (the postgres NOTIFY trigger
|
|
98
|
+
* function body uses `$$`), so the plpgsql block stays intact as a
|
|
99
|
+
* single statement. SQLite has no `$$` quoting so the splitter is a
|
|
100
|
+
* superset.
|
|
101
|
+
*
|
|
102
|
+
* Caller passes the dialect explicitly. Defaults to `'postgres'` for
|
|
103
|
+
* call-sites that haven't migrated yet.
|
|
104
|
+
*/
|
|
105
|
+
export declare const applySchema: (tables: ReadonlyArray<AnyTable>, dialect?: DialectId) => Effect.Effect<void, never, SqlClient.SqlClient>;
|
|
106
|
+
|
|
107
|
+
/** Per-row backfill body executed by the migration runner. */
|
|
108
|
+
declare type BackfillFunction<TsType = unknown> = (row: Readonly<Record<string, unknown>>) => TsType | Promise<TsType>;
|
|
109
|
+
|
|
110
|
+
declare interface BackfillSpec<TsType = unknown> {
|
|
111
|
+
/**
|
|
112
|
+
* `sql`: server-side expression evaluated by `UPDATE ... SET col = <expr>`.
|
|
113
|
+
* Cheapest path — single DB round-trip, transactional, scales to
|
|
114
|
+
* millions of rows.
|
|
115
|
+
* `js`: per-row JS function. Pulled in batches, transformed locally,
|
|
116
|
+
* pushed back. Use when the value can't be expressed in SQL (calls
|
|
117
|
+
* to an embedding model, third-party API, library-only helpers).
|
|
118
|
+
* 10-100× slower than SQL for the same data — the planner warns
|
|
119
|
+
* above ~10k rows.
|
|
120
|
+
*/
|
|
121
|
+
readonly kind: 'sql' | 'js';
|
|
122
|
+
readonly sql?: Statement.Fragment;
|
|
123
|
+
readonly fn?: BackfillFunction<TsType>;
|
|
124
|
+
readonly batchSize?: number;
|
|
125
|
+
readonly sleepMs?: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export declare type BootEnvironment = 'dev' | 'staging' | 'prod';
|
|
129
|
+
|
|
130
|
+
export declare type BootMigrationOutcome = {
|
|
131
|
+
readonly kind: 'skipped';
|
|
132
|
+
readonly reason: 'VOLTRO_AUTO_MIGRATE=0';
|
|
133
|
+
} | {
|
|
134
|
+
readonly kind: 'up-to-date';
|
|
135
|
+
readonly fingerprint: string;
|
|
136
|
+
} | {
|
|
137
|
+
readonly kind: 'applied';
|
|
138
|
+
readonly applied: AppliedMigration;
|
|
139
|
+
readonly plan: MigrationPlan;
|
|
140
|
+
} | {
|
|
141
|
+
readonly kind: 'prod-mismatch';
|
|
142
|
+
readonly expected: string;
|
|
143
|
+
readonly actual?: string | undefined;
|
|
144
|
+
} | {
|
|
145
|
+
readonly kind: 'refused-blocked';
|
|
146
|
+
readonly plan: MigrationPlan;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** Partition into ≤`n`-sized chunks (disjoint, complete). Exported for the
|
|
150
|
+
* batching regression test — the introspection's correctness reduces to this
|
|
151
|
+
* producing a complete, non-overlapping partition of the table list. */
|
|
152
|
+
export declare const chunkTables: <T>(xs: ReadonlyArray<T>, n: number) => T[][];
|
|
153
|
+
|
|
154
|
+
declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType> {
|
|
155
|
+
readonly type: Type;
|
|
156
|
+
readonly nullable: boolean;
|
|
157
|
+
readonly unique: boolean;
|
|
158
|
+
readonly hasDefault: boolean;
|
|
159
|
+
readonly defaultValue?: unknown;
|
|
160
|
+
/**
|
|
161
|
+
* Application-side default factory. When set, the MutationStore
|
|
162
|
+
* wrapper evaluates this at insert-time IF the caller omits the
|
|
163
|
+
* column from the row. Distinct from `defaultValue` (which is a
|
|
164
|
+
* literal landed in the DDL DEFAULT clause). Functions can do
|
|
165
|
+
* things DDL can't — `() => crypto.randomUUID()`,
|
|
166
|
+
* `() => process.env.REGION`. NOT emitted in DDL.
|
|
167
|
+
*/
|
|
168
|
+
readonly defaultFactory?: () => unknown;
|
|
169
|
+
/**
|
|
170
|
+
* Application-side computed value. Re-evaluated on INSERT and on
|
|
171
|
+
* UPDATE from the rest of the row (after merging defaults on insert;
|
|
172
|
+
* after merging the patch over the existing row on update). The
|
|
173
|
+
* return value overwrites whatever the caller passed for this column.
|
|
174
|
+
* NOT emitted in DDL.
|
|
175
|
+
*
|
|
176
|
+
* Example: `slug: text().computed(row => slugify(row.title))`.
|
|
177
|
+
*
|
|
178
|
+
* If the computed function reads a column the row doesn't carry,
|
|
179
|
+
* it gets `undefined` — handle that in the function.
|
|
180
|
+
*/
|
|
181
|
+
readonly computed?: (row: Readonly<Record<string, unknown>>) => unknown;
|
|
182
|
+
readonly onUpdate?: 'now' | (() => unknown);
|
|
183
|
+
readonly references?: () => TableLike;
|
|
184
|
+
/**
|
|
185
|
+
* Cascade-on-delete behavior for a `reference()` column. Default
|
|
186
|
+
* `'restrict'` — Postgres / MySQL / MariaDB / MSSQL / SQLite all block
|
|
187
|
+
* the parent delete when child rows still point at it. Pre-release
|
|
188
|
+
* choice: `'restrict'` is safer than `'cascade'` (deleting a user
|
|
189
|
+
* shouldn't silently nuke 100k posts) and the explicit `'cascade'`
|
|
190
|
+
* opt-in is one keystroke when the parent-child relationship genuinely
|
|
191
|
+
* is owned-lifecycle.
|
|
192
|
+
*/
|
|
193
|
+
readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
194
|
+
/**
|
|
195
|
+
* Cascade-on-update — default `'noAction'`. Rarely used in practice.
|
|
196
|
+
* Named `refOnUpdate` (not `onUpdate`) to avoid colliding with the
|
|
197
|
+
* existing column-level `onUpdate('now' | …)` trigger field which
|
|
198
|
+
* has nothing to do with FK semantics.
|
|
199
|
+
*/
|
|
200
|
+
readonly refOnUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
201
|
+
/**
|
|
202
|
+
* Auto-create a B-tree index on this FK column. Default `true` — FK
|
|
203
|
+
* columns are nearly always read-by, and the lookup cost of a missing
|
|
204
|
+
* index dwarfs the write cost of an unnecessary one. Opt-out
|
|
205
|
+
* (`{ index: false }`) for the rare table where the parent-child set
|
|
206
|
+
* is small enough that a full-scan beats the index-maintenance
|
|
207
|
+
* overhead.
|
|
208
|
+
*/
|
|
209
|
+
readonly fkAutoIndex?: boolean;
|
|
210
|
+
readonly jsonType?: unknown;
|
|
211
|
+
/**
|
|
212
|
+
* Closed value set, set by `text().oneOf([...])`. When present:
|
|
213
|
+
*
|
|
214
|
+
* - The row type narrows from `string` to the union of literals.
|
|
215
|
+
* - The Postgres migration emitter adds a `CHECK (col IN (...))`
|
|
216
|
+
* constraint so a bad value is rejected by the DB regardless
|
|
217
|
+
* of which client wrote it.
|
|
218
|
+
* - The in-memory data store can validate at insert time
|
|
219
|
+
* (helps catch test-data drift early).
|
|
220
|
+
*
|
|
221
|
+
* For Postgres-native ENUM types: not used here. We treat the
|
|
222
|
+
* underlying column as `text` and lean on CHECK — that keeps adding /
|
|
223
|
+
* removing values a one-statement migration instead of the
|
|
224
|
+
* three-statement dance Postgres ENUMs need (`ALTER TYPE … ADD VALUE`
|
|
225
|
+
* + rewrite, or `ALTER TYPE … RENAME VALUE`).
|
|
226
|
+
*/
|
|
227
|
+
readonly oneOf?: ReadonlyArray<string>;
|
|
228
|
+
/**
|
|
229
|
+
* Bounded text length, set by `text().maxLength(n)`. When present the
|
|
230
|
+
* migrator emits `VARCHAR(n)` (postgres / mysql / mariadb) / `NVARCHAR(n)`
|
|
231
|
+
* (mssql) instead of the unbounded `TEXT` / `LONGTEXT` default. The point
|
|
232
|
+
* is index-ability: a bounded column can back a plain BTREE UNIQUE or
|
|
233
|
+
* composite index, where an unbounded text column can only take a HASH
|
|
234
|
+
* long-unique constraint or a prefix index. `n` is a character count.
|
|
235
|
+
*/
|
|
236
|
+
readonly maxLength?: number;
|
|
237
|
+
/**
|
|
238
|
+
* Raw SQL `CHECK` expressions, set by `column.check(expr)` (accumulates
|
|
239
|
+
* — call it more than once for multiple checks). Each emits an inline
|
|
240
|
+
* `CHECK (<expr>)` on the column. DB-enforced regardless of client,
|
|
241
|
+
* unlike app-level `table().validate(Schema)`. The developer owns the
|
|
242
|
+
* expr's cross-dialect portability. Distinct from `oneOf` (which emits
|
|
243
|
+
* its own `CHECK (col IN (...))`).
|
|
244
|
+
*/
|
|
245
|
+
readonly checks?: ReadonlyArray<string>;
|
|
246
|
+
/**
|
|
247
|
+
* DB-level generated column. Set by `.generatedAs(expr, { stored })`.
|
|
248
|
+
*
|
|
249
|
+
* Distinct from `.computed(row => ...)` (application-time, eval'd
|
|
250
|
+
* at insert) — generated columns are computed by the DB engine
|
|
251
|
+
* itself. Visible to raw SQL, kept consistent on UPDATE without
|
|
252
|
+
* re-stamping, can be indexed (combine with `.expressionIndex`).
|
|
253
|
+
*
|
|
254
|
+
* `stored: true` → `GENERATED ALWAYS AS (expr) STORED` (Postgres/
|
|
255
|
+
* MySQL/MariaDB/SQLite; PERSISTED on MSSQL).
|
|
256
|
+
* Disk-resident, indexable.
|
|
257
|
+
* `stored: false` → VIRTUAL on MySQL/MariaDB/SQLite. Recomputed on
|
|
258
|
+
* read, no disk cost, NOT indexable. Postgres
|
|
259
|
+
* has no VIRTUAL — falls back to STORED with a
|
|
260
|
+
* boot warning.
|
|
261
|
+
*
|
|
262
|
+
* The expression is emitted verbatim (caller-quoted). MUST
|
|
263
|
+
* reference only OTHER columns of the same row — there's no way
|
|
264
|
+
* to reference the subject/tenant context from a generated column.
|
|
265
|
+
* Use `.computed(row => ...)` for context-aware values.
|
|
266
|
+
*/
|
|
267
|
+
readonly generatedAs?: {
|
|
268
|
+
readonly expr: string;
|
|
269
|
+
readonly stored: boolean;
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Postgres-native ENUM type binding, populated when the column is
|
|
273
|
+
* built from `dbEnum(...).column()`. Distinct from `oneOf`:
|
|
274
|
+
*
|
|
275
|
+
* - On Postgres, this column's SQL type is the user-defined ENUM
|
|
276
|
+
* (`status order_status` not `status text CHECK (...)`).
|
|
277
|
+
* - On MySQL / MariaDB, emits a native `ENUM('a','b','c')` column.
|
|
278
|
+
* - On MSSQL / SQLite, falls back to TEXT + CHECK (same shape as
|
|
279
|
+
* `oneOf` — distinct constraint name so the diff can tell them
|
|
280
|
+
* apart).
|
|
281
|
+
*
|
|
282
|
+
* Migration ops `add-enum-value` / `rename-enum-value` apply natively
|
|
283
|
+
* on Postgres (cheap `ALTER TYPE … ADD VALUE`); MySQL needs a column
|
|
284
|
+
* rewrite (`MODIFY col ENUM(...)`); MSSQL / SQLite recreate the CHECK.
|
|
285
|
+
*/
|
|
286
|
+
readonly enumName?: string;
|
|
287
|
+
readonly enumValues?: ReadonlyArray<string>;
|
|
288
|
+
/**
|
|
289
|
+
* Element type for `array(...)` columns. Populated only when
|
|
290
|
+
* `type === 'array'`. The framework emits the DB-native array
|
|
291
|
+
* type on postgres (`text[]`, `integer[]`, etc.) and falls back
|
|
292
|
+
* to a JSON column on other dialects.
|
|
293
|
+
*/
|
|
294
|
+
readonly arrayElement?: ColumnType;
|
|
295
|
+
/**
|
|
296
|
+
* Field-level encryption flag, set by `.encrypted()`. When `true`
|
|
297
|
+
* the runtime's store middleware encrypts the value on write and
|
|
298
|
+
* decrypts on read via the registered field cipher (AES-256-GCM,
|
|
299
|
+
* keyed from the Secrets-Resolver — see `@voltro/plugin-governance`).
|
|
300
|
+
* The stored column is an opaque `enc:v1:…` string, so the DB type
|
|
301
|
+
* stays `text`. A row written while encryption is active can't be
|
|
302
|
+
* read back without the key — losing the key loses the data, by
|
|
303
|
+
* design. Boot fails loud if an `.encrypted()` column is present but
|
|
304
|
+
* no cipher is registered.
|
|
305
|
+
*/
|
|
306
|
+
readonly encrypted?: boolean;
|
|
307
|
+
/**
|
|
308
|
+
* Data-sensitivity classification, set by `.sensitive(class)`. Declares that
|
|
309
|
+
* this column holds personal / sensitive data of a given CLASS (`'email'`,
|
|
310
|
+
* `'fullName'`, `'phone'`, `'secret'`, …). It changes nothing at runtime or
|
|
311
|
+
* in the DDL — it is metadata read by the export layer (`voltro data export
|
|
312
|
+
* --profile …`) to decide how to pseudonymise / anonymise the column when
|
|
313
|
+
* copying prod data to a lower environment. The class picks the
|
|
314
|
+
* format-preserving fake transform. Orthogonal to `.encrypted()` (which
|
|
315
|
+
* protects at rest): an encrypted column is decrypted on read, so it is
|
|
316
|
+
* PLAINTEXT at export time and is treated as sensitive automatically. See
|
|
317
|
+
* `.safe()` for the other half of the fail-closed classification.
|
|
318
|
+
*/
|
|
319
|
+
readonly sensitive?: {
|
|
320
|
+
readonly class: string;
|
|
321
|
+
};
|
|
322
|
+
/**
|
|
323
|
+
* Reviewed-safe marker, set by `.safe()`. Declares that this column holds NO
|
|
324
|
+
* personal / sensitive data and may be copied verbatim to a lower
|
|
325
|
+
* environment. It exists so masking can be FAIL-CLOSED: under a masking
|
|
326
|
+
* profile, a column that is neither `.sensitive()` nor `.safe()` blocks the
|
|
327
|
+
* export until it is consciously classified — so a newly-added column can
|
|
328
|
+
* never silently leak. Metadata only; no runtime/DDL effect.
|
|
329
|
+
*/
|
|
330
|
+
readonly safe?: boolean;
|
|
331
|
+
/**
|
|
332
|
+
* Fixed-point precision (total significant digits), set by
|
|
333
|
+
* `decimal(precision, scale)` / `numeric(precision, scale)`. Populated
|
|
334
|
+
* only when `type === 'decimal'`. Drives the DDL size —
|
|
335
|
+
* `NUMERIC(p,s)` (postgres) / `DECIMAL(p,s)` (mysql/mariadb/mssql) —
|
|
336
|
+
* so a money column round-trips at full precision. On sqlite/turso
|
|
337
|
+
* (no fixed-point type) the column is `NUMERIC` affinity and the value
|
|
338
|
+
* carries as a TEXT string, so `p`/`s` are informational only there.
|
|
339
|
+
*/
|
|
340
|
+
readonly precision?: number;
|
|
341
|
+
/**
|
|
342
|
+
* Fixed-point scale (digits after the decimal point), set by
|
|
343
|
+
* `decimal(precision, scale)`. Populated only when `type === 'decimal'`.
|
|
344
|
+
* MUST be `<= precision`. See {@link ColumnDefinition.precision}.
|
|
345
|
+
*/
|
|
346
|
+
readonly scale?: number;
|
|
347
|
+
/**
|
|
348
|
+
* Vector dimensionality. Populated only when `type === 'vector'`.
|
|
349
|
+
* The migration emitter uses it to size the column DDL —
|
|
350
|
+
* `VECTOR(n)` on postgres (pgvector) + MariaDB 11.7+ / MySQL 9,
|
|
351
|
+
* and a sized fallback (`VARBINARY(MAX)` / `BLOB`) elsewhere. The
|
|
352
|
+
* HNSW index emitter + the `vectorEmbedding()` mixin also read it.
|
|
353
|
+
*/
|
|
354
|
+
readonly vectorDim?: number;
|
|
355
|
+
/**
|
|
356
|
+
* Vector storage precision. Populated only when `type === 'vector'`.
|
|
357
|
+
* `'float32'` (default) emits `VECTOR(n)`; `'half'` emits pgvector's
|
|
358
|
+
* `HALFVEC(n)` (half the bytes, marginally lower recall). MariaDB /
|
|
359
|
+
* MySQL have no half type — the emitter upcasts to `VECTOR(n)` there.
|
|
360
|
+
*/
|
|
361
|
+
readonly vectorPrecision?: 'float32' | 'half';
|
|
362
|
+
/**
|
|
363
|
+
* Verbatim DDL fragment for a `raw(ddl)` escape-hatch column.
|
|
364
|
+
* Populated only when `type === 'raw'`. The migration emitter returns
|
|
365
|
+
* this string unchanged as the column's SQL type on EVERY dialect —
|
|
366
|
+
* the fragment is the single source of truth for the column's type,
|
|
367
|
+
* nullability, and default. Because of that, `.nullable()`,
|
|
368
|
+
* `.default()`, and `.unique()` are rejected at declaration time on a
|
|
369
|
+
* `raw` column (two sources of truth would drift).
|
|
370
|
+
*/
|
|
371
|
+
readonly rawDdl?: string;
|
|
372
|
+
/**
|
|
373
|
+
* Spatial column metadata (`@voltro/plugin-postgis`'s `geography()` /
|
|
374
|
+
* `geometry()`). The row TYPE stays `text` (handlers read/write WKT/EWKT
|
|
375
|
+
* strings); this drives the migration emitter to produce real
|
|
376
|
+
* `geography(Point,4326)` / `geometry(Polygon,4326)` DDL on postgres +
|
|
377
|
+
* `CREATE EXTENSION IF NOT EXISTS postgis`. PostGIS is postgres-only, so
|
|
378
|
+
* the emitter THROWS on any other dialect (the documented fail-loud guard)
|
|
379
|
+
* rather than silently emitting a broken TEXT column. Generic on purpose —
|
|
380
|
+
* the postgis plugin is the only producer, but core owns the DDL hook (the
|
|
381
|
+
* same pattern as `vectorDim` / `enumName`) so the column type is real, not
|
|
382
|
+
* a fabricated marker nothing consumes.
|
|
383
|
+
*/
|
|
384
|
+
readonly spatial?: {
|
|
385
|
+
/** PostGIS base type. */
|
|
386
|
+
readonly kind: 'geography' | 'geometry';
|
|
387
|
+
/** Geometry subtype: `Point` / `LineString` / `Polygon` / … */
|
|
388
|
+
readonly geomKind: string;
|
|
389
|
+
/** Spatial reference id (e.g. 4326 = WGS-84 lat/lon). */
|
|
390
|
+
readonly srid: number;
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Captured `id()` options. Populated only on the primary-key column
|
|
394
|
+
* (`type: 'id'`). At column-construction time the `prefix` for typeid
|
|
395
|
+
* may still be missing — `table()` resolves it against the table name
|
|
396
|
+
* and the resolved scheme lands on `idScheme` below.
|
|
397
|
+
*/
|
|
398
|
+
readonly idSchemeInput?: IdSchemeInput;
|
|
399
|
+
/**
|
|
400
|
+
* Fully-resolved id scheme for the primary-key column. Set by
|
|
401
|
+
* `table()` after walking the field map; downstream code (DDL emit,
|
|
402
|
+
* SchemaRegistry, MutationStore auto-inject) reads this exclusively.
|
|
403
|
+
*/
|
|
404
|
+
readonly idScheme?: IdScheme;
|
|
405
|
+
/**
|
|
406
|
+
* Migration backfill expression. Populated by `.backfill(sqlOrFn, opts?)`.
|
|
407
|
+
*
|
|
408
|
+
* The migration planner reads this when ADD COLUMN + NOT NULL on a
|
|
409
|
+
* populated table would otherwise be classified `needs-backfill` →
|
|
410
|
+
* with a backfill declared, the planner emits a three-step plan
|
|
411
|
+
* (ADD COLUMN nullable → UPDATE via expression → SET NOT NULL) inside
|
|
412
|
+
* one transaction (or one forward-roll group on mysql/mariadb).
|
|
413
|
+
*
|
|
414
|
+
* Has zero effect at insert/query time — pure planner metadata.
|
|
415
|
+
*/
|
|
416
|
+
readonly backfill?: BackfillSpec<TsType>;
|
|
417
|
+
/**
|
|
418
|
+
* Rename marker. Populated by `.renamedFrom('oldName')`.
|
|
419
|
+
*
|
|
420
|
+
* The migration planner sees `oldName` missing + this column present
|
|
421
|
+
* + the `renamedFrom` annotation → classifies as RENAME instead of
|
|
422
|
+
* DROP+ADD (which would lose data). The annotation stays in the code
|
|
423
|
+
* until `_voltro_migrations` records that the rename was applied in
|
|
424
|
+
* the env(s) the developer cares about; removing it earlier yields
|
|
425
|
+
* a clear refuse-to-plan message rather than a silent drop.
|
|
426
|
+
*/
|
|
427
|
+
readonly renamedFrom?: string;
|
|
428
|
+
/**
|
|
429
|
+
* Dropped marker. Set by the standalone `dropped()` helper at the
|
|
430
|
+
* column's field-map slot. The planner treats the column as
|
|
431
|
+
* intentionally going away — emits a `lossy` op that's auto-allowed
|
|
432
|
+
* (no `VOLTRO_DESTRUCTIVE_OK=1` required) because the developer
|
|
433
|
+
* declared intent explicitly in the schema diff.
|
|
434
|
+
*
|
|
435
|
+
* The drop-marker stays for one applied migration cycle, then the
|
|
436
|
+
* developer removes the field map entry entirely.
|
|
437
|
+
*/
|
|
438
|
+
readonly dropped?: boolean;
|
|
439
|
+
/**
|
|
440
|
+
* Type-narrowing marker. Populated by `.narrowedFrom(from, { using })`.
|
|
441
|
+
*
|
|
442
|
+
* The planner uses `from` to know what the live-DB column type was
|
|
443
|
+
* before this migration, and `using` as the dialect-cast expression
|
|
444
|
+
* when the conversion isn't implicit (e.g. `text → integer` on
|
|
445
|
+
* postgres requires `USING col::integer`).
|
|
446
|
+
*/
|
|
447
|
+
readonly narrowedFrom?: NarrowedFromSpec;
|
|
448
|
+
/**
|
|
449
|
+
* Extended unique-constraint metadata. Populated by
|
|
450
|
+
* `.unique({ dedup })`. The legacy boolean `unique` flag above
|
|
451
|
+
* stays as the canonical "is this column unique?" signal — `uniqueSpec`
|
|
452
|
+
* carries the planner-only metadata (dedup strategy) so existing
|
|
453
|
+
* runtime consumers don't need to learn the new shape.
|
|
454
|
+
*/
|
|
455
|
+
readonly uniqueSpec?: UniqueSpec;
|
|
456
|
+
/**
|
|
457
|
+
* Orphan-handling policy used when adding a foreign key to a
|
|
458
|
+
* populated column. Mirrors the `ReferenceOptions.orphanPolicy` API:
|
|
459
|
+
*
|
|
460
|
+
* - `'fail'` (default) → bare ADD CONSTRAINT (`needs-backfill`, applies);
|
|
461
|
+
* the DB rejects it if an existing row is an orphan — exactly
|
|
462
|
+
* like tightening a column to NOT NULL. NOT blocked: the
|
|
463
|
+
* planner is pure (no row counts), so it can't tell a clean /
|
|
464
|
+
* empty table from one with orphans.
|
|
465
|
+
* - `'null'` → planner emits `UPDATE … SET col = NULL` before adding FK
|
|
466
|
+
* - `'delete'` → planner emits `DELETE FROM child WHERE …` before adding FK
|
|
467
|
+
*
|
|
468
|
+
* Has no runtime semantics — purely planner metadata.
|
|
469
|
+
*/
|
|
470
|
+
readonly orphanPolicy?: 'fail' | 'null' | 'delete';
|
|
471
|
+
/**
|
|
472
|
+
* Phantom field so TypeScript can infer the row type at the column level.
|
|
473
|
+
* Never read at runtime.
|
|
474
|
+
*/
|
|
475
|
+
readonly __tsType?: TsType;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* A column as seen by the planner: either declared in code (extracted
|
|
480
|
+
* from `ColumnDefinition`) or live in the DB (extracted via introspection).
|
|
481
|
+
*
|
|
482
|
+
* The shape is intentionally minimal — only the bits that affect diff
|
|
483
|
+
* classification land here. Things like `idScheme`, `computed`,
|
|
484
|
+
* `defaultFactory` etc. live in the runtime layer and don't survive
|
|
485
|
+
* the round-trip to `information_schema`.
|
|
486
|
+
*/
|
|
487
|
+
export declare interface ColumnSnapshot {
|
|
488
|
+
readonly name: string;
|
|
489
|
+
readonly type: ColumnType;
|
|
490
|
+
readonly nullable: boolean;
|
|
491
|
+
readonly unique: boolean;
|
|
492
|
+
readonly hasDefault: boolean;
|
|
493
|
+
readonly defaultValue?: unknown;
|
|
494
|
+
/**
|
|
495
|
+
* Type-narrowing acknowledgement from `.narrowedFrom(from, { using })`.
|
|
496
|
+
* Only ever set on the DECLARED side; introspection leaves it
|
|
497
|
+
* undefined (the live DB has no notion of a prior type). When
|
|
498
|
+
* `from` equals the live column type the planner downgrades the
|
|
499
|
+
* otherwise-blocked type change to `needs-backfill` and threads
|
|
500
|
+
* `using` into the `ALTER COLUMN … TYPE … USING <using>` cast. */
|
|
501
|
+
readonly narrowedFrom?: {
|
|
502
|
+
readonly from: ColumnType;
|
|
503
|
+
readonly using?: string;
|
|
504
|
+
};
|
|
505
|
+
readonly references?: {
|
|
506
|
+
readonly table: string;
|
|
507
|
+
readonly column: string;
|
|
508
|
+
readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
509
|
+
readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
510
|
+
/**
|
|
511
|
+
* Orphan-handling policy for adding this FK to a POPULATED column
|
|
512
|
+
* (`reference(() => t, { orphanPolicy })`). Only ever set on the
|
|
513
|
+
* DECLARED side; introspection leaves it undefined (the live DB has
|
|
514
|
+
* no notion of it). `normRef` strips it before comparison so it never
|
|
515
|
+
* causes FK churn — the planner reads it directly to classify +
|
|
516
|
+
* drive the applier's orphan pre-step. See the references-diff in
|
|
517
|
+
* `classifyColumnChange`.
|
|
518
|
+
*/
|
|
519
|
+
readonly orphanPolicy?: 'fail' | 'null' | 'delete';
|
|
520
|
+
};
|
|
521
|
+
/** `oneOf(['a','b'])` → CHECK constraint values. */
|
|
522
|
+
readonly oneOf?: ReadonlyArray<string>;
|
|
523
|
+
/**
|
|
524
|
+
* Fixed-point precision + scale for a `decimal(p,s)` / `numeric(p,s)`
|
|
525
|
+
* column (`type === 'decimal'`). Drives the emitted `NUMERIC(p,s)` /
|
|
526
|
+
* `DECIMAL(p,s)` DDL. Set on the DECLARED side; introspection leaves them
|
|
527
|
+
* undefined (the planner compares the `decimal` type structurally, not the
|
|
528
|
+
* exact precision — an ALTER of precision alone is not auto-classified, same
|
|
529
|
+
* posture as `generatedAs`). See `snapshotPgType` / `snapshotMysqlType`.
|
|
530
|
+
*/
|
|
531
|
+
readonly precision?: number;
|
|
532
|
+
readonly scale?: number;
|
|
533
|
+
/**
|
|
534
|
+
* Id-generation scheme for `type: 'id'` columns. Mirrors
|
|
535
|
+
* `ColumnDefinition.idScheme.kind` so the applier can tell a numeric
|
|
536
|
+
* auto-increment PK (`kind: 'numeric'`) from a typeid/string PK and
|
|
537
|
+
* emit the dialect-correct auto-increment DDL (BIGSERIAL / BIGINT
|
|
538
|
+
* AUTO_INCREMENT / IDENTITY / INTEGER AUTOINCREMENT). Only ever set on
|
|
539
|
+
* the DECLARED side; introspection leaves it undefined (the live DB
|
|
540
|
+
* doesn't report a scheme — a numeric PK is detectable there from the
|
|
541
|
+
* integer type + auto_increment, which the diff doesn't need).
|
|
542
|
+
*/
|
|
543
|
+
readonly idScheme?: {
|
|
544
|
+
readonly kind: string;
|
|
545
|
+
};
|
|
546
|
+
/**
|
|
547
|
+
* Verbatim DDL fragment for a `raw(ddl)` column (`type === 'raw'`).
|
|
548
|
+
* Emitted unchanged as the column's SQL type on every dialect — the
|
|
549
|
+
* applier and the CREATE-TABLE emitter both defer to it. Only ever set
|
|
550
|
+
* on the declared side; the live DB reports a concrete type, not the
|
|
551
|
+
* original fragment, so introspection leaves it undefined.
|
|
552
|
+
*/
|
|
553
|
+
readonly rawDdl?: string;
|
|
554
|
+
/**
|
|
555
|
+
* `.generatedAs(expr, { stored })` — a DB-computed column. When set the
|
|
556
|
+
* applier emits `GENERATED ALWAYS AS (<expr>) STORED|VIRTUAL` (mssql:
|
|
557
|
+
* `AS (<expr>) PERSISTED`) and SUPPRESSES NOT NULL / DEFAULT / inline
|
|
558
|
+
* UNIQUE / REFERENCES (the value is computed; the DB rejects those
|
|
559
|
+
* alongside a generation clause). Only ever set on the DECLARED side —
|
|
560
|
+
* introspection does NOT read generation expressions, so the live snapshot
|
|
561
|
+
* leaves it undefined. The planner's `sameColumnShape` deliberately does
|
|
562
|
+
* NOT compare this field, so a declared-generated column never diffs
|
|
563
|
+
* against its introspected-as-plain live form (no churn). Consequence:
|
|
564
|
+
* the declarative engine creates generated columns correctly but does NOT
|
|
565
|
+
* detect a CHANGE to the expression after the fact — recreate the table
|
|
566
|
+
* (drop + re-migrate) to alter a generation expression.
|
|
567
|
+
*/
|
|
568
|
+
readonly generatedAs?: {
|
|
569
|
+
readonly expr: string;
|
|
570
|
+
readonly stored: boolean;
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* Data-sensitivity classification from `.sensitive(class)` (or implied by
|
|
574
|
+
* `.encrypted()`). DECLARED-side only — introspection never reports it. Read
|
|
575
|
+
* by the export masker to pseudonymise/anonymise the column when copying to a
|
|
576
|
+
* lower environment; the planner ignores it (no DDL, no churn). See `safe`.
|
|
577
|
+
*/
|
|
578
|
+
readonly sensitive?: {
|
|
579
|
+
readonly class: string;
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* Reviewed-safe marker from `.safe()`. DECLARED-side only. Lets the export
|
|
583
|
+
* masker be fail-closed: a column that is neither `sensitive` nor `safe`
|
|
584
|
+
* blocks a masking export until classified.
|
|
585
|
+
*/
|
|
586
|
+
readonly safe?: boolean;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
declare type ColumnType = 'id' | 'text' | 'integer' | 'real' | 'decimal' | 'bigint' | 'boolean' | 'timestamp' | 'date' | 'json' | 'bytes' | 'reference' | 'vector' | 'enum' | 'array' | 'interval' | 'raw';
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Build a `SchemaSnapshot` from an array of `TableLike` declarations.
|
|
593
|
+
* Caller is responsible for passing the FULL set of tables (user-
|
|
594
|
+
* declared + framework). The fingerprint over this snapshot is what the
|
|
595
|
+
* boot-time prod check compares against `_voltro_migration_plans`.
|
|
596
|
+
*/
|
|
597
|
+
export declare const declaredSnapshot: (tables: ReadonlyArray<TableLike>) => SchemaSnapshot;
|
|
598
|
+
|
|
599
|
+
export declare const defaultClause: (column: ColumnDefinition<unknown>, dialect: DialectId) => string | null;
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* `DEFAULT <expr>` for a plain-object json default, per dialect's json-column
|
|
603
|
+
* type. Single source for the auto-migrate (`defaultClause`) AND declarative-
|
|
604
|
+
* applier paths so the two can't drift (the introspected form must round-trip
|
|
605
|
+
* against `normalizeDefault`, so the emit shape matters on both sides).
|
|
606
|
+
*/
|
|
607
|
+
export declare const defaultJsonClause: (value: object, dialect: DialectId) => string;
|
|
608
|
+
|
|
609
|
+
export declare const describeOutcome: (outcome: BootMigrationOutcome, dialectId: string) => string;
|
|
610
|
+
|
|
611
|
+
/** Dialect IDs the framework supports. */
|
|
612
|
+
declare type DialectId = 'postgres' | 'mysql' | 'mariadb' | 'mssql' | 'sqlite' | 'turso';
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Output of `voltro db drift`: compares the live DB's introspected
|
|
616
|
+
* schema against the last applied fingerprint in `_voltro_migrations`.
|
|
617
|
+
*
|
|
618
|
+
* `isDrifted: false` is the happy path. A drift report names the
|
|
619
|
+
* concrete divergences so the operator can either reconcile in code or
|
|
620
|
+
* via a corrective migration.
|
|
621
|
+
*/
|
|
622
|
+
export declare interface DriftReport {
|
|
623
|
+
readonly isDrifted: boolean;
|
|
624
|
+
readonly lastAppliedFingerprint?: string;
|
|
625
|
+
readonly liveFingerprint: string;
|
|
626
|
+
readonly divergences: ReadonlyArray<{
|
|
627
|
+
readonly kind: 'unexpected' | 'missing';
|
|
628
|
+
readonly description: string;
|
|
629
|
+
}>;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export declare const emitDropColumnDdl: (op: Extract<MigrationOperation, {
|
|
633
|
+
kind: "drop-column";
|
|
634
|
+
}>, q: typeof quote) => string;
|
|
635
|
+
|
|
636
|
+
export declare const emitDropColumnDdlMysql: (op: Extract<MigrationOperation, {
|
|
637
|
+
kind: "drop-column";
|
|
638
|
+
}>) => string;
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Evolution-aware DDL emitter for framework-managed tables (the
|
|
642
|
+
* `_voltro_*` bookkeeping set + the `voltro_isr_cache` runtime
|
|
643
|
+
* cache). Adds one extra step between CREATE TABLE and CREATE INDEX:
|
|
644
|
+
*
|
|
645
|
+
* ALTER TABLE <t> ADD COLUMN IF NOT EXISTS <col> <type>
|
|
646
|
+
*
|
|
647
|
+
* for every column the framework's declared shape carries. Without
|
|
648
|
+
* this the CREATE TABLE IF NOT EXISTS path is fine for fresh DBs but
|
|
649
|
+
* silently no-ops on existing tables — subsequent CREATE INDEX IF
|
|
650
|
+
* NOT EXISTS then PARSE-fails when an index references a column the
|
|
651
|
+
* framework added in a later release that the live DB never picked
|
|
652
|
+
* up (pg error 42703).
|
|
653
|
+
*
|
|
654
|
+
* Postgres-only — the other dialects' framework-bookkeeping path
|
|
655
|
+
* doesn't suffer this because @effect/sql-cluster / migrationRunner
|
|
656
|
+
* own their own schema. The applier in the per-dialect packages does
|
|
657
|
+
* the equivalent work for their own bookkeeping.
|
|
658
|
+
*
|
|
659
|
+
* For user / plugin / cloud tables this path is wrong — they need the
|
|
660
|
+
* full planner refuse-to-boot semantics on column drops, renames,
|
|
661
|
+
* type changes. Framework bookkeeping is a tighter contract:
|
|
662
|
+
* additive-only column evolution, no destructive changes, no user
|
|
663
|
+
* data ever lives in a `.dropped()` slot. The framework versions
|
|
664
|
+
* its own schema additively + can safely ALTER TABLE forward.
|
|
665
|
+
*/
|
|
666
|
+
export declare const emitFrameworkBootstrapSql: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => string;
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Emit the full schema DDL SCOPED to a tenant namespace. The container is
|
|
670
|
+
* created first (CREATE SCHEMA / DATABASE / ATTACH), then the table DDL
|
|
671
|
+
* runs INSIDE it:
|
|
672
|
+
*
|
|
673
|
+
* - postgres → `SET search_path TO "<ns>"` precedes the (unchanged)
|
|
674
|
+
* table DDL, so unqualified CREATE TABLEs land in the tenant schema.
|
|
675
|
+
* The migrate connection is dedicated + disposed after, so the
|
|
676
|
+
* session-scoped search_path never leaks to a request connection.
|
|
677
|
+
* - mysql / mariadb → `USE \`<ns>\`` precedes the DDL (same idea).
|
|
678
|
+
* - mssql / sqlite → these dialects have no per-session schema switch
|
|
679
|
+
* usable here, so the table + index names are SCHEMA-QUALIFIED
|
|
680
|
+
* (`[ns].[todos]` / `"ns"."todos"`). The migrator passes the
|
|
681
|
+
* namespace through to the per-table emitter.
|
|
682
|
+
*
|
|
683
|
+
* On mssql/sqlite, namespaced DDL qualification covers CREATE TABLE +
|
|
684
|
+
* CREATE INDEX. FK targets that reference framework/core tables in the
|
|
685
|
+
* shared schema are NOT re-pointed — database-per-tenant FKs across
|
|
686
|
+
* containers are a deliberate non-goal (each tenant namespace is
|
|
687
|
+
* self-contained for its own tables).
|
|
688
|
+
*/
|
|
689
|
+
export declare const emitNamespacedSchemaSql: (tables: ReadonlyArray<AnyTable>, dialect: DialectId, namespace: string, sqliteFile?: string) => string;
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* DDL that creates a tenant's namespace CONTAINER, per dialect:
|
|
693
|
+
*
|
|
694
|
+
* - postgres / mssql → a SCHEMA
|
|
695
|
+
* - mysql / mariadb → a DATABASE (SCHEMA ≡ DATABASE — database-per-tenant)
|
|
696
|
+
* - sqlite → an ATTACHed database file
|
|
697
|
+
*
|
|
698
|
+
* `namespace` MUST already be a safe identifier (`tenant_<id>` from
|
|
699
|
+
* `resolveTenantNamespace`) — it's emitted in an identifier position, so
|
|
700
|
+
* the caller owns the injection guard. Idempotent where the dialect
|
|
701
|
+
* supports it (`IF NOT EXISTS` / `OBJECT_ID` guard); sqlite's `ATTACH`
|
|
702
|
+
* errors if the alias is already attached, which the migrator treats as
|
|
703
|
+
* "already provisioned".
|
|
704
|
+
*/
|
|
705
|
+
export declare const emitNamespaceProvisionDdl: (namespace: string, dialect: DialectId, sqliteFile?: string) => string;
|
|
706
|
+
|
|
707
|
+
export declare const emitSchemaSql: (entities: ReadonlyArray<AnyTable | AnyView>, dialect: DialectId) => string;
|
|
708
|
+
|
|
709
|
+
declare type EmptyMerge = unknown;
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Filename pattern the discovery walker matches: timestamp-prefixed
|
|
713
|
+
* `.ts` files in any depth under the project's `migrations/` dir.
|
|
714
|
+
*
|
|
715
|
+
* <UTC-timestamp>_<slug>.ts
|
|
716
|
+
* 20260415_120000_split_address_out.ts
|
|
717
|
+
*
|
|
718
|
+
* Timestamp format is YYYYMMDD_HHMMSS — sortable, globally unique
|
|
719
|
+
* without coordination, human-readable. The `_<slug>` is for humans
|
|
720
|
+
* + tools; the runner only orders by the leading timestamp.
|
|
721
|
+
*/
|
|
722
|
+
export declare const FILE_MIGRATION_PATTERN: RegExp;
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* A file-based migration descriptor. Default-export an instance from
|
|
726
|
+
* a `migrations/<timestamp>_<slug>.ts` file; the framework's file
|
|
727
|
+
* runner picks it up via the discovery walker.
|
|
728
|
+
*/
|
|
729
|
+
export declare interface FileMigration {
|
|
730
|
+
/** Stable id — convention is `<UTC-timestamp>_<slug>`. Must match
|
|
731
|
+
* the file's basename minus the `.ts`. Recorded in
|
|
732
|
+
* `_voltro_migration_plans.id` so `voltro db rollback <id>`
|
|
733
|
+
* can find the right `down` body. */
|
|
734
|
+
readonly id: string;
|
|
735
|
+
/** Human description that surfaces in `voltro db plans` + the
|
|
736
|
+
* history timeline. Keep it short — what the migration does. */
|
|
737
|
+
readonly description: string;
|
|
738
|
+
/** The forward body. Runs inside a transaction on dialects that
|
|
739
|
+
* support transactional DDL (postgres / mssql / sqlite); runs as
|
|
740
|
+
* separate implicit-commit statements on mysql / mariadb. */
|
|
741
|
+
readonly up: (ctx: FileMigrationContext) => Promise<void> | Effect.Effect<void, unknown, never>;
|
|
742
|
+
/** The reverse body — required. The runner uses this for
|
|
743
|
+
* `voltro db rollback <id>`. Non-destructive inverses don't
|
|
744
|
+
* bring back data dropped in `up`; design accordingly (see
|
|
745
|
+
* Rollback docs). */
|
|
746
|
+
readonly down: (ctx: FileMigrationContext) => Promise<void> | Effect.Effect<void, unknown, never>;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* The runtime context a file-based migration's `up` / `down` body
|
|
751
|
+
* receives.
|
|
752
|
+
*/
|
|
753
|
+
export declare interface FileMigrationContext {
|
|
754
|
+
/** The live `@effect/sql` SqlClient — issue raw DDL via
|
|
755
|
+
* `sql.unsafe(...)`, tagged-template fragments via `sql\`...\``,
|
|
756
|
+
* per-dialect dispatch via `sql.onDialectOrElse({...})`. */
|
|
757
|
+
readonly sql: SqlClient.SqlClient;
|
|
758
|
+
/**
|
|
759
|
+
* Lightweight log surface scoped to the migration. Lands in
|
|
760
|
+
* `voltro logs --filter 'migrations:file:<id>'` for any apply
|
|
761
|
+
* that runs through the dev / start boot path.
|
|
762
|
+
*/
|
|
763
|
+
readonly log: {
|
|
764
|
+
readonly info: (message: string, fields?: Record<string, unknown>) => Effect.Effect<void>;
|
|
765
|
+
readonly warn: (message: string, fields?: Record<string, unknown>) => Effect.Effect<void>;
|
|
766
|
+
};
|
|
767
|
+
/** ISO-8601 timestamp when this migration started (deterministic
|
|
768
|
+
* across the up/down pair via the applied_at column). */
|
|
769
|
+
readonly appliedAt: string;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export declare interface FileMigrationRunResult {
|
|
773
|
+
readonly applied: ReadonlyArray<{
|
|
774
|
+
id: string;
|
|
775
|
+
durationMs: number;
|
|
776
|
+
}>;
|
|
777
|
+
readonly skipped: ReadonlyArray<string>;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Stable-hash a SchemaSnapshot. Returns the hex-encoded sha256 of the
|
|
782
|
+
* canonicalised JSON representation. Truncated identifiers are NOT
|
|
783
|
+
* accepted as equivalent — the hash sees the full name. Length: 64 hex
|
|
784
|
+
* chars; the CLI displays the first 16 in boot logs and devtools UI.
|
|
785
|
+
*/
|
|
786
|
+
export declare const fingerprintSchema: (snapshot: SchemaSnapshot) => string;
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Fully-resolved scheme stored on the `id` column definition AFTER
|
|
790
|
+
* `table()` walks the fields. For `typeid`, the prefix is always
|
|
791
|
+
* populated here — either user-supplied or derived from the table name.
|
|
792
|
+
*/
|
|
793
|
+
declare type IdScheme = {
|
|
794
|
+
readonly kind: 'typeid';
|
|
795
|
+
readonly prefix: string;
|
|
796
|
+
} | {
|
|
797
|
+
readonly kind: 'ulid';
|
|
798
|
+
} | {
|
|
799
|
+
readonly kind: 'numeric';
|
|
800
|
+
} | {
|
|
801
|
+
readonly kind: 'snowflake';
|
|
802
|
+
} | {
|
|
803
|
+
readonly kind: 'custom';
|
|
804
|
+
readonly generate: (tableName: string) => string;
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* User-facing input to `id()`. Some shapes leave the prefix to be
|
|
809
|
+
* derived from the table name at register-time.
|
|
810
|
+
*
|
|
811
|
+
* Defaults:
|
|
812
|
+
* - `undefined` → `{ scheme: 'typeid' }` with derived prefix
|
|
813
|
+
* - `{ prefix }` → typeid with that prefix
|
|
814
|
+
* - `{ scheme: 'typeid' }` → derived prefix
|
|
815
|
+
* - `{ scheme: 'typeid', prefix }` → explicit prefix
|
|
816
|
+
*/
|
|
817
|
+
declare type IdSchemeInput = undefined | {
|
|
818
|
+
readonly prefix: string;
|
|
819
|
+
} | {
|
|
820
|
+
readonly scheme: 'typeid';
|
|
821
|
+
readonly prefix?: string;
|
|
822
|
+
} | {
|
|
823
|
+
readonly scheme: 'ulid';
|
|
824
|
+
} | {
|
|
825
|
+
readonly scheme: 'numeric';
|
|
826
|
+
} | {
|
|
827
|
+
readonly scheme: 'snowflake';
|
|
828
|
+
} | {
|
|
829
|
+
readonly scheme: 'custom';
|
|
830
|
+
readonly generate: (tableName: string) => string;
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Live tables the planner must NOT drop, parsed from `VOLTRO_DB_IGNORE_TABLES`
|
|
835
|
+
* (comma-separated). The framework already self-excludes its own `_voltro_*`
|
|
836
|
+
* runtime tables; this is the USER escape hatch for their own unmanaged infra
|
|
837
|
+
* tables (e.g. a Strapi→Voltro `_strapi_id_map`), so the diff doesn't try to
|
|
838
|
+
* drop a table the migration tooling itself depends on.
|
|
839
|
+
*
|
|
840
|
+
* Single-sourced here so the BOOT auto-migrate (`runDev`, below) and the
|
|
841
|
+
* `voltro db plan/apply` CLI paths honour the EXACT same ignore-list — before
|
|
842
|
+
* this, only the CLI read the env var, so `voltro dev` would still refuse-to-boot
|
|
843
|
+
* on a `drop-table` for a table the CLI was told to leave alone.
|
|
844
|
+
*/
|
|
845
|
+
export declare const ignoreTablesFromEnv: () => {
|
|
846
|
+
ignoreTables?: ReadonlyArray<string>;
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* A (possibly composite) secondary index declared at the table level
|
|
851
|
+
* via `.index('byName', [...fields])`. The migration emitter turns each
|
|
852
|
+
* one into a `CREATE INDEX IF NOT EXISTS` statement; the runtime's
|
|
853
|
+
* SchemaRegistry indexes the fields for the non-indexed-query warning
|
|
854
|
+
* (see `@voltro/runtime/schemaRegistry.ts`).
|
|
855
|
+
*
|
|
856
|
+
* Postgres uses leading-prefix matching: an index on
|
|
857
|
+
* `[tenantId, email]` helps queries that filter on `tenantId` alone OR
|
|
858
|
+
* `tenantId AND email`, but NOT queries that filter only on `email`.
|
|
859
|
+
* The warning logic accounts for that — the FIRST field of a composite
|
|
860
|
+
* is treated as "covered", the rest aren't.
|
|
861
|
+
*/
|
|
862
|
+
/**
|
|
863
|
+
* An entry in `TableIndex.fields` is either a column name (the
|
|
864
|
+
* common case) or an expression object. Expression objects let an
|
|
865
|
+
* index sit on `lower("email")` / `date_trunc('month', "createdAt")`
|
|
866
|
+
* etc. The expression string is emitted verbatim into the DDL —
|
|
867
|
+
* caller is responsible for quoting identifiers correctly.
|
|
868
|
+
*/
|
|
869
|
+
declare type IndexField = string | {
|
|
870
|
+
readonly expr: string;
|
|
871
|
+
} | {
|
|
872
|
+
readonly jsonPath: JsonIndexPath;
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Index access method. `btree` (default) and `gist` are the original
|
|
877
|
+
* two; `gin` / `brin` / `hnsw` extend the DSL with real per-dialect
|
|
878
|
+
* compilation + fallbacks (see the support matrix in `migrate.ts`'s
|
|
879
|
+
* `usingClauseFor`).
|
|
880
|
+
*
|
|
881
|
+
* - `btree` — default; the all-purpose ordered index. Implicit on every
|
|
882
|
+
* dialect.
|
|
883
|
+
* - `gist` — postgres-only (PostGIS spatial / range types). Warn +
|
|
884
|
+
* btree elsewhere.
|
|
885
|
+
* - `gin` — postgres inverted index for `jsonb` containment / array
|
|
886
|
+
* membership / fulltext. mysql/mariadb have no GIN → warn + skip (a
|
|
887
|
+
* btree on a JSON/array column is useless); mssql/sqlite → warn +
|
|
888
|
+
* btree.
|
|
889
|
+
* - `brin` — postgres block-range index for naturally-ordered large
|
|
890
|
+
* tables (append-only timestamps). Other dialects → warn + btree (a
|
|
891
|
+
* legitimate, if larger, substitute for the range-scan use case).
|
|
892
|
+
* - `hnsw` — postgres + pgvector approximate-nearest-neighbour index
|
|
893
|
+
* for `vector` columns. Off-postgres → warn + skip (no analogue; a
|
|
894
|
+
* btree on a vector is meaningless). Requires exactly one `vector`
|
|
895
|
+
* field — validated at declaration time.
|
|
896
|
+
*/
|
|
897
|
+
declare type IndexKind = 'btree' | 'gist' | 'gin' | 'brin' | 'hnsw';
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Per-kind tuning options. Today only `hnsw` carries knobs (the pgvector
|
|
901
|
+
* build-time params). `m` controls graph connectivity (default 16),
|
|
902
|
+
* `efConstruction` the build-time candidate-list size (default 64).
|
|
903
|
+
*/
|
|
904
|
+
declare interface IndexKindOptions {
|
|
905
|
+
readonly hnsw?: {
|
|
906
|
+
readonly m?: number;
|
|
907
|
+
readonly efConstruction?: number;
|
|
908
|
+
/**
|
|
909
|
+
* Distance metric the index (and matching `nearestNeighbours`
|
|
910
|
+
* queries) use. Selects the pgvector opclass:
|
|
911
|
+
* - `'cosine'` → `vector_cosine_ops` (default; the right choice for
|
|
912
|
+
* normalised embeddings from most modern models)
|
|
913
|
+
* - `'l2'` → `vector_l2_ops` (Euclidean)
|
|
914
|
+
* - `'inner'` → `vector_ip_ops` (negative inner product)
|
|
915
|
+
* The opclass MUST match the metric the query orders by, or the
|
|
916
|
+
* index can't accelerate the search. Set `opclass` directly only to
|
|
917
|
+
* override the mapping (e.g. `halfvec_cosine_ops`).
|
|
918
|
+
*/
|
|
919
|
+
readonly distance?: VectorDistance;
|
|
920
|
+
/**
|
|
921
|
+
* pgvector opclass token spliced into `USING hnsw (col <opclass>)`.
|
|
922
|
+
* When unset, derived from `distance` (default `vector_cosine_ops`).
|
|
923
|
+
* Set explicitly to override (e.g. `'halfvec_cosine_ops'` for a
|
|
924
|
+
* `vector(n, { precision: 'half' })` column).
|
|
925
|
+
*/
|
|
926
|
+
readonly opclass?: string;
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
export declare interface IndexSnapshot {
|
|
931
|
+
readonly name: string;
|
|
932
|
+
readonly table: string;
|
|
933
|
+
readonly columns: ReadonlyArray<string>;
|
|
934
|
+
readonly unique: boolean;
|
|
935
|
+
/**
|
|
936
|
+
* Expression / json-path index (`expressionIndex(...)` / `jsonIndex(...)`),
|
|
937
|
+
* i.e. at least one key is a SQL expression rather than a bare column. The
|
|
938
|
+
* DB normalises the expression text on introspection (`(lower("email"))` →
|
|
939
|
+
* `lower(email)`), so it can NEVER round-trip byte-for-byte. The planner
|
|
940
|
+
* therefore matches such indexes by NAME + uniqueness only (see
|
|
941
|
+
* `sameIndexShape`) — without this they re-emit on every `db plan` forever
|
|
942
|
+
* (postgres pre-fix: they were INVISIBLE to introspect — an `add` with no
|
|
943
|
+
* `drop` that never converged to "up to date").
|
|
944
|
+
*/
|
|
945
|
+
readonly expression?: boolean;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
export declare const _internalCmp: (name: string) => (a: {
|
|
949
|
+
name: string;
|
|
950
|
+
}, b: {
|
|
951
|
+
name: string;
|
|
952
|
+
}) => number;
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Read the current shape of the connected database. Dispatched per
|
|
956
|
+
* dialect; supports postgres only.
|
|
957
|
+
*
|
|
958
|
+
* The returned `SchemaSnapshot` is the same shape `declaredSnapshot`
|
|
959
|
+
* produces — they're directly diffable via `planMigrations`.
|
|
960
|
+
*
|
|
961
|
+
* Restricted to the `public` schema today; multi-schema apps will
|
|
962
|
+
* grow a `schema` parameter when we have a real need.
|
|
963
|
+
*/
|
|
964
|
+
export declare const introspectSchema: (sql: SqlClient.SqlClient) => Effect.Effect<SchemaSnapshot, SqlError_2>;
|
|
965
|
+
|
|
966
|
+
/** Type-guard for the discovery walker. */
|
|
967
|
+
export declare const isFileMigration: (value: unknown) => value is FileMigration;
|
|
968
|
+
|
|
969
|
+
/** Type guard — narrows an unknown value to a {@link RawSqlFragment}. */
|
|
970
|
+
export declare const isRawSqlFragment: (value: unknown) => value is RawSqlFragment;
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* A json-path index entry, produced by {@link jsonIndex}. Unlike a plain
|
|
974
|
+
* `{ expr }` (verbatim, dialect-specific) this carries the column + path
|
|
975
|
+
* abstractly and is lowered to each dialect's json accessor at DDL time —
|
|
976
|
+
* through the SAME `jsonPathSql` the predicate compiler uses, so a
|
|
977
|
+
* `jsonField('p','k')` FILTER and a `jsonIndex('p','k')` INDEX produce
|
|
978
|
+
* byte-identical SQL and the optimiser actually uses the index.
|
|
979
|
+
*/
|
|
980
|
+
declare interface JsonIndexPath {
|
|
981
|
+
readonly column: string;
|
|
982
|
+
readonly path: ReadonlyArray<string | number>;
|
|
983
|
+
/** Index the value as a number (for `gt/gte/lt/lte` range filters)
|
|
984
|
+
* rather than as text. Set via {@link JsonIndexField.numeric}. */
|
|
985
|
+
readonly numeric: boolean;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export declare const mapPgType: (dataType: string, udtName?: string) => ColumnType;
|
|
989
|
+
|
|
990
|
+
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;
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Factory for declaring a file-based migration. The shape is
|
|
994
|
+
* intentionally minimal — the framework deals with discovery,
|
|
995
|
+
* ordering, transaction wrapping + history tracking. Bodies stay
|
|
996
|
+
* focused on the actual data move.
|
|
997
|
+
*
|
|
998
|
+
* @example
|
|
999
|
+
*
|
|
1000
|
+
* export default migration({
|
|
1001
|
+
* id: '20260415_120000_split_address_out',
|
|
1002
|
+
* description: 'Move users.address* into a separate addresses table.',
|
|
1003
|
+
* up: async ({ sql, log }) => {
|
|
1004
|
+
* await sql.unsafe(`CREATE TABLE addresses ( ... )`)
|
|
1005
|
+
* await sql.unsafe(`INSERT INTO addresses (...) SELECT (...) FROM users`)
|
|
1006
|
+
* await sql.unsafe(`ALTER TABLE users DROP COLUMN addressStreet`)
|
|
1007
|
+
* log.info('migrated address fields to new table')
|
|
1008
|
+
* },
|
|
1009
|
+
* down: async ({ sql }) => {
|
|
1010
|
+
* await sql.unsafe(`ALTER TABLE users ADD COLUMN addressStreet text`)
|
|
1011
|
+
* // ... etc — see file-based docs for the full pattern
|
|
1012
|
+
* },
|
|
1013
|
+
* })
|
|
1014
|
+
*/
|
|
1015
|
+
export declare const migration: (spec: FileMigration) => FileMigration;
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Discriminated union for the concrete DDL operations the planner
|
|
1019
|
+
* emits. Every variant carries the `table` (most ops act on one
|
|
1020
|
+
* table) plus whatever payload the applier needs.
|
|
1021
|
+
*
|
|
1022
|
+
* The planner does NOT emit raw SQL here — that's the applier's job,
|
|
1023
|
+
* dispatched per-dialect via `sql.onDialectOrElse`. Keeping the
|
|
1024
|
+
* payload structural (not stringified) lets the same plan apply
|
|
1025
|
+
* cleanly on every dialect.
|
|
1026
|
+
*/
|
|
1027
|
+
export declare type MigrationOperation = {
|
|
1028
|
+
readonly kind: 'create-table';
|
|
1029
|
+
readonly table: string;
|
|
1030
|
+
readonly columns: ReadonlyArray<ColumnSnapshot>;
|
|
1031
|
+
readonly indexes: ReadonlyArray<IndexSnapshot>;
|
|
1032
|
+
readonly primaryKey?: ReadonlyArray<string>;
|
|
1033
|
+
} | {
|
|
1034
|
+
readonly kind: 'drop-table';
|
|
1035
|
+
readonly table: string;
|
|
1036
|
+
} | {
|
|
1037
|
+
readonly kind: 'add-column';
|
|
1038
|
+
readonly table: string;
|
|
1039
|
+
readonly column: ColumnSnapshot;
|
|
1040
|
+
} | {
|
|
1041
|
+
readonly kind: 'drop-column';
|
|
1042
|
+
readonly table: string;
|
|
1043
|
+
readonly column: string;
|
|
1044
|
+
} | {
|
|
1045
|
+
readonly kind: 'rename-column';
|
|
1046
|
+
readonly table: string;
|
|
1047
|
+
readonly from: string;
|
|
1048
|
+
readonly to: string;
|
|
1049
|
+
} | {
|
|
1050
|
+
readonly kind: 'alter-column-nullability';
|
|
1051
|
+
readonly table: string;
|
|
1052
|
+
readonly column: string;
|
|
1053
|
+
readonly toNullable: boolean;
|
|
1054
|
+
} | {
|
|
1055
|
+
readonly kind: 'alter-column-type';
|
|
1056
|
+
readonly table: string;
|
|
1057
|
+
readonly column: string;
|
|
1058
|
+
readonly from: ColumnType;
|
|
1059
|
+
readonly to: ColumnType;
|
|
1060
|
+
readonly using?: string;
|
|
1061
|
+
} | {
|
|
1062
|
+
readonly kind: 'alter-column-default';
|
|
1063
|
+
readonly table: string;
|
|
1064
|
+
readonly column: string;
|
|
1065
|
+
readonly defaultValue?: unknown;
|
|
1066
|
+
readonly hasDefault: boolean;
|
|
1067
|
+
} | {
|
|
1068
|
+
readonly kind: 'add-index';
|
|
1069
|
+
readonly table: string;
|
|
1070
|
+
readonly index: IndexSnapshot;
|
|
1071
|
+
} | {
|
|
1072
|
+
readonly kind: 'drop-index';
|
|
1073
|
+
readonly table: string;
|
|
1074
|
+
readonly index: string;
|
|
1075
|
+
} | {
|
|
1076
|
+
readonly kind: 'add-unique';
|
|
1077
|
+
readonly table: string;
|
|
1078
|
+
readonly column: string;
|
|
1079
|
+
} | {
|
|
1080
|
+
readonly kind: 'drop-unique';
|
|
1081
|
+
readonly table: string;
|
|
1082
|
+
readonly column: string;
|
|
1083
|
+
} | {
|
|
1084
|
+
readonly kind: 'add-unique-composite';
|
|
1085
|
+
readonly table: string;
|
|
1086
|
+
readonly name: string;
|
|
1087
|
+
readonly fields: ReadonlyArray<string>;
|
|
1088
|
+
readonly dedup?: 'fail' | 'suffix-counter' | {
|
|
1089
|
+
sql: string;
|
|
1090
|
+
};
|
|
1091
|
+
} | {
|
|
1092
|
+
readonly kind: 'drop-unique-composite';
|
|
1093
|
+
readonly table: string;
|
|
1094
|
+
readonly name: string;
|
|
1095
|
+
} | {
|
|
1096
|
+
readonly kind: 'add-foreign-key';
|
|
1097
|
+
readonly table: string;
|
|
1098
|
+
readonly column: string;
|
|
1099
|
+
readonly targetTable: string;
|
|
1100
|
+
readonly targetColumn: string;
|
|
1101
|
+
readonly onDelete?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
1102
|
+
readonly onUpdate?: 'cascade' | 'restrict' | 'setNull' | 'noAction';
|
|
1103
|
+
readonly orphanPolicy?: 'null' | 'delete';
|
|
1104
|
+
} | {
|
|
1105
|
+
readonly kind: 'drop-foreign-key';
|
|
1106
|
+
readonly table: string;
|
|
1107
|
+
readonly column: string;
|
|
1108
|
+
} | {
|
|
1109
|
+
readonly kind: 'add-check';
|
|
1110
|
+
readonly table: string;
|
|
1111
|
+
readonly column: string;
|
|
1112
|
+
readonly values: ReadonlyArray<string>;
|
|
1113
|
+
} | {
|
|
1114
|
+
readonly kind: 'drop-check';
|
|
1115
|
+
readonly table: string;
|
|
1116
|
+
readonly column: string;
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
export declare interface MigrationPlan {
|
|
1120
|
+
readonly operations: ReadonlyArray<PlannedOperation>;
|
|
1121
|
+
/** Pre-state fingerprint (live DB at plan time). */
|
|
1122
|
+
readonly fromFingerprint: string;
|
|
1123
|
+
/** Post-state fingerprint (what the DB will look like after apply). */
|
|
1124
|
+
readonly toFingerprint: string;
|
|
1125
|
+
/** Count summaries by class — what `voltro db plan` prints. */
|
|
1126
|
+
readonly summary: {
|
|
1127
|
+
readonly safe: number;
|
|
1128
|
+
readonly needsDefault: number;
|
|
1129
|
+
readonly needsBackfill: number;
|
|
1130
|
+
readonly needsRenameAnnotation: number;
|
|
1131
|
+
readonly lossy: number;
|
|
1132
|
+
readonly onlineRequired: number;
|
|
1133
|
+
readonly multiStep: number;
|
|
1134
|
+
readonly blocked: number;
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
declare interface MixinDefinition<F extends Record<string, ColumnDefinition<unknown>>> {
|
|
1139
|
+
/**
|
|
1140
|
+
* Stable dedup key. Two mixins with the same `id` reaching a table via
|
|
1141
|
+
* different paths (direct + transitive) are materialised once. Convention:
|
|
1142
|
+
* `<pluginNamespace>/<mixinName>` — e.g. `voltro/audit`, `voltro/tenant`,
|
|
1143
|
+
* `acmeBilling/invoiceable`.
|
|
1144
|
+
*/
|
|
1145
|
+
readonly id?: string;
|
|
1146
|
+
readonly fields: F;
|
|
1147
|
+
/**
|
|
1148
|
+
* Other mixins this one needs in place. Resolved depth-first before the
|
|
1149
|
+
* mixin itself is applied, so its `beforeInsert` / `defaultWhere` can rely
|
|
1150
|
+
* on the columns added by its dependencies.
|
|
1151
|
+
*/
|
|
1152
|
+
readonly requires?: ReadonlyArray<AnyMixin>;
|
|
1153
|
+
/**
|
|
1154
|
+
* Indexes the mixin contributes when applied to a table. Used by the
|
|
1155
|
+
* built-in `tenant()` mixin to auto-index `tenantId` without
|
|
1156
|
+
* resurrecting a column-level `.index()` modifier (which we
|
|
1157
|
+
* deliberately collapsed into the single table-level surface). The
|
|
1158
|
+
* table-builder merges these into its `appliedIndexes` and
|
|
1159
|
+
* auto-names anonymous entries per-table.
|
|
1160
|
+
*/
|
|
1161
|
+
readonly indexes?: ReadonlyArray<MixinIndex>;
|
|
1162
|
+
readonly behaviors?: {
|
|
1163
|
+
readonly beforeInsert?: (...args: unknown[]) => unknown;
|
|
1164
|
+
readonly beforeUpdate?: (...args: unknown[]) => unknown;
|
|
1165
|
+
readonly onDelete?: (...args: unknown[]) => unknown;
|
|
1166
|
+
readonly defaultWhere?: (...args: unknown[]) => unknown;
|
|
1167
|
+
};
|
|
1168
|
+
readonly policies?: ReadonlyArray<{
|
|
1169
|
+
readonly name: string;
|
|
1170
|
+
readonly predicate: (...args: unknown[]) => boolean;
|
|
1171
|
+
}>;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Index contribution from a mixin. The fields are column names that
|
|
1176
|
+
* MUST exist either on the host table or on the mixin's own `fields`
|
|
1177
|
+
* map. The optional `name` lets the mixin pin the literal index
|
|
1178
|
+
* identifier; omitted, the table-builder auto-generates
|
|
1179
|
+
* `<tableName>_<col1>_<col2>_idx` so the same mixin applied to
|
|
1180
|
+
* different tables produces distinct DDL names.
|
|
1181
|
+
*/
|
|
1182
|
+
declare interface MixinIndex {
|
|
1183
|
+
readonly fields: ReadonlyArray<string>;
|
|
1184
|
+
readonly name?: string;
|
|
1185
|
+
/**
|
|
1186
|
+
* Index access method. Default `'btree'`. The `vectorEmbedding()` mixin
|
|
1187
|
+
* sets `kind: 'hnsw'` so the vector column it contributes also gets its
|
|
1188
|
+
* ANN index — without resurrecting a column-level `.index()` modifier.
|
|
1189
|
+
* The table-builder forwards `kind` + `kindOptions` into the
|
|
1190
|
+
* `TableIndex` it materialises from this contribution.
|
|
1191
|
+
*/
|
|
1192
|
+
readonly kind?: IndexKind;
|
|
1193
|
+
/** Per-kind tuning (e.g. `kindOptions.hnsw.{m, efConstruction, distance}`). */
|
|
1194
|
+
readonly kindOptions?: IndexKindOptions;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Type-narrowing migration marker. Tells the planner that this column
|
|
1199
|
+
* previously had type `from` and is now being narrowed/converted to its
|
|
1200
|
+
* current type. Without `using`, narrowing fails for rows whose value
|
|
1201
|
+
* can't fit the new type. The `using` fragment is the dialect-specific
|
|
1202
|
+
* cast expression (Postgres `... USING col::int`, MSSQL CONVERT/CAST,
|
|
1203
|
+
* MySQL CAST).
|
|
1204
|
+
*/
|
|
1205
|
+
declare interface NarrowedFromSpec {
|
|
1206
|
+
readonly from: ColumnType;
|
|
1207
|
+
/**
|
|
1208
|
+
* Raw SQL cast expression spliced into the planner's
|
|
1209
|
+
* `ALTER COLUMN … TYPE … USING <using>` (postgres) / shadow-copy
|
|
1210
|
+
* (`shadow := <using>(old)`). Authored by the developer in the
|
|
1211
|
+
* migration — e.g. `'role::role_enum'`, `'flag::int::text'`. Omit when
|
|
1212
|
+
* the conversion is implicit on the dialect (e.g. `varchar → text`).
|
|
1213
|
+
*/
|
|
1214
|
+
readonly using?: string;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* DDL fragment for a numeric primary key column. Each dialect has its
|
|
1219
|
+
* own auto-increment idiom — there's no portable shorthand, so we
|
|
1220
|
+
* dispatch per dialect explicitly. The fragment includes type + PK +
|
|
1221
|
+
* auto-increment in one statement; the caller skips the generic
|
|
1222
|
+
* `NOT NULL` / `PRIMARY KEY` additions for numeric ids.
|
|
1223
|
+
*/
|
|
1224
|
+
export declare const numericIdSql: (name: string, dialect: DialectId) => string;
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* The seven classes of schema operations the planner recognises. The
|
|
1228
|
+
* default policy for each is encoded by the planner; the applier
|
|
1229
|
+
* obeys it. Keep this enum in lockstep with the AGENTS.md template's
|
|
1230
|
+
* "Schema migrations" section — agents grep for these strings.
|
|
1231
|
+
*/
|
|
1232
|
+
export declare type OperationClass =
|
|
1233
|
+
/** Reversible, side-effect-free for existing data. Auto-applied. */
|
|
1234
|
+
'safe'
|
|
1235
|
+
/** ADD COLUMN + NOT NULL with `.default(value)` declared.
|
|
1236
|
+
* Applied as `ADD COLUMN ... NOT NULL DEFAULT <value>`. */
|
|
1237
|
+
| 'needs-default'
|
|
1238
|
+
/** ADD COLUMN + NOT NULL on a populated table, ADD UNIQUE / FK / CHECK
|
|
1239
|
+
* with existing offenders. Requires a deliberate declaration
|
|
1240
|
+
* (`.backfill()`, `.unique({dedup})`, `reference({orphanPolicy})`).
|
|
1241
|
+
* Applied as a three-step plan (ADD nullable → run backfill → SET
|
|
1242
|
+
* NOT NULL). */
|
|
1243
|
+
| 'needs-backfill'
|
|
1244
|
+
/** Column appears removed + a new one appeared. Looks like DROP+ADD
|
|
1245
|
+
* but is probably a rename. Refuse-to-plan without an explicit
|
|
1246
|
+
* `.renamedFrom('oldName')` marker — silent renames lose data. */
|
|
1247
|
+
| 'needs-rename-annotation'
|
|
1248
|
+
/** DROP COLUMN, DROP TABLE, narrow type with existing offenders.
|
|
1249
|
+
* Refuse-to-plan unless `dropped()` / `.narrowedFrom()` declared
|
|
1250
|
+
* OR `VOLTRO_DESTRUCTIVE_OK=1` set. */
|
|
1251
|
+
| 'lossy'
|
|
1252
|
+
/** Large-table operation that should run online (CREATE INDEX
|
|
1253
|
+
* CONCURRENTLY, batched backfill, shadow-column swap). The applier
|
|
1254
|
+
* auto-rewrites these once the row count crosses the threshold
|
|
1255
|
+
* (`online-after`, default 50k). */
|
|
1256
|
+
| 'online-required'
|
|
1257
|
+
/** Operations the planner can't infer from a diff (table split /
|
|
1258
|
+
* merge, custom data move, type-change with USING). Requires a
|
|
1259
|
+
* file-based migration in `migrations/<id>.ts`. */
|
|
1260
|
+
| 'multi-step';
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* The vocabulary of concrete DDL-level operations the planner emits.
|
|
1264
|
+
* Each `MigrationOperation` carries one of these kinds plus the data
|
|
1265
|
+
* the applier needs to execute it.
|
|
1266
|
+
*
|
|
1267
|
+
* `kind` strings are short + lowercase-kebab so they round-trip
|
|
1268
|
+
* cleanly through JSON (the `operations` column of `_voltro_migrations`)
|
|
1269
|
+
* and can be grepped in `voltro logs` output.
|
|
1270
|
+
*/
|
|
1271
|
+
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';
|
|
1272
|
+
|
|
1273
|
+
export declare const parseEnumCheck: (clause: string) => {
|
|
1274
|
+
readonly column: string;
|
|
1275
|
+
readonly values: ReadonlyArray<string>;
|
|
1276
|
+
} | null;
|
|
1277
|
+
|
|
1278
|
+
export declare interface PlanInput {
|
|
1279
|
+
readonly declared: ReadonlyArray<TableLike>;
|
|
1280
|
+
readonly live: SchemaSnapshot;
|
|
1281
|
+
/**
|
|
1282
|
+
* Threshold above which safe DDL ops on a populated table get
|
|
1283
|
+
* promoted to `online-required` (CONCURRENTLY / batched / shadow-
|
|
1284
|
+
* column). Pass undefined to use the framework default (50_000).
|
|
1285
|
+
*/
|
|
1286
|
+
readonly onlineAfter?: number;
|
|
1287
|
+
/**
|
|
1288
|
+
* The target dialect. Drives storage-collapse type equivalence (sqlite
|
|
1289
|
+
* stores timestamp/json as TEXT, mssql stores json as NVARCHAR) so the plan
|
|
1290
|
+
* doesn't churn phantom alter-column-type ops. Omit → no collapse (postgres
|
|
1291
|
+
* / mysql / mariadb distinguish these natively anyway).
|
|
1292
|
+
*/
|
|
1293
|
+
readonly dialect?: DialectId;
|
|
1294
|
+
/**
|
|
1295
|
+
* Live tables to LEAVE ALONE — never planned as a drop even though no
|
|
1296
|
+
* declared entity matches them. The framework already self-excludes its own
|
|
1297
|
+
* runtime tables (`_voltro_*` / `_cloud_*` / `cluster_*`, see
|
|
1298
|
+
* `isFrameworkOwnedLiveTable`); this is the USER escape hatch for their own
|
|
1299
|
+
* unmanaged infra tables (e.g. a Strapi→Voltro `_strapi_id_map`), so the diff
|
|
1300
|
+
* doesn't try to drop a table the migration tooling itself depends on. The
|
|
1301
|
+
* CLI populates it from `VOLTRO_DB_IGNORE_TABLES` (comma-separated names).
|
|
1302
|
+
*/
|
|
1303
|
+
readonly ignoreTables?: ReadonlyArray<string>;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Compute the migration plan between a declared schema (with
|
|
1308
|
+
* annotations) and a live snapshot.
|
|
1309
|
+
*
|
|
1310
|
+
* The output is deterministic: same inputs → same `MigrationPlan`
|
|
1311
|
+
* (modulo op insertion order, which follows table-then-kind sort
|
|
1312
|
+
* order). Lets the applier replay the plan from `_voltro_migration_plans`
|
|
1313
|
+
* without surprises.
|
|
1314
|
+
*
|
|
1315
|
+
* The planner does NOT execute pre-check queries — that's the
|
|
1316
|
+
* applier's job (it needs live SQL access). The classifications here
|
|
1317
|
+
* are based purely on the snapshot shapes + the annotations on
|
|
1318
|
+
* `ColumnDefinition`. The applier may downgrade `safe` → `online-required`
|
|
1319
|
+
* once it knows the row count exceeds the threshold.
|
|
1320
|
+
*/
|
|
1321
|
+
export declare const planMigrations: (input: PlanInput) => MigrationPlan;
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* One row in a `MigrationPlan` — a concrete operation paired with the
|
|
1325
|
+
* planner's classification + any blocking reason.
|
|
1326
|
+
*
|
|
1327
|
+
* `blocked` is set when the classification refuses-to-plan (lossy
|
|
1328
|
+
* without `dropped()`, rename without `.renamedFrom()`, etc.). The
|
|
1329
|
+
* applier checks `blocked` before executing; the CLI's `voltro db plan`
|
|
1330
|
+
* output highlights it red.
|
|
1331
|
+
*
|
|
1332
|
+
* `backfill` / `dedup` / `orphanPolicy` mirror the user's annotations
|
|
1333
|
+
* straight from `ColumnDefinition` so the applier doesn't need to
|
|
1334
|
+
* re-introspect them.
|
|
1335
|
+
*/
|
|
1336
|
+
export declare interface PlannedOperation {
|
|
1337
|
+
readonly op: MigrationOperation;
|
|
1338
|
+
readonly classification: OperationClass;
|
|
1339
|
+
/** Why the op was classified this way (also surfaces in the CLI). */
|
|
1340
|
+
readonly reason?: string;
|
|
1341
|
+
/** Set on lossy / needs-rename / multi-step ops with no resolution. */
|
|
1342
|
+
readonly blocked?: {
|
|
1343
|
+
readonly fix: string;
|
|
1344
|
+
};
|
|
1345
|
+
/** Pre-check estimate the planner ran (row counts, offender count). */
|
|
1346
|
+
readonly estimate?: {
|
|
1347
|
+
readonly affectedRows?: number;
|
|
1348
|
+
readonly offenderCount?: number;
|
|
1349
|
+
};
|
|
1350
|
+
/** Whether the op is atomic for each supported dialect. Postgres,
|
|
1351
|
+
* mssql, sqlite are atomic for all DDL; mysql/mariadb implicit-commit
|
|
1352
|
+
* every statement. */
|
|
1353
|
+
readonly atomicByDialect: {
|
|
1354
|
+
readonly postgres: boolean;
|
|
1355
|
+
readonly mysql: boolean;
|
|
1356
|
+
readonly mariadb: boolean;
|
|
1357
|
+
readonly mssql: boolean;
|
|
1358
|
+
readonly sqlite: boolean;
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Imperative `applyNamespacedSchema` against a fresh SqlClient — the
|
|
1364
|
+
* entry point the CLI / runtime use to provision a tenant's namespace
|
|
1365
|
+
* (eager at migrate time, or lazily on first use).
|
|
1366
|
+
*/
|
|
1367
|
+
export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>, namespace: string, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId) => Promise<void>;
|
|
1368
|
+
|
|
1369
|
+
declare const quote: (name: string) => string;
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* A captured raw-SQL fragment: the literal string segments of a tagged
|
|
1373
|
+
* template plus the interpolated values, in template order
|
|
1374
|
+
* (`strings[0]`, `values[0]`, `strings[1]`, …, `strings[n]`). The store
|
|
1375
|
+
* rebinds it onto its live `sql` at execution time — `strings` splice in
|
|
1376
|
+
* verbatim, `values` bind as parameters.
|
|
1377
|
+
*/
|
|
1378
|
+
export declare interface RawSqlFragment {
|
|
1379
|
+
readonly _tag: 'RawSqlFragment';
|
|
1380
|
+
readonly strings: ReadonlyArray<string>;
|
|
1381
|
+
readonly values: ReadonlyArray<unknown>;
|
|
1382
|
+
/**
|
|
1383
|
+
* Tables this raw read depends on, for reactive invalidation. Raw
|
|
1384
|
+
* reads are otherwise untracked — the query planner can't infer which
|
|
1385
|
+
* tables an arbitrary SQL string touches. Declare them here (or via
|
|
1386
|
+
* `store.raw(fragment, { dependsOn })`) to opt the read into the
|
|
1387
|
+
* reactive layer's change-driven recomputation.
|
|
1388
|
+
*/
|
|
1389
|
+
readonly dependsOn?: ReadonlyArray<string>;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
export declare const releaseMigrationLock: (sql: SqlClient.SqlClient) => Effect.Effect<void, SqlError_2>;
|
|
1393
|
+
|
|
1394
|
+
export declare const renderColumnMysql: (col: ColumnSnapshot) => string;
|
|
1395
|
+
|
|
1396
|
+
export declare const renderColumnPg: (col: ColumnSnapshot) => string;
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Roll back a single file-based migration by id. Looks up the
|
|
1400
|
+
* migration's `down` body via file discovery (the file must still be
|
|
1401
|
+
* present on disk — file-based rollback is NOT idempotent across
|
|
1402
|
+
* file deletion).
|
|
1403
|
+
*
|
|
1404
|
+
* The runner refuses to roll back any migration that isn't the
|
|
1405
|
+
* latest applied file-based one (chain integrity); use `voltro db
|
|
1406
|
+
* rollback --to <id>` to walk multiple back.
|
|
1407
|
+
*/
|
|
1408
|
+
export declare const rollbackFileBasedMigration: (sql: SqlClient.SqlClient, ctx: {
|
|
1409
|
+
projectRoot: string;
|
|
1410
|
+
id: string;
|
|
1411
|
+
}) => Effect.Effect<{
|
|
1412
|
+
id: string;
|
|
1413
|
+
durationMs: number;
|
|
1414
|
+
}, unknown>;
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* Discover + apply every pending file-based migration under
|
|
1418
|
+
* `projectRoot/migrations/`. Runs under the same advisory lock as
|
|
1419
|
+
* the planner-based applier, so concurrent invocations serialise.
|
|
1420
|
+
*
|
|
1421
|
+
* - Returns `applied` (newly-run ids + durations) + `skipped`
|
|
1422
|
+
* (ids that were already applied) for the caller's log line.
|
|
1423
|
+
* - Failures abort the run + propagate as the Effect's error
|
|
1424
|
+
* channel; the caller (dev.ts boot) is expected to refuse-to-boot.
|
|
1425
|
+
*/
|
|
1426
|
+
export declare const runFileBasedMigrations: (sql: SqlClient.SqlClient, ctx: RunFileBasedMigrationsCtx) => Effect.Effect<FileMigrationRunResult, unknown>;
|
|
1427
|
+
|
|
1428
|
+
export declare interface RunFileBasedMigrationsCtx {
|
|
1429
|
+
readonly projectRoot: string;
|
|
1430
|
+
readonly env: 'dev' | 'staging' | 'prod';
|
|
1431
|
+
readonly appliedBy: string;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Evolution-aware bootstrap for framework-managed tables. Same
|
|
1436
|
+
* imperative shape as `runMigrate` but uses `emitFrameworkBootstrapSql`
|
|
1437
|
+
* (which adds `ALTER TABLE ADD COLUMN IF NOT EXISTS` between
|
|
1438
|
+
* CREATE TABLE and CREATE INDEX). Use this for `_voltro_*` and
|
|
1439
|
+
* `voltro_isr_cache` so framework releases that grow new columns
|
|
1440
|
+
* don't crash old DBs at boot.
|
|
1441
|
+
*/
|
|
1442
|
+
export declare const runFrameworkBootstrap: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId) => Promise<void>;
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Run `applySchema` against a fresh SqlClient from the provided sqlLayer.
|
|
1446
|
+
* Imperative entry point for the voltro dev CLI; callers that already
|
|
1447
|
+
* own a runtime can use `applySchema` directly.
|
|
1448
|
+
*
|
|
1449
|
+
* `dialect` is passed through to the DDL emitter so SQL types + reactive
|
|
1450
|
+
* trigger emission match the underlying engine.
|
|
1451
|
+
*/
|
|
1452
|
+
export declare const runMigrate: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId) => Promise<void>;
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Top-level entry point — called once per boot. Returns a
|
|
1456
|
+
* `BootMigrationOutcome`; the caller logs + acts on it (refuse-to-boot
|
|
1457
|
+
* on `refused-blocked` / `prod-mismatch`, continue otherwise).
|
|
1458
|
+
*
|
|
1459
|
+
* Wires the dev/prod dispatch + the VOLTRO_AUTO_MIGRATE escape hatch.
|
|
1460
|
+
*/
|
|
1461
|
+
export declare const runPlannedMigrations: (sql: SqlClient.SqlClient, declared: ReadonlyArray<TableLike>, ctx: RunPlannedMigrationsCtx) => Effect.Effect<BootMigrationOutcome, SqlError_2, SqlClient.SqlClient>;
|
|
1462
|
+
|
|
1463
|
+
export declare interface RunPlannedMigrationsCtx {
|
|
1464
|
+
/** What `voltro logs` should attribute this run to. */
|
|
1465
|
+
readonly appliedBy: string;
|
|
1466
|
+
readonly environment: BootEnvironment;
|
|
1467
|
+
/** Pretty-name dialect used for log lines. */
|
|
1468
|
+
readonly dialectId: string;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* The full schema as the planner sees it on one side of the diff.
|
|
1473
|
+
* Both the declared side and the introspected side conform to this
|
|
1474
|
+
* shape so the diff algorithm is symmetric.
|
|
1475
|
+
*/
|
|
1476
|
+
export declare interface SchemaSnapshot {
|
|
1477
|
+
readonly tables: ReadonlyArray<TableSnapshot>;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* Short form for log output / UI. Same hash, first 16 chars; cheap to
|
|
1482
|
+
* recognise in scans of `voltro logs` output. Never used for equality
|
|
1483
|
+
* checks — those go through the full hash.
|
|
1484
|
+
*/
|
|
1485
|
+
export declare const shortFingerprint: (fp: string) => string;
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* The `sql` template tag — captures a tagged-template literal into a
|
|
1489
|
+
* {@link RawSqlFragment} descriptor without binding it to any client.
|
|
1490
|
+
*
|
|
1491
|
+
* ```ts
|
|
1492
|
+
* import { sql } from '@voltro/database/sql'
|
|
1493
|
+
* const rows = await store.raw<{ n: number }>(
|
|
1494
|
+
* sql`SELECT count(*) AS n FROM events WHERE occurred_at > ${cutoff}`,
|
|
1495
|
+
* { dependsOn: ['events'] },
|
|
1496
|
+
* )
|
|
1497
|
+
* ```
|
|
1498
|
+
*
|
|
1499
|
+
* `cutoff` is bound as a parameter by the store, never interpolated —
|
|
1500
|
+
* a value containing `'; DROP TABLE …` round-trips as data.
|
|
1501
|
+
*/
|
|
1502
|
+
export declare const sql: (strings: TemplateStringsArray, ...values: ReadonlyArray<unknown>) => RawSqlFragment;
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* Map a logical column type to its dialect-specific DDL keyword.
|
|
1506
|
+
*
|
|
1507
|
+
* Supported across all five dialects: postgres, mysql, mariadb,
|
|
1508
|
+
* mssql, and sqlite.
|
|
1509
|
+
*
|
|
1510
|
+
* Notes on the sqlite mappings:
|
|
1511
|
+
* - `TIMESTAMPTZ` becomes `TEXT` storing ISO-8601 strings — sqlite
|
|
1512
|
+
* has no native timezone type. Apps that read these via the
|
|
1513
|
+
* framework's typed query path stay agnostic; raw users get a
|
|
1514
|
+
* string they can `new Date()`.
|
|
1515
|
+
* - `JSONB` becomes `TEXT` (sqlite has `JSON` validation extensions
|
|
1516
|
+
* but plain `TEXT` storage with `json()` accessors is the more
|
|
1517
|
+
* portable default).
|
|
1518
|
+
* - `VECTOR` is unsupported on sqlite — emitted as `BLOB`.
|
|
1519
|
+
*/
|
|
1520
|
+
export declare const sqlType: (column: ColumnDefinition<unknown>, dialect: DialectId) => string;
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Effect that performs the squash + returns a summary. Fails if:
|
|
1524
|
+
* - no eligible rows match (`before` cut everything off)
|
|
1525
|
+
* - the latest pre-squash fingerprint differs from `fingerprint`
|
|
1526
|
+
* (signals uncommitted drift — refuse to lock in)
|
|
1527
|
+
*/
|
|
1528
|
+
export declare const squashMigrationPlans: (options: SquashOptions) => Effect.Effect<SquashResult, Error, SqlClient.SqlClient>;
|
|
1529
|
+
|
|
1530
|
+
export declare interface SquashOptions {
|
|
1531
|
+
/** ISO-8601 timestamp (`'2026-06-01T00:00:00Z'`) — rows applied
|
|
1532
|
+
* before this point are squashed. */
|
|
1533
|
+
readonly before: string;
|
|
1534
|
+
/** Optional human-readable note attached to the snapshot row. */
|
|
1535
|
+
readonly note?: string;
|
|
1536
|
+
/** Pre-validated current schema fingerprint. Compared with the
|
|
1537
|
+
* most-recent pre-squash row's fingerprint to ensure the squash
|
|
1538
|
+
* doesn't lock in drift. */
|
|
1539
|
+
readonly fingerprint: string;
|
|
1540
|
+
/** The principal name written into `appliedBy` on the snapshot
|
|
1541
|
+
* row. Typically the CLI user. */
|
|
1542
|
+
readonly appliedBy: string;
|
|
1543
|
+
/** Environment tag for the snapshot row. */
|
|
1544
|
+
readonly environment: string;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
export declare interface SquashResult {
|
|
1548
|
+
/** Number of migration_plans rows marked squashed. */
|
|
1549
|
+
readonly squashedCount: number;
|
|
1550
|
+
/** The fingerprint carried on the synthetic snapshot row. */
|
|
1551
|
+
readonly snapshotFingerprint: string;
|
|
1552
|
+
/** ID of the inserted snapshot row. */
|
|
1553
|
+
readonly snapshotId: string;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
declare interface Table<Name extends string, Fields extends Record<string, ColumnDefinition<unknown>>, Reactive extends boolean = false, IxNames extends string = never> extends TableLike {
|
|
1557
|
+
readonly tableName: Name;
|
|
1558
|
+
readonly fields: Fields;
|
|
1559
|
+
readonly isReactive: Reactive;
|
|
1560
|
+
readonly appliedMixins: ReadonlyArray<AnyMixin>;
|
|
1561
|
+
readonly appliedIndexes: ReadonlyArray<TableIndex>;
|
|
1562
|
+
readonly appliedUniques: ReadonlyArray<TableUnique>;
|
|
1563
|
+
readonly appliedFullText: ReadonlyArray<TableFullTextIndex>;
|
|
1564
|
+
readonly appliedChecks: ReadonlyArray<TableCheck>;
|
|
1565
|
+
/**
|
|
1566
|
+
* Composite PRIMARY KEY column set, declared via `.primaryKey(['a','b'])`.
|
|
1567
|
+
* When set it REPLACES the single-column `id()` PK assumption: no column
|
|
1568
|
+
* emits an inline `PRIMARY KEY`, and the table gets a table-level
|
|
1569
|
+
* `PRIMARY KEY (a, b)` constraint instead. Empty (the default) means the
|
|
1570
|
+
* `id()` column carries the PK inline, as before.
|
|
1571
|
+
*/
|
|
1572
|
+
readonly appliedPrimaryKey: ReadonlyArray<string>;
|
|
1573
|
+
/**
|
|
1574
|
+
* Optional `effect/Schema` the framework decodes the row against
|
|
1575
|
+
* BEFORE every INSERT. Populated by `.validate(schema)`. Set
|
|
1576
|
+
* per-table at table-construction time; immutable thereafter.
|
|
1577
|
+
*/
|
|
1578
|
+
readonly insertSchema?: Schema.Schema.Any;
|
|
1579
|
+
/**
|
|
1580
|
+
* Optional `effect/Schema` the framework decodes a patch against
|
|
1581
|
+
* BEFORE every UPDATE. Populated by `.validatePatch(schema)`. The
|
|
1582
|
+
* framework wraps the schema in `Schema.partial` so a patch that
|
|
1583
|
+
* omits fields is still valid — only the fields present in the patch
|
|
1584
|
+
* are checked. Set per-table at table-construction time; immutable
|
|
1585
|
+
* thereafter.
|
|
1586
|
+
*/
|
|
1587
|
+
readonly validatePatchSchema?: Schema.Schema.Any;
|
|
1588
|
+
with: <const Mixins extends ReadonlyArray<AnyMixin>>(...mixins: Mixins) => Table<Name, Fields & MergeMixinFields<Mixins>, Reactive, IxNames>;
|
|
1589
|
+
/**
|
|
1590
|
+
* Attach an effect/Schema that validates EVERY insert against the
|
|
1591
|
+
* table. The MutationStore decodes the incoming row right before
|
|
1592
|
+
* the INSERT — failure throws a typed `TableValidationError` the
|
|
1593
|
+
* mutation handler surfaces as its typed-error channel.
|
|
1594
|
+
*
|
|
1595
|
+
* Applies on INSERT. Pair with `.validatePatch(...)` for UPDATE
|
|
1596
|
+
* patches.
|
|
1597
|
+
*
|
|
1598
|
+
* Example:
|
|
1599
|
+
* ```ts
|
|
1600
|
+
* import { Schema } from 'effect'
|
|
1601
|
+
*
|
|
1602
|
+
* export const users = table('users', {
|
|
1603
|
+
* id: id(),
|
|
1604
|
+
* email: text(),
|
|
1605
|
+
* }).validate(Schema.Struct({
|
|
1606
|
+
* email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+$/)),
|
|
1607
|
+
* }))
|
|
1608
|
+
* ```
|
|
1609
|
+
*
|
|
1610
|
+
* The schema only needs to describe the fields you care about —
|
|
1611
|
+
* the framework merges defaults + computed BEFORE validation, so
|
|
1612
|
+
* `id` and `createdAt` are already filled in by the time the
|
|
1613
|
+
* decoder sees the row.
|
|
1614
|
+
*/
|
|
1615
|
+
validate: <S extends Schema.Schema.Any>(schema: S) => Table<Name, Fields, Reactive, IxNames>;
|
|
1616
|
+
/**
|
|
1617
|
+
* Attach an effect/Schema that validates UPDATE patches against the
|
|
1618
|
+
* table. The framework wraps the schema in `Schema.partial`, so a
|
|
1619
|
+
* patch that only touches some columns is valid — every field that
|
|
1620
|
+
* IS present must satisfy its rule. The MutationStore decodes the
|
|
1621
|
+
* (stamped) patch right before the UPDATE; failure throws the same
|
|
1622
|
+
* typed `TableValidationFailed` error `.validate(...)` raises on
|
|
1623
|
+
* insert.
|
|
1624
|
+
*
|
|
1625
|
+
* Example:
|
|
1626
|
+
* ```ts
|
|
1627
|
+
* import { Schema } from 'effect'
|
|
1628
|
+
*
|
|
1629
|
+
* export const users = table('users', {
|
|
1630
|
+
* id: id(),
|
|
1631
|
+
* email: text(),
|
|
1632
|
+
* age: integer(),
|
|
1633
|
+
* })
|
|
1634
|
+
* .validate(Schema.Struct({
|
|
1635
|
+
* email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+$/)),
|
|
1636
|
+
* }))
|
|
1637
|
+
* .validatePatch(Schema.Struct({
|
|
1638
|
+
* age: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)),
|
|
1639
|
+
* }))
|
|
1640
|
+
* ```
|
|
1641
|
+
*
|
|
1642
|
+
* Pass the same shape you'd pass to `.validate(...)` — the partial
|
|
1643
|
+
* wrapping is applied for you, so don't pre-wrap fields in
|
|
1644
|
+
* `Schema.optional`.
|
|
1645
|
+
*/
|
|
1646
|
+
validatePatch: <S extends Schema.Schema.Any>(schema: S) => Table<Name, Fields, Reactive, IxNames>;
|
|
1647
|
+
/**
|
|
1648
|
+
* Declare a (possibly composite) secondary index on the table.
|
|
1649
|
+
*
|
|
1650
|
+
* The single surface for ALL indexes — column-level `.index()` does
|
|
1651
|
+
* not exist, on purpose. One pattern, no footgun. Two signatures:
|
|
1652
|
+
*
|
|
1653
|
+
* 1. **Auto-named** (most cases): pass just the field list.
|
|
1654
|
+
*
|
|
1655
|
+
* ```ts
|
|
1656
|
+
* table('apps', { id, projectId, ... })
|
|
1657
|
+
* .index(['projectId']) // → "apps_projectId_idx"
|
|
1658
|
+
* .index(['projectId', 'kind']) // → "apps_projectId_kind_idx"
|
|
1659
|
+
* ```
|
|
1660
|
+
*
|
|
1661
|
+
* The auto-generated name surfaces in Postgres EXPLAIN output but
|
|
1662
|
+
* is NOT typed-addressable via `.using(...)` — when you DON'T
|
|
1663
|
+
* care about hot-path observability, auto-naming keeps the
|
|
1664
|
+
* schema file short.
|
|
1665
|
+
*
|
|
1666
|
+
* 2. **Explicit name** (for hot paths): pass a literal name.
|
|
1667
|
+
*
|
|
1668
|
+
* ```ts
|
|
1669
|
+
* table('org_memberships', { ... })
|
|
1670
|
+
* .index('byUserOrg', ['userId', 'orgId'])
|
|
1671
|
+
* .index('byOrgRole', ['orgId', 'role'])
|
|
1672
|
+
* ```
|
|
1673
|
+
*
|
|
1674
|
+
* The literal `IxName` is added to the table's `IxNames` union,
|
|
1675
|
+
* so `database.orgMemberships.using('byUserOrg')` autocompletes
|
|
1676
|
+
* against `'byUserOrg' | 'byOrgRole'` and rejects typos at
|
|
1677
|
+
* compile time. The literal also surfaces verbatim in EXPLAIN
|
|
1678
|
+
* output and DevTools — the right call for any access pattern
|
|
1679
|
+
* you actually care about debugging at scale.
|
|
1680
|
+
*
|
|
1681
|
+
* Both forms accept 1+ field names, type-checked against the row.
|
|
1682
|
+
* Names must be unique within the table; collisions throw at
|
|
1683
|
+
* declaration time so the error site is the schema file, not the
|
|
1684
|
+
* migration runner.
|
|
1685
|
+
*/
|
|
1686
|
+
index: {
|
|
1687
|
+
<const F extends readonly [keyof Fields & string, ...Array<keyof Fields & string>]>(fields: F, options?: {
|
|
1688
|
+
where?: string;
|
|
1689
|
+
kind?: IndexKind;
|
|
1690
|
+
kindOptions?: IndexKindOptions;
|
|
1691
|
+
}): Table<Name, Fields, Reactive, IxNames>;
|
|
1692
|
+
<const IxName extends string, const F extends readonly [keyof Fields & string, ...Array<keyof Fields & string>]>(name: IxName, fields: F, options?: {
|
|
1693
|
+
where?: string;
|
|
1694
|
+
kind?: IndexKind;
|
|
1695
|
+
kindOptions?: IndexKindOptions;
|
|
1696
|
+
}): Table<Name, Fields, Reactive, IxNames | IxName>;
|
|
1697
|
+
};
|
|
1698
|
+
/**
|
|
1699
|
+
* Declare an EXPRESSION-aware index — entries can be column names
|
|
1700
|
+
* (string) OR expression objects (`{ expr: 'lower("email")' }`).
|
|
1701
|
+
* Use this when you need an index on `lower("email")`,
|
|
1702
|
+
* `date_trunc('month', "createdAt")`, or any other server-side
|
|
1703
|
+
* expression. Plain-column indexes should use `.index(...)` so the
|
|
1704
|
+
* compiler keeps catching typos.
|
|
1705
|
+
*
|
|
1706
|
+
* ```ts
|
|
1707
|
+
* .expressionIndex('byEmailCi', [{ expr: 'lower("email")' }])
|
|
1708
|
+
* .expressionIndex('byOrgMonth', ['orgId', { expr: "date_trunc('month', \"createdAt\")" }])
|
|
1709
|
+
* ```
|
|
1710
|
+
*
|
|
1711
|
+
* `options.where` makes it a partial expression index (postgres /
|
|
1712
|
+
* sqlite / mssql); the underlying machinery is shared with
|
|
1713
|
+
* `.index(...)`'s where option.
|
|
1714
|
+
*/
|
|
1715
|
+
expressionIndex: <const IxName extends string>(name: IxName, fields: ReadonlyArray<IndexField>, options?: {
|
|
1716
|
+
where?: string;
|
|
1717
|
+
kind?: IndexKind;
|
|
1718
|
+
kindOptions?: IndexKindOptions;
|
|
1719
|
+
}) => Table<Name, Fields, Reactive, IxNames | IxName>;
|
|
1720
|
+
/**
|
|
1721
|
+
* Declare a (possibly composite) UNIQUE constraint at the table
|
|
1722
|
+
* level. Enforces uniqueness across one or more columns — the DB
|
|
1723
|
+
* rejects conflicting writes with a `PrimaryKeyConflictError`
|
|
1724
|
+
* (mapped from each dialect's native conflict code).
|
|
1725
|
+
*
|
|
1726
|
+
* Two signatures parallel `.index(...)`:
|
|
1727
|
+
*
|
|
1728
|
+
* 1. **Auto-named** (`uq` suffix to disambiguate from indexes):
|
|
1729
|
+
* ```ts
|
|
1730
|
+
* table('org_slugs', { id, orgId, slug })
|
|
1731
|
+
* .unique(['orgId', 'slug']) // → "org_slugs_orgId_slug_uq"
|
|
1732
|
+
* ```
|
|
1733
|
+
*
|
|
1734
|
+
* 2. **Explicit name**:
|
|
1735
|
+
* ```ts
|
|
1736
|
+
* .unique('byOrgSlug', ['orgId', 'slug'])
|
|
1737
|
+
* ```
|
|
1738
|
+
*
|
|
1739
|
+
* Optional third arg passes a dedup policy for the migration
|
|
1740
|
+
* planner's needs-backfill path:
|
|
1741
|
+
* ```ts
|
|
1742
|
+
* .unique('byOrgSlug', ['orgId', 'slug'], { dedup: 'suffix-counter' })
|
|
1743
|
+
* ```
|
|
1744
|
+
*
|
|
1745
|
+
* Distinct from the column-level `text().unique()` modifier —
|
|
1746
|
+
* that's single-column and lives ON the column. Composite
|
|
1747
|
+
* uniqueness MUST be declared at the table level.
|
|
1748
|
+
*/
|
|
1749
|
+
unique: {
|
|
1750
|
+
<const F extends readonly [keyof Fields & string, ...Array<keyof Fields & string>]>(fields: F, options?: {
|
|
1751
|
+
dedup?: TableUnique['dedup'];
|
|
1752
|
+
}): Table<Name, Fields, Reactive, IxNames>;
|
|
1753
|
+
<const UqName extends string, const F extends readonly [keyof Fields & string, ...Array<keyof Fields & string>]>(name: UqName, fields: F, options?: {
|
|
1754
|
+
dedup?: TableUnique['dedup'];
|
|
1755
|
+
}): Table<Name, Fields, Reactive, IxNames>;
|
|
1756
|
+
};
|
|
1757
|
+
/**
|
|
1758
|
+
* Declare a named table-level `CHECK` constraint — a DB-ENFORCED
|
|
1759
|
+
* invariant that holds regardless of which client writes the row
|
|
1760
|
+
* (defense-in-depth beyond app-level `.validate(Schema)`). Use for
|
|
1761
|
+
* cross-column rules (`"startsAt" < "endsAt"`) or single-column bounds
|
|
1762
|
+
* you want the database itself to guarantee.
|
|
1763
|
+
*
|
|
1764
|
+
* `expr` is RAW SQL — you own its cross-dialect portability. Stick to
|
|
1765
|
+
* unquoted column names + standard operators; avoid dialect-specific
|
|
1766
|
+
* functions / regex (those belong in `.validate(Schema)`). `CHECK` is
|
|
1767
|
+
* enforced on Postgres / MySQL 8+ / MariaDB / MSSQL / SQLite.
|
|
1768
|
+
*
|
|
1769
|
+
* ```ts
|
|
1770
|
+
* table('bookings', { startsAt: timestamp(), endsAt: timestamp(), seats: integer() })
|
|
1771
|
+
* .check('booking_window', 'startsAt < endsAt')
|
|
1772
|
+
* .check('seats_positive', 'seats > 0')
|
|
1773
|
+
* ```
|
|
1774
|
+
*
|
|
1775
|
+
* For a single-column check, `column.check(expr)` is the terser form.
|
|
1776
|
+
*/
|
|
1777
|
+
check: <const CkName extends string>(name: CkName, expr: string) => Table<Name, Fields, Reactive, IxNames>;
|
|
1778
|
+
/**
|
|
1779
|
+
* Declare a full-text-search index (S7). One method, three back-
|
|
1780
|
+
* ends:
|
|
1781
|
+
*
|
|
1782
|
+
* - **Postgres**: emits a STORED tsvector column derived from the
|
|
1783
|
+
* listed columns + a GIN index. Search via `.matching(name,
|
|
1784
|
+
* query)` compiles to `<col>_tsv @@ plainto_tsquery(...)`.
|
|
1785
|
+
* - **MySQL / MariaDB**: emits a native `FULLTEXT INDEX`; search
|
|
1786
|
+
* compiles to `MATCH(col1, col2) AGAINST(? IN NATURAL LANGUAGE MODE)`.
|
|
1787
|
+
* - **MSSQL / SQLite**: no native catalog auto-creation — falls
|
|
1788
|
+
* back to `LIKE '%query%'` across each column. Warns at boot
|
|
1789
|
+
* so the operator knows the engine isn't optimal.
|
|
1790
|
+
*
|
|
1791
|
+
* `config` (default 'english') sets the tsvector language config
|
|
1792
|
+
* on postgres; ignored elsewhere. `weights` is per-column tsvector
|
|
1793
|
+
* weight letters ('A' most-significant — 'D' least); ignored
|
|
1794
|
+
* elsewhere.
|
|
1795
|
+
*
|
|
1796
|
+
* ```ts
|
|
1797
|
+
* posts = table('posts', {
|
|
1798
|
+
* id: id(),
|
|
1799
|
+
* title: text(),
|
|
1800
|
+
* body: text(),
|
|
1801
|
+
* }).fullTextIndex('postSearch', ['title', 'body'], {
|
|
1802
|
+
* config: 'english',
|
|
1803
|
+
* weights: { title: 'A', body: 'B' },
|
|
1804
|
+
* })
|
|
1805
|
+
* ```
|
|
1806
|
+
*/
|
|
1807
|
+
fullTextIndex: <const FtName extends string>(name: FtName, columns: ReadonlyArray<keyof Fields & string>, options?: {
|
|
1808
|
+
config?: string;
|
|
1809
|
+
weights?: Record<string, 'A' | 'B' | 'C' | 'D'>;
|
|
1810
|
+
}) => Table<Name, Fields, Reactive, IxNames>;
|
|
1811
|
+
/**
|
|
1812
|
+
* Declare a COMPOSITE PRIMARY KEY across two or more columns. Replaces the
|
|
1813
|
+
* default single-column `id()` primary key: the listed columns become the
|
|
1814
|
+
* table's PK, emitted as a table-level `PRIMARY KEY (a, b)` constraint on
|
|
1815
|
+
* every dialect (no column carries an inline `PRIMARY KEY`).
|
|
1816
|
+
*
|
|
1817
|
+
* Use for natural composite keys — a join table keyed on both its FKs, a
|
|
1818
|
+
* time-series table keyed on `(deviceId, ts)`, a per-tenant sequence keyed
|
|
1819
|
+
* on `(tenantId, seq)`.
|
|
1820
|
+
*
|
|
1821
|
+
* ```ts
|
|
1822
|
+
* table('memberships', {
|
|
1823
|
+
* userId: reference(() => users),
|
|
1824
|
+
* orgId: reference(() => orgs),
|
|
1825
|
+
* role: text(),
|
|
1826
|
+
* }).primaryKey(['userId', 'orgId'])
|
|
1827
|
+
* // → PRIMARY KEY ("userId", "orgId")
|
|
1828
|
+
* ```
|
|
1829
|
+
*
|
|
1830
|
+
* The columns are type-checked against the row. A composite PK implies
|
|
1831
|
+
* NOT NULL on each member column (the DB enforces it). Don't also declare an
|
|
1832
|
+
* `id()` column — the composite key is THE key; a stray `id()` would emit a
|
|
1833
|
+
* second inline PK and the DDL would be rejected.
|
|
1834
|
+
*/
|
|
1835
|
+
primaryKey: <const F extends readonly [keyof Fields & string, keyof Fields & string, ...Array<keyof Fields & string>]>(fields: F) => Table<Name, Fields, Reactive, IxNames>;
|
|
1836
|
+
/**
|
|
1837
|
+
* Opt the table into the reactive engine. Applies
|
|
1838
|
+
* `alter table <name> replica identity full` at migration time so the WAL
|
|
1839
|
+
* stream carries full pre-images on UPDATE/DELETE. See the schema-DSL plan.
|
|
1840
|
+
*/
|
|
1841
|
+
reactive: () => Table<Name, Fields, true, IxNames>;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/**
|
|
1845
|
+
* A named table-level `CHECK` constraint, set by `.check(name, expr)`.
|
|
1846
|
+
* `expr` is a raw SQL boolean expression — the developer owns its
|
|
1847
|
+
* cross-dialect portability (use unbounded column names + standard
|
|
1848
|
+
* operators; avoid dialect-specific functions / regex). Column-level
|
|
1849
|
+
* checks (`text().check(expr)`) carry their expr on the ColumnDefinition
|
|
1850
|
+
* instead and emit an inline `CHECK`.
|
|
1851
|
+
*/
|
|
1852
|
+
declare interface TableCheck {
|
|
1853
|
+
readonly name: string;
|
|
1854
|
+
readonly expr: string;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* A composite UNIQUE constraint declared at the table level via
|
|
1859
|
+
* `.unique([...])` / `.unique('name', [...])`. Distinct from the
|
|
1860
|
+
* column-level `.unique()` modifier (which only covers a single
|
|
1861
|
+
* column) and distinct from `.index([...])` (which doesn't enforce
|
|
1862
|
+
* uniqueness).
|
|
1863
|
+
*
|
|
1864
|
+
* Emits a real DB-level UNIQUE CONSTRAINT inside CREATE TABLE — the
|
|
1865
|
+
* DB rejects conflicting writes, not the application. That's the
|
|
1866
|
+
* whole point: a multi-tenant `(orgId, slug)` constraint can't be
|
|
1867
|
+
* enforced reliably at the app layer (race conditions), but the DB
|
|
1868
|
+
* gives you a hard guarantee + a hard error.
|
|
1869
|
+
*
|
|
1870
|
+
* `dedup` mirrors the single-column unique-with-dedup story: when
|
|
1871
|
+
* the planner classifies an add-composite-unique on a populated
|
|
1872
|
+
* table that already has duplicates, the annotation tells the
|
|
1873
|
+
* applier how to resolve them. `'fail'` (default) refuses; `'suffix-
|
|
1874
|
+
* counter'` appends `-2`/`-3`/... to the LAST field; a `sql`-fragment
|
|
1875
|
+
* runs custom SQL.
|
|
1876
|
+
*/
|
|
1877
|
+
/**
|
|
1878
|
+
* Full-text index declaration (S7). Postgres builds a tsvector
|
|
1879
|
+
* generated column + GIN index; MySQL / MariaDB get a native
|
|
1880
|
+
* `FULLTEXT INDEX`; MSSQL / SQLite fall back to a per-column LIKE
|
|
1881
|
+
* scan (no native FTS catalog setup attempted — that's a DBA task).
|
|
1882
|
+
*/
|
|
1883
|
+
declare interface TableFullTextIndex {
|
|
1884
|
+
readonly name: string;
|
|
1885
|
+
readonly columns: ReadonlyArray<string>;
|
|
1886
|
+
/**
|
|
1887
|
+
* Postgres tsvector language config (default 'english') and per-
|
|
1888
|
+
* column weight letters ('A'..'D'). Both ignored on other dialects.
|
|
1889
|
+
*/
|
|
1890
|
+
readonly config?: string;
|
|
1891
|
+
readonly weights?: Readonly<Record<string, 'A' | 'B' | 'C' | 'D'>>;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
declare interface TableIndex {
|
|
1895
|
+
readonly name: string;
|
|
1896
|
+
readonly fields: ReadonlyArray<IndexField>;
|
|
1897
|
+
/**
|
|
1898
|
+
* Optional partial-index predicate emitted as a `WHERE` clause.
|
|
1899
|
+
* Use cases: index only non-soft-deleted rows, only active users,
|
|
1900
|
+
* only pending orders.
|
|
1901
|
+
*
|
|
1902
|
+
* Postgres / SQLite / MSSQL ("filtered index") support this
|
|
1903
|
+
* natively. MySQL + MariaDB don't — the DDL emitter falls back to
|
|
1904
|
+
* a full index on those dialects and logs a warning at boot so the
|
|
1905
|
+
* caller knows the predicate was dropped.
|
|
1906
|
+
*
|
|
1907
|
+
* Emitted verbatim (caller-quoted), same conventions as the
|
|
1908
|
+
* expression-field form.
|
|
1909
|
+
*/
|
|
1910
|
+
readonly where?: string;
|
|
1911
|
+
/**
|
|
1912
|
+
* Index access method. Default `'btree'` on every dialect. See
|
|
1913
|
+
* {@link IndexKind} for the per-kind semantics + the per-dialect
|
|
1914
|
+
* fallback matrix (postgres `gist`/`gin`/`brin`/`hnsw`; warn+btree or
|
|
1915
|
+
* skip+warn elsewhere).
|
|
1916
|
+
*/
|
|
1917
|
+
readonly kind?: IndexKind;
|
|
1918
|
+
/**
|
|
1919
|
+
* Per-kind tuning knobs. Today only `hnsw` (`kindOptions.hnsw.{m,
|
|
1920
|
+
* efConstruction, opclass}`). Ignored for kinds that don't read it.
|
|
1921
|
+
*/
|
|
1922
|
+
readonly kindOptions?: IndexKindOptions;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
declare interface TableLike {
|
|
1926
|
+
readonly tableName: string;
|
|
1927
|
+
readonly fields: Record<string, ColumnDefinition<unknown>>;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
export declare interface TableSnapshot {
|
|
1931
|
+
readonly name: string;
|
|
1932
|
+
readonly columns: ReadonlyArray<ColumnSnapshot>;
|
|
1933
|
+
readonly indexes: ReadonlyArray<IndexSnapshot>;
|
|
1934
|
+
/**
|
|
1935
|
+
* Composite PRIMARY KEY column set (`.primaryKey(['a','b'])`). Set on the
|
|
1936
|
+
* DECLARED side only — it drives the `PRIMARY KEY (a, b)` line in the
|
|
1937
|
+
* applier's create-table emission. Absent (the default) means the single
|
|
1938
|
+
* `id()` column carries the PK inline. Introspection leaves it undefined;
|
|
1939
|
+
* the live PK is compared via the synthesised `<table>_pkey` index.
|
|
1940
|
+
*/
|
|
1941
|
+
readonly primaryKey?: ReadonlyArray<string>;
|
|
1942
|
+
/**
|
|
1943
|
+
* Approximate row count from the live DB's planner stats — used by
|
|
1944
|
+
* the migration planner to promote `safe` ops to `online-required`
|
|
1945
|
+
* when the table exceeds the configured threshold (default 50_000).
|
|
1946
|
+
* Pulled from `pg_class.reltuples` on postgres; equivalent stats
|
|
1947
|
+
* on other dialects. Absent on the declared side of the diff (no
|
|
1948
|
+
* data exists yet), present from introspection.
|
|
1949
|
+
*/
|
|
1950
|
+
readonly rowCount?: number;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
declare interface TableUnique {
|
|
1954
|
+
readonly name: string;
|
|
1955
|
+
readonly fields: ReadonlyArray<string>;
|
|
1956
|
+
readonly dedup?: 'fail' | 'suffix-counter' | {
|
|
1957
|
+
sql: string;
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
/**
|
|
1962
|
+
* Unique-constraint metadata. Set by `.unique()` (or `.unique({ dedup })`).
|
|
1963
|
+
*
|
|
1964
|
+
* The planner reads `dedup` when ADD UNIQUE is applied to a populated
|
|
1965
|
+
* column — without it, the constraint fails for any existing duplicates.
|
|
1966
|
+
*
|
|
1967
|
+
* - `'fail'` — refuse to migrate; surface offending rows in the plan
|
|
1968
|
+
* output (the safe default).
|
|
1969
|
+
* - `'suffix-counter'` — auto-rename duplicates `value`, `value-2`,
|
|
1970
|
+
* `value-3`, … (only sensible for text columns).
|
|
1971
|
+
* - `sql\`...\`` — custom UPDATE expression keyed off `id` /
|
|
1972
|
+
* ROW_NUMBER() to disambiguate.
|
|
1973
|
+
*/
|
|
1974
|
+
declare interface UniqueSpec {
|
|
1975
|
+
readonly dedup?: 'fail' | 'suffix-counter' | Statement.Fragment;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
/**
|
|
1979
|
+
* Vector distance metric. Drives the HNSW opclass at index-build time and
|
|
1980
|
+
* the distance operator at query time — the two MUST agree or the index
|
|
1981
|
+
* can't accelerate the search.
|
|
1982
|
+
* - `'cosine'` — 1 − cosine similarity (pgvector `<=>`)
|
|
1983
|
+
* - `'l2'` — Euclidean distance (pgvector `<->`)
|
|
1984
|
+
* - `'inner'` — negative inner product (pgvector `<#>`)
|
|
1985
|
+
*/
|
|
1986
|
+
declare type VectorDistance = 'cosine' | 'l2' | 'inner';
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* A read-only SQL view descriptor. Structurally a `TableLike` (carries
|
|
1990
|
+
* `tableName` + `fields`) so the query builder + row decoder + by-name
|
|
1991
|
+
* registry treat it uniformly, PLUS `isView: true` and the `viewSelect`
|
|
1992
|
+
* SQL body the migrator emits. There is no mutation surface — a view is
|
|
1993
|
+
* queryable, never writable.
|
|
1994
|
+
*/
|
|
1995
|
+
declare interface View<Name extends string, Fields extends Record<string, ColumnDefinition<unknown>>> extends TableLike {
|
|
1996
|
+
readonly tableName: Name;
|
|
1997
|
+
readonly fields: Fields;
|
|
1998
|
+
/** Marks this descriptor as a VIEW so the migrator + store read path can
|
|
1999
|
+
* distinguish it from a base table. */
|
|
2000
|
+
readonly isView: true;
|
|
2001
|
+
/** The raw SELECT body emitted verbatim into `CREATE VIEW <name> AS …`.
|
|
2002
|
+
* The author owns its cross-dialect portability (same contract as a
|
|
2003
|
+
* `raw()` column or an `expressionIndex` expression). */
|
|
2004
|
+
readonly viewSelect: string;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* A stable 64-bit integer derived from `voltro_migration_lock`. Postgres
|
|
2009
|
+
* `pg_advisory_lock(bigint)` wants a number; we hard-code a constant
|
|
2010
|
+
* inside the safe-integer range so every consumer races for the same
|
|
2011
|
+
* lock without doing string hashing at runtime.
|
|
2012
|
+
*/
|
|
2013
|
+
export declare const VOLTRO_MIGRATION_LOCK_KEY = 6322741009312437n;
|
|
2014
|
+
|
|
2015
|
+
/**
|
|
2016
|
+
* Convenience wrapper: acquire → run `work` → release. Releases even
|
|
2017
|
+
* on failure via `Effect.ensuring`, but a process crash mid-work
|
|
2018
|
+
* leaves the postgres lock held until that connection closes (which
|
|
2019
|
+
* the pooled SqlClient does on Effect scope cleanup).
|
|
2020
|
+
*/
|
|
2021
|
+
export declare const withMigrationLock: <A, E, R = never>(sql: SqlClient.SqlClient, work: Effect.Effect<A, E, R>) => Effect.Effect<A, E | SqlError_2, R>;
|
|
2022
|
+
|
|
2023
|
+
export { }
|