@voltro/database 0.81.0 → 0.83.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 CHANGED
@@ -1,13 +1,18 @@
1
1
  import { ConfigError } from 'effect';
2
2
  import { Effect } from 'effect';
3
+ import { Fragment } from '@effect/sql/Statement';
3
4
  import { Layer } from 'effect';
4
5
  import { Schema } from 'effect';
5
6
  import { SqlClient } from '@effect/sql';
6
- import { SqlError } from '@effect/sql';
7
- import { SqlError as SqlError_2 } from '@effect/sql/SqlError';
7
+ import { SqlError } from '@effect/sql/SqlError';
8
+ import { SqlError as SqlError_2 } from '@effect/sql';
8
9
  import { Statement } from '@effect/sql';
9
10
 
10
- export declare const acquireMigrationLock: (sql: SqlClient.SqlClient, scope?: MigrationLockScope) => Effect.Effect<HeldMigrationLock, SqlError_2>;
11
+ /** Every hash this source may have been recorded under: its own, and the body
12
+ * before each framework edit in {@link FRAMEWORK_MIGRATION_EDITS}. */
13
+ export declare const acceptedBodyHashes: (source: string) => ReadonlySet<string>;
14
+
15
+ export declare const acquireMigrationLock: (sql: SqlClient.SqlClient, scope?: MigrationLockScope) => Effect.Effect<HeldMigrationLock, SqlError>;
11
16
 
12
17
  export declare const addColumnDdl: (op: Extract<MigrationOperation, {
13
18
  kind: "add-column";
@@ -161,7 +166,36 @@ declare type AnyView = View<string, Record<string, ColumnDefinition<unknown>>>;
161
166
  */
162
167
  export declare const appliedAtValue: (sql: SqlClient.SqlClient, iso: string) => Effect.Effect<string>;
163
168
 
164
- export declare const appliedFileMigrationIds: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<string>, SqlError_2>;
169
+ /**
170
+ * Does the plan ledger exist on THIS database? Asked of the CATALOG, which
171
+ * ANSWERS on a virgin database rather than failing.
172
+ *
173
+ * The read below used to be a plain `SELECT` with `Effect.catchAll(() => [])`
174
+ * beside it, and that is correct only OUTSIDE a transaction — the rule
175
+ * `objectReadable` states in `applier.ts`, in those words, about its own query.
176
+ * `voltro db apply` opens ONE transaction on the plan-transactional dialects,
177
+ * so against a database without the ledger the failed SELECT aborted that
178
+ * transaction and every later statement of the deploy died — starting with the
179
+ * first migration's own `SAVEPOINT`:
180
+ *
181
+ * ERROR: relation "_voltro_migration_plans" does not exist
182
+ * ERROR: current transaction is aborted, commands ignored until end of
183
+ * transaction block
184
+ *
185
+ * The catch turned an error postgres does not forget into one only Effect had
186
+ * forgotten. So a project with even ONE file under `migrations/` could not
187
+ * `db apply` against an empty database at all — every new environment, every
188
+ * reset local database, every hermetic e2e stack — and the schema was never
189
+ * created.
190
+ *
191
+ * The fix is NOT to create the ledger first. Both apply paths used to open with
192
+ * `applySchema(voltroFrameworkTables)`, and it was removed for a reason the
193
+ * header of `runApplyFromPlanFile` records: on a virgin database it created
194
+ * tables and thereby moved the very fingerprint the saved plan was measured
195
+ * against. The ledger is planned like any other declared table; what had to
196
+ * change is that ASKING for it stops being a statement that can fail.
197
+ */
198
+ export declare const appliedFileMigrationIds: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<string>, SqlError>;
165
199
 
166
200
  /**
167
201
  * The applied file migrations WITH the body hash recorded at the time.
@@ -175,7 +209,7 @@ export declare const appliedFileMigrationIds: (sql: SqlClient.SqlClient) => Effe
175
209
  export declare const appliedFileMigrations: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<{
176
210
  readonly id: string;
177
211
  readonly hash: string;
178
- }>, SqlError_2>;
212
+ }>, SqlError>;
179
213
 
180
214
  /**
181
215
  * The operations the resume ledger says RAN — `started`, `added` or `applied` —
@@ -245,9 +279,14 @@ export declare const applyNamespacedSchema: (tables: ReadonlyArray<AnyTable>, na
245
279
  * interruption, which is exactly the set a JS `finally` inside `Effect.gen`
246
280
  * does not cover.
247
281
  */
248
- export declare const applyPlan: (sql: SqlClient.SqlClient, plan: MigrationPlan, ctx: ApplyPlanCtx) => Effect.Effect<AppliedMigration, SqlError_2, SqlClient.SqlClient>;
282
+ export declare const applyPlan: (sql: SqlClient.SqlClient, plan: MigrationPlan, ctx: ApplyPlanCtx) => Effect.Effect<AppliedMigration, SqlError, SqlClient.SqlClient>;
249
283
 
250
284
  export declare interface ApplyPlanCtx {
285
+ /** Prepare schema-owned resources under the migration lock after replan,
286
+ * refusal checks and interrupted-run reconciliation, before planned work.
287
+ * Not called when no operations remain. Ledger/extension bootstrap may
288
+ * already have run; this is not a dry-run boundary or its own transaction. */
289
+ readonly beforeExecute?: Effect.Effect<void, SqlError, SqlClient.SqlClient>;
251
290
  /** Tables declared by user code — needed to look up
252
291
  * `.backfill()` specs for add-column ops, since the snapshot
253
292
  * doesn't carry them. */
@@ -319,7 +358,7 @@ export declare interface ApplyPlanCtx {
319
358
  * migration was a framework bug. `voltro db apply` did exactly that for two
320
359
  * releases while `voltro dev`'s boot diff, which uses this parameter, did not.
321
360
  */
322
- readonly replan: (sql: SqlClient.SqlClient) => Effect.Effect<MigrationPlan, SqlError_2, SqlClient.SqlClient>;
361
+ readonly replan: (sql: SqlClient.SqlClient) => Effect.Effect<MigrationPlan, SqlError, SqlClient.SqlClient>;
323
362
  /**
324
363
  * The declared store charset — the one `replan` plans with. Here only for
325
364
  * the resume ledger this creates before the first statement: nothing declares
@@ -342,7 +381,7 @@ cdcChannel?: string, options?: SchemaApplyOptions) => Effect.Effect<void, never,
342
381
  * `.unprepared`, the text protocol, like every DDL statement a prepared
343
382
  * protocol may refuse on one of the two engines.
344
383
  */
345
- export declare const applyUndeclaredCharsetWork: (sql: SqlClient.SqlClient, work: UndeclaredCharsetWork) => Effect.Effect<void, SqlError_2>;
384
+ export declare const applyUndeclaredCharsetWork: (sql: SqlClient.SqlClient, work: UndeclaredCharsetWork) => Effect.Effect<void, SqlError>;
346
385
 
347
386
  /**
348
387
  * The OPT-IN refuse gate over the advisory. By default (`VOLTRO_ROLLING_DEPLOY`
@@ -544,6 +583,9 @@ declare type ChangeEvent = {
544
583
  * re-read failed, or the row was already gone). Re-read or resync.
545
584
  */
546
585
  readonly oversized?: 'rehydrated' | 'tombstone' | 'unrecovered';
586
+ /** A net-change feed may coalesce intermediate writes and supply current
587
+ * images or delete keys only. It cannot support exact historical replay. */
588
+ readonly netChange?: true;
547
589
  /**
548
590
  * How the event reached this process. Absent/'inline' = emitted by this
549
591
  * process's own write path; 'injected' = delivered over a cross-instance
@@ -634,9 +676,13 @@ export declare const charsetColumnsOf: (table: TableSnapshot) => ReadonlyArray<{
634
676
  * Without `mayWrite` nothing is created or written: a missing table or row is
635
677
  * `unclaimed`, and only an existing FOREIGN claim is reported.
636
678
  */
637
- export declare const checkSchemaOwner: (sql: SqlClient.SqlClient, claim: SchemaOwnerClaim, options: {
679
+ export declare const checkSchemaOwner: (sql: SqlClient.SqlClient, claim: SchemaOwnerClaim,
680
+ /** `charset`: the declared store charset (MySQL family) the owner table is
681
+ * created in — the same one every other framework table gets. */
682
+ options: {
638
683
  readonly mayWrite: boolean;
639
- }) => Effect.Effect<SchemaOwnership, SqlError_2>;
684
+ readonly charset: MysqlCharset | undefined;
685
+ }) => Effect.Effect<SchemaOwnership, SqlError>;
640
686
 
641
687
  /** Partition into ≤`n`-sized chunks (disjoint, complete). Exported for the
642
688
  * batching regression test — the introspection's correctness reduces to this
@@ -1553,7 +1599,7 @@ export declare type ColumnInput = {
1553
1599
  * `drop-check` still removes a CHECK the declaration no longer has: it runs
1554
1600
  * after the column changes and reads the catalog again.
1555
1601
  */
1556
- export declare const columnLevelChecksMysql: (sql: SqlClient.SqlClient, table: string) => Effect.Effect<ReadonlyMap<string, string>, SqlError_2>;
1602
+ export declare const columnLevelChecksMysql: (sql: SqlClient.SqlClient, table: string) => Effect.Effect<ReadonlyMap<string, string>, SqlError>;
1557
1603
 
1558
1604
  /**
1559
1605
  * A column as seen by the planner: either declared in code (extracted
@@ -1824,7 +1870,7 @@ export declare const convergeUndeclaredCharset: (sql: SqlClient.SqlClient, optio
1824
1870
  readonly declaredNames: ReadonlySet<string>;
1825
1871
  readonly charset: MysqlCharset | undefined;
1826
1872
  readonly lockSchema?: string | undefined;
1827
- }) => Effect.Effect<UndeclaredCharsetWork, SqlError_2>;
1873
+ }) => Effect.Effect<UndeclaredCharsetWork, SqlError>;
1828
1874
 
1829
1875
  /** A live foreign key a charset conversion has to carry across the rewrite. */
1830
1876
  export declare interface ConvertCharsetForeignKey {
@@ -2431,8 +2477,8 @@ export declare const defaultJsonClause: (value: object, dialect: DialectId) => s
2431
2477
  export declare interface DeferredData {
2432
2478
  readonly table: string;
2433
2479
  readonly op: MigrationOperation;
2434
- readonly data: Effect.Effect<void, SqlError_2>;
2435
- readonly after: Effect.Effect<void, SqlError_2>;
2480
+ readonly data: Effect.Effect<void, SqlError>;
2481
+ readonly after: Effect.Effect<void, SqlError>;
2436
2482
  }
2437
2483
 
2438
2484
  export declare const DEPLOY_LEDGER_TABLE = "_voltro_deploys";
@@ -2452,6 +2498,9 @@ export declare const describeOutcome: (outcome: BootMigrationOutcome, dialectId:
2452
2498
  /** `bf_links`, `bf_links.ticket_key` — the spelling a message uses. */
2453
2499
  export declare const describeProbe: (probe: SchemaProbe) => string;
2454
2500
 
2501
+ /** A row whose `fingerprint` describes the schema — every newest-row reader filters by this. */
2502
+ export declare const describesSchema: (sql: SqlClient.SqlClient) => Fragment;
2503
+
2455
2504
  /** One line per finding, for the CLI and the doctor. */
2456
2505
  export declare const describeStoreCharsetFinding: (f: StoreCharsetFinding) => string;
2457
2506
 
@@ -2491,11 +2540,33 @@ export declare const detectReactiveTriggerDrift: (sql: SqlClient.SqlClient, tabl
2491
2540
  /** The app's `cdcChannel`. Part of the trigger NAME on any non-default
2492
2541
  * channel, so omitting it here reported every table as missing on a database
2493
2542
  * where every trigger was present. */
2494
- channel?: string) => Effect.Effect<ReactiveTriggerDrift, SqlError_2>;
2543
+ channel?: string) => Effect.Effect<ReactiveTriggerDrift, SqlError>;
2495
2544
 
2496
2545
  /** Dialect IDs the framework supports. */
2497
2546
  declare type DialectId = 'postgres' | 'mysql' | 'mariadb' | 'mssql' | 'sqlite' | 'turso';
2498
2547
 
2548
+ /**
2549
+ * Resolve the live dialect from the `SqlClient`.
2550
+ *
2551
+ * `sql.onDialectOrElse` collapses the six voltro dialects onto four driver
2552
+ * tokens: mysql+mariadb share `mysql` (one `@effect/sql-mysql2` client) and
2553
+ * sqlite+turso share `sqlite`. That collapse is fine for every consumer here —
2554
+ * the paired dialects behave identically for everything this module decides.
2555
+ *
2556
+ * Single copy on purpose: a second, subtly different dialect switch is how a
2557
+ * per-dialect divergence gets shipped in this codebase.
2558
+ */
2559
+ export declare const dialectOf: (sql: {
2560
+ readonly onDialectOrElse: <A, B, C, D, E, F>(options: {
2561
+ readonly orElse: () => A;
2562
+ readonly sqlite?: () => B;
2563
+ readonly pg?: () => C;
2564
+ readonly mysql?: () => D;
2565
+ readonly mssql?: () => E;
2566
+ readonly clickhouse?: () => F;
2567
+ }) => A | B | C | D | E | F;
2568
+ }) => DialectId;
2569
+
2499
2570
  /**
2500
2571
  * Output of `voltro db drift`: compares the live DB's introspected
2501
2572
  * schema against the last applied fingerprint in `_voltro_migrations`.
@@ -2811,7 +2882,19 @@ declare type EmptyMerge = unknown;
2811
2882
  export declare const engineFromVersion: (version: string) => MysqlEngine;
2812
2883
 
2813
2884
  /** The deploy ledger (`_voltro_deploys`) — same bootstrap, same reason: it has to exist before the first plan. */
2814
- export declare const ensureDeployLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId, charset?: MysqlCharset) => Effect.Effect<void, SqlError_2>;
2885
+ export declare const ensureDeployLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId, charset: MysqlCharset | undefined) => Effect.Effect<void, SqlError>;
2886
+
2887
+ /**
2888
+ * Make every declared full-text index exist, on a database the PLANNER
2889
+ * migrates. The planner diffs what it introspects — columns, indexes — and
2890
+ * deliberately skips the FTS artifacts (a generated tsvector column and its GIN
2891
+ * index, a FULLTEXT index, an FTS5 shadow table): they are framework plumbing
2892
+ * for a declaration, carry no data of their own, and their DDL is idempotent.
2893
+ * So they CONVERGE after the plan instead of being planned — without this, a
2894
+ * table the planner created never got its index, and `.matching()` failed on
2895
+ * the missing column. Returns how many statements ran.
2896
+ */
2897
+ export declare const ensureFullTextIndexes: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => Effect.Effect<number, SqlError_2.SqlError, SqlClient.SqlClient>;
2815
2898
 
2816
2899
  /**
2817
2900
  * Create `_voltro_migration_ops` if it isn't there yet.
@@ -2826,7 +2909,7 @@ export declare const ensureDeployLedgerTable: (sql: SqlClient.SqlClient, dialect
2826
2909
  * framework-table set because it has to exist before the FIRST plan — and the
2827
2910
  * first plan is the one that creates the framework tables.
2828
2911
  */
2829
- export declare const ensureLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId, charset?: MysqlCharset) => Effect.Effect<void, SqlError_2>;
2912
+ export declare const ensureLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId, charset: MysqlCharset | undefined) => Effect.Effect<void, SqlError>;
2830
2913
 
2831
2914
  /**
2832
2915
  * The PLAN ledger (`_voltro_migration_plans`) — same bootstrap, one caller.
@@ -2841,7 +2924,11 @@ export declare const ensureLedgerTable: (sql: SqlClient.SqlClient, dialect: Dial
2841
2924
  * Index-free like the others, so the planner still owns the table's shape and a
2842
2925
  * re-plan converges by adding the indexes it declares.
2843
2926
  */
2844
- export declare const ensurePlanLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId) => Effect.Effect<void, SqlError_2>;
2927
+ export declare const ensurePlanLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId, charset: MysqlCharset | undefined) => Effect.Effect<void, SqlError>;
2928
+
2929
+ /** The plugin-migration ledger (`_voltro_plugin_migrations`) — same bootstrap:
2930
+ * a plugin's steps run after the schema apply, outside any plan. */
2931
+ export declare const ensurePluginMigrationsLedgerTable: (sql: SqlClient.SqlClient, dialect: DialectId, charset: MysqlCharset | undefined) => Effect.Effect<void, SqlError>;
2845
2932
 
2846
2933
  /**
2847
2934
  * The in-progress marker (`_voltro_replace_in_progress`) — same bootstrap,
@@ -2857,11 +2944,11 @@ export declare const ensurePlanLedgerTable: (sql: SqlClient.SqlClient, dialect:
2857
2944
  * exits 1 over a database that is otherwise fine. The planner emits no trigger
2858
2945
  * for this table either, so the shape converges on the next apply.
2859
2946
  */
2860
- export declare const ensureReplaceMarkerTable: (sql: SqlClient.SqlClient, dialect: DialectId) => Effect.Effect<void, SqlError_2>;
2947
+ export declare const ensureReplaceMarkerTable: (sql: SqlClient.SqlClient, dialect: DialectId, charset: MysqlCharset | undefined) => Effect.Effect<void, SqlError>;
2861
2948
 
2862
2949
  /** The schema-owner row's table (`_voltro_schema_owner`) — bootstrapped so the
2863
2950
  * ownership check can run BEFORE the first plan, which is the plan it guards. */
2864
- export declare const ensureSchemaOwnerTable: (sql: SqlClient.SqlClient, dialect: DialectId) => Effect.Effect<void, SqlError_2>;
2951
+ export declare const ensureSchemaOwnerTable: (sql: SqlClient.SqlClient, dialect: DialectId, charset: MysqlCharset | undefined) => Effect.Effect<void, SqlError>;
2865
2952
 
2866
2953
  /** Exported for the same reason as {@link enumTypeBlocks}, and it MUST run
2867
2954
  * before it — see that function's note on ordering. */
@@ -2918,6 +3005,21 @@ export declare const failedExpectations: (expectations: ReadonlyArray<{
2918
3005
  readonly check: () => Promise<boolean> | Effect.Effect<boolean, unknown, never>;
2919
3006
  }>) => Effect.Effect<ReadonlyArray<string>, unknown>;
2920
3007
 
3008
+ /**
3009
+ * Look up the most recently applied plan's fingerprint. Returns
3010
+ * `undefined` if the table doesn't exist yet or no rows are present —
3011
+ * both meaning "the framework hasn't applied a planner-style migration
3012
+ * in this DB yet" (typical for fresh installs).
3013
+ *
3014
+ * A failed read is NOT one of those two unless the table is really absent.
3015
+ * This read used to end in `catchAll(() => undefined)`, and on SQL Server its
3016
+ * `LIMIT 1` was a syntax error: every production boot there reported
3017
+ * `live=<no _voltro_migration_plans row>` over a table holding the row, and
3018
+ * refused. So a failure is followed by a probe that is valid on every dialect;
3019
+ * if the table answers it, the original failure is the answer, loudly.
3020
+ */
3021
+ export declare const fetchLastFingerprint: (sql: SqlClient.SqlClient) => Effect.Effect<string | undefined, never>;
3022
+
2921
3023
  /**
2922
3024
  * Filename pattern the discovery walker matches: timestamp-prefixed
2923
3025
  * `.ts` files in any depth under the project's `migrations/` dir.
@@ -3223,6 +3325,49 @@ export declare const formatBranchRehearsal: (report: BranchRehearsalReport) => s
3223
3325
  /** Human report, or `undefined` when the database and the schema agree. */
3224
3326
  export declare const formatReactiveTriggerDrift: (drift: ReactiveTriggerDrift) => string | undefined;
3225
3327
 
3328
+ /**
3329
+ * Edits the FRAMEWORK made to migration files — through a codemod, after they
3330
+ * may already have run — each with the inverse that recovers the body as it was
3331
+ * when it ran.
3332
+ *
3333
+ * `0.78.0/01_file-migration-phase` inserted `phase: 'before-schema',` into every
3334
+ * migration without one, and 0.78.0 made the field required. Every migration
3335
+ * applied before that then warned on every boot that its body had changed,
3336
+ * forever: the file cannot be put back, and the recorded hash is the operator's
3337
+ * to keep, not to hand-edit. A warning that is always there is the one nobody
3338
+ * reads when a real edit lands, so an edit we made is not drift.
3339
+ *
3340
+ * A codemod that rewrites files under `migrations/` adds its inverse here in the
3341
+ * same change — `fileMigrationCodemodInverses.test.ts` fails otherwise, and runs
3342
+ * the real codemod over every file shape it has measured. An inverse returns
3343
+ * the CANDIDATE originals, because the rewrite is not always one line: ts-morph
3344
+ * replaces the whitespace around the property it inserts with its own (LF, two
3345
+ * spaces), so a one-line spec, a CRLF file and a tab-indented one each lose
3346
+ * bytes that only the file's own style can put back. It undoes exactly what the
3347
+ * codemod wrote: a hand-written `after-schema` on an applied migration is still
3348
+ * an edit, and still said.
3349
+ */
3350
+ export declare const FRAMEWORK_MIGRATION_EDITS: ReadonlyArray<{
3351
+ readonly codemod: string;
3352
+ readonly inverse: (source: string) => ReadonlyArray<string>;
3353
+ }>;
3354
+
3355
+ /**
3356
+ * The DDL that makes a table's `.fullTextIndex(...)` declarations exist —
3357
+ * THE one builder, used by the full-schema emit and by
3358
+ * `ensureFullTextIndexes` (the planner path's convergence). Idempotent per
3359
+ * dialect:
3360
+ * postgres: STORED tsvector column + GIN index (`IF NOT EXISTS`)
3361
+ * mysql/mariadb: native FULLTEXT INDEX (a re-run's duplicate is swallowed)
3362
+ * sqlite: native FTS5 virtual table + sync triggers (`IF NOT EXISTS`)
3363
+ * mssql: nothing — a ranked LIKE fallback at query time
3364
+ * Turso has no FTS5 and throws.
3365
+ */
3366
+ export declare const fullTextIndexSql: (table: AnyTable, dialect: DialectId) => ReadonlyArray<string>;
3367
+
3368
+ /** A converged schema can still have an unfinished history write. Boot must not skip it. */
3369
+ export declare const hasInterruptedMigration: (sql: SqlClient.SqlClient) => Effect.Effect<boolean, SqlError>;
3370
+
3226
3371
  /**
3227
3372
  * A lock that is being HELD, and the only way to let it go.
3228
3373
  *
@@ -3236,7 +3381,7 @@ export declare const formatReactiveTriggerDrift: (drift: ReactiveTriggerDrift) =
3236
3381
  export declare interface HeldMigrationLock {
3237
3382
  /** Release the lock and return its connection to the pool. Idempotent only
3238
3383
  * in the sense that the dialect statements are; call it once. */
3239
- readonly release: Effect.Effect<void, SqlError_2>;
3384
+ readonly release: Effect.Effect<void, SqlError>;
3240
3385
  }
3241
3386
 
3242
3387
  /**
@@ -3460,7 +3605,7 @@ declare interface InsertIgnoreResult {
3460
3605
  readonly outcome: 'inserted' | 'ignored';
3461
3606
  }
3462
3607
 
3463
- export declare const introspectSchema: (sql: SqlClient.SqlClient) => Effect.Effect<SchemaSnapshot, SqlError_2>;
3608
+ export declare const introspectSchema: (sql: SqlClient.SqlClient) => Effect.Effect<SchemaSnapshot, SqlError>;
3464
3609
 
3465
3610
  /**
3466
3611
  * A `down` that says there is no inverse — with the reason, which is what a
@@ -3486,6 +3631,9 @@ export declare const isAlreadySatisfied: (err: unknown, rules: ReadonlyArray<Alr
3486
3631
  /** Type-guard for the discovery walker. */
3487
3632
  export declare const isFileMigration: (value: unknown) => value is FileMigration;
3488
3633
 
3634
+ /** A `migrations/<id>.ts` row: its `fingerprint` is the body hash. */
3635
+ export declare const isFileMigrationRow: (sql: SqlClient.SqlClient) => Fragment;
3636
+
3489
3637
  export declare const isMysqlCharset: (value: unknown) => value is MysqlCharset;
3490
3638
 
3491
3639
  /** Type guard — narrows an unknown value to a {@link RawSqlFragment}. */
@@ -3535,6 +3683,22 @@ declare type LeafOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' |
3535
3683
 
3536
3684
  declare type LeafOperator_2 = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'startsWith' | 'arrayContains' | 'arrayOverlaps' | 'arrayHas' | 'isNull' | 'isNotNull' | 'spatial';
3537
3685
 
3686
+ /**
3687
+ * `LIMIT n`, spelled for the dialect — after an `ORDER BY`, which every caller
3688
+ * has. SQL Server has no LIMIT; `OFFSET 0 ROWS FETCH NEXT n ROWS ONLY` is its
3689
+ * spelling, valid in a derived table too.
3690
+ *
3691
+ * One helper because the same mistake was written six times: a batched
3692
+ * backfill failed on its first page, and `voltro serve`'s schema check read the
3693
+ * newest applied plan with `LIMIT 1`, got a syntax error back, and reported it
3694
+ * as "no plan row" — so a production serve on SQL Server refused every boot,
3695
+ * including the one right after `voltro db apply`.
3696
+ *
3697
+ * Splicing a number into SQL text needs a reason: this one is framework-chosen
3698
+ * and floored to a positive integer here, so it cannot carry anything but digits.
3699
+ */
3700
+ export declare const limitClause: (sql: SqlClient.SqlClient, n: number) => Statement.Fragment;
3701
+
3538
3702
  /**
3539
3703
  * Build the context for one migration body. Returns the context and the
3540
3704
  * post-conditions it collects, so the runner can evaluate them after `up`.
@@ -4039,6 +4203,20 @@ export declare class MigrationSkipped {
4039
4203
  constructor(reason: string);
4040
4204
  }
4041
4205
 
4206
+ export declare interface MigrationSnapshotCandidate {
4207
+ readonly planId: string;
4208
+ readonly objects: ReadonlyArray<SnapshotRestorationObject>;
4209
+ }
4210
+
4211
+ export declare interface MigrationSquashPreview {
4212
+ readonly before: string;
4213
+ readonly fingerprint: string;
4214
+ readonly planIds: ReadonlyArray<string>;
4215
+ readonly count: number;
4216
+ /** Opaque selection identity, not an authorization credential. */
4217
+ readonly token: string;
4218
+ }
4219
+
4042
4220
  declare interface MixinDefinition<F extends Record<string, ColumnDefinition<unknown>>> {
4043
4221
  /**
4044
4222
  * Stable dedup key. Two mixins with the same `id` reaching a table via
@@ -4423,6 +4601,9 @@ export declare const plannedCreations: (plan: MigrationPlan) => ReadonlyArray<Sc
4423
4601
  */
4424
4602
  export declare interface PlannedOperation {
4425
4603
  readonly op: MigrationOperation;
4604
+ /** Exact retained object name assigned by the applier before soft-drop DDL.
4605
+ * Persisted in operation/resume history; absent when the history has no recorded identity or for hard drops. */
4606
+ readonly softDropSnapshot?: string;
4426
4607
  readonly classification: OperationClass;
4427
4608
  /** Why the op was classified this way (also surfaces in the CLI). */
4428
4609
  readonly reason?: string;
@@ -4494,6 +4675,10 @@ declare interface PredicateLeaf_2 {
4494
4675
  readonly path?: ReadonlyArray<string | number>;
4495
4676
  }
4496
4677
 
4678
+ /** Read the complete eligible history, independent of the dashboard's recent
4679
+ * history limit. The token must be rechecked by squashMigrationPlans on write. */
4680
+ export declare const previewMigrationSquash: (options: Pick<SquashOptions, "before" | "fingerprint">) => Effect.Effect<MigrationSquashPreview, Error, SqlClient.SqlClient>;
4681
+
4497
4682
  /**
4498
4683
  * Imperative `applyNamespacedSchema` against a fresh SqlClient — the
4499
4684
  * entry point the CLI / runtime use to provision a tenant's namespace
@@ -4517,7 +4702,7 @@ declare interface PredicateLeaf_2 {
4517
4702
  * Eager provisioning is the APP's move (call this from a seed/startup over its
4518
4703
  * own tenant table); the framework has no tenant registry to enumerate.
4519
4704
  */
4520
- export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>, namespace: string, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
4705
+ export declare const provisionTenantNamespace: (tables: ReadonlyArray<AnyTable>, namespace: string, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError_2.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
4521
4706
 
4522
4707
  /**
4523
4708
  * The runtime-facing materialized query — a plain bag of strings the store and
@@ -4659,6 +4844,16 @@ declare interface QueryDescriptor<R = Row> {
4659
4844
  readonly alias: string;
4660
4845
  readonly order: boolean;
4661
4846
  };
4847
+ /** Each query word matches as a word BEGINNING (`tomato` finds
4848
+ * `tomatoes`), every word required — `.matching(…, { prefix: true })`. */
4849
+ readonly prefix?: boolean;
4850
+ /**
4851
+ * Set by a store whose engine keeps no full-text index (Turso's MVCC mode
4852
+ * has no FTS5): the match compiles to a case-insensitive LIKE over the
4853
+ * covered columns, ranked by how many of them hit — the same fallback as
4854
+ * mssql. Never set by the query builder.
4855
+ */
4856
+ readonly fallback?: 'like';
4662
4857
  };
4663
4858
  /**
4664
4859
  * Common-Table-Expression definitions (Q4) — `WITH <name> AS (...)`.
@@ -4717,6 +4912,8 @@ declare interface QueryDescriptor<R = Row> {
4717
4912
  readonly joins?: ReadonlyArray<{
4718
4913
  readonly kind: 'inner' | 'left';
4719
4914
  readonly table: string;
4915
+ /** Declared shape for typed joins; name-only joins use the table registry. */
4916
+ readonly sourceTable?: TableLike;
4720
4917
  readonly alias: string;
4721
4918
  readonly on: Predicate;
4722
4919
  }>;
@@ -4937,6 +5134,16 @@ declare interface QueryDescriptor_2<R = Row_2> {
4937
5134
  readonly alias: string;
4938
5135
  readonly order: boolean;
4939
5136
  };
5137
+ /** Each query word matches as a word BEGINNING (`tomato` finds
5138
+ * `tomatoes`), every word required — `.matching(…, { prefix: true })`. */
5139
+ readonly prefix?: boolean;
5140
+ /**
5141
+ * Set by a store whose engine keeps no full-text index (Turso's MVCC mode
5142
+ * has no FTS5): the match compiles to a case-insensitive LIKE over the
5143
+ * covered columns, ranked by how many of them hit — the same fallback as
5144
+ * mssql. Never set by the query builder.
5145
+ */
5146
+ readonly fallback?: 'like';
4940
5147
  };
4941
5148
  /**
4942
5149
  * Common-Table-Expression definitions (Q4) — `WITH <name> AS (...)`.
@@ -4995,6 +5202,8 @@ declare interface QueryDescriptor_2<R = Row_2> {
4995
5202
  readonly joins?: ReadonlyArray<{
4996
5203
  readonly kind: 'inner' | 'left';
4997
5204
  readonly table: string;
5205
+ /** Declared shape for typed joins; name-only joins use the table registry. */
5206
+ readonly sourceTable?: TableLike_2;
4998
5207
  readonly alias: string;
4999
5208
  readonly on: Predicate_2;
5000
5209
  }>;
@@ -5196,8 +5405,10 @@ export declare const reactiveTriggerRepairSql: (input: {
5196
5405
  readonly channel?: string;
5197
5406
  }) => ReadonlyArray<string>;
5198
5407
 
5408
+ export declare const readMigrationSnapshotCandidates: (sql: SqlClient.SqlClient, live: SchemaSnapshot) => Effect.Effect<ReadonlyArray<MigrationSnapshotCandidate>, SqlError>;
5409
+
5199
5410
  /** Every string column of a base table in the connected database. */
5200
- export declare const readUndeclaredCharsetColumns: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<UndeclaredCharsetColumn>, SqlError_2>;
5411
+ export declare const readUndeclaredCharsetColumns: (sql: SqlClient.SqlClient) => Effect.Effect<ReadonlyArray<UndeclaredCharsetColumn>, SqlError>;
5201
5412
 
5202
5413
  /**
5203
5414
  * Record that a schema was applied even though there was nothing to DO.
@@ -5298,7 +5509,12 @@ export declare const renderColumnPg: (col: ColumnSnapshot, hasCompositePk?: bool
5298
5509
  */
5299
5510
  export declare const resolveMigrationLockSchema: (scope?: MigrationLockScope) => string | undefined;
5300
5511
 
5301
- export declare const resolveMysqlEngine: (sql: SqlClient.SqlClient) => Effect.Effect<MysqlEngine, SqlError_2>;
5512
+ export declare const resolveMysqlEngine: (sql: SqlClient.SqlClient) => Effect.Effect<MysqlEngine, SqlError>;
5513
+
5514
+ /** Restore only recorded objects, under the same lock as schema migrations. */
5515
+ export declare const restoreMigrationSnapshots: (sql: SqlClient.SqlClient, planId: string, options?: {
5516
+ readonly beforeExecute?: (sql: SqlClient.SqlClient) => Effect.Effect<void, SqlError, SqlClient.SqlClient>;
5517
+ }) => Effect.Effect<SnapshotRestorationResult, SqlError | Error, SqlClient.SqlClient>;
5302
5518
 
5303
5519
  declare type RetryDecision = 'retry' | 'noRetry';
5304
5520
 
@@ -5352,6 +5568,9 @@ export declare const rollbackFileBasedMigration: (sql: SqlClient.SqlClient, ctx:
5352
5568
  id: string;
5353
5569
  lockSchema?: string;
5354
5570
  store: DataStore;
5571
+ /** Runs under the migration lock after all refusal checks, immediately
5572
+ * before down. Failure leaves the body and ledger untouched. */
5573
+ beforeExecute?: Effect.Effect<void, unknown>;
5355
5574
  }) => Effect.Effect<{
5356
5575
  id: string;
5357
5576
  durationMs: number;
@@ -5450,6 +5669,10 @@ declare interface RuleViolationDetail {
5450
5669
  export declare const runFileBasedMigrations: (sql: SqlClient.SqlClient, ctx: RunFileBasedMigrationsCtx) => Effect.Effect<FileMigrationRunResult, unknown>;
5451
5670
 
5452
5671
  export declare interface RunFileBasedMigrationsCtx {
5672
+ /** Runs once under the migration lock, after pending work is rechecked and
5673
+ * before its bodies start. Not called for an empty/already-applied pass.
5674
+ * Failure aborts the pass. Preparation is outside each body's transaction. */
5675
+ readonly beforeExecute?: Effect.Effect<void, unknown>;
5453
5676
  readonly projectRoot: string;
5454
5677
  readonly env: 'dev' | 'staging' | 'prod';
5455
5678
  readonly appliedBy: string;
@@ -5476,6 +5699,12 @@ export declare interface RunFileBasedMigrationsCtx {
5476
5699
  * called — which is the defect the phase exists to remove, moved up a layer.
5477
5700
  */
5478
5701
  readonly phase: MigrationPhase;
5702
+ /**
5703
+ * The declared store charset (MySQL family), for the ledgers this run
5704
+ * creates before any plan has. Required, `undefined` spelled out — see
5705
+ * `ensureBootstrapTable`.
5706
+ */
5707
+ readonly charset: MysqlCharset | undefined;
5479
5708
  }
5480
5709
 
5481
5710
  /**
@@ -5486,7 +5715,7 @@ export declare interface RunFileBasedMigrationsCtx {
5486
5715
  * `voltro_isr_cache` so framework releases that grow new columns
5487
5716
  * don't crash old DBs at boot.
5488
5717
  */
5489
- export declare const runFrameworkBootstrap: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
5718
+ export declare const runFrameworkBootstrap: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError_2.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
5490
5719
 
5491
5720
  /**
5492
5721
  * Run `applySchema` against a fresh SqlClient from the provided sqlLayer.
@@ -5496,7 +5725,7 @@ export declare const runFrameworkBootstrap: (tables: ReadonlyArray<AnyTable>, sq
5496
5725
  * `dialect` is passed through to the DDL emitter so SQL types + reactive
5497
5726
  * trigger emission match the underlying engine.
5498
5727
  */
5499
- export declare const runMigrate: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
5728
+ export declare const runMigrate: (tables: ReadonlyArray<AnyTable>, sqlLayer: Layer.Layer<SqlClient.SqlClient, ConfigError.ConfigError | SqlError_2.SqlError, never>, dialect?: DialectId, options?: SchemaApplyOptions) => Promise<void>;
5500
5729
 
5501
5730
  /**
5502
5731
  * Execute ONE planned operation with the applier's own per-dialect
@@ -5507,7 +5736,7 @@ export declare const runMigrate: (tables: ReadonlyArray<AnyTable>, sqlLayer: Lay
5507
5736
  * shape from: the live table with the change applied, materialised as a
5508
5737
  * declaration by the caller.
5509
5738
  */
5510
- export declare const runMigrationOp: (sql: SqlClient.SqlClient, planned: PlannedOperation, declared: ReadonlyArray<TableLike>) => Effect.Effect<AppliedOp, SqlError_2>;
5739
+ export declare const runMigrationOp: (sql: SqlClient.SqlClient, planned: PlannedOperation, declared: ReadonlyArray<TableLike>) => Effect.Effect<AppliedOp, SqlError>;
5511
5740
 
5512
5741
  /**
5513
5742
  * Top-level entry point — called once per boot. Returns a
@@ -5516,9 +5745,11 @@ export declare const runMigrationOp: (sql: SqlClient.SqlClient, planned: Planned
5516
5745
  *
5517
5746
  * Wires the dev/prod dispatch + the VOLTRO_AUTO_MIGRATE escape hatch.
5518
5747
  */
5519
- export declare const runPlannedMigrations: (sql: SqlClient.SqlClient, declared: ReadonlyArray<TableLike>, ctx: RunPlannedMigrationsCtx) => Effect.Effect<BootMigrationOutcome, SqlError_2, SqlClient.SqlClient>;
5748
+ export declare const runPlannedMigrations: (sql: SqlClient.SqlClient, declared: ReadonlyArray<TableLike>, ctx: RunPlannedMigrationsCtx) => Effect.Effect<BootMigrationOutcome, SqlError, SqlClient.SqlClient>;
5520
5749
 
5521
5750
  export declare interface RunPlannedMigrationsCtx {
5751
+ /** Forwarded to the applier only when an approved schema plan executes. */
5752
+ readonly beforeExecute?: Effect.Effect<void, SqlError, SqlClient.SqlClient>;
5522
5753
  /** What `voltro logs` should attribute this run to. */
5523
5754
  readonly appliedBy: string;
5524
5755
  readonly environment: BootEnvironment;
@@ -5543,10 +5774,10 @@ export declare interface RunPlannedMigrationsCtx {
5543
5774
  * answers `already-absent` before reaching here). This function assumes the
5544
5775
  * object is there.
5545
5776
  */
5546
- export declare const runSchemaOp: (sql: SqlClient.SqlClient, op: MigrationSchemaOp, live: SchemaSnapshot) => Effect.Effect<void, SqlError_2>;
5777
+ export declare const runSchemaOp: (sql: SqlClient.SqlClient, op: MigrationSchemaOp, live: SchemaSnapshot) => Effect.Effect<void, SqlError>;
5547
5778
 
5548
5779
  /** Runs one Effect over the connection — the shape `ManagedRuntime.runPromise` and a store's `run` share. */
5549
- export declare type RunSql = <A>(effect: Effect.Effect<A, SqlError_2, SqlClient.SqlClient>) => Promise<A>;
5780
+ export declare type RunSql = <A>(effect: Effect.Effect<A, SqlError, SqlClient.SqlClient>) => Promise<A>;
5550
5781
 
5551
5782
  export declare const SCHEMA_OWNER_TABLE = "_voltro_schema_owner";
5552
5783
 
@@ -5673,6 +5904,19 @@ export declare const snapshotColumn: (name: string, def: ColumnDefinition<unknow
5673
5904
  /** Needed only for `storageType` — a caller that renders DDL does not want it. */
5674
5905
  dialect?: DialectId) => ColumnSnapshot;
5675
5906
 
5907
+ export declare interface SnapshotRestorationObject {
5908
+ readonly kind: 'column' | 'table';
5909
+ readonly table: string;
5910
+ readonly originalName: string;
5911
+ readonly snapshotName: string;
5912
+ }
5913
+
5914
+ export declare interface SnapshotRestorationResult {
5915
+ readonly restoredColumns: number;
5916
+ readonly restoredTables: number;
5917
+ readonly objects: ReadonlyArray<SnapshotRestorationObject>;
5918
+ }
5919
+
5676
5920
  /**
5677
5921
  * PostGIS spatial-distance clause carried on a {@link QueryDescriptor}.
5678
5922
  * Drives a `ST_Distance` projected column (distance-as-a-value) and/or a
@@ -5784,7 +6028,7 @@ export declare const sqlType: (column: ColumnDefinition<unknown>, dialect: Diale
5784
6028
  * Effect that performs the squash + returns a summary. Fails if:
5785
6029
  * - no eligible rows match (`before` cut everything off)
5786
6030
  * - the latest pre-squash fingerprint differs from `fingerprint`
5787
- * (signals uncommitted drift — refuse to lock in)
6031
+ * (the cut-off may select an older schema, or declarations may be unapplied)
5788
6032
  */
5789
6033
  export declare const squashMigrationPlans: (options: SquashOptions) => Effect.Effect<SquashResult, Error, SqlClient.SqlClient>;
5790
6034
 
@@ -5803,6 +6047,9 @@ export declare interface SquashOptions {
5803
6047
  readonly appliedBy: string;
5804
6048
  /** Environment tag for the snapshot row. */
5805
6049
  readonly environment: string;
6050
+ /** Confirmation token from previewMigrationSquash. Rechecked under the lock
6051
+ * before writing; changed eligibility refuses without changing history. */
6052
+ readonly expectedPreview?: string;
5806
6053
  }
5807
6054
 
5808
6055
  export declare interface SquashResult {
@@ -6539,9 +6786,9 @@ export declare class TransactionBrokenByBodyError extends Error {
6539
6786
  * Hand the schema to a different app name — `voltro db claim`. The one
6540
6787
  * deliberate way to change the owner: an app was RENAMED, not replaced.
6541
6788
  */
6542
- export declare const transferSchemaOwner: (sql: SqlClient.SqlClient, claim: SchemaOwnerClaim) => Effect.Effect<{
6789
+ export declare const transferSchemaOwner: (sql: SqlClient.SqlClient, claim: SchemaOwnerClaim, charset: MysqlCharset | undefined) => Effect.Effect<{
6543
6790
  readonly previous: string | undefined;
6544
- }, SqlError_2>;
6791
+ }, SqlError>;
6545
6792
 
6546
6793
  export declare const unblockLossy: (plan: MigrationPlan, scope?: ReturnType<typeof destructiveScope>) => MigrationPlan;
6547
6794
 
@@ -6756,7 +7003,7 @@ declare interface WindowSpec_2 {
6756
7003
  }>;
6757
7004
  }
6758
7005
 
6759
- export declare const withMigrationLock: <A, E, R = never>(sql: SqlClient.SqlClient, work: Effect.Effect<A, E, R>, scope?: MigrationLockScope) => Effect.Effect<A, E | SqlError_2, R>;
7006
+ export declare const withMigrationLock: <A, E, R = never>(sql: SqlClient.SqlClient, work: Effect.Effect<A, E, R>, scope?: MigrationLockScope) => Effect.Effect<A, E | SqlError, R>;
6760
7007
 
6761
7008
  /**
6762
7009
  * Wrap a migration context so its four existence questions are recorded when —