@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/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 { }