@vibeorm/migrate 2.0.2 → 2.1.1

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.
Files changed (54) hide show
  1. package/dist/adoption.d.ts +16 -0
  2. package/dist/adoption.d.ts.map +1 -0
  3. package/dist/compile-desired-state.d.ts +12 -0
  4. package/dist/compile-desired-state.d.ts.map +1 -0
  5. package/dist/ddl/postgres.d.ts.map +1 -1
  6. package/dist/ddl/render.d.ts +7 -0
  7. package/dist/ddl/render.d.ts.map +1 -1
  8. package/dist/ddl/sqlite.d.ts +5 -0
  9. package/dist/ddl/sqlite.d.ts.map +1 -1
  10. package/dist/desired-state.d.ts +69 -0
  11. package/dist/desired-state.d.ts.map +1 -0
  12. package/dist/differ.d.ts.map +1 -1
  13. package/dist/down.d.ts +2 -0
  14. package/dist/down.d.ts.map +1 -1
  15. package/dist/exclusions.d.ts +26 -0
  16. package/dist/exclusions.d.ts.map +1 -0
  17. package/dist/execution.d.ts +1 -1
  18. package/dist/execution.d.ts.map +1 -1
  19. package/dist/fs.d.ts +7 -0
  20. package/dist/fs.d.ts.map +1 -0
  21. package/dist/fs.js +6758 -0
  22. package/dist/fs.js.map +41 -0
  23. package/dist/history.d.ts +14 -0
  24. package/dist/history.d.ts.map +1 -0
  25. package/dist/index.d.ts +14 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1672 -203
  28. package/dist/index.js.map +29 -19
  29. package/dist/inspect-objects.d.ts +116 -0
  30. package/dist/inspect-objects.d.ts.map +1 -0
  31. package/dist/introspect/mysql.d.ts.map +1 -1
  32. package/dist/introspect/postgres.d.ts +4 -0
  33. package/dist/introspect/postgres.d.ts.map +1 -1
  34. package/dist/introspect/shared.d.ts.map +1 -1
  35. package/dist/inventory.d.ts +15 -0
  36. package/dist/inventory.d.ts.map +1 -0
  37. package/dist/normalize.d.ts +2 -5
  38. package/dist/normalize.d.ts.map +1 -1
  39. package/dist/object-ownership.d.ts +58 -0
  40. package/dist/object-ownership.d.ts.map +1 -0
  41. package/dist/planner.d.ts +29 -0
  42. package/dist/planner.d.ts.map +1 -0
  43. package/dist/push.d.ts +9 -36
  44. package/dist/push.d.ts.map +1 -1
  45. package/dist/reference-state.d.ts +14 -0
  46. package/dist/reference-state.d.ts.map +1 -0
  47. package/dist/runner.d.ts +14 -1
  48. package/dist/runner.d.ts.map +1 -1
  49. package/dist/sql-statements.d.ts +9 -0
  50. package/dist/sql-statements.d.ts.map +1 -0
  51. package/dist/sqlite-types.d.ts.map +1 -1
  52. package/dist/types.d.ts +23 -1
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +7 -3
package/dist/index.js CHANGED
@@ -3,6 +3,10 @@
3
3
  function describeStep(params) {
4
4
  const { step } = params;
5
5
  switch (step.kind) {
6
+ case "addExclusion":
7
+ return `add exclusion "${step.exclusion.name}" on "${step.table}"`;
8
+ case "dropExclusion":
9
+ return `drop exclusion "${step.exclusion.name}" on "${step.table}"`;
6
10
  case "enableRls":
7
11
  return `enable native RLS on "${step.table}"`;
8
12
  case "forceRls":
@@ -67,6 +71,7 @@ function describeStep(params) {
67
71
  }
68
72
  // packages/migrate/src/differ.ts
69
73
  import { isEnumType as isEnumType6, unsupportedCapability as unsupportedCapability6 } from "@vibeorm/schema";
74
+ import { VibeError as VibeError7 } from "@vibeorm/schema";
70
75
 
71
76
  // packages/migrate/src/enum-values.ts
72
77
  import { isEnumType, unsupportedCapability } from "@vibeorm/schema";
@@ -423,13 +428,27 @@ var ENUM_CHECK_PATTERN = /CONSTRAINT "((?:[^"]|"")+)_enum_((?:[^"]|"")+)" CHECK
423
428
  function parseSqliteNamedChecks(params) {
424
429
  const { tableSql } = params;
425
430
  const result = [];
426
- for (const match of tableSql.matchAll(/CONSTRAINT "((?:[^"]|"")+)" CHECK\s*\(/g)) {
427
- const name = match[1].replaceAll('""', '"');
431
+ for (const match of tableSql.matchAll(/\bCONSTRAINT\s+(?:"((?:[^"]|"")+)"|`((?:[^`]|``)+)`|\[([^\]]+)\]|([A-Za-z_][A-Za-z_0-9]*))\s+CHECK\s*\(/gi)) {
432
+ const name = match[1]?.replaceAll('""', '"') ?? match[2]?.replaceAll("``", "`") ?? match[3] ?? match[4];
428
433
  const open = (match.index ?? 0) + match[0].length - 1;
429
434
  let depth = 0;
430
435
  let close = -1;
436
+ let quote;
431
437
  for (let index = open;index < tableSql.length; index++) {
432
438
  const char = tableSql[index];
439
+ if (quote !== undefined) {
440
+ if (char === quote) {
441
+ if (tableSql[index + 1] === quote)
442
+ index++;
443
+ else
444
+ quote = undefined;
445
+ }
446
+ continue;
447
+ }
448
+ if (char === "'" || char === '"' || char === "`") {
449
+ quote = char;
450
+ continue;
451
+ }
433
452
  if (char === "(")
434
453
  depth += 1;
435
454
  else if (char === ")") {
@@ -742,6 +761,7 @@ function normalizeIndexes(params) {
742
761
  result.set(key, {
743
762
  key,
744
763
  name,
764
+ ...index.constraintName === undefined ? {} : { constraintName: index.constraintName },
745
765
  kind,
746
766
  unique,
747
767
  columns,
@@ -945,7 +965,7 @@ function normalizeFks(params) {
945
965
  continue;
946
966
  result.set(key, {
947
967
  key,
948
- constraintName: `${table}_${columns.join("_")}_fkey`,
968
+ constraintName: relation.constraintName ?? `${table}_${columns.join("_")}_fkey`,
949
969
  columns,
950
970
  refTable,
951
971
  refColumns,
@@ -1242,6 +1262,9 @@ function planRenames(params) {
1242
1262
  // packages/migrate/src/differ.ts
1243
1263
  function diffSchemas(params) {
1244
1264
  const dialect = params.dialect ?? "postgres";
1265
+ for (const schema of [params.from, params.to])
1266
+ if (dialect !== "postgres" && schema.models.some((model) => (model.exclusions?.length ?? 0) > 0))
1267
+ throw unsupportedCapability6({ dialect, feature: "exclusionConstraints" });
1245
1268
  assertRlsDialect({ schema: params.from, dialect });
1246
1269
  assertRlsDialect({ schema: params.to, dialect });
1247
1270
  if (dialect !== "postgres")
@@ -1251,6 +1274,8 @@ function diffSchemas(params) {
1251
1274
  const from = normalizeSchema({ schema: params.from, dialect });
1252
1275
  const to = normalizeSchema({ schema: params.to, dialect });
1253
1276
  const buckets = {
1277
+ dropExclusion: [],
1278
+ addExclusion: [],
1254
1279
  dropView: [],
1255
1280
  createEnum: [],
1256
1281
  addEnumValue: [],
@@ -1276,6 +1301,23 @@ function diffSchemas(params) {
1276
1301
  diffViews({ from, to, buckets });
1277
1302
  diffJoinTables({ from, to, buckets });
1278
1303
  diffTables({ from, to, buckets, dialect });
1304
+ for (const model of params.to.models) {
1305
+ const table = model.dbName ?? model.name;
1306
+ const previous = params.from.models.find((entry) => (entry.dbName ?? entry.name) === table);
1307
+ for (const exclusion of model.exclusions ?? []) {
1308
+ const before = previous?.exclusions?.find((entry) => entry.name === exclusion.name);
1309
+ if (before?.definition === exclusion.definition)
1310
+ continue;
1311
+ if (before?.catalogDefinition)
1312
+ throw new VibeError7({ code: "VIBE_MIGRATION", message: "Exclusion comparison requires normalized evidence", meta: { reason: "exclusion-inspection-unavailable", table, name: exclusion.name } });
1313
+ if (before !== undefined)
1314
+ buckets.dropExclusion.push({ kind: "dropExclusion", table, exclusion: before, destructive: false });
1315
+ buckets.addExclusion.push({ kind: "addExclusion", table, exclusion, destructive: false });
1316
+ }
1317
+ for (const exclusion of previous?.exclusions ?? [])
1318
+ if (!model.exclusions?.some((entry) => entry.name === exclusion.name))
1319
+ buckets.dropExclusion.push({ kind: "dropExclusion", table, exclusion, destructive: false });
1320
+ }
1279
1321
  refuseDependentEnumStorageChanges({ from, to, buckets, dialect });
1280
1322
  return withRlsSteps({ from: params.from, to: params.to, steps: [
1281
1323
  ...renamed.steps,
@@ -1283,6 +1325,7 @@ function diffSchemas(params) {
1283
1325
  ...buckets.createEnum,
1284
1326
  ...buckets.addEnumValue,
1285
1327
  ...buckets.dropForeignKey,
1328
+ ...buckets.dropExclusion,
1286
1329
  ...buckets.dropIndex,
1287
1330
  ...buckets.dropJoinTable,
1288
1331
  ...buckets.dropTable,
@@ -1294,6 +1337,7 @@ function diffSchemas(params) {
1294
1337
  ...buckets.addPrimaryKey,
1295
1338
  ...buckets.dropCheck,
1296
1339
  ...buckets.addCheck,
1340
+ ...buckets.addExclusion,
1297
1341
  ...buckets.createIndex,
1298
1342
  ...buckets.addForeignKey,
1299
1343
  ...buckets.createJoinTable,
@@ -1421,6 +1465,7 @@ function createWholeTable(params) {
1421
1465
  table: table.table,
1422
1466
  columns: [...table.columns.values()].map((column) => column.field),
1423
1467
  primaryKey: table.pk,
1468
+ ...table.model.primaryKeyConstraintName === undefined ? {} : { primaryKeyConstraintName: table.model.primaryKeyConstraintName },
1424
1469
  checks: [...table.checks].map(([name, expression]) => ({ name, expression })),
1425
1470
  destructive: false
1426
1471
  });
@@ -1467,6 +1512,9 @@ function diffOneTable(params) {
1467
1512
  changes.push("nullability");
1468
1513
  if (before.defaultKey !== column.defaultKey)
1469
1514
  changes.push("default");
1515
+ const collationChanged = dialect === "mysql" && column.field.collation !== undefined && before.field.collation?.toLowerCase() !== column.field.collation.toLowerCase();
1516
+ if (collationChanged)
1517
+ changes.push("collation");
1470
1518
  if (changes.length === 0)
1471
1519
  continue;
1472
1520
  const storageChange = isEnumStringTransition({ from: before.field, to: column.field });
@@ -1501,7 +1549,7 @@ function diffOneTable(params) {
1501
1549
  from: before.field,
1502
1550
  to: column.field,
1503
1551
  changes,
1504
- destructive: narrowingType
1552
+ destructive: narrowingType || collationChanged
1505
1553
  });
1506
1554
  }
1507
1555
  for (const [name] of previous.columns) {
@@ -1513,20 +1561,26 @@ function diffOneTable(params) {
1513
1561
  const pkAfter = current.pk.join(",");
1514
1562
  if (pkBefore !== pkAfter) {
1515
1563
  if (previous.pk.length > 0) {
1516
- buckets.dropPrimaryKey.push({ kind: "dropPrimaryKey", table, destructive: false });
1564
+ buckets.dropPrimaryKey.push({ kind: "dropPrimaryKey", table, destructive: false, ...previous.model.primaryKeyConstraintName === undefined ? {} : { constraintName: previous.model.primaryKeyConstraintName } });
1517
1565
  }
1518
1566
  if (current.pk.length > 0) {
1519
1567
  buckets.addPrimaryKey.push({
1520
1568
  kind: "addPrimaryKey",
1521
1569
  table,
1522
1570
  columns: current.pk,
1571
+ ...current.model.primaryKeyConstraintName === undefined ? {} : { constraintName: current.model.primaryKeyConstraintName },
1523
1572
  destructive: false
1524
1573
  });
1525
1574
  }
1526
1575
  }
1527
1576
  for (const [name, expression] of current.checks) {
1528
- if (previous.checks.has(name))
1577
+ const before = previous.checks.get(name);
1578
+ if (before === expression)
1529
1579
  continue;
1580
+ if (before !== undefined && previous.model.checks?.some((check) => check.name === name && check.catalogExpression === true))
1581
+ throw new VibeError7({ code: "VIBE_MIGRATION", message: "Unequal catalog CHECK text requires normalized evidence or explicit replacement through the live planner", meta: { reason: "check-inspection-unavailable", table, name } });
1582
+ if (before !== undefined)
1583
+ buckets.dropCheck.push({ kind: "dropCheck", table, name, destructive: false });
1530
1584
  buckets.addCheck.push({ kind: "addCheck", table, name, expression, destructive: false });
1531
1585
  }
1532
1586
  for (const [name] of previous.checks) {
@@ -1552,7 +1606,7 @@ function diffOneTable(params) {
1552
1606
  for (const [key, index] of previous.indexes) {
1553
1607
  if (current.indexes.has(key))
1554
1608
  continue;
1555
- buckets.dropIndex.push({ kind: "dropIndex", name: index.name, table, destructive: false });
1609
+ buckets.dropIndex.push({ kind: "dropIndex", name: index.name, table, destructive: false, ...index.constraintName === undefined ? {} : { constraintName: index.constraintName } });
1556
1610
  }
1557
1611
  for (const [key, fk] of current.fks) {
1558
1612
  const before = previous.fks.get(key);
@@ -1601,9 +1655,13 @@ function diffOneTable(params) {
1601
1655
  });
1602
1656
  }
1603
1657
  }
1658
+ // packages/migrate/src/ddl/render.ts
1659
+ import { VibeError as VibeError11, sqlObjectKey } from "@vibeorm/schema";
1660
+
1604
1661
  // packages/migrate/src/ddl/postgres.ts
1605
- import { VibeError as VibeError7, isEnumType as isEnumType7 } from "@vibeorm/schema";
1606
- import { postgresDialect as postgresDialect3, renderRlsPolicyPath } from "@vibeorm/sql";
1662
+ import { renderExclusionDdl } from "@vibeorm/sql";
1663
+ import { VibeError as VibeError8, isEnumType as isEnumType7 } from "@vibeorm/schema";
1664
+ import { postgresDialect as postgresDialect3, renderRlsPolicyPath, renderConstraintOwnedIndexDrop } from "@vibeorm/sql";
1607
1665
  function renderPostgresSteps(params) {
1608
1666
  refuseSameTransactionEnumUse({ steps: params.steps });
1609
1667
  return params.steps.flatMap((step) => renderStep({ step }));
@@ -1638,7 +1696,7 @@ function refuseSameTransactionEnumUse(params) {
1638
1696
  const found = use(field);
1639
1697
  if (found === undefined)
1640
1698
  continue;
1641
- throw new VibeError7({
1699
+ throw new VibeError8({
1642
1700
  code: "VIBE_MIGRATION",
1643
1701
  message: `enum value "${found.value}" is added to "${found.enumName}" and used as a default on "${step.table}"."${field.dbName ?? field.name}" in the same migration \u2014 PostgreSQL cannot use an enum member before the transaction that added it commits. Split this into two migrations: the first containing only the ALTER TYPE ... ADD VALUE, the second the column change`,
1644
1702
  meta: { enum: found.enumName, value: found.value, table: step.table, column: field.dbName ?? field.name, rule: "enum-value-used-in-adding-transaction" }
@@ -1649,6 +1707,9 @@ function refuseSameTransactionEnumUse(params) {
1649
1707
  function renderStep(params) {
1650
1708
  const { step } = params;
1651
1709
  switch (step.kind) {
1710
+ case "addExclusion":
1711
+ case "dropExclusion":
1712
+ return renderExclusionDdl({ dialect: postgresDialect3, table: step.table, exclusion: step.exclusion, operation: step.kind === "addExclusion" ? "add" : "drop" });
1652
1713
  case "enableRls":
1653
1714
  case "forceRls":
1654
1715
  case "disableRls":
@@ -1671,7 +1732,7 @@ function renderStep(params) {
1671
1732
  const lines = step.columns.map((column) => ` ${columnDefSql({ field: column })}`);
1672
1733
  if (step.primaryKey.length > 0) {
1673
1734
  const pkCols = step.primaryKey.map((column) => q(column)).join(", ");
1674
- lines.push(` CONSTRAINT ${q(`${step.table}_pkey`)} PRIMARY KEY (${pkCols})`);
1735
+ lines.push(` CONSTRAINT ${q(step.primaryKeyConstraintName ?? `${step.table}_pkey`)} PRIMARY KEY (${pkCols})`);
1675
1736
  }
1676
1737
  for (const check of step.checks ?? []) {
1677
1738
  lines.push(` CONSTRAINT ${q(check.name)} CHECK (${check.expression})`);
@@ -1683,7 +1744,7 @@ ${lines.join(`,
1683
1744
  }
1684
1745
  case "dropTable":
1685
1746
  case "dropJoinTable":
1686
- return [`DROP TABLE ${q(step.table)} CASCADE;`];
1747
+ return [`DROP TABLE ${q(step.table)};`];
1687
1748
  case "dropView":
1688
1749
  return [`DROP VIEW IF EXISTS ${q(step.table)};`];
1689
1750
  case "addColumn":
@@ -1703,6 +1764,11 @@ ${lines.join(`,
1703
1764
  ];
1704
1765
  }
1705
1766
  case "dropIndex":
1767
+ if (step.constraintName !== undefined) {
1768
+ if (step.table === undefined)
1769
+ throw new VibeError8({ code: "VIBE_MIGRATION", message: "Constraint-backed index removal requires its owning table" });
1770
+ return [renderConstraintOwnedIndexDrop({ dialect: postgresDialect3, table: step.table, constraint: step.constraintName }).text];
1771
+ }
1706
1772
  return [`DROP INDEX ${q(step.name)};`];
1707
1773
  case "addCheck":
1708
1774
  return [
@@ -1753,18 +1819,18 @@ ${lines.join(`,
1753
1819
  case "addPrimaryKey": {
1754
1820
  const cols = step.columns.map((column) => q(column)).join(", ");
1755
1821
  return [
1756
- `ALTER TABLE ${q(step.table)} ADD CONSTRAINT ${q(`${step.table}_pkey`)} PRIMARY KEY (${cols});`
1822
+ `ALTER TABLE ${q(step.table)} ADD CONSTRAINT ${q(step.constraintName ?? `${step.table}_pkey`)} PRIMARY KEY (${cols});`
1757
1823
  ];
1758
1824
  }
1759
1825
  case "dropPrimaryKey":
1760
- return [`ALTER TABLE ${q(step.table)} DROP CONSTRAINT ${q(`${step.table}_pkey`)};`];
1826
+ return [`ALTER TABLE ${q(step.table)} DROP CONSTRAINT ${q(step.constraintName ?? `${step.table}_pkey`)};`];
1761
1827
  }
1762
1828
  }
1763
1829
  function columnDefSql(params) {
1764
1830
  const { field } = params;
1765
1831
  const name = q(field.dbName ?? field.name);
1766
1832
  if (field.default?.kind === "autoincrement" && !isIdentityColumn({ field })) {
1767
- throw new VibeError7({
1833
+ throw new VibeError8({
1768
1834
  code: "VIBE_MIGRATION",
1769
1835
  message: `autoincrement default on "${field.name}" requires a non-list Int or BigInt column`,
1770
1836
  meta: { field: field.name }
@@ -1800,7 +1866,7 @@ function renderAlterColumn(params) {
1800
1866
  }
1801
1867
  if (step.changes.includes("default") || droppedDefault) {
1802
1868
  if (step.to.default?.kind === "autoincrement") {
1803
- throw new VibeError7({
1869
+ throw new VibeError8({
1804
1870
  code: "VIBE_MIGRATION",
1805
1871
  message: `cannot add autoincrement to existing column "${step.table}"."${step.column}" \u2014 drop and recreate the column instead`,
1806
1872
  meta: { table: step.table, column: step.column }
@@ -1816,7 +1882,7 @@ function q(name) {
1816
1882
  }
1817
1883
 
1818
1884
  // packages/migrate/src/ddl/sqlite.ts
1819
- import { VibeError as VibeError8, isEnumType as isEnumType8, unsupportedCapability as unsupportedCapability7 } from "@vibeorm/schema";
1885
+ import { VibeError as VibeError9, isEnumType as isEnumType8, unsupportedCapability as unsupportedCapability7 } from "@vibeorm/schema";
1820
1886
  import { sqliteDialect } from "@vibeorm/sql";
1821
1887
 
1822
1888
  // packages/migrate/src/ddl/drop-order.ts
@@ -1885,7 +1951,7 @@ function renderSqliteSteps(params) {
1885
1951
  continue;
1886
1952
  const awaitsNewParent = [...toNorm?.tables.get(table)?.fks.values() ?? []].some((fk) => createdTables.has(fk.refTable));
1887
1953
  if (awaitsNewParent) {
1888
- throw new VibeError8({
1954
+ throw new VibeError9({
1889
1955
  code: "VIBE_MIGRATION",
1890
1956
  message: `cannot order this sqlite migration safely: rebuilding "${table}" must happen BEFORE the dropped table(s) it references (${[
1891
1957
  ...droppedTables
@@ -1905,11 +1971,16 @@ function renderSqliteSteps(params) {
1905
1971
  return renderTableRebuild({
1906
1972
  table,
1907
1973
  from: fromNorm,
1908
- to: toNorm
1974
+ to: toNorm,
1975
+ restore: params.restore?.get(table) ?? [],
1976
+ preservedColumns: params.preservedColumns?.get(table) ?? new Map
1909
1977
  });
1910
1978
  };
1911
1979
  for (const step of steps) {
1912
1980
  switch (step.kind) {
1981
+ case "addExclusion":
1982
+ case "dropExclusion":
1983
+ throw new VibeError9({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "sqlite", feature: "exclusionConstraints" } });
1913
1984
  case "createEnum":
1914
1985
  case "dropEnum":
1915
1986
  case "addEnumValue":
@@ -1994,7 +2065,7 @@ function renderSqliteSteps(params) {
1994
2065
  case "renameColumn": {
1995
2066
  const renamed = step.kind === "renameTable" ? step.from : step.table;
1996
2067
  if (rebuildSet.has(renamed) || step.kind === "renameTable" && rebuildSet.has(step.to)) {
1997
- throw new VibeError8({
2068
+ throw new VibeError9({
1998
2069
  code: "VIBE_MIGRATION",
1999
2070
  message: `sqlite cannot rename "${renamed}" in the same migration that rebuilds it \u2014 the table-rebuild recipe is written against a single table name. Split the rename and the column/constraint change into two migrations`,
2000
2071
  meta: { reason: "rename-ambiguous", table: renamed, dialect: "sqlite" }
@@ -2046,6 +2117,9 @@ function classifyRebuilds(params) {
2046
2117
  };
2047
2118
  for (const step of steps) {
2048
2119
  switch (step.kind) {
2120
+ case "addExclusion":
2121
+ case "dropExclusion":
2122
+ throw new VibeError9({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "sqlite", feature: "exclusionConstraints" } });
2049
2123
  case "alterColumn":
2050
2124
  case "dropForeignKey":
2051
2125
  case "addPrimaryKey":
@@ -2095,7 +2169,7 @@ function renderTableRebuild(params) {
2095
2169
  const source = from.tables.get(table);
2096
2170
  const target = to.tables.get(table);
2097
2171
  if (source === undefined || target === undefined) {
2098
- throw new VibeError8({
2172
+ throw new VibeError9({
2099
2173
  code: "VIBE_MIGRATION",
2100
2174
  message: `cannot rebuild "${table}" \u2014 the table is missing from the ${source === undefined ? "from" : "to"} schema`,
2101
2175
  meta: { table }
@@ -2117,9 +2191,10 @@ function renderTableRebuild(params) {
2117
2191
  onDelete: CASCADE_ACTION_SQL[fk.onDelete],
2118
2192
  onUpdate: CASCADE_ACTION_SQL[fk.onUpdate]
2119
2193
  })),
2120
- enums: to.enums
2194
+ enums: to.enums,
2195
+ preservedColumns: params.preservedColumns
2121
2196
  }));
2122
- const common = [...target.columns.keys()].filter((name) => source.columns.has(name));
2197
+ const common = [...new Set([...target.columns.keys(), ...params.preservedColumns.keys()])].filter((name) => params.preservedColumns.get(name)?.generated !== true && (source.columns.has(name) || params.preservedColumns.has(name)));
2123
2198
  if (common.length > 0) {
2124
2199
  const cols = common.map((name) => q2(name)).join(", ");
2125
2200
  statements.push(`INSERT INTO ${q2(tmp)} (${cols}) SELECT ${cols} FROM ${q2(table)};`);
@@ -2136,7 +2211,7 @@ function renderTableRebuild(params) {
2136
2211
  nullsNotDistinct: index.nullsNotDistinct
2137
2212
  }));
2138
2213
  }
2139
- statements.push("PRAGMA foreign_key_check;", "COMMIT;", "PRAGMA foreign_keys=ON;");
2214
+ statements.push(...params.restore, "PRAGMA foreign_key_check;", "COMMIT;", "PRAGMA foreign_keys=ON;");
2140
2215
  return statements;
2141
2216
  }
2142
2217
  function isCheapAddColumn(params) {
@@ -2182,12 +2257,16 @@ function canCheapDropColumn(params) {
2182
2257
  function createTableSql(params) {
2183
2258
  const { table, constraintTable, columns, primaryKey, fks, enums } = params;
2184
2259
  const autoincrementPk = autoincrementPkColumn({ columns, primaryKey });
2185
- const lines = columns.map((field) => ` ${columnDefSql2({
2260
+ const lines = columns.map((field) => ` ${params.preservedColumns?.get(field.dbName ?? field.name)?.sql ?? columnDefSql2({
2186
2261
  field,
2187
2262
  constraintTable,
2188
2263
  enums,
2189
2264
  inlinePkAutoincrement: (field.dbName ?? field.name) === autoincrementPk
2190
2265
  })}`);
2266
+ for (const [name, column] of params.preservedColumns ?? []) {
2267
+ if (!columns.some((field) => (field.dbName ?? field.name) === name))
2268
+ lines.push(` ${column.sql}`);
2269
+ }
2191
2270
  if (primaryKey.length > 0 && autoincrementPk === undefined) {
2192
2271
  lines.push(` PRIMARY KEY (${primaryKey.map((column) => q2(column)).join(", ")})`);
2193
2272
  }
@@ -2217,7 +2296,7 @@ function columnDefSql2(params) {
2217
2296
  const { field, constraintTable, enums, inlinePkAutoincrement } = params;
2218
2297
  const name = field.dbName ?? field.name;
2219
2298
  if (field.default?.kind === "autoincrement" && !inlinePkAutoincrement) {
2220
- throw new VibeError8({
2299
+ throw new VibeError9({
2221
2300
  code: "VIBE_MIGRATION",
2222
2301
  message: `autoincrement on sqlite requires "${field.name}" to be the single-column Int or BigInt primary key`,
2223
2302
  meta: { field: field.name }
@@ -2236,7 +2315,7 @@ function columnDefSql2(params) {
2236
2315
  const enumName = field.type.enum;
2237
2316
  const values = resolveEnumValues({ name: enumName, enums });
2238
2317
  if (values === undefined) {
2239
- throw new VibeError8({
2318
+ throw new VibeError9({
2240
2319
  code: "VIBE_MIGRATION",
2241
2320
  message: `enum "${enumName}" on column "${field.name}" is not declared in the schema \u2014 sqlite rendering needs the target schema's enums for the CHECK constraint`,
2242
2321
  meta: { field: field.name, enum: enumName }
@@ -2271,7 +2350,7 @@ function requireSchemas(params) {
2271
2350
  const { step, fromNorm, toNorm } = params;
2272
2351
  if (fromNorm !== undefined && toNorm !== undefined)
2273
2352
  return;
2274
- throw new VibeError8({
2353
+ throw new VibeError9({
2275
2354
  code: "VIBE_MIGRATION",
2276
2355
  message: `sqlite step "${describeStep({ step })}" requires a table rebuild \u2014 renderSteps needs the { from, to } schemas to construct it`,
2277
2356
  meta: { step: step.kind }
@@ -2282,7 +2361,7 @@ function q2(name) {
2282
2361
  }
2283
2362
 
2284
2363
  // packages/migrate/src/ddl/mysql.ts
2285
- import { VibeError as VibeError9, unsupportedCapability as unsupportedCapability8 } from "@vibeorm/schema";
2364
+ import { VibeError as VibeError10, unsupportedCapability as unsupportedCapability8 } from "@vibeorm/schema";
2286
2365
  import { mysqlDialect as mysqlDialect2 } from "@vibeorm/sql";
2287
2366
  function renderMysqlSteps(params) {
2288
2367
  for (const step of params.steps)
@@ -2400,11 +2479,14 @@ ${lines.join(`,
2400
2479
  return [
2401
2480
  `ALTER TABLE ${q3(step.table)} ADD CONSTRAINT ${q3(step.name)} CHECK (${step.expression});`
2402
2481
  ];
2482
+ case "addExclusion":
2483
+ case "dropExclusion":
2484
+ throw new VibeError10({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "mysql", feature: "exclusionConstraints" } });
2403
2485
  case "dropCheck":
2404
2486
  return [`ALTER TABLE ${q3(step.table)} DROP CHECK ${q3(step.name)};`];
2405
2487
  case "dropIndex": {
2406
2488
  if (step.table === undefined) {
2407
- throw new VibeError9({
2489
+ throw new VibeError10({
2408
2490
  code: "VIBE_MIGRATION",
2409
2491
  message: `dropIndex "${step.name}" carries no table \u2014 mysql's DROP INDEX requires one`,
2410
2492
  meta: { index: step.name }
@@ -2460,7 +2542,7 @@ function columnDefSql3(params) {
2460
2542
  const name = q3(field.dbName ?? field.name);
2461
2543
  const autoincrement = field.default?.kind === "autoincrement";
2462
2544
  if (autoincrement && field.type !== "Int" && field.type !== "BigInt") {
2463
- throw new VibeError9({
2545
+ throw new VibeError10({
2464
2546
  code: "VIBE_MIGRATION",
2465
2547
  message: `autoincrement default on "${field.name}" requires a non-list Int or BigInt column`,
2466
2548
  meta: { field: field.name }
@@ -2468,7 +2550,7 @@ function columnDefSql3(params) {
2468
2550
  }
2469
2551
  let def = `${name} ${mysqlColumnTypeSql({ field, enums })}`;
2470
2552
  if (field.collation !== undefined)
2471
- def += ` COLLATE ${field.collation}`;
2553
+ def += ` COLLATE ${q3(field.collation)}`;
2472
2554
  if (!field.isOptional)
2473
2555
  def += " NOT NULL";
2474
2556
  if (autoincrement)
@@ -2482,15 +2564,16 @@ function renderAlterColumn2(params) {
2482
2564
  const { step, enums } = params;
2483
2565
  assertEnumStringTransition({ from: step.from, to: step.to, dialect: "mysql", table: step.table, column: step.column });
2484
2566
  if (step.to.default?.kind === "autoincrement" && step.from.default?.kind !== "autoincrement") {
2485
- throw new VibeError9({
2567
+ throw new VibeError10({
2486
2568
  code: "VIBE_MIGRATION",
2487
2569
  message: `cannot add autoincrement to existing column "${step.table}"."${step.column}" \u2014 drop and recreate the column instead`,
2488
2570
  meta: { table: step.table, column: step.column }
2489
2571
  });
2490
2572
  }
2491
- if (step.changes.includes("type") || step.changes.includes("nullability")) {
2573
+ if (step.changes.includes("type") || step.changes.includes("nullability") || step.changes.includes("collation")) {
2574
+ const field = step.to.type === "String" && step.to.collation === undefined && step.from.collation !== undefined ? { ...step.to, collation: step.from.collation } : step.to;
2492
2575
  return [
2493
- `ALTER TABLE ${q3(step.table)} MODIFY COLUMN ${columnDefSql3({ field: step.to, enums })};`
2576
+ `ALTER TABLE ${q3(step.table)} MODIFY COLUMN ${columnDefSql3({ field, enums })};`
2494
2577
  ];
2495
2578
  }
2496
2579
  const defaultExpr = mysqlDefaultSql({ field: step.to });
@@ -2507,17 +2590,64 @@ function inlineIndexSql(params) {
2507
2590
  }
2508
2591
 
2509
2592
  // packages/migrate/src/ddl/render.ts
2593
+ function declaredSqlitePreservation(params) {
2594
+ const restore = new Map;
2595
+ const columns = new Map;
2596
+ let to = params.to;
2597
+ if (params.from === undefined || to === undefined || (params.from.migrationObjects?.length ?? 0) === 0)
2598
+ return { to, restore, columns };
2599
+ const invalid = sqliteInvalidatedTables({ steps: params.steps, from: params.from });
2600
+ const retained = new Set((to.migrationObjects ?? []).map((object) => sqlObjectKey({ identity: object.identity })));
2601
+ for (const object of params.from.migrationObjects ?? []) {
2602
+ const identity = object.identity;
2603
+ if (!("table" in identity) || identity.table.namespace !== "main" || !invalid.rebuilt.has(identity.table.name) || !retained.has(sqlObjectKey({ identity })))
2604
+ continue;
2605
+ const facts = object.expected?.facts;
2606
+ if (facts === undefined)
2607
+ throw new VibeError11({ code: "VIBE_MIGRATION", message: "SQLite rebuild requires independently declared preservation definitions", meta: { reason: "sqlite-preservation-unavailable", identity } });
2608
+ if (identity.kind === "trigger" || identity.kind === "index") {
2609
+ if (identity.kind === "index" && to.models.some((model) => (model.dbName ?? model.name) === identity.table.name && model.indexes.some((index) => index.name === identity.name)))
2610
+ continue;
2611
+ if (typeof facts.definition !== "string")
2612
+ throw new VibeError11({ code: "VIBE_MIGRATION", message: "Missing SQLite restoration SQL", meta: { reason: "sqlite-preservation-unavailable", identity } });
2613
+ const statements = restore.get(identity.table.name) ?? [];
2614
+ statements.push(facts.definition);
2615
+ restore.set(identity.table.name, statements);
2616
+ } else if (identity.kind === "column" || identity.kind === "generatedColumn") {
2617
+ if (typeof facts.columnDefinition !== "string")
2618
+ throw new VibeError11({ code: "VIBE_MIGRATION", message: "Missing SQLite column clause", meta: { reason: "sqlite-preservation-unavailable", identity } });
2619
+ const fields = columns.get(identity.table.name) ?? new Map;
2620
+ fields.set(identity.column, { sql: facts.columnDefinition, generated: Number(facts.generated) > 1 });
2621
+ columns.set(identity.table.name, fields);
2622
+ } else if (identity.kind === "columnDefault") {
2623
+ if (facts.definition !== null && typeof facts.definition !== "string")
2624
+ throw new VibeError11({ code: "VIBE_MIGRATION", message: "Missing SQLite default clause", meta: { reason: "sqlite-preservation-unavailable", identity } });
2625
+ const expression = facts.definition;
2626
+ to = { ...to, models: to.models.map((model) => (model.dbName ?? model.name) !== identity.table.name ? model : { ...model, fields: model.fields.map((field) => {
2627
+ if ((field.dbName ?? field.name) !== identity.column)
2628
+ return field;
2629
+ const { default: ignored, ...rest } = field;
2630
+ return { ...rest, ...expression === null ? {} : { default: { kind: "dbgenerated", sql: expression } } };
2631
+ }) }) };
2632
+ }
2633
+ }
2634
+ return { to, restore, columns };
2635
+ }
2510
2636
  function renderSteps(params) {
2511
2637
  const { steps, dialect, from, to } = params;
2512
2638
  switch (dialect) {
2513
2639
  case "postgres":
2514
2640
  return renderPostgresSteps({ steps });
2515
- case "sqlite":
2641
+ case "sqlite": {
2642
+ const preserved = declaredSqlitePreservation({ ...params, ...from === undefined ? {} : { from }, ...to === undefined ? {} : { to } });
2516
2643
  return renderSqliteSteps({
2517
2644
  steps,
2645
+ restore: params.sqliteRestore ?? preserved.restore,
2646
+ preservedColumns: params.sqliteColumns ?? preserved.columns,
2518
2647
  ...from === undefined ? {} : { from },
2519
- ...to === undefined ? {} : { to }
2648
+ ...preserved.to === undefined ? {} : { to: preserved.to }
2520
2649
  });
2650
+ }
2521
2651
  case "mysql":
2522
2652
  return renderMysqlSteps({
2523
2653
  steps,
@@ -2527,11 +2657,12 @@ function renderSteps(params) {
2527
2657
  }
2528
2658
  }
2529
2659
  // packages/migrate/src/introspect/postgres.ts
2660
+ import { renderExclusionCatalog } from "@vibeorm/sql";
2530
2661
  import { parsePgArrayLiteral } from "@vibeorm/sql";
2531
2662
  import { scalarListDefault as scalarListDefault2 } from "@vibeorm/schema";
2532
2663
  import { exactIntegerLiteral } from "@vibeorm/schema";
2533
2664
  import { postgresDialect as postgresDialect5, renderRlsReadinessQuery } from "@vibeorm/sql";
2534
- import { VibeError as VibeError11 } from "@vibeorm/schema";
2665
+ import { VibeError as VibeError13 } from "@vibeorm/schema";
2535
2666
 
2536
2667
  // packages/migrate/src/journal-table.ts
2537
2668
  var MIGRATIONS_TABLE_NAME = "_vibe_migrations";
@@ -2583,6 +2714,7 @@ function addFkRelations(params) {
2583
2714
  type: "manyToOne",
2584
2715
  fields: group.columns,
2585
2716
  references: group.refColumns,
2717
+ constraintName: group.constraintName,
2586
2718
  onDelete: group.onDelete,
2587
2719
  onUpdate: group.onUpdate,
2588
2720
  isList: false,
@@ -2710,7 +2842,7 @@ function lowerFirst(value) {
2710
2842
  }
2711
2843
 
2712
2844
  // packages/migrate/src/introspect/postgres-rls.ts
2713
- import { VibeError as VibeError10, assertValidSchemaIR as assertValidSchemaIR2 } from "@vibeorm/schema";
2845
+ import { VibeError as VibeError12, assertValidSchemaIR as assertValidSchemaIR2 } from "@vibeorm/schema";
2714
2846
  import { NATIVE_RLS, postgresDialect as postgresDialect4, renderRlsPolicy as renderRlsPolicy2 } from "@vibeorm/sql";
2715
2847
  var CATALOG_SQL = `SELECT c.relname AS table_name, c.relrowsecurity AS enabled,
2716
2848
  c.relforcerowsecurity AS force, p.polname AS policy_name, p.polcmd AS command,
@@ -2741,7 +2873,7 @@ async function readPostgresRls(params) {
2741
2873
  return [...tables.values()];
2742
2874
  }
2743
2875
  function conflict(params) {
2744
- throw new VibeError10({
2876
+ throw new VibeError12({
2745
2877
  code: "VIBE_MIGRATION",
2746
2878
  message: `Native RLS on public.${params.table} cannot be managed safely (${params.attributes.join(", ")}). Review the live policies and restore the deployed declaration, or remove conflicting policies explicitly with administrator credentials. Supply rlsSchema/previousSchema for the deployed policy; no policy is deleted or adopted automatically.`,
2747
2879
  meta: params
@@ -2963,8 +3095,13 @@ var INDEXES_QUERY = `
2963
3095
  SELECT
2964
3096
  tablename AS table_name,
2965
3097
  indexname AS index_name,
2966
- indexdef AS index_def
2967
- FROM pg_indexes
3098
+ indexdef AS index_def,
3099
+ con.conname AS backing_constraint,
3100
+ con.contype AS backing_constraint_type
3101
+ FROM pg_indexes ix
3102
+ JOIN pg_catalog.pg_namespace ns ON ns.nspname = ix.schemaname
3103
+ JOIN pg_catalog.pg_class idx ON idx.relnamespace = ns.oid AND idx.relname = ix.indexname
3104
+ LEFT JOIN pg_catalog.pg_constraint con ON con.conindid = idx.oid AND con.contype IN ('p', 'u', 'x')
2968
3105
  WHERE schemaname = 'public'
2969
3106
  ORDER BY tablename, indexname;
2970
3107
  `;
@@ -2976,11 +3113,11 @@ async function introspectPostgres(params) {
2976
3113
  const savedPath = params.inTransaction === true ? await executor(renderRlsReadinessQuery({ dialect: postgresDialect5, query: "readSearchPath" })) : undefined;
2977
3114
  const searchPath = savedPath?.[0]?.searchPath;
2978
3115
  if (params.inTransaction === true && typeof searchPath !== "string")
2979
- throw new VibeError11({ code: "VIBE_MIGRATION", message: "Cannot read the migration search_path" });
3116
+ throw new VibeError13({ code: "VIBE_MIGRATION", message: "Cannot read the migration search_path" });
2980
3117
  const matched = await verifyPostgresRls({ executor, ...params.rlsSchema === undefined ? {} : { schema: params.rlsSchema }, ...params.requireInstalledRls === undefined ? {} : { requireInstalled: params.requireInstalledRls } }).then((verified) => verified, (error) => {
2981
- if (error instanceof VibeError11)
3118
+ if (error instanceof VibeError13)
2982
3119
  throw error;
2983
- throw new VibeError11({ code: "VIBE_MIGRATION", message: "Native RLS verification could not complete. Check migration privileges and supply the deployed schema as previousSchema/rlsSchema before changing referenced columns.", meta: { reason: "rls-verification-unavailable" }, cause: error });
3120
+ throw new VibeError13({ code: "VIBE_MIGRATION", message: "Native RLS verification could not complete. Check migration privileges and supply the deployed schema as previousSchema/rlsSchema before changing referenced columns.", meta: { reason: "rls-verification-unavailable" }, cause: error });
2984
3121
  });
2985
3122
  const physical = await introspectPhysicalPostgres({ executor });
2986
3123
  if (typeof searchPath === "string")
@@ -2993,7 +3130,7 @@ async function introspectPostgres(params) {
2993
3130
  }
2994
3131
  async function introspectPhysicalPostgres(params) {
2995
3132
  const { executor } = params;
2996
- const [tableRows, viewRows, columnRows, constraintRows, fkRows, enumRows, indexRows, checkRows] = await Promise.all([
3133
+ const [tableRows, viewRows, columnRows, constraintRows, fkRows, enumRows, indexRows, checkRows, exclusionRows] = await Promise.all([
2997
3134
  executor({ text: TABLES_QUERY }),
2998
3135
  executor({ text: VIEWS_QUERY }),
2999
3136
  executor({ text: COLUMNS_QUERY }),
@@ -3001,7 +3138,8 @@ async function introspectPhysicalPostgres(params) {
3001
3138
  executor({ text: FOREIGN_KEYS_QUERY }),
3002
3139
  executor({ text: ENUMS_QUERY }),
3003
3140
  executor({ text: INDEXES_QUERY }),
3004
- executor({ text: CHECKS_QUERY })
3141
+ executor({ text: CHECKS_QUERY }),
3142
+ executor({ text: renderExclusionCatalog({ dialect: postgresDialect5 }) })
3005
3143
  ]);
3006
3144
  const tables = tableRows.map((row) => row.table_name);
3007
3145
  const views = viewRows.map((row) => row.table_name);
@@ -3035,6 +3173,7 @@ async function introspectPhysicalPostgres(params) {
3035
3173
  }
3036
3174
  const enumNames = new Set(enums.map((entry) => entry.name));
3037
3175
  const pkByTable = new Map;
3176
+ const pkNames = new Map;
3038
3177
  const uniquesByConstraint = new Map;
3039
3178
  const constraintNames = new Set;
3040
3179
  for (const row of constraintRows) {
@@ -3043,6 +3182,7 @@ async function introspectPhysicalPostgres(params) {
3043
3182
  const columnName = row.column_name;
3044
3183
  constraintNames.add(constraintName);
3045
3184
  if (row.constraint_type === "PRIMARY KEY") {
3185
+ pkNames.set(table, constraintName);
3046
3186
  const list = pkByTable.get(table);
3047
3187
  if (list === undefined)
3048
3188
  pkByTable.set(table, [columnName]);
@@ -3063,16 +3203,17 @@ async function introspectPhysicalPostgres(params) {
3063
3203
  const expression = stripCheckDefinition({ definition: String(row.definition ?? "") });
3064
3204
  const list = checksByTable.get(table);
3065
3205
  if (list === undefined)
3066
- checksByTable.set(table, [{ name, expression }]);
3206
+ checksByTable.set(table, [{ name, expression, catalogExpression: true }]);
3067
3207
  else
3068
- list.push({ name, expression });
3208
+ list.push({ name, expression, catalogExpression: true });
3069
3209
  }
3070
3210
  const fkGroupsByName = new Map;
3071
3211
  for (const row of fkRows) {
3072
3212
  const constraintName = row.constraint_name;
3073
- const existing = fkGroupsByName.get(constraintName);
3213
+ const key = JSON.stringify([row.table_name, constraintName]);
3214
+ const existing = fkGroupsByName.get(key);
3074
3215
  if (existing === undefined) {
3075
- fkGroupsByName.set(constraintName, {
3216
+ fkGroupsByName.set(key, {
3076
3217
  constraintName,
3077
3218
  table: row.table_name,
3078
3219
  columns: [row.column_name],
@@ -3095,6 +3236,8 @@ async function introspectPhysicalPostgres(params) {
3095
3236
  }));
3096
3237
  const indexes = [];
3097
3238
  for (const row of indexRows) {
3239
+ if (row.backing_constraint_type === "x")
3240
+ continue;
3098
3241
  const parsed = parseIndexDef({
3099
3242
  table: row.table_name,
3100
3243
  name: row.index_name,
@@ -3123,10 +3266,12 @@ async function introspectPhysicalPostgres(params) {
3123
3266
  isView: false,
3124
3267
  columns,
3125
3268
  pkByTable,
3269
+ pkNames,
3126
3270
  uniquesByConstraint,
3127
3271
  constraintNames,
3128
3272
  indexes,
3129
3273
  enumNames,
3274
+ exclusions: exclusionRows.filter((row) => row.table_name === table).map((row) => ({ name: String(row.constraint_name), definition: String(row.definition), requiredExtensions: [], catalogDefinition: true })),
3130
3275
  checks: checksByTable.get(table) ?? []
3131
3276
  }));
3132
3277
  }
@@ -3136,6 +3281,7 @@ async function introspectPhysicalPostgres(params) {
3136
3281
  isView: true,
3137
3282
  columns,
3138
3283
  pkByTable,
3284
+ pkNames,
3139
3285
  uniquesByConstraint,
3140
3286
  constraintNames,
3141
3287
  indexes: [],
@@ -3163,8 +3309,7 @@ function buildModel(params) {
3163
3309
  continue;
3164
3310
  if (unique.columns.length === 1)
3165
3311
  uniqueSingles.add(unique.columns[0]);
3166
- if (unique.columns.length !== 1 || name !== `${table}_${unique.columns[0]}_key`)
3167
- multiUniques.push({ name, columns: unique.columns });
3312
+ multiUniques.push({ name, columns: unique.columns });
3168
3313
  }
3169
3314
  const tableIndexes = indexes.filter((index) => index.table === table && !constraintNames.has(index.name));
3170
3315
  for (const index of tableIndexes) {
@@ -3179,6 +3324,7 @@ function buildModel(params) {
3179
3324
  for (const unique of multiUniques) {
3180
3325
  modelIndexes.push({
3181
3326
  name: unique.name,
3327
+ constraintName: unique.name,
3182
3328
  kind: "unique",
3183
3329
  fields: unique.columns.map((name) => ({ name }))
3184
3330
  });
@@ -3203,9 +3349,11 @@ function buildModel(params) {
3203
3349
  fields,
3204
3350
  relations: [],
3205
3351
  indexes: modelIndexes,
3352
+ ...params.exclusions?.length ? { exclusions: params.exclusions } : {},
3206
3353
  ...params.checks.length === 0 ? {} : { checks: params.checks },
3207
3354
  isView,
3208
- ...pk.length > 1 ? { primaryKey: { fields: pk } } : {}
3355
+ ...pk.length > 1 ? { primaryKey: { fields: pk } } : {},
3356
+ ...params.pkNames.get(table) === undefined ? {} : { primaryKeyConstraintName: params.pkNames.get(table) }
3209
3357
  };
3210
3358
  }
3211
3359
  function stripCheckDefinition(params) {
@@ -3426,7 +3574,7 @@ function toNumberOrNull(value) {
3426
3574
  }
3427
3575
  // packages/migrate/src/introspect/sqlite.ts
3428
3576
  import { exactIntegerLiteral as exactIntegerLiteral2 } from "@vibeorm/schema";
3429
- import { sqliteDialect as sqliteDialect2 } from "@vibeorm/sql";
3577
+ import { sqliteDialect as sqliteDialect2, sqliteTableConstraints } from "@vibeorm/sql";
3430
3578
  var SQLITE_ACTION_TO_CASCADE = {
3431
3579
  "NO ACTION": "NoAction",
3432
3580
  RESTRICT: "Restrict",
@@ -3490,7 +3638,7 @@ async function introspectSqlite(params) {
3490
3638
  indexes.push(...tableIndexes);
3491
3639
  uniqueSinglesByTable.set(table.name, uniqueSingles);
3492
3640
  modelIndexesByTable.set(table.name, modelIndexes);
3493
- fkGroups.push(...await readForeignKeys({ executor, table: table.name, columns }));
3641
+ fkGroups.push(...await readForeignKeys({ executor, table: table.name, columns, tableSql: table.sql ?? "" }));
3494
3642
  }
3495
3643
  const joinTables = new Map;
3496
3644
  for (const table of tables) {
@@ -3689,7 +3837,7 @@ async function readForeignKeys(params) {
3689
3837
  }
3690
3838
  }
3691
3839
  return [...groups.values()].map((group) => ({
3692
- constraintName: `${table}_${group.columns.join("_")}_fkey`,
3840
+ constraintName: sqliteTableConstraints({ sql: params.tableSql }).find((clause) => clause.named && clause.type === "foreignKey" && JSON.stringify(clause.columns) === JSON.stringify(group.columns))?.name ?? `${table}_${group.columns.join("_")}_fkey`,
3693
3841
  table,
3694
3842
  columns: group.columns,
3695
3843
  refTable: group.refTable,
@@ -3729,7 +3877,7 @@ function buildModel2(params) {
3729
3877
  fields,
3730
3878
  relations: [],
3731
3879
  indexes: [...modelIndexes],
3732
- ...params.checks.length === 0 ? {} : { checks: params.checks },
3880
+ ...params.checks.length === 0 ? {} : { checks: params.checks.map((check) => ({ ...check, catalogExpression: true })) },
3733
3881
  isView,
3734
3882
  ...pk.length > 1 ? { primaryKey: { fields: pk } } : {}
3735
3883
  };
@@ -3800,6 +3948,7 @@ var COLUMNS_QUERY2 = `
3800
3948
  column_type,
3801
3949
  is_nullable,
3802
3950
  column_default,
3951
+ collation_name,
3803
3952
  extra,
3804
3953
  ordinal_position
3805
3954
  FROM information_schema.columns
@@ -3878,9 +4027,9 @@ async function introspectMysql(params) {
3878
4027
  const expression = stripBalancedOuterParens(String(catalogField({ row, name: "definition" }) ?? ""));
3879
4028
  const list = checksByTable.get(table);
3880
4029
  if (list === undefined)
3881
- checksByTable.set(table, [{ name, expression }]);
4030
+ checksByTable.set(table, [{ name, expression, catalogExpression: true }]);
3882
4031
  else
3883
- list.push({ name, expression });
4032
+ list.push({ name, expression, catalogExpression: true });
3884
4033
  }
3885
4034
  const tables = tableRows.map((row) => catalogField({ row, name: "table_name" }));
3886
4035
  const views = viewRows.map((row) => catalogField({ row, name: "table_name" }));
@@ -3890,6 +4039,7 @@ async function introspectMysql(params) {
3890
4039
  columnType: catalogField({ row, name: "column_type" }),
3891
4040
  nullable: catalogField({ row, name: "is_nullable" }) === "YES",
3892
4041
  columnDefault: catalogField({ row, name: "column_default" }) ?? null,
4042
+ collation: catalogField({ row, name: "collation_name" }) ?? null,
3893
4043
  extra: (catalogField({ row, name: "extra" }) ?? "").toLowerCase()
3894
4044
  }));
3895
4045
  const rulesByConstraint = new Map;
@@ -4046,7 +4196,8 @@ function buildModel3(params) {
4046
4196
  isUpdatedAt: false,
4047
4197
  ...defaultValue === undefined ? {} : { default: defaultValue },
4048
4198
  ...mapped.nativeType === undefined ? {} : { nativeType: mapped.nativeType },
4049
- ...mapped.isUnsupported === true ? { isUnsupported: true } : {}
4199
+ ...mapped.isUnsupported === true ? { isUnsupported: true } : {},
4200
+ ...mapped.type === "String" && column.collation !== null ? { collation: column.collation } : {}
4050
4201
  };
4051
4202
  });
4052
4203
  const modelIndexes = [];
@@ -4188,17 +4339,17 @@ function isNumericScalar2(params) {
4188
4339
  }
4189
4340
  // packages/migrate/src/runner.ts
4190
4341
  import { createHash } from "crypto";
4191
- import { VibeError as VibeError15 } from "@vibeorm/schema";
4342
+ import { VibeError as VibeError21 } from "@vibeorm/schema";
4192
4343
  import {
4193
- DIALECT_CAPABILITIES as DIALECT_CAPABILITIES2,
4194
- getSqlDialect as getSqlDialect2,
4344
+ DIALECT_CAPABILITIES as DIALECT_CAPABILITIES3,
4345
+ getSqlDialect as getSqlDialect3,
4195
4346
  renderMigrationSessionRead,
4196
4347
  renderMigrationSessionRestore,
4197
4348
  renderMigrationSessionSet
4198
4349
  } from "@vibeorm/sql";
4199
4350
 
4200
4351
  // packages/migrate/src/execution.ts
4201
- import { VibeError as VibeError12 } from "@vibeorm/schema";
4352
+ import { VibeError as VibeError14 } from "@vibeorm/schema";
4202
4353
  var EXECUTION_SIDECAR_FILENAME = "execution.json";
4203
4354
  var EXECUTION_SIDECAR_VERSION = 1;
4204
4355
  function isDefaultExecution(params) {
@@ -4237,7 +4388,7 @@ ${lines.join(`
4237
4388
  `)}`;
4238
4389
  }
4239
4390
  function invalid(params) {
4240
- return new VibeError12({
4391
+ return new VibeError14({
4241
4392
  code: "VIBE_MIGRATION",
4242
4393
  message: `invalid execution metadata for migration "${params.migration}": ${params.detail}`,
4243
4394
  meta: { reason: "execution-metadata-invalid", migration: params.migration }
@@ -4288,7 +4439,7 @@ function parseExecutionSidecar(params) {
4288
4439
  try {
4289
4440
  parsed = JSON.parse(params.text);
4290
4441
  } catch (error) {
4291
- throw new VibeError12({
4442
+ throw new VibeError14({
4292
4443
  code: "VIBE_MIGRATION",
4293
4444
  message: `invalid execution metadata for migration "${migration}": not valid JSON`,
4294
4445
  meta: { reason: "execution-metadata-invalid", migration },
@@ -4315,6 +4466,13 @@ function describeMigrationRisks(params) {
4315
4466
  const risks = [];
4316
4467
  for (const step of steps) {
4317
4468
  switch (step.kind) {
4469
+ case "dropExclusion":
4470
+ case "dropCheck":
4471
+ case "dropForeignKey":
4472
+ case "dropPrimaryKey":
4473
+ case "dropIndex":
4474
+ risks.push({ kind: "integrityLoss", detail: `${step.kind} removes a constraint or index; integrity and dependent objects must be checked separately from row loss`, ..."table" in step && step.table !== undefined ? { table: step.table } : {} });
4475
+ break;
4318
4476
  case "alterColumn":
4319
4477
  if (step.changes.includes("type")) {
4320
4478
  risks.push({ kind: "tableRewrite", table: step.table, detail: `changing the type of "${step.table}"."${step.column}" rewrites every row of the table while holding ACCESS EXCLUSIVE` });
@@ -4350,8 +4508,11 @@ function describeMigrationRisks(params) {
4350
4508
  risks.push({ kind: "exclusiveLock", table: step.table, detail: `renaming "${step.table}"."${step.from}" to "${step.to}" takes ACCESS EXCLUSIVE and breaks every client still using the old name` });
4351
4509
  break;
4352
4510
  case "dropTable":
4353
- case "dropColumn":
4354
4511
  case "dropJoinTable":
4512
+ risks.push({ kind: "integrityLoss", table: step.table, detail: `${step.kind} also removes the table's constraints and indexes; their integrity loss requires separate acknowledgement` });
4513
+ risks.push({ kind: "exclusiveLock", detail: `${step.kind} takes ACCESS EXCLUSIVE and is not recoverable by re-running the migration` });
4514
+ break;
4515
+ case "dropColumn":
4355
4516
  risks.push({ kind: "exclusiveLock", detail: `${step.kind} takes ACCESS EXCLUSIVE and is not recoverable by re-running the migration` });
4356
4517
  break;
4357
4518
  default:
@@ -4368,7 +4529,7 @@ function describeMigrationRisks(params) {
4368
4529
  }
4369
4530
 
4370
4531
  // packages/migrate/src/recovery.ts
4371
- import { VibeError as VibeError13 } from "@vibeorm/schema";
4532
+ import { VibeError as VibeError15 } from "@vibeorm/schema";
4372
4533
  import {
4373
4534
  getSqlDialect,
4374
4535
  renderColumnStateQuery,
@@ -4377,7 +4538,7 @@ import {
4377
4538
  renderRelationStateQuery
4378
4539
  } from "@vibeorm/sql";
4379
4540
  function conflict2(params) {
4380
- return new VibeError13({
4541
+ return new VibeError15({
4381
4542
  code: "VIBE_MIGRATION",
4382
4543
  message: params.message,
4383
4544
  meta: { reason: params.reason, ...params.meta }
@@ -4605,7 +4766,7 @@ function artifactCreateSql(params) {
4605
4766
  }
4606
4767
 
4607
4768
  // packages/migrate/src/execute.ts
4608
- import { VibeError as VibeError14 } from "@vibeorm/schema";
4769
+ import { VibeError as VibeError16 } from "@vibeorm/schema";
4609
4770
  var SELF_MANAGED_MARKER = "PRAGMA foreign_keys=OFF";
4610
4771
  var FK_CHECK_PREFIX = "PRAGMA foreign_key_check";
4611
4772
  function usesSelfManagedTransactions(params) {
@@ -4646,7 +4807,7 @@ function assertSelfManagedResumeSafe(params) {
4646
4807
  const completedIndex = params.direction === "up" ? params.start + 1 : -(params.start + 2);
4647
4808
  const rewind = selfManagedResumeStart({ sql: params.sql, start: params.start });
4648
4809
  const retryIndex = params.direction === "up" ? rewind : -(rewind + 1);
4649
- throw new VibeError14({
4810
+ throw new VibeError16({
4650
4811
  code: "VIBE_MIGRATION",
4651
4812
  message: `migration "${params.migration}" has a legacy sqlite checkpoint at ${params.direction} COMMIT (statement ${params.start + 1}). The block may already have committed; replay is refused. Inspect the block's effects. If it committed, set its journal statement_index to ${completedIndex} and error to NULL to resume the remaining statements. Only if the entire block did not commit, set statement_index to ${retryIndex} and error to NULL to retry the block. Do not mark the whole migration applied unless all remaining statements also ran.`,
4652
4813
  meta: { reason: "uncertain-statement", migration: params.migration, statementIndex: params.start, direction: params.direction, dialect: "sqlite" }
@@ -4675,7 +4836,7 @@ function unsupportedScript(params) {
4675
4836
  const { migration, direction, index, problem, found } = params;
4676
4837
  const subject = direction === "down" ? `the down script of migration "${migration}"` : `migration "${migration}"`;
4677
4838
  const position = `${direction === "down" ? "down statement" : "statement"} ${index + 1} of ${params.statements}`;
4678
- return new VibeError14({
4839
+ return new VibeError16({
4679
4840
  code: "VIBE_MIGRATION",
4680
4841
  message: `${subject} manages its own sqlite transactions, and ${position} breaks the shape recovery depends on: ${found}. ` + `A table-rebuild block must be written exactly as \`${SUPPORTED_BLOCK_SHAPE}\`. ` + "Inside that block a crash rolls everything back, so a resume can replay it safely. " + "A statement that sits in a foreign-keys-off region with no transaction around it commits on its own, " + "and the resume would run it a second time with no error raised either time. " + "Nothing has been executed. Move the statement after `PRAGMA foreign_keys=ON`, where it commits together " + "with its own journal checkpoint, or give it its own `BEGIN;` and `COMMIT;`. " + 'See "Self-managed sqlite scripts" in docs/migrations.md.',
4681
4842
  meta: { reason: "unsupported-self-managed-script", problem, migration, statementIndex: index, direction }
@@ -4731,7 +4892,7 @@ async function executeStatement(params) {
4731
4892
  const rows = await executor({ text: statement, values: [] });
4732
4893
  if (rows.length > 0) {
4733
4894
  const sample = rows.slice(0, 5).map((row) => `${String(row.table)} rowid ${String(row.rowid)} \u2192 ${String(row.parent)}`).join("; ");
4734
- throw new VibeError14({
4895
+ throw new VibeError16({
4735
4896
  code: "VIBE_MIGRATION",
4736
4897
  message: `foreign key check failed after table rebuild: ${rows.length} violation(s) \u2014 ${sample}`,
4737
4898
  meta: { violations: rows }
@@ -4749,6 +4910,767 @@ async function cleanupSelfManagedFailure(params) {
4749
4910
  await params.executor({ text: "PRAGMA foreign_keys=ON;" });
4750
4911
  } catch {}
4751
4912
  }
4913
+ // packages/migrate/src/desired-state.ts
4914
+ import { VibeError as VibeError17, assertValidSchemaIR as assertValidSchemaIR3, isSqlObjectDefinition, isSqlObjectIdentity, providerToDialect, sqlObjectKey as sqlObjectKey2 } from "@vibeorm/schema";
4915
+ var UNFINALIZED_DESIRED_STATE = Object.freeze({ version: 1, status: "unfinalized" });
4916
+ function isUnfinalizedDesiredState(params) {
4917
+ const value = JSON.parse(params.text);
4918
+ return record(value) && value.version === 1 && value.status === "unfinalized" && Object.keys(value).length === 2;
4919
+ }
4920
+ var DESIRED_STATE_METADATA = Object.freeze({ filename: "desired-state.json", version: 1 });
4921
+ function createDesiredState(params) {
4922
+ assertValidSchemaIR3({ schema: params.schema });
4923
+ const provider = providerToDialect({ provider: params.schema.datasource.provider }) === params.dialect ? params.schema.datasource.provider : params.dialect === "postgres" ? "postgresql" : params.dialect;
4924
+ const schema = { ...params.schema, datasource: { provider } };
4925
+ const definitions = params.definitions ?? (schema.migrationObjects ?? []).flatMap((object) => object.expected === undefined ? [] : [object.expected]);
4926
+ const seen = new Set;
4927
+ for (const definition of definitions) {
4928
+ if (!isSqlObjectDefinition(definition))
4929
+ throw invalid2({ migration: "declaration", detail: "malformed expected definition" });
4930
+ const key = sqlObjectKey2({ identity: definition.identity });
4931
+ if (seen.has(key))
4932
+ throw invalid2({ migration: "declaration", detail: `duplicate definition ${key}` });
4933
+ seen.add(key);
4934
+ }
4935
+ return { schema, definitions };
4936
+ }
4937
+ function invalid2(params) {
4938
+ return new VibeError17({ code: "VIBE_MIGRATION", message: `Invalid desired state for migration "${params.migration}": ${params.detail}`, meta: { reason: "desired-state-invalid", migration: params.migration }, ...params.cause === undefined ? {} : { cause: params.cause } });
4939
+ }
4940
+ function record(value) {
4941
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4942
+ }
4943
+ function keysOnly(value, allowed) {
4944
+ return Object.keys(value).every((key) => allowed.includes(key));
4945
+ }
4946
+ function modelShape(value) {
4947
+ if (!record(value) || typeof value.name !== "string" || !Array.isArray(value.fields) || !Array.isArray(value.relations) || !Array.isArray(value.indexes))
4948
+ return false;
4949
+ return value.fields.every((field) => record(field) && typeof field.name === "string" && (typeof field.type === "string" || record(field.type) && typeof field.type.enum === "string") && ["isId", "isUnique", "isOptional", "isList"].every((key) => typeof field[key] === "boolean")) && value.relations.every(record) && value.indexes.every((index) => record(index) && Array.isArray(index.fields) && index.fields.every(record)) && (value.checks === undefined || Array.isArray(value.checks) && value.checks.every((check) => record(check) && typeof check.name === "string" && typeof check.expression === "string"));
4950
+ }
4951
+ function state(value) {
4952
+ if (!record(value) || !keysOnly(value, ["schema", "definitions", "normalization"]) || !record(value.schema) || !Array.isArray(value.definitions) || !value.definitions.every(isSqlObjectDefinition))
4953
+ return false;
4954
+ const schema = value.schema;
4955
+ return record(schema.datasource) && ["postgresql", "pglite", "sqlite", "mysql"].includes(String(schema.datasource.provider)) && keysOnly(schema, ["datasource", "models", "enums", "extensions", "rls", "migrationObjects"]) && keysOnly(schema.datasource, ["provider"]) && Array.isArray(schema.models) && schema.models.every(modelShape) && Array.isArray(schema.enums) && schema.enums.every((entry) => record(entry) && typeof entry.name === "string" && Array.isArray(entry.values) && entry.values.every((label) => typeof label === "string")) && Array.isArray(schema.extensions) && schema.extensions.every(record) && (value.normalization === undefined || record(value.normalization) && value.normalization.format === "catalog-v1" && ["postgres", "sqlite", "mysql"].includes(String(value.normalization.dialect)) && typeof value.normalization.serverVersion === "string");
4956
+ }
4957
+ function assertDesiredState(params) {
4958
+ const value = params.value;
4959
+ if (!record(value) || !keysOnly(value, ["version", "dialect", "from", "to", "down", "unmanagedRemovals"]) || value.version !== 1 || !["postgres", "sqlite", "mysql"].includes(String(value.dialect)) || !state(value.from) || !state(value.to) || value.down !== undefined && !state(value.down)) {
4960
+ throw invalid2({ migration: params.migration, detail: "unsupported version or malformed state" });
4961
+ }
4962
+ if (value.unmanagedRemovals !== undefined && (!Array.isArray(value.unmanagedRemovals) || !value.unmanagedRemovals.every(isSqlObjectIdentity)))
4963
+ throw invalid2({ migration: params.migration, detail: "malformed unmanaged-removal identities" });
4964
+ for (const entry of [value.from, value.to, ...value.down === undefined ? [] : [value.down]]) {
4965
+ assertValidSchemaIR3({ schema: entry.schema });
4966
+ if (providerToDialect({ provider: entry.schema.datasource.provider }) !== value.dialect)
4967
+ throw invalid2({ migration: params.migration, detail: "schema provider differs from sidecar dialect" });
4968
+ if (entry.normalization !== undefined && entry.normalization.dialect !== value.dialect)
4969
+ throw invalid2({ migration: params.migration, detail: "normalization dialect differs from sidecar dialect" });
4970
+ const keys = entry.definitions.map((definition) => sqlObjectKey2({ identity: definition.identity }));
4971
+ if (new Set(keys).size !== keys.length)
4972
+ throw invalid2({ migration: params.migration, detail: "duplicate definition" });
4973
+ }
4974
+ }
4975
+ function canonicalMigrationJson(params) {
4976
+ const value = params.value;
4977
+ if (Array.isArray(value))
4978
+ return `[${value.map((entry) => canonicalMigrationJson({ value: entry })).join(",")}]`;
4979
+ if (record(value))
4980
+ return `{${Object.keys(value).filter((key) => value[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${canonicalMigrationJson({ value: value[key] })}`).join(",")}}`;
4981
+ return JSON.stringify(value) ?? "null";
4982
+ }
4983
+ function desiredStateChecksumSuffix(params) {
4984
+ if (params.desiredState === undefined)
4985
+ return "";
4986
+ assertDesiredState({ value: params.desiredState, migration: "checksum" });
4987
+ return `
4988
+ -- vibeorm:desired-state ${canonicalMigrationJson({ value: params.desiredState })}`;
4989
+ }
4990
+ function serializeDesiredStateSidecar(params) {
4991
+ assertDesiredState({ value: params.desiredState, migration: "serialize" });
4992
+ return JSON.stringify(params.desiredState, null, 2) + `
4993
+ `;
4994
+ }
4995
+ function parseDesiredStateSidecar(params) {
4996
+ try {
4997
+ const value = JSON.parse(params.text);
4998
+ const checked = { value, migration: params.migration };
4999
+ assertDesiredState(checked);
5000
+ return checked.value;
5001
+ } catch (cause) {
5002
+ throw invalid2({ migration: params.migration, detail: "malformed metadata", cause });
5003
+ }
5004
+ }
5005
+
5006
+ // packages/migrate/src/exclusions.ts
5007
+ import { randomUUID } from "crypto";
5008
+ import { VibeError as VibeError18, sqlObjectKey as sqlObjectKey3 } from "@vibeorm/schema";
5009
+ import { postgresDialect as postgresDialect6, renderExclusionCatalog as renderExclusionCatalog2, renderExclusionProbe } from "@vibeorm/sql";
5010
+ async function deparseExclusion(params) {
5011
+ const probe = `vibe_exclusion_${randomUUID().replaceAll("-", "")}`;
5012
+ await params.executor({ text: "SAVEPOINT vibe_exclusion_probe" });
5013
+ try {
5014
+ for (const text of renderExclusionProbe({ dialect: postgresDialect6, table: params.table, probe, definition: params.definition }))
5015
+ await params.executor({ text });
5016
+ const rows = await params.executor({ text: renderExclusionCatalog2({ dialect: postgresDialect6, probe }) });
5017
+ if (typeof rows[0]?.definition !== "string")
5018
+ throw new VibeError18({ code: "VIBE_MIGRATION", message: "Exclusion probe unavailable", meta: { reason: "exclusion-inspection-unavailable" } });
5019
+ return rows[0].definition;
5020
+ } finally {
5021
+ await params.executor({ text: "ROLLBACK TO SAVEPOINT vibe_exclusion_probe" });
5022
+ await params.executor({ text: "RELEASE SAVEPOINT vibe_exclusion_probe" });
5023
+ }
5024
+ }
5025
+ async function prepareExclusionDiff(params) {
5026
+ const models = [];
5027
+ for (const model of params.from.models) {
5028
+ const table = model.dbName ?? model.name;
5029
+ const target = params.to.models.find((entry) => (entry.dbName ?? entry.name) === table);
5030
+ const exclusions = [];
5031
+ for (const before of model.exclusions ?? []) {
5032
+ const next = target?.exclusions?.find((entry) => entry.name === before.name);
5033
+ if (next === undefined || next.definition === before.definition) {
5034
+ exclusions.push(before);
5035
+ continue;
5036
+ }
5037
+ const identity = { kind: "constraint", constraintType: "exclusion", table: { namespace: "public", name: table }, name: before.name };
5038
+ if (params.probe === "rollback") {
5039
+ const normalized = await deparseExclusion({ executor: params.executor, table, definition: next.definition });
5040
+ exclusions.push(normalized === before.definition ? next : { name: before.name, definition: before.definition, requiredExtensions: before.requiredExtensions });
5041
+ continue;
5042
+ }
5043
+ const previous = params.previous?.schema.models.find((entry) => (entry.dbName ?? entry.name) === table)?.exclusions?.find((entry) => entry.name === before.name);
5044
+ if (previous !== undefined) {
5045
+ exclusions.push(previous);
5046
+ continue;
5047
+ }
5048
+ if (params.replaceChecks?.some((entry) => sqlObjectKey3({ identity: entry }) === sqlObjectKey3({ identity }))) {
5049
+ exclusions.push({ name: before.name, definition: before.definition, requiredExtensions: before.requiredExtensions });
5050
+ continue;
5051
+ }
5052
+ throw new VibeError18({ code: "VIBE_MIGRATION", message: "Exclusion equivalence requires a rollback probe or explicit replacement", meta: { reason: "explicit-replacement-required", identity } });
5053
+ }
5054
+ models.push({ ...model, exclusions });
5055
+ }
5056
+ return { ...params.from, models };
5057
+ }
5058
+ async function verifyExclusions(params) {
5059
+ if (!params.expected.schema.models.some((model) => (model.exclusions?.length ?? 0) > 0))
5060
+ return [];
5061
+ const rows = await params.executor({ text: renderExclusionCatalog2({ dialect: postgresDialect6 }) });
5062
+ const objects = [];
5063
+ for (const model of params.expected.schema.models)
5064
+ for (const exclusion of model.exclusions ?? []) {
5065
+ const table = model.dbName ?? model.name;
5066
+ const identity = { kind: "constraint", constraintType: "exclusion", table: { namespace: "public", name: table }, name: exclusion.name };
5067
+ const row = rows.find((entry) => entry.table_name === table && entry.constraint_name === exclusion.name);
5068
+ if (row === undefined) {
5069
+ objects.push({ identity, ownership: "schema", state: "missing", differences: [] });
5070
+ continue;
5071
+ }
5072
+ const differences = ["validated", "valid", "ready"].flatMap((attribute) => row[attribute] === true ? [] : [{ attribute, expected: true, actual: row[attribute] }]);
5073
+ const independentlyVerified = params.expected.definitions.some((entry) => sqlObjectKey3({ identity: entry.identity }) === sqlObjectKey3({ identity }));
5074
+ const definition = independentlyVerified ? row.definition : params.probe === "rollback" ? await deparseExclusion({ executor: params.executor, table, definition: exclusion.definition }) : (exclusion.normalizationSource === exclusion.definition ? exclusion.normalizedDefinition : undefined) ?? exclusion.definition;
5075
+ if (definition !== row.definition)
5076
+ differences.push({ attribute: "definition", expected: definition, actual: row.definition });
5077
+ objects.push({ identity, ownership: "schema", state: differences.length === 0 ? "matching" : params.probe === "rollback" ? "changed" : "unavailable", differences });
5078
+ }
5079
+ return objects;
5080
+ }
5081
+
5082
+ // packages/migrate/src/inspect-objects.ts
5083
+ import { randomUUID as randomUUID2 } from "crypto";
5084
+ import { VibeError as VibeError20, sqlObjectKey as sqlObjectKey5, DIALECT_CAPABILITIES as DIALECT_CAPABILITIES2 } from "@vibeorm/schema";
5085
+ import { getSqlDialect as getSqlDialect2, renderMigrationObjectQuery, renderCheckProbe, renderMigrationNamespace, renderMigrationDependencies, renderMysqlObjectMembers, renderMysqlCheckProbe, sqliteColumnClauses, renderMigrationVisibility, sqliteTableConstraints as sqliteTableConstraints2 } from "@vibeorm/sql";
5086
+
5087
+ // packages/migrate/src/object-ownership.ts
5088
+ import { VibeError as VibeError19, sqlObjectKey as sqlObjectKey4 } from "@vibeorm/schema";
5089
+ function legacyMigrationObjects(params) {
5090
+ return params.artifacts.map((artifact) => {
5091
+ const owner = artifact.owns;
5092
+ const table = "table" in owner ? { namespace: params.namespace, name: owner.table } : undefined;
5093
+ let identity;
5094
+ switch (owner.kind) {
5095
+ case "postgresExtension":
5096
+ case "table":
5097
+ identity = { kind: owner.kind, name: { namespace: params.namespace, name: owner.name } };
5098
+ break;
5099
+ case "column":
5100
+ identity = { kind: "column", table, column: owner.column };
5101
+ break;
5102
+ case "index":
5103
+ case "trigger":
5104
+ identity = { kind: owner.kind, table, name: owner.name };
5105
+ break;
5106
+ }
5107
+ return { id: artifact.id, dialect: params.dialect, identity, create: artifact.create, drop: artifact.drop, dependsOn: artifact.expected?.dependencies ?? [], ...artifact.expected === undefined ? {} : { expected: artifact.expected } };
5108
+ });
5109
+ }
5110
+ function preserveMigrationObjects(params) {
5111
+ const namespace = params.namespace ?? (params.to.datasource.provider === "sqlite" ? "main" : "public");
5112
+ const objects = params.objects.filter((object) => ("table" in object.identity ? object.identity.table.namespace : object.identity.name.namespace) === namespace);
5113
+ const models = params.from.models.flatMap((model) => {
5114
+ const table = model.dbName ?? model.name;
5115
+ const target = params.to.models.find((entry) => (entry.dbName ?? entry.name) === table);
5116
+ if (objects.some((object) => (object.identity.kind === "table" || object.identity.kind === "view") && object.identity.name.name === table))
5117
+ return [];
5118
+ const owners = objects.map((object) => object.identity).filter((identity) => ("table" in identity) && identity.table.name === table);
5119
+ return [{
5120
+ ...model,
5121
+ fields: model.fields.flatMap((field) => {
5122
+ const column = field.dbName ?? field.name;
5123
+ if (owners.some((owner) => owner.kind === "column" && owner.column === column))
5124
+ return [];
5125
+ if (owners.some((owner) => (owner.kind === "columnDefault" || owner.kind === "generatedColumn") && owner.column === column)) {
5126
+ const authored = target?.fields.find((entry) => (entry.dbName ?? entry.name) === column);
5127
+ if (authored === undefined)
5128
+ return [field];
5129
+ const { default: ignored, ...rest } = field;
5130
+ return [{ ...rest, ...authored.default === undefined ? {} : { default: authored.default } }];
5131
+ }
5132
+ return [field];
5133
+ }),
5134
+ exclusions: (model.exclusions ?? []).filter((exclusion) => !owners.some((owner) => owner.kind === "constraint" && owner.constraintType === "exclusion" && owner.name === exclusion.name)),
5135
+ checks: (model.checks ?? []).filter((check) => !owners.some((owner) => owner.kind === "constraint" && owner.constraintType === "check" && owner.name === check.name)),
5136
+ indexes: model.indexes.filter((index) => !owners.some((owner) => owner.kind === "index" && owner.name === index.name)),
5137
+ relations: model.relations.filter((relation) => !owners.some((owner) => owner.kind === "constraint" && owner.constraintType === "foreignKey" && owner.name === relation.constraintName))
5138
+ }];
5139
+ });
5140
+ return { ...params.from, models };
5141
+ }
5142
+ function sqlitePreservedSchema(params) {
5143
+ return { ...params.to, models: params.to.models.map((model) => {
5144
+ const table = model.dbName ?? model.name;
5145
+ const before = params.from.models.find((entry) => (entry.dbName ?? entry.name) === table);
5146
+ if (before === undefined)
5147
+ return model;
5148
+ const owners = params.objects.map((object) => object.identity).filter((owner) => ("table" in owner) && owner.table.namespace === "main" && owner.table.name === table);
5149
+ return {
5150
+ ...model,
5151
+ checks: [...model.checks ?? [], ...(before.checks ?? []).filter((check) => !model.checks?.some((entry) => entry.name === check.name) && owners.some((owner) => owner.kind === "constraint" && owner.constraintType === "check" && owner.name === check.name))],
5152
+ indexes: [...model.indexes, ...before.indexes.filter((index) => !model.indexes.some((entry) => entry.name === index.name) && owners.some((owner) => owner.kind === "index" && owner.name === index.name))],
5153
+ relations: [...model.relations, ...before.relations.filter((relation) => !model.relations.some((entry) => entry.constraintName === relation.constraintName) && owners.some((owner) => owner.kind === "constraint" && owner.constraintType === "foreignKey" && owner.name === relation.constraintName))]
5154
+ };
5155
+ }) };
5156
+ }
5157
+ function removalIdentity(params) {
5158
+ const { step, namespace } = params;
5159
+ const table = "table" in step && step.table !== undefined ? { namespace, name: step.table } : undefined;
5160
+ switch (step.kind) {
5161
+ case "dropTable":
5162
+ case "dropJoinTable":
5163
+ return { kind: "table", name: table };
5164
+ case "dropView":
5165
+ return { kind: "view", name: table };
5166
+ case "dropColumn":
5167
+ return { kind: "column", table, column: step.column };
5168
+ case "alterColumn":
5169
+ return step.changes.includes("default") && step.from.default !== undefined ? { kind: "columnDefault", table, column: step.column } : undefined;
5170
+ case "dropIndex":
5171
+ return table === undefined ? undefined : step.constraintName === undefined ? { kind: "index", table, name: step.name } : { kind: "constraint", constraintType: "unique", table, name: step.constraintName };
5172
+ case "dropExclusion":
5173
+ return { kind: "constraint", constraintType: "exclusion", table, name: step.exclusion.name };
5174
+ case "dropCheck":
5175
+ return { kind: "constraint", constraintType: "check", table, name: step.name };
5176
+ case "dropForeignKey":
5177
+ return { kind: "constraint", constraintType: "foreignKey", table, name: step.constraintName };
5178
+ case "dropPrimaryKey":
5179
+ return { kind: "constraint", constraintType: "primaryKey", table, name: step.constraintName ?? `${step.table}_pkey` };
5180
+ default:
5181
+ return;
5182
+ }
5183
+ }
5184
+ function schemaObjectIdentities(params) {
5185
+ const normalized = normalizeSchema({ schema: params.schema, dialect: params.dialect });
5186
+ const result = [];
5187
+ for (const [name, model] of normalized.tables) {
5188
+ const table = { namespace: params.namespace, name };
5189
+ result.push({ kind: "table", name: table });
5190
+ for (const [column, definition] of model.columns) {
5191
+ result.push({ kind: "column", table, column });
5192
+ if (definition.defaultKey !== null)
5193
+ result.push({ kind: "columnDefault", table, column });
5194
+ if (params.dialect === "postgres" && definition.field.default?.kind === "autoincrement")
5195
+ result.push({ kind: "sequence", name: { namespace: params.namespace, name: `${name}_${column}_seq` } });
5196
+ }
5197
+ for (const exclusion of model.model.exclusions ?? [])
5198
+ result.push({ kind: "constraint", constraintType: "exclusion", table, name: exclusion.name });
5199
+ for (const check of model.checks.keys())
5200
+ result.push({ kind: "constraint", constraintType: "check", table, name: check });
5201
+ for (const index of model.indexes.values()) {
5202
+ result.push({ kind: "index", table, name: index.name });
5203
+ if (index.constraintName !== undefined)
5204
+ result.push({ kind: "constraint", constraintType: "unique", table, name: index.constraintName });
5205
+ }
5206
+ for (const fk of model.fks.values())
5207
+ result.push({ kind: "constraint", constraintType: "foreignKey", table, name: fk.constraintName });
5208
+ if (model.pk.length > 0)
5209
+ result.push({ kind: "constraint", constraintType: "primaryKey", table, name: model.model.primaryKeyConstraintName ?? (params.dialect === "mysql" ? "PRIMARY" : `${name}_pkey`) });
5210
+ }
5211
+ for (const name of normalized.joinTables.keys()) {
5212
+ const table = { namespace: params.namespace, name };
5213
+ result.push({ kind: "table", name: table });
5214
+ for (const column of ["A", "B"])
5215
+ result.push({ kind: "column", table, column }, { kind: "constraint", constraintType: "foreignKey", table, name: `${name}_${column}_fkey` });
5216
+ for (const suffix of ["AB_unique", "B_index"])
5217
+ result.push({ kind: "index", table, name: `${name}_${suffix}` });
5218
+ }
5219
+ for (const name of normalized.views.keys())
5220
+ result.push({ kind: "view", name: { namespace: params.namespace, name } });
5221
+ return result;
5222
+ }
5223
+ function assertRetainedMigrationObjects(params) {
5224
+ const keys = new Set(params.to.map((object) => sqlObjectKey4({ identity: object.identity })));
5225
+ const removed = params.from.filter((object) => !keys.has(sqlObjectKey4({ identity: object.identity })));
5226
+ if (removed.length > 0)
5227
+ throw new VibeError19({ code: "VIBE_MIGRATION", message: "Removing migration ownership requires a reviewed hand-authored migration and new desired state", meta: { reason: "migration-object-removal-review-required", objects: removed.map((object) => object.identity) } });
5228
+ }
5229
+ function orderedMigrationObjects(params) {
5230
+ const byKey = new Map(params.objects.map((object) => [sqlObjectKey4({ identity: object.identity }), object]));
5231
+ const result = [];
5232
+ const visiting = new Set;
5233
+ const visited = new Set;
5234
+ const visit = (object) => {
5235
+ const key = sqlObjectKey4({ identity: object.identity });
5236
+ if (visited.has(key))
5237
+ return;
5238
+ if (visiting.has(key))
5239
+ throw new VibeError19({ code: "VIBE_SCHEMA_INVALID", message: "Migration object dependency cycle", meta: { id: object.id } });
5240
+ visiting.add(key);
5241
+ for (const dependency of object.dependsOn) {
5242
+ const target = byKey.get(sqlObjectKey4({ identity: dependency }));
5243
+ if (target !== undefined)
5244
+ visit(target);
5245
+ }
5246
+ visiting.delete(key);
5247
+ visited.add(key);
5248
+ result.push(object);
5249
+ };
5250
+ for (const object of params.objects)
5251
+ visit(object);
5252
+ return result;
5253
+ }
5254
+ function assertNamedRemovals(params) {
5255
+ const approved = new Set((params.approved ?? []).map((identity) => sqlObjectKey4({ identity })));
5256
+ const missing = params.removals.filter((identity) => !approved.has(sqlObjectKey4({ identity })));
5257
+ if (missing.length > 0)
5258
+ throw new VibeError19({ code: "VIBE_MIGRATION", message: "Unmanaged removals require individually named approvals", meta: { reason: "unmanaged-removal-required", objects: missing } });
5259
+ const integrity = params.removals.filter((identity) => identity.kind === "constraint" || identity.kind === "index");
5260
+ if (integrity.length > 0 && params.acceptIntegrityLoss !== true)
5261
+ throw new VibeError19({ code: "VIBE_MIGRATION", message: "Named constraint or index removals also require acceptIntegrityLoss", meta: { reason: "integrity-acknowledgement-required", objects: integrity } });
5262
+ }
5263
+ function assertRemovalAuthorization(params) {
5264
+ const declared = params.previous === undefined ? [] : schemaObjectIdentities({ schema: params.previous.schema, dialect: params.dialect, namespace: params.namespace });
5265
+ const known = new Set(declared.map((identity) => sqlObjectKey4({ identity })));
5266
+ const approved = new Set((params.removeUnmanaged ?? []).map((identity) => sqlObjectKey4({ identity })));
5267
+ const unknown = [];
5268
+ const integrity = [];
5269
+ for (const step of params.steps) {
5270
+ const identity = removalIdentity({ step, namespace: params.namespace });
5271
+ if (identity === undefined)
5272
+ continue;
5273
+ const key = sqlObjectKey4({ identity });
5274
+ if (!known.has(key) && !approved.has(key))
5275
+ unknown.push(identity);
5276
+ if (identity.kind === "constraint" || identity.kind === "index")
5277
+ integrity.push(identity);
5278
+ if (step.kind === "dropTable" || step.kind === "dropJoinTable") {
5279
+ for (const child of [...declared, ...params.removeUnmanaged ?? []]) {
5280
+ if ((child.kind === "constraint" || child.kind === "index") && child.table.namespace === params.namespace && child.table.name === step.table)
5281
+ integrity.push(child);
5282
+ }
5283
+ }
5284
+ }
5285
+ if (unknown.length > 0)
5286
+ throw new VibeError19({ code: "VIBE_MIGRATION", message: "Unknown object removals require individually named removeUnmanaged decisions", meta: { reason: "unmanaged-removal-required", objects: unknown } });
5287
+ if (integrity.length > 0 && params.acceptIntegrityLoss !== true)
5288
+ throw new VibeError19({ code: "VIBE_MIGRATION", message: "Constraint or index removal requires acceptIntegrityLoss; data-loss acknowledgement does not authorize it", meta: { reason: "integrity-acknowledgement-required", objects: integrity } });
5289
+ }
5290
+
5291
+ // packages/migrate/src/inspect-objects.ts
5292
+ function factsOf(params) {
5293
+ const facts = {};
5294
+ for (const [name, value] of Object.entries(params.row)) {
5295
+ if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))
5296
+ facts[name] = value;
5297
+ else if (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
5298
+ facts[name] = value;
5299
+ else
5300
+ return;
5301
+ }
5302
+ return facts;
5303
+ }
5304
+ async function inspectSqlObject(params) {
5305
+ return Promise.resolve().then(async () => {
5306
+ const namespace = "table" in params.identity ? params.identity.table.namespace : params.identity.name.namespace;
5307
+ if (params.dialect === "mysql")
5308
+ await assertCatalogVisibility({ executor: params.executor, dialect: params.dialect, namespace });
5309
+ const query = renderMigrationObjectQuery({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: params.identity });
5310
+ const rows = await params.executor({ text: query.text, values: [...query.values] });
5311
+ if (rows.length === 0)
5312
+ return { state: "missing" };
5313
+ if (rows.length !== 1)
5314
+ return { state: "unavailable", reason: "ambiguous-catalog-identity" };
5315
+ let facts = factsOf({ row: rows[0] });
5316
+ if (facts === undefined)
5317
+ return { state: "unavailable", reason: "unsupported-catalog-value" };
5318
+ if (params.dialect === "mysql" && (params.identity.kind === "function" || params.identity.kind === "view") && facts.definition === null)
5319
+ return { state: "unavailable", reason: "definition-not-visible" };
5320
+ if (params.dialect === "sqlite" && params.identity.kind === "constraint") {
5321
+ const name = params.identity.name;
5322
+ if (params.identity.constraintType === "check") {
5323
+ const check = parseSqliteNamedChecks({ tableSql: String(facts.definition ?? "") }).find((entry) => entry.name === name);
5324
+ if (check === undefined)
5325
+ return { state: "missing" };
5326
+ facts = { type: "check", definition: check.expression };
5327
+ } else {
5328
+ const constraintType = params.identity.constraintType;
5329
+ const constraint = sqliteTableConstraints2({ sql: String(facts.definition ?? "") }).find((entry) => entry.name === name && entry.type === constraintType);
5330
+ if (constraint === undefined)
5331
+ return { state: "unavailable", reason: "constraint-definition-inspection-unavailable" };
5332
+ facts = { type: constraint.type, definition: constraint.sql };
5333
+ }
5334
+ }
5335
+ if (params.dialect === "sqlite" && (params.identity.kind === "column" || params.identity.kind === "columnDefault" || params.identity.kind === "generatedColumn")) {
5336
+ const column = params.identity.column;
5337
+ const clause = sqliteColumnClauses({ sql: String(facts.tableDefinition ?? "") }).find((entry) => entry.name === column);
5338
+ if (clause === undefined)
5339
+ return { state: "unavailable", reason: "column-definition-inspection-unavailable" };
5340
+ const { tableDefinition: ignored, ...attributes } = facts;
5341
+ facts = { ...attributes, columnDefinition: clause.sql };
5342
+ }
5343
+ if (params.identity.kind === "columnDefault")
5344
+ facts = { definition: facts.definition ?? null };
5345
+ if (params.dialect === "postgres") {
5346
+ const query2 = renderMigrationDependencies({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: params.identity });
5347
+ const rows2 = await params.executor({ text: query2.text, values: [...query2.values] });
5348
+ if (rows2.some((row) => typeof row.address !== "string"))
5349
+ return { state: "unavailable", reason: "dependency-inspection-unavailable" };
5350
+ facts = { ...facts, dependencyAddresses: rows2.map((row) => String(row.address)).sort() };
5351
+ }
5352
+ if (params.dialect === "mysql") {
5353
+ const query2 = renderMysqlObjectMembers({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: params.identity });
5354
+ if (query2 !== undefined) {
5355
+ const rows2 = await params.executor({ text: query2.text, values: [...query2.values] });
5356
+ if (params.identity.kind === "function" && rows2.length === 0)
5357
+ return { state: "unavailable", reason: "function-signature-unavailable" };
5358
+ if (params.identity.kind === "function") {
5359
+ const signature = rows2.filter((row) => row.mode !== null).map((row) => ({ namespace: "mysql", name: String(row.type) }));
5360
+ if (canonicalMigrationJson({ value: signature }) !== canonicalMigrationJson({ value: params.identity.argumentTypes }))
5361
+ return { state: "missing" };
5362
+ }
5363
+ facts = { ...facts, members: rows2.map((row) => canonicalMigrationJson({ value: row })) };
5364
+ }
5365
+ }
5366
+ const dependencies = [];
5367
+ if (params.identity.kind === "trigger" && params.dialect === "postgres") {
5368
+ if (facts.functionArguments !== "")
5369
+ return { state: "unavailable", reason: "trigger-function-signature-unavailable" };
5370
+ dependencies.push({ kind: "function", name: { namespace: String(facts.functionNamespace), name: String(facts.functionName) }, argumentTypes: [] });
5371
+ }
5372
+ return { state: "present", definition: { identity: params.identity, facts, dependencies, format: "catalog-v1" } };
5373
+ }).catch((error) => ({ state: "unavailable", reason: error instanceof VibeError20 ? String(error.meta.feature ?? error.code) : "catalog-query-failed" }));
5374
+ }
5375
+ async function assertCatalogVisibility(params) {
5376
+ if (params.dialect !== "mysql")
5377
+ return;
5378
+ const query = renderMigrationVisibility({ dialect: getSqlDialect2({ dialect: params.dialect }), namespace: params.namespace });
5379
+ const rows = await params.executor({ text: query.text, values: [...query.values] });
5380
+ const grants = new Set(rows.map((row) => row.privilege));
5381
+ const missing = ["SELECT", "TRIGGER", "SHOW VIEW", "EXECUTE"].filter((privilege) => !grants.has(privilege));
5382
+ if (missing.length > 0)
5383
+ throw new VibeError20({ code: "VIBE_MIGRATION", message: "MySQL catalog visibility requires direct database-wide grants; role-only or partial grants cannot prove object absence", meta: { reason: "inventory-unavailable", missing } });
5384
+ }
5385
+ function compare(params) {
5386
+ const differences = [];
5387
+ for (const key of new Set([...Object.keys(params.expected.facts), ...Object.keys(params.live.facts)])) {
5388
+ const expected2 = params.expected.facts[key];
5389
+ const actual2 = params.live.facts[key];
5390
+ if (expected2 === undefined !== (actual2 === undefined) || canonicalMigrationJson({ value: expected2 }) !== canonicalMigrationJson({ value: actual2 }))
5391
+ differences.push({ attribute: key, expected: expected2, actual: actual2 });
5392
+ }
5393
+ const expected = params.expected.dependencies.map((identity) => sqlObjectKey5({ identity })).sort();
5394
+ const actual = params.live.dependencies.map((identity) => sqlObjectKey5({ identity })).sort();
5395
+ if (JSON.stringify(expected) !== JSON.stringify(actual))
5396
+ differences.push({ attribute: "dependencies", expected, actual });
5397
+ return differences;
5398
+ }
5399
+ function assertProbeMode(params) {
5400
+ if (params.probe !== undefined && params.probe !== "none" && DIALECT_CAPABILITIES2[params.dialect].checkNormalization !== params.probe)
5401
+ throw new VibeError20({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `${params.probe} CHECK probes are unavailable on ${params.dialect}`, meta: { feature: "checkProbe", dialect: params.dialect } });
5402
+ }
5403
+ async function verifyMigrationState(params) {
5404
+ assertProbeMode({ dialect: params.dialect, probe: params.probe });
5405
+ const declarations = params.expected.schema.migrationObjects ?? [];
5406
+ const owned = new Set(declarations.map((object) => sqlObjectKey5({ identity: object.identity })));
5407
+ const definitions = new Map(params.expected.definitions.map((definition) => [sqlObjectKey5({ identity: definition.identity }), definition]));
5408
+ const identities = new Map(params.expected.definitions.map((definition) => [sqlObjectKey5({ identity: definition.identity }), definition.identity]));
5409
+ for (const object of declarations)
5410
+ identities.set(sqlObjectKey5({ identity: object.identity }), object.identity);
5411
+ const objects = [];
5412
+ for (const [key, identity] of identities) {
5413
+ const ownership = owned.has(key) ? "migration" : "schema";
5414
+ const expected = definitions.get(key);
5415
+ const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
5416
+ if (live.state !== "present") {
5417
+ objects.push({ identity, ownership, state: live.state, differences: [], ...live.state === "unavailable" ? { reason: live.reason } : {} });
5418
+ } else if (expected === undefined) {
5419
+ objects.push({ identity, ownership, state: "unavailable", reason: "independent-definition-required", differences: [] });
5420
+ } else {
5421
+ const differences = compare({ expected, live: live.definition });
5422
+ objects.push({ identity, ownership, state: differences.length === 0 ? "matching" : "changed", differences });
5423
+ }
5424
+ }
5425
+ if (params.expected.schema.models.length > 0 || params.expected.schema.enums.length > 0)
5426
+ objects.push(...await verifySchemaObjects(params));
5427
+ if (params.dialect === "postgres")
5428
+ objects.push(...await verifyExclusions(params));
5429
+ return { inSync: objects.every((object) => object.state === "matching"), objects };
5430
+ }
5431
+ async function verifySchemaObjects(params) {
5432
+ const physical = params.dialect === "postgres" ? await introspectPhysicalPostgres({ executor: params.executor }) : params.dialect === "mysql" ? await introspectMysql({ executor: params.executor }) : await introspectSqlite({ executor: params.executor });
5433
+ const expected = normalizeSchema({ schema: params.expected.schema, dialect: params.dialect });
5434
+ const actual = normalizeSchema({ schema: physical, dialect: params.dialect });
5435
+ const objects = [];
5436
+ const namespace = await migrationNamespace({ executor: params.executor, dialect: params.dialect });
5437
+ for (const [name, desired] of expected.tables) {
5438
+ const table = { namespace, name };
5439
+ const live = actual.tables.get(name);
5440
+ if (live === undefined) {
5441
+ const presence = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity: { kind: "table", name: table } });
5442
+ objects.push({ identity: { kind: "table", name: table }, ownership: "schema", state: presence.state === "missing" ? "missing" : "unavailable", ...presence.state === "missing" ? {} : { reason: "schema-definition-not-visible" }, differences: [] });
5443
+ continue;
5444
+ }
5445
+ const differences = [];
5446
+ const pin = (attribute, expected2, actual2) => {
5447
+ if (canonicalMigrationJson({ value: expected2 }) !== canonicalMigrationJson({ value: actual2 }))
5448
+ differences.push({ attribute, expected: expected2, actual: actual2 });
5449
+ };
5450
+ pin("primaryKey", desired.pk, live.pk);
5451
+ for (const [column, field] of desired.columns) {
5452
+ const stored = live.columns.get(column);
5453
+ pin(`columns.${column}.type`, field.typeKey, stored?.typeKey);
5454
+ pin(`columns.${column}.nullable`, field.nullable, stored?.nullable);
5455
+ if (params.dialect === "mysql" && field.field.collation !== undefined) {
5456
+ pin(`columns.${column}.collation`, field.field.collation.toLowerCase(), stored?.field.collation?.toLowerCase());
5457
+ }
5458
+ const ownedDefault = params.expected.schema.migrationObjects?.some((object) => (object.identity.kind === "columnDefault" || object.identity.kind === "generatedColumn") && object.identity.table.namespace === namespace && object.identity.table.name === name && object.identity.column === column);
5459
+ if (!ownedDefault)
5460
+ pin(`columns.${column}.default`, field.defaultKey, stored?.defaultKey);
5461
+ }
5462
+ for (const [key, fk] of desired.fks) {
5463
+ const stored = live.fks.get(key);
5464
+ pin(`foreignKeys.${fk.constraintName}`, { columns: fk.columns, refTable: fk.refTable, refColumns: fk.refColumns, onDelete: fk.onDelete, onUpdate: fk.onUpdate }, stored === undefined ? undefined : { columns: stored.columns, refTable: stored.refTable, refColumns: stored.refColumns, onDelete: stored.onDelete, onUpdate: stored.onUpdate });
5465
+ if (params.dialect === "postgres" && stored !== undefined) {
5466
+ const identity = { kind: "constraint", constraintType: "foreignKey", table, name: stored.constraintName };
5467
+ if (params.expected.definitions.some((definition) => sqlObjectKey5({ identity: definition.identity }) === sqlObjectKey5({ identity })))
5468
+ continue;
5469
+ const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
5470
+ if (inspection.state !== "present") {
5471
+ objects.push({ identity, ownership: "schema", state: inspection.state, differences: [], ...inspection.state === "unavailable" ? { reason: inspection.reason } : {} });
5472
+ } else {
5473
+ const differences2 = ["validated", "triggersEnabled"].flatMap((attribute) => inspection.definition.facts[attribute] === true ? [] : [{ attribute, expected: true, actual: inspection.definition.facts[attribute] }]);
5474
+ objects.push({ identity, ownership: "schema", state: differences2.length === 0 ? "matching" : "changed", differences: differences2 });
5475
+ }
5476
+ }
5477
+ }
5478
+ for (const [key, index] of desired.indexes) {
5479
+ pin(`indexes.${index.name}`, key, live.indexes.get(key)?.key);
5480
+ if (params.dialect !== "postgres" || !live.indexes.has(key))
5481
+ continue;
5482
+ const identity = { kind: "index", table, name: live.indexes.get(key).name };
5483
+ if (params.expected.definitions.some((definition) => sqlObjectKey5({ identity: definition.identity }) === sqlObjectKey5({ identity })))
5484
+ continue;
5485
+ const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
5486
+ if (inspection.state !== "present") {
5487
+ objects.push({ identity, ownership: "schema", state: inspection.state, differences: [], ...inspection.state === "unavailable" ? { reason: inspection.reason } : {} });
5488
+ } else {
5489
+ const differences2 = ["valid", "ready"].flatMap((attribute) => inspection.definition.facts[attribute] === true ? [] : [{ attribute, expected: true, actual: inspection.definition.facts[attribute] }]);
5490
+ objects.push({ identity, ownership: "schema", state: differences2.length === 0 ? "matching" : "changed", differences: differences2 });
5491
+ }
5492
+ }
5493
+ objects.push({ identity: { kind: "table", name: table }, ownership: "schema", state: differences.length === 0 ? "matching" : "changed", differences });
5494
+ for (const [check, expression] of desired.checks) {
5495
+ const identity = { kind: "constraint", constraintType: "check", table, name: check };
5496
+ if (params.expected.definitions.some((definition) => sqlObjectKey5({ identity: definition.identity }) === sqlObjectKey5({ identity })))
5497
+ continue;
5498
+ const stored = live.checks.get(check);
5499
+ if (stored === undefined) {
5500
+ objects.push({ identity, ownership: "schema", state: "missing", differences: [] });
5501
+ continue;
5502
+ }
5503
+ if (params.dialect !== "sqlite") {
5504
+ const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
5505
+ if (inspection.state !== "present") {
5506
+ objects.push({ identity, ownership: "schema", state: inspection.state, differences: [] });
5507
+ continue;
5508
+ }
5509
+ const disabled = params.dialect === "postgres" ? inspection.definition.facts.validated !== true : inspection.definition.facts.enforced !== "YES";
5510
+ if (disabled) {
5511
+ objects.push({ identity, ownership: "schema", state: "changed", differences: [{ attribute: params.dialect === "postgres" ? "validated" : "enforced", expected: params.dialect === "postgres" ? true : "YES", actual: params.dialect === "postgres" ? inspection.definition.facts.validated : inspection.definition.facts.enforced }] });
5512
+ continue;
5513
+ }
5514
+ }
5515
+ if (stored === expression) {
5516
+ objects.push({ identity, ownership: "schema", state: "matching", differences: [] });
5517
+ continue;
5518
+ }
5519
+ if (params.dialect === "postgres" && params.probe === "rollback") {
5520
+ const normalized = await deparsePostgresCheck({ executor: params.executor, table, expression, probe: "rollback" });
5521
+ const read = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
5522
+ const actual2 = read.state === "present" ? read.definition.facts.definition : undefined;
5523
+ objects.push({ identity, ownership: "schema", state: normalized === actual2 ? "matching" : "changed", differences: normalized === actual2 ? [] : [{ attribute: "definition", expected: normalized, actual: actual2 }] });
5524
+ } else if (params.dialect === "mysql" && params.probe === "temporary") {
5525
+ const normalized = await deparseMysqlCheck({ executor: params.executor, table, expression, probe: "temporary" });
5526
+ const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
5527
+ const actual2 = inspection.state === "present" ? inspection.definition.facts.definition : undefined;
5528
+ const match = normalized === actual2;
5529
+ objects.push({ identity, ownership: "schema", state: inspection.state === "unavailable" ? "unavailable" : match ? "matching" : "changed", differences: match ? [] : [{ attribute: "definition", expected: normalized, actual: actual2 }] });
5530
+ } else
5531
+ objects.push({ identity, ownership: "schema", state: "unavailable", reason: "explicit-replacement-required", differences: [{ attribute: "definition", expected: expression, actual: stored }] });
5532
+ }
5533
+ }
5534
+ for (const [name, join] of expected.joinTables) {
5535
+ const stored = actual.joinTables.get(name);
5536
+ const shape = (value) => value === undefined ? undefined : { a: { table: value.a.table, column: value.a.column }, b: { table: value.b.table, column: value.b.column } };
5537
+ const matching = canonicalMigrationJson({ value: shape(join) }) === canonicalMigrationJson({ value: shape(stored) });
5538
+ objects.push({ identity: { kind: "table", name: { namespace, name } }, ownership: "schema", state: stored === undefined ? "missing" : matching ? "matching" : "changed", differences: matching ? [] : [{ attribute: "joinTable", expected: shape(join), actual: shape(stored) }] });
5539
+ }
5540
+ for (const [name] of expected.views) {
5541
+ if (!params.expected.definitions.some((definition) => definition.identity.kind === "view" && definition.identity.name.namespace === namespace && definition.identity.name.name === name))
5542
+ objects.push({ identity: { kind: "view", name: { namespace, name } }, ownership: "schema", state: actual.views.has(name) ? "unavailable" : "missing", reason: "independent-definition-required", differences: [] });
5543
+ }
5544
+ return objects;
5545
+ }
5546
+ async function verifyMigrationTransition(params) {
5547
+ const report = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: params.to, ...params.probe === undefined ? {} : { probe: params.probe } });
5548
+ const namespace = await migrationNamespace(params);
5549
+ const identities = (state2) => [...schemaObjectIdentities({ schema: state2.schema, dialect: params.dialect, namespace }), ...(state2.schema.migrationObjects ?? []).map((object) => object.identity), ...state2.definitions.map((definition) => definition.identity)];
5550
+ const retained = new Set(identities(params.to).map((identity) => sqlObjectKey5({ identity })));
5551
+ const removed = new Map([...identities(params.from).filter((identity) => !retained.has(sqlObjectKey5({ identity }))), ...params.removals ?? []].map((identity) => [sqlObjectKey5({ identity }), identity]));
5552
+ const objects = [...report.objects];
5553
+ for (const identity of removed.values()) {
5554
+ const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
5555
+ const absent = live.state === "missing" || identity.kind === "columnDefault" && live.state === "present" && live.definition.facts.definition === null;
5556
+ if (!absent)
5557
+ objects.push({ identity, ownership: "schema", state: live.state === "unavailable" ? "unavailable" : "changed", reason: live.state === "unavailable" ? live.reason : "expected-absent", differences: [{ attribute: "presence", expected: false, actual: live.state === "present" ? true : undefined }] });
5558
+ }
5559
+ return { inSync: report.inSync && objects.every((object) => object.state === "matching"), objects };
5560
+ }
5561
+ async function migrationNamespace(params) {
5562
+ if (params.dialect !== "mysql")
5563
+ return params.dialect === "sqlite" ? "main" : "public";
5564
+ const query = renderMigrationNamespace({ dialect: getSqlDialect2({ dialect: params.dialect }) });
5565
+ const rows = await params.executor({ text: query.text, values: [] });
5566
+ const namespace = rows[0]?.namespace;
5567
+ if (typeof namespace !== "string" || namespace.length === 0)
5568
+ throw new VibeError20({ code: "VIBE_MIGRATION", message: "Cannot determine the migration namespace", meta: { reason: "inspection-unavailable" } });
5569
+ return namespace;
5570
+ }
5571
+ function assertMigrationState(params) {
5572
+ if (!params.report.inSync)
5573
+ throw new VibeError20({ code: "VIBE_MIGRATION", message: "Migration state could not be verified", meta: { reason: "migration-state-unverified", objects: params.report.objects.filter((object) => object.state !== "matching") } });
5574
+ }
5575
+ async function deparsePostgresCheck(params) {
5576
+ const definition = await deparsePostgresCheckDefinition({ ...params, constraint: "vibeorm_check_probe" });
5577
+ return String(definition.facts.definition);
5578
+ }
5579
+ async function deparsePostgresCheckDefinition(params) {
5580
+ const name = "vibeorm_check_probe";
5581
+ await params.executor({ text: "SAVEPOINT vibeorm_check_probe" });
5582
+ try {
5583
+ const query = renderCheckProbe({ dialect: getSqlDialect2({ dialect: "postgres" }), table: params.table, expression: params.expression, name });
5584
+ await params.executor({ text: query.text });
5585
+ const result = await inspectSqlObject({ executor: params.executor, dialect: "postgres", identity: { kind: "constraint", constraintType: "check", table: params.table, name } });
5586
+ if (result.state !== "present" || typeof result.definition.facts.definition !== "string")
5587
+ throw new VibeError20({ code: "VIBE_MIGRATION", message: "CHECK normalization probe unavailable", meta: { reason: "check-inspection-unavailable" } });
5588
+ return { ...result.definition, identity: { kind: "constraint", constraintType: "check", table: params.table, name: params.constraint }, facts: { ...result.definition.facts, definition: result.definition.facts.definition.replace(/ NOT VALID$/, ""), validated: true } };
5589
+ } finally {
5590
+ await params.executor({ text: "ROLLBACK TO SAVEPOINT vibeorm_check_probe" });
5591
+ await params.executor({ text: "RELEASE SAVEPOINT vibeorm_check_probe" });
5592
+ }
5593
+ }
5594
+ async function deparseMysqlCheck(params) {
5595
+ const probe = `vibe_check_${randomUUID2().replaceAll("-", "")}`;
5596
+ const dialect = getSqlDialect2({ dialect: "mysql" });
5597
+ const clone = renderMysqlCheckProbe({ dialect, probe, operation: "clone", table: params.table });
5598
+ await params.executor({ text: clone.text });
5599
+ let created = true;
5600
+ try {
5601
+ const read = renderMysqlCheckProbe({ dialect, probe, operation: "read" });
5602
+ const source = await params.executor({ text: read.text, values: [] });
5603
+ const definition = source[0]?.["Create Table"];
5604
+ if (typeof definition !== "string")
5605
+ throw new VibeError20({ code: "VIBE_MIGRATION", message: "Cannot read temporary clone definition" });
5606
+ const drop = renderMysqlCheckProbe({ dialect, probe, operation: "drop" });
5607
+ await params.executor({ text: drop.text });
5608
+ created = false;
5609
+ const add = renderMysqlCheckProbe({ dialect, probe, operation: "add", expression: params.expression, definition });
5610
+ await params.executor({ text: add.text });
5611
+ created = true;
5612
+ const rows = await params.executor({ text: read.text, values: [] });
5613
+ const normalizedDefinition = rows[0]?.["Create Table"];
5614
+ const check = typeof normalizedDefinition === "string" ? parseSqliteNamedChecks({ tableSql: normalizedDefinition }).find((entry) => entry.name === probe) : undefined;
5615
+ if (check === undefined)
5616
+ throw new VibeError20({ code: "VIBE_MIGRATION", message: "MySQL temporary CHECK deparse unavailable", meta: { reason: "check-inspection-unavailable" } });
5617
+ return check.expression;
5618
+ } finally {
5619
+ if (created) {
5620
+ const drop = renderMysqlCheckProbe({ dialect, probe, operation: "drop" });
5621
+ await params.executor({ text: drop.text });
5622
+ }
5623
+ }
5624
+ }
5625
+ async function prepareCheckDiff(params) {
5626
+ assertProbeMode({ dialect: params.dialect, probe: params.probe });
5627
+ const models = [];
5628
+ for (const model of params.from.models) {
5629
+ const table = model.dbName ?? model.name;
5630
+ const target = params.to.models.find((entry) => (entry.dbName ?? entry.name) === table);
5631
+ const checks = [];
5632
+ for (const check of model.checks ?? []) {
5633
+ const next = target?.checks?.find((entry) => entry.name === check.name);
5634
+ if (next === undefined || next.expression === check.expression) {
5635
+ checks.push({ name: check.name, expression: check.expression });
5636
+ continue;
5637
+ }
5638
+ const identity = { kind: "constraint", constraintType: "check", table: { namespace: await migrationNamespace({ executor: params.executor, dialect: params.dialect }), name: table }, name: check.name };
5639
+ if (params.dialect === "postgres" && params.probe === "rollback") {
5640
+ const desired = await deparsePostgresCheck({ executor: params.executor, table: identity.table, expression: next.expression, probe: "rollback" });
5641
+ const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
5642
+ if (live.state !== "present")
5643
+ throw new VibeError20({ code: "VIBE_MIGRATION", message: "Live CHECK inspection unavailable", meta: { reason: "check-inspection-unavailable", identity } });
5644
+ if (live.definition.facts.definition === desired) {
5645
+ checks.push(next);
5646
+ continue;
5647
+ }
5648
+ checks.push({ name: check.name, expression: check.expression });
5649
+ continue;
5650
+ }
5651
+ if (params.dialect === "mysql" && params.probe === "temporary") {
5652
+ const desired = await deparseMysqlCheck({ executor: params.executor, table: identity.table, expression: next.expression, probe: "temporary" });
5653
+ const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
5654
+ if (live.state !== "present")
5655
+ throw new VibeError20({ code: "VIBE_MIGRATION", message: "Live CHECK inspection unavailable", meta: { reason: "check-inspection-unavailable", identity } });
5656
+ checks.push(desired === live.definition.facts.definition ? next : { name: check.name, expression: check.expression });
5657
+ continue;
5658
+ }
5659
+ const previous = params.previous?.schema.models.find((entry) => (entry.dbName ?? entry.name) === table)?.checks?.find((entry) => entry.name === check.name);
5660
+ if (previous !== undefined) {
5661
+ checks.push(previous.expression === next.expression ? next : { name: check.name, expression: check.expression });
5662
+ continue;
5663
+ }
5664
+ if (params.replaceChecks?.some((entry) => sqlObjectKey5({ identity: entry }) === sqlObjectKey5({ identity }))) {
5665
+ checks.push({ name: check.name, expression: check.expression });
5666
+ continue;
5667
+ }
5668
+ throw new VibeError20({ code: "VIBE_MIGRATION", message: `Cannot establish CHECK equivalence for ${table}.${check.name}; request a rollback probe or an explicit replacement`, meta: { reason: "explicit-replacement-required", identity } });
5669
+ }
5670
+ models.push({ ...model, checks });
5671
+ }
5672
+ return { ...params.from, models };
5673
+ }
4752
5674
 
4753
5675
  // packages/migrate/src/runner.ts
4754
5676
  function migrationRlsSecurityChanges(params) {
@@ -4832,11 +5754,11 @@ function assertNativeMigrationMetadata(params) {
4832
5754
  const native = params.migration.rls !== undefined || params.sql.some((sql2) => containsNativeRlsSql({ sql: sql2, dialect: params.dialect }));
4833
5755
  if (!native)
4834
5756
  return;
4835
- if (!DIALECT_CAPABILITIES2[params.dialect].nativeRowLevelSecurity) {
4836
- throw new VibeError15({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Native RLS migrations are unsupported on ${params.dialect}`, meta: { feature: "nativeRowLevelSecurity", dialect: params.dialect } });
5757
+ if (!DIALECT_CAPABILITIES3[params.dialect].nativeRowLevelSecurity) {
5758
+ throw new VibeError21({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Native RLS migrations are unsupported on ${params.dialect}`, meta: { feature: "nativeRowLevelSecurity", dialect: params.dialect } });
4837
5759
  }
4838
5760
  if (params.migration.rls === undefined) {
4839
- throw new VibeError15({ code: "VIBE_MIGRATION", message: "Native RLS migration files require trusted rls.from/rls.to schema metadata. SQL alone cannot prove policy ownership or drift; regenerate with preserved security metadata.", meta: { reason: "rls-migration-metadata-required", migration: params.migration.name } });
5761
+ throw new VibeError21({ code: "VIBE_MIGRATION", message: "Native RLS migration files require trusted rls.from/rls.to schema metadata. SQL alone cannot prove policy ownership or drift; regenerate with preserved security metadata.", meta: { reason: "rls-migration-metadata-required", migration: params.migration.name } });
4840
5762
  }
4841
5763
  }
4842
5764
  var PG_ADVISORY_LOCK_KEY = 1447645253;
@@ -4876,11 +5798,27 @@ async function ensureMigrationsTable(params) {
4876
5798
  function migrationChecksum(params) {
4877
5799
  const suffix = executionChecksumSuffix({ ...params.execution === undefined ? {} : { execution: params.execution } });
4878
5800
  return createHash("sha256").update(params.sql.join(`
4879
- `) + suffix).digest("hex");
5801
+ `) + suffix + (params.desiredStatePending === true ? `
5802
+ -- vibeorm:desired-state unfinalized-v1` : "") + desiredStateChecksumSuffix({ ...params.desiredState === undefined ? {} : { desiredState: params.desiredState } }) + (params.desiredState === undefined ? "" : `
5803
+ -- vibeorm:down ${JSON.stringify(params.downSql ?? null)}${executionChecksumSuffix({ ...params.downExecution === undefined ? {} : { execution: params.downExecution } })}`)).digest("hex");
4880
5804
  }
4881
5805
  function checksumOf(params) {
4882
5806
  const { migration } = params;
4883
- return migrationChecksum({ sql: migration.sql, ...migration.execution === undefined ? {} : { execution: migration.execution } });
5807
+ return migrationChecksum(migration);
5808
+ }
5809
+ async function assertForwardStart(params) {
5810
+ const state2 = params.migration.desiredState;
5811
+ if (state2 === undefined)
5812
+ return;
5813
+ const report = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state2.from, ...params.probe === undefined ? {} : { probe: params.probe } });
5814
+ if (report.inSync)
5815
+ return;
5816
+ if (state2.down !== undefined) {
5817
+ const restored = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state2.down, ...params.probe === undefined ? {} : { probe: params.probe } });
5818
+ if (restored.inSync)
5819
+ return;
5820
+ }
5821
+ assertMigrationState({ report });
4884
5822
  }
4885
5823
  function generateMigration(params) {
4886
5824
  const { from, to, name } = params;
@@ -4891,9 +5829,10 @@ function generateMigration(params) {
4891
5829
  extensions: params.extensions ?? []
4892
5830
  });
4893
5831
  const allArtifacts = [...artifacts.setup, ...artifacts.objects];
4894
- const strippedFrom = stripExtensionArtifacts({ schema: from, artifacts: allArtifacts });
5832
+ assertRetainedMigrationObjects({ from: from.migrationObjects ?? [], to: to.migrationObjects ?? [] });
5833
+ const strippedFrom = preserveMigrationObjects({ from: stripExtensionArtifacts({ schema: from, artifacts: allArtifacts }), to, objects: [...from.migrationObjects ?? [], ...to.migrationObjects ?? []] });
4895
5834
  const steps = diffSchemas({ from: strippedFrom, to, dialect, ...params.renames === undefined ? {} : { renames: params.renames } });
4896
- const coreSql = renderSteps({ steps, dialect, from: strippedFrom, to });
5835
+ const coreSql = renderSteps({ steps, dialect, from: dialect === "sqlite" ? from : strippedFrom, to: dialect === "sqlite" ? sqlitePreservedSchema({ from, to, objects: [...from.migrationObjects ?? [], ...to.migrationObjects ?? []] }) : to });
4897
5836
  const missing = planMissingArtifacts({ introspected: from, artifacts: allArtifacts });
4898
5837
  const sql2 = [
4899
5838
  ...artifactCreateSql({
@@ -4902,6 +5841,16 @@ function generateMigration(params) {
4902
5841
  ...coreSql,
4903
5842
  ...artifactCreateSql({
4904
5843
  artifacts: missing.filter((artifact) => artifact.owns.kind !== "postgresExtension")
5844
+ }),
5845
+ ...orderedMigrationObjects({ objects: to.migrationObjects ?? [] }).flatMap((object) => {
5846
+ if (object.dialect !== dialect)
5847
+ throw new VibeError21({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Migration object ${object.id} requires ${object.dialect}` });
5848
+ const previous = (params.desiredState?.from.schema.migrationObjects ?? from.migrationObjects)?.find((entry) => entry.id === object.id);
5849
+ if (previous === undefined)
5850
+ return [...object.create];
5851
+ if (JSON.stringify(previous) !== JSON.stringify(object))
5852
+ throw new VibeError21({ code: "VIBE_MIGRATION", message: `Migration-owned object ${object.id} changed; supply a reviewed forward SQL migration`, meta: { reason: "migration-object-replacement-required", id: object.id } });
5853
+ return [];
4905
5854
  })
4906
5855
  ];
4907
5856
  return {
@@ -4910,13 +5859,21 @@ function generateMigration(params) {
4910
5859
  destructive: steps.some((step) => step.destructive),
4911
5860
  ...securityChangesOf({ steps }).length === 0 ? {} : { securityChanges: securityChangesOf({ steps }) },
4912
5861
  ...from.rls === undefined && to.rls === undefined ? {} : { rls: { from, to } },
4913
- ...params.execution === undefined ? {} : { execution: params.execution }
5862
+ ...params.execution === undefined ? {} : { execution: params.execution },
5863
+ desiredState: params.desiredState ?? { version: 1, dialect, from: createDesiredState({ schema: from, dialect }), to: createDesiredState({ schema: to, dialect }) }
4914
5864
  };
4915
5865
  }
4916
5866
  async function applyMigrations(params) {
4917
5867
  const { executor, migrations } = params;
4918
5868
  const dialect = params.dialect ?? "postgres";
4919
5869
  for (const migration of migrations) {
5870
+ if (migration.desiredStatePending === true)
5871
+ throw new VibeError21({ code: "VIBE_MIGRATION", message: `Migration ${migration.name} has unfinalized desired state; finalize its reviewed before/after declarations before applying`, meta: { reason: "desired-state-unfinalized", migration: migration.name } });
5872
+ if (migration.desiredState !== undefined) {
5873
+ assertDesiredState({ value: migration.desiredState, migration: migration.name });
5874
+ if (migration.desiredState.dialect !== dialect)
5875
+ throw new VibeError21({ code: "VIBE_MIGRATION", message: "Migration desired-state dialect mismatch", meta: { reason: "desired-state-dialect-mismatch", migration: migration.name } });
5876
+ }
4920
5877
  assertNativeMigrationMetadata({ migration, sql: migration.sql, dialect });
4921
5878
  assertExecutable({ migration, execution: migration.execution, sql: migration.sql, dialect });
4922
5879
  }
@@ -4930,7 +5887,7 @@ async function applyMigrations(params) {
4930
5887
  const stored = recorded.get(migration.name);
4931
5888
  if (stored !== undefined) {
4932
5889
  if (stored.statementIndex < 0) {
4933
- throw new VibeError15({
5890
+ throw new VibeError21({
4934
5891
  code: "VIBE_MIGRATION",
4935
5892
  message: `migration "${migration.name}" is mid-rollback (${-stored.statementIndex - 1} down statement(s) done) \u2014 finish it with \`migrate down\` or repair with resolveMigration before applying`,
4936
5893
  meta: { migration: migration.name, statementIndex: stored.statementIndex }
@@ -4942,6 +5899,10 @@ async function applyMigrations(params) {
4942
5899
  continue;
4943
5900
  }
4944
5901
  assertResumable({ migration, stored });
5902
+ assertNamedRemovals({ removals: migration.desiredState?.unmanagedRemovals ?? [], approved: params.removeUnmanaged, acceptIntegrityLoss: params.acceptIntegrityLoss });
5903
+ if (migration.desiredState !== undefined)
5904
+ assertRemovalAuthorization({ steps: diffSchemas({ from: migration.desiredState.from.schema, to: migration.desiredState.to.schema, dialect }), previous: migration.desiredState.from, dialect, namespace: dialect === "sqlite" ? "main" : "public", ...params.acceptIntegrityLoss === undefined ? {} : { acceptIntegrityLoss: params.acceptIntegrityLoss } });
5905
+ assertSecurityAcknowledgement({ securityChanges: [...migration.securityChanges ?? [], ...migrationRlsSecurityChanges({ migration, down: false })], ...params.acceptSecurityChanges === undefined ? {} : { acceptSecurityChanges: params.acceptSecurityChanges } });
4945
5906
  await withSessionLimits({
4946
5907
  executor,
4947
5908
  dialect,
@@ -4951,6 +5912,7 @@ async function applyMigrations(params) {
4951
5912
  dialect,
4952
5913
  migration,
4953
5914
  start: stored.statementIndex,
5915
+ ...params.probe === undefined ? {} : { probe: params.probe },
4954
5916
  insertRecord: false,
4955
5917
  ...stored.error === undefined ? {} : { storedError: stored.error }
4956
5918
  })
@@ -4958,19 +5920,35 @@ async function applyMigrations(params) {
4958
5920
  applied.push(migration.name);
4959
5921
  continue;
4960
5922
  }
5923
+ assertNamedRemovals({ removals: migration.desiredState?.unmanagedRemovals ?? [], approved: params.removeUnmanaged, acceptIntegrityLoss: params.acceptIntegrityLoss });
5924
+ if (migration.desiredState !== undefined) {
5925
+ const state2 = migration.desiredState;
5926
+ assertRemovalAuthorization({ steps: diffSchemas({ from: state2.from.schema, to: state2.to.schema, dialect }), previous: state2.from, dialect, namespace: dialect === "sqlite" ? "main" : "public", ...params.acceptIntegrityLoss === undefined ? {} : { acceptIntegrityLoss: params.acceptIntegrityLoss } });
5927
+ }
4961
5928
  assertSecurityAcknowledgement({ securityChanges: [...migration.securityChanges ?? [], ...migrationRlsSecurityChanges({ migration, down: false })], ...params.acceptSecurityChanges === undefined ? {} : { acceptSecurityChanges: params.acceptSecurityChanges } });
4962
5929
  if (usesStatementBookkeeping({ dialect, sql: migration.sql, ...migration.execution === undefined ? {} : { execution: migration.execution } })) {
4963
5930
  await withSessionLimits({
4964
5931
  executor,
4965
5932
  dialect,
4966
5933
  ...migration.execution === undefined ? {} : { execution: migration.execution },
4967
- run: () => runNonTransactional({ executor, dialect, migration, start: 0, insertRecord: true })
5934
+ run: () => runNonTransactional({ executor, dialect, migration, start: 0, insertRecord: true, ...params.probe === undefined ? {} : { probe: params.probe } })
4968
5935
  });
4969
5936
  } else {
4970
- await runTransactional({ executor, dialect, migration });
5937
+ await runTransactional({ executor, dialect, migration, ...params.probe === undefined ? {} : { probe: params.probe } });
4971
5938
  }
4972
5939
  applied.push(migration.name);
4973
5940
  }
5941
+ const final = migrations.at(-1);
5942
+ if (final?.desiredState !== undefined) {
5943
+ if (params.probe === "rollback")
5944
+ await executor({ text: "BEGIN" });
5945
+ try {
5946
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: final.desiredState.from, to: final.desiredState.to, removals: final.desiredState.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5947
+ } finally {
5948
+ if (params.probe === "rollback")
5949
+ await executor({ text: "ROLLBACK" });
5950
+ }
5951
+ }
4974
5952
  return { applied, skipped };
4975
5953
  } finally {
4976
5954
  await releaseLock({ executor, dialect });
@@ -5050,14 +6028,22 @@ async function resolveMigration(params) {
5050
6028
  async function rollbackMigration(params) {
5051
6029
  const { executor, migration } = params;
5052
6030
  const dialect = params.dialect ?? "postgres";
6031
+ if (migration.desiredStatePending === true)
6032
+ throw new VibeError21({ code: "VIBE_MIGRATION", message: "Unfinalized desired state cannot be rolled back", meta: { reason: "desired-state-unfinalized", migration: migration.name } });
5053
6033
  const downSql = migration.downSql;
5054
6034
  if (downSql === undefined) {
5055
- throw new VibeError15({
6035
+ throw new VibeError21({
5056
6036
  code: "VIBE_MIGRATION",
5057
6037
  message: `migration "${migration.name}" has no down.sql \u2014 it predates down-migration support; roll it back manually, then repair with resolveMigration (as: "rolled-back")`,
5058
6038
  meta: { migration: migration.name }
5059
6039
  });
5060
6040
  }
6041
+ if (migration.desiredState !== undefined) {
6042
+ assertDesiredState({ value: migration.desiredState, migration: migration.name });
6043
+ if (migration.desiredState.dialect !== dialect)
6044
+ throw new VibeError21({ code: "VIBE_MIGRATION", message: "Migration desired-state dialect mismatch", meta: { reason: "desired-state-dialect-mismatch" } });
6045
+ assertRemovalAuthorization({ steps: diffSchemas({ from: migration.desiredState.to.schema, to: (migration.desiredState.down ?? migration.desiredState.from).schema, dialect }), previous: migration.desiredState.to, dialect, namespace: dialect === "sqlite" ? "main" : "public", ...params.acceptIntegrityLoss === undefined ? {} : { acceptIntegrityLoss: params.acceptIntegrityLoss } });
6046
+ }
5061
6047
  assertNativeMigrationMetadata({ migration, sql: downSql, dialect });
5062
6048
  assertExecutable({ migration, execution: migration.downExecution, sql: downSql, dialect });
5063
6049
  assertSecurityAcknowledgement({ securityChanges: [...migration.downSecurityChanges ?? [], ...migrationRlsSecurityChanges({ migration, down: true })], ...params.acceptSecurityChanges === undefined ? {} : { acceptSecurityChanges: params.acceptSecurityChanges } });
@@ -5067,7 +6053,7 @@ async function rollbackMigration(params) {
5067
6053
  const recorded = await recordedRows({ executor, dialect });
5068
6054
  const stored = recorded.get(migration.name);
5069
6055
  if (stored === undefined) {
5070
- throw new VibeError15({
6056
+ throw new VibeError21({
5071
6057
  code: "VIBE_MIGRATION",
5072
6058
  message: `migration "${migration.name}" is not recorded as applied \u2014 nothing to roll back`,
5073
6059
  meta: { migration: migration.name }
@@ -5078,7 +6064,7 @@ async function rollbackMigration(params) {
5078
6064
  if (!resumingDown) {
5079
6065
  const partialUp = !isCompleteRecord({ dialect, migration, stored });
5080
6066
  if (partialUp) {
5081
- throw new VibeError15({
6067
+ throw new VibeError21({
5082
6068
  code: "VIBE_MIGRATION",
5083
6069
  message: `migration "${migration.name}" is only partially applied (${stored.statementIndex} of ${migration.sql.length} statement(s)) \u2014 its down migration mirrors the FULL up; re-run applyMigrations to finish it (it resumes), or repair with resolveMigration`,
5084
6070
  meta: { migration: migration.name, statementIndex: stored.statementIndex }
@@ -5097,11 +6083,12 @@ async function rollbackMigration(params) {
5097
6083
  migration,
5098
6084
  downSql,
5099
6085
  start,
6086
+ ...params.probe === undefined ? {} : { probe: params.probe },
5100
6087
  ...stored.error === undefined ? {} : { storedError: stored.error }
5101
6088
  })
5102
6089
  });
5103
6090
  } else {
5104
- await runTransactionalDown({ executor, dialect, migration, downSql });
6091
+ await runTransactionalDown({ executor, dialect, migration, downSql, ...params.probe === undefined ? {} : { probe: params.probe } });
5105
6092
  }
5106
6093
  } finally {
5107
6094
  await releaseLock({ executor, dialect });
@@ -5114,11 +6101,15 @@ async function runTransactionalDown(params) {
5114
6101
  if (wrap)
5115
6102
  await executor({ text: "BEGIN" });
5116
6103
  try {
6104
+ if (migration.desiredState !== undefined)
6105
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.desiredState.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5117
6106
  if (migration.rls !== undefined)
5118
6107
  await introspectPostgres({ executor, rlsSchema: migration.rls.to, inTransaction: true, requireInstalledRls: true });
5119
6108
  for (const statement of downSql) {
5120
6109
  await executeStatement({ executor, statement, dialect });
5121
6110
  }
6111
+ if (migration.desiredState !== undefined)
6112
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.to, to: migration.desiredState.down ?? migration.desiredState.from, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5122
6113
  if (migration.rls !== undefined)
5123
6114
  await introspectPostgres({ executor, rlsSchema: migration.rls.from, inTransaction: true, requireInstalledRls: true });
5124
6115
  await executor({
@@ -5135,7 +6126,7 @@ async function runTransactionalDown(params) {
5135
6126
  } else {
5136
6127
  await cleanupSelfManagedFailure({ executor });
5137
6128
  }
5138
- throw new VibeError15({
6129
+ throw new VibeError21({
5139
6130
  code: "VIBE_MIGRATION",
5140
6131
  message: `rollback of "${migration.name}" failed: ${error instanceof Error ? error.message : String(error)}`,
5141
6132
  meta: { migration: migration.name },
@@ -5166,10 +6157,15 @@ async function runNonTransactionalDown(params) {
5166
6157
  if (marks && start < downSql.length && resumeOperation === undefined && inflightStatement({ ...params.storedError === undefined ? {} : { error: params.storedError } }) === start) {
5167
6158
  throw uncertainStatement({ migration: migration.name, statements: downSql, index: start, dialect, direction: "down" });
5168
6159
  }
5169
- if (start === 0)
6160
+ if (start === 0) {
6161
+ if (migration.desiredState !== undefined)
6162
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.desiredState.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5170
6163
  await recordProgress(0);
6164
+ }
5171
6165
  let cursor = start;
5172
6166
  if (resumeOperation !== undefined && await establishOperationState({ executor, dialect, operation: resumeOperation }) === "satisfied") {
6167
+ if (start + 1 === downSql.length && migration.desiredState !== undefined)
6168
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.to, to: migration.desiredState.down ?? migration.desiredState.from, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5173
6169
  await recordProgress(start + 1);
5174
6170
  cursor = start + 1;
5175
6171
  }
@@ -5184,10 +6180,17 @@ async function runNonTransactionalDown(params) {
5184
6180
  }
5185
6181
  if (atomic)
5186
6182
  await executor({ text: "BEGIN" });
6183
+ let executed = false;
5187
6184
  try {
5188
- if (commitsBlock)
6185
+ if (commitsBlock) {
6186
+ if (migration.desiredState !== undefined && downSql.slice(index + 1).every((statement) => /^\s*PRAGMA\s+foreign_keys\s*=\s*ON\s*;?\s*$/i.test(statement)))
6187
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.to, to: migration.desiredState.down ?? migration.desiredState.from, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5189
6188
  await recordProgress(index + 1);
6189
+ }
5190
6190
  await executeStatement({ executor, statement: downSql[index], dialect });
6191
+ executed = true;
6192
+ if (index === downSql.length - 1 && migration.desiredState !== undefined)
6193
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.to, to: migration.desiredState.down ?? migration.desiredState.from, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5191
6194
  if (atomic) {
5192
6195
  await recordProgress(index + 1);
5193
6196
  await executor({ text: "COMMIT" });
@@ -5198,10 +6201,10 @@ async function runNonTransactionalDown(params) {
5198
6201
  try {
5199
6202
  await executor({
5200
6203
  text: `UPDATE ${table} SET ${q5(dialect, "error")} = ${ph(dialect, 1)} WHERE ${q5(dialect, "name")} = ${ph(dialect, 2)}`,
5201
- values: [error instanceof Error ? error.message : String(error), migration.name]
6204
+ values: [executed && marks ? `${INFLIGHT_PREFIX}${index}` : error instanceof Error ? error.message : String(error), migration.name]
5202
6205
  });
5203
6206
  } catch {}
5204
- throw new VibeError15({
6207
+ throw new VibeError21({
5205
6208
  code: "VIBE_MIGRATION",
5206
6209
  message: `rollback of "${migration.name}" failed at down statement ${index + 1} of ${downSql.length}: ${error instanceof Error ? error.message : String(error)} \u2014 progress is recorded; re-running the rollback resumes from this statement`,
5207
6210
  meta: { migration: migration.name, statementIndex: index },
@@ -5211,6 +6214,8 @@ async function runNonTransactionalDown(params) {
5211
6214
  if (!atomic && !commitsBlock)
5212
6215
  await recordProgress(index + 1);
5213
6216
  }
6217
+ if (migration.desiredState !== undefined)
6218
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.to, to: migration.desiredState.down ?? migration.desiredState.from, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5214
6219
  await executor({
5215
6220
  text: `DELETE FROM ${table} WHERE ${q5(dialect, "name")} = ${ph(dialect, 1)}`,
5216
6221
  values: [migration.name]
@@ -5226,10 +6231,11 @@ async function runTransactional(params) {
5226
6231
  const limits = migration.execution;
5227
6232
  if (wrap && dialect === "postgres" && limits !== undefined) {
5228
6233
  if ((limits.lockTimeoutMs ?? 0) > 0)
5229
- await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect2({ dialect }), setting: "lock_timeout", milliseconds: limits.lockTimeoutMs, local: true }).text });
6234
+ await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect3({ dialect }), setting: "lock_timeout", milliseconds: limits.lockTimeoutMs, local: true }).text });
5230
6235
  if ((limits.statementTimeoutMs ?? 0) > 0)
5231
- await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect2({ dialect }), setting: "statement_timeout", milliseconds: limits.statementTimeoutMs, local: true }).text });
6236
+ await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect3({ dialect }), setting: "statement_timeout", milliseconds: limits.statementTimeoutMs, local: true }).text });
5232
6237
  }
6238
+ await assertForwardStart({ executor, dialect, migration, ...params.probe === undefined ? {} : { probe: params.probe } });
5233
6239
  if (migration.rls !== undefined)
5234
6240
  await introspectPostgres({ executor, rlsSchema: migration.rls.from, inTransaction: true, requireInstalledRls: true });
5235
6241
  let statementIndex = 0;
@@ -5237,6 +6243,8 @@ async function runTransactional(params) {
5237
6243
  await executeStatement({ executor, statement, dialect });
5238
6244
  statementIndex += 1;
5239
6245
  }
6246
+ if (migration.desiredState !== undefined)
6247
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.desiredState.unmanagedRemovals, dialect, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5240
6248
  if (migration.rls !== undefined)
5241
6249
  await introspectPostgres({ executor, rlsSchema: migration.rls.to, inTransaction: true, requireInstalledRls: true });
5242
6250
  await executor({
@@ -5253,7 +6261,7 @@ async function runTransactional(params) {
5253
6261
  } else {
5254
6262
  await cleanupSelfManagedFailure({ executor });
5255
6263
  }
5256
- throw new VibeError15({
6264
+ throw new VibeError21({
5257
6265
  code: "VIBE_MIGRATION",
5258
6266
  message: `migration "${migration.name}" failed: ${error instanceof Error ? error.message : String(error)}`,
5259
6267
  meta: { migration: migration.name },
@@ -5274,6 +6282,11 @@ async function runNonTransactional(params) {
5274
6282
  const marks = marksInflight({ dialect, ...execution === undefined ? {} : { execution } });
5275
6283
  const start = selfManaged ? selfManagedResumeStart({ sql: migration.sql, start: params.start }) : params.start;
5276
6284
  if (insertRecord) {
6285
+ if (migration.desiredState !== undefined) {
6286
+ await assertForwardStart({ executor, dialect, migration, ...params.probe === undefined ? {} : { probe: params.probe } });
6287
+ if (migration.sql.length === 0)
6288
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.desiredState.unmanagedRemovals, dialect, ...params.probe === undefined ? {} : { probe: params.probe } }) });
6289
+ }
5277
6290
  await executor({
5278
6291
  text: `INSERT INTO ${table} (${q5(dialect, "name")}, ${q5(dialect, "checksum")}, ${q5(dialect, "statement_index")}) VALUES (${ph(dialect, 1)}, ${ph(dialect, 2)}, 0)`,
5279
6292
  values: [migration.name, checksumOf({ migration })]
@@ -5289,8 +6302,10 @@ async function runNonTransactional(params) {
5289
6302
  if (marks && !insertRecord && start < migration.sql.length) {
5290
6303
  const operation = declared ? operationFor({ ...execution === undefined ? {} : { execution }, statement: start }) : undefined;
5291
6304
  if (operation !== undefined) {
5292
- const state = await establishOperationState({ executor, dialect, operation });
5293
- if (state === "satisfied") {
6305
+ const state2 = await establishOperationState({ executor, dialect, operation });
6306
+ if (state2 === "satisfied") {
6307
+ if (start + 1 === migration.sql.length && migration.desiredState !== undefined)
6308
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.desiredState.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5294
6309
  await checkpoint(start + 1);
5295
6310
  cursor = start + 1;
5296
6311
  }
@@ -5309,10 +6324,17 @@ async function runNonTransactional(params) {
5309
6324
  }
5310
6325
  if (atomic)
5311
6326
  await executor({ text: "BEGIN" });
6327
+ let executed = false;
5312
6328
  try {
5313
- if (commitsBlock)
6329
+ if (commitsBlock) {
6330
+ if (migration.desiredState !== undefined && migration.sql.slice(index + 1).every((statement) => /^\s*PRAGMA\s+foreign_keys\s*=\s*ON\s*;?\s*$/i.test(statement)))
6331
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.desiredState.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5314
6332
  await checkpoint(index + 1);
6333
+ }
5315
6334
  await executeStatement({ executor, statement: migration.sql[index], dialect });
6335
+ executed = true;
6336
+ if (index === migration.sql.length - 1 && migration.desiredState !== undefined)
6337
+ assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.desiredState.unmanagedRemovals, dialect, ...params.probe === undefined ? {} : { probe: params.probe } }) });
5316
6338
  if (atomic) {
5317
6339
  await checkpoint(index + 1);
5318
6340
  await executor({ text: "COMMIT" });
@@ -5323,12 +6345,12 @@ async function runNonTransactional(params) {
5323
6345
  try {
5324
6346
  await executor({
5325
6347
  text: `UPDATE ${table} SET ${q5(dialect, "error")} = ${ph(dialect, 1)} WHERE ${q5(dialect, "name")} = ${ph(dialect, 2)}`,
5326
- values: [error instanceof Error ? error.message : String(error), migration.name]
6348
+ values: [executed && marks ? `${INFLIGHT_PREFIX}${index}` : error instanceof Error ? error.message : String(error), migration.name]
5327
6349
  });
5328
6350
  } catch {}
5329
6351
  const sqlState = sqlStateOf(error);
5330
6352
  const timeout = sqlState === "55P03" ? "lock-timeout" : sqlState === "57014" ? "statement-timeout" : undefined;
5331
- throw new VibeError15({
6353
+ throw new VibeError21({
5332
6354
  code: "VIBE_MIGRATION",
5333
6355
  message: timeout === "lock-timeout" ? `migration "${migration.name}" gave up waiting for a lock at statement ${index + 1} of ${migration.sql.length} (lock_timeout ${String(execution?.lockTimeoutMs ?? 0)}ms, SQLSTATE 55P03) \u2014 another session holds a conflicting lock. Nothing of this statement was applied; progress is recorded and re-running resumes here.` : timeout === "statement-timeout" ? `migration "${migration.name}" exceeded its statement_timeout at statement ${index + 1} of ${migration.sql.length} (${String(execution?.statementTimeoutMs ?? 0)}ms, SQLSTATE 57014) \u2014 progress is recorded and re-running resumes here.` : `migration "${migration.name}" failed at statement ${index + 1} of ${migration.sql.length}: ${error instanceof Error ? error.message : String(error)} \u2014 progress is recorded; re-running applyMigrations resumes from this statement`,
5334
6356
  meta: { migration: migration.name, statementIndex: index, ...timeout === undefined ? {} : { reason: timeout, sqlState } },
@@ -5352,7 +6374,7 @@ async function acquireLock(params) {
5352
6374
  });
5353
6375
  const acquired = Number(rows[0]?.vibe_lock) === 1;
5354
6376
  if (!acquired) {
5355
- throw new VibeError15({
6377
+ throw new VibeError21({
5356
6378
  code: "VIBE_MIGRATION",
5357
6379
  message: `could not acquire the migration lock ('${MYSQL_LOCK_NAME}') within ${MYSQL_LOCK_TIMEOUT_SECONDS}s \u2014 another migration run is in progress`,
5358
6380
  meta: { lock: MYSQL_LOCK_NAME }
@@ -5389,7 +6411,7 @@ function uncertainStatement(params) {
5389
6411
  const subject = direction === "down" ? `rollback of "${migration}"` : `migration "${migration}"`;
5390
6412
  const position = `${direction === "down" ? "down statement" : "statement"} ${index + 1} of ${statements.length}`;
5391
6413
  const why = dialect === "mysql" ? "MySQL commits every statement on its own, so nothing outside the statement records whether it finished." : "That statement declares no operation whose effect recovery can read back from the catalog.";
5392
- return new VibeError15({
6414
+ return new VibeError21({
5393
6415
  code: "VIBE_MIGRATION",
5394
6416
  message: `${subject} was interrupted while ${position} was running. ${why} ` + "It may or may not have committed, and replaying it could apply it twice, so VibeORM stops here instead of guessing. " + 'Inspect what that statement did to the database, then follow "Recovering from a stopped migration" in docs/migrations.md: ' + `clear the \`error\` column of this migration's journal row if the statement did NOT land, set \`statement_index\` to ${direction === "down" ? -(index + 2) : index + 1} if it DID, ` + "or finish the whole migration by hand and record that with `vibeorm migrate resolve --applied`. " + `The statement was: ${statements[index].slice(0, 200)}`,
5395
6417
  meta: {
@@ -5406,7 +6428,7 @@ function assertExecutable(params) {
5406
6428
  if (execution === undefined || isDefaultExecution({ execution }))
5407
6429
  return;
5408
6430
  if (dialect !== "postgres") {
5409
- throw new VibeError15({
6431
+ throw new VibeError21({
5410
6432
  code: "VIBE_UNSUPPORTED_CAPABILITY",
5411
6433
  message: `migration "${migration.name}" declares explicit execution semantics (transaction: "${execution.transaction}"), which only the postgres runner implements \u2014 ${dialect} has no separate nontransactional regime to opt into`,
5412
6434
  meta: { reason: "nontransactional-unsupported", dialect, migration: migration.name, feature: "nontransactionalMigrations" }
@@ -5429,7 +6451,7 @@ async function withSessionLimits(params) {
5429
6451
  const statement = execution?.statementTimeoutMs ?? 0;
5430
6452
  if (dialect !== "postgres" || lock === 0 && statement === 0)
5431
6453
  return params.run();
5432
- const sqlDialect = getSqlDialect2({ dialect });
6454
+ const sqlDialect = getSqlDialect3({ dialect });
5433
6455
  const previous = (await executor({ text: renderMigrationSessionRead({ dialect: sqlDialect }).text, values: [] }))[0];
5434
6456
  if (lock > 0)
5435
6457
  await executor({ text: renderMigrationSessionSet({ dialect: sqlDialect, setting: "lock_timeout", milliseconds: lock }).text });
@@ -5451,7 +6473,7 @@ function usesStatementBookkeeping(params) {
5451
6473
  const { dialect, sql: sql2 } = params;
5452
6474
  if (params.execution?.transaction === "none")
5453
6475
  return true;
5454
- if (!DIALECT_CAPABILITIES2[dialect].transactionalDdl)
6476
+ if (!DIALECT_CAPABILITIES3[dialect].transactionalDdl)
5455
6477
  return true;
5456
6478
  return dialect === "sqlite" && usesSelfManagedTransactions({ sql: sql2 });
5457
6479
  }
@@ -5487,7 +6509,7 @@ function assertChecksumMatches(params) {
5487
6509
  const actual = checksumOf({ migration });
5488
6510
  if (stored === actual)
5489
6511
  return;
5490
- throw new VibeError15({
6512
+ throw new VibeError21({
5491
6513
  code: "VIBE_MIGRATION",
5492
6514
  message: `checksum mismatch for migration "${migration.name}" \u2014 its SQL changed after it was applied (recorded ${stored.slice(0, 12)}\u2026, current ${actual.slice(0, 12)}\u2026)`,
5493
6515
  meta: { migration: migration.name, recorded: stored, current: actual }
@@ -5498,7 +6520,7 @@ function assertResumable(params) {
5498
6520
  const actual = checksumOf({ migration });
5499
6521
  if (stored.checksum === actual)
5500
6522
  return;
5501
- throw new VibeError15({
6523
+ throw new VibeError21({
5502
6524
  code: "VIBE_MIGRATION",
5503
6525
  message: `migration "${migration.name}" is partially applied (${stored.statementIndex} statement(s) done) and its SQL has CHANGED since \u2014 refusing to resume; restore the original SQL or repair with resolveMigration`,
5504
6526
  meta: {
@@ -5510,12 +6532,13 @@ function assertResumable(params) {
5510
6532
  });
5511
6533
  }
5512
6534
  function q5(dialect, name) {
5513
- return getSqlDialect2({ dialect }).quoteIdent({ name });
6535
+ return getSqlDialect3({ dialect }).quoteIdent({ name });
5514
6536
  }
5515
6537
  function ph(dialect, index) {
5516
- return getSqlDialect2({ dialect }).placeholder({ index });
6538
+ return getSqlDialect3({ dialect }).placeholder({ index });
5517
6539
  }
5518
6540
  // packages/migrate/src/down.ts
6541
+ import { VibeError as VibeError22, sqlObjectKey as sqlObjectKey6 } from "@vibeorm/schema";
5519
6542
  function buildRollbackTarget(params) {
5520
6543
  const { from, to, dialect } = params;
5521
6544
  const skipped = [];
@@ -5539,7 +6562,18 @@ function buildRollbackTarget(params) {
5539
6562
  patched = true;
5540
6563
  return { ...enumDef, values: [...enumDef.values, ...added] };
5541
6564
  });
5542
- return { target: patched ? { ...from, enums } : from, skipped };
6565
+ const models = dialect !== "mysql" ? from.models : from.models.map((model) => {
6566
+ const afterModel = to.models.find((candidate) => (candidate.dbName ?? candidate.name) === (model.dbName ?? model.name));
6567
+ return { ...model, fields: model.fields.map((field) => {
6568
+ const after = afterModel?.fields.find((candidate) => (candidate.dbName ?? candidate.name) === (field.dbName ?? field.name));
6569
+ if (field.type !== "String" || field.collation !== undefined || after?.collation === undefined)
6570
+ return field;
6571
+ patched = true;
6572
+ skipped.push(`column "${model.dbName ?? model.name}"."${field.dbName ?? field.name}": previous collation was not recorded; "${after.collation}" is retained`);
6573
+ return { ...field, collation: after.collation };
6574
+ }) };
6575
+ });
6576
+ return { target: patched ? { ...from, enums, models } : from, skipped };
5543
6577
  }
5544
6578
  function forceReAddedColumnsNullable(params) {
5545
6579
  return params.steps.map((step) => {
@@ -5557,7 +6591,8 @@ function generateDownMigration(params) {
5557
6591
  extensions: params.extensions ?? []
5558
6592
  });
5559
6593
  const allArtifacts = [...artifacts.setup, ...artifacts.objects];
5560
- const strippedFrom = stripExtensionArtifacts({ schema: from, artifacts: allArtifacts });
6594
+ const owned = [...from.migrationObjects ?? [], ...to.migrationObjects ?? []];
6595
+ const strippedFrom = preserveMigrationObjects({ from: stripExtensionArtifacts({ schema: from, artifacts: allArtifacts }), to: from, objects: owned });
5561
6596
  const { target, skipped } = buildRollbackTarget({ from: strippedFrom, to, dialect });
5562
6597
  const toViewNames = new Set(to.models.filter((model) => model.isView).map((model) => model.dbName ?? model.name));
5563
6598
  for (const model of target.models) {
@@ -5571,7 +6606,11 @@ function generateDownMigration(params) {
5571
6606
  const steps = forceReAddedColumnsNullable({
5572
6607
  steps: diffSchemas({ from: to, to: target, dialect })
5573
6608
  });
5574
- const coreSql = renderSteps({ steps, dialect, from: to, to: target });
6609
+ const restored = { ...target, models: target.models.map((model) => ({ ...model, fields: model.fields.map((field) => {
6610
+ const added = steps.find((step) => step.kind === "addColumn" && step.table === (model.dbName ?? model.name) && (step.column.dbName ?? step.column.name) === (field.dbName ?? field.name));
6611
+ return added?.kind === "addColumn" ? { ...field, isOptional: added.column.isOptional } : field;
6612
+ }) })) };
6613
+ const coreSql = renderSteps({ steps, dialect, from: to, to: dialect === "sqlite" ? sqlitePreservedSchema({ from, to: restored, objects: from.migrationObjects ?? [] }) : restored });
5575
6614
  const created = planMissingArtifacts({ introspected: from, artifacts: allArtifacts });
5576
6615
  const artifactDropSql = [];
5577
6616
  for (const artifact of [...created].reverse()) {
@@ -5581,93 +6620,403 @@ function generateDownMigration(params) {
5581
6620
  }
5582
6621
  artifactDropSql.push(...artifact.drop);
5583
6622
  }
6623
+ const previousKeys = new Set((from.migrationObjects ?? []).map((object) => sqlObjectKey6({ identity: object.identity })));
6624
+ const ownedDropSql = [];
6625
+ for (const object of orderedMigrationObjects({ objects: to.migrationObjects ?? [] }).reverse()) {
6626
+ if (previousKeys.has(sqlObjectKey6({ identity: object.identity })))
6627
+ continue;
6628
+ if (object.identity.kind === "postgresExtension") {
6629
+ skipped.push(`extension ${object.id} remains installed`);
6630
+ continue;
6631
+ }
6632
+ if (object.drop.length === 0)
6633
+ throw new VibeError22({ code: "VIBE_MIGRATION", message: `Migration object ${object.id} needs reviewed reverse SQL`, meta: { reason: "migration-object-reverse-required", id: object.id } });
6634
+ ownedDropSql.push(...object.drop);
6635
+ }
5584
6636
  return {
5585
6637
  name,
5586
- sql: [...artifactDropSql, ...coreSql],
6638
+ target: restored,
6639
+ sql: [...ownedDropSql, ...artifactDropSql, ...coreSql],
5587
6640
  destructive: steps.some((step) => step.destructive),
5588
6641
  ...securityChangesOf({ steps }).length === 0 ? {} : { securityChanges: securityChangesOf({ steps }) },
5589
6642
  skipped
5590
6643
  };
5591
6644
  }
5592
6645
  // packages/migrate/src/push.ts
5593
- import { VibeError as VibeError16, assertValidSchemaIR as assertValidSchemaIR3, providerToDialect } from "@vibeorm/schema";
5594
- import { DIALECT_CAPABILITIES as DIALECT_CAPABILITIES3, sqliteDialect as sqliteDialect3, renderDiagnostic } from "@vibeorm/sql";
6646
+ import { VibeError as VibeError25, assertValidSchemaIR as assertValidSchemaIR4, providerToDialect as providerToDialect2, sqlObjectKey as sqlObjectKey9 } from "@vibeorm/schema";
6647
+ import { DIALECT_CAPABILITIES as DIALECT_CAPABILITIES4 } from "@vibeorm/sql";
6648
+
6649
+ // packages/migrate/src/planner.ts
6650
+ import { VibeError as VibeError24, sqlObjectKey as sqlObjectKey8 } from "@vibeorm/schema";
6651
+ import { renderSqliteRebuildObjects, sqliteDialect as sqliteDialect3 } from "@vibeorm/sql";
6652
+
6653
+ // packages/migrate/src/inventory.ts
6654
+ import { VibeError as VibeError23, isSqlObjectIdentity as isSqlObjectIdentity2, sqlObjectKey as sqlObjectKey7 } from "@vibeorm/schema";
6655
+ import { getSqlDialect as getSqlDialect4, renderMigrationInventory, renderSqliteColumnInventory, renderMysqlObjectMembers as renderMysqlObjectMembers2, sqliteTableConstraints as sqliteTableConstraints3, renderSqliteForeignKeyInventory } from "@vibeorm/sql";
6656
+ async function inspectMigrationInventory(params) {
6657
+ await assertCatalogVisibility(params);
6658
+ const query = renderMigrationInventory({ dialect: getSqlDialect4({ dialect: params.dialect }), namespace: params.namespace });
6659
+ const rows = await params.executor({ text: query.text, values: [...query.values] });
6660
+ const objects = [];
6661
+ const kinds = {
6662
+ c: "check",
6663
+ f: "foreignKey",
6664
+ x: "exclusion",
6665
+ u: "unique",
6666
+ p: "primaryKey",
6667
+ CHECK: "check",
6668
+ "FOREIGN KEY": "foreignKey",
6669
+ UNIQUE: "unique",
6670
+ "PRIMARY KEY": "primaryKey"
6671
+ };
6672
+ for (const row of rows) {
6673
+ const name = row.name;
6674
+ if (typeof name !== "string" || typeof row.kind !== "string")
6675
+ throw new VibeError23({ code: "VIBE_MIGRATION", message: "Malformed catalog identity", meta: { reason: "inventory-unavailable" } });
6676
+ const qualified = { namespace: params.namespace, name };
6677
+ const table = { namespace: params.namespace, name: String(row.parent ?? "") };
6678
+ let identity;
6679
+ switch (row.kind) {
6680
+ case "table":
6681
+ case "view":
6682
+ case "sequence":
6683
+ identity = { kind: row.kind, name: qualified };
6684
+ break;
6685
+ case "trigger":
6686
+ case "index":
6687
+ identity = { kind: row.kind, table, name };
6688
+ break;
6689
+ case "column":
6690
+ case "columnDefault":
6691
+ case "generatedColumn":
6692
+ identity = { kind: row.kind, table, column: name };
6693
+ break;
6694
+ case "constraint": {
6695
+ const constraintType = kinds[String(row.subtype)];
6696
+ if (constraintType === undefined)
6697
+ throw new VibeError23({ code: "VIBE_MIGRATION", message: "Unknown constraint catalog kind", meta: { reason: "inventory-unavailable" } });
6698
+ identity = { kind: "constraint", table, name, constraintType };
6699
+ break;
6700
+ }
6701
+ case "function": {
6702
+ let argumentTypes = JSON.parse(String(row.signature));
6703
+ if (params.dialect === "mysql") {
6704
+ const query2 = renderMysqlObjectMembers2({ dialect: getSqlDialect4({ dialect: params.dialect }), identity: { kind: "function", name: qualified, argumentTypes: [] } });
6705
+ const members = await params.executor({ text: query2.text, values: [...query2.values] });
6706
+ if (members.length === 0)
6707
+ throw new VibeError23({ code: "VIBE_MIGRATION", message: "MySQL function signature unavailable", meta: { reason: "inventory-unavailable", name } });
6708
+ argumentTypes = members.filter((member) => member.mode !== null).map((member) => ({ namespace: "mysql", name: String(member.type) }));
6709
+ }
6710
+ const candidate = { kind: "function", name: qualified, argumentTypes };
6711
+ if (!isSqlObjectIdentity2(candidate))
6712
+ throw new VibeError23({ code: "VIBE_MIGRATION", message: "Invalid function signature in catalog", meta: { reason: "inventory-unavailable" } });
6713
+ identity = candidate;
6714
+ break;
6715
+ }
6716
+ default:
6717
+ throw new VibeError23({ code: "VIBE_MIGRATION", message: `Unknown catalog object kind ${row.kind}`, meta: { reason: "inventory-unavailable" } });
6718
+ }
6719
+ objects.push({ identity, ...typeof row.backing === "string" ? { backingConstraint: row.backing } : {} });
6720
+ if (params.dialect === "sqlite" && row.kind === "table" && typeof row.definition === "string" && !/^\s*CREATE\s+VIRTUAL\s+TABLE\b/i.test(row.definition)) {
6721
+ const clauses = sqliteTableConstraints3({ sql: row.definition });
6722
+ for (const constraint of clauses) {
6723
+ if (constraint.type === "check")
6724
+ continue;
6725
+ const name2 = constraint.named ? constraint.name : constraint.type === "primaryKey" ? `${qualified.name}_pkey` : constraint.type === "foreignKey" ? `${qualified.name}_${constraint.columns.join("_")}_fkey` : constraint.name;
6726
+ objects.push({ identity: { kind: "constraint", constraintType: constraint.type, table: qualified, name: name2 }, ...constraint.type === "unique" && !constraint.named ? { inspectionUnavailable: "sqlite-anonymous-unique-constraint" } : {} });
6727
+ }
6728
+ const checks = parseSqliteNamedChecks({ tableSql: row.definition });
6729
+ for (const check of checks)
6730
+ objects.push({ identity: { kind: "constraint", constraintType: "check", table: qualified, name: check.name } });
6731
+ if ([...row.definition.matchAll(/\bCHECK\s*\(/gi)].length > checks.length)
6732
+ objects.push({ identity: { kind: "table", name: qualified }, inspectionUnavailable: "sqlite-unnamed-check-definition-unavailable" });
6733
+ const query2 = renderSqliteColumnInventory({ dialect: getSqlDialect4({ dialect: params.dialect }), namespace: params.namespace, table: name });
6734
+ const columns = await params.executor({ text: query2.text, values: [...query2.values] });
6735
+ if (columns.some((column) => Number(column.pk) > 0) && !clauses.some((clause) => clause.type === "primaryKey"))
6736
+ objects.push({ identity: { kind: "constraint", constraintType: "primaryKey", table: qualified, name: `${qualified.name}_pkey` } });
6737
+ const fkQuery = renderSqliteForeignKeyInventory({ dialect: getSqlDialect4({ dialect: params.dialect }), namespace: params.namespace, table: name });
6738
+ const foreignKeys = await params.executor({ text: fkQuery.text, values: [...fkQuery.values] });
6739
+ const groups = new Map;
6740
+ for (const fk of foreignKeys)
6741
+ groups.set(Number(fk.id), [...groups.get(Number(fk.id)) ?? [], String(fk.from)]);
6742
+ for (const fields of groups.values()) {
6743
+ if (clauses.some((clause) => clause.type === "foreignKey" && JSON.stringify(clause.columns) === JSON.stringify(fields)))
6744
+ continue;
6745
+ objects.push({ identity: { kind: "constraint", constraintType: "foreignKey", table: qualified, name: `${qualified.name}_${fields.join("_")}_fkey` }, inspectionUnavailable: "sqlite-inline-foreign-key-definition-unavailable" });
6746
+ }
6747
+ for (const column of columns) {
6748
+ objects.push({ identity: { kind: "column", table: qualified, column: String(column.name) } });
6749
+ if (Number(column.hidden) > 1)
6750
+ objects.push({ identity: { kind: "generatedColumn", table: qualified, column: String(column.name) } });
6751
+ else if (column.definition !== null && column.definition !== undefined)
6752
+ objects.push({ identity: { kind: "columnDefault", table: qualified, column: String(column.name) } });
6753
+ }
6754
+ }
6755
+ }
6756
+ return [...new Map(objects.map((object) => [sqlObjectKey7({ identity: object.identity }), object])).values()];
6757
+ }
6758
+
6759
+ // packages/migrate/src/planner.ts
6760
+ async function planSchemaMigration(params) {
6761
+ const { executor, schema, dialect } = params;
6762
+ const namespace = params.namespace ?? await migrationNamespace({ executor, dialect });
6763
+ const collected = collectExtensionArtifacts({ schema, dialect, extensions: params.extensions ?? [] });
6764
+ const artifacts = [...collected.setup, ...collected.objects];
6765
+ const legacyObjects = legacyMigrationObjects({ artifacts, dialect, namespace });
6766
+ const objects = orderedMigrationObjects({ objects: schema.migrationObjects ?? [] });
6767
+ assertRetainedMigrationObjects({ from: params.previous?.schema.migrationObjects ?? [], to: objects });
6768
+ for (const object of objects)
6769
+ if (object.dialect !== dialect)
6770
+ throw new VibeError24({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Migration object ${object.id} requires ${object.dialect}`, meta: { feature: "migrationObject", dialect } });
6771
+ const physical = params.physical ?? (dialect === "postgres" ? await introspectPhysicalPostgres({ executor }) : dialect === "mysql" ? await introspectMysql({ executor }) : await introspectSqlite({ executor }));
6772
+ if (params.previous !== undefined)
6773
+ assertMigrationState({ report: await verifyMigrationState({ executor, expected: params.previous, dialect, ...params.probe === undefined ? {} : { probe: params.probe } }) });
6774
+ const inventory = await inspectMigrationInventory({ executor, dialect, namespace });
6775
+ for (const object of inventory) {
6776
+ const identity = object.identity;
6777
+ if (identity.kind !== "table" || physical.models.some((model) => (model.dbName ?? model.name) === identity.name.name) || normalizeSchema({ schema: physical, dialect }).joinTables.has(identity.name.name))
6778
+ continue;
6779
+ throw new VibeError24({ code: "VIBE_MIGRATION", message: "Catalog table is not visible to schema introspection; automatic planning cannot prove the complete physical state", meta: { reason: "inventory-unavailable", identity: object.identity } });
6780
+ }
6781
+ const states = [];
6782
+ if (dialect === "mysql" && inventory.some((object) => object.identity.kind === "view" || object.identity.kind === "function" || object.identity.kind === "trigger") && params.previous === undefined && physical.models.length > 0)
6783
+ states.push(...inventory.filter((object) => object.identity.kind === "view" || object.identity.kind === "function" || object.identity.kind === "trigger").map((object) => ({ identity: object.identity, ownership: "unknown", state: "unavailable", reason: "dependency-inspection-unavailable", differences: [] })));
6784
+ const creates = [];
6785
+ for (const object of objects) {
6786
+ for (const dependency of object.dependsOn) {
6787
+ if ([...objects.map((entry) => entry.identity), ...schemaObjectIdentities({ schema, dialect, namespace })].some((identity) => sqlObjectKey8({ identity }) === sqlObjectKey8({ identity: dependency })))
6788
+ continue;
6789
+ const state2 = await inspectSqlObject({ executor, dialect, identity: dependency });
6790
+ if (state2.state !== "present")
6791
+ states.push({ identity: dependency, ownership: "migration", state: "unavailable", reason: "dependency-unavailable", differences: [] });
6792
+ }
6793
+ const expected = object.expected ?? params.previous?.definitions.find((entry) => sqlObjectKey8({ identity: entry.identity }) === sqlObjectKey8({ identity: object.identity }));
6794
+ const live = await inspectSqlObject({ executor, dialect, identity: object.identity });
6795
+ if (expected === undefined)
6796
+ states.push({ identity: object.identity, ownership: "migration", state: "unavailable", differences: [], reason: "independent-definition-required" });
6797
+ else if (live.state === "missing") {
6798
+ states.push({ identity: object.identity, ownership: "migration", state: "missing", differences: [] });
6799
+ creates.push(...object.create);
6800
+ } else {
6801
+ const report = await verifyMigrationState({ executor, dialect, expected: { schema: { ...schema, models: [], migrationObjects: [object] }, definitions: [expected] } });
6802
+ states.push(...report.objects);
6803
+ }
6804
+ }
6805
+ for (const object of legacyObjects) {
6806
+ if (objects.some((entry) => sqlObjectKey8({ identity: entry.identity }) === sqlObjectKey8({ identity: object.identity })))
6807
+ throw new VibeError24({ code: "VIBE_SCHEMA_INVALID", message: "Duplicate combined and migration-only ownership", meta: { identity: object.identity } });
6808
+ const expected = object.expected ?? params.previous?.definitions.find((entry) => sqlObjectKey8({ identity: entry.identity }) === sqlObjectKey8({ identity: object.identity }));
6809
+ if (expected === undefined)
6810
+ states.push({ identity: object.identity, ownership: "migration", state: "unavailable", reason: "legacy-definition-required", differences: [] });
6811
+ else {
6812
+ const live = await inspectSqlObject({ executor, dialect, identity: object.identity });
6813
+ if (live.state === "missing")
6814
+ states.push({ identity: object.identity, ownership: "migration", state: "missing", differences: [] });
6815
+ else
6816
+ states.push(...(await verifyMigrationState({ executor, dialect, expected: { schema: { ...schema, models: [], migrationObjects: [object] }, definitions: [expected] } })).objects);
6817
+ }
6818
+ }
6819
+ const protectedObjects = [...legacyObjects, ...objects, ...params.previous?.schema.migrationObjects ?? []];
6820
+ const stripped = preserveMigrationObjects({ from: stripExtensionArtifacts({ schema: physical, artifacts }), to: schema, objects: protectedObjects, namespace });
6821
+ const checked = await prepareCheckDiff({ executor, from: stripped, to: schema, dialect, ...params.probe === undefined ? {} : { probe: params.probe }, ...params.previous === undefined ? {} : { previous: params.previous }, ...params.replaceChecks === undefined ? {} : { replaceChecks: params.replaceChecks } });
6822
+ const from = await prepareExclusionDiff({ ...params, from: checked, to: schema });
6823
+ const steps = diffSchemas({ from, to: schema, dialect });
6824
+ const known = new Set(schemaObjectIdentities({ schema: params.previous?.schema ?? schema, dialect, namespace }).map((identity) => sqlObjectKey8({ identity })));
6825
+ const declaredIndexes = normalizeSchema({ schema: params.previous?.schema ?? schema, dialect });
6826
+ const physicalIndexes = normalizeSchema({ schema: physical, dialect });
6827
+ for (const [table, model] of physicalIndexes.tables)
6828
+ for (const [key, index] of model.indexes) {
6829
+ if (index.constraintName !== undefined && declaredIndexes.tables.get(table)?.indexes.has(key))
6830
+ known.add(sqlObjectKey8({ identity: { kind: "constraint", constraintType: "unique", table: { namespace, name: table }, name: index.constraintName } }));
6831
+ }
6832
+ const migrationOwned = new Set(protectedObjects.map((object) => sqlObjectKey8({ identity: object.identity })));
6833
+ const removals = steps.flatMap((step) => {
6834
+ const identity = removalIdentity({ step, namespace });
6835
+ return identity === undefined ? [] : [identity];
6836
+ });
6837
+ const droppedTables = new Set(steps.flatMap((step) => step.kind === "dropTable" || step.kind === "dropJoinTable" ? [step.table] : []));
6838
+ const unmanagedRemovals = [];
6839
+ const invalid3 = dialect === "sqlite" ? sqliteInvalidatedTables({ steps, from: physical }) : { rebuilt: new Set, dropped: new Set };
6840
+ for (const object of inventory) {
6841
+ const key = sqlObjectKey8({ identity: object.identity });
6842
+ const normalizedOwner = object.backingConstraint === undefined ? undefined : inventory.find((entry) => entry.identity.kind === "constraint" && entry.identity.name === object.backingConstraint && ("table" in object.identity) && entry.identity.table.name === object.identity.table.name);
6843
+ const ownership = migrationOwned.has(key) ? "migration" : known.has(key) || normalizedOwner !== undefined && known.has(sqlObjectKey8({ identity: normalizedOwner.identity })) ? "schema" : "unknown";
6844
+ if (object.inspectionUnavailable !== undefined)
6845
+ states.push({ identity: object.identity, ownership, state: "unavailable", reason: object.inspectionUnavailable, differences: [] });
6846
+ if (object.inspectionUnavailable !== undefined && invalid3.rebuilt.has("table" in object.identity ? object.identity.table.name : object.identity.name.name))
6847
+ throw new VibeError24({ code: "VIBE_MIGRATION", message: "SQLite rebuild cannot discard an unrepresented constraint; declare its independent definition or use a reviewed migration", meta: { reason: "sqlite-preservation-unavailable", identity: object.identity } });
6848
+ if (ownership === "unknown")
6849
+ states.push({ identity: object.identity, ownership, state: "unavailable", reason: "unmanaged", differences: [] });
6850
+ let collateral = "table" in object.identity && object.identity.table.namespace === namespace && droppedTables.has(object.identity.table.name);
6851
+ const declared = protectedObjects.find((entry) => sqlObjectKey8({ identity: entry.identity }) === key);
6852
+ if (declared?.dependsOn.some((dependency) => removals.some((identity) => sqlObjectKey8({ identity }) === sqlObjectKey8({ identity: dependency })) || ("table" in dependency) && dependency.table.namespace === namespace && droppedTables.has(dependency.table.name)))
6853
+ collateral = true;
6854
+ if (ownership === "unknown" && object.identity.kind === "function" && steps.some((step) => step.kind === "dropTable" || step.kind === "dropColumn" || step.kind === "dropJoinTable"))
6855
+ states.push({ identity: object.identity, ownership, state: "unavailable", reason: "dependency-inspection-unavailable", differences: [] });
6856
+ if (dialect === "postgres" && steps.some((step) => step.kind === "dropTable" || step.kind === "dropColumn" || step.kind === "dropJoinTable")) {
6857
+ const inspected = await inspectSqlObject({ executor, dialect, identity: object.identity });
6858
+ if (inspected.state !== "present") {
6859
+ states.push({ identity: object.identity, ownership, state: "unavailable", reason: "dependency-inspection-unavailable", differences: [] });
6860
+ } else {
6861
+ const addresses = inspected.definition.facts.dependencyAddresses;
6862
+ if (Array.isArray(addresses))
6863
+ for (const address of addresses) {
6864
+ const value = JSON.parse(String(address));
6865
+ if (!Array.isArray(value) || !Array.isArray(value[2]))
6866
+ throw new VibeError24({ code: "VIBE_MIGRATION", message: "Malformed catalog dependency address" });
6867
+ const names = value[2];
6868
+ if (names[0] !== namespace || typeof names[1] !== "string")
6869
+ continue;
6870
+ if (droppedTables.has(names[1]) || steps.some((step) => step.kind === "dropColumn" && step.table === names[1] && step.column === names[2]))
6871
+ collateral = true;
6872
+ }
6873
+ }
6874
+ }
6875
+ if (dialect === "sqlite" && ownership === "unknown" && object.identity.kind === "generatedColumn" && invalid3.rebuilt.has(object.identity.table.name))
6876
+ throw new VibeError24({ code: "VIBE_MIGRATION", message: "SQLite generated-column preservation requires an independent ownership declaration", meta: { reason: "sqlite-preservation-unavailable", identity: object.identity } });
6877
+ const removed = collateral || removals.some((identity) => sqlObjectKey8({ identity }) === key);
6878
+ if (removed && ownership === "migration")
6879
+ throw new VibeError24({ code: "VIBE_MIGRATION", message: "Automatic DDL would remove a migration-owned object", meta: { reason: "migration-owned-removal", identity: object.identity } });
6880
+ if (removed && ownership === "unknown" && object.backingConstraint === undefined)
6881
+ unmanagedRemovals.push(object.identity);
6882
+ }
6883
+ const invalidated = new Set([...invalid3.rebuilt, ...invalid3.dropped]);
6884
+ const installed = await probeInstalledExtensionState({ executor, dialect, artifacts });
6885
+ const triggers = installed.triggers === undefined ? undefined : new Set(installed.triggers);
6886
+ for (const artifact of artifacts)
6887
+ if (artifact.owns.kind === "trigger" && invalidated.has(artifact.owns.table))
6888
+ triggers?.delete(artifact.owns.name);
6889
+ const missing = planMissingArtifacts({ introspected: physical, artifacts, installed: { ...installed, ...triggers === undefined ? {} : { triggers } } });
6890
+ const restore = new Map;
6891
+ const preservedColumns = new Map;
6892
+ const preservedDefaults = new Map;
6893
+ if (invalid3.rebuilt.size > 0) {
6894
+ const query = renderSqliteRebuildObjects({ dialect: sqliteDialect3 });
6895
+ const rows = await executor({ text: query.text, values: [] });
6896
+ const targetIndexes = normalizeSchema({ schema, dialect });
6897
+ for (const row of rows) {
6898
+ const table = String(row.tbl_name);
6899
+ if (!invalid3.rebuilt.has(table) || typeof row.sql !== "string")
6900
+ continue;
6901
+ if (row.type === "index" && (steps.some((step) => step.kind === "dropIndex" && step.table === table && step.name === row.name) || [...targetIndexes.tables.get(table)?.indexes.values() ?? []].some((index) => index.name === row.name)))
6902
+ continue;
6903
+ const statements = restore.get(table) ?? [];
6904
+ statements.push(row.sql);
6905
+ restore.set(table, statements);
6906
+ }
6907
+ for (const object of protectedObjects) {
6908
+ const identity = object.identity;
6909
+ if (identity.kind !== "column" && identity.kind !== "columnDefault" && identity.kind !== "generatedColumn" || !invalid3.rebuilt.has(identity.table.name))
6910
+ continue;
6911
+ const live = await inspectSqlObject({ executor, dialect, identity });
6912
+ if (live.state !== "present" || identity.kind !== "columnDefault" && typeof live.definition.facts.columnDefinition !== "string")
6913
+ throw new VibeError24({ code: "VIBE_MIGRATION", message: "Cannot read the verified SQLite column definition for preservation", meta: { reason: "sqlite-preservation-unavailable", identity } });
6914
+ if (identity.kind === "columnDefault") {
6915
+ const definition = live.definition.facts.definition;
6916
+ if (definition !== null && typeof definition !== "string")
6917
+ throw new VibeError24({ code: "VIBE_MIGRATION", message: "Cannot preserve SQLite default", meta: { identity } });
6918
+ const defaults = preservedDefaults.get(identity.table.name) ?? new Map;
6919
+ defaults.set(identity.column, definition);
6920
+ preservedDefaults.set(identity.table.name, defaults);
6921
+ } else {
6922
+ const columns = preservedColumns.get(identity.table.name) ?? new Map;
6923
+ columns.set(identity.column, { sql: String(live.definition.facts.columnDefinition), generated: Number(live.definition.facts.generated) > 1 });
6924
+ preservedColumns.set(identity.table.name, columns);
6925
+ }
6926
+ }
6927
+ }
6928
+ const preserved = dialect === "sqlite" ? sqlitePreservedSchema({ from: physical, to: schema, objects: protectedObjects }) : schema;
6929
+ const target = { ...preserved, models: preserved.models.map((model) => ({ ...model, fields: model.fields.map((field) => {
6930
+ const defaults = preservedDefaults.get(model.dbName ?? model.name);
6931
+ const column = field.dbName ?? field.name;
6932
+ if (!defaults?.has(column))
6933
+ return field;
6934
+ const { default: ignored, ...rest } = field;
6935
+ const expression = defaults.get(column);
6936
+ return { ...rest, ...typeof expression === "string" ? { default: { kind: "dbgenerated", sql: expression } } : {} };
6937
+ }) })) };
6938
+ const core = renderSteps({ steps, dialect, from: dialect === "sqlite" ? physical : from, to: target, sqliteRestore: restore, sqliteColumns: preservedColumns });
6939
+ const sql2 = [
6940
+ ...artifactCreateSql({ artifacts: missing.filter((artifact) => artifact.owns.kind === "postgresExtension") }),
6941
+ ...core,
6942
+ ...artifactCreateSql({ artifacts: missing.filter((artifact) => artifact.owns.kind !== "postgresExtension" && !(artifact.owns.kind === "trigger" && invalid3.rebuilt.has(artifact.owns.table))) }),
6943
+ ...creates
6944
+ ];
6945
+ if (sql2.length === 0) {
6946
+ const definitions = [...params.previous?.definitions ?? []];
6947
+ for (const object of objects)
6948
+ if (object.expected !== undefined && !definitions.some((definition) => sqlObjectKey8({ identity: definition.identity }) === sqlObjectKey8({ identity: object.identity })))
6949
+ definitions.push(object.expected);
6950
+ const report = await verifyMigrationState({ executor, dialect, expected: { schema, definitions }, ...params.probe === undefined ? {} : { probe: params.probe } });
6951
+ states.push(...report.objects);
6952
+ }
6953
+ const blocked = states.some((state2) => state2.reason === "dependency-inspection-unavailable" || state2.ownership !== "unknown" && state2.reason !== "legacy-definition-required" && (state2.state === "unavailable" || state2.state === "changed" || state2.state === "missing" && sql2.length === 0));
6954
+ return { physical, from, namespace, steps, objects: states, unmanagedRemovals, blocked, sql: blocked ? [] : sql2 };
6955
+ }
6956
+
6957
+ // packages/migrate/src/push.ts
5595
6958
  async function push(params) {
5596
- const { executor, schema } = params;
5597
- assertValidSchemaIR3({ schema });
5598
- const dialect = providerToDialect({ provider: schema.datasource.provider });
6959
+ assertValidSchemaIR4({ schema: params.schema });
6960
+ const dialect = providerToDialect2({ provider: params.schema.datasource.provider });
5599
6961
  if (dialect !== "postgres")
5600
6962
  return pushChanges({ ...params, dialect });
5601
- await executor({ text: "BEGIN" });
6963
+ await params.executor({ text: "BEGIN" });
5602
6964
  let committed = false;
5603
6965
  try {
5604
6966
  const result = await pushChanges({ ...params, dialect });
5605
6967
  if (params.dryRun !== true) {
5606
- await executor({ text: "COMMIT" });
6968
+ await params.executor({ text: "COMMIT" });
5607
6969
  committed = true;
5608
6970
  }
5609
6971
  return result;
5610
6972
  } finally {
5611
6973
  if (!committed)
5612
- await executor({ text: "ROLLBACK" });
6974
+ await params.executor({ text: "ROLLBACK" });
5613
6975
  }
5614
6976
  }
5615
6977
  async function pushChanges(params) {
5616
6978
  const { executor, schema, dialect } = params;
5617
- const artifacts = collectExtensionArtifacts({
6979
+ const physical = dialect === "postgres" ? await introspectPostgres({ executor, rlsSchema: params.previousSchema ?? schema, inTransaction: true, requireInstalledRls: params.previousSchema !== undefined }) : undefined;
6980
+ const plan = await planSchemaMigration({
6981
+ executor,
5618
6982
  schema,
5619
6983
  dialect,
5620
- extensions: params.extensions ?? []
6984
+ ...physical === undefined ? {} : { physical },
6985
+ ...params.previousState === undefined ? {} : { previous: params.previousState },
6986
+ ...params.extensions === undefined ? {} : { extensions: params.extensions },
6987
+ ...params.probe === undefined ? {} : { probe: params.probe },
6988
+ ...params.replaceChecks === undefined ? {} : { replaceChecks: params.replaceChecks }
5621
6989
  });
5622
- const allArtifacts = [...artifacts.setup, ...artifacts.objects];
5623
- const introspected = dialect === "postgres" ? await introspectPostgres({ executor, rlsSchema: params.previousSchema ?? schema, inTransaction: true, requireInstalledRls: params.previousSchema !== undefined }) : await introspectDialect({ executor, dialect });
5624
- const from = stripExtensionArtifacts({ schema: introspected, artifacts: allArtifacts });
5625
- const steps = diffSchemas({ from, to: schema, dialect });
6990
+ const { steps, sql: sql2 } = plan;
5626
6991
  const securityChanges = securityChangesOf({ steps });
5627
- if (params.dryRun !== true)
5628
- assertSecurityAcknowledgement({ securityChanges, ...params.acceptSecurityChanges === undefined ? {} : { acceptSecurityChanges: params.acceptSecurityChanges } });
6992
+ if (params.dryRun === true)
6993
+ return { steps, applied: [], planned: sql2, securityChanges, objects: plan.objects, blocked: plan.blocked, unmanagedRemovals: plan.unmanagedRemovals };
6994
+ if (plan.blocked)
6995
+ throw new VibeError25({ code: "VIBE_MIGRATION", message: "Schema push is blocked by unverified object definitions", meta: { reason: "migration-state-unverified", objects: plan.objects } });
6996
+ assertSecurityAcknowledgement({ securityChanges, ...params.acceptSecurityChanges === undefined ? {} : { acceptSecurityChanges: params.acceptSecurityChanges } });
5629
6997
  const destructive = steps.filter((step) => step.destructive);
5630
- if (params.dryRun !== true && destructive.length > 0 && params.acceptDataLoss !== true) {
5631
- throw new VibeError16({
5632
- code: "VIBE_MIGRATION",
5633
- message: `push refused: ${destructive.length} destructive step(s) would lose data \u2014 pass acceptDataLoss: true to proceed:
5634
- ${destructive.map((step) => ` - ${describeStep({ step })}`).join(`
5635
- `)}`,
5636
- meta: { destructive: destructive.map((step) => describeStep({ step })) }
5637
- });
5638
- }
5639
- const installed = await probeInstalledExtensionState({
5640
- executor,
6998
+ if (destructive.length > 0 && params.acceptDataLoss !== true)
6999
+ throw new VibeError25({ code: "VIBE_MIGRATION", message: `push refused: ${destructive.length} destructive step(s) would lose data; pass acceptDataLoss: true: ${destructive.map((step) => describeStep({ step })).join("; ")}`, meta: { destructive: destructive.map((step) => describeStep({ step })) } });
7000
+ assertRemovalAuthorization({
7001
+ steps,
5641
7002
  dialect,
5642
- artifacts: allArtifacts
5643
- });
5644
- const invalid2 = dialect === "sqlite" ? sqliteInvalidatedTables({ steps, from }) : { rebuilt: new Set, dropped: new Set };
5645
- const invalidated = new Set([...invalid2.rebuilt, ...invalid2.dropped]);
5646
- const restoreSql = invalid2.rebuilt.size > 0 ? await captureUnownedSqliteTriggers({ executor, tables: invalid2.rebuilt, artifacts: allArtifacts }) : [];
5647
- const missing = planMissingArtifacts({
5648
- introspected,
5649
- artifacts: allArtifacts,
5650
- installed: forgetInvalidatedArtifacts({ installed, artifacts: allArtifacts, invalidated })
7003
+ namespace: plan.namespace,
7004
+ ...params.previousState === undefined ? {} : { previous: params.previousState },
7005
+ ...params.acceptIntegrityLoss === undefined ? {} : { acceptIntegrityLoss: params.acceptIntegrityLoss },
7006
+ ...params.removeUnmanaged === undefined ? {} : { removeUnmanaged: params.removeUnmanaged }
5651
7007
  });
5652
- const setupSql = artifactCreateSql({
5653
- artifacts: missing.filter((artifact) => artifact.owns.kind === "postgresExtension")
5654
- });
5655
- const objectSql = artifactCreateSql({
5656
- artifacts: missing.filter((artifact) => artifact.owns.kind !== "postgresExtension")
5657
- });
5658
- const coreSql = renderSteps({ steps, dialect, from, to: schema });
5659
- const sql2 = [...setupSql, ...coreSql, ...restoreSql, ...objectSql];
5660
- if (params.dryRun === true)
5661
- return { steps, applied: [], planned: sql2, securityChanges };
5662
- if (sql2.length === 0)
5663
- return { steps, applied: [] };
5664
- const wrap = dialect !== "postgres" && DIALECT_CAPABILITIES3[dialect].transactionalDdl && !usesSelfManagedTransactions({ sql: sql2 });
7008
+ const approved = new Set((params.removeUnmanaged ?? []).map((identity) => sqlObjectKey9({ identity })));
7009
+ const unapproved = plan.unmanagedRemovals.filter((identity) => !approved.has(sqlObjectKey9({ identity })));
7010
+ if (unapproved.length > 0)
7011
+ throw new VibeError25({ code: "VIBE_MIGRATION", message: "DDL would remove unmanaged dependent objects; approve their exact identities", meta: { reason: "unmanaged-removal-required", objects: unapproved } });
7012
+ const wrap = dialect !== "postgres" && DIALECT_CAPABILITIES4[dialect].transactionalDdl && !usesSelfManagedTransactions({ sql: sql2 });
5665
7013
  if (wrap)
5666
7014
  await executor({ text: "BEGIN" });
5667
7015
  try {
5668
- for (const statement of sql2) {
7016
+ for (const statement of sql2)
5669
7017
  await executeStatement({ executor, statement, dialect });
5670
- }
7018
+ if ((schema.migrationObjects?.length ?? 0) > 0)
7019
+ assertMigrationState({ report: await verifyMigrationState({ executor, dialect, expected: createDesiredState({ schema: { ...schema, models: [] }, dialect }) }) });
5671
7020
  if (wrap)
5672
7021
  await executor({ text: "COMMIT" });
5673
7022
  } catch (error) {
@@ -5675,71 +7024,178 @@ ${destructive.map((step) => ` - ${describeStep({ step })}`).join(`
5675
7024
  try {
5676
7025
  await executor({ text: "ROLLBACK" });
5677
7026
  } catch {}
5678
- } else if (dialect === "sqlite") {
7027
+ } else if (dialect === "sqlite")
5679
7028
  await cleanupSelfManagedFailure({ executor });
5680
- }
5681
- throw new VibeError16({
5682
- code: "VIBE_MIGRATION",
5683
- message: `push failed while applying schema changes: ${error instanceof Error ? error.message : String(error)}`,
5684
- meta: { statements: sql2 },
5685
- cause: error
5686
- });
7029
+ throw new VibeError25({ code: "VIBE_MIGRATION", message: `push failed while applying schema changes: ${error instanceof Error ? error.message : String(error)}`, meta: { statements: sql2 }, cause: error });
5687
7030
  }
5688
- return { steps, applied: sql2, ...securityChanges.length === 0 ? {} : { securityChanges } };
7031
+ return { steps, applied: sql2, securityChanges, objects: plan.objects, desiredState: createDesiredState({ schema, dialect }) };
5689
7032
  }
5690
- function forgetInvalidatedArtifacts(params) {
5691
- const { installed, artifacts, invalidated } = params;
5692
- if (invalidated.size === 0 || installed.triggers === undefined)
5693
- return installed;
5694
- const surviving = new Set(installed.triggers);
5695
- for (const artifact of artifacts) {
5696
- if (artifact.owns.kind !== "trigger")
7033
+ async function pull(params) {
7034
+ switch (params.dialect ?? "postgres") {
7035
+ case "postgres":
7036
+ return introspectPostgres({ executor: params.executor });
7037
+ case "sqlite":
7038
+ return introspectSqlite({ executor: params.executor });
7039
+ case "mysql":
7040
+ return introspectMysql({ executor: params.executor });
7041
+ }
7042
+ }
7043
+ // packages/migrate/src/reference-state.ts
7044
+ import { VibeError as VibeError26, sqlObjectKey as sqlObjectKey10 } from "@vibeorm/schema";
7045
+ async function captureMigrationReference(params) {
7046
+ const namespace = await migrationNamespace({ executor: params.referenceExecutor, dialect: params.dialect });
7047
+ const identities = [...schemaObjectIdentities({ schema: params.schema, dialect: params.dialect, namespace }), ...(params.schema.migrationObjects ?? []).map((object) => object.identity)];
7048
+ const definitions = [];
7049
+ for (const identity of new Map(identities.map((identity2) => [sqlObjectKey10({ identity: identity2 }), identity2])).values()) {
7050
+ if (!params.schema.migrationObjects?.some((object) => sqlObjectKey10({ identity: object.identity }) === sqlObjectKey10({ identity })) && !(identity.kind === "constraint" && identity.constraintType === "check"))
5697
7051
  continue;
5698
- if (invalidated.has(artifact.owns.table))
5699
- surviving.delete(artifact.owns.name);
7052
+ const result = await inspectSqlObject({ executor: params.referenceExecutor, dialect: params.dialect, identity });
7053
+ if (result.state !== "present")
7054
+ throw new VibeError26({ code: "VIBE_MIGRATION", message: "Independent reference object is missing or unavailable", meta: { reason: "reference-unavailable", identity, state: result.state } });
7055
+ definitions.push(result.definition);
7056
+ }
7057
+ return createDesiredState({ schema: params.schema, dialect: params.dialect, definitions });
7058
+ }
7059
+ // packages/migrate/src/compile-desired-state.ts
7060
+ import { VibeError as VibeError27 } from "@vibeorm/schema";
7061
+ import { renderExclusionSetup, getSqlDialect as getSqlDialect5, renderMigrationServerVersion } from "@vibeorm/sql";
7062
+ async function compileDesiredState(params) {
7063
+ if (params.dialect !== "postgres")
7064
+ throw new VibeError27({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Rollback declaration compilation requires PostgreSQL", meta: { feature: "desiredStateCompilation", dialect: params.dialect } });
7065
+ if (params.steps.some((step) => step.kind === "alterColumn" || step.kind === "addColumn" || step.kind === "addEnumValue"))
7066
+ throw new VibeError27({ code: "VIBE_MIGRATION", message: "CHECK compilation after column or enum changes requires an independently built reference with the new type context", meta: { reason: "reference-required" } });
7067
+ const definitions = (params.schema.migrationObjects ?? []).flatMap((object) => object.expected === undefined ? [] : [object.expected]);
7068
+ await params.executor({ text: "SAVEPOINT vibeorm_desired_state" });
7069
+ try {
7070
+ const newObjects = params.steps.filter((step) => step.kind === "createTable" || step.kind === "createEnum" || step.kind === "addEnumValue");
7071
+ for (const text of renderSteps({ steps: newObjects, dialect: "postgres" }))
7072
+ await params.executor({ text });
7073
+ const models = [];
7074
+ for (const model of params.schema.models) {
7075
+ const table = { namespace: "public", name: model.dbName ?? model.name };
7076
+ const exclusions = [];
7077
+ for (const exclusion of model.exclusions ?? []) {
7078
+ for (const text of renderExclusionSetup({ dialect: getSqlDialect5({ dialect: "postgres" }), exclusion }))
7079
+ await params.executor({ text });
7080
+ exclusions.push({ ...exclusion, normalizationSource: exclusion.definition, normalizedDefinition: await deparseExclusion({ executor: params.executor, table: table.name, definition: exclusion.definition }) });
7081
+ }
7082
+ models.push({ ...model, ...exclusions.length ? { exclusions } : {} });
7083
+ for (const check of model.checks ?? [])
7084
+ definitions.push(await deparsePostgresCheckDefinition({ executor: params.executor, table, expression: check.expression, constraint: check.name, probe: "rollback" }));
7085
+ }
7086
+ const query = renderMigrationServerVersion({ dialect: getSqlDialect5({ dialect: params.dialect }) });
7087
+ const version = (await params.executor({ text: query.text, values: [] }))[0]?.version;
7088
+ if (typeof version !== "string")
7089
+ throw new VibeError27({ code: "VIBE_MIGRATION", message: "Cannot determine definition normalization version" });
7090
+ return { ...createDesiredState({ schema: { ...params.schema, models }, dialect: params.dialect, definitions }), normalization: { format: "catalog-v1", dialect: params.dialect, serverVersion: version } };
7091
+ } finally {
7092
+ await params.executor({ text: "ROLLBACK TO SAVEPOINT vibeorm_desired_state" });
7093
+ await params.executor({ text: "RELEASE SAVEPOINT vibeorm_desired_state" });
5700
7094
  }
5701
- return { ...installed, triggers: surviving };
5702
7095
  }
5703
- async function captureUnownedSqliteTriggers(params) {
5704
- const { executor, tables, artifacts } = params;
5705
- const owned = new Set(artifacts.flatMap((artifact) => artifact.owns.kind === "trigger" ? [artifact.owns.name] : []));
5706
- const rows = await executor(renderDiagnostic({ dialect: sqliteDialect3, query: "sqliteTriggers" }));
5707
- return rows.filter((row) => tables.has(String(row["tbl_name"])) && !owned.has(String(row["name"])) && typeof row["sql"] === "string").map((row) => `${String(row["sql"]).trimEnd().replace(/;$/, "")};`);
7096
+ // packages/migrate/src/history.ts
7097
+ import { VibeError as VibeError28 } from "@vibeorm/schema";
7098
+ import { getSqlDialect as getSqlDialect6, renderMigrationHistoryQuery } from "@vibeorm/sql";
7099
+ var MISSING_TABLE_IDENTIFIERS = new Set(["42P01", "42S02", "ER_NO_SUCH_TABLE"]);
7100
+ function isMissingTable(params) {
7101
+ let current = params.cause;
7102
+ for (let depth = 0;depth < 6 && current !== null && typeof current === "object"; depth++) {
7103
+ const record2 = current;
7104
+ for (const key of ["code", "errno", "sqlState"]) {
7105
+ const value = record2[key];
7106
+ if (typeof value === "string" && MISSING_TABLE_IDENTIFIERS.has(value))
7107
+ return true;
7108
+ }
7109
+ current = record2.cause;
7110
+ }
7111
+ return false;
5708
7112
  }
5709
- async function pull(params) {
5710
- return introspectDialect({ executor: params.executor, dialect: params.dialect ?? "postgres" });
7113
+ async function readMigrationHistory(params) {
7114
+ const namespace = await migrationNamespace({ executor: params.executor, dialect: params.dialect });
7115
+ if (params.dialect === "sqlite") {
7116
+ const presence = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity: { kind: "table", name: { namespace, name: "_vibe_migrations" } } });
7117
+ if (presence.state === "missing")
7118
+ return { tablePresent: false, recorded: [] };
7119
+ if (presence.state === "unavailable")
7120
+ throw new VibeError28({ code: "VIBE_MIGRATION", message: "Migration history inspection unavailable", meta: { reason: "history-unavailable" } });
7121
+ }
7122
+ const query = renderMigrationHistoryQuery({ dialect: getSqlDialect6({ dialect: params.dialect }), namespace });
7123
+ const rows = await params.executor({ text: query.text, values: [] }).catch((cause) => {
7124
+ if (isMissingTable({ cause }))
7125
+ return;
7126
+ throw new VibeError28({ code: "VIBE_MIGRATION", message: "Cannot inspect migration history; check SELECT privileges", meta: { reason: "history-unavailable" }, cause });
7127
+ });
7128
+ if (rows === undefined)
7129
+ return { tablePresent: false, recorded: [] };
7130
+ const byName = new Map(params.migrations.map((migration) => [migration.name, migration]));
7131
+ const recorded = [];
7132
+ for (const row of rows) {
7133
+ if (typeof row.name !== "string" || typeof row.checksum !== "string" || !Number.isSafeInteger(Number(row.statement_index)))
7134
+ throw new VibeError28({ code: "VIBE_MIGRATION", message: "Malformed migration journal row", meta: { reason: "history-unavailable" } });
7135
+ const migration = byName.get(row.name);
7136
+ const statementIndex = Number(row.statement_index);
7137
+ recorded.push({ name: row.name, checksum: row.checksum, statementIndex, state: migration === undefined ? "unknown-file" : row.checksum !== migrationChecksum(migration) ? "mismatched" : statementIndex < 0 ? "rolling-back" : statementIndex < migration.sql.length || row.error !== null && row.error !== undefined ? "partial" : "applied" });
7138
+ }
7139
+ return { tablePresent: true, recorded };
7140
+ }
7141
+ // packages/migrate/src/adoption.ts
7142
+ import { VibeError as VibeError29 } from "@vibeorm/schema";
7143
+ async function adoptMigration(params) {
7144
+ const report = await verifyMigrationState({ executor: params.executor, expected: params.reference, dialect: params.dialect });
7145
+ assertMigrationState({ report });
7146
+ return {
7147
+ name: params.name,
7148
+ sql: [],
7149
+ downSql: [],
7150
+ destructive: false,
7151
+ desiredState: { version: 1, dialect: params.dialect, from: params.reference, to: params.reference }
7152
+ };
5711
7153
  }
5712
- async function introspectDialect(params) {
5713
- const { executor, dialect } = params;
5714
- switch (dialect) {
5715
- case "postgres":
5716
- return introspectPostgres({ executor });
5717
- case "sqlite":
5718
- return introspectSqlite({ executor });
5719
- case "mysql":
5720
- return introspectMysql({ executor });
7154
+ function deployedDesiredState(params) {
7155
+ for (const row of params.recorded) {
7156
+ if (row.state !== "applied" || row.checksum === "")
7157
+ throw new VibeError29({ code: "VIBE_MIGRATION", message: "Desired state requires complete, checksum-verified migration history", meta: { reason: "desired-state-history-unverified", migration: row.name, state: row.state } });
5721
7158
  }
7159
+ const applied = new Set(params.recorded.map((row) => row.name));
7160
+ if (params.migrations.some((migration) => applied.has(migration.name) && migration.desiredStatePending === true))
7161
+ throw new VibeError29({ code: "VIBE_MIGRATION", message: "Applied unfinalized metadata cannot establish desired state", meta: { reason: "desired-state-history-unverified" } });
7162
+ const last = [...params.migrations].reverse().find((migration) => applied.has(migration.name));
7163
+ if (last !== undefined && last.desiredState === undefined && params.migrations.some((migration) => applied.has(migration.name) && migration.desiredState !== undefined))
7164
+ throw new VibeError29({ code: "VIBE_MIGRATION", message: "A later metadata-free migration invalidates the earlier desired-state baseline; adopt an independent current reference", meta: { reason: "desired-state-history-unverified", migration: last.name } });
7165
+ return last?.desiredState?.to;
5722
7166
  }
5723
7167
  export {
7168
+ verifyMigrationState,
5724
7169
  stripExtensionArtifacts,
5725
7170
  serializeExecutionSidecar,
7171
+ serializeDesiredStateSidecar,
7172
+ schemaObjectIdentities,
5726
7173
  rollbackMigration,
5727
7174
  resolveMigration,
5728
7175
  renderSteps,
7176
+ removalIdentity,
5729
7177
  recordedMigrations,
7178
+ readMigrationHistory,
5730
7179
  push,
5731
7180
  pull,
5732
7181
  probeInstalledExtensionState,
7182
+ prepareCheckDiff,
7183
+ planSchemaMigration,
5733
7184
  planRenames,
5734
7185
  planMissingArtifacts,
5735
7186
  parseExecutionSidecar,
7187
+ parseDesiredStateSidecar,
5736
7188
  operationFor,
5737
7189
  migrationStatus,
7190
+ migrationNamespace,
5738
7191
  migrationChecksum,
7192
+ legacyMigrationObjects,
5739
7193
  isDefaultExecution,
5740
7194
  introspectSqlite,
5741
7195
  introspectPostgres,
5742
7196
  introspectMysql,
7197
+ inspectSqlObject,
7198
+ inspectMigrationInventory,
5743
7199
  generateMigration,
5744
7200
  generateDownMigration,
5745
7201
  executionChecksumSuffix,
@@ -5750,13 +7206,26 @@ export {
5750
7206
  describeStep,
5751
7207
  describeOperation,
5752
7208
  describeMigrationRisks,
7209
+ deployedDesiredState,
7210
+ deparsePostgresCheckDefinition,
7211
+ deparsePostgresCheck,
7212
+ deparseMysqlCheck,
7213
+ createDesiredState,
7214
+ compileDesiredState,
5753
7215
  collectExtensionArtifacts,
7216
+ captureMigrationReference,
5754
7217
  assertValidExecution,
7218
+ assertRemovalAuthorization,
7219
+ assertProbeMode,
7220
+ assertMigrationState,
5755
7221
  artifactCreateSql,
5756
7222
  applyMigrations,
7223
+ adoptMigration,
7224
+ UNFINALIZED_DESIRED_STATE,
5757
7225
  MIGRATIONS_TABLE_NAME,
5758
7226
  EXECUTION_SIDECAR_VERSION,
5759
- EXECUTION_SIDECAR_FILENAME
7227
+ EXECUTION_SIDECAR_FILENAME,
7228
+ DESIRED_STATE_METADATA
5760
7229
  };
5761
7230
 
5762
- //# debugId=A821378E4DC91CC064756E2164756E21
7231
+ //# debugId=EA05FB5D6F27037864756E2164756E21