@voltro/database 0.20.2 → 0.22.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
@@ -550,6 +550,31 @@ export declare interface ColumnSnapshot {
550
550
  readonly from: ColumnType;
551
551
  readonly using?: string;
552
552
  };
553
+ /**
554
+ * Bounded text length from `text().maxLength(n)` — carried on BOTH sides.
555
+ *
556
+ * DECLARED: from the column definition. LIVE: from introspection, and ONLY when
557
+ * the dialect materialises a length. An unbounded text family member
558
+ * (`text`/`longtext`/`mediumtext`/`clob`) leaves this `undefined` rather than
559
+ * reporting the type's theoretical maximum — mysql answers 4294967295 for
560
+ * `longtext`, and comparing that against a declared `undefined` would re-emit an
561
+ * ALTER on every boot forever.
562
+ *
563
+ * WHY IT IS HERE AT ALL. It was absent from the snapshot entirely: the value
564
+ * lived on the column definition, was read only when rendering CREATE DDL, and
565
+ * never reached the comparison. So the differ was not comparing lengths wrongly
566
+ * — it could not see them. Adding `.maxLength(n)` to an EXISTING column planned
567
+ * zero operations and reported "schema is up to date" while the live column
568
+ * stayed `longtext`. A consumer hit that trying to apply the documented remedy
569
+ * for MariaDB's hash long-unique, and their contrast is what pinned it: a column
570
+ * bounded AT CREATION was `varchar(64)` (DDL path, fine), one bounded afterwards
571
+ * stayed `longtext` (diff path, blind).
572
+ *
573
+ * NOT compared on sqlite: it has no length-enforced type, so the declared value
574
+ * is real and the live side can never report one. Comparing them there would
575
+ * re-emit forever. See `sameColumnShape`.
576
+ */
577
+ readonly maxLength?: number;
553
578
  readonly references?: {
554
579
  readonly table: string;
555
580
  readonly column: string;
@@ -764,30 +789,31 @@ export declare const emitDropColumnDdlMysql: (op: Extract<MigrationOperation, {
764
789
  }>) => string;
765
790
 
766
791
  /**
767
- * Evolution-aware DDL emitter for framework-managed tables (the
768
- * `_voltro_*` bookkeeping set + the `voltro_isr_cache` runtime
769
- * cache). Adds one extra step between CREATE TABLE and CREATE INDEX:
792
+ * Bootstrap DDL for framework-managed tables — `CREATE TABLE IF NOT EXISTS`, and
793
+ * deliberately almost nothing else.
770
794
  *
771
- * ALTER TABLE <t> ADD COLUMN IF NOT EXISTS <col> <type>
795
+ * It exists for ONE reason: `applyPlan` records into `_voltro_migration_plans`,
796
+ * so that table has to exist before the planner can run. Everything after
797
+ * that — columns, indexes, types, nullability — belongs to the planner, which
798
+ * handles framework tables exactly like user tables, on every dialect.
772
799
  *
773
- * for every column the framework's declared shape carries. Without
774
- * this the CREATE TABLE IF NOT EXISTS path is fine for fresh DBs but
775
- * silently no-ops on existing tables subsequent CREATE INDEX IF
776
- * NOT EXISTS then PARSE-fails when an index references a column the
777
- * framework added in a later release that the live DB never picked
778
- * up (pg error 42703).
800
+ * IT USED TO DO MORE, AND THAT WAS THE BUG. It also emitted
801
+ * `ALTER TABLE ADD COLUMN IF NOT EXISTS` and `CREATE INDEX IF NOT EXISTS`, and
802
+ * the ADD COLUMN half was POSTGRES-ONLY because that is the dialect with the
803
+ * syntax. Framework tables were filtered out of the boot planner at the time, so
804
+ * this was their only evolution path which meant a framework release that added
805
+ * a column reached a postgres user's database on boot and a MariaDB user's never,
806
+ * silently. One `voltro dev`, two behaviours, decided by the driver.
779
807
  *
780
- * Postgres-only the other dialects' framework-bookkeeping path
781
- * doesn't suffer this because @effect/sql-cluster / migrationRunner
782
- * own their own schema. The applier in the per-dialect packages does
783
- * the equivalent work for their own bookkeeping.
808
+ * The filter is asymmetric now (see `plannableLive` in `migrations/boot.ts`) and
809
+ * the planner owns evolution, so the extra steps here are not merely redundant —
810
+ * they are a second, weaker path, and the index step actively hurt: it ran BEFORE
811
+ * the planner could add a column, so an index over a newly-added column failed
812
+ * the boot (pg 42703) rather than waiting one step. Deleted rather than fixed;
813
+ * fixing it would have kept two paths.
784
814
  *
785
- * For user / plugin / cloud tables this path is wrong they need the
786
- * full planner refuse-to-boot semantics on column drops, renames,
787
- * type changes. Framework bookkeeping is a tighter contract:
788
- * additive-only column evolution, no destructive changes, no user
789
- * data ever lives in a `.dropped()` slot. The framework versions
790
- * its own schema additively + can safely ALTER TABLE forward.
815
+ * Dialect-uniform now, because there is nothing left in it that only one dialect
816
+ * can express.
791
817
  */
792
818
  export declare const emitFrameworkBootstrapSql: (tables: ReadonlyArray<AnyTable>, dialect: DialectId) => string;
793
819
 
@@ -1074,16 +1100,6 @@ export declare interface IndexSnapshot {
1074
1100
  readonly expression?: boolean;
1075
1101
  }
1076
1102
 
1077
- /**
1078
- * Read the current shape of the connected database. Dispatched per
1079
- * dialect; supports postgres only.
1080
- *
1081
- * The returned `SchemaSnapshot` is the same shape `declaredSnapshot`
1082
- * produces — they're directly diffable via `planMigrations`.
1083
- *
1084
- * Restricted to the `public` schema today; multi-schema apps will
1085
- * grow a `schema` parameter when we have a real need.
1086
- */
1087
1103
  export declare const introspectSchema: (sql: SqlClient.SqlClient) => Effect.Effect<SchemaSnapshot, SqlError_2>;
1088
1104
 
1089
1105
  /** Type-guard for the discovery walker. */
@@ -1195,6 +1211,15 @@ export declare type MigrationOperation = {
1195
1211
  readonly kind: 'drop-index';
1196
1212
  readonly table: string;
1197
1213
  readonly index: string;
1214
+ } | {
1215
+ readonly kind: 'rename-index';
1216
+ readonly table: string;
1217
+ readonly from: string;
1218
+ readonly to: string;
1219
+ } | {
1220
+ readonly kind: 'rename-table';
1221
+ readonly from: string;
1222
+ readonly to: string;
1198
1223
  } | {
1199
1224
  readonly kind: 'add-unique';
1200
1225
  readonly table: string;
@@ -1705,6 +1730,23 @@ declare interface Table<Name extends string, Fields extends Record<string, Colum
1705
1730
  readonly appliedUniques: ReadonlyArray<TableUnique>;
1706
1731
  readonly appliedFullText: ReadonlyArray<TableFullTextIndex>;
1707
1732
  readonly appliedChecks: ReadonlyArray<TableCheck>;
1733
+ /**
1734
+ * The name this table used to have, set by `.renamedFrom('old_name')`.
1735
+ * Named apart from the METHOD that sets it — one interface cannot carry both.
1736
+ *
1737
+ * The table-level twin of a column's `.renamedFrom()`, and it exists for the
1738
+ * same reason: a rename and a drop+create are structurally identical to a
1739
+ * differ (old table gone, new table present) and mean completely different
1740
+ * things. Without the marker the planner has to assume drop + create, which
1741
+ * for a TABLE is data loss, so it refuses — and the rename can only be done
1742
+ * by hand.
1743
+ *
1744
+ * A chained method, matching a column's `.renamedFrom()` — the two mean the
1745
+ * same thing one level apart, so they read the same. It survives every
1746
+ * subsequent `.index(...)` / `.with(...)` because each rebuild spreads its
1747
+ * options through.
1748
+ */
1749
+ readonly previousTableName?: string;
1708
1750
  /**
1709
1751
  * Composite PRIMARY KEY column set, declared via `.primaryKey(['a','b'])`.
1710
1752
  * When set it REPLACES the single-column `id()` PK assumption: no column
@@ -2041,6 +2083,23 @@ declare interface Table<Name extends string, Fields extends Record<string, Colum
2041
2083
  * says so at boot.
2042
2084
  */
2043
2085
  nonReactive: () => Table<Name, Fields, false, IxNames>;
2086
+ /**
2087
+ * Declare the name this table used to have, so the planner folds what would
2088
+ * otherwise be DROP + CREATE — the loss of every row — into an in-place
2089
+ * `rename-table`.
2090
+ *
2091
+ * Deliberately the SAME SHAPE as a column's `.renamedFrom()`, because the two
2092
+ * mean the same thing one level apart, and a reader who learned one will reach
2093
+ * for the other. It first shipped as a third argument to `table()`; that made
2094
+ * the one API in this family that had to be looked up.
2095
+ *
2096
+ * export const notes = table('archive_notes', { … }).renamedFrom('notes')
2097
+ *
2098
+ * Drop the marker once the rename has been applied in every environment. A
2099
+ * marker whose old table is absent is a silent no-op, so it can sit in source
2100
+ * across a staged rollout.
2101
+ */
2102
+ renamedFrom: (oldName: string) => Table<Name, Fields, Reactive, IxNames>;
2044
2103
  }
2045
2104
 
2046
2105
  /**
@@ -2164,6 +2223,13 @@ export declare interface TableSnapshot {
2164
2223
  * data exists yet), present from introspection.
2165
2224
  */
2166
2225
  readonly rowCount?: number;
2226
+ /**
2227
+ * The name this table previously had (`table(name, fields, { renamedFrom })`).
2228
+ * DECLARED side only — introspection has no way to know. The planner pairs it
2229
+ * against a live table of that name and folds what would otherwise be
2230
+ * DROP + CREATE into a `rename-table`.
2231
+ */
2232
+ readonly renamedFrom?: string;
2167
2233
  }
2168
2234
 
2169
2235
  declare interface TableUnique {