@vibeorm/migrate 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adoption.d.ts +16 -0
- package/dist/adoption.d.ts.map +1 -0
- package/dist/compile-desired-state.d.ts +12 -0
- package/dist/compile-desired-state.d.ts.map +1 -0
- package/dist/ddl/postgres.d.ts.map +1 -1
- package/dist/ddl/render.d.ts +7 -0
- package/dist/ddl/render.d.ts.map +1 -1
- package/dist/ddl/sqlite.d.ts +5 -0
- package/dist/ddl/sqlite.d.ts.map +1 -1
- package/dist/desired-state.d.ts +69 -0
- package/dist/desired-state.d.ts.map +1 -0
- package/dist/differ.d.ts.map +1 -1
- package/dist/down.d.ts +2 -0
- package/dist/down.d.ts.map +1 -1
- package/dist/exclusions.d.ts +26 -0
- package/dist/exclusions.d.ts.map +1 -0
- package/dist/execution.d.ts +1 -1
- package/dist/execution.d.ts.map +1 -1
- package/dist/fs.d.ts +7 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +6758 -0
- package/dist/fs.js.map +41 -0
- package/dist/history.d.ts +14 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1672 -203
- package/dist/index.js.map +29 -19
- package/dist/inspect-objects.d.ts +116 -0
- package/dist/inspect-objects.d.ts.map +1 -0
- package/dist/introspect/mysql.d.ts.map +1 -1
- package/dist/introspect/postgres.d.ts +4 -0
- package/dist/introspect/postgres.d.ts.map +1 -1
- package/dist/introspect/shared.d.ts.map +1 -1
- package/dist/inventory.d.ts +15 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/normalize.d.ts +2 -5
- package/dist/normalize.d.ts.map +1 -1
- package/dist/object-ownership.d.ts +58 -0
- package/dist/object-ownership.d.ts.map +1 -0
- package/dist/planner.d.ts +29 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/push.d.ts +9 -36
- package/dist/push.d.ts.map +1 -1
- package/dist/reference-state.d.ts +14 -0
- package/dist/reference-state.d.ts.map +1 -0
- package/dist/runner.d.ts +14 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/sql-statements.d.ts +9 -0
- package/dist/sql-statements.d.ts.map +1 -0
- package/dist/sqlite-types.d.ts.map +1 -1
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- 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(
|
|
427
|
-
const name = match[1]
|
|
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
|
-
|
|
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 {
|
|
1606
|
-
import {
|
|
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
|
|
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)}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
3118
|
+
if (error instanceof VibeError13)
|
|
2982
3119
|
throw error;
|
|
2983
|
-
throw new
|
|
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
|
|
3213
|
+
const key = JSON.stringify([row.table_name, constraintName]);
|
|
3214
|
+
const existing = fkGroupsByName.get(key);
|
|
3074
3215
|
if (existing === undefined) {
|
|
3075
|
-
fkGroupsByName.set(
|
|
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
|
-
|
|
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
|
|
4342
|
+
import { VibeError as VibeError21 } from "@vibeorm/schema";
|
|
4192
4343
|
import {
|
|
4193
|
-
DIALECT_CAPABILITIES as
|
|
4194
|
-
getSqlDialect as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
4836
|
-
throw new
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
5293
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
6535
|
+
return getSqlDialect3({ dialect }).quoteIdent({ name });
|
|
5514
6536
|
}
|
|
5515
6537
|
function ph(dialect, index) {
|
|
5516
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
5594
|
-
import { DIALECT_CAPABILITIES as
|
|
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
|
-
|
|
5597
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
5628
|
-
|
|
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 (
|
|
5631
|
-
throw new
|
|
5632
|
-
|
|
5633
|
-
|
|
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
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
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
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
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,
|
|
7031
|
+
return { steps, applied: sql2, securityChanges, objects: plan.objects, desiredState: createDesiredState({ schema, dialect }) };
|
|
5689
7032
|
}
|
|
5690
|
-
function
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
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
|
-
|
|
5699
|
-
|
|
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
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
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
|
|
5710
|
-
|
|
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
|
-
|
|
5713
|
-
const
|
|
5714
|
-
|
|
5715
|
-
|
|
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=
|
|
7231
|
+
//# debugId=EA05FB5D6F27037864756E2164756E21
|