@vibeorm/migrate 2.6.0 → 3.0.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/LICENSE +21 -0
- package/dist/desired-state.d.ts +9 -0
- package/dist/differ.d.ts +7 -0
- package/dist/fs.js +449 -269
- package/dist/fs.js.map +15 -14
- package/dist/index.js +820 -467
- package/dist/index.js.map +17 -15
- package/dist/inspect-objects.d.ts +8 -0
- package/dist/introspect/group.d.ts +6 -0
- package/dist/introspect/mysql.d.ts +4 -3
- package/dist/normalize.d.ts +19 -1
- package/dist/object-ownership.d.ts +8 -0
- package/dist/postgres-types.d.ts +24 -0
- package/dist/sqlite-fts-upgrade.d.ts +64 -0
- package/package.json +6 -4
- package/dist/adoption.d.ts.map +0 -1
- package/dist/artifact-probe.d.ts.map +0 -1
- package/dist/baseline-record.d.ts.map +0 -1
- package/dist/baseline.d.ts.map +0 -1
- package/dist/compare-reference-rls.d.ts.map +0 -1
- package/dist/compare-sqlite-primary-key.d.ts.map +0 -1
- package/dist/compile-desired-state.d.ts.map +0 -1
- package/dist/ddl/drop-order.d.ts.map +0 -1
- package/dist/ddl/mysql.d.ts.map +0 -1
- package/dist/ddl/postgres.d.ts.map +0 -1
- package/dist/ddl/render.d.ts.map +0 -1
- package/dist/ddl/rls.d.ts.map +0 -1
- package/dist/ddl/sqlite.d.ts.map +0 -1
- package/dist/desired-state.d.ts.map +0 -1
- package/dist/differ.d.ts.map +0 -1
- package/dist/down.d.ts.map +0 -1
- package/dist/enum-values.d.ts.map +0 -1
- package/dist/exclusions.d.ts.map +0 -1
- package/dist/execute.d.ts.map +0 -1
- package/dist/execution.d.ts.map +0 -1
- package/dist/extensions.d.ts.map +0 -1
- package/dist/external-dependencies.d.ts.map +0 -1
- package/dist/external-objects.d.ts.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/history.d.ts.map +0 -1
- package/dist/index-predicates.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/inspect-objects.d.ts.map +0 -1
- package/dist/introspect/mysql.d.ts.map +0 -1
- package/dist/introspect/postgres-rls.d.ts.map +0 -1
- package/dist/introspect/postgres.d.ts.map +0 -1
- package/dist/introspect/shared.d.ts.map +0 -1
- package/dist/introspect/sqlite.d.ts.map +0 -1
- package/dist/inventory.d.ts.map +0 -1
- package/dist/journal-read.d.ts.map +0 -1
- package/dist/journal-table.d.ts.map +0 -1
- package/dist/migration-checksum.d.ts.map +0 -1
- package/dist/module-adoption-record.d.ts.map +0 -1
- package/dist/module-adoption-reference.d.ts.map +0 -1
- package/dist/module-adoption.d.ts.map +0 -1
- package/dist/module-bundle.d.ts.map +0 -1
- package/dist/module-export.d.ts.map +0 -1
- package/dist/module-history.d.ts.map +0 -1
- package/dist/module-journal.d.ts.map +0 -1
- package/dist/module-origin.d.ts.map +0 -1
- package/dist/module-ownership.d.ts.map +0 -1
- package/dist/module-pending.d.ts.map +0 -1
- package/dist/module-readiness.d.ts.map +0 -1
- package/dist/mysql-catalog-visibility.d.ts.map +0 -1
- package/dist/mysql-types.d.ts.map +0 -1
- package/dist/normalize.d.ts.map +0 -1
- package/dist/object-ownership.d.ts.map +0 -1
- package/dist/operator-classes.d.ts.map +0 -1
- package/dist/planner.d.ts.map +0 -1
- package/dist/postgres-default-equivalence.d.ts.map +0 -1
- package/dist/postgres-type-identity.d.ts.map +0 -1
- package/dist/postgres-types.d.ts.map +0 -1
- package/dist/push.d.ts.map +0 -1
- package/dist/recovery.d.ts.map +0 -1
- package/dist/reference-state.d.ts.map +0 -1
- package/dist/relations.d.ts.map +0 -1
- package/dist/renames.d.ts.map +0 -1
- package/dist/replay-objects.d.ts.map +0 -1
- package/dist/replay-plan.d.ts.map +0 -1
- package/dist/replay-reference.d.ts.map +0 -1
- package/dist/runner.d.ts.map +0 -1
- package/dist/shadow-dedicated.d.ts.map +0 -1
- package/dist/shadow-guard.d.ts.map +0 -1
- package/dist/shadow-types.d.ts.map +0 -1
- package/dist/shadow.d.ts.map +0 -1
- package/dist/sql-statements.d.ts.map +0 -1
- package/dist/sqlite-types.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/verify-history.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/migrate/src/external-objects.ts
|
|
3
|
-
import { IDENTIFIER_LIMITS, VibeError as
|
|
3
|
+
import { IDENTIFIER_LIMITS, VibeError as VibeError18, identifierLength, sqlNamespaceIssue, sqlObjectKey as sqlObjectKey6, sqlObjectNamespace as sqlObjectNamespace2, qualifiedSqlName as qualifiedSqlName8, enumStorageOf as enumStorageOf3, unsupportedCapability as unsupportedCapability7 } from "@vibeorm/schema";
|
|
4
4
|
import { getSqlDialect as getSqlDialect3, renderExternalOwnershipCatalog, renderReplayCoverageGaps } from "@vibeorm/sql";
|
|
5
5
|
|
|
6
6
|
// packages/migrate/src/inventory.ts
|
|
7
|
-
import { VibeError as
|
|
7
|
+
import { VibeError as VibeError17, defaultForeignKeyName as defaultForeignKeyName2, defaultPrimaryKeyName as defaultPrimaryKeyName2, isSqlObjectIdentity as isSqlObjectIdentity2, sqlObjectKey as sqlObjectKey5 } from "@vibeorm/schema";
|
|
8
8
|
import { getSqlDialect as getSqlDialect2, renderMigrationInventory, renderSqliteColumnInventory as renderSqliteColumnInventory2, renderMysqlObjectMembers as renderMysqlObjectMembers2, sqliteTableConstraints as sqliteTableConstraints3, renderSqliteForeignKeyInventory } from "@vibeorm/sql";
|
|
9
9
|
|
|
10
10
|
// packages/migrate/src/exclusions.ts
|
|
@@ -85,16 +85,12 @@ async function verifyExclusions(params) {
|
|
|
85
85
|
|
|
86
86
|
// packages/migrate/src/inspect-objects.ts
|
|
87
87
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
88
|
-
import { VibeError as
|
|
88
|
+
import { VibeError as VibeError16, sqlObjectKey as sqlObjectKey4, sqlObjectNamespace, DIALECT_CAPABILITIES as DIALECT_CAPABILITIES2, qualifiedSqlName as qualifiedSqlName7 } from "@vibeorm/schema";
|
|
89
89
|
import { getSqlDialect, renderMigrationObjectBatch, renderMigrationObjectQuery as renderMigrationObjectQuery2, renderCheckProbe, renderMigrationNamespace, renderMigrationDependencies, renderMysqlObjectMembers, renderMysqlCheckProbe, sqliteColumnClauses as sqliteColumnClauses2, renderMigrationVisibility, sqliteTableConstraints as sqliteTableConstraints2, renderSqliteColumnInventory } from "@vibeorm/sql";
|
|
90
90
|
|
|
91
91
|
// packages/migrate/src/desired-state.ts
|
|
92
92
|
import { VibeError as VibeError2, assertValidSchemaIR, isSqlObjectDefinition, isSqlObjectIdentity, providerToDialect, sqlObjectKey as sqlObjectKey2 } from "@vibeorm/schema";
|
|
93
93
|
var UNFINALIZED_DESIRED_STATE = Object.freeze({ version: 1, status: "unfinalized" });
|
|
94
|
-
function isUnfinalizedDesiredState(params) {
|
|
95
|
-
const value = JSON.parse(params.text);
|
|
96
|
-
return record(value) && value.version === 1 && value.status === "unfinalized" && Object.keys(value).length === 2;
|
|
97
|
-
}
|
|
98
94
|
var DESIRED_STATE_METADATA = Object.freeze({ filename: "desired-state.json", version: 1 });
|
|
99
95
|
function createDesiredState(params) {
|
|
100
96
|
assertValidSchemaIR({ schema: params.schema });
|
|
@@ -177,6 +173,15 @@ function parseDesiredStateSidecar(params) {
|
|
|
177
173
|
return checked.value;
|
|
178
174
|
}, failure: (cause) => invalid({ migration: params.migration, detail: "malformed metadata", cause }) });
|
|
179
175
|
}
|
|
176
|
+
function loadDesiredStateSidecar(params) {
|
|
177
|
+
return parseMigrationMetadata({ text: params.text, parse: (value) => {
|
|
178
|
+
if (record(value) && value.version === 1 && value.status === "unfinalized" && Object.keys(value).length === 2)
|
|
179
|
+
return { desiredStatePending: true };
|
|
180
|
+
const checked = { value, migration: params.migration };
|
|
181
|
+
assertDesiredState(checked);
|
|
182
|
+
return { desiredState: checked.value };
|
|
183
|
+
}, failure: (cause) => invalid({ migration: params.migration, detail: "malformed metadata", cause }) });
|
|
184
|
+
}
|
|
180
185
|
function parseMigrationMetadata(params) {
|
|
181
186
|
try {
|
|
182
187
|
return params.parse(JSON.parse(params.text));
|
|
@@ -233,6 +238,9 @@ var SCALAR_TO_PG = {
|
|
|
233
238
|
var CANONICAL_EQUIVALENTS = {
|
|
234
239
|
DateTime: ["timestamptz(3)", "timestamptz(6)"]
|
|
235
240
|
};
|
|
241
|
+
var EXACT_CANONICAL_EQUIVALENTS = {
|
|
242
|
+
DateTime: ["timestamptz(6)"]
|
|
243
|
+
};
|
|
236
244
|
var CANONICAL_UDT = {
|
|
237
245
|
text: "String",
|
|
238
246
|
bool: "Boolean",
|
|
@@ -301,13 +309,15 @@ var NATIVE_ALIASES = {
|
|
|
301
309
|
var IMPLICIT_MODIFIERS = {
|
|
302
310
|
bit: "(1)"
|
|
303
311
|
};
|
|
312
|
+
var DEFAULT_PRECISION_SIX = new Set(["timestamp", "time", "timetz"]);
|
|
304
313
|
function normalizeNativeType(params) {
|
|
305
314
|
const compact = params.raw.toLowerCase().replace(/\s+/g, "");
|
|
306
315
|
const match = compact.match(/^([a-z_0-9]+)(\(.*\))?(\[\])?$/);
|
|
307
316
|
if (match === null)
|
|
308
317
|
return compact;
|
|
309
318
|
const base = NATIVE_ALIASES[match[1]] ?? match[1];
|
|
310
|
-
|
|
319
|
+
const modifier = DEFAULT_PRECISION_SIX.has(base) && match[2] === "(6)" ? "" : match[2] ?? IMPLICIT_MODIFIERS[base] ?? "";
|
|
320
|
+
return `${base}${modifier}${match[3] ?? ""}`;
|
|
311
321
|
}
|
|
312
322
|
function parseUnsupportedPostgresType(params) {
|
|
313
323
|
const temporal = /^\s*(time(?:stamp)?)\s*(\(\s*\d+\s*\))?\s+(with|without)\s+time\s+zone\s*((?:\[\s*\]\s*)*)$/i.exec(params.raw);
|
|
@@ -320,7 +330,7 @@ function parseUnsupportedPostgresType(params) {
|
|
|
320
330
|
return NATIVE_ALIASES[base] === undefined ? undefined : parsePostgresTypeName({ raw: normalizeNativeType(params) });
|
|
321
331
|
}
|
|
322
332
|
function columnTypeKey(params) {
|
|
323
|
-
const { field } = params;
|
|
333
|
+
const { field, exact } = params;
|
|
324
334
|
const listSuffix = field.isList ? "[]" : "";
|
|
325
335
|
if (field.isUnsupported === true) {
|
|
326
336
|
const parsed = parseUnsupportedPostgresType({ raw: field.nativeType ?? "" });
|
|
@@ -331,7 +341,7 @@ function columnTypeKey(params) {
|
|
|
331
341
|
const scalar = namespace === undefined || namespace === "pg_catalog" ? scalarForBuiltinType({ name: parsed.name }) : undefined;
|
|
332
342
|
if (scalar !== undefined) {
|
|
333
343
|
const { isUnsupported: _unsupported, nativeTypeIdentity: _identity, ...rest } = field;
|
|
334
|
-
return columnTypeKey({ field: { ...rest, type: scalar, nativeType: `${parsed.name}${parsed.modifier ?? ""}`, isList: parsed.array || field.isList } });
|
|
344
|
+
return columnTypeKey({ field: { ...rest, type: scalar, nativeType: `${parsed.name}${parsed.modifier ?? ""}`, isList: parsed.array || field.isList }, ...exact === undefined ? {} : { exact } });
|
|
335
345
|
}
|
|
336
346
|
return `u:${JSON.stringify([namespace ?? null, parsed.name, parsed.modifier, parsed.array || field.isList])}`;
|
|
337
347
|
}
|
|
@@ -342,12 +352,37 @@ function columnTypeKey(params) {
|
|
|
342
352
|
if (field.nativeType !== undefined) {
|
|
343
353
|
const normalized = normalizeNativeType({ raw: field.nativeType });
|
|
344
354
|
const canonical = normalizeNativeType({ raw: SCALAR_TO_PG[field.type] });
|
|
345
|
-
const
|
|
355
|
+
const equivalents = exact === true ? EXACT_CANONICAL_EQUIVALENTS : CANONICAL_EQUIVALENTS;
|
|
356
|
+
const isCanonical = normalized === canonical || (equivalents[field.type]?.includes(normalized) ?? false);
|
|
346
357
|
if (!isCanonical)
|
|
347
358
|
return `n:${normalized}${listSuffix}`;
|
|
348
359
|
}
|
|
349
360
|
return `s:${field.type}${listSuffix}`;
|
|
350
361
|
}
|
|
362
|
+
function declaresExactColumnType(params) {
|
|
363
|
+
const { field } = params;
|
|
364
|
+
if (field.isUnsupported === true || field.nativeType === undefined || isEnumType2(field.type))
|
|
365
|
+
return false;
|
|
366
|
+
return CANONICAL_EQUIVALENTS[field.type]?.includes(normalizeNativeType({ raw: field.nativeType })) ?? false;
|
|
367
|
+
}
|
|
368
|
+
function timestamptzPrecision(params) {
|
|
369
|
+
const { field } = params;
|
|
370
|
+
if (field.type !== "DateTime" || field.isUnsupported === true)
|
|
371
|
+
return;
|
|
372
|
+
const normalized = field.nativeType === undefined ? "timestamptz" : normalizeNativeType({ raw: field.nativeType });
|
|
373
|
+
if (normalized === "timestamptz")
|
|
374
|
+
return 6;
|
|
375
|
+
const match = /^timestamptz\((\d)\)$/.exec(normalized);
|
|
376
|
+
return match === null ? undefined : Number(match[1]);
|
|
377
|
+
}
|
|
378
|
+
function isTimestamptzPrecisionWidening(params) {
|
|
379
|
+
const { from, to } = params;
|
|
380
|
+
if (from.isList !== to.isList)
|
|
381
|
+
return false;
|
|
382
|
+
const before = timestamptzPrecision({ field: from });
|
|
383
|
+
const after = timestamptzPrecision({ field: to });
|
|
384
|
+
return before !== undefined && after !== undefined && after >= before;
|
|
385
|
+
}
|
|
351
386
|
function isScalarToArrayChange(params) {
|
|
352
387
|
const { from, to } = params;
|
|
353
388
|
if (from.isList || !to.isList)
|
|
@@ -1016,6 +1051,32 @@ function parseMysqlEnumColumnType(params) {
|
|
|
1016
1051
|
}
|
|
1017
1052
|
|
|
1018
1053
|
// packages/migrate/src/normalize.ts
|
|
1054
|
+
function firstByName(params) {
|
|
1055
|
+
const result = new Map;
|
|
1056
|
+
for (const entry of params.entries)
|
|
1057
|
+
if (!result.has(entry.name))
|
|
1058
|
+
result.set(entry.name, entry);
|
|
1059
|
+
return result;
|
|
1060
|
+
}
|
|
1061
|
+
function normalizationLookup(params) {
|
|
1062
|
+
const enums = new Map;
|
|
1063
|
+
const scopedEnums = new Map;
|
|
1064
|
+
for (const entry of params.schema.enums) {
|
|
1065
|
+
const namespace = qualifiedSqlName3({ name: entry.name, namespace: entry.namespace }).namespace;
|
|
1066
|
+
for (const name of new Set([entry.name, entry.dbName ?? entry.name])) {
|
|
1067
|
+
if (!enums.has(name))
|
|
1068
|
+
enums.set(name, entry);
|
|
1069
|
+
const key = JSON.stringify([namespace, name]);
|
|
1070
|
+
if (!scopedEnums.has(key))
|
|
1071
|
+
scopedEnums.set(key, entry);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return { models: firstByName({ entries: params.schema.models }), fields: new Map(params.schema.models.map((model) => [model, firstByName({ entries: model.fields })])), enums, scopedEnums };
|
|
1075
|
+
}
|
|
1076
|
+
function lookupFieldName(params) {
|
|
1077
|
+
const field = params.lookup.fields.get(params.model)?.get(params.name);
|
|
1078
|
+
return field?.dbName ?? field?.name ?? params.name;
|
|
1079
|
+
}
|
|
1019
1080
|
function normalizeSchema(params) {
|
|
1020
1081
|
const { schema } = params;
|
|
1021
1082
|
const dialect = params.dialect ?? "postgres";
|
|
@@ -1028,6 +1089,7 @@ function normalizeSchema(params) {
|
|
|
1028
1089
|
continue;
|
|
1029
1090
|
enums.set(physicalNameKey({ name: enumDef.dbName ?? enumDef.name, namespace: enumDef.namespace, dialect }), enumDef);
|
|
1030
1091
|
}
|
|
1092
|
+
const lookup = normalizationLookup({ schema });
|
|
1031
1093
|
const tables = new Map;
|
|
1032
1094
|
const views = new Map;
|
|
1033
1095
|
for (const model of schema.models) {
|
|
@@ -1037,25 +1099,25 @@ function normalizeSchema(params) {
|
|
|
1037
1099
|
views.set(physicalNameKey({ name: model.dbName ?? model.name, namespace: model.namespace, dialect }), model);
|
|
1038
1100
|
continue;
|
|
1039
1101
|
}
|
|
1040
|
-
const table = normalizeModel({ model,
|
|
1102
|
+
const table = normalizeModel({ model, lookup, dialect, enums });
|
|
1041
1103
|
tables.set(physicalNameKey({ name: table.table, namespace: table.namespace, dialect }), table);
|
|
1042
1104
|
}
|
|
1043
1105
|
const joinTables = new Map;
|
|
1044
1106
|
for (const pair of detectImplicitM2MPairs({ schema })) {
|
|
1045
|
-
const join = normalizeJoinTable({ pair,
|
|
1107
|
+
const join = normalizeJoinTable({ pair, lookup, dialect });
|
|
1046
1108
|
joinTables.set(physicalNameKey({ name: join.table, namespace: join.namespace, dialect }), join);
|
|
1047
1109
|
}
|
|
1048
1110
|
return { tables, joinTables, enums, views };
|
|
1049
1111
|
}
|
|
1050
1112
|
function physicalEnumField(params) {
|
|
1051
|
-
const { field,
|
|
1113
|
+
const { field, lookup, dialect, table } = params;
|
|
1052
1114
|
if (!isEnumType5(field.type))
|
|
1053
1115
|
return field;
|
|
1054
1116
|
const type = field.type;
|
|
1055
1117
|
if (type.namespace !== undefined)
|
|
1056
1118
|
assertSqlNamespace({ namespace: type.namespace, dialect });
|
|
1057
1119
|
const name = type.enum;
|
|
1058
|
-
const enumDef =
|
|
1120
|
+
const enumDef = type.namespace === undefined ? lookup.enums.get(name) : lookup.scopedEnums.get(JSON.stringify([type.namespace, name]));
|
|
1059
1121
|
if (enumDef === undefined)
|
|
1060
1122
|
return field;
|
|
1061
1123
|
if (enumStorageOf({ enumDef }) !== "string") {
|
|
@@ -1094,16 +1156,20 @@ function assertStringEnumLabelsFit(params) {
|
|
|
1094
1156
|
}
|
|
1095
1157
|
}
|
|
1096
1158
|
function columnTypeKeyFor(params) {
|
|
1097
|
-
const { field, dialect, enums } = params;
|
|
1159
|
+
const { field, dialect, enums, exact } = params;
|
|
1098
1160
|
switch (dialect) {
|
|
1099
1161
|
case "postgres":
|
|
1100
|
-
return columnTypeKey({ field });
|
|
1162
|
+
return columnTypeKey({ field, ...exact === undefined ? {} : { exact } });
|
|
1101
1163
|
case "sqlite":
|
|
1102
1164
|
return sqliteColumnTypeKey({ field, enums });
|
|
1103
1165
|
case "mysql":
|
|
1104
1166
|
return mysqlColumnTypeKey({ field, enums });
|
|
1105
1167
|
}
|
|
1106
1168
|
}
|
|
1169
|
+
function columnTypeChanged(params) {
|
|
1170
|
+
const { declared, actual } = params;
|
|
1171
|
+
return declared.pinsExactType ? declared.exactTypeKey !== actual.exactTypeKey : declared.typeKey !== actual.typeKey;
|
|
1172
|
+
}
|
|
1107
1173
|
function defaultFingerprintFor(params) {
|
|
1108
1174
|
const { field, dialect } = params;
|
|
1109
1175
|
switch (dialect) {
|
|
@@ -1116,46 +1182,49 @@ function defaultFingerprintFor(params) {
|
|
|
1116
1182
|
}
|
|
1117
1183
|
}
|
|
1118
1184
|
function normalizeModel(params) {
|
|
1119
|
-
const { model,
|
|
1185
|
+
const { model, lookup, dialect, enums } = params;
|
|
1120
1186
|
const table = model.dbName ?? model.name;
|
|
1121
1187
|
const columns = new Map;
|
|
1122
1188
|
for (const logicalField of model.fields) {
|
|
1123
|
-
const field = physicalEnumField({ field: logicalField,
|
|
1189
|
+
const field = physicalEnumField({ field: logicalField, lookup, dialect, table });
|
|
1124
1190
|
const name = field.dbName ?? field.name;
|
|
1191
|
+
const typeKey = columnTypeKeyFor({ field, dialect, enums });
|
|
1125
1192
|
columns.set(name, {
|
|
1126
1193
|
name,
|
|
1127
1194
|
field,
|
|
1128
|
-
typeKey
|
|
1195
|
+
typeKey,
|
|
1196
|
+
exactTypeKey: dialect === "postgres" ? columnTypeKeyFor({ field, dialect, enums, exact: true }) : typeKey,
|
|
1197
|
+
pinsExactType: dialect === "postgres" && declaresExactColumnType({ field }),
|
|
1129
1198
|
nullable: field.isOptional,
|
|
1130
1199
|
defaultKey: defaultFingerprintFor({ field, dialect })
|
|
1131
1200
|
});
|
|
1132
1201
|
}
|
|
1133
|
-
const pk = normalizePk({ model });
|
|
1134
|
-
const indexes = normalizeIndexes({ model, table, dialect });
|
|
1135
|
-
const fks = normalizeFks({ model, table,
|
|
1202
|
+
const pk = normalizePk({ model, lookup });
|
|
1203
|
+
const indexes = normalizeIndexes({ model, table, dialect, lookup });
|
|
1204
|
+
const fks = normalizeFks({ model, table, lookup, dialect });
|
|
1136
1205
|
const checks = new Map;
|
|
1137
1206
|
for (const check of model.checks ?? [])
|
|
1138
1207
|
checks.set(check.name, check.expression);
|
|
1139
1208
|
return { model, table, ...model.namespace === undefined ? {} : { namespace: model.namespace }, columns, pk, indexes, fks, checks };
|
|
1140
1209
|
}
|
|
1141
1210
|
function normalizePk(params) {
|
|
1142
|
-
const { model } = params;
|
|
1211
|
+
const { model, lookup } = params;
|
|
1143
1212
|
if (model.primaryKey !== undefined && model.primaryKey.fields.length > 0) {
|
|
1144
|
-
return model.primaryKey.fields.map((name) =>
|
|
1213
|
+
return model.primaryKey.fields.map((name) => lookupFieldName({ lookup, model, name }));
|
|
1145
1214
|
}
|
|
1146
1215
|
return model.fields.filter((field) => field.isId).map((field) => field.dbName ?? field.name);
|
|
1147
1216
|
}
|
|
1148
1217
|
function normalizeIndexes(params) {
|
|
1149
|
-
const { model, table, dialect } = params;
|
|
1218
|
+
const { model, table, dialect, lookup } = params;
|
|
1150
1219
|
const result = new Map;
|
|
1151
1220
|
const add = (index) => {
|
|
1152
1221
|
const unique = index.kind === "unique";
|
|
1153
1222
|
const kind = unique ? "btree" : index.kind;
|
|
1154
1223
|
const columns = index.fields.map((field) => {
|
|
1155
|
-
const fieldIR =
|
|
1224
|
+
const fieldIR = lookup.fields.get(model)?.get(field.name);
|
|
1156
1225
|
const decimal = fieldIR !== undefined && fieldIR.type === "Decimal" && fieldIR.isList !== true;
|
|
1157
1226
|
return {
|
|
1158
|
-
name:
|
|
1227
|
+
name: fieldIR?.dbName ?? fieldIR?.name ?? field.name,
|
|
1159
1228
|
desc: field.sort === "desc",
|
|
1160
1229
|
...decimal ? { decimal: true } : {},
|
|
1161
1230
|
...field.operatorClass === undefined ? {} : { operatorClass: field.operatorClass }
|
|
@@ -1189,8 +1258,7 @@ function normalizeIndexes(params) {
|
|
|
1189
1258
|
}
|
|
1190
1259
|
function indexComparisonKey(params) {
|
|
1191
1260
|
const { unique, kind, columns, where, nullsNotDistinct } = params;
|
|
1192
|
-
|
|
1193
|
-
return `${unique ? "u" : "i"}${nullsNotDistinct === true ? "n" : ""}:${kind}:${cols}:${normalizeWhere({ where })}`;
|
|
1261
|
+
return JSON.stringify([unique, nullsNotDistinct === true, kind, columns.map((column) => [column.name, column.desc === true, column.operatorClass === undefined ? null : [column.operatorClass.namespace, column.operatorClass.name]]), normalizeWhere({ where })]);
|
|
1194
1262
|
}
|
|
1195
1263
|
function normalizeWhere(params) {
|
|
1196
1264
|
const { where } = params;
|
|
@@ -1352,22 +1420,22 @@ function parsePrimaryExpression(params) {
|
|
|
1352
1420
|
return { text: parts.join(" "), at, kind: "atom" };
|
|
1353
1421
|
}
|
|
1354
1422
|
function normalizeFks(params) {
|
|
1355
|
-
const { model, table,
|
|
1423
|
+
const { model, table, lookup } = params;
|
|
1356
1424
|
const result = new Map;
|
|
1357
1425
|
for (const relation of model.relations) {
|
|
1358
1426
|
if (relation.fields === undefined || relation.fields.length === 0)
|
|
1359
1427
|
continue;
|
|
1360
|
-
const target =
|
|
1428
|
+
const target = lookup.models.get(relation.targetModel);
|
|
1361
1429
|
if (target !== undefined && target.isView)
|
|
1362
1430
|
continue;
|
|
1363
1431
|
const refTable = target === undefined ? relation.targetModel : target.dbName ?? target.name;
|
|
1364
|
-
const columns = relation.fields.map((name) =>
|
|
1365
|
-
const refColumns = (relation.references ?? []).map((name) => target === undefined ? name :
|
|
1432
|
+
const columns = relation.fields.map((name) => lookupFieldName({ lookup, model, name }));
|
|
1433
|
+
const refColumns = (relation.references ?? []).map((name) => target === undefined ? name : lookupFieldName({ lookup, model: target, name }));
|
|
1366
1434
|
const required = relation.fields.every((name) => {
|
|
1367
|
-
const field =
|
|
1435
|
+
const field = lookup.fields.get(model)?.get(name);
|
|
1368
1436
|
return field === undefined ? true : !field.isOptional;
|
|
1369
1437
|
});
|
|
1370
|
-
const key =
|
|
1438
|
+
const key = JSON.stringify([columns, physicalNameKey({ name: refTable, namespace: target?.namespace, dialect: params.dialect }), refColumns]);
|
|
1371
1439
|
if (result.has(key))
|
|
1372
1440
|
continue;
|
|
1373
1441
|
result.set(key, {
|
|
@@ -1384,17 +1452,17 @@ function normalizeFks(params) {
|
|
|
1384
1452
|
return result;
|
|
1385
1453
|
}
|
|
1386
1454
|
function normalizeJoinTable(params) {
|
|
1387
|
-
const { pair,
|
|
1455
|
+
const { pair, lookup, dialect } = params;
|
|
1388
1456
|
return {
|
|
1389
1457
|
table: pair.tableName,
|
|
1390
1458
|
...pair.namespace === undefined ? {} : { namespace: pair.namespace },
|
|
1391
|
-
a: joinTableEnd({ modelName: pair.modelA,
|
|
1392
|
-
b: joinTableEnd({ modelName: pair.modelB,
|
|
1459
|
+
a: joinTableEnd({ modelName: pair.modelA, lookup, dialect, joinTable: pair.tableName }),
|
|
1460
|
+
b: joinTableEnd({ modelName: pair.modelB, lookup, dialect, joinTable: pair.tableName })
|
|
1393
1461
|
};
|
|
1394
1462
|
}
|
|
1395
1463
|
function joinTableEnd(params) {
|
|
1396
|
-
const { modelName,
|
|
1397
|
-
const model =
|
|
1464
|
+
const { modelName, lookup, joinTable } = params;
|
|
1465
|
+
const model = lookup.models.get(modelName);
|
|
1398
1466
|
if (model === undefined) {
|
|
1399
1467
|
throw new VibeError7({
|
|
1400
1468
|
code: "VIBE_MIGRATION",
|
|
@@ -1402,7 +1470,7 @@ function joinTableEnd(params) {
|
|
|
1402
1470
|
meta: { joinTable, model: modelName }
|
|
1403
1471
|
});
|
|
1404
1472
|
}
|
|
1405
|
-
const pkFields = model.primaryKey !== undefined && model.primaryKey.fields.length > 0 ? model.primaryKey.fields.map((name) =>
|
|
1473
|
+
const pkFields = model.primaryKey !== undefined && model.primaryKey.fields.length > 0 ? model.primaryKey.fields.map((name) => lookup.fields.get(model)?.get(name)) : model.fields.filter((field) => field.isId);
|
|
1406
1474
|
const pkField = pkFields[0];
|
|
1407
1475
|
if (pkFields.length !== 1 || pkField === undefined) {
|
|
1408
1476
|
throw new VibeError7({
|
|
@@ -1415,7 +1483,7 @@ function joinTableEnd(params) {
|
|
|
1415
1483
|
table: model.dbName ?? model.name,
|
|
1416
1484
|
...model.namespace === undefined ? {} : { namespace: model.namespace },
|
|
1417
1485
|
column: pkField.dbName ?? pkField.name,
|
|
1418
|
-
field: physicalEnumField({ field: pkField,
|
|
1486
|
+
field: physicalEnumField({ field: pkField, lookup, dialect: params.dialect, table: model.dbName ?? model.name })
|
|
1419
1487
|
};
|
|
1420
1488
|
}
|
|
1421
1489
|
function physicalNameKey(params) {
|
|
@@ -1431,10 +1499,19 @@ function schemaNamespaces(params) {
|
|
|
1431
1499
|
...(params.schema.migrationObjects ?? []).flatMap((object) => object.identity.kind === "namespace" ? [object.identity.name] : [])
|
|
1432
1500
|
])];
|
|
1433
1501
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1502
|
+
|
|
1503
|
+
// packages/migrate/src/introspect/group.ts
|
|
1504
|
+
function groupCatalogRows(params) {
|
|
1505
|
+
const groups = new Map;
|
|
1506
|
+
for (const row of params.rows) {
|
|
1507
|
+
const key = params.key(row);
|
|
1508
|
+
const group = groups.get(key);
|
|
1509
|
+
if (group === undefined)
|
|
1510
|
+
groups.set(key, [row]);
|
|
1511
|
+
else
|
|
1512
|
+
group.push(row);
|
|
1513
|
+
}
|
|
1514
|
+
return groups;
|
|
1438
1515
|
}
|
|
1439
1516
|
|
|
1440
1517
|
// packages/migrate/src/postgres-type-identity.ts
|
|
@@ -1838,10 +1915,15 @@ async function introspectPhysicalPostgres(params) {
|
|
|
1838
1915
|
read("checks"),
|
|
1839
1916
|
Promise.all(namespaces.map(async (namespace) => (await executor({ text: renderExclusionCatalog2({ dialect: postgresDialect6, namespace }) })).map((row) => ({ ...row, table_name: physicalNameKey({ namespace, name: String(row.table_name) }) })))).then((rows) => rows.flat())
|
|
1840
1917
|
]);
|
|
1841
|
-
const keysQuery = renderPostgresIndexKeys({ dialect: postgresDialect6 });
|
|
1918
|
+
const keysQuery = renderPostgresIndexKeys({ dialect: postgresDialect6, namespaces });
|
|
1842
1919
|
const indexKeys = await executor({ text: keysQuery.text, values: [...keysQuery.values] });
|
|
1843
1920
|
if (indexKeys.some((row) => typeof row.table_namespace !== "string" || typeof row.table_name !== "string" || typeof row.index_name !== "string" || typeof row.method !== "string" || typeof row.unique !== "boolean" || typeof row.descending !== "boolean" || typeof row.exact_default !== "boolean" && row.column_name !== null || typeof row.operator_namespace !== "string" || typeof row.operator_name !== "string" || !Number.isSafeInteger(Number(row.ordinal)) || Number(row.ordinal) < 1))
|
|
1844
1921
|
throw new VibeError11({ code: "VIBE_MIGRATION", message: "PostgreSQL index-key identity is incomplete", meta: { reason: "index-inspection-unavailable" } });
|
|
1922
|
+
const keysByIndex = groupCatalogRows({ rows: indexKeys, key: (row) => JSON.stringify([physicalNameKey({ namespace: String(row.table_namespace), name: String(row.table_name) }), row.index_name]) });
|
|
1923
|
+
const catalogColumns = new Map(columnRows.map((row) => [JSON.stringify([row.table_name, row.column_name]), row]));
|
|
1924
|
+
const foreignKeysByTable = groupCatalogRows({ rows: fkRows, key: (row) => String(row.table_name) });
|
|
1925
|
+
const catalogIndexesByTable = groupCatalogRows({ rows: indexRows, key: (row) => String(row.table_name) });
|
|
1926
|
+
const exclusionsByTable = groupCatalogRows({ rows: exclusionRows, key: (row) => String(row.table_name) });
|
|
1845
1927
|
const tables = tableRows.map((row) => row.table_name);
|
|
1846
1928
|
const views = viewRows.map((row) => row.table_name);
|
|
1847
1929
|
const columns = columnRows.map((row) => ({
|
|
@@ -1859,6 +1941,8 @@ async function introspectPhysicalPostgres(params) {
|
|
|
1859
1941
|
numericScale: toNumberOrNull(row.numeric_scale),
|
|
1860
1942
|
fullType: row.full_type ?? null
|
|
1861
1943
|
}));
|
|
1944
|
+
const columnsByTable = groupCatalogRows({ rows: columns, key: (column) => column.table });
|
|
1945
|
+
const columnsByIdentity = new Map(columns.map((column) => [JSON.stringify([column.table, column.name]), column]));
|
|
1862
1946
|
const enums = [];
|
|
1863
1947
|
{
|
|
1864
1948
|
const valuesByName = new Map;
|
|
@@ -1899,6 +1983,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
1899
1983
|
entry.columns.push(columnName);
|
|
1900
1984
|
}
|
|
1901
1985
|
}
|
|
1986
|
+
const uniquesByTable = groupCatalogRows({ rows: uniquesByConstraint.values(), key: (unique) => unique.table });
|
|
1902
1987
|
const checksByTable = new Map;
|
|
1903
1988
|
for (const row of checkRows) {
|
|
1904
1989
|
const table = row.table_name;
|
|
@@ -1933,7 +2018,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
1933
2018
|
const fkGroups = [...fkGroupsByName.values()].map((group) => ({
|
|
1934
2019
|
...group,
|
|
1935
2020
|
required: group.columns.every((columnName) => {
|
|
1936
|
-
const column =
|
|
2021
|
+
const column = columnsByIdentity.get(JSON.stringify([group.table, columnName]));
|
|
1937
2022
|
return column === undefined ? true : !column.nullable;
|
|
1938
2023
|
})
|
|
1939
2024
|
}));
|
|
@@ -1941,7 +2026,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
1941
2026
|
for (const row of indexRows) {
|
|
1942
2027
|
if (row.backing_constraint_type === "x")
|
|
1943
2028
|
continue;
|
|
1944
|
-
const operands =
|
|
2029
|
+
const operands = keysByIndex.get(JSON.stringify([row.table_name, row.index_name])) ?? [];
|
|
1945
2030
|
if (row.plain_columns === true && operands.length !== Number(row.key_count))
|
|
1946
2031
|
throw new VibeError11({ code: "VIBE_MIGRATION", message: "Plain index keys could not be read completely; refusing name-only operator-class introspection", meta: { reason: "index-inspection-unavailable", table: row.table_name, index: row.index_name } });
|
|
1947
2032
|
if (operands.length > 0) {
|
|
@@ -1966,22 +2051,24 @@ async function introspectPhysicalPostgres(params) {
|
|
|
1966
2051
|
indexes.push(parsed);
|
|
1967
2052
|
}
|
|
1968
2053
|
}
|
|
2054
|
+
const indexesByTable = groupCatalogRows({ rows: indexes, key: (index) => index.table });
|
|
2055
|
+
const fkGroupsByTable = groupCatalogRows({ rows: fkGroups, key: (group) => group.table });
|
|
1969
2056
|
const joinTables = new Map;
|
|
1970
2057
|
for (const table of tables) {
|
|
1971
2058
|
const physicalTable = physicalNames.get(table);
|
|
1972
2059
|
if (!physicalTable.name.startsWith("_"))
|
|
1973
2060
|
continue;
|
|
1974
|
-
const tableFks =
|
|
1975
|
-
const tableIndexes =
|
|
1976
|
-
if (pkByTable.has(table) || (checksByTable.get(table)?.length ?? 0) > 0 ||
|
|
2061
|
+
const tableFks = foreignKeysByTable.get(table) ?? [];
|
|
2062
|
+
const tableIndexes = catalogIndexesByTable.get(table) ?? [];
|
|
2063
|
+
if (pkByTable.has(table) || (checksByTable.get(table)?.length ?? 0) > 0 || exclusionsByTable.has(table))
|
|
1977
2064
|
continue;
|
|
1978
2065
|
if (!tableFks.every((row) => row.validated === true && row.deferrable === false && row.triggers_enabled === true))
|
|
1979
2066
|
continue;
|
|
1980
2067
|
if (tableIndexes.length !== 2 || !tableIndexes.every((row) => row.valid === true && row.ready === true && row.immediate === true && row.plain_columns === true && typeof row.key_count === "number" && row.key_count === row.attribute_count))
|
|
1981
2068
|
continue;
|
|
1982
2069
|
if (tableFks.some((row) => {
|
|
1983
|
-
const source =
|
|
1984
|
-
const target =
|
|
2070
|
+
const source = catalogColumns.get(JSON.stringify([table, row.column_name]));
|
|
2071
|
+
const target = catalogColumns.get(JSON.stringify([row.foreign_table_name, row.foreign_column_name]));
|
|
1985
2072
|
return source === undefined || target === undefined || typeof source.full_type !== "string" || source.full_type.length === 0 || source.full_type !== target.full_type || source.column_default !== null || source.is_identity !== "NO" || source.is_generated !== "NEVER" || source.is_nullable !== "NO" || source.collation_name === undefined || source.collation_name !== target.collation_name;
|
|
1986
2073
|
}))
|
|
1987
2074
|
continue;
|
|
@@ -1989,9 +2076,9 @@ async function introspectPhysicalPostgres(params) {
|
|
|
1989
2076
|
continue;
|
|
1990
2077
|
const detected = detectJoinTable({
|
|
1991
2078
|
table: physicalTable.name,
|
|
1992
|
-
columnNames:
|
|
1993
|
-
fkGroups:
|
|
1994
|
-
indexes:
|
|
2079
|
+
columnNames: (columnsByTable.get(table) ?? []).map((column) => column.name),
|
|
2080
|
+
fkGroups: (fkGroupsByTable.get(table) ?? []).map((group) => ({ ...group, table: physicalTable.name, refTable: physicalNames.get(group.refTable)?.name ?? group.refTable })),
|
|
2081
|
+
indexes: (indexesByTable.get(table) ?? []).map((index) => ({ ...index, table: physicalTable.name }))
|
|
1995
2082
|
});
|
|
1996
2083
|
if (detected !== null)
|
|
1997
2084
|
joinTables.set(table, { ...detected, ...physicalTable.namespace === "public" && namespaces.length === 1 ? {} : { relationName: detected.relationName ?? physicalTable.name.slice(1) }, tableA: physicalNameKey({ namespace: physicalTable.namespace, name: detected.tableA }), tableB: physicalNameKey({ namespace: physicalTable.namespace, name: detected.tableB }) });
|
|
@@ -2004,14 +2091,14 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2004
2091
|
table,
|
|
2005
2092
|
physicalTable: physicalNames.get(table).name,
|
|
2006
2093
|
isView: false,
|
|
2007
|
-
columns,
|
|
2094
|
+
columns: columnsByTable.get(table) ?? [],
|
|
2008
2095
|
pkByTable,
|
|
2009
2096
|
pkNames,
|
|
2010
|
-
|
|
2097
|
+
uniques: uniquesByTable.get(table) ?? [],
|
|
2011
2098
|
constraintNames,
|
|
2012
|
-
indexes,
|
|
2099
|
+
indexes: indexesByTable.get(table) ?? [],
|
|
2013
2100
|
enumNames,
|
|
2014
|
-
exclusions:
|
|
2101
|
+
exclusions: (exclusionsByTable.get(table) ?? []).map((row) => ({ name: String(row.constraint_name), definition: String(row.definition), requiredExtensions: [], catalogDefinition: true })),
|
|
2015
2102
|
checks: checksByTable.get(table) ?? []
|
|
2016
2103
|
}));
|
|
2017
2104
|
}
|
|
@@ -2020,10 +2107,10 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2020
2107
|
table: view,
|
|
2021
2108
|
physicalTable: physicalNames.get(view).name,
|
|
2022
2109
|
isView: true,
|
|
2023
|
-
columns,
|
|
2110
|
+
columns: columnsByTable.get(view) ?? [],
|
|
2024
2111
|
pkByTable,
|
|
2025
2112
|
pkNames,
|
|
2026
|
-
|
|
2113
|
+
uniques: uniquesByTable.get(view) ?? [],
|
|
2027
2114
|
constraintNames,
|
|
2028
2115
|
indexes: [],
|
|
2029
2116
|
enumNames,
|
|
@@ -2043,14 +2130,16 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2043
2130
|
logicalNames.add(name);
|
|
2044
2131
|
return name;
|
|
2045
2132
|
};
|
|
2133
|
+
const modelBasenames = groupCatalogRows({ rows: physicalNames.values(), key: (entry) => entry.name });
|
|
2134
|
+
const enumBasenames = groupCatalogRows({ rows: enumIdentities.values(), key: (entry) => entry.name });
|
|
2046
2135
|
const modelNames = new Map(models.map((model) => {
|
|
2047
2136
|
const identity = physicalNames.get(model.name);
|
|
2048
|
-
const duplicates =
|
|
2137
|
+
const duplicates = (modelBasenames.get(identity.name)?.length ?? 0) > 1;
|
|
2049
2138
|
return [model.name, logicalName({ key: model.name, name: identity.name, duplicates })];
|
|
2050
2139
|
}));
|
|
2051
2140
|
const enumLogicalNames = new Map(enums.map((entry) => {
|
|
2052
2141
|
const identity = enumIdentities.get(entry.name);
|
|
2053
|
-
const duplicates =
|
|
2142
|
+
const duplicates = (enumBasenames.get(identity.name)?.length ?? 0) > 1;
|
|
2054
2143
|
return [entry.name, logicalName({ key: entry.name, name: identity.name, duplicates })];
|
|
2055
2144
|
}));
|
|
2056
2145
|
const qualifiedModels = models.map((model) => {
|
|
@@ -2088,20 +2177,18 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2088
2177
|
return resolved.schema;
|
|
2089
2178
|
}
|
|
2090
2179
|
function buildModel(params) {
|
|
2091
|
-
const { table, isView, columns, pkByTable,
|
|
2092
|
-
const tableColumns = columns
|
|
2180
|
+
const { table, isView, columns, pkByTable, uniques, constraintNames, indexes, enumNames } = params;
|
|
2181
|
+
const tableColumns = columns;
|
|
2093
2182
|
const pk = pkByTable.get(table) ?? [];
|
|
2094
2183
|
const uniqueSingles = new Set;
|
|
2095
2184
|
const multiUniques = [];
|
|
2096
|
-
for (const unique of
|
|
2185
|
+
for (const unique of uniques) {
|
|
2097
2186
|
const name = unique.name;
|
|
2098
|
-
if (unique.table !== table)
|
|
2099
|
-
continue;
|
|
2100
2187
|
if (unique.columns.length === 1)
|
|
2101
2188
|
uniqueSingles.add(unique.columns[0]);
|
|
2102
2189
|
multiUniques.push({ name, columns: unique.columns });
|
|
2103
2190
|
}
|
|
2104
|
-
const tableIndexes = indexes.filter((index) =>
|
|
2191
|
+
const tableIndexes = indexes.filter((index) => !constraintNames.has(JSON.stringify([table, index.name])));
|
|
2105
2192
|
for (const index of tableIndexes) {
|
|
2106
2193
|
if (!index.unique)
|
|
2107
2194
|
continue;
|
|
@@ -2199,8 +2286,9 @@ function reverseMapType(params) {
|
|
|
2199
2286
|
function nonCanonicalModifier(params) {
|
|
2200
2287
|
const { column, udt, scalar } = params;
|
|
2201
2288
|
if (column.dataType === "ARRAY" && column.fullType !== null) {
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2289
|
+
const temporal = parseUnsupportedPostgresType({ raw: column.fullType });
|
|
2290
|
+
const element = temporal?.name === "timestamptz" ? `timestamptz${temporal.modifier}` : normalizeNativeType({ raw: column.fullType }).replace(/\[\]$/, "");
|
|
2291
|
+
if (element !== "timestamptz(6)" && /^(numeric|timestamptz)\([\d,]+\)$/.test(element))
|
|
2204
2292
|
return element;
|
|
2205
2293
|
}
|
|
2206
2294
|
if (scalar === "DateTime" && udt === "timestamptz") {
|
|
@@ -2217,9 +2305,9 @@ function nonCanonicalModifier(params) {
|
|
|
2217
2305
|
}
|
|
2218
2306
|
function withModifier(params) {
|
|
2219
2307
|
const { column, udt } = params;
|
|
2220
|
-
if (udt === "varchar" && column.dataType === "ARRAY" && column.fullType !== null) {
|
|
2308
|
+
if ((udt === "varchar" || udt === "bpchar" || udt === "bit" || udt === "varbit") && column.dataType === "ARRAY" && column.fullType !== null) {
|
|
2221
2309
|
const elementType = normalizeNativeType({ raw: column.fullType }).replace(/\[\]$/, "");
|
|
2222
|
-
if (/^varchar\(\d+\)$/.test(elementType))
|
|
2310
|
+
if (/^(varchar|char|bit|varbit)\(\d+\)$/.test(elementType))
|
|
2223
2311
|
return elementType;
|
|
2224
2312
|
}
|
|
2225
2313
|
if ((udt === "varchar" || udt === "bpchar") && column.charMaxLength !== null) {
|
|
@@ -2229,6 +2317,10 @@ function withModifier(params) {
|
|
|
2229
2317
|
return "char";
|
|
2230
2318
|
if ((udt === "bit" || udt === "varbit") && column.charMaxLength !== null)
|
|
2231
2319
|
return `${udt}(${column.charMaxLength})`;
|
|
2320
|
+
if ((udt === "timestamp" || udt === "time" || udt === "timetz") && column.dataType === "ARRAY" && column.fullType !== null) {
|
|
2321
|
+
const element = parseUnsupportedPostgresType({ raw: column.fullType });
|
|
2322
|
+
return element === undefined || element.modifier === "" || element.modifier === "(6)" ? udt : `${udt}${element.modifier}`;
|
|
2323
|
+
}
|
|
2232
2324
|
if ((udt === "timestamp" || udt === "time" || udt === "timetz") && column.datetimePrecision !== null && column.datetimePrecision !== 6) {
|
|
2233
2325
|
return `${udt}(${column.datetimePrecision})`;
|
|
2234
2326
|
}
|
|
@@ -2381,7 +2473,7 @@ function toNumberOrNull(value) {
|
|
|
2381
2473
|
}
|
|
2382
2474
|
|
|
2383
2475
|
// packages/migrate/src/introspect/mysql.ts
|
|
2384
|
-
import { exactIntegerLiteral as exactIntegerLiteral2 } from "@vibeorm/schema";
|
|
2476
|
+
import { VibeError as VibeError12, exactIntegerLiteral as exactIntegerLiteral2 } from "@vibeorm/schema";
|
|
2385
2477
|
import { mysqlDialect as mysqlDialect2 } from "@vibeorm/sql";
|
|
2386
2478
|
|
|
2387
2479
|
// packages/migrate/src/journal-table.ts
|
|
@@ -2510,6 +2602,12 @@ async function introspectMysql(params) {
|
|
|
2510
2602
|
collation: catalogField({ row, name: "collation_name" }) ?? null,
|
|
2511
2603
|
extra: (catalogField({ row, name: "extra" }) ?? "").toLowerCase()
|
|
2512
2604
|
}));
|
|
2605
|
+
const columnsByTable = groupCatalogRows({ rows: columns, key: (column) => column.table });
|
|
2606
|
+
const columnsByIdentity = new Map(columns.map((column) => [JSON.stringify([column.table, column.name]), column]));
|
|
2607
|
+
const catalogColumns = new Map(columnRows.map((row) => [JSON.stringify([catalogField({ row, name: "table_name" }), catalogField({ row, name: "column_name" })]), row]));
|
|
2608
|
+
const statisticsByTable = groupCatalogRows({ rows: statisticRows, key: (row) => String(catalogField({ row, name: "table_name" })) });
|
|
2609
|
+
const foreignKeysByTable = groupCatalogRows({ rows: fkRows, key: (row) => String(catalogField({ row, name: "table_name" })) });
|
|
2610
|
+
const enginesByTable = new Map(tableRows.map((row) => [catalogField({ row, name: "table_name" }), catalogField({ row, name: "engine" })]));
|
|
2513
2611
|
const rulesByConstraint = new Map;
|
|
2514
2612
|
for (const row of refRows) {
|
|
2515
2613
|
rulesByConstraint.set(catalogField({ row, name: "constraint_name" }), {
|
|
@@ -2521,6 +2619,15 @@ async function introspectMysql(params) {
|
|
|
2521
2619
|
for (const row of fkRows) {
|
|
2522
2620
|
const constraintName = catalogField({ row, name: "constraint_name" });
|
|
2523
2621
|
const table = catalogField({ row, name: "table_name" });
|
|
2622
|
+
const tableSchema = catalogField({ row, name: "table_schema" });
|
|
2623
|
+
const referencedSchema = catalogField({ row, name: "referenced_table_schema" });
|
|
2624
|
+
if (typeof tableSchema === "string" && typeof referencedSchema === "string" && tableSchema !== referencedSchema) {
|
|
2625
|
+
throw new VibeError12({
|
|
2626
|
+
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
2627
|
+
message: `MySQL cross-database foreign key "${constraintName}" from "${tableSchema}.${table}" to "${referencedSchema}" cannot be introspected without losing its database identity`,
|
|
2628
|
+
meta: { dialect: "mysql", feature: "cross-database foreign keys", constraint: constraintName, table, tableSchema, referencedSchema }
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2524
2631
|
const key = `${table}\x00${constraintName}`;
|
|
2525
2632
|
const existing = groupsByKey.get(key);
|
|
2526
2633
|
if (existing === undefined) {
|
|
@@ -2543,17 +2650,11 @@ async function introspectMysql(params) {
|
|
|
2543
2650
|
onDelete: rules?.onDelete ?? "NoAction",
|
|
2544
2651
|
onUpdate: rules?.onUpdate ?? "NoAction",
|
|
2545
2652
|
required: group.columns.every((columnName) => {
|
|
2546
|
-
const column =
|
|
2653
|
+
const column = columnsByIdentity.get(JSON.stringify([group.table, columnName]));
|
|
2547
2654
|
return column === undefined ? true : !column.nullable;
|
|
2548
2655
|
})
|
|
2549
2656
|
};
|
|
2550
2657
|
});
|
|
2551
|
-
const fkNamesByTable = new Map;
|
|
2552
|
-
for (const group of fkGroups) {
|
|
2553
|
-
const set = fkNamesByTable.get(group.table) ?? new Set;
|
|
2554
|
-
set.add(group.constraintName);
|
|
2555
|
-
fkNamesByTable.set(group.table, set);
|
|
2556
|
-
}
|
|
2557
2658
|
const pkByTable = new Map;
|
|
2558
2659
|
const indexes = [];
|
|
2559
2660
|
{
|
|
@@ -2562,6 +2663,12 @@ async function introspectMysql(params) {
|
|
|
2562
2663
|
const table = catalogField({ row, name: "table_name" });
|
|
2563
2664
|
const name = catalogField({ row, name: "index_name" });
|
|
2564
2665
|
const columnName = catalogField({ row, name: "column_name" });
|
|
2666
|
+
const type = catalogField({ row, name: "index_type" });
|
|
2667
|
+
const unique = Number(catalogField({ row, name: "non_unique" })) === 0;
|
|
2668
|
+
const kind = typeof type === "string" ? type.toLowerCase() : "btree";
|
|
2669
|
+
if (catalogField({ row, name: "sub_part" }) != null || catalogField({ row, name: "expression" }) != null || catalogField({ row, name: "is_visible" }) === "NO" || typeof columnName !== "string" || kind !== "btree" && (kind !== "fulltext" || unique)) {
|
|
2670
|
+
throw new VibeError12({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `MySQL index "${table}.${name}" cannot be introspected without losing its physical shape`, meta: { dialect: "mysql", feature: "index introspection", table, index: name } });
|
|
2671
|
+
}
|
|
2565
2672
|
if (name === "PRIMARY") {
|
|
2566
2673
|
const list = pkByTable.get(table);
|
|
2567
2674
|
if (list === undefined)
|
|
@@ -2570,8 +2677,6 @@ async function introspectMysql(params) {
|
|
|
2570
2677
|
list.push(columnName);
|
|
2571
2678
|
continue;
|
|
2572
2679
|
}
|
|
2573
|
-
if (fkNamesByTable.get(table)?.has(name) === true)
|
|
2574
|
-
continue;
|
|
2575
2680
|
const key = `${table}\x00${name}`;
|
|
2576
2681
|
const entry = byKey.get(key);
|
|
2577
2682
|
const column = {
|
|
@@ -2582,7 +2687,8 @@ async function introspectMysql(params) {
|
|
|
2582
2687
|
byKey.set(key, {
|
|
2583
2688
|
table,
|
|
2584
2689
|
name,
|
|
2585
|
-
unique
|
|
2690
|
+
unique,
|
|
2691
|
+
kind,
|
|
2586
2692
|
columns: [column]
|
|
2587
2693
|
});
|
|
2588
2694
|
} else {
|
|
@@ -2590,34 +2696,39 @@ async function introspectMysql(params) {
|
|
|
2590
2696
|
}
|
|
2591
2697
|
}
|
|
2592
2698
|
for (const entry of byKey.values()) {
|
|
2593
|
-
|
|
2699
|
+
const support = groupsByKey.get(`${entry.table}\x00${entry.name}`);
|
|
2700
|
+
if (!entry.unique && entry.kind === "btree" && support !== undefined && entry.columns.length === support.columns.length && entry.columns.every((column, index) => column.name === support.columns[index] && !column.desc))
|
|
2701
|
+
continue;
|
|
2702
|
+
indexes.push(entry);
|
|
2594
2703
|
}
|
|
2595
2704
|
}
|
|
2705
|
+
const indexesByTable = groupCatalogRows({ rows: indexes, key: (index) => index.table });
|
|
2706
|
+
const fkGroupsByTable = groupCatalogRows({ rows: fkGroups, key: (group) => group.table });
|
|
2596
2707
|
const enums = [];
|
|
2597
2708
|
const joinTables = new Map;
|
|
2598
2709
|
let foreignKeysEnabled;
|
|
2599
2710
|
for (const table of tables) {
|
|
2600
2711
|
if (!table.startsWith("_") || pkByTable.has(table) || (checksByTable.get(table)?.length ?? 0) > 0)
|
|
2601
2712
|
continue;
|
|
2602
|
-
const tableIndexes =
|
|
2713
|
+
const tableIndexes = statisticsByTable.get(table) ?? [];
|
|
2603
2714
|
if (new Set(tableIndexes.map((row) => catalogField({ row, name: "index_name" }))).size !== 2 || !tableIndexes.every((row) => catalogField({ row, name: "index_type" }) === "BTREE" && catalogField({ row, name: "is_visible" }) === "YES" && catalogField({ row, name: "sub_part" }) === null && catalogField({ row, name: "expression" }) === null && catalogField({ row, name: "collation" }) === "A" && Number.isInteger(Number(catalogField({ row, name: "seq_in_index" }))) && Number(catalogField({ row, name: "seq_in_index" })) > 0))
|
|
2604
2715
|
continue;
|
|
2605
|
-
const tableFks =
|
|
2716
|
+
const tableFks = foreignKeysByTable.get(table) ?? [];
|
|
2606
2717
|
if (tableFks.some((row) => {
|
|
2607
2718
|
const targetTable = catalogField({ row, name: "referenced_table_name" });
|
|
2608
|
-
const source =
|
|
2609
|
-
const target =
|
|
2719
|
+
const source = catalogColumns.get(JSON.stringify([table, catalogField({ row, name: "column_name" })]));
|
|
2720
|
+
const target = catalogColumns.get(JSON.stringify([targetTable, catalogField({ row, name: "referenced_column_name" })]));
|
|
2610
2721
|
if (source === undefined || target === undefined)
|
|
2611
2722
|
return true;
|
|
2612
2723
|
const type = catalogField({ row: source, name: "column_type" });
|
|
2613
|
-
return typeof type !== "string" || type.length === 0 || type !== catalogField({ row: target, name: "column_type" }) || catalogField({ row: source, name: "column_default" }) !== null || catalogField({ row: source, name: "extra" }) !== "" || catalogField({ row: source, name: "generation_expression" }) !== "" || catalogField({ row: source, name: "is_nullable" }) !== "NO" || catalogField({ row: source, name: "collation_name" }) === undefined || catalogField({ row: source, name: "collation_name" }) !== catalogField({ row: target, name: "collation_name" }) || catalogField({ row, name: "table_schema" }) === undefined || catalogField({ row, name: "table_schema" }) !== catalogField({ row, name: "referenced_table_schema" }) || ![table, targetTable].every((name) =>
|
|
2724
|
+
return typeof type !== "string" || type.length === 0 || type !== catalogField({ row: target, name: "column_type" }) || catalogField({ row: source, name: "column_default" }) !== null || catalogField({ row: source, name: "extra" }) !== "" || catalogField({ row: source, name: "generation_expression" }) !== "" || catalogField({ row: source, name: "is_nullable" }) !== "NO" || catalogField({ row: source, name: "collation_name" }) === undefined || catalogField({ row: source, name: "collation_name" }) !== catalogField({ row: target, name: "collation_name" }) || catalogField({ row, name: "table_schema" }) === undefined || catalogField({ row, name: "table_schema" }) !== catalogField({ row, name: "referenced_table_schema" }) || ![table, targetTable].every((name) => enginesByTable.get(name) === "InnoDB");
|
|
2614
2725
|
}))
|
|
2615
2726
|
continue;
|
|
2616
2727
|
const detected = detectJoinTable({
|
|
2617
2728
|
table,
|
|
2618
|
-
columnNames:
|
|
2619
|
-
fkGroups,
|
|
2620
|
-
indexes
|
|
2729
|
+
columnNames: (columnsByTable.get(table) ?? []).map((column) => column.name),
|
|
2730
|
+
fkGroups: fkGroupsByTable.get(table) ?? [],
|
|
2731
|
+
indexes: indexesByTable.get(table) ?? []
|
|
2621
2732
|
});
|
|
2622
2733
|
if (detected === null)
|
|
2623
2734
|
continue;
|
|
@@ -2629,7 +2740,7 @@ async function introspectMysql(params) {
|
|
|
2629
2740
|
continue;
|
|
2630
2741
|
const q = (name) => mysqlDialect2.quoteIdent({ name });
|
|
2631
2742
|
let valid = true;
|
|
2632
|
-
for (const fk of
|
|
2743
|
+
for (const fk of fkGroupsByTable.get(table) ?? []) {
|
|
2633
2744
|
const rows = await executor({ text: `SELECT 1 AS invalid FROM ${q(table)} child LEFT JOIN ${q(fk.refTable)} parent ON child.${q(fk.columns[0])} = parent.${q(fk.refColumns[0])} WHERE parent.${q(fk.refColumns[0])} IS NULL LIMIT 1`, values: [] });
|
|
2634
2745
|
if (rows.length > 0) {
|
|
2635
2746
|
valid = false;
|
|
@@ -2646,9 +2757,9 @@ async function introspectMysql(params) {
|
|
|
2646
2757
|
models.push(buildModel2({
|
|
2647
2758
|
table,
|
|
2648
2759
|
isView: false,
|
|
2649
|
-
columns,
|
|
2760
|
+
columns: columnsByTable.get(table) ?? [],
|
|
2650
2761
|
pkByTable,
|
|
2651
|
-
indexes,
|
|
2762
|
+
indexes: indexesByTable.get(table) ?? [],
|
|
2652
2763
|
enums,
|
|
2653
2764
|
checks: checksByTable.get(table) ?? []
|
|
2654
2765
|
}));
|
|
@@ -2657,7 +2768,7 @@ async function introspectMysql(params) {
|
|
|
2657
2768
|
models.push(buildModel2({
|
|
2658
2769
|
table: view,
|
|
2659
2770
|
isView: true,
|
|
2660
|
-
columns,
|
|
2771
|
+
columns: columnsByTable.get(view) ?? [],
|
|
2661
2772
|
pkByTable: new Map,
|
|
2662
2773
|
indexes: [],
|
|
2663
2774
|
enums,
|
|
@@ -2675,9 +2786,9 @@ async function introspectMysql(params) {
|
|
|
2675
2786
|
}
|
|
2676
2787
|
function buildModel2(params) {
|
|
2677
2788
|
const { table, isView, columns, pkByTable, indexes, enums } = params;
|
|
2678
|
-
const tableColumns = columns
|
|
2789
|
+
const tableColumns = columns;
|
|
2679
2790
|
const pk = pkByTable.get(table) ?? [];
|
|
2680
|
-
const tableIndexes = indexes
|
|
2791
|
+
const tableIndexes = indexes;
|
|
2681
2792
|
const uniqueSingles = new Set;
|
|
2682
2793
|
for (const index of tableIndexes) {
|
|
2683
2794
|
if (index.unique && index.columns.length === 1) {
|
|
@@ -2708,7 +2819,7 @@ function buildModel2(params) {
|
|
|
2708
2819
|
continue;
|
|
2709
2820
|
modelIndexes.push({
|
|
2710
2821
|
name: index.name,
|
|
2711
|
-
kind: index.unique ? "unique" :
|
|
2822
|
+
kind: index.unique ? "unique" : index.kind,
|
|
2712
2823
|
fields: index.columns.map((column) => ({
|
|
2713
2824
|
name: column.name,
|
|
2714
2825
|
...column.desc ? { sort: "desc" } : {}
|
|
@@ -3240,7 +3351,7 @@ function q(name) {
|
|
|
3240
3351
|
}
|
|
3241
3352
|
|
|
3242
3353
|
// packages/migrate/src/execute.ts
|
|
3243
|
-
import { VibeError as
|
|
3354
|
+
import { VibeError as VibeError13 } from "@vibeorm/schema";
|
|
3244
3355
|
var SELF_MANAGED_MARKER = "PRAGMA foreign_keys=OFF";
|
|
3245
3356
|
var FK_CHECK_PREFIX = "PRAGMA foreign_key_check";
|
|
3246
3357
|
function usesSelfManagedTransactions(params) {
|
|
@@ -3281,7 +3392,7 @@ function assertSelfManagedResumeSafe(params) {
|
|
|
3281
3392
|
const completedIndex = params.direction === "up" ? params.start + 1 : -(params.start + 2);
|
|
3282
3393
|
const rewind = selfManagedResumeStart({ sql: params.sql, start: params.start });
|
|
3283
3394
|
const retryIndex = params.direction === "up" ? rewind : -(rewind + 1);
|
|
3284
|
-
throw new
|
|
3395
|
+
throw new VibeError13({
|
|
3285
3396
|
code: "VIBE_MIGRATION",
|
|
3286
3397
|
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.`,
|
|
3287
3398
|
meta: { reason: "uncertain-statement", migration: params.migration, statementIndex: params.start, direction: params.direction, dialect: "sqlite" }
|
|
@@ -3310,7 +3421,7 @@ function unsupportedScript(params) {
|
|
|
3310
3421
|
const { migration, direction, index, problem, found } = params;
|
|
3311
3422
|
const subject = direction === "down" ? `the down script of migration "${migration}"` : `migration "${migration}"`;
|
|
3312
3423
|
const position = `${direction === "down" ? "down statement" : "statement"} ${index + 1} of ${params.statements}`;
|
|
3313
|
-
return new
|
|
3424
|
+
return new VibeError13({
|
|
3314
3425
|
code: "VIBE_MIGRATION",
|
|
3315
3426
|
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.',
|
|
3316
3427
|
meta: { reason: "unsupported-self-managed-script", problem, migration, statementIndex: index, direction }
|
|
@@ -3366,7 +3477,7 @@ async function executeStatement(params) {
|
|
|
3366
3477
|
const rows = await executor({ text: statement, values: [] });
|
|
3367
3478
|
if (rows.length > 0) {
|
|
3368
3479
|
const sample = rows.slice(0, 5).map((row) => `${String(row.table)} rowid ${String(row.rowid)} \u2192 ${String(row.parent)}`).join("; ");
|
|
3369
|
-
throw new
|
|
3480
|
+
throw new VibeError13({
|
|
3370
3481
|
code: "VIBE_MIGRATION",
|
|
3371
3482
|
message: `foreign key check failed after table rebuild: ${rows.length} violation(s) \u2014 ${sample}`,
|
|
3372
3483
|
meta: { violations: rows }
|
|
@@ -3421,7 +3532,7 @@ async function prepareOperatorClassDiff(params) {
|
|
|
3421
3532
|
}
|
|
3422
3533
|
|
|
3423
3534
|
// packages/migrate/src/postgres-default-equivalence.ts
|
|
3424
|
-
import { VibeError as
|
|
3535
|
+
import { VibeError as VibeError14 } from "@vibeorm/schema";
|
|
3425
3536
|
import { postgresDialect as postgresDialect8, renderPostgresDefaultFunctionIdentity } from "@vibeorm/sql";
|
|
3426
3537
|
var IDENTIFIER = '(?:"(?:[^"\\u0000]|"")+"|[A-Za-z_][A-Za-z_0-9$]*)';
|
|
3427
3538
|
var CALL = new RegExp(`^\\s*(${IDENTIFIER})(?:\\s*\\.\\s*(${IDENTIFIER}))?\\s*\\(\\s*\\)\\s*$`);
|
|
@@ -3440,7 +3551,7 @@ async function equivalentPostgresDefaultFunction(params) {
|
|
|
3440
3551
|
return false;
|
|
3441
3552
|
const query = renderPostgresDefaultFunctionIdentity({ dialect: postgresDialect8, table: params.table, column: params.column });
|
|
3442
3553
|
const rows = await params.executor({ text: query.text, values: [...query.values] }).catch((cause) => {
|
|
3443
|
-
throw new
|
|
3554
|
+
throw new VibeError14({ code: "VIBE_MIGRATION", message: "Cannot inspect the stored default function identity", meta: { reason: "default-inspection-unavailable", table: params.table, column: params.column }, cause });
|
|
3444
3555
|
});
|
|
3445
3556
|
if (rows.length !== 1)
|
|
3446
3557
|
return false;
|
|
@@ -3454,7 +3565,7 @@ async function equivalentPostgresDefaultFunction(params) {
|
|
|
3454
3565
|
}
|
|
3455
3566
|
|
|
3456
3567
|
// packages/migrate/src/object-ownership.ts
|
|
3457
|
-
import { VibeError as
|
|
3568
|
+
import { VibeError as VibeError15, defaultPrimaryKeyName, enumStorageOf as enumStorageOf2, implicitJoinTableObjectNames, postgresGeneratedObjectName, qualifiedSqlName as qualifiedSqlName6, sqlObjectKey as sqlObjectKey3 } from "@vibeorm/schema";
|
|
3458
3569
|
function legacyMigrationObjects(params) {
|
|
3459
3570
|
return params.artifacts.map((artifact) => {
|
|
3460
3571
|
const owner = artifact.owns;
|
|
@@ -3556,7 +3667,10 @@ function removalIdentity(params) {
|
|
|
3556
3667
|
}
|
|
3557
3668
|
}
|
|
3558
3669
|
function schemaObjectIdentities(params) {
|
|
3559
|
-
|
|
3670
|
+
return schemaObjectIdentitiesFromNormalized({ ...params, normalized: normalizeSchema({ schema: params.schema, dialect: params.dialect }) });
|
|
3671
|
+
}
|
|
3672
|
+
function schemaObjectIdentitiesFromNormalized(params) {
|
|
3673
|
+
const { normalized } = params;
|
|
3560
3674
|
const result = [];
|
|
3561
3675
|
if (params.dialect === "postgres") {
|
|
3562
3676
|
for (const namespace of schemaNamespaces({ schema: params.schema }))
|
|
@@ -3606,7 +3720,7 @@ function assertRetainedMigrationObjects(params) {
|
|
|
3606
3720
|
const keys = new Set(params.to.map((object) => sqlObjectKey3({ identity: object.identity })));
|
|
3607
3721
|
const removed = params.from.filter((object) => !keys.has(sqlObjectKey3({ identity: object.identity })));
|
|
3608
3722
|
if (removed.length > 0)
|
|
3609
|
-
throw new
|
|
3723
|
+
throw new VibeError15({ code: "VIBE_MIGRATION", message: "Removing migration ownership requires a reviewed hand-authored migration and new desired state", meta: { reason: "migration-object-removal-review-required", shadowReusable: true, objects: removed.map((object) => object.identity) } });
|
|
3610
3724
|
}
|
|
3611
3725
|
function orderedMigrationObjects(params) {
|
|
3612
3726
|
const byKey = new Map(params.objects.map((object) => [sqlObjectKey3({ identity: object.identity }), object]));
|
|
@@ -3618,7 +3732,7 @@ function orderedMigrationObjects(params) {
|
|
|
3618
3732
|
if (visited.has(key))
|
|
3619
3733
|
return;
|
|
3620
3734
|
if (visiting.has(key))
|
|
3621
|
-
throw new
|
|
3735
|
+
throw new VibeError15({ code: "VIBE_SCHEMA_INVALID", message: "Migration object dependency cycle", meta: { id: object.id } });
|
|
3622
3736
|
visiting.add(key);
|
|
3623
3737
|
for (const dependency of object.dependsOn) {
|
|
3624
3738
|
const target = byKey.get(sqlObjectKey3({ identity: dependency }));
|
|
@@ -3637,10 +3751,10 @@ function assertNamedRemovals(params) {
|
|
|
3637
3751
|
const approved = new Set((params.approved ?? []).map((identity) => sqlObjectKey3({ identity })));
|
|
3638
3752
|
const missing = params.removals.filter((identity) => !approved.has(sqlObjectKey3({ identity })));
|
|
3639
3753
|
if (missing.length > 0)
|
|
3640
|
-
throw new
|
|
3754
|
+
throw new VibeError15({ code: "VIBE_MIGRATION", message: "Unmanaged removals require individually named approvals", meta: { reason: "unmanaged-removal-required", shadowReusable: true, objects: missing } });
|
|
3641
3755
|
const integrity = params.removals.filter((identity) => identity.kind === "constraint" || identity.kind === "index");
|
|
3642
3756
|
if (integrity.length > 0 && params.acceptIntegrityLoss !== true)
|
|
3643
|
-
throw new
|
|
3757
|
+
throw new VibeError15({ code: "VIBE_MIGRATION", message: "Named constraint or index removals also require acceptIntegrityLoss", meta: { reason: "integrity-acknowledgement-required", shadowReusable: true, objects: integrity } });
|
|
3644
3758
|
}
|
|
3645
3759
|
function assertRemovalAuthorization(params) {
|
|
3646
3760
|
const declared = params.previous === undefined ? [] : schemaObjectIdentities({ schema: params.previous.schema, dialect: params.dialect, namespace: params.namespace });
|
|
@@ -3665,9 +3779,9 @@ function assertRemovalAuthorization(params) {
|
|
|
3665
3779
|
}
|
|
3666
3780
|
}
|
|
3667
3781
|
if (unknown.length > 0)
|
|
3668
|
-
throw new
|
|
3782
|
+
throw new VibeError15({ code: "VIBE_MIGRATION", message: "Unknown object removals require individually named removeUnmanaged decisions", meta: { reason: "unmanaged-removal-required", shadowReusable: true, objects: unknown } });
|
|
3669
3783
|
if (integrity.length > 0 && params.acceptIntegrityLoss !== true)
|
|
3670
|
-
throw new
|
|
3784
|
+
throw new VibeError15({ code: "VIBE_MIGRATION", message: "Constraint or index removal requires acceptIntegrityLoss; data-loss acknowledgement does not authorize it", meta: { reason: "integrity-acknowledgement-required", shadowReusable: true, objects: integrity } });
|
|
3671
3785
|
}
|
|
3672
3786
|
|
|
3673
3787
|
// packages/migrate/src/inspect-objects.ts
|
|
@@ -3752,7 +3866,7 @@ async function inspectSqlObject(params) {
|
|
|
3752
3866
|
}
|
|
3753
3867
|
}
|
|
3754
3868
|
return { state: "present", definition: { identity: params.identity, facts, dependencies: [], format: "catalog-v2" } };
|
|
3755
|
-
}).catch((error) => ({ state: "unavailable", reason: error instanceof
|
|
3869
|
+
}).catch((error) => ({ state: "unavailable", reason: error instanceof VibeError16 ? String(error.meta.feature ?? error.code) : "catalog-query-failed" }));
|
|
3756
3870
|
}
|
|
3757
3871
|
function postgresInspection(params) {
|
|
3758
3872
|
if (!params.addresses.every((address) => typeof address === "string"))
|
|
@@ -3767,7 +3881,7 @@ function postgresInspection(params) {
|
|
|
3767
3881
|
return { state: "present", definition: { identity: params.identity, facts, dependencies, format: "catalog-v2" } };
|
|
3768
3882
|
}
|
|
3769
3883
|
function unavailableReason(params) {
|
|
3770
|
-
return params.error instanceof
|
|
3884
|
+
return params.error instanceof VibeError16 ? String(params.error.meta.feature ?? params.error.meta.reason ?? params.error.code) : "catalog-query-failed";
|
|
3771
3885
|
}
|
|
3772
3886
|
async function readBatch(params) {
|
|
3773
3887
|
const positions = new Set(params.batch.positions);
|
|
@@ -3775,7 +3889,7 @@ async function readBatch(params) {
|
|
|
3775
3889
|
const rows = new Map;
|
|
3776
3890
|
for (const row of await params.executor({ text: params.batch.text, values: [...params.batch.values] })) {
|
|
3777
3891
|
if (typeof row.position !== "number" || !Number.isInteger(row.position) || !positions.has(row.position))
|
|
3778
|
-
throw new
|
|
3892
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Invalid batched catalog identity", meta: { reason: "catalog-query-failed" } });
|
|
3779
3893
|
const matches = rows.get(row.position) ?? [];
|
|
3780
3894
|
matches.push(row);
|
|
3781
3895
|
rows.set(row.position, matches);
|
|
@@ -3820,7 +3934,7 @@ async function assertCatalogVisibility(params) {
|
|
|
3820
3934
|
const grants = new Set(rows.map((row) => row.privilege));
|
|
3821
3935
|
const missing = ["SELECT", "TRIGGER", "SHOW VIEW", "EXECUTE"].filter((privilege) => !grants.has(privilege));
|
|
3822
3936
|
if (missing.length > 0)
|
|
3823
|
-
throw new
|
|
3937
|
+
throw new VibeError16({ 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 } });
|
|
3824
3938
|
}
|
|
3825
3939
|
var CATALOG_V1_OMITTED_FACTS = Object.freeze({
|
|
3826
3940
|
function: ["definerOwner"],
|
|
@@ -3844,9 +3958,20 @@ function compare(params) {
|
|
|
3844
3958
|
differences.push({ attribute: "dependencies", expected, actual });
|
|
3845
3959
|
return differences;
|
|
3846
3960
|
}
|
|
3961
|
+
function compareInspectedSqlObject(params) {
|
|
3962
|
+
const { identity, ownership, expected, live } = params;
|
|
3963
|
+
if (live.state !== "present")
|
|
3964
|
+
return { identity, ownership, state: live.state, differences: [], ...live.state === "unavailable" ? { reason: live.reason } : {} };
|
|
3965
|
+
if (expected === undefined)
|
|
3966
|
+
return identity.kind === "namespace" ? { identity, ownership, state: "matching", differences: [] } : { identity, ownership, state: "unavailable", reason: "independent-definition-required", differences: [] };
|
|
3967
|
+
if (params.dialect === "postgres" && identity.kind === "table" && (expected.facts.persistence === undefined || expected.facts.options === undefined))
|
|
3968
|
+
return { identity, ownership, state: "unavailable", reason: "table-physical-reference-required", differences: compare({ expected, live: live.definition }) };
|
|
3969
|
+
const differences = compare({ expected, live: live.definition });
|
|
3970
|
+
return { identity, ownership, state: differences.length === 0 ? "matching" : "changed", differences };
|
|
3971
|
+
}
|
|
3847
3972
|
function assertProbeMode(params) {
|
|
3848
3973
|
if (params.probe !== undefined && params.probe !== "none" && DIALECT_CAPABILITIES2[params.dialect].checkNormalization !== params.probe)
|
|
3849
|
-
throw new
|
|
3974
|
+
throw new VibeError16({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `${params.probe} CHECK probes are unavailable on ${params.dialect}`, meta: { feature: "checkProbe", dialect: params.dialect } });
|
|
3850
3975
|
}
|
|
3851
3976
|
async function verifyMigrationState(params) {
|
|
3852
3977
|
return verifyState(params);
|
|
@@ -3869,18 +3994,7 @@ async function verifyState(params) {
|
|
|
3869
3994
|
const expected = definitions.get(key);
|
|
3870
3995
|
const live = inspections === undefined ? await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity }) : inspections[position];
|
|
3871
3996
|
position++;
|
|
3872
|
-
|
|
3873
|
-
objects.push({ identity, ownership, state: live.state, differences: [], ...live.state === "unavailable" ? { reason: live.reason } : {} });
|
|
3874
|
-
} else if (expected === undefined && identity.kind === "namespace") {
|
|
3875
|
-
objects.push({ identity, ownership, state: "matching", differences: [] });
|
|
3876
|
-
} else if (expected === undefined) {
|
|
3877
|
-
objects.push({ identity, ownership, state: "unavailable", reason: "independent-definition-required", differences: [] });
|
|
3878
|
-
} else if (params.dialect === "postgres" && identity.kind === "table" && (expected.facts.persistence === undefined || expected.facts.options === undefined)) {
|
|
3879
|
-
objects.push({ identity, ownership, state: "unavailable", reason: "table-physical-reference-required", differences: compare({ expected, live: live.definition }) });
|
|
3880
|
-
} else {
|
|
3881
|
-
const differences = compare({ expected, live: live.definition });
|
|
3882
|
-
objects.push({ identity, ownership, state: differences.length === 0 ? "matching" : "changed", differences });
|
|
3883
|
-
}
|
|
3997
|
+
objects.push(compareInspectedSqlObject({ identity, ownership, expected, live, dialect: params.dialect }));
|
|
3884
3998
|
}
|
|
3885
3999
|
if (params.dialect === "postgres" && schemaNamespaces({ schema: params.expected.schema }).length > 0 || params.expected.schema.models.length > 0 || params.expected.schema.enums.length > 0)
|
|
3886
4000
|
objects.push(...await verifySchemaObjects(params));
|
|
@@ -3944,7 +4058,8 @@ async function verifySchemaObjects(params) {
|
|
|
3944
4058
|
pin("primaryKey", desired.pk, live.pk);
|
|
3945
4059
|
for (const [column, field] of desired.columns) {
|
|
3946
4060
|
const stored = live.columns.get(column);
|
|
3947
|
-
|
|
4061
|
+
const exactType = field.pinsExactType;
|
|
4062
|
+
pin(`columns.${column}.type`, exactType ? field.exactTypeKey : field.typeKey, exactType ? stored?.exactTypeKey : stored?.typeKey);
|
|
3948
4063
|
pin(`columns.${column}.nullable`, field.nullable, stored?.nullable);
|
|
3949
4064
|
if (params.dialect === "mysql" && field.field.collation !== undefined) {
|
|
3950
4065
|
pin(`columns.${column}.collation`, field.field.collation.toLowerCase(), stored?.field.collation?.toLowerCase());
|
|
@@ -4048,7 +4163,7 @@ async function verifyMigrationTransition(params) {
|
|
|
4048
4163
|
}
|
|
4049
4164
|
async function verifySqliteTransitionBeforeRestore(params) {
|
|
4050
4165
|
if (params.dialect !== "sqlite")
|
|
4051
|
-
throw new
|
|
4166
|
+
throw new VibeError16({ code: "VIBE_INTERNAL", message: "SQLite rebuild verification requires the SQLite dialect" });
|
|
4052
4167
|
await executeStatement({ executor: params.executor, dialect: "sqlite", statement: "PRAGMA foreign_key_check;" });
|
|
4053
4168
|
return verifyTransition({ ...params, sqliteRestorePending: true });
|
|
4054
4169
|
}
|
|
@@ -4058,7 +4173,7 @@ async function verifyTransition(params) {
|
|
|
4058
4173
|
for (const identity of params.removals ?? []) {
|
|
4059
4174
|
const owner = externalObjectOwner({ identity, external });
|
|
4060
4175
|
if (owner !== undefined)
|
|
4061
|
-
throw new
|
|
4176
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Transition removals cannot target externally owned objects", meta: { reason: "external-owned-removal", owner, identity } });
|
|
4062
4177
|
}
|
|
4063
4178
|
params = { ...params, from: projectExternalState({ state: params.from, external }), to: projectExternalState({ state: params.to, external }) };
|
|
4064
4179
|
}
|
|
@@ -4083,12 +4198,12 @@ async function migrationNamespace(params) {
|
|
|
4083
4198
|
const rows = await params.executor({ text: query.text, values: [] });
|
|
4084
4199
|
const namespace = rows[0]?.namespace;
|
|
4085
4200
|
if (typeof namespace !== "string" || namespace.length === 0)
|
|
4086
|
-
throw new
|
|
4201
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Cannot determine the migration namespace", meta: { reason: "inspection-unavailable" } });
|
|
4087
4202
|
return namespace;
|
|
4088
4203
|
}
|
|
4089
4204
|
function assertMigrationState(params) {
|
|
4090
4205
|
if (!params.report.inSync)
|
|
4091
|
-
throw new
|
|
4206
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Migration state could not be verified", meta: { reason: "migration-state-unverified", shadowReusable: true, objects: params.report.objects.filter((object) => object.state !== "matching") } });
|
|
4092
4207
|
}
|
|
4093
4208
|
async function deparsePostgresCheck(params) {
|
|
4094
4209
|
const definition = await deparsePostgresCheckDefinition({ ...params, constraint: "vibeorm_check_probe" });
|
|
@@ -4102,7 +4217,7 @@ async function deparsePostgresCheckDefinition(params) {
|
|
|
4102
4217
|
await params.executor({ text: query.text });
|
|
4103
4218
|
const result = await inspectSqlObject({ executor: params.executor, dialect: "postgres", identity: { kind: "constraint", constraintType: "check", table: params.table, name } });
|
|
4104
4219
|
if (result.state !== "present" || typeof result.definition.facts.definition !== "string")
|
|
4105
|
-
throw new
|
|
4220
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "CHECK normalization probe unavailable", meta: { reason: "check-inspection-unavailable" } });
|
|
4106
4221
|
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 } };
|
|
4107
4222
|
} finally {
|
|
4108
4223
|
await params.executor({ text: "ROLLBACK TO SAVEPOINT vibeorm_check_probe" });
|
|
@@ -4120,7 +4235,7 @@ async function deparseMysqlCheck(params) {
|
|
|
4120
4235
|
const source = await params.executor({ text: read.text, values: [] });
|
|
4121
4236
|
const definition = source[0]?.["Create Table"];
|
|
4122
4237
|
if (typeof definition !== "string")
|
|
4123
|
-
throw new
|
|
4238
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Cannot read temporary clone definition" });
|
|
4124
4239
|
const drop = renderMysqlCheckProbe({ dialect, probe, operation: "drop" });
|
|
4125
4240
|
await params.executor({ text: drop.text });
|
|
4126
4241
|
created = false;
|
|
@@ -4131,7 +4246,7 @@ async function deparseMysqlCheck(params) {
|
|
|
4131
4246
|
const normalizedDefinition = rows[0]?.["Create Table"];
|
|
4132
4247
|
const check = typeof normalizedDefinition === "string" ? parseSqliteNamedChecks({ tableSql: normalizedDefinition }).find((entry) => entry.name === probe) : undefined;
|
|
4133
4248
|
if (check === undefined)
|
|
4134
|
-
throw new
|
|
4249
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "MySQL temporary CHECK deparse unavailable", meta: { reason: "check-inspection-unavailable" } });
|
|
4135
4250
|
return check.expression;
|
|
4136
4251
|
} finally {
|
|
4137
4252
|
if (created) {
|
|
@@ -4158,7 +4273,7 @@ async function prepareCheckDiff(params) {
|
|
|
4158
4273
|
const desired = await deparsePostgresCheck({ executor: params.executor, table: identity.table, expression: next.expression, probe: "rollback" });
|
|
4159
4274
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
4160
4275
|
if (live.state !== "present")
|
|
4161
|
-
throw new
|
|
4276
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Live CHECK inspection unavailable", meta: { reason: "check-inspection-unavailable", identity } });
|
|
4162
4277
|
if (live.definition.facts.definition === desired) {
|
|
4163
4278
|
checks.push(next);
|
|
4164
4279
|
continue;
|
|
@@ -4170,7 +4285,7 @@ async function prepareCheckDiff(params) {
|
|
|
4170
4285
|
const desired = await deparseMysqlCheck({ executor: params.executor, table: identity.table, expression: next.expression, probe: "temporary" });
|
|
4171
4286
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
4172
4287
|
if (live.state !== "present")
|
|
4173
|
-
throw new
|
|
4288
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Live CHECK inspection unavailable", meta: { reason: "check-inspection-unavailable", identity } });
|
|
4174
4289
|
checks.push(desired === live.definition.facts.definition ? next : { name: check.name, expression: check.expression });
|
|
4175
4290
|
continue;
|
|
4176
4291
|
}
|
|
@@ -4183,7 +4298,7 @@ async function prepareCheckDiff(params) {
|
|
|
4183
4298
|
checks.push({ name: check.name, expression: check.expression });
|
|
4184
4299
|
continue;
|
|
4185
4300
|
}
|
|
4186
|
-
throw new
|
|
4301
|
+
throw new VibeError16({ 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 } });
|
|
4187
4302
|
}
|
|
4188
4303
|
models.push({ ...model, checks });
|
|
4189
4304
|
}
|
|
@@ -4210,7 +4325,7 @@ async function inspectMigrationInventory(params) {
|
|
|
4210
4325
|
for (const row of rows) {
|
|
4211
4326
|
const name = row.name;
|
|
4212
4327
|
if (typeof name !== "string" || typeof row.kind !== "string")
|
|
4213
|
-
throw new
|
|
4328
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "Malformed catalog identity", meta: { reason: "inventory-unavailable" } });
|
|
4214
4329
|
const qualified = { namespace: params.namespace, name };
|
|
4215
4330
|
const table = { namespace: params.namespace, name: String(row.parent ?? "") };
|
|
4216
4331
|
let identity;
|
|
@@ -4236,7 +4351,7 @@ async function inspectMigrationInventory(params) {
|
|
|
4236
4351
|
case "constraint": {
|
|
4237
4352
|
const constraintType = kinds[String(row.subtype)];
|
|
4238
4353
|
if (constraintType === undefined)
|
|
4239
|
-
throw new
|
|
4354
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "Unknown constraint catalog kind", meta: { reason: "inventory-unavailable" } });
|
|
4240
4355
|
identity = { kind: "constraint", table, name, constraintType };
|
|
4241
4356
|
break;
|
|
4242
4357
|
}
|
|
@@ -4246,17 +4361,17 @@ async function inspectMigrationInventory(params) {
|
|
|
4246
4361
|
const query = renderMysqlObjectMembers2({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: { kind: "function", name: qualified, argumentTypes: [] } });
|
|
4247
4362
|
const members = await params.executor({ text: query.text, values: [...query.values] });
|
|
4248
4363
|
if (members.length === 0)
|
|
4249
|
-
throw new
|
|
4364
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "MySQL function signature unavailable", meta: { reason: "inventory-unavailable", name } });
|
|
4250
4365
|
argumentTypes = members.filter((member) => member.mode !== null).map((member) => ({ namespace: "mysql", name: String(member.type) }));
|
|
4251
4366
|
}
|
|
4252
4367
|
const candidate = { kind: "function", name: qualified, argumentTypes };
|
|
4253
4368
|
if (!isSqlObjectIdentity2(candidate))
|
|
4254
|
-
throw new
|
|
4369
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "Invalid function signature in catalog", meta: { reason: "inventory-unavailable" } });
|
|
4255
4370
|
identity = candidate;
|
|
4256
4371
|
break;
|
|
4257
4372
|
}
|
|
4258
4373
|
default:
|
|
4259
|
-
throw new
|
|
4374
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: `Unknown catalog object kind ${row.kind}`, meta: { reason: "inventory-unavailable" } });
|
|
4260
4375
|
}
|
|
4261
4376
|
objects.push({ identity, ...typeof row.backing === "string" ? { backingConstraint: row.backing } : {} });
|
|
4262
4377
|
if (params.dialect === "sqlite" && row.kind === "table" && typeof row.definition === "string" && !/^\s*CREATE\s+VIRTUAL\s+TABLE\b/i.test(row.definition)) {
|
|
@@ -4301,7 +4416,7 @@ async function inspectMigrationInventory(params) {
|
|
|
4301
4416
|
// packages/migrate/src/external-objects.ts
|
|
4302
4417
|
var partitionPatterns = new WeakMap;
|
|
4303
4418
|
function invalid2(params) {
|
|
4304
|
-
return new
|
|
4419
|
+
return new VibeError18({ code: "VIBE_CONFIG", message: `Invalid external ownership declaration: ${params.detail}`, meta: { reason: "external-declaration-invalid", ...params.owner === undefined ? {} : { owner: params.owner } }, ...params.cause === undefined ? {} : { cause: params.cause } });
|
|
4305
4420
|
}
|
|
4306
4421
|
function record2(value) {
|
|
4307
4422
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -4389,7 +4504,7 @@ async function parseExternalObjects(params) {
|
|
|
4389
4504
|
}
|
|
4390
4505
|
return Object.freeze(result);
|
|
4391
4506
|
}).catch((cause) => {
|
|
4392
|
-
if (cause instanceof
|
|
4507
|
+
if (cause instanceof VibeError18)
|
|
4393
4508
|
throw cause;
|
|
4394
4509
|
throw invalid2({ detail: "regular-expression pattern cannot be compiled", cause });
|
|
4395
4510
|
});
|
|
@@ -4413,7 +4528,7 @@ function declaredTableOwner(params) {
|
|
|
4413
4528
|
return patterns?.some(({ pattern }) => pattern.exec(params.name.name)?.[0] === params.name.name) === true;
|
|
4414
4529
|
});
|
|
4415
4530
|
if (matches.length > 1)
|
|
4416
|
-
throw new
|
|
4531
|
+
throw new VibeError18({ code: "VIBE_CONFIG", message: "A table is claimed by more than one external owner", meta: { reason: "external-owner-collision", identity: { kind: "table", name: params.name }, owners: matches.map((entry) => entry.owner) } });
|
|
4417
4532
|
return matches[0]?.owner;
|
|
4418
4533
|
}
|
|
4419
4534
|
function externalObjectOwner(params) {
|
|
@@ -4435,13 +4550,13 @@ function resolveExternalObjectOwnership(params) {
|
|
|
4435
4550
|
if (params.external.length > 0 && params.dialect !== "postgres")
|
|
4436
4551
|
throw unsupportedCapability7({ dialect: params.dialect, feature: "externalObjects" });
|
|
4437
4552
|
if (params.catalog.inventory.some((object) => object.inspectionUnavailable !== undefined))
|
|
4438
|
-
throw new
|
|
4553
|
+
throw new VibeError18({ code: "VIBE_MIGRATION", message: "External ownership requires complete catalog visibility", meta: { reason: "external-inventory-unavailable", objects: params.catalog.inventory.filter((object) => object.inspectionUnavailable !== undefined) } });
|
|
4439
4554
|
const tables = new Map;
|
|
4440
4555
|
const assign = (entry) => {
|
|
4441
4556
|
const key = nameKey(entry.name);
|
|
4442
4557
|
const prior = tables.get(key)?.owner ?? declaredTableOwner({ external: params.external, name: entry.name });
|
|
4443
4558
|
if (prior !== undefined && prior !== entry.owner)
|
|
4444
|
-
throw new
|
|
4559
|
+
throw new VibeError18({ code: "VIBE_CONFIG", message: "An inherited table has conflicting external owners", meta: { reason: "external-owner-collision", identity: { kind: "table", name: entry.name }, owners: [prior, entry.owner] } });
|
|
4445
4560
|
if (tables.has(key))
|
|
4446
4561
|
return false;
|
|
4447
4562
|
tables.set(key, entry);
|
|
@@ -4472,7 +4587,7 @@ function resolveExternalObjectOwnership(params) {
|
|
|
4472
4587
|
return [];
|
|
4473
4588
|
const whole = schemaOwner({ external: params.external, namespace: edge.sequence.namespace });
|
|
4474
4589
|
if (whole !== undefined && whole !== owner)
|
|
4475
|
-
throw new
|
|
4590
|
+
throw new VibeError18({ code: "VIBE_CONFIG", message: "An owned sequence has conflicting external owners", meta: { reason: "external-owner-collision", identity: { kind: "sequence", name: edge.sequence }, owners: [whole, owner] } });
|
|
4476
4591
|
return [{ owner, name: edge.sequence }];
|
|
4477
4592
|
});
|
|
4478
4593
|
const closure = { declarations: params.external, catalog: params.catalog, inventory: params.catalog.inventory, tables: [...tables.values()], sequences, owners: [], identities: [] };
|
|
@@ -4480,7 +4595,7 @@ function resolveExternalObjectOwnership(params) {
|
|
|
4480
4595
|
const owner = externalObjectOwner({ external: closure, identity: { kind: "table", name: edge.table } });
|
|
4481
4596
|
const targetOwner = externalObjectOwner({ external: closure, identity: { kind: "table", name: edge.target } });
|
|
4482
4597
|
if (owner !== targetOwner && (owner !== undefined || targetOwner !== undefined))
|
|
4483
|
-
throw new
|
|
4598
|
+
throw new VibeError18({ code: "VIBE_MIGRATION", message: "Foreign keys cannot cross external ownership boundaries", meta: { reason: "external-cross-owner-foreign-key", name: edge.name, table: edge.table, target: edge.target, owner: owner ?? "application", targetOwner: targetOwner ?? "application" } });
|
|
4484
4599
|
}
|
|
4485
4600
|
const observed = new Map;
|
|
4486
4601
|
for (const namespace of params.catalog.namespaces) {
|
|
@@ -4508,7 +4623,7 @@ function catalogName(params) {
|
|
|
4508
4623
|
const namespace = params.row[params.namespace];
|
|
4509
4624
|
const name = params.row[params.name];
|
|
4510
4625
|
if (!identifier(namespace) || !identifier(name))
|
|
4511
|
-
throw new
|
|
4626
|
+
throw new VibeError18({ code: "VIBE_MIGRATION", message: "Malformed external ownership catalog endpoint", meta: { reason: "external-inventory-unavailable" } });
|
|
4512
4627
|
return { namespace, name };
|
|
4513
4628
|
}
|
|
4514
4629
|
async function inspectExternalObjects(params) {
|
|
@@ -4521,7 +4636,7 @@ async function inspectExternalObjects(params) {
|
|
|
4521
4636
|
};
|
|
4522
4637
|
const namespaceRows = await read("namespaces");
|
|
4523
4638
|
if (namespaceRows.some((row) => !identifier(row.namespace) || row.visible !== true))
|
|
4524
|
-
throw new
|
|
4639
|
+
throw new VibeError18({ code: "VIBE_MIGRATION", message: "External ownership cannot prove visibility of every inspected namespace", meta: { reason: "external-inventory-unavailable" } });
|
|
4525
4640
|
const namespaces = namespaceRows.map((row) => String(row.namespace));
|
|
4526
4641
|
const [inherited, owned, foreignKeys] = await Promise.all([read("inheritance"), read("ownedSequences"), read("foreignKeys")]);
|
|
4527
4642
|
const inventory = params.inventory ?? (await Promise.all(namespaces.map((namespace) => inspectMigrationInventory({ executor: params.executor, dialect: params.dialect, namespace })))).flat();
|
|
@@ -4542,7 +4657,7 @@ function assertExternalOwnershipClaims(params) {
|
|
|
4542
4657
|
for (const identity of params.identities) {
|
|
4543
4658
|
const owner = externalObjectOwner({ identity, external: params.external }) ?? (identity.kind === "namespace" ? [...params.external.tables, ...params.external.sequences].find((entry) => entry.name.namespace === identity.name)?.owner : undefined);
|
|
4544
4659
|
if (owner !== undefined)
|
|
4545
|
-
throw new
|
|
4660
|
+
throw new VibeError18({ code: "VIBE_CONFIG", message: "Application or module declarations collide with external ownership", meta: { reason: "external-owned-object-collision", owner, identity } });
|
|
4546
4661
|
}
|
|
4547
4662
|
}
|
|
4548
4663
|
function projectExternalSchema(params) {
|
|
@@ -4552,7 +4667,7 @@ function projectExternalSchema(params) {
|
|
|
4552
4667
|
for (const model of models)
|
|
4553
4668
|
for (const relation of model.relations)
|
|
4554
4669
|
if (removed.has(relation.targetModel))
|
|
4555
|
-
throw new
|
|
4670
|
+
throw new VibeError18({ code: "VIBE_MIGRATION", message: "Application relations cannot depend on externally owned tables", meta: { reason: "external-cross-owner-foreign-key", model: model.name, relation: relation.name } });
|
|
4556
4671
|
return {
|
|
4557
4672
|
...schema,
|
|
4558
4673
|
models,
|
|
@@ -4585,7 +4700,7 @@ function migrationCoverageGaps(params) {
|
|
|
4585
4700
|
return [...new Map(kept.map((gap) => [JSON.stringify(gap), gap])).values()];
|
|
4586
4701
|
}
|
|
4587
4702
|
// packages/migrate/src/module-adoption.ts
|
|
4588
|
-
import { VibeError as
|
|
4703
|
+
import { VibeError as VibeError22 } from "@vibeorm/schema";
|
|
4589
4704
|
|
|
4590
4705
|
// packages/migrate/src/mysql-catalog-visibility.ts
|
|
4591
4706
|
function hasMysqlGlobalCatalogVisibility(params) {
|
|
@@ -4603,10 +4718,10 @@ function hasMysqlGlobalCatalogVisibility(params) {
|
|
|
4603
4718
|
}
|
|
4604
4719
|
|
|
4605
4720
|
// packages/migrate/src/module-ownership.ts
|
|
4606
|
-
import { VibeError as
|
|
4721
|
+
import { VibeError as VibeError19, sqlObjectKey as sqlObjectKey7, sqlObjectNamespace as sqlObjectNamespace3 } from "@vibeorm/schema";
|
|
4607
4722
|
import { getSqlDialect as getSqlDialect4, renderModuleDependencyQuery } from "@vibeorm/sql";
|
|
4608
4723
|
function refusal(params) {
|
|
4609
|
-
return new
|
|
4724
|
+
return new VibeError19({ code: "VIBE_MIGRATION", message: "Module ownership or dependencies cannot be reconciled with the declared and live schema; no package SQL was authorized", meta: params });
|
|
4610
4725
|
}
|
|
4611
4726
|
function identities(params) {
|
|
4612
4727
|
return [...schemaObjectIdentities({ schema: params.state.schema, dialect: params.dialect, namespace: params.namespace }), ...(params.state.schema.migrationObjects ?? []).map((object) => object.identity), ...params.state.definitions.map((definition) => definition.identity)];
|
|
@@ -4667,7 +4782,7 @@ function assertModuleOwnership(params) {
|
|
|
4667
4782
|
throw refusal({ reason: "module-mutates-application-state", moduleId: origin.moduleId });
|
|
4668
4783
|
}
|
|
4669
4784
|
function dependencyUnavailable(params) {
|
|
4670
|
-
return new
|
|
4785
|
+
return new VibeError19({ code: "VIBE_MIGRATION", message: "Cannot establish module foreign-key dependencies across namespaces. MySQL requires direct global SHOW VIEW (or SELECT) without partial revokes; SHOW VIEW exposes metadata without granting external row reads. Restore catalog visibility before retrying", meta: { reason: "module-dependency-unavailable", moduleId: params.origin.moduleId }, ...params.cause === undefined ? {} : { cause: params.cause } });
|
|
4671
4786
|
}
|
|
4672
4787
|
async function assertCrossNamespaceModuleDependencies(params) {
|
|
4673
4788
|
const { executor, origin } = params;
|
|
@@ -4729,12 +4844,12 @@ async function assertApplicationModuleState(params) {
|
|
|
4729
4844
|
const options = { executor: params.executor, dialect: origin.dialect, from: state.from, to: state.to, removals: state.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } };
|
|
4730
4845
|
const report = params.sqliteRestorePending === true ? await verifySqliteTransitionBeforeRestore(options) : await verifyMigrationTransition(options);
|
|
4731
4846
|
if (!report.inSync)
|
|
4732
|
-
throw new
|
|
4847
|
+
throw new VibeError19({ code: "VIBE_MIGRATION", message: `Application SQL cannot change objects owned by module ${origin.moduleId}; use its reviewed module upgrade`, meta: { reason: "module-application-mutation", moduleId: origin.moduleId, report } });
|
|
4733
4848
|
}
|
|
4734
4849
|
}
|
|
4735
4850
|
|
|
4736
4851
|
// packages/migrate/src/sql-statements.ts
|
|
4737
|
-
import { VibeError as
|
|
4852
|
+
import { VibeError as VibeError20 } from "@vibeorm/schema";
|
|
4738
4853
|
var COMPOUND_BODY_HEADER = /^(?:\s|--[^\n]*\n|\/\*[\s\S]*?\*\/)*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:TEMP|TEMPORARY)\s+)?(?:DEFINER\s*=\s*\S+\s+)?(?:TRIGGER|PROCEDURE|FUNCTION|EVENT)\b/i;
|
|
4739
4854
|
var BLOCK_OPENERS = new Set(["BEGIN", "CASE", "IF", "LOOP", "WHILE", "REPEAT"]);
|
|
4740
4855
|
function splitSqlStatements(params) {
|
|
@@ -4810,7 +4925,7 @@ function splitSqlStatements(params) {
|
|
|
4810
4925
|
continue;
|
|
4811
4926
|
}
|
|
4812
4927
|
if (dialect === "mysql" || dialect === undefined) {
|
|
4813
|
-
throw new
|
|
4928
|
+
throw new VibeError20({ code: "VIBE_MIGRATION", message: "SQL string backslash escaping depends on the session SQL mode; use mode-independent quoting and pass the migration dialect", meta: { reason: "sql-mode-ambiguous", dialect } });
|
|
4814
4929
|
}
|
|
4815
4930
|
}
|
|
4816
4931
|
if (value === endQuote) {
|
|
@@ -4884,10 +4999,10 @@ function joinSqlStatements(params) {
|
|
|
4884
4999
|
|
|
4885
5000
|
// packages/migrate/src/module-origin.ts
|
|
4886
5001
|
import { createHash } from "crypto";
|
|
4887
|
-
import { VibeError as
|
|
5002
|
+
import { VibeError as VibeError21, isSqlObjectIdentity as isSqlObjectIdentity3, sqlObjectKey as sqlObjectKey8 } from "@vibeorm/schema";
|
|
4888
5003
|
var MODULE_ORIGIN_METADATA = Object.freeze({ filename: "module-origin.json", version: 1 });
|
|
4889
5004
|
function invalid3() {
|
|
4890
|
-
return new
|
|
5005
|
+
return new VibeError21({ code: "VIBE_MIGRATION", message: "Module migration origin is malformed or differs from its immutable package artifact; restore the reviewed artifact", meta: { reason: "module-origin-invalid" } });
|
|
4891
5006
|
}
|
|
4892
5007
|
function record3(value) {
|
|
4893
5008
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -4951,7 +5066,7 @@ function assertMaterializedModuleMigration(params) {
|
|
|
4951
5066
|
return;
|
|
4952
5067
|
const restored = restoreModuleMigration({ migration, origin: migration.moduleOrigin });
|
|
4953
5068
|
if (migration.desiredState === undefined || canonicalMigrationJson({ value: restored }) !== canonicalMigrationJson({ value: migration }))
|
|
4954
|
-
throw new
|
|
5069
|
+
throw new VibeError21({ code: "VIBE_MIGRATION", message: "Materialized module metadata differs from its immutable source", meta: { reason: "module-source-state-mismatch", moduleId: migration.moduleOrigin.moduleId } });
|
|
4955
5070
|
}
|
|
4956
5071
|
function restoreModuleMigration(params) {
|
|
4957
5072
|
const { migration, origin } = params;
|
|
@@ -4976,7 +5091,7 @@ function restoreModuleMigration(params) {
|
|
|
4976
5091
|
// packages/migrate/src/module-adoption.ts
|
|
4977
5092
|
var MODULE_ADOPTION_METADATA = Object.freeze({ filename: "module-adoption.json", version: 1 });
|
|
4978
5093
|
function invalid4() {
|
|
4979
|
-
return new
|
|
5094
|
+
return new VibeError22({ code: "VIBE_MIGRATION", message: "Local module adoption proof is malformed or lacks immutable source provenance; restore the reviewed local history", meta: { reason: "module-adoption-invalid" } });
|
|
4980
5095
|
}
|
|
4981
5096
|
function assertMarker(params) {
|
|
4982
5097
|
const value = params.value;
|
|
@@ -5011,22 +5126,22 @@ function serializeModuleAdoption(params) {
|
|
|
5011
5126
|
`;
|
|
5012
5127
|
}
|
|
5013
5128
|
// packages/migrate/src/module-adoption-record.ts
|
|
5014
|
-
import { VibeError as
|
|
5129
|
+
import { VibeError as VibeError43 } from "@vibeorm/schema";
|
|
5015
5130
|
|
|
5016
5131
|
// packages/migrate/src/module-adoption-reference.ts
|
|
5017
|
-
import { VibeError as
|
|
5132
|
+
import { VibeError as VibeError42, isEnumType as isEnumType9, sqlObjectKey as sqlObjectKey14 } from "@vibeorm/schema";
|
|
5018
5133
|
|
|
5019
5134
|
// packages/migrate/src/module-pending.ts
|
|
5020
|
-
import { VibeError as
|
|
5135
|
+
import { VibeError as VibeError38, sqlObjectKey as sqlObjectKey11, qualifiedSqlName as qualifiedSqlName11 } from "@vibeorm/schema";
|
|
5021
5136
|
|
|
5022
5137
|
// packages/migrate/src/module-bundle.ts
|
|
5023
|
-
import { VibeError as
|
|
5138
|
+
import { VibeError as VibeError37 } from "@vibeorm/schema";
|
|
5024
5139
|
|
|
5025
5140
|
// packages/migrate/src/migration-checksum.ts
|
|
5026
5141
|
import { createHash as createHash2 } from "crypto";
|
|
5027
5142
|
|
|
5028
5143
|
// packages/migrate/src/execution.ts
|
|
5029
|
-
import { VibeError as
|
|
5144
|
+
import { VibeError as VibeError23 } from "@vibeorm/schema";
|
|
5030
5145
|
var EXECUTION_SIDECAR_FILENAME = "execution.json";
|
|
5031
5146
|
var EXECUTION_SIDECAR_VERSION = 1;
|
|
5032
5147
|
function isDefaultExecution(params) {
|
|
@@ -5065,7 +5180,7 @@ ${lines.join(`
|
|
|
5065
5180
|
`)}`;
|
|
5066
5181
|
}
|
|
5067
5182
|
function invalid5(params) {
|
|
5068
|
-
return new
|
|
5183
|
+
return new VibeError23({
|
|
5069
5184
|
code: "VIBE_MIGRATION",
|
|
5070
5185
|
message: `invalid execution metadata for migration "${params.migration}": ${params.detail}`,
|
|
5071
5186
|
meta: { reason: "execution-metadata-invalid", migration: params.migration }
|
|
@@ -5116,7 +5231,7 @@ function parseExecutionSidecar(params) {
|
|
|
5116
5231
|
try {
|
|
5117
5232
|
parsed = JSON.parse(params.text);
|
|
5118
5233
|
} catch (error) {
|
|
5119
|
-
throw new
|
|
5234
|
+
throw new VibeError23({
|
|
5120
5235
|
code: "VIBE_MIGRATION",
|
|
5121
5236
|
message: `invalid execution metadata for migration "${migration}": not valid JSON`,
|
|
5122
5237
|
meta: { reason: "execution-metadata-invalid", migration },
|
|
@@ -5215,7 +5330,7 @@ function migrationChecksum(params) {
|
|
|
5215
5330
|
}
|
|
5216
5331
|
|
|
5217
5332
|
// packages/migrate/src/shadow-guard.ts
|
|
5218
|
-
import { VibeError as
|
|
5333
|
+
import { VibeError as VibeError24 } from "@vibeorm/schema";
|
|
5219
5334
|
import { getSqlDialect as getSqlDialect5, renderShadowOperation } from "@vibeorm/sql";
|
|
5220
5335
|
var replayExecutors = new WeakSet;
|
|
5221
5336
|
function isShadowReplayExecutor(params) {
|
|
@@ -5227,7 +5342,7 @@ async function assertDatabaseNotShadow(params) {
|
|
|
5227
5342
|
const [query] = renderShadowOperation({ dialect: getSqlDialect5({ dialect: params.dialect }), operation: { kind: "markerExists" } });
|
|
5228
5343
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
5229
5344
|
if (rows.length !== 0)
|
|
5230
|
-
throw new
|
|
5345
|
+
throw new VibeError24({ code: "VIBE_MIGRATION", message: "This database is claimed as a disposable migration shadow. Application migrations must use the deployment target.", meta: { reason: "shadow-database-is-not-target" } });
|
|
5231
5346
|
}
|
|
5232
5347
|
function runShadowReplay(params) {
|
|
5233
5348
|
replayExecutors.add(params.executor);
|
|
@@ -5237,7 +5352,7 @@ function runShadowReplay(params) {
|
|
|
5237
5352
|
}
|
|
5238
5353
|
|
|
5239
5354
|
// packages/migrate/src/external-dependencies.ts
|
|
5240
|
-
import { VibeError as
|
|
5355
|
+
import { VibeError as VibeError25, unsupportedCapability as unsupportedCapability8 } from "@vibeorm/schema";
|
|
5241
5356
|
import { getSqlDialect as getSqlDialect6, renderExternalDependents } from "@vibeorm/sql";
|
|
5242
5357
|
function externalDependencyTargets(params) {
|
|
5243
5358
|
return params.steps.flatMap((step) => {
|
|
@@ -5260,7 +5375,7 @@ function externalDependencyTargets(params) {
|
|
|
5260
5375
|
});
|
|
5261
5376
|
}
|
|
5262
5377
|
function unavailable() {
|
|
5263
|
-
return new
|
|
5378
|
+
return new VibeError25({ code: "VIBE_MIGRATION", message: "Malformed catalog dependency row", meta: { reason: "external-inventory-unavailable" } });
|
|
5264
5379
|
}
|
|
5265
5380
|
function text(params) {
|
|
5266
5381
|
const value = params.row[params.key];
|
|
@@ -5301,7 +5416,7 @@ function assertNoExternalDependents(params) {
|
|
|
5301
5416
|
const dependent = dependentOwner({ row, external: params.external });
|
|
5302
5417
|
if (dependent.owner === undefined)
|
|
5303
5418
|
continue;
|
|
5304
|
-
throw new
|
|
5419
|
+
throw new VibeError25({
|
|
5305
5420
|
code: "VIBE_MIGRATION",
|
|
5306
5421
|
message: "An externally owned object depends on an application object this change drops or retypes; automatic DDL is refused",
|
|
5307
5422
|
meta: { reason: "external-owned-dependency", owner: dependent.owner, ...dependent.identity === undefined ? {} : { identity: dependent.identity }, dependent: { type, identity }, dependency: target.dependency, referenced }
|
|
@@ -5322,9 +5437,9 @@ async function refuseExternalDependents(params) {
|
|
|
5322
5437
|
}
|
|
5323
5438
|
|
|
5324
5439
|
// packages/migrate/src/module-history.ts
|
|
5325
|
-
import { VibeError as
|
|
5440
|
+
import { VibeError as VibeError26, sqlObjectKey as sqlObjectKey9, providerToDialect as providerToDialect3 } from "@vibeorm/schema";
|
|
5326
5441
|
function refusal2(params) {
|
|
5327
|
-
return new
|
|
5442
|
+
return new VibeError26({ code: "VIBE_MIGRATION", message: `Module ${params.moduleId} history, ownership or predecessor state is inconsistent; restore or complete the reviewed upgrade first`, meta: params });
|
|
5328
5443
|
}
|
|
5329
5444
|
function moduleHistoryHeads(params) {
|
|
5330
5445
|
const heads = new Map;
|
|
@@ -5431,11 +5546,11 @@ function assertModuleUpgradeHistory(params) {
|
|
|
5431
5546
|
}
|
|
5432
5547
|
|
|
5433
5548
|
// packages/migrate/src/module-journal.ts
|
|
5434
|
-
import { VibeError as
|
|
5549
|
+
import { VibeError as VibeError28 } from "@vibeorm/schema";
|
|
5435
5550
|
import { getSqlDialect as getSqlDialect7, MODULE_JOURNAL_COLUMN, renderModuleJournal } from "@vibeorm/sql";
|
|
5436
5551
|
|
|
5437
5552
|
// packages/migrate/src/journal-read.ts
|
|
5438
|
-
import { VibeError as
|
|
5553
|
+
import { VibeError as VibeError27 } from "@vibeorm/schema";
|
|
5439
5554
|
var MISSING_TABLE_IDENTIFIERS = new Set(["42P01", "42S02", "ER_NO_SUCH_TABLE"]);
|
|
5440
5555
|
function isMissingTable(params) {
|
|
5441
5556
|
let current = params.cause;
|
|
@@ -5459,7 +5574,7 @@ async function readJournalRows(params) {
|
|
|
5459
5574
|
if (presence.state === "missing")
|
|
5460
5575
|
return;
|
|
5461
5576
|
}
|
|
5462
|
-
throw new
|
|
5577
|
+
throw new VibeError27({ code: "VIBE_MIGRATION", message: "Cannot inspect migration history; check SELECT privileges", meta: { reason: "history-unavailable" }, cause });
|
|
5463
5578
|
});
|
|
5464
5579
|
if (rows === undefined)
|
|
5465
5580
|
return;
|
|
@@ -5467,7 +5582,7 @@ async function readJournalRows(params) {
|
|
|
5467
5582
|
for (const row of rows) {
|
|
5468
5583
|
const index = row.statement_index;
|
|
5469
5584
|
if (typeof row.name !== "string" || typeof row.checksum !== "string" || typeof index !== "number" && typeof index !== "string" || typeof index === "string" && !/^-?\d+$/.test(index) || !Number.isSafeInteger(Number(index)) || row.module_origin !== null && row.module_origin !== undefined && typeof row.module_origin !== "string" || row.error !== null && row.error !== undefined && typeof row.error !== "string" || result.has(row.name)) {
|
|
5470
|
-
throw new
|
|
5585
|
+
throw new VibeError27({ code: "VIBE_MIGRATION", message: "Malformed migration journal row", meta: { reason: "history-unavailable" } });
|
|
5471
5586
|
}
|
|
5472
5587
|
result.set(row.name, { checksum: row.checksum, statementIndex: Number(index), ...typeof row.error === "string" ? { error: row.error } : {}, ...typeof row.module_origin === "string" ? { moduleEntry: row.module_origin } : {} });
|
|
5473
5588
|
}
|
|
@@ -5476,7 +5591,7 @@ async function readJournalRows(params) {
|
|
|
5476
5591
|
|
|
5477
5592
|
// packages/migrate/src/module-journal.ts
|
|
5478
5593
|
function invalid6() {
|
|
5479
|
-
return new
|
|
5594
|
+
return new VibeError28({ code: "VIBE_MIGRATION", message: "Module migration history is missing, altered or inconsistent; restore the reviewed history before using this module", meta: { reason: "module-history-invalid" } });
|
|
5480
5595
|
}
|
|
5481
5596
|
function parseModuleJournalEntry(params) {
|
|
5482
5597
|
return parseMigrationMetadata({ text: params.text, failure: invalid6, parse: (value) => {
|
|
@@ -5535,7 +5650,7 @@ function moduleRecordComplete(params) {
|
|
|
5535
5650
|
}
|
|
5536
5651
|
|
|
5537
5652
|
// packages/migrate/src/runner.ts
|
|
5538
|
-
import { VibeError as
|
|
5653
|
+
import { VibeError as VibeError36 } from "@vibeorm/schema";
|
|
5539
5654
|
import {
|
|
5540
5655
|
DIALECT_CAPABILITIES as DIALECT_CAPABILITIES3,
|
|
5541
5656
|
getSqlDialect as getSqlDialect9,
|
|
@@ -5545,7 +5660,7 @@ import {
|
|
|
5545
5660
|
} from "@vibeorm/sql";
|
|
5546
5661
|
|
|
5547
5662
|
// packages/migrate/src/recovery.ts
|
|
5548
|
-
import { VibeError as
|
|
5663
|
+
import { VibeError as VibeError29 } from "@vibeorm/schema";
|
|
5549
5664
|
import {
|
|
5550
5665
|
getSqlDialect as getSqlDialect8,
|
|
5551
5666
|
renderColumnStateQuery,
|
|
@@ -5561,7 +5676,7 @@ function constraintUnverifiable(params) {
|
|
|
5561
5676
|
});
|
|
5562
5677
|
}
|
|
5563
5678
|
function conflict2(params) {
|
|
5564
|
-
return new
|
|
5679
|
+
return new VibeError29({
|
|
5565
5680
|
code: "VIBE_MIGRATION",
|
|
5566
5681
|
message: params.message,
|
|
5567
5682
|
meta: { reason: params.reason, ...params.meta }
|
|
@@ -5666,10 +5781,10 @@ async function establishOperationState(params) {
|
|
|
5666
5781
|
|
|
5667
5782
|
// packages/migrate/src/differ.ts
|
|
5668
5783
|
import { assertIndexOperatorClasses as assertIndexOperatorClasses2, isEnumType as isEnumType6, qualifiedSqlName as qualifiedSqlName9, unsupportedCapability as unsupportedCapability10 } from "@vibeorm/schema";
|
|
5669
|
-
import { VibeError as
|
|
5784
|
+
import { VibeError as VibeError31 } from "@vibeorm/schema";
|
|
5670
5785
|
|
|
5671
5786
|
// packages/migrate/src/renames.ts
|
|
5672
|
-
import { VibeError as
|
|
5787
|
+
import { VibeError as VibeError30, unsupportedCapability as unsupportedCapability9 } from "@vibeorm/schema";
|
|
5673
5788
|
function tableOf(model) {
|
|
5674
5789
|
return model.dbName ?? model.name;
|
|
5675
5790
|
}
|
|
@@ -5680,7 +5795,7 @@ function hasColumn(params) {
|
|
|
5680
5795
|
return params.model.fields.some((field) => (field.dbName ?? field.name) === params.column);
|
|
5681
5796
|
}
|
|
5682
5797
|
function ambiguous(params) {
|
|
5683
|
-
return new
|
|
5798
|
+
return new VibeError30({
|
|
5684
5799
|
code: "VIBE_MIGRATION",
|
|
5685
5800
|
message: `rename refused: ${params.detail}`,
|
|
5686
5801
|
meta: { reason: "rename-ambiguous", ...params.meta }
|
|
@@ -5782,17 +5897,34 @@ function planRenames(params) {
|
|
|
5782
5897
|
|
|
5783
5898
|
// packages/migrate/src/differ.ts
|
|
5784
5899
|
function diffSchemas(params) {
|
|
5900
|
+
return diffSchemasWithNormalization(params).steps;
|
|
5901
|
+
}
|
|
5902
|
+
function diffSchemasWithNormalization(params) {
|
|
5785
5903
|
const dialect = params.dialect ?? "postgres";
|
|
5786
5904
|
assertIndexOperatorClasses2({ schema: params.to, dialect });
|
|
5787
|
-
for (const kind of ["models", "enums"])
|
|
5905
|
+
for (const kind of ["models", "enums"]) {
|
|
5906
|
+
const sourceByName = new Map;
|
|
5907
|
+
const sourcePhysical = new Set;
|
|
5908
|
+
const targetPhysical = new Set(params.to[kind].map((entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect })));
|
|
5909
|
+
const removedByBasename = new Map;
|
|
5910
|
+
for (const entry of params.from[kind]) {
|
|
5911
|
+
if (!sourceByName.has(entry.name))
|
|
5912
|
+
sourceByName.set(entry.name, entry);
|
|
5913
|
+
const basename = entry.dbName ?? entry.name;
|
|
5914
|
+
const key = physicalNameKey({ name: basename, namespace: entry.namespace, dialect });
|
|
5915
|
+
sourcePhysical.add(key);
|
|
5916
|
+
if (!targetPhysical.has(key) && !removedByBasename.has(basename))
|
|
5917
|
+
removedByBasename.set(basename, entry);
|
|
5918
|
+
}
|
|
5788
5919
|
for (const entry of params.to[kind]) {
|
|
5789
5920
|
const key = physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect });
|
|
5790
|
-
const before =
|
|
5791
|
-
if (before !== undefined && qualifiedSqlName9({ name: entry.name, namespace: before.namespace }).namespace !== qualifiedSqlName9({ name: entry.name, namespace: entry.namespace }).namespace && !
|
|
5921
|
+
const before = sourceByName.get(entry.name) ?? removedByBasename.get(entry.dbName ?? entry.name);
|
|
5922
|
+
if (before !== undefined && qualifiedSqlName9({ name: entry.name, namespace: before.namespace }).namespace !== qualifiedSqlName9({ name: entry.name, namespace: entry.namespace }).namespace && !sourcePhysical.has(key))
|
|
5792
5923
|
throw unsupportedCapability10({ dialect, feature: "automaticNamespaceMove", hint: "Moving populated objects to a fixed namespace requires an explicit reviewed migration" });
|
|
5793
5924
|
}
|
|
5794
|
-
|
|
5795
|
-
|
|
5925
|
+
}
|
|
5926
|
+
let from = normalizeSchema({ schema: params.from, dialect });
|
|
5927
|
+
const to = normalizeSchema({ schema: params.to, dialect });
|
|
5796
5928
|
for (const schema of [params.from, params.to])
|
|
5797
5929
|
if (dialect !== "postgres" && schema.models.some((model) => (model.exclusions?.length ?? 0) > 0))
|
|
5798
5930
|
throw unsupportedCapability10({ dialect, feature: "exclusionConstraints" });
|
|
@@ -5801,11 +5933,12 @@ function diffSchemas(params) {
|
|
|
5801
5933
|
if (dialect !== "postgres")
|
|
5802
5934
|
refuseScalarListColumns({ schema: params.to, dialect });
|
|
5803
5935
|
const renamed = planRenames({ from: params.from, to: params.to, renames: params.renames ?? [], dialect });
|
|
5936
|
+
if (renamed.from !== params.from)
|
|
5937
|
+
from = normalizeSchema({ schema: renamed.from, dialect });
|
|
5804
5938
|
params = { ...params, from: renamed.from };
|
|
5805
|
-
const
|
|
5806
|
-
const to = normalizeSchema({ schema: params.to, dialect });
|
|
5939
|
+
const sourceNamespaces = new Set(dialect === "postgres" ? schemaNamespaces({ schema: params.from }) : []);
|
|
5807
5940
|
const buckets = {
|
|
5808
|
-
createNamespace: dialect === "postgres" ? schemaNamespaces({ schema: params.to }).filter((namespace) => !
|
|
5941
|
+
createNamespace: dialect === "postgres" ? schemaNamespaces({ schema: params.to }).filter((namespace) => !sourceNamespaces.has(namespace)).map((namespace) => ({ kind: "createNamespace", namespace, destructive: false })) : [],
|
|
5809
5942
|
dropExclusion: [],
|
|
5810
5943
|
addExclusion: [],
|
|
5811
5944
|
dropView: [],
|
|
@@ -5833,16 +5966,22 @@ function diffSchemas(params) {
|
|
|
5833
5966
|
diffViews({ from, to, buckets });
|
|
5834
5967
|
diffJoinTables({ from, to, buckets });
|
|
5835
5968
|
diffTables({ from, to, buckets, dialect });
|
|
5969
|
+
const sourceModels = new Map;
|
|
5970
|
+
for (const model of params.from.models) {
|
|
5971
|
+
const key = physicalNameKey({ name: model.dbName ?? model.name, namespace: model.namespace, dialect });
|
|
5972
|
+
if (!sourceModels.has(key))
|
|
5973
|
+
sourceModels.set(key, model);
|
|
5974
|
+
}
|
|
5836
5975
|
for (const model of params.to.models) {
|
|
5837
5976
|
const table = model.dbName ?? model.name;
|
|
5838
5977
|
const placement = model.namespace === undefined ? {} : { namespace: model.namespace };
|
|
5839
|
-
const previous =
|
|
5978
|
+
const previous = sourceModels.get(physicalNameKey({ name: table, namespace: model.namespace, dialect }));
|
|
5840
5979
|
for (const exclusion of model.exclusions ?? []) {
|
|
5841
5980
|
const before = previous?.exclusions?.find((entry) => entry.name === exclusion.name);
|
|
5842
5981
|
if (before?.definition === exclusion.definition)
|
|
5843
5982
|
continue;
|
|
5844
5983
|
if (before?.catalogDefinition)
|
|
5845
|
-
throw new
|
|
5984
|
+
throw new VibeError31({ code: "VIBE_MIGRATION", message: "Exclusion comparison requires normalized evidence", meta: { reason: "exclusion-inspection-unavailable", table, name: exclusion.name } });
|
|
5846
5985
|
if (before !== undefined)
|
|
5847
5986
|
buckets.dropExclusion.push({ kind: "dropExclusion", table, ...placement, exclusion: before, destructive: false });
|
|
5848
5987
|
buckets.addExclusion.push({ kind: "addExclusion", table, ...placement, exclusion, destructive: false });
|
|
@@ -5852,7 +5991,8 @@ function diffSchemas(params) {
|
|
|
5852
5991
|
buckets.dropExclusion.push({ kind: "dropExclusion", table, exclusion, destructive: false });
|
|
5853
5992
|
}
|
|
5854
5993
|
refuseDependentEnumStorageChanges({ from, to, buckets, dialect });
|
|
5855
|
-
|
|
5994
|
+
refuseMysqlForeignKeyColumnRewrites({ from, to, buckets, dialect });
|
|
5995
|
+
return { from, to, steps: withRlsSteps({ from: params.from, to: params.to, steps: [
|
|
5856
5996
|
...buckets.createNamespace,
|
|
5857
5997
|
...renamed.steps,
|
|
5858
5998
|
...buckets.dropView,
|
|
@@ -5876,7 +6016,7 @@ function diffSchemas(params) {
|
|
|
5876
6016
|
...buckets.addForeignKey,
|
|
5877
6017
|
...buckets.createJoinTable,
|
|
5878
6018
|
...buckets.dropEnum
|
|
5879
|
-
] });
|
|
6019
|
+
] }) };
|
|
5880
6020
|
}
|
|
5881
6021
|
function refuseScalarListColumns(params) {
|
|
5882
6022
|
const { schema, dialect } = params;
|
|
@@ -5943,6 +6083,40 @@ function refuseDependentEnumStorageChanges(params) {
|
|
|
5943
6083
|
}
|
|
5944
6084
|
}
|
|
5945
6085
|
}
|
|
6086
|
+
function refuseMysqlForeignKeyColumnRewrites(params) {
|
|
6087
|
+
const { from, to, buckets, dialect } = params;
|
|
6088
|
+
if (dialect !== "mysql")
|
|
6089
|
+
return;
|
|
6090
|
+
const droppedFks = new Set(buckets.dropForeignKey.map((step) => step.kind === "dropForeignKey" ? `${step.table}\x00${step.constraintName}` : ""));
|
|
6091
|
+
for (const step of buckets.alterColumn) {
|
|
6092
|
+
if (step.kind !== "alterColumn")
|
|
6093
|
+
continue;
|
|
6094
|
+
const collation = step.changes.includes("collation");
|
|
6095
|
+
const type = step.changes.includes("type") && !isMysqlVarcharWidening({ from: step.from, to: step.to });
|
|
6096
|
+
if (!collation && !type)
|
|
6097
|
+
continue;
|
|
6098
|
+
const joined = [...from.joinTables].some(([key, join]) => to.joinTables.has(key) && [join.a, join.b].some((end) => end.table === step.table && end.column === step.column));
|
|
6099
|
+
const foreign = [...from.tables].some(([tableKey, table]) => [...table.fks].some(([fkKey, fk]) => to.tables.get(tableKey)?.fks.has(fkKey) === true && !droppedFks.has(`${table.table}\x00${fk.constraintName}`) && (table.table === step.table && fk.columns.includes(step.column) || fk.refTable === step.table && fk.refColumns.includes(step.column))));
|
|
6100
|
+
if (!joined && !foreign)
|
|
6101
|
+
continue;
|
|
6102
|
+
throw unsupportedCapability10({
|
|
6103
|
+
dialect,
|
|
6104
|
+
feature: `${collation ? "collation" : "type"} change of "${step.table}"."${step.column}" ${joined ? "referenced by an implicit many-to-many join table" : "in a foreign key"}`,
|
|
6105
|
+
hint: `drop the foreign key, change both columns' ${collation ? "collation" : "type"} and re-add it in a reviewed manual migration`
|
|
6106
|
+
});
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
function isMysqlVarcharWidening(params) {
|
|
6110
|
+
const length = (field) => {
|
|
6111
|
+
if (isEnumType6(field.type) || field.isList || field.isUnsupported === true)
|
|
6112
|
+
return null;
|
|
6113
|
+
const match = /^varchar\((\d+)\)$/.exec(normalizeMysqlType({ raw: mysqlColumnTypeSql({ field, enums: new Map }) }));
|
|
6114
|
+
return match === null ? null : Number(match[1]);
|
|
6115
|
+
};
|
|
6116
|
+
const before = length(params.from);
|
|
6117
|
+
const after = length(params.to);
|
|
6118
|
+
return before !== null && after !== null && after >= before;
|
|
6119
|
+
}
|
|
5946
6120
|
function diffViews(params) {
|
|
5947
6121
|
const { from, to, buckets } = params;
|
|
5948
6122
|
for (const [name, view] of from.views) {
|
|
@@ -6051,7 +6225,7 @@ function diffOneTable(params) {
|
|
|
6051
6225
|
continue;
|
|
6052
6226
|
}
|
|
6053
6227
|
const changes = [];
|
|
6054
|
-
if (
|
|
6228
|
+
if (columnTypeChanged({ declared: column, actual: before }))
|
|
6055
6229
|
changes.push("type");
|
|
6056
6230
|
if (before.nullable !== column.nullable)
|
|
6057
6231
|
changes.push("nullability");
|
|
@@ -6074,7 +6248,7 @@ function diffOneTable(params) {
|
|
|
6074
6248
|
assertStringEnumLabelsFit({ field: column.field, values, dialect, table });
|
|
6075
6249
|
}
|
|
6076
6250
|
}
|
|
6077
|
-
let narrowingType = changes.includes("type") && !isScalarToArrayChange({ from: before.field, to: column.field }) && !(storageChange && column.field.type === "String");
|
|
6251
|
+
let narrowingType = changes.includes("type") && !isScalarToArrayChange({ from: before.field, to: column.field }) && !(dialect === "postgres" && isTimestamptzPrecisionWidening({ from: before.field, to: column.field })) && !(storageChange && column.field.type === "String");
|
|
6078
6252
|
if (dialect !== "postgres" && changes.includes("type") && isEnumType6(before.field.type) && isEnumType6(column.field.type)) {
|
|
6079
6253
|
const fromValues = resolveEnumValues({ name: before.field.type.enum, enums: fromEnums });
|
|
6080
6254
|
const toValues = resolveEnumValues({ name: column.field.type.enum, enums: toEnums });
|
|
@@ -6098,7 +6272,7 @@ function diffOneTable(params) {
|
|
|
6098
6272
|
from: before.field,
|
|
6099
6273
|
to: column.field,
|
|
6100
6274
|
changes,
|
|
6101
|
-
destructive: narrowingType || collationChanged
|
|
6275
|
+
destructive: narrowingType || collationChanged || dialect === "mysql" && before.nullable && !column.nullable
|
|
6102
6276
|
});
|
|
6103
6277
|
}
|
|
6104
6278
|
for (const [name] of previous.columns) {
|
|
@@ -6106,9 +6280,7 @@ function diffOneTable(params) {
|
|
|
6106
6280
|
buckets.dropColumn.push({ kind: "dropColumn", table, ...placement, column: name, destructive: true });
|
|
6107
6281
|
}
|
|
6108
6282
|
}
|
|
6109
|
-
|
|
6110
|
-
const pkAfter = current.pk.join(",");
|
|
6111
|
-
if (pkBefore !== pkAfter) {
|
|
6283
|
+
if (previous.pk.length !== current.pk.length || previous.pk.some((column, index) => column !== current.pk[index])) {
|
|
6112
6284
|
if (previous.pk.length > 0) {
|
|
6113
6285
|
buckets.dropPrimaryKey.push({ kind: "dropPrimaryKey", table, ...placement, destructive: false, ...previous.model.primaryKeyConstraintName === undefined ? {} : { constraintName: previous.model.primaryKeyConstraintName } });
|
|
6114
6286
|
}
|
|
@@ -6128,7 +6300,7 @@ function diffOneTable(params) {
|
|
|
6128
6300
|
if (before === expression)
|
|
6129
6301
|
continue;
|
|
6130
6302
|
if (before !== undefined && previous.model.checks?.some((check) => check.name === name && check.catalogExpression === true))
|
|
6131
|
-
throw new
|
|
6303
|
+
throw new VibeError31({ 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 } });
|
|
6132
6304
|
if (before !== undefined)
|
|
6133
6305
|
buckets.dropCheck.push({ kind: "dropCheck", table, ...placement, name, destructive: false });
|
|
6134
6306
|
buckets.addCheck.push({ kind: "addCheck", table, ...placement, name, expression, destructive: false });
|
|
@@ -6214,11 +6386,11 @@ function diffOneTable(params) {
|
|
|
6214
6386
|
}
|
|
6215
6387
|
|
|
6216
6388
|
// packages/migrate/src/ddl/render.ts
|
|
6217
|
-
import { VibeError as
|
|
6389
|
+
import { VibeError as VibeError35, sqlObjectKey as sqlObjectKey10 } from "@vibeorm/schema";
|
|
6218
6390
|
|
|
6219
6391
|
// packages/migrate/src/ddl/postgres.ts
|
|
6220
6392
|
import { renderExclusionSetup } from "@vibeorm/sql";
|
|
6221
|
-
import { VibeError as
|
|
6393
|
+
import { VibeError as VibeError32, assertSqlNamespace as assertSqlNamespace3, defaultPrimaryKeyName as defaultPrimaryKeyName3, implicitJoinTableObjectNames as implicitJoinTableObjectNames2, isEnumType as isEnumType7 } from "@vibeorm/schema";
|
|
6222
6394
|
import { postgresDialect as postgresDialect9, renderRlsPolicyPath, renderIndexColumns } from "@vibeorm/sql";
|
|
6223
6395
|
function renderPostgresSteps(params) {
|
|
6224
6396
|
refuseSameTransactionEnumUse({ steps: params.steps });
|
|
@@ -6255,7 +6427,7 @@ function refuseSameTransactionEnumUse(params) {
|
|
|
6255
6427
|
const found = use(field);
|
|
6256
6428
|
if (found === undefined)
|
|
6257
6429
|
continue;
|
|
6258
|
-
throw new
|
|
6430
|
+
throw new VibeError32({
|
|
6259
6431
|
code: "VIBE_MIGRATION",
|
|
6260
6432
|
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`,
|
|
6261
6433
|
meta: { enum: found.enumName, value: found.value, table: step.table, column: field.dbName ?? field.name, rule: "enum-value-used-in-adding-transaction" }
|
|
@@ -6329,7 +6501,7 @@ ${lines.join(`,
|
|
|
6329
6501
|
case "dropIndex":
|
|
6330
6502
|
if (step.constraintName !== undefined) {
|
|
6331
6503
|
if (step.table === undefined)
|
|
6332
|
-
throw new
|
|
6504
|
+
throw new VibeError32({ code: "VIBE_MIGRATION", message: "Constraint-backed index removal requires its owning table" });
|
|
6333
6505
|
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.constraintName)};`];
|
|
6334
6506
|
}
|
|
6335
6507
|
return [`DROP INDEX ${qualifiedName(step.name, step.namespace)};`];
|
|
@@ -6394,7 +6566,7 @@ function columnDefSql(params) {
|
|
|
6394
6566
|
const { field } = params;
|
|
6395
6567
|
const name = q2(field.dbName ?? field.name);
|
|
6396
6568
|
if (field.default?.kind === "autoincrement" && !isIdentityColumn({ field })) {
|
|
6397
|
-
throw new
|
|
6569
|
+
throw new VibeError32({
|
|
6398
6570
|
code: "VIBE_MIGRATION",
|
|
6399
6571
|
message: `autoincrement default on "${field.name}" requires a non-list Int or BigInt column`,
|
|
6400
6572
|
meta: { field: field.name }
|
|
@@ -6430,7 +6602,7 @@ function renderAlterColumn(params) {
|
|
|
6430
6602
|
}
|
|
6431
6603
|
if (step.changes.includes("default") || droppedDefault) {
|
|
6432
6604
|
if (step.to.default?.kind === "autoincrement") {
|
|
6433
|
-
throw new
|
|
6605
|
+
throw new VibeError32({
|
|
6434
6606
|
code: "VIBE_MIGRATION",
|
|
6435
6607
|
message: `cannot add autoincrement to existing column "${step.table}"."${step.column}" \u2014 drop and recreate the column instead`,
|
|
6436
6608
|
meta: { table: step.table, column: step.column }
|
|
@@ -6450,7 +6622,7 @@ function q2(name) {
|
|
|
6450
6622
|
}
|
|
6451
6623
|
|
|
6452
6624
|
// packages/migrate/src/ddl/sqlite.ts
|
|
6453
|
-
import { VibeError as
|
|
6625
|
+
import { VibeError as VibeError33, implicitJoinTableObjectNames as implicitJoinTableObjectNames3, isEnumType as isEnumType8, unsupportedCapability as unsupportedCapability11 } from "@vibeorm/schema";
|
|
6454
6626
|
import { sqliteDialect as sqliteDialect3 } from "@vibeorm/sql";
|
|
6455
6627
|
|
|
6456
6628
|
// packages/migrate/src/ddl/drop-order.ts
|
|
@@ -6598,7 +6770,7 @@ function renderSqliteSteps(params) {
|
|
|
6598
6770
|
continue;
|
|
6599
6771
|
const awaitsNewParent = [...toNorm?.tables.get(table)?.fks.values() ?? []].some((fk) => createdTables.has(fk.refTable));
|
|
6600
6772
|
if (awaitsNewParent) {
|
|
6601
|
-
throw new
|
|
6773
|
+
throw new VibeError33({
|
|
6602
6774
|
code: "VIBE_MIGRATION",
|
|
6603
6775
|
message: `cannot order this sqlite migration safely: rebuilding "${table}" must happen BEFORE the dropped table(s) it references (${[
|
|
6604
6776
|
...droppedTables
|
|
@@ -6627,7 +6799,7 @@ function renderSqliteSteps(params) {
|
|
|
6627
6799
|
switch (step.kind) {
|
|
6628
6800
|
case "addExclusion":
|
|
6629
6801
|
case "dropExclusion":
|
|
6630
|
-
throw new
|
|
6802
|
+
throw new VibeError33({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "sqlite", feature: "exclusionConstraints" } });
|
|
6631
6803
|
case "createEnum":
|
|
6632
6804
|
case "dropEnum":
|
|
6633
6805
|
case "addEnumValue":
|
|
@@ -6713,7 +6885,7 @@ function renderSqliteSteps(params) {
|
|
|
6713
6885
|
case "renameColumn": {
|
|
6714
6886
|
const renamed = step.kind === "renameTable" ? step.from : step.table;
|
|
6715
6887
|
if (rebuildSet.has(renamed) || step.kind === "renameTable" && rebuildSet.has(step.to)) {
|
|
6716
|
-
throw new
|
|
6888
|
+
throw new VibeError33({
|
|
6717
6889
|
code: "VIBE_MIGRATION",
|
|
6718
6890
|
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`,
|
|
6719
6891
|
meta: { reason: "rename-ambiguous", table: renamed, dialect: "sqlite" }
|
|
@@ -6767,7 +6939,7 @@ function classifyRebuilds(params) {
|
|
|
6767
6939
|
switch (step.kind) {
|
|
6768
6940
|
case "addExclusion":
|
|
6769
6941
|
case "dropExclusion":
|
|
6770
|
-
throw new
|
|
6942
|
+
throw new VibeError33({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "sqlite", feature: "exclusionConstraints" } });
|
|
6771
6943
|
case "alterColumn":
|
|
6772
6944
|
case "dropForeignKey":
|
|
6773
6945
|
case "addPrimaryKey":
|
|
@@ -6828,7 +7000,7 @@ function renderTableRebuild(params) {
|
|
|
6828
7000
|
const source = from.tables.get(table);
|
|
6829
7001
|
const target = to.tables.get(table);
|
|
6830
7002
|
if (source === undefined || target === undefined) {
|
|
6831
|
-
throw new
|
|
7003
|
+
throw new VibeError33({
|
|
6832
7004
|
code: "VIBE_MIGRATION",
|
|
6833
7005
|
message: `cannot rebuild "${table}" \u2014 the table is missing from the ${source === undefined ? "from" : "to"} schema`,
|
|
6834
7006
|
meta: { table }
|
|
@@ -6963,7 +7135,7 @@ function columnDefSql2(params) {
|
|
|
6963
7135
|
const { field, constraintTable, enums, inlinePkAutoincrement } = params;
|
|
6964
7136
|
const name = field.dbName ?? field.name;
|
|
6965
7137
|
if (field.default?.kind === "autoincrement" && !inlinePkAutoincrement) {
|
|
6966
|
-
throw new
|
|
7138
|
+
throw new VibeError33({
|
|
6967
7139
|
code: "VIBE_MIGRATION",
|
|
6968
7140
|
message: `autoincrement on sqlite requires "${field.name}" to be the single-column Int or BigInt primary key`,
|
|
6969
7141
|
meta: { field: field.name }
|
|
@@ -6982,7 +7154,7 @@ function columnDefSql2(params) {
|
|
|
6982
7154
|
const enumName = field.type.enum;
|
|
6983
7155
|
const values = resolveEnumValues({ name: enumName, enums });
|
|
6984
7156
|
if (values === undefined) {
|
|
6985
|
-
throw new
|
|
7157
|
+
throw new VibeError33({
|
|
6986
7158
|
code: "VIBE_MIGRATION",
|
|
6987
7159
|
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`,
|
|
6988
7160
|
meta: { field: field.name, enum: enumName }
|
|
@@ -7017,7 +7189,7 @@ function requireSchemas(params) {
|
|
|
7017
7189
|
const { step, fromNorm, toNorm } = params;
|
|
7018
7190
|
if (fromNorm !== undefined && toNorm !== undefined)
|
|
7019
7191
|
return;
|
|
7020
|
-
throw new
|
|
7192
|
+
throw new VibeError33({
|
|
7021
7193
|
code: "VIBE_MIGRATION",
|
|
7022
7194
|
message: `sqlite step "${describeStep({ step })}" requires a table rebuild \u2014 renderSteps needs the { from, to } schemas to construct it`,
|
|
7023
7195
|
meta: { step: step.kind }
|
|
@@ -7028,7 +7200,7 @@ function q3(name) {
|
|
|
7028
7200
|
}
|
|
7029
7201
|
|
|
7030
7202
|
// packages/migrate/src/ddl/mysql.ts
|
|
7031
|
-
import { VibeError as
|
|
7203
|
+
import { VibeError as VibeError34, implicitJoinTableObjectNames as implicitJoinTableObjectNames4, unsupportedCapability as unsupportedCapability12 } from "@vibeorm/schema";
|
|
7032
7204
|
import { mysqlDialect as mysqlDialect3 } from "@vibeorm/sql";
|
|
7033
7205
|
function renderMysqlSteps(params) {
|
|
7034
7206
|
for (const step of params.steps) {
|
|
@@ -7176,12 +7348,12 @@ ${lines.join(`,
|
|
|
7176
7348
|
];
|
|
7177
7349
|
case "addExclusion":
|
|
7178
7350
|
case "dropExclusion":
|
|
7179
|
-
throw new
|
|
7351
|
+
throw new VibeError34({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "mysql", feature: "exclusionConstraints" } });
|
|
7180
7352
|
case "dropCheck":
|
|
7181
7353
|
return [`ALTER TABLE ${q4(step.table)} DROP CHECK ${q4(step.name)};`];
|
|
7182
7354
|
case "dropIndex": {
|
|
7183
7355
|
if (step.table === undefined) {
|
|
7184
|
-
throw new
|
|
7356
|
+
throw new VibeError34({
|
|
7185
7357
|
code: "VIBE_MIGRATION",
|
|
7186
7358
|
message: `dropIndex "${step.name}" carries no table \u2014 mysql's DROP INDEX requires one`,
|
|
7187
7359
|
meta: { index: step.name }
|
|
@@ -7211,8 +7383,8 @@ ${lines.join(`,
|
|
|
7211
7383
|
case "dropForeignKey":
|
|
7212
7384
|
return [`ALTER TABLE ${q4(step.table)} DROP FOREIGN KEY ${q4(step.constraintName)};`];
|
|
7213
7385
|
case "createJoinTable": {
|
|
7214
|
-
const aType =
|
|
7215
|
-
const bType =
|
|
7386
|
+
const aType = joinColumnTypeSql({ field: step.a.field, enums });
|
|
7387
|
+
const bType = joinColumnTypeSql({ field: step.b.field, enums });
|
|
7216
7388
|
const names = implicitJoinTableObjectNames4({ table: step.table });
|
|
7217
7389
|
return [
|
|
7218
7390
|
`CREATE TABLE ${q4(step.table)} (
|
|
@@ -7233,12 +7405,17 @@ ${lines.join(`,
|
|
|
7233
7405
|
return [`ALTER TABLE ${q4(step.table)} DROP PRIMARY KEY;`];
|
|
7234
7406
|
}
|
|
7235
7407
|
}
|
|
7408
|
+
function joinColumnTypeSql(params) {
|
|
7409
|
+
const { field, enums } = params;
|
|
7410
|
+
const type = mysqlColumnTypeSql({ field: { ...field, isList: false }, enums });
|
|
7411
|
+
return field.collation === undefined ? type : `${type} COLLATE ${q4(field.collation)}`;
|
|
7412
|
+
}
|
|
7236
7413
|
function columnDefSql3(params) {
|
|
7237
7414
|
const { field, enums } = params;
|
|
7238
7415
|
const name = q4(field.dbName ?? field.name);
|
|
7239
7416
|
const autoincrement = field.default?.kind === "autoincrement";
|
|
7240
7417
|
if (autoincrement && field.type !== "Int" && field.type !== "BigInt") {
|
|
7241
|
-
throw new
|
|
7418
|
+
throw new VibeError34({
|
|
7242
7419
|
code: "VIBE_MIGRATION",
|
|
7243
7420
|
message: `autoincrement default on "${field.name}" requires a non-list Int or BigInt column`,
|
|
7244
7421
|
meta: { field: field.name }
|
|
@@ -7260,7 +7437,7 @@ function renderAlterColumn2(params) {
|
|
|
7260
7437
|
const { step, enums } = params;
|
|
7261
7438
|
assertEnumStringTransition({ from: step.from, to: step.to, dialect: "mysql", table: step.table, column: step.column });
|
|
7262
7439
|
if (step.to.default?.kind === "autoincrement" && step.from.default?.kind !== "autoincrement") {
|
|
7263
|
-
throw new
|
|
7440
|
+
throw new VibeError34({
|
|
7264
7441
|
code: "VIBE_MIGRATION",
|
|
7265
7442
|
message: `cannot add autoincrement to existing column "${step.table}"."${step.column}" \u2014 drop and recreate the column instead`,
|
|
7266
7443
|
meta: { table: step.table, column: step.column }
|
|
@@ -7300,24 +7477,24 @@ function declaredSqlitePreservation(params) {
|
|
|
7300
7477
|
continue;
|
|
7301
7478
|
const facts = object.expected?.facts;
|
|
7302
7479
|
if (facts === undefined)
|
|
7303
|
-
throw new
|
|
7480
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "SQLite rebuild requires independently declared preservation definitions", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
7304
7481
|
if (identity.kind === "trigger" || identity.kind === "index") {
|
|
7305
7482
|
if (identity.kind === "index" && to.models.some((model) => (model.dbName ?? model.name) === identity.table.name && model.indexes.some((index) => index.name === identity.name)))
|
|
7306
7483
|
continue;
|
|
7307
7484
|
if (typeof facts.definition !== "string")
|
|
7308
|
-
throw new
|
|
7485
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Missing SQLite restoration SQL", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
7309
7486
|
const statements = restore.get(identity.table.name) ?? [];
|
|
7310
7487
|
statements.push(facts.definition);
|
|
7311
7488
|
restore.set(identity.table.name, statements);
|
|
7312
7489
|
} else if (identity.kind === "column" || identity.kind === "generatedColumn") {
|
|
7313
7490
|
if (typeof facts.columnDefinition !== "string")
|
|
7314
|
-
throw new
|
|
7491
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Missing SQLite column clause", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
7315
7492
|
const fields = columns.get(identity.table.name) ?? new Map;
|
|
7316
7493
|
fields.set(identity.column, { sql: facts.columnDefinition, generated: Number(facts.generated) > 1 });
|
|
7317
7494
|
columns.set(identity.table.name, fields);
|
|
7318
7495
|
} else if (identity.kind === "columnDefault") {
|
|
7319
7496
|
if (facts.definition !== null && typeof facts.definition !== "string")
|
|
7320
|
-
throw new
|
|
7497
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Missing SQLite default clause", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
7321
7498
|
const expression = facts.definition;
|
|
7322
7499
|
to = { ...to, models: to.models.map((model) => (model.dbName ?? model.name) !== identity.table.name ? model : { ...model, fields: model.fields.map((field) => {
|
|
7323
7500
|
if ((field.dbName ?? field.name) !== identity.column)
|
|
@@ -7354,7 +7531,7 @@ function renderSteps(params) {
|
|
|
7354
7531
|
}
|
|
7355
7532
|
|
|
7356
7533
|
// packages/migrate/src/extensions.ts
|
|
7357
|
-
import { extensionDeclarations } from "@vibeorm/schema";
|
|
7534
|
+
import { extensionDeclarations, providerToDialect as providerToDialect4, qualifiedSqlName as qualifiedSqlName10, SQL_NAMESPACES } from "@vibeorm/schema";
|
|
7358
7535
|
function collectExtensionArtifacts(params) {
|
|
7359
7536
|
const { schema, dialect, extensions } = params;
|
|
7360
7537
|
const setup = [];
|
|
@@ -7421,6 +7598,10 @@ function stripExtensionArtifacts(params) {
|
|
|
7421
7598
|
const models = [];
|
|
7422
7599
|
for (const model of schema.models) {
|
|
7423
7600
|
const tableName = model.dbName ?? model.name;
|
|
7601
|
+
if (providerToDialect4({ provider: schema.datasource.provider }) === "postgres" && qualifiedSqlName10({ name: tableName, namespace: model.namespace }).namespace !== SQL_NAMESPACES.postgres) {
|
|
7602
|
+
models.push(model);
|
|
7603
|
+
continue;
|
|
7604
|
+
}
|
|
7424
7605
|
if (tables.exact.has(tableName) || tables.prefixes.some((prefix) => tableName.startsWith(prefix))) {
|
|
7425
7606
|
continue;
|
|
7426
7607
|
}
|
|
@@ -7444,6 +7625,8 @@ function planMissingArtifacts(params) {
|
|
|
7444
7625
|
return [];
|
|
7445
7626
|
const modelByTable = new Map;
|
|
7446
7627
|
for (const model of introspected.models) {
|
|
7628
|
+
if (providerToDialect4({ provider: introspected.datasource.provider }) === "postgres" && qualifiedSqlName10({ name: model.dbName ?? model.name, namespace: model.namespace }).namespace !== SQL_NAMESPACES.postgres)
|
|
7629
|
+
continue;
|
|
7447
7630
|
modelByTable.set(model.dbName ?? model.name, model);
|
|
7448
7631
|
}
|
|
7449
7632
|
return artifacts.filter((artifact) => {
|
|
@@ -7602,10 +7785,10 @@ function assertNativeMigrationMetadata(params) {
|
|
|
7602
7785
|
if (!native)
|
|
7603
7786
|
return;
|
|
7604
7787
|
if (!DIALECT_CAPABILITIES3[params.dialect].nativeRowLevelSecurity) {
|
|
7605
|
-
throw new
|
|
7788
|
+
throw new VibeError36({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Native RLS migrations are unsupported on ${params.dialect}`, meta: { feature: "nativeRowLevelSecurity", dialect: params.dialect } });
|
|
7606
7789
|
}
|
|
7607
7790
|
if (params.migration.rls === undefined) {
|
|
7608
|
-
throw new
|
|
7791
|
+
throw new VibeError36({ 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 } });
|
|
7609
7792
|
}
|
|
7610
7793
|
}
|
|
7611
7794
|
var PG_ADVISORY_LOCK_KEY = 1447645253;
|
|
@@ -7686,12 +7869,12 @@ function generateMigration(params) {
|
|
|
7686
7869
|
}),
|
|
7687
7870
|
...orderedMigrationObjects({ objects: to.migrationObjects ?? [] }).flatMap((object) => {
|
|
7688
7871
|
if (object.dialect !== dialect)
|
|
7689
|
-
throw new
|
|
7872
|
+
throw new VibeError36({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Migration object ${object.id} requires ${object.dialect}` });
|
|
7690
7873
|
const previous = (params.desiredState?.from.schema.migrationObjects ?? from.migrationObjects)?.find((entry) => entry.id === object.id);
|
|
7691
7874
|
if (previous === undefined)
|
|
7692
7875
|
return [...object.create];
|
|
7693
7876
|
if (JSON.stringify(previous) !== JSON.stringify(object))
|
|
7694
|
-
throw new
|
|
7877
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Migration-owned object ${object.id} changed; supply a reviewed forward SQL migration`, meta: { reason: "migration-object-replacement-required", shadowReusable: true, id: object.id } });
|
|
7695
7878
|
return [];
|
|
7696
7879
|
})
|
|
7697
7880
|
];
|
|
@@ -7717,8 +7900,8 @@ async function withMigrationLock(params) {
|
|
|
7717
7900
|
}, (cause) => releaseLock({ executor, dialect: params.dialect }).then(() => {
|
|
7718
7901
|
throw cause;
|
|
7719
7902
|
}, (cleanupCause) => {
|
|
7720
|
-
if (cause instanceof
|
|
7721
|
-
throw new
|
|
7903
|
+
if (cause instanceof VibeError36)
|
|
7904
|
+
throw new VibeError36({ code: cause.code, message: cause.message, meta: { ...cause.meta, cleanupCause }, cause });
|
|
7722
7905
|
throw cause;
|
|
7723
7906
|
}));
|
|
7724
7907
|
};
|
|
@@ -7748,11 +7931,11 @@ function assertMigrationMetadata(params) {
|
|
|
7748
7931
|
assertModuleOwnership({ origin: migration.moduleOrigin, desiredState: migration.desiredState });
|
|
7749
7932
|
}
|
|
7750
7933
|
if (migration.desiredStatePending === true)
|
|
7751
|
-
throw new
|
|
7934
|
+
throw new VibeError36({ 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 } });
|
|
7752
7935
|
if (migration.desiredState !== undefined) {
|
|
7753
7936
|
assertDesiredState({ value: migration.desiredState, migration: migration.name });
|
|
7754
7937
|
if (migration.desiredState.dialect !== dialect)
|
|
7755
|
-
throw new
|
|
7938
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Migration desired-state dialect mismatch", meta: { reason: "desired-state-dialect-mismatch", migration: migration.name } });
|
|
7756
7939
|
}
|
|
7757
7940
|
assertNativeMigrationMetadata({ migration, sql: migration.sql, dialect });
|
|
7758
7941
|
assertExecutable({ migration, execution: migration.execution, sql: migration.sql, dialect });
|
|
@@ -7762,7 +7945,7 @@ async function applyMigrations(params) {
|
|
|
7762
7945
|
const dialect = params.dialect ?? "postgres";
|
|
7763
7946
|
const trustedReplay = isShadowReplayExecutor({ executor });
|
|
7764
7947
|
if (params.history !== undefined && params.history !== "complete" && params.history !== "subset")
|
|
7765
|
-
throw new
|
|
7948
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Unknown migration history validation mode", meta: { reason: "history-mode-invalid" } });
|
|
7766
7949
|
if (params.history === "complete")
|
|
7767
7950
|
assertDistinctMigrationNames({ migrations });
|
|
7768
7951
|
for (const migration of migrations)
|
|
@@ -7778,7 +7961,7 @@ async function applyMigrations(params) {
|
|
|
7778
7961
|
assertModuleUpgradeHistory({ migrations, records: moduleRecords });
|
|
7779
7962
|
const namespace = await migrationNamespace({ executor, dialect });
|
|
7780
7963
|
if (dialect !== "postgres" && migrations.some((migration) => migration.moduleOrigin !== undefined && moduleNamespace({ origin: migration.moduleOrigin }) !== namespace))
|
|
7781
|
-
throw new
|
|
7964
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Module placement differs from the active database namespace", meta: { reason: "module-placement-mismatch" } });
|
|
7782
7965
|
}
|
|
7783
7966
|
if (params.history === "complete")
|
|
7784
7967
|
assertCompleteHistory({ migrations, recorded, dialect });
|
|
@@ -7789,7 +7972,7 @@ async function applyMigrations(params) {
|
|
|
7789
7972
|
});
|
|
7790
7973
|
const adopted = pending.find((migration) => migration.moduleAdoption !== undefined);
|
|
7791
7974
|
if (adopted !== undefined && !trustedReplay)
|
|
7792
|
-
throw new
|
|
7975
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "An adopted source must be recorded by the verified adoption workflow, never executed by ordinary apply", meta: { reason: "module-adoption-recording-required", migration: adopted.name } });
|
|
7793
7976
|
if (external !== undefined && !trustedReplay)
|
|
7794
7977
|
for (const migration of pending)
|
|
7795
7978
|
await assertExternalMigrationMutation({ executor, migration, dialect, namespace: await migrationNamespace({ executor, dialect }), external });
|
|
@@ -7805,7 +7988,7 @@ async function applyMigrations(params) {
|
|
|
7805
7988
|
await assertLiveModuleOwnership({ executor, origin: migration.moduleOrigin, resume: stored !== undefined });
|
|
7806
7989
|
if (stored !== undefined) {
|
|
7807
7990
|
if (stored.statementIndex < 0) {
|
|
7808
|
-
throw new
|
|
7991
|
+
throw new VibeError36({
|
|
7809
7992
|
code: "VIBE_MIGRATION",
|
|
7810
7993
|
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`,
|
|
7811
7994
|
meta: { migration: migration.name, statementIndex: stored.statementIndex }
|
|
@@ -7963,7 +8146,7 @@ async function repairMigration(params) {
|
|
|
7963
8146
|
const { executor, name, dialect } = params;
|
|
7964
8147
|
const moduleRecord = (await readModuleJournal({ executor, dialect })).get(name);
|
|
7965
8148
|
if (moduleRecord?.entry !== undefined)
|
|
7966
|
-
throw new
|
|
8149
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Module history cannot be marked applied or deleted without verified module recovery", meta: { reason: "module-resolution-required", moduleId: moduleRecord.entry.migration.moduleOrigin.moduleId } });
|
|
7967
8150
|
await ensureMigrationsTable({ executor, dialect });
|
|
7968
8151
|
if (params.as === "applied") {
|
|
7969
8152
|
const insert = `INSERT INTO ${q5(dialect, MIGRATIONS_TABLE_NAME)} (${q5(dialect, "name")}, ${q5(dialect, "checksum")}, ${q5(dialect, "statement_index")}) VALUES (${ph(dialect, 1)}, ${ph(dialect, 2)}, ${RESOLVED_STATEMENT_INDEX})`;
|
|
@@ -7985,7 +8168,7 @@ async function rollbackMigration(params) {
|
|
|
7985
8168
|
const stored = assertRollbackRecord({ migration, stored: recorded.get(migration.name), dialect });
|
|
7986
8169
|
const newest = [...recorded.keys()].at(-1);
|
|
7987
8170
|
if (newest !== migration.name)
|
|
7988
|
-
throw new
|
|
8171
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Migration "${migration.name}" is not the newest recorded migration; select rollback again`, meta: { reason: "rollback-not-latest", migration: migration.name, newest } });
|
|
7989
8172
|
await validateRollbackRecovery({ executor, migration, downSql, stored, dialect });
|
|
7990
8173
|
await executeRollback({ ...params, executor, dialect, downSql, stored });
|
|
7991
8174
|
} });
|
|
@@ -8003,7 +8186,7 @@ async function rollbackMigrations(params) {
|
|
|
8003
8186
|
const target = journal.findIndex((row) => row.name === params.to);
|
|
8004
8187
|
const row = journal[target];
|
|
8005
8188
|
if (row?.state !== "applied")
|
|
8006
|
-
throw new
|
|
8189
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Rollback target "${params.to}" is not a known applied migration`, meta: { reason: "rollback-target-invalid", to: params.to, state: row?.state } });
|
|
8007
8190
|
assertMigrationMetadata({ migration: byName.get(row.name), dialect });
|
|
8008
8191
|
start = target + 1;
|
|
8009
8192
|
}
|
|
@@ -8011,7 +8194,7 @@ async function rollbackMigrations(params) {
|
|
|
8011
8194
|
for (const row of journal.slice(start).reverse()) {
|
|
8012
8195
|
const migration = byName.get(row.name);
|
|
8013
8196
|
if (migration === undefined)
|
|
8014
|
-
throw new
|
|
8197
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Cannot roll back "${row.name}" without its migration file`, meta: { reason: "rollback-file-missing", migration: row.name } });
|
|
8015
8198
|
const stored = assertRollbackRecord({ migration, stored: recorded.get(row.name), dialect });
|
|
8016
8199
|
const downSql = validateRollbackFile({ ...params, migration, dialect });
|
|
8017
8200
|
plan.push({ migration, downSql, stored });
|
|
@@ -8030,21 +8213,21 @@ function validateRollbackFile(params) {
|
|
|
8030
8213
|
const { migration, dialect } = params;
|
|
8031
8214
|
assertModuleAdoption({ migration });
|
|
8032
8215
|
if (migration.moduleAdoption !== undefined)
|
|
8033
|
-
throw new
|
|
8216
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Reversal across verified module adoption is refused; it would execute destructive source down SQL against pre-existing data", meta: { reason: "module-adoption-reverse-refused", migration: migration.name } });
|
|
8034
8217
|
if (migration.desiredStatePending === true)
|
|
8035
|
-
throw new
|
|
8218
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Unfinalized desired state cannot be rolled back", meta: { reason: "desired-state-unfinalized", migration: migration.name } });
|
|
8036
8219
|
if (migration.moduleOrigin !== undefined) {
|
|
8037
8220
|
assertMaterializedModuleMigration({ migration });
|
|
8038
8221
|
assertModuleOwnership({ origin: migration.moduleOrigin, desiredState: migration.desiredState });
|
|
8039
8222
|
}
|
|
8040
8223
|
const downSql = migration.downSql;
|
|
8041
8224
|
if (downSql === undefined)
|
|
8042
|
-
throw new
|
|
8225
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `migration "${migration.name}" has no down.sql; roll it back manually, then repair with resolveMigration (as: "rolled-back")`, meta: { reason: "rollback-down-missing", migration: migration.name } });
|
|
8043
8226
|
assertProbeMode({ dialect, probe: params.probe });
|
|
8044
8227
|
if (migration.desiredState !== undefined) {
|
|
8045
8228
|
assertDesiredState({ value: migration.desiredState, migration: migration.name });
|
|
8046
8229
|
if (migration.desiredState.dialect !== dialect)
|
|
8047
|
-
throw new
|
|
8230
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Migration desired-state dialect mismatch", meta: { reason: "desired-state-dialect-mismatch" } });
|
|
8048
8231
|
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 } });
|
|
8049
8232
|
}
|
|
8050
8233
|
assertNativeMigrationMetadata({ migration, sql: downSql, dialect });
|
|
@@ -8057,10 +8240,10 @@ function validateRollbackFile(params) {
|
|
|
8057
8240
|
function assertRollbackRecord(params) {
|
|
8058
8241
|
const { migration, stored, dialect } = params;
|
|
8059
8242
|
if (stored === undefined)
|
|
8060
|
-
throw new
|
|
8243
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `migration "${migration.name}" is not recorded as applied; nothing to roll back`, meta: { migration: migration.name } });
|
|
8061
8244
|
assertChecksumMatches({ migration, stored: stored.checksum });
|
|
8062
8245
|
if (stored.statementIndex >= 0 && (!isCompleteRecord({ dialect, migration, stored }) || stored.error !== undefined))
|
|
8063
|
-
throw new
|
|
8246
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `migration "${migration.name}" is only partially applied or has an unresolved error; its down mirrors the FULL up. Finish or repair with resolveMigration`, meta: { reason: "rollback-partial-up", migration: migration.name, statementIndex: stored.statementIndex } });
|
|
8064
8247
|
return stored;
|
|
8065
8248
|
}
|
|
8066
8249
|
async function validateRollbackRecovery(params) {
|
|
@@ -8069,7 +8252,7 @@ async function validateRollbackRecovery(params) {
|
|
|
8069
8252
|
return;
|
|
8070
8253
|
const start = -stored.statementIndex - 1;
|
|
8071
8254
|
if (start > downSql.length)
|
|
8072
|
-
throw new
|
|
8255
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Rollback progress exceeds its statement count", meta: { reason: "rollback-progress-invalid", migration: migration.name } });
|
|
8073
8256
|
if (dialect === "sqlite" && usesSelfManagedTransactions({ sql: downSql }))
|
|
8074
8257
|
assertSelfManagedResumeSafe({ sql: downSql, start, migration: migration.name, direction: "down" });
|
|
8075
8258
|
if (!marksInflight({ dialect, execution: migration.downExecution }) || start >= downSql.length)
|
|
@@ -8135,7 +8318,7 @@ async function runTransactionalDown(params) {
|
|
|
8135
8318
|
} else {
|
|
8136
8319
|
await cleanupSelfManagedFailure({ executor });
|
|
8137
8320
|
}
|
|
8138
|
-
throw new
|
|
8321
|
+
throw new VibeError36({
|
|
8139
8322
|
code: "VIBE_MIGRATION",
|
|
8140
8323
|
message: `rollback of "${migration.name}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
8141
8324
|
meta: { migration: migration.name, ...timeoutDiagnostic({ error }) },
|
|
@@ -8218,7 +8401,7 @@ async function runNonTransactionalDown(params) {
|
|
|
8218
8401
|
values: [marks ? `${INFLIGHT_PREFIX}${index}` : error instanceof Error ? error.message : String(error), migration.name]
|
|
8219
8402
|
});
|
|
8220
8403
|
} catch {}
|
|
8221
|
-
throw new
|
|
8404
|
+
throw new VibeError36({
|
|
8222
8405
|
code: "VIBE_MIGRATION",
|
|
8223
8406
|
message: `rollback of "${migration.name}" failed at down statement ${index + 1} of ${downSql.length}: ${error instanceof Error ? error.message : String(error)}. ${marks ? "The statement outcome is unknown; inspect its effects before resolving or retrying." : "Progress is recorded; re-running the rollback resumes from this statement."}`,
|
|
8224
8407
|
meta: { migration: migration.name, statementIndex: index, ...marks ? { outcome: "unknown" } : {}, ...timeoutDiagnostic({ error }) },
|
|
@@ -8280,7 +8463,7 @@ async function runTransactional(params) {
|
|
|
8280
8463
|
} else {
|
|
8281
8464
|
await cleanupSelfManagedFailure({ executor });
|
|
8282
8465
|
}
|
|
8283
|
-
throw new
|
|
8466
|
+
throw new VibeError36({
|
|
8284
8467
|
code: "VIBE_MIGRATION",
|
|
8285
8468
|
message: `migration "${migration.name}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
8286
8469
|
meta: { migration: migration.name },
|
|
@@ -8376,7 +8559,7 @@ async function runNonTransactional(params) {
|
|
|
8376
8559
|
} catch {}
|
|
8377
8560
|
const sqlState = sqlStateOf(error);
|
|
8378
8561
|
const timeout = sqlState === "55P03" ? "lock-timeout" : sqlState === "57014" ? "statement-timeout" : undefined;
|
|
8379
|
-
throw new
|
|
8562
|
+
throw new VibeError36({
|
|
8380
8563
|
code: "VIBE_MIGRATION",
|
|
8381
8564
|
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). Another session holds a conflicting lock; inspect the declared operation before retrying.` : 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). Inspect the declared operation before retrying.` : `migration "${migration.name}" failed at statement ${index + 1} of ${migration.sql.length}: ${error instanceof Error ? error.message : String(error)}. ${marks ? "The statement outcome is unknown; inspect its effects before resolving or retrying." : "Progress is recorded; re-running applyMigrations resumes from this statement."}`,
|
|
8382
8565
|
meta: { migration: migration.name, statementIndex: index, ...marks ? { outcome: "unknown" } : {}, ...timeout === undefined ? {} : { reason: timeout, sqlState } },
|
|
@@ -8404,7 +8587,7 @@ async function acquireLock(params) {
|
|
|
8404
8587
|
});
|
|
8405
8588
|
const acquired = Number(rows[0]?.vibe_lock) === 1;
|
|
8406
8589
|
if (!acquired) {
|
|
8407
|
-
throw new
|
|
8590
|
+
throw new VibeError36({
|
|
8408
8591
|
code: "VIBE_MIGRATION",
|
|
8409
8592
|
message: `could not acquire the migration lock ('${MYSQL_LOCK_NAME}') within ${MYSQL_LOCK_TIMEOUT_SECONDS}s \u2014 another migration run is in progress`,
|
|
8410
8593
|
meta: { lock: MYSQL_LOCK_NAME }
|
|
@@ -8439,7 +8622,7 @@ function uncertainStatement(params) {
|
|
|
8439
8622
|
const subject = direction === "down" ? `rollback of "${migration}"` : `migration "${migration}"`;
|
|
8440
8623
|
const position = `${direction === "down" ? "down statement" : "statement"} ${index + 1} of ${statements.length}`;
|
|
8441
8624
|
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.";
|
|
8442
|
-
return new
|
|
8625
|
+
return new VibeError36({
|
|
8443
8626
|
code: "VIBE_MIGRATION",
|
|
8444
8627
|
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)}`,
|
|
8445
8628
|
meta: {
|
|
@@ -8457,7 +8640,7 @@ function assertExecutable(params) {
|
|
|
8457
8640
|
if (execution === undefined || isDefaultExecution({ execution }))
|
|
8458
8641
|
return;
|
|
8459
8642
|
if (dialect !== "postgres") {
|
|
8460
|
-
throw new
|
|
8643
|
+
throw new VibeError36({
|
|
8461
8644
|
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
8462
8645
|
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`,
|
|
8463
8646
|
meta: { reason: "nontransactional-unsupported", dialect, migration: migration.name, feature: "nontransactionalMigrations" }
|
|
@@ -8533,7 +8716,7 @@ function assertChecksumMatches(params) {
|
|
|
8533
8716
|
const actual = checksumOf({ migration });
|
|
8534
8717
|
if (stored === actual)
|
|
8535
8718
|
return;
|
|
8536
|
-
throw new
|
|
8719
|
+
throw new VibeError36({
|
|
8537
8720
|
code: "VIBE_MIGRATION",
|
|
8538
8721
|
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)`,
|
|
8539
8722
|
meta: { migration: migration.name, recorded: stored, current: actual }
|
|
@@ -8544,7 +8727,7 @@ function assertResumable(params) {
|
|
|
8544
8727
|
const actual = checksumOf({ migration });
|
|
8545
8728
|
if (stored.checksum === actual)
|
|
8546
8729
|
return;
|
|
8547
|
-
throw new
|
|
8730
|
+
throw new VibeError36({
|
|
8548
8731
|
code: "VIBE_MIGRATION",
|
|
8549
8732
|
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`,
|
|
8550
8733
|
meta: {
|
|
@@ -8564,7 +8747,7 @@ function assertDistinctMigrationNames(params) {
|
|
|
8564
8747
|
const names = new Set;
|
|
8565
8748
|
for (const migration of params.migrations) {
|
|
8566
8749
|
if (names.has(migration.name))
|
|
8567
|
-
throw new
|
|
8750
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Duplicate migration file identity "${migration.name}"`, meta: { reason: "history-duplicate-file", migration: migration.name } });
|
|
8568
8751
|
names.add(migration.name);
|
|
8569
8752
|
}
|
|
8570
8753
|
}
|
|
@@ -8572,15 +8755,15 @@ function assertCompleteHistory(params) {
|
|
|
8572
8755
|
const byName = new Map(params.migrations.map((migration) => [migration.name, migration]));
|
|
8573
8756
|
for (const name of params.recorded.keys()) {
|
|
8574
8757
|
if (!byName.has(name))
|
|
8575
|
-
throw new
|
|
8758
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Recorded migration "${name}" has no file in the complete chain`, meta: { reason: "history-unknown-file", migration: name } });
|
|
8576
8759
|
}
|
|
8577
8760
|
let index = 0;
|
|
8578
8761
|
for (const [name, stored] of params.recorded) {
|
|
8579
8762
|
const migration = params.migrations[index];
|
|
8580
8763
|
if (migration?.name !== name)
|
|
8581
|
-
throw new
|
|
8764
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Recorded migration order is not a prefix of the supplied complete chain", meta: { reason: "history-order-conflict", migration: name, expected: migration?.name, position: index } });
|
|
8582
8765
|
if (stored.statementIndex >= 0 && !isCompleteRecord({ dialect: params.dialect, migration, stored }) && index !== params.recorded.size - 1)
|
|
8583
|
-
throw new
|
|
8766
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Partial migration "${name}" precedes later recorded work`, meta: { reason: "history-partial-not-last", migration: name } });
|
|
8584
8767
|
index++;
|
|
8585
8768
|
}
|
|
8586
8769
|
}
|
|
@@ -8589,16 +8772,16 @@ async function assertApplyHistory(params) {
|
|
|
8589
8772
|
const byName = new Map(migrations.map((migration) => [migration.name, migration]));
|
|
8590
8773
|
for (const [name, stored] of recorded) {
|
|
8591
8774
|
if (stored.statementIndex < 0) {
|
|
8592
|
-
throw new
|
|
8775
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Migration "${name}" is mid-rollback; finish or resolve it before applying`, meta: { reason: "history-rolling-back", migration: name, statementIndex: stored.statementIndex } });
|
|
8593
8776
|
}
|
|
8594
8777
|
const migration = byName.get(name);
|
|
8595
8778
|
if (migration === undefined) {
|
|
8596
8779
|
if (stored.error !== undefined)
|
|
8597
|
-
throw new
|
|
8780
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Migration "${name}" has an unresolved journal error but no supplied file`, meta: { reason: "history-unknown-dirty", migration: name } });
|
|
8598
8781
|
continue;
|
|
8599
8782
|
}
|
|
8600
8783
|
if (isCompleteRecord({ dialect, migration, stored }) && stored.error !== undefined)
|
|
8601
|
-
throw new
|
|
8784
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Migration "${name}" has an unresolved journal error`, meta: { reason: "history-dirty", migration: name } });
|
|
8602
8785
|
if (isCompleteRecord({ dialect, migration, stored }))
|
|
8603
8786
|
assertChecksumMatches({ migration, stored: stored.checksum });
|
|
8604
8787
|
else
|
|
@@ -8632,17 +8815,17 @@ function ph(dialect, index) {
|
|
|
8632
8815
|
// packages/migrate/src/module-bundle.ts
|
|
8633
8816
|
function defineModuleMigrations(params) {
|
|
8634
8817
|
if (!Array.isArray(params.steps) || params.steps.length === 0)
|
|
8635
|
-
throw new
|
|
8818
|
+
throw new VibeError37({ code: "VIBE_MIGRATION", message: "A module upgrade bundle must contain its initial revision", meta: { reason: "module-chain-invalid", moduleId: params.moduleId } });
|
|
8636
8819
|
const migrations = [];
|
|
8637
8820
|
const revisions = new Set;
|
|
8638
8821
|
let previous = null;
|
|
8639
8822
|
for (const step of params.steps) {
|
|
8640
8823
|
if (step.previousRevision !== previous || revisions.has(step.revision))
|
|
8641
|
-
throw new
|
|
8824
|
+
throw new VibeError37({ code: "VIBE_MIGRATION", message: `Module ${params.moduleId} has a duplicate revision or broken predecessor chain`, meta: { reason: "module-chain-invalid", moduleId: params.moduleId, revision: step.revision } });
|
|
8642
8825
|
const origin = createModuleOrigin({ moduleId: params.moduleId, dialect: params.dialect, revision: step.revision, previousRevision: step.previousRevision, contracts: step.contracts, owned: step.owned, source: step.migration });
|
|
8643
8826
|
const predecessor = migrations.at(-1)?.moduleOrigin;
|
|
8644
8827
|
if (predecessor !== undefined && canonicalMigrationJson({ value: predecessor.source.desiredState.to }) !== canonicalMigrationJson({ value: origin.source.desiredState.from }))
|
|
8645
|
-
throw new
|
|
8828
|
+
throw new VibeError37({ code: "VIBE_MIGRATION", message: "Module source state differs from its predecessor", meta: { reason: "module-predecessor-state", moduleId: params.moduleId } });
|
|
8646
8829
|
validateMigrationReplay({ migrations: [origin.source], dialect: params.dialect, acceptSecurityChanges: true, acceptIntegrityLoss: true, removeUnmanaged: origin.source.desiredState?.unmanagedRemovals ?? [] });
|
|
8647
8830
|
migrations.push(Object.freeze({ ...origin.source, moduleOrigin: origin }));
|
|
8648
8831
|
revisions.add(step.revision);
|
|
@@ -8652,21 +8835,21 @@ function defineModuleMigrations(params) {
|
|
|
8652
8835
|
}
|
|
8653
8836
|
function moduleMigrationFiles(params) {
|
|
8654
8837
|
if (params.bundle === null || typeof params.bundle !== "object" || !Array.isArray(params.bundle.migrations))
|
|
8655
|
-
throw new
|
|
8838
|
+
throw new VibeError37({ code: "VIBE_MIGRATION", message: "Module bundle must contain an immutable migration chain", meta: { reason: "module-chain-invalid" } });
|
|
8656
8839
|
if (!/^[A-Za-z0-9_-]+$/.test(params.prefix))
|
|
8657
|
-
throw new
|
|
8840
|
+
throw new VibeError37({ code: "VIBE_MIGRATION", message: "Module migration prefix must be a safe local directory name", meta: { reason: "module-prefix-invalid" } });
|
|
8658
8841
|
const bundle = defineModuleMigrations({ moduleId: params.bundle.moduleId, dialect: params.bundle.dialect, steps: params.bundle.migrations.map((migration) => {
|
|
8659
8842
|
const origin = migration?.moduleOrigin;
|
|
8660
8843
|
if (migration.moduleAdoption !== undefined)
|
|
8661
|
-
throw new
|
|
8844
|
+
throw new VibeError37({ code: "VIBE_MIGRATION", message: "Local adoption provenance cannot be exported as immutable package source", meta: { reason: "module-adoption-author-source" } });
|
|
8662
8845
|
if (origin === undefined || origin.moduleId !== params.bundle.moduleId || origin.dialect !== params.bundle.dialect)
|
|
8663
|
-
throw new
|
|
8846
|
+
throw new VibeError37({ code: "VIBE_MIGRATION", message: "Module bundle lacks matching immutable origin metadata", meta: { reason: "module-origin-invalid" } });
|
|
8664
8847
|
assertModuleOrigin({ value: origin });
|
|
8665
8848
|
restoreModuleMigration({ migration, origin });
|
|
8666
8849
|
const { name: localName, sql: localSql, downSql: localDown, moduleOrigin: localOrigin, ...actual } = migration;
|
|
8667
8850
|
const { name: sourceName, sql: sourceSql, downSql: sourceDown, ...expected } = origin.source;
|
|
8668
8851
|
if (localSql === undefined || localDown === undefined !== (sourceDown === undefined) || canonicalMigrationJson({ value: actual }) !== canonicalMigrationJson({ value: expected }))
|
|
8669
|
-
throw new
|
|
8852
|
+
throw new VibeError37({ code: "VIBE_MIGRATION", message: "Module bundle metadata differs from its sealed author source", meta: { reason: "module-origin-invalid", moduleId: origin.moduleId } });
|
|
8670
8853
|
return { revision: origin.revision, previousRevision: origin.previousRevision, contracts: origin.contracts, owned: origin.owned, migration: origin.source };
|
|
8671
8854
|
}) });
|
|
8672
8855
|
return Object.freeze(bundle.migrations.map((migration, index) => Object.freeze({ ...migration, name: `${params.prefix}_${String(index + 1).padStart(4, "0")}` })));
|
|
@@ -8674,10 +8857,10 @@ function moduleMigrationFiles(params) {
|
|
|
8674
8857
|
|
|
8675
8858
|
// packages/migrate/src/module-pending.ts
|
|
8676
8859
|
function refusal3(params) {
|
|
8677
|
-
return new
|
|
8860
|
+
return new VibeError38({ code: "VIBE_MIGRATION", message: "Module registration or immutable upgrade history conflicts with the application's declarations; review the registration and deployment sequence", meta: params });
|
|
8678
8861
|
}
|
|
8679
8862
|
function physicalKey(params) {
|
|
8680
|
-
return sqlObjectKey11({ identity: { kind: "table", name:
|
|
8863
|
+
return sqlObjectKey11({ identity: { kind: "table", name: qualifiedSqlName11({ name: params.entry.dbName ?? params.entry.name, namespace: params.entry.namespace }) } });
|
|
8681
8864
|
}
|
|
8682
8865
|
function composeModuleState(params) {
|
|
8683
8866
|
const { current, from, to } = params;
|
|
@@ -8788,7 +8971,7 @@ function pendingModuleMigrations(params) {
|
|
|
8788
8971
|
|
|
8789
8972
|
// packages/migrate/src/replay-reference.ts
|
|
8790
8973
|
import { createHash as createHash3 } from "crypto";
|
|
8791
|
-
import { VibeError as
|
|
8974
|
+
import { VibeError as VibeError39, sqlObjectKey as sqlObjectKey12, sqlObjectNamespace as sqlObjectNamespace4 } from "@vibeorm/schema";
|
|
8792
8975
|
import { getSqlDialect as getSqlDialect10, renderMigrationServerVersion, renderReplayCoverageGaps as renderReplayCoverageGaps2, renderShadowOperation as renderShadowOperation2, SHADOW_DATABASE } from "@vibeorm/sql";
|
|
8793
8976
|
function migrationReferenceNamespaces(params) {
|
|
8794
8977
|
return [...new Set([params.namespace, ...params.migrations.flatMap((migration) => [
|
|
@@ -8804,7 +8987,7 @@ async function captureReplayReference(params) {
|
|
|
8804
8987
|
for (const origin of params.namespaceOrigins ?? []) {
|
|
8805
8988
|
assertModuleOrigin({ value: origin });
|
|
8806
8989
|
if (origin.dialect !== dialect)
|
|
8807
|
-
throw new
|
|
8990
|
+
throw new VibeError39({ code: "VIBE_MIGRATION", message: "Namespace context origin uses a different dialect", meta: { reason: "module-dialect-mismatch", moduleId: origin.moduleId } });
|
|
8808
8991
|
}
|
|
8809
8992
|
const namespace = await migrationNamespace({ executor, dialect });
|
|
8810
8993
|
await assertCatalogVisibility({ executor, dialect, namespace });
|
|
@@ -8813,17 +8996,17 @@ async function captureReplayReference(params) {
|
|
|
8813
8996
|
const [query] = renderShadowOperation2({ dialect: sqlDialect, operation: { kind: "markerColumns" } });
|
|
8814
8997
|
const columns = await executor({ text: query.text, values: [...query.values] });
|
|
8815
8998
|
if (columns.length !== 1 || columns[0]?.name !== "payload" || columns[0]?.type !== "longtext" || columns[0]?.nullable !== "NO")
|
|
8816
|
-
throw new
|
|
8999
|
+
throw new VibeError39({ code: "VIBE_MIGRATION", message: "Replay changed the private shadow marker; its objects cannot be excluded from reference inspection", meta: { reason: "shadow-ownership-changed" } });
|
|
8817
9000
|
}
|
|
8818
9001
|
if (params.privateNamespace !== undefined) {
|
|
8819
9002
|
if (dialect !== "postgres" || params.privateNamespace !== SHADOW_DATABASE.schema)
|
|
8820
|
-
throw new
|
|
9003
|
+
throw new VibeError39({ code: "VIBE_MIGRATION", message: "Unknown private shadow namespace", meta: { reason: "shadow-ownership-invalid" } });
|
|
8821
9004
|
const [inventoryQuery] = renderShadowOperation2({ dialect: sqlDialect, operation: { kind: "inventory" } });
|
|
8822
9005
|
const privateObjects = (await executor({ text: inventoryQuery.text, values: [...inventoryQuery.values] })).filter((row) => row.namespace === params.privateNamespace);
|
|
8823
9006
|
const [columnsQuery] = renderShadowOperation2({ dialect: sqlDialect, operation: { kind: "markerColumns" } });
|
|
8824
9007
|
const columns = await executor({ text: columnsQuery.text, values: [...columnsQuery.values] });
|
|
8825
9008
|
if (columns.length !== 1 || columns[0]?.name !== "payload" || columns[0]?.type !== "text" || columns[0]?.nullable !== "NO" || privateObjects.some((row) => !(row.kind === "namespace" && row.name === SHADOW_DATABASE.schema || row.kind === "relation" && row.name === SHADOW_DATABASE.table)))
|
|
8826
|
-
throw new
|
|
9009
|
+
throw new VibeError39({ code: "VIBE_MIGRATION", message: "Replay changed the private shadow namespace; its objects cannot be excluded from reference inspection", meta: { reason: "shadow-ownership-changed", objects: privateObjects } });
|
|
8827
9010
|
}
|
|
8828
9011
|
const namespaces = dialect === "postgres" ? [...new Set([...migrationReferenceNamespaces({ migrations: params.migrations, namespace }), ...(params.namespaceOrigins ?? []).map((origin) => moduleNamespace({ origin }))])] : [namespace];
|
|
8829
9012
|
const external = (params.external?.length ?? 0) === 0 ? undefined : await inspectExternalObjects({ executor, dialect, external: params.external });
|
|
@@ -8834,7 +9017,7 @@ async function captureReplayReference(params) {
|
|
|
8834
9017
|
}))).flat();
|
|
8835
9018
|
const gaps = migrationCoverageGaps({ gaps: coverageRows, namespace, namespaces, external });
|
|
8836
9019
|
if (gaps.length > 0)
|
|
8837
|
-
throw new
|
|
9020
|
+
throw new VibeError39({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Replayed history contains objects whose full definitions are not represented by migration inspection. Use reviewed SQL for changes; automatic generation and verification require supported reference coverage.", meta: { feature: "replayInventoryCoverage", dialect, namespace, objects: gaps } });
|
|
8838
9021
|
const rls = [...params.migrations].reverse().find((migration) => migration.rls !== undefined)?.rls?.to;
|
|
8839
9022
|
const physical = dialect === "postgres" ? await introspectPostgres({ executor, namespaces: inspectedNamespaces, ...rls === undefined ? {} : { rlsSchema: rls, requireInstalledRls: true } }) : dialect === "mysql" ? await introspectMysql({ executor }) : await introspectSqlite({ executor });
|
|
8840
9023
|
const privateTable = params.privateTable !== undefined && physical.models.some((model) => !model.isView && (model.dbName ?? model.name) === params.privateTable) ? params.privateTable : undefined;
|
|
@@ -8882,7 +9065,7 @@ async function captureReplayReference(params) {
|
|
|
8882
9065
|
}
|
|
8883
9066
|
|
|
8884
9067
|
// packages/migrate/src/verify-history.ts
|
|
8885
|
-
import { VibeError as
|
|
9068
|
+
import { VibeError as VibeError41, sqlObjectKey as sqlObjectKey13 } from "@vibeorm/schema";
|
|
8886
9069
|
import { getSqlDialect as getSqlDialect11, renderReplayCoverageGaps as renderReplayCoverageGaps3 } from "@vibeorm/sql";
|
|
8887
9070
|
|
|
8888
9071
|
// packages/migrate/src/compare-sqlite-primary-key.ts
|
|
@@ -8953,7 +9136,7 @@ function compareSqlitePrimaryKeyRepresentations(params) {
|
|
|
8953
9136
|
}
|
|
8954
9137
|
|
|
8955
9138
|
// packages/migrate/src/compare-reference-rls.ts
|
|
8956
|
-
import { VibeError as
|
|
9139
|
+
import { VibeError as VibeError40 } from "@vibeorm/schema";
|
|
8957
9140
|
function securityFacts(params) {
|
|
8958
9141
|
return new Map(params.tables.filter((table) => table.enabled || table.force || table.policies.length > 0).map((table) => [table.table, {
|
|
8959
9142
|
table: table.table,
|
|
@@ -8964,7 +9147,7 @@ function securityFacts(params) {
|
|
|
8964
9147
|
}
|
|
8965
9148
|
async function comparePostgresReferenceRls(params) {
|
|
8966
9149
|
if (params.schema.rls !== undefined && params.referenceExecutor === undefined)
|
|
8967
|
-
throw new
|
|
9150
|
+
throw new VibeError40({ code: "VIBE_MIGRATION", message: "Native RLS verification requires the independently replayed reference connection; target policy probes are not permitted.", meta: { reason: "verification-rls-reference-required" } });
|
|
8968
9151
|
const expected = securityFacts({ tables: params.schema.rls === undefined ? [] : await readPostgresRls({ executor: params.referenceExecutor }) });
|
|
8969
9152
|
const actual = securityFacts({ tables: await readPostgresRls({ executor: params.executor }) });
|
|
8970
9153
|
const objects = [...new Set([...expected.keys(), ...actual.keys()])].sort().map((table) => {
|
|
@@ -8988,14 +9171,14 @@ function selectMigrationVerificationPrefix(params) {
|
|
|
8988
9171
|
for (const [index, file] of params.migrations.entries()) {
|
|
8989
9172
|
const previous = params.migrations[index - 1];
|
|
8990
9173
|
if (seen.has(file.name) || previous !== undefined && previous.name.localeCompare(file.name) > 0) {
|
|
8991
|
-
throw new
|
|
9174
|
+
throw new VibeError41({ code: "VIBE_MIGRATION", message: "Verification requires a complete local migration chain in unique name order", meta: { reason: "verification-history-order", migration: file.name } });
|
|
8992
9175
|
}
|
|
8993
9176
|
seen.add(file.name);
|
|
8994
9177
|
}
|
|
8995
9178
|
for (const [index, row] of params.recorded.entries()) {
|
|
8996
9179
|
const file = params.migrations[index];
|
|
8997
9180
|
if (row.state !== "applied" || row.statementIndex < 0 || row.checksum === "" || file?.name !== row.name || file.desiredStatePending === true || migrationChecksum(file) !== row.checksum) {
|
|
8998
|
-
throw new
|
|
9181
|
+
throw new VibeError41({ code: "VIBE_MIGRATION", message: "Verification requires an exact, complete, checksum-verified applied prefix; inspect migrate status and resolve the history discrepancy first", meta: { reason: "verification-history-unverified", migration: row.name, state: row.state } });
|
|
8999
9182
|
}
|
|
9000
9183
|
}
|
|
9001
9184
|
return params.migrations.slice(0, params.recorded.length);
|
|
@@ -9026,7 +9209,7 @@ async function verifyMigrationReference(params) {
|
|
|
9026
9209
|
}))).flat();
|
|
9027
9210
|
const gaps = migrationCoverageGaps({ gaps: coverageRows, namespace, namespaces, external });
|
|
9028
9211
|
if (gaps.length > 0)
|
|
9029
|
-
throw new
|
|
9212
|
+
throw new VibeError41({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Target contains objects whose full definitions are outside migration inspection coverage; verification is unavailable. No target changes were made.", meta: { feature: "targetInventoryCoverage", dialect, namespace, objects: gaps } });
|
|
9030
9213
|
const security = dialect === "postgres" ? await comparePostgresReferenceRls({ executor, referenceExecutor: params.referenceExecutor, schema: reference.state.schema }) : { inSync: true, objects: [] };
|
|
9031
9214
|
const inspected = await verifyMigrationState({ executor, dialect, expected: reference.state, external, probe: "none", batch: true });
|
|
9032
9215
|
const structural = dialect === "sqlite" ? compareSqlitePrimaryKeyRepresentations({ report: inspected }) : inspected;
|
|
@@ -9179,12 +9362,12 @@ async function mysqlComparisonFacts(params) {
|
|
|
9179
9362
|
return { ...facts, members };
|
|
9180
9363
|
}
|
|
9181
9364
|
function referenceContextError() {
|
|
9182
|
-
return new
|
|
9365
|
+
return new VibeError41({ code: "VIBE_MIGRATION", message: "Replay and target dialect/namespace differ; safe reference translation is unavailable. Use a supported same-namespace reference setup", meta: { reason: "verification-reference-context" } });
|
|
9183
9366
|
}
|
|
9184
9367
|
|
|
9185
9368
|
// packages/migrate/src/module-adoption-reference.ts
|
|
9186
9369
|
function refusal4(params) {
|
|
9187
|
-
return new
|
|
9370
|
+
return new VibeError42({ code: "VIBE_MIGRATION", message: "Module adoption needs complete independent source and application references with compatible ownership; no target SQL or metadata was authorized", meta: params });
|
|
9188
9371
|
}
|
|
9189
9372
|
function moduleAdoptionSource(params) {
|
|
9190
9373
|
if (params.migrations.length === 0)
|
|
@@ -9294,7 +9477,7 @@ async function verifyModuleAdoptionTarget(params) {
|
|
|
9294
9477
|
await assertLiveModuleOwnership({ executor: params.executor, origin, resume: true });
|
|
9295
9478
|
const report = await verifyMigrationReference({ executor: params.executor, dialect: origin.dialect, reference: params.reference, referenceExecutor: params.referenceExecutor });
|
|
9296
9479
|
if (!report.inSync)
|
|
9297
|
-
throw new
|
|
9480
|
+
throw new VibeError42({ code: "VIBE_MIGRATION", message: "Populated target differs from independently replayed package/application structure; no package SQL or metadata was applied", meta: { reason: "module-adoption-target-mismatch", report } });
|
|
9298
9481
|
}
|
|
9299
9482
|
|
|
9300
9483
|
// packages/migrate/src/history.ts
|
|
@@ -9310,10 +9493,10 @@ async function readMigrationHistory(params) {
|
|
|
9310
9493
|
|
|
9311
9494
|
// packages/migrate/src/module-adoption-record.ts
|
|
9312
9495
|
function refusal5(params) {
|
|
9313
|
-
return new
|
|
9496
|
+
return new VibeError43({ code: "VIBE_MIGRATION", message: "Module adoption cannot record an incomplete, concurrent or altered history; no package SQL was executed", meta: params });
|
|
9314
9497
|
}
|
|
9315
9498
|
function transactionFailure(params) {
|
|
9316
|
-
return params.cause instanceof
|
|
9499
|
+
return params.cause instanceof VibeError43 ? params.cause : new VibeError43({ code: "VIBE_MIGRATION", message: "Module adoption recording failed; inspect the local provenance and journal before retrying", meta: { reason: "module-adoption-transaction-failed" }, cause: params.cause });
|
|
9317
9500
|
}
|
|
9318
9501
|
async function recordModuleAdoption(params) {
|
|
9319
9502
|
moduleAdoptionSource(params);
|
|
@@ -9376,9 +9559,9 @@ async function recordModuleAdoption(params) {
|
|
|
9376
9559
|
await record();
|
|
9377
9560
|
}).then(undefined, async (cause) => {
|
|
9378
9561
|
if (committed)
|
|
9379
|
-
throw new
|
|
9562
|
+
throw new VibeError43({ code: "VIBE_MIGRATION", message: "All adopted revisions committed, but filesystem lease cleanup failed; inspect the remaining publication lease before retrying", meta: { reason: "module-adoption-publication-cleanup-failed", recorded: true }, cause });
|
|
9380
9563
|
await executor({ text: "ROLLBACK" }).then(undefined, (rollback) => {
|
|
9381
|
-
throw new
|
|
9564
|
+
throw new VibeError43({ code: "VIBE_MIGRATION", message: "Adoption recording failed and rollback also failed; the journal outcome is uncertain and must be inspected before recovery", meta: { reason: "module-adoption-rollback-failed" }, cause: new AggregateError([cause, rollback], "Adoption recording and rollback failed") });
|
|
9382
9565
|
});
|
|
9383
9566
|
throw transactionFailure({ cause });
|
|
9384
9567
|
});
|
|
@@ -9389,14 +9572,14 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
9389
9572
|
import { mkdtemp, rm } from "fs/promises";
|
|
9390
9573
|
import { tmpdir } from "os";
|
|
9391
9574
|
import { join } from "path";
|
|
9392
|
-
import { VibeError as
|
|
9575
|
+
import { VibeError as VibeError45, providerToDialect as providerToDialect6, sqlObjectKey as sqlObjectKey15, unsupportedCapability as unsupportedCapability13 } from "@vibeorm/schema";
|
|
9393
9576
|
import { getSqlDialect as getSqlDialect14, renderShadowOperation as renderShadowOperation4, SHADOW_DATABASE as SHADOW_DATABASE3 } from "@vibeorm/sql";
|
|
9394
9577
|
|
|
9395
9578
|
// packages/migrate/src/shadow-dedicated.ts
|
|
9396
|
-
import { SQL_NAMESPACES, VibeError as
|
|
9579
|
+
import { SQL_NAMESPACES as SQL_NAMESPACES2, VibeError as VibeError44, providerToDialect as providerToDialect5, sqlNamespaceIssue as sqlNamespaceIssue2 } from "@vibeorm/schema";
|
|
9397
9580
|
import { getSqlDialect as getSqlDialect13, renderShadowOperation as renderShadowOperation3, SHADOW_DATABASE as SHADOW_DATABASE2 } from "@vibeorm/sql";
|
|
9398
9581
|
function refuse(params) {
|
|
9399
|
-
throw new
|
|
9582
|
+
throw new VibeError44({ code: "VIBE_MIGRATION", message: params.message, meta: { reason: params.reason, ...params.objects === undefined ? {} : { objects: params.objects } } });
|
|
9400
9583
|
}
|
|
9401
9584
|
function readIdentity(params) {
|
|
9402
9585
|
const row = params.rows[0];
|
|
@@ -9464,7 +9647,7 @@ async function readMarker(params) {
|
|
|
9464
9647
|
if (params.rows.length !== 1 || typeof payload !== "string")
|
|
9465
9648
|
return refuse({ reason: "shadow-ownership-invalid", message: "Shadow ownership marker is missing or malformed; automatic claim/reset is refused." });
|
|
9466
9649
|
const value = await Promise.resolve().then(() => JSON.parse(payload)).then((result) => result, (cause) => {
|
|
9467
|
-
throw new
|
|
9650
|
+
throw new VibeError44({ code: "VIBE_MIGRATION", message: "Shadow marker is not valid JSON", meta: { reason: "shadow-ownership-invalid" }, cause });
|
|
9468
9651
|
});
|
|
9469
9652
|
if (value === null || typeof value !== "object" || !("version" in value) || value.version !== 1 || !("ownerKey" in value) || typeof value.ownerKey !== "string" || !("identity" in value) || typeof value.identity !== "string" || !("status" in value) || value.status !== "READY" && value.status !== "BUILDING" || !("journal" in value) || !Array.isArray(value.journal) || !value.journal.every((row) => row !== null && typeof row === "object" && ("name" in row) && typeof row.name === "string" && ("checksum" in row) && typeof row.checksum === "string"))
|
|
9470
9653
|
return refuse({ reason: "shadow-ownership-invalid", message: "Shadow marker has an unsupported ownership or replay-journal shape." });
|
|
@@ -9478,8 +9661,8 @@ async function withDedicatedShadow(params) {
|
|
|
9478
9661
|
const intendedJournal = completedJournal({ rows: params.migrations.map((migration) => ({ name: migration.name, checksum: migrationChecksum(migration), statement_index: migration.sql.length, error: null })), reason: "shadow-replay-journal-changed" });
|
|
9479
9662
|
if (shadow.ownerKey.length === 0 || shadow.targetUrl.length === 0)
|
|
9480
9663
|
return refuse({ reason: "shadow-identity-unavailable", message: "Reusable shadow mode requires a project owner and a resolvable deployment target for an independent identity check." });
|
|
9481
|
-
const dialect =
|
|
9482
|
-
const declaredNamespaces = dialect === "postgres" ? migrationReferenceNamespaces({ migrations: params.migrations, namespace:
|
|
9664
|
+
const dialect = providerToDialect5({ provider: shadow.provider });
|
|
9665
|
+
const declaredNamespaces = dialect === "postgres" ? migrationReferenceNamespaces({ migrations: params.migrations, namespace: SQL_NAMESPACES2.postgres }).filter((namespace) => namespace !== SQL_NAMESPACES2.postgres) : [];
|
|
9483
9666
|
const sqlDialect = getSqlDialect13({ dialect });
|
|
9484
9667
|
const execute = async (executor, operation) => {
|
|
9485
9668
|
let rows = [];
|
|
@@ -9508,13 +9691,13 @@ async function withDedicatedShadow(params) {
|
|
|
9508
9691
|
return refuse({ reason: "shadow-identity-changed", message: "Shadow identity changed after lease acquisition; reset is refused." });
|
|
9509
9692
|
if (dialect === "mysql") {
|
|
9510
9693
|
const grants = await execute(executor, { kind: "grants" }).then(undefined, (cause) => {
|
|
9511
|
-
throw new
|
|
9694
|
+
throw new VibeError44({ code: "VIBE_MIGRATION", message: "Cannot establish dedicated MySQL catalog visibility from direct grants", meta: { reason: "shadow-inventory-unavailable" }, cause });
|
|
9512
9695
|
});
|
|
9513
9696
|
assertMysqlVisibility({ rows: grants, database: identity.database });
|
|
9514
9697
|
}
|
|
9515
9698
|
const existing = (await execute(executor, { kind: "markerExists" })).length > 0;
|
|
9516
9699
|
const inventory = await execute(executor, { kind: "inventory" }).then(undefined, (cause) => {
|
|
9517
|
-
throw new
|
|
9700
|
+
throw new VibeError44({ code: "VIBE_MIGRATION", message: "Cannot inspect the complete dedicated shadow inventory; no reset was attempted", meta: { reason: "shadow-inventory-unavailable" }, cause });
|
|
9518
9701
|
});
|
|
9519
9702
|
const externalReferences = inventory.filter((row) => row.kind === "EXTERNAL FOREIGN KEY");
|
|
9520
9703
|
if (externalReferences.length !== 0)
|
|
@@ -9567,7 +9750,7 @@ async function withDedicatedShadow(params) {
|
|
|
9567
9750
|
if (remaining.length !== 0)
|
|
9568
9751
|
return refuse({ reason: "shadow-reset-incomplete", message: "Shadow reset left objects behind. Replay was not started.", objects: remaining });
|
|
9569
9752
|
const result = await params.runReference(executor, dialect === "mysql" ? SHADOW_DATABASE2.table : undefined).then((value) => ({ ok: true, value }), (error) => {
|
|
9570
|
-
if (!(error instanceof
|
|
9753
|
+
if (!(error instanceof VibeError44) || error.meta.shadowInvalid === true || error.meta.shadowReusable !== true)
|
|
9571
9754
|
throw error;
|
|
9572
9755
|
return { ok: false, error };
|
|
9573
9756
|
});
|
|
@@ -9591,13 +9774,13 @@ async function withDedicatedShadow(params) {
|
|
|
9591
9774
|
function finishShadowWork(params) {
|
|
9592
9775
|
return Promise.resolve().then(params.run).then(async (value) => {
|
|
9593
9776
|
await Promise.resolve().then(params.cleanup).then(undefined, (cause) => {
|
|
9594
|
-
throw new
|
|
9777
|
+
throw new VibeError45({ code: "VIBE_MIGRATION", message: "Migration reference completed but its owned resource could not be cleaned. Inspect the named artifact before explicit cleanup.", meta: { cleanupReason: "shadow-cleanup-failed", artifact: params.artifact, cleanupFailure: cause instanceof VibeError45 ? cause.code : "unavailable" }, cause });
|
|
9595
9778
|
});
|
|
9596
9779
|
return value;
|
|
9597
9780
|
}, async (primary) => {
|
|
9598
9781
|
const [cleanup] = await Promise.allSettled([Promise.resolve().then(params.cleanup)]);
|
|
9599
9782
|
if (cleanup.status === "rejected")
|
|
9600
|
-
throw new
|
|
9783
|
+
throw new VibeError45({ code: primary instanceof VibeError45 ? primary.code : "VIBE_MIGRATION", message: "Migration reference failed and its owned resource could not be cleaned. Inspect the named artifact before explicit cleanup.", meta: { cleanupReason: "shadow-cleanup-failed", artifact: params.artifact, cleanupFailure: cleanup.reason instanceof VibeError45 ? cleanup.reason.code : "unavailable" }, cause: { primary, cleanup: cleanup.reason } });
|
|
9601
9784
|
throw primary;
|
|
9602
9785
|
});
|
|
9603
9786
|
}
|
|
@@ -9609,21 +9792,21 @@ function withShadowConnection(params) {
|
|
|
9609
9792
|
return finishShadowWork({ artifact: params.artifact, cleanup: () => params.connection.close(), run: () => params.connection.executor.withSession === undefined ? runPinned(params.connection.executor) : params.connection.executor.withSession({ run: runPinned }) });
|
|
9610
9793
|
}
|
|
9611
9794
|
async function withMigrationReference(params) {
|
|
9612
|
-
const dialect =
|
|
9795
|
+
const dialect = providerToDialect6({ provider: params.shadow.provider });
|
|
9613
9796
|
const external = params.external ?? params.replay?.external;
|
|
9614
9797
|
const replay = { ...params.replay, external, migrations: params.migrations, dialect };
|
|
9615
9798
|
validateMigrationReplay(replay);
|
|
9616
9799
|
for (const origin of params.namespaceOrigins ?? []) {
|
|
9617
9800
|
assertModuleOrigin({ value: origin });
|
|
9618
9801
|
if (origin.dialect !== dialect)
|
|
9619
|
-
throw new
|
|
9802
|
+
throw new VibeError45({ code: "VIBE_MIGRATION", message: "Namespace context origin uses a different dialect", meta: { reason: "module-dialect-mismatch", moduleId: origin.moduleId } });
|
|
9620
9803
|
}
|
|
9621
9804
|
const hasAdoption = params.migrations.some((migration) => migration.moduleAdoption !== undefined);
|
|
9622
9805
|
if (hasAdoption && params.shadow.provider === "mysql")
|
|
9623
9806
|
throw unsupportedCapability13({ dialect: "mysql", feature: "moduleAdoptionIndependentMysqlNamespace", hint: "Independent source and application databases require a proven metadata rebase before grouped adoption replay is supported" });
|
|
9624
9807
|
const adoptionShadow = params.shadow.mode === "automatic" ? params.shadow : undefined;
|
|
9625
9808
|
if (hasAdoption && adoptionShadow === undefined)
|
|
9626
|
-
throw new
|
|
9809
|
+
throw new VibeError45({ code: "VIBE_CONFIG", message: "Adopted history requires an independent automatic package source reference; a single dedicated shadow cannot prove both histories. Configure shadowAdminUrl.", meta: { reason: "module-adoption-automatic-shadow-required" } });
|
|
9627
9810
|
const inspect = async (executor, privateTable) => {
|
|
9628
9811
|
if ((external?.length ?? 0) > 0) {
|
|
9629
9812
|
const ownership = await inspectExternalObjects({ executor, dialect, external });
|
|
@@ -9650,7 +9833,7 @@ async function withMigrationReference(params) {
|
|
|
9650
9833
|
await withMigrationReference({ shadow: adoptionShadow, openConnection: params.openConnection, migrations: moduleAdoptionSource({ migrations: group }), replay: sourceReplay, run: async ({ executor: sourceExecutor, reference: source }) => {
|
|
9651
9834
|
const fingerprint = moduleAdoptionReferenceFingerprint({ migrations: group, reference: source });
|
|
9652
9835
|
if (group.some((migration) => migration.moduleAdoption?.referenceFingerprint !== fingerprint))
|
|
9653
|
-
throw new
|
|
9836
|
+
throw new VibeError45({ code: "VIBE_MIGRATION", message: "Local adoption proof does not match independently replayed immutable package source", meta: { reason: "module-adoption-reference-unverified", moduleId: first.moduleOrigin?.moduleId } });
|
|
9654
9837
|
const application = await captureReplayReference({
|
|
9655
9838
|
executor,
|
|
9656
9839
|
dialect,
|
|
@@ -9697,14 +9880,14 @@ async function withMigrationReference(params) {
|
|
|
9697
9880
|
} });
|
|
9698
9881
|
}
|
|
9699
9882
|
if (!("adminUrl" in params.shadow))
|
|
9700
|
-
throw new
|
|
9883
|
+
throw new VibeError45({ code: "VIBE_CONFIG", message: "Automatic server shadows require an explicit admin URL" });
|
|
9701
9884
|
const { provider, adminUrl } = params.shadow;
|
|
9702
9885
|
const url = await Promise.resolve().then(() => new URL(adminUrl)).then((value) => value, (cause) => {
|
|
9703
|
-
throw new
|
|
9886
|
+
throw new VibeError45({ code: "VIBE_CONFIG", message: "shadowAdminUrl must be an explicit server database URL", cause });
|
|
9704
9887
|
});
|
|
9705
9888
|
const expected = provider === "postgresql" ? ["postgres:", "postgresql:"] : ["mysql:"];
|
|
9706
9889
|
if (!expected.includes(url.protocol) || url.hostname.length === 0 || url.pathname.length <= 1 || [...url.searchParams.keys()].some((key) => ["database", "dbname", "host", "hostaddr", "port", "user", "socketpath", "connectionstring", "uri", "url", "service"].includes(key.toLowerCase())))
|
|
9707
|
-
throw new
|
|
9890
|
+
throw new VibeError45({ code: "VIBE_CONFIG", message: "Shadow admin URL has an unsupported protocol or conflicting connection selector", meta: { reason: "shadow-url-ambiguous" } });
|
|
9708
9891
|
const prefix = /^\/vibeorm_test(?:_|$)/.test(url.pathname) ? "vibeorm_test_shadow" : "vibeorm_shadow";
|
|
9709
9892
|
const name = `${prefix}_${randomUUID3().replaceAll("-", "")}`;
|
|
9710
9893
|
const connection = await params.openConnection({ provider, url: adminUrl });
|
|
@@ -9712,7 +9895,7 @@ async function withMigrationReference(params) {
|
|
|
9712
9895
|
const sqlDialect = getSqlDialect14({ dialect });
|
|
9713
9896
|
for (const query of renderShadowOperation4({ dialect: sqlDialect, operation: { kind: "createDatabase", name } })) {
|
|
9714
9897
|
await admin({ text: query.text, values: [...query.values] }).then(undefined, (cause) => {
|
|
9715
|
-
throw new
|
|
9898
|
+
throw new VibeError45({ code: "VIBE_MIGRATION", message: "Shadow CREATE did not report success. No deletion was attempted; inspect the named artifact on the configured admin server before explicit cleanup.", meta: { reason: "shadow-create-outcome-unknown", artifact: name }, cause });
|
|
9716
9899
|
});
|
|
9717
9900
|
}
|
|
9718
9901
|
return finishShadowWork({ artifact: name, cleanup: async () => {
|
|
@@ -9727,10 +9910,10 @@ async function withMigrationReference(params) {
|
|
|
9727
9910
|
}
|
|
9728
9911
|
// packages/migrate/src/replay-objects.ts
|
|
9729
9912
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
9730
|
-
import { VibeError as
|
|
9913
|
+
import { VibeError as VibeError46, sqlObjectKey as sqlObjectKey16 } from "@vibeorm/schema";
|
|
9731
9914
|
import { getSqlDialect as getSqlDialect15 } from "@vibeorm/sql";
|
|
9732
9915
|
function comparisonFailure(params) {
|
|
9733
|
-
return params.cause instanceof
|
|
9916
|
+
return params.cause instanceof VibeError46 ? params.cause : new VibeError46({ code: "VIBE_MIGRATION", message: "Authored custom SQL could not be safely compiled on the independent scratch reference. Review dependencies and provide an explicit SQL migration when replacement is not supported.", meta: { reason: "migration-object-comparison-unavailable" }, cause: params.cause });
|
|
9734
9917
|
}
|
|
9735
9918
|
function leadingCommand(params) {
|
|
9736
9919
|
const { sql } = params;
|
|
@@ -9772,11 +9955,11 @@ async function compareReplayMigrationObjects(params) {
|
|
|
9772
9955
|
if (objects.length === 0)
|
|
9773
9956
|
return params.schema;
|
|
9774
9957
|
if (params.reference.dialect !== "postgres")
|
|
9775
|
-
throw new
|
|
9958
|
+
throw new VibeError46({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Independent authored custom-object comparison requires transactional scratch DDL. Use reviewed SQL for this dialect's custom objects.", meta: { feature: "authoredCustomObjectComparison", dialect: params.reference.dialect, objects: objects.map((object) => object.identity) } });
|
|
9776
9959
|
const before = new Map(params.reference.state.definitions.map((definition) => [sqlObjectKey16({ identity: definition.identity }), definition]));
|
|
9777
9960
|
for (const object of objects) {
|
|
9778
9961
|
if (object.dialect !== params.reference.dialect || object.create.length === 0 || before.has(sqlObjectKey16({ identity: object.identity })) && object.drop.length === 0 || [...object.create, ...object.drop].some((sql) => /^(?:BEGIN|START|COMMIT|END|ABORT|ROLLBACK|PREPARE|VACUUM|CREATE\s+DATABASE|DROP\s+DATABASE)\b/i.test(leadingCommand({ sql }))))
|
|
9779
|
-
throw new
|
|
9962
|
+
throw new VibeError46({ code: "VIBE_MIGRATION", message: "Custom object cannot be safely dropped/recreated in a rollback-only shadow transaction. Supply a reviewed SQL migration.", meta: { reason: "migration-object-comparison-unavailable", identity: object.identity } });
|
|
9780
9963
|
}
|
|
9781
9964
|
const definitions = new Map;
|
|
9782
9965
|
const sentinel = getSqlDialect15({ dialect: "postgres" }).quoteIdent({ name: `vibe_probe_${randomUUID4().replaceAll("-", "")}` });
|
|
@@ -9784,7 +9967,7 @@ async function compareReplayMigrationObjects(params) {
|
|
|
9784
9967
|
await Promise.resolve().then(() => params.executor({ text: `RELEASE SAVEPOINT ${sentinel}` })).then(() => {
|
|
9785
9968
|
return;
|
|
9786
9969
|
}, (cause) => {
|
|
9787
|
-
throw new
|
|
9970
|
+
throw new VibeError46({ code: "VIBE_MIGRATION", message: "Authored SQL destroyed the scratch transaction or its savepoint. Discard this scratch reference; no comparison result can be trusted.", meta: { reason: "migration-object-transaction-lost", shadowInvalid: true }, cause });
|
|
9788
9971
|
});
|
|
9789
9972
|
await params.executor({ text: `SAVEPOINT ${sentinel}` });
|
|
9790
9973
|
};
|
|
@@ -9812,7 +9995,7 @@ async function compareReplayMigrationObjects(params) {
|
|
|
9812
9995
|
for (const object of objects) {
|
|
9813
9996
|
const inspected = await inspectSqlObject({ executor: params.executor, dialect: "postgres", identity: object.identity });
|
|
9814
9997
|
if (inspected.state !== "present")
|
|
9815
|
-
throw new
|
|
9998
|
+
throw new VibeError46({ code: "VIBE_MIGRATION", message: "Authored custom SQL did not produce an inspectable object in the independent scratch probe.", meta: { reason: "migration-object-comparison-unavailable", identity: object.identity, state: inspected.state } });
|
|
9816
9999
|
definitions.set(sqlObjectKey16({ identity: object.identity }), inspected.definition);
|
|
9817
10000
|
}
|
|
9818
10001
|
for (const [key, expected] of before) {
|
|
@@ -9820,32 +10003,173 @@ async function compareReplayMigrationObjects(params) {
|
|
|
9820
10003
|
continue;
|
|
9821
10004
|
const inspected = await inspectSqlObject({ executor: params.executor, dialect: "postgres", identity: expected.identity });
|
|
9822
10005
|
if (inspected.state !== "present" || canonicalMigrationJson({ value: inspected.definition }) !== canonicalMigrationJson({ value: expected }))
|
|
9823
|
-
throw new
|
|
10006
|
+
throw new VibeError46({ code: "VIBE_MIGRATION", message: "Custom scratch replacement changed a dependent object outside the declared replacement set. Supply reviewed SQL.", meta: { reason: "migration-object-dependency-unavailable", identity: expected.identity } });
|
|
9824
10007
|
}
|
|
9825
10008
|
await checkSentinel();
|
|
9826
10009
|
}).then(() => ({ ok: true }), (cause) => ({ ok: false, error: comparisonFailure({ cause }) }));
|
|
9827
|
-
const rollback = await Promise.resolve().then(() => params.executor({ text: "ROLLBACK" })).then(() => ({ ok: true }), (cause) => ({ ok: false, error: cause instanceof
|
|
10010
|
+
const rollback = await Promise.resolve().then(() => params.executor({ text: "ROLLBACK" })).then(() => ({ ok: true }), (cause) => ({ ok: false, error: cause instanceof VibeError46 ? cause : new VibeError46({ code: "VIBE_MIGRATION", message: "Scratch rollback failed", meta: { reason: "migration-object-rollback-failed" }, cause }) }));
|
|
9828
10011
|
if (!rollback.ok)
|
|
9829
|
-
throw new
|
|
10012
|
+
throw new VibeError46({ code: "VIBE_MIGRATION", message: "Custom comparison scratch could not be rolled back. Discard the scratch reference and inspect the preserved execution and cleanup errors.", meta: { reason: "migration-object-rollback-failed", shadowInvalid: true, cleanupFailure: rollback.error }, cause: outcome.ok ? rollback.error : outcome.error });
|
|
9830
10013
|
if (!outcome.ok)
|
|
9831
10014
|
throw outcome.error;
|
|
9832
10015
|
for (const object of objects) {
|
|
9833
10016
|
const key = sqlObjectKey16({ identity: object.identity });
|
|
9834
10017
|
const previous = before.get(key);
|
|
9835
10018
|
if (previous !== undefined && canonicalMigrationJson({ value: previous }) !== canonicalMigrationJson({ value: definitions.get(key) }))
|
|
9836
|
-
throw new
|
|
10019
|
+
throw new VibeError46({ code: "VIBE_MIGRATION", message: "Authored custom object differs from replayed history. Supply a reviewed forward SQL migration; generation will not silently replace its body.", meta: { reason: "migration-object-replacement-required", shadowReusable: true, identity: object.identity, id: object.id } });
|
|
9837
10020
|
}
|
|
9838
10021
|
return { ...params.schema, migrationObjects: objects.map((object) => ({ ...object, expected: definitions.get(sqlObjectKey16({ identity: object.identity })) })) };
|
|
9839
10022
|
}
|
|
9840
10023
|
// packages/migrate/src/replay-plan.ts
|
|
9841
|
-
import { VibeError as
|
|
10024
|
+
import { VibeError as VibeError51, sqlObjectKey as sqlObjectKey19 } from "@vibeorm/schema";
|
|
10025
|
+
|
|
10026
|
+
// packages/migrate/src/sqlite-fts-upgrade.ts
|
|
10027
|
+
import { VibeError as VibeError47, sqlObjectKey as sqlObjectKey17 } from "@vibeorm/schema";
|
|
10028
|
+
import { sqliteFtsUpdateUpgrade } from "@vibeorm/sql";
|
|
10029
|
+
var actionReceipts = new WeakMap;
|
|
10030
|
+
function specs(params) {
|
|
10031
|
+
const result = [];
|
|
10032
|
+
for (const group of params.collected.groups)
|
|
10033
|
+
for (const artifact of group.objects) {
|
|
10034
|
+
const owner = artifact.owns;
|
|
10035
|
+
const expected = artifact.expected;
|
|
10036
|
+
if (owner.kind !== "trigger" || artifact.create.length !== 0 || artifact.drop.length !== 0 || expected?.identity.kind !== "trigger" || expected.identity.table.namespace !== "main" || expected.format !== "catalog-v2" || expected.facts.type !== "trigger" || typeof expected.facts.definition !== "string" || expected.dependencies.length !== 0)
|
|
10037
|
+
continue;
|
|
10038
|
+
const identity = { kind: "trigger", table: { namespace: "main", name: owner.table }, name: owner.name };
|
|
10039
|
+
if (sqlObjectKey17({ identity }) !== sqlObjectKey17({ identity: expected.identity }))
|
|
10040
|
+
continue;
|
|
10041
|
+
const matches = [];
|
|
10042
|
+
for (const bundle of group.objects) {
|
|
10043
|
+
if (bundle.owns.kind !== "trigger" || bundle.owns.table !== owner.table || bundle.create.length !== 4 || bundle.drop.length !== 3)
|
|
10044
|
+
continue;
|
|
10045
|
+
const create = bundle.create[2];
|
|
10046
|
+
const parsed = sqliteFtsUpdateUpgrade({ create, installed: expected.facts.definition, table: owner.table, name: owner.name });
|
|
10047
|
+
if (parsed?.kind !== "tableRequired" || bundle.owns.name !== `${parsed.ftsTable}_ai`)
|
|
10048
|
+
continue;
|
|
10049
|
+
const tableArtifact = group.objects.find((entry) => entry.owns.kind === "table" && entry.owns.name === parsed.ftsTable && entry.create.length === 1);
|
|
10050
|
+
if (tableArtifact === undefined)
|
|
10051
|
+
continue;
|
|
10052
|
+
const declared = sqliteFtsUpdateUpgrade({ create, installed: expected.facts.definition, table: owner.table, name: owner.name, tableDefinition: tableArtifact.create[0] });
|
|
10053
|
+
if (declared?.kind !== "recognized" || declared.installed !== "current")
|
|
10054
|
+
continue;
|
|
10055
|
+
matches.push({ artifact, bundle, tableArtifact, identity, create, ftsTable: declared.ftsTable, currentDefinition: declared.currentDefinition });
|
|
10056
|
+
}
|
|
10057
|
+
if (matches.length === 1)
|
|
10058
|
+
result.push(matches[0]);
|
|
10059
|
+
}
|
|
10060
|
+
return result;
|
|
10061
|
+
}
|
|
10062
|
+
function tableIdentity(params) {
|
|
10063
|
+
return { kind: "table", name: { namespace: "main", name: params.spec.ftsTable } };
|
|
10064
|
+
}
|
|
10065
|
+
function unavailable2(params) {
|
|
10066
|
+
return { identity: params.identity, ownership: "migration", state: "unavailable", reason: params.reason, differences: [] };
|
|
10067
|
+
}
|
|
10068
|
+
async function planSqliteFtsUpdates(params) {
|
|
10069
|
+
const states = new Map;
|
|
10070
|
+
const forcedArtifacts = new Set;
|
|
10071
|
+
const before = [];
|
|
10072
|
+
const after = [];
|
|
10073
|
+
const checks = [];
|
|
10074
|
+
for (const spec of specs(params)) {
|
|
10075
|
+
const key = sqlObjectKey17({ identity: spec.identity });
|
|
10076
|
+
const live = await inspectSqlObject({ executor: params.executor, dialect: "sqlite", identity: spec.identity });
|
|
10077
|
+
const state = compareInspectedSqlObject({ identity: spec.identity, ownership: "migration", expected: spec.artifact.expected, live, dialect: "sqlite" });
|
|
10078
|
+
states.set(key, state);
|
|
10079
|
+
if (live.state === "unavailable")
|
|
10080
|
+
continue;
|
|
10081
|
+
const beforeTrigger = live.state === "present" && typeof live.definition.facts.definition === "string" ? live.definition.facts.definition : undefined;
|
|
10082
|
+
if (live.state === "present" && beforeTrigger === undefined)
|
|
10083
|
+
continue;
|
|
10084
|
+
const candidate = sqliteFtsUpdateUpgrade({ create: spec.create, installed: beforeTrigger ?? spec.currentDefinition, table: spec.identity.table.name, name: spec.identity.name });
|
|
10085
|
+
if (candidate?.kind !== "tableRequired")
|
|
10086
|
+
continue;
|
|
10087
|
+
const table = await inspectSqlObject({ executor: params.executor, dialect: "sqlite", identity: tableIdentity({ spec }) });
|
|
10088
|
+
if (table.state === "unavailable") {
|
|
10089
|
+
states.set(key, unavailable2({ identity: spec.identity, reason: table.reason }));
|
|
10090
|
+
continue;
|
|
10091
|
+
}
|
|
10092
|
+
const beforeTable = table.state === "present" && typeof table.definition.facts.definition === "string" ? table.definition.facts.definition : undefined;
|
|
10093
|
+
const recognized = sqliteFtsUpdateUpgrade({
|
|
10094
|
+
create: spec.create,
|
|
10095
|
+
installed: beforeTrigger ?? spec.currentDefinition,
|
|
10096
|
+
table: spec.identity.table.name,
|
|
10097
|
+
name: spec.identity.name,
|
|
10098
|
+
tableDefinition: table.state === "missing" ? spec.tableArtifact.create[0] : beforeTable ?? ""
|
|
10099
|
+
});
|
|
10100
|
+
if (recognized?.kind !== "recognized") {
|
|
10101
|
+
states.set(key, unavailable2({ identity: spec.identity, reason: "sqlite-fts-table-unverified" }));
|
|
10102
|
+
continue;
|
|
10103
|
+
}
|
|
10104
|
+
const installBundle = table.state === "missing" || !params.installed.triggers?.has(`${spec.ftsTable}_ai`) || !params.installed.triggers?.has(`${spec.ftsTable}_ad`);
|
|
10105
|
+
if (table.state === "missing")
|
|
10106
|
+
forcedArtifacts.add(spec.tableArtifact.id);
|
|
10107
|
+
if (installBundle)
|
|
10108
|
+
forcedArtifacts.add(spec.bundle.id);
|
|
10109
|
+
else if (live.state === "missing")
|
|
10110
|
+
after.push(spec.create, spec.bundle.create[3]);
|
|
10111
|
+
const legacy = live.state === "present" && recognized.installed === "legacy";
|
|
10112
|
+
if (legacy) {
|
|
10113
|
+
before.push(recognized.drop);
|
|
10114
|
+
if (!installBundle)
|
|
10115
|
+
after.push(spec.create);
|
|
10116
|
+
states.set(key, { ...state, state: "changed", reason: "sqlite-fts-update-upgrade" });
|
|
10117
|
+
}
|
|
10118
|
+
checks.push({ spec, beforeTrigger, beforeTable, changes: installBundle || live.state === "missing" || legacy });
|
|
10119
|
+
}
|
|
10120
|
+
return { states, forcedArtifacts, before, after, checks };
|
|
10121
|
+
}
|
|
10122
|
+
function retainSqliteFtsPlan(params) {
|
|
10123
|
+
if (params.fts.checks.length > 0)
|
|
10124
|
+
actionReceipts.set(params.plan, { fts: params.fts, ...params.transaction === undefined ? {} : { transaction: params.transaction } });
|
|
10125
|
+
}
|
|
10126
|
+
function sqliteFtsTransaction(params) {
|
|
10127
|
+
return actionReceipts.get(params.plan)?.transaction;
|
|
10128
|
+
}
|
|
10129
|
+
function stateChanged(params) {
|
|
10130
|
+
return new VibeError47({ code: "VIBE_MIGRATION", message: "SQLite FTS state changed after planning; re-plan before replacing synchronization", meta: { reason: "sqlite-fts-state-changed", identity: params.identity } });
|
|
10131
|
+
}
|
|
10132
|
+
async function assertSqliteFtsApplyState(params) {
|
|
10133
|
+
const receipt = actionReceipts.get(params.plan)?.fts;
|
|
10134
|
+
if (receipt === undefined)
|
|
10135
|
+
return;
|
|
10136
|
+
for (const check of receipt.checks) {
|
|
10137
|
+
if (!check.changes)
|
|
10138
|
+
continue;
|
|
10139
|
+
for (const observation of [{ identity: check.spec.identity, definition: check.beforeTrigger }, { identity: tableIdentity({ spec: check.spec }), definition: check.beforeTable }]) {
|
|
10140
|
+
const live = await inspectSqlObject({ executor: params.executor, dialect: "sqlite", identity: observation.identity });
|
|
10141
|
+
if (observation.definition === undefined ? live.state !== "missing" : live.state !== "present" || live.definition.facts.definition !== observation.definition)
|
|
10142
|
+
throw stateChanged({ identity: observation.identity });
|
|
10143
|
+
}
|
|
10144
|
+
}
|
|
10145
|
+
}
|
|
10146
|
+
async function verifySqliteFtsApplyState(params) {
|
|
10147
|
+
const receipt = actionReceipts.get(params.plan)?.fts;
|
|
10148
|
+
if (receipt === undefined)
|
|
10149
|
+
return;
|
|
10150
|
+
for (const { spec } of receipt.checks) {
|
|
10151
|
+
const live = await inspectSqlObject({ executor: params.executor, dialect: "sqlite", identity: spec.identity });
|
|
10152
|
+
const expected = { identity: spec.identity, format: "catalog-v2", facts: { definition: spec.currentDefinition, type: "trigger" }, dependencies: [] };
|
|
10153
|
+
const compared = compareInspectedSqlObject({ identity: spec.identity, ownership: "migration", expected, live, dialect: "sqlite" });
|
|
10154
|
+
if (compared.state !== "matching")
|
|
10155
|
+
throw new VibeError47({ code: "VIBE_MIGRATION", message: "SQLite FTS UPDATE synchronization did not match its independent definition after apply", meta: { reason: "migration-state-unverified", objects: [compared] } });
|
|
10156
|
+
const table = await inspectSqlObject({ executor: params.executor, dialect: "sqlite", identity: tableIdentity({ spec }) });
|
|
10157
|
+
if (table.state !== "present" || typeof table.definition.facts.definition !== "string" || sqliteFtsUpdateUpgrade({ create: spec.create, installed: spec.currentDefinition, table: spec.identity.table.name, name: spec.identity.name, tableDefinition: table.definition.facts.definition })?.kind !== "recognized")
|
|
10158
|
+
throw stateChanged({ identity: tableIdentity({ spec }) });
|
|
10159
|
+
for (const suffix of ["ai", "ad"]) {
|
|
10160
|
+
const identity = { kind: "trigger", table: spec.identity.table, name: `${spec.ftsTable}_${suffix}` };
|
|
10161
|
+
if ((await inspectSqlObject({ executor: params.executor, dialect: "sqlite", identity })).state !== "present")
|
|
10162
|
+
throw stateChanged({ identity });
|
|
10163
|
+
}
|
|
10164
|
+
}
|
|
10165
|
+
}
|
|
9842
10166
|
|
|
9843
10167
|
// packages/migrate/src/planner.ts
|
|
9844
|
-
import { VibeError as
|
|
10168
|
+
import { VibeError as VibeError50, sqlObjectKey as sqlObjectKey18, qualifiedSqlName as qualifiedSqlName13 } from "@vibeorm/schema";
|
|
9845
10169
|
import { renderSqliteRebuildObjects, sqliteDialect as sqliteDialect4 } from "@vibeorm/sql";
|
|
9846
10170
|
|
|
9847
10171
|
// packages/migrate/src/artifact-probe.ts
|
|
9848
|
-
import { VibeError as
|
|
10172
|
+
import { VibeError as VibeError48 } from "@vibeorm/schema";
|
|
9849
10173
|
var SAVEPOINT = "vibeorm_artifact_probe";
|
|
9850
10174
|
async function probeChangedArtifacts(params) {
|
|
9851
10175
|
const { executor, namespace } = params;
|
|
@@ -9868,7 +10192,7 @@ async function probeChangedArtifacts(params) {
|
|
|
9868
10192
|
await executor({ text: `RELEASE SAVEPOINT ${SAVEPOINT}` });
|
|
9869
10193
|
}
|
|
9870
10194
|
if (after.state !== "present")
|
|
9871
|
-
throw new
|
|
10195
|
+
throw new VibeError48({ code: "VIBE_MIGRATION", message: `Extension artifact ${artifact.id} did not produce its object when probed; its create SQL cannot be verified against the replayed history`, meta: { reason: "artifact-probe-unavailable", id: artifact.id, identity } });
|
|
9872
10196
|
if (canonicalMigrationJson({ value: before.definition.facts }) === canonicalMigrationJson({ value: after.definition.facts }))
|
|
9873
10197
|
continue;
|
|
9874
10198
|
const definition = before.definition.facts.definition;
|
|
@@ -9878,8 +10202,8 @@ async function probeChangedArtifacts(params) {
|
|
|
9878
10202
|
}
|
|
9879
10203
|
|
|
9880
10204
|
// packages/migrate/src/index-predicates.ts
|
|
9881
|
-
import { VibeError as
|
|
9882
|
-
import { defaultIndexName as defaultIndexName3, qualifiedSqlName as
|
|
10205
|
+
import { VibeError as VibeError49 } from "@vibeorm/schema";
|
|
10206
|
+
import { defaultIndexName as defaultIndexName3, qualifiedSqlName as qualifiedSqlName12 } from "@vibeorm/schema";
|
|
9883
10207
|
function physicalIndexName(params) {
|
|
9884
10208
|
return params.index.name ?? defaultIndexName3({ table: params.table, columns: params.columns, unique: params.index.kind === "unique", dialect: "postgres" });
|
|
9885
10209
|
}
|
|
@@ -9906,7 +10230,7 @@ async function prepareIndexPredicateDiff(params) {
|
|
|
9906
10230
|
indexes.push(index);
|
|
9907
10231
|
continue;
|
|
9908
10232
|
}
|
|
9909
|
-
const deparsed = await deparseIndexPredicate({ executor: params.executor, table:
|
|
10233
|
+
const deparsed = await deparseIndexPredicate({ executor: params.executor, table: qualifiedSqlName12({ name: table, namespace: live.namespace, defaultNamespace: params.namespace }), where: next.where });
|
|
9910
10234
|
indexes.push(deparsed === index.where ? { ...index, where: next.where } : index);
|
|
9911
10235
|
}
|
|
9912
10236
|
models.push({ ...live, indexes });
|
|
@@ -9919,7 +10243,7 @@ async function deparseIndexPredicate(params) {
|
|
|
9919
10243
|
const match = /^CHECK \((.*)\)$/s.exec(definition);
|
|
9920
10244
|
return match === null ? undefined : match[1];
|
|
9921
10245
|
} catch (error) {
|
|
9922
|
-
if (error instanceof
|
|
10246
|
+
if (error instanceof VibeError49 && error.code === "VIBE_MIGRATION")
|
|
9923
10247
|
throw error;
|
|
9924
10248
|
return;
|
|
9925
10249
|
}
|
|
@@ -9943,19 +10267,23 @@ async function planSchemaMigration(params) {
|
|
|
9943
10267
|
assertRetainedMigrationObjects({ from: (previous?.schema.migrationObjects ?? []).filter((object) => object.identity.kind !== "namespace" || !schemaNamespaces({ schema }).includes(object.identity.name)), to: objects });
|
|
9944
10268
|
for (const object of objects)
|
|
9945
10269
|
if (object.dialect !== dialect)
|
|
9946
|
-
throw new
|
|
10270
|
+
throw new VibeError50({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Migration object ${object.id} requires ${object.dialect}`, meta: { feature: "migrationObject", dialect } });
|
|
9947
10271
|
const physical = params.physical ?? (dialect === "postgres" ? await introspectPhysicalPostgres({ executor, namespaces }) : dialect === "mysql" ? await introspectMysql({ executor }) : await introspectSqlite({ executor }));
|
|
9948
10272
|
const comparablePhysical = external === undefined ? physical : projectExternalSchema({ schema: physical, external });
|
|
9949
10273
|
if (previous !== undefined)
|
|
9950
10274
|
assertMigrationState({ report: await verifyMigrationState({ executor, expected: previous, dialect, ...params.probe === undefined ? {} : { probe: params.probe } }) });
|
|
9951
10275
|
const inventory = params.physicalInventory ?? external?.inventory ?? (await Promise.all((dialect === "postgres" ? namespaces : [namespace]).map((scope) => inspectMigrationInventory({ executor, dialect, namespace: scope })))).flat();
|
|
9952
10276
|
const comparisonInventory = external === undefined ? inventory : projectExternalInventory({ inventory, external });
|
|
9953
|
-
const
|
|
10277
|
+
const visibleModels = new Set(physical.models.map((model) => physicalNameKey({ name: model.dbName ?? model.name, namespace: model.namespace, dialect })));
|
|
10278
|
+
let visible;
|
|
9954
10279
|
for (const object of inventory) {
|
|
9955
10280
|
const identity = object.identity;
|
|
9956
|
-
if (identity.kind !== "table" ||
|
|
10281
|
+
if (identity.kind !== "table" || visibleModels.has(physicalNameKey({ ...identity.name, dialect })))
|
|
10282
|
+
continue;
|
|
10283
|
+
visible ??= normalizeSchema({ schema: physical, dialect });
|
|
10284
|
+
if (visible.joinTables.has(physicalNameKey({ ...identity.name, dialect })))
|
|
9957
10285
|
continue;
|
|
9958
|
-
throw new
|
|
10286
|
+
throw new VibeError50({ 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 } });
|
|
9959
10287
|
}
|
|
9960
10288
|
const states = resolved.unavailable.map((identity) => {
|
|
9961
10289
|
const exists = identity.kind === "column" && [physical, previous?.schema].some((source) => source?.models.some((model) => physicalNameKey({ name: model.dbName ?? model.name, namespace: model.namespace, dialect }) === physicalNameKey({ ...identity.table, dialect }) && model.fields.some((field) => (field.dbName ?? field.name) === identity.column)));
|
|
@@ -9966,13 +10294,13 @@ async function planSchemaMigration(params) {
|
|
|
9966
10294
|
const creates = [];
|
|
9967
10295
|
for (const object of objects) {
|
|
9968
10296
|
for (const dependency of object.dependsOn) {
|
|
9969
|
-
if ([...objects.map((entry) => entry.identity), ...schemaObjectIdentities({ schema, dialect, namespace })].some((identity) =>
|
|
10297
|
+
if ([...objects.map((entry) => entry.identity), ...schemaObjectIdentities({ schema, dialect, namespace })].some((identity) => sqlObjectKey18({ identity }) === sqlObjectKey18({ identity: dependency })))
|
|
9970
10298
|
continue;
|
|
9971
10299
|
const state = await inspectSqlObject({ executor, dialect, identity: dependency });
|
|
9972
10300
|
if (state.state !== "present")
|
|
9973
10301
|
states.push({ identity: dependency, ownership: "migration", state: "unavailable", reason: "dependency-unavailable", differences: [] });
|
|
9974
10302
|
}
|
|
9975
|
-
const expected = object.expected ?? previous?.definitions.find((entry) =>
|
|
10303
|
+
const expected = object.expected ?? previous?.definitions.find((entry) => sqlObjectKey18({ identity: entry.identity }) === sqlObjectKey18({ identity: object.identity }));
|
|
9976
10304
|
const live = await inspectSqlObject({ executor, dialect, identity: object.identity });
|
|
9977
10305
|
if (object.identity.kind === "namespace") {
|
|
9978
10306
|
states.push({ identity: object.identity, ownership: "schema", state: live.state === "present" ? "matching" : live.state, differences: [], ...live.state === "unavailable" ? { reason: live.reason } : {} });
|
|
@@ -9984,14 +10312,21 @@ async function planSchemaMigration(params) {
|
|
|
9984
10312
|
states.push({ identity: object.identity, ownership: "migration", state: "missing", differences: [] });
|
|
9985
10313
|
creates.push(...object.create);
|
|
9986
10314
|
} else {
|
|
9987
|
-
|
|
9988
|
-
states.push(...report.objects);
|
|
10315
|
+
states.push(compareInspectedSqlObject({ identity: object.identity, ownership: "migration", expected, live, dialect }));
|
|
9989
10316
|
}
|
|
9990
10317
|
}
|
|
10318
|
+
for (const object of legacyObjects)
|
|
10319
|
+
if (objects.some((entry) => sqlObjectKey18({ identity: entry.identity }) === sqlObjectKey18({ identity: object.identity })))
|
|
10320
|
+
throw new VibeError50({ code: "VIBE_SCHEMA_INVALID", message: "Duplicate combined and migration-only ownership", meta: { identity: object.identity } });
|
|
10321
|
+
const installed = await probeInstalledExtensionState({ executor, dialect, artifacts });
|
|
10322
|
+
const sqliteFts = dialect === "sqlite" && collected.objects.some((artifact) => artifact.owns.kind === "trigger" && artifact.create.length === 0 && artifact.drop.length === 0 && artifact.expected !== undefined) ? await planSqliteFtsUpdates({ executor, collected, installed }) : undefined;
|
|
9991
10323
|
for (const object of legacyObjects) {
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
10324
|
+
const qualifiedFts = sqliteFts?.states.get(sqlObjectKey18({ identity: object.identity }));
|
|
10325
|
+
if (qualifiedFts !== undefined) {
|
|
10326
|
+
states.push(qualifiedFts);
|
|
10327
|
+
continue;
|
|
10328
|
+
}
|
|
10329
|
+
const expected = object.expected ?? previous?.definitions.find((entry) => sqlObjectKey18({ identity: entry.identity }) === sqlObjectKey18({ identity: object.identity }));
|
|
9995
10330
|
if (expected === undefined)
|
|
9996
10331
|
states.push({ identity: object.identity, ownership: "migration", state: "unavailable", reason: "legacy-definition-required", differences: [] });
|
|
9997
10332
|
else {
|
|
@@ -9999,7 +10334,7 @@ async function planSchemaMigration(params) {
|
|
|
9999
10334
|
if (live.state === "missing")
|
|
10000
10335
|
states.push({ identity: object.identity, ownership: "migration", state: "missing", differences: [] });
|
|
10001
10336
|
else
|
|
10002
|
-
states.push(
|
|
10337
|
+
states.push(compareInspectedSqlObject({ identity: object.identity, ownership: "migration", expected, live, dialect }));
|
|
10003
10338
|
}
|
|
10004
10339
|
}
|
|
10005
10340
|
const protectedObjects = [...legacyObjects, ...objects, ...previous?.schema.migrationObjects ?? []];
|
|
@@ -10007,16 +10342,17 @@ async function planSchemaMigration(params) {
|
|
|
10007
10342
|
const checked = await prepareCheckDiff({ executor, from: stripped, to: schema, dialect, ...params.probe === undefined ? {} : { probe: params.probe }, ...previous === undefined ? {} : { previous }, ...params.replaceChecks === undefined ? {} : { replaceChecks: params.replaceChecks } });
|
|
10008
10343
|
const exclusionsPrepared = await prepareExclusionDiff({ ...params, ...previous === undefined ? {} : { previous }, from: checked, to: schema });
|
|
10009
10344
|
const from = dialect === "postgres" ? await prepareOperatorClassDiff({ executor, from: await prepareIndexPredicateDiff({ executor, from: exclusionsPrepared, to: schema, namespace, ...params.probe === undefined ? {} : { probe: params.probe } }), to: schema }) : exclusionsPrepared;
|
|
10010
|
-
const
|
|
10011
|
-
const
|
|
10012
|
-
const declaredIndexes = normalizeSchema({ schema: previous
|
|
10345
|
+
const comparison = diffSchemasWithNormalization({ from, to: schema, dialect });
|
|
10346
|
+
const steps = comparison.steps;
|
|
10347
|
+
const declaredIndexes = previous === undefined ? comparison.to : normalizeSchema({ schema: previous.schema, dialect });
|
|
10348
|
+
const known = new Set(schemaObjectIdentitiesFromNormalized({ schema: previous?.schema ?? schema, dialect, namespace, normalized: declaredIndexes }).map((identity) => sqlObjectKey18({ identity })));
|
|
10013
10349
|
const physicalIndexes = normalizeSchema({ schema: physical, dialect });
|
|
10014
10350
|
for (const [table, model] of physicalIndexes.tables)
|
|
10015
10351
|
for (const [key, index] of model.indexes) {
|
|
10016
10352
|
if (index.constraintName !== undefined && declaredIndexes.tables.get(table)?.indexes.has(key))
|
|
10017
|
-
known.add(
|
|
10353
|
+
known.add(sqlObjectKey18({ identity: { kind: "constraint", constraintType: "unique", table: qualifiedSqlName13({ name: model.table, namespace: model.namespace, defaultNamespace: namespace }), name: index.constraintName } }));
|
|
10018
10354
|
}
|
|
10019
|
-
const migrationOwned = new Set(protectedObjects.map((object) =>
|
|
10355
|
+
const migrationOwned = new Set(protectedObjects.map((object) => sqlObjectKey18({ identity: object.identity })));
|
|
10020
10356
|
const removals = steps.flatMap((step) => {
|
|
10021
10357
|
const identity = removalIdentity({ step, namespace });
|
|
10022
10358
|
return identity === undefined ? [] : [identity];
|
|
@@ -10025,30 +10361,30 @@ async function planSchemaMigration(params) {
|
|
|
10025
10361
|
for (const identity of removals) {
|
|
10026
10362
|
const owner = externalObjectOwner({ external, identity });
|
|
10027
10363
|
if (owner !== undefined)
|
|
10028
|
-
throw new
|
|
10364
|
+
throw new VibeError50({ code: "VIBE_MIGRATION", message: "Automatic DDL cannot remove externally owned objects", meta: { reason: "external-owned-removal", owner, identity } });
|
|
10029
10365
|
}
|
|
10030
10366
|
const enumRemovals = new Set(removals.flatMap((identity) => identity.kind === "enum" ? [physicalNameKey({ ...identity.name, dialect })] : []));
|
|
10031
10367
|
if (external !== undefined)
|
|
10032
10368
|
await refuseExternalDependents({ executor, dialect, external, steps, namespace });
|
|
10033
10369
|
const droppedTables = new Set(steps.flatMap((step) => step.kind === "dropTable" || step.kind === "dropJoinTable" ? [physicalNameKey({ name: step.table, namespace: step.namespace, dialect })] : []));
|
|
10034
10370
|
const unmanagedRemovals = [];
|
|
10035
|
-
const invalid = dialect === "sqlite" ? sqliteInvalidatedTables({ steps, from: physical }) : { rebuilt: new Set, dropped: new Set };
|
|
10371
|
+
const invalid = dialect === "sqlite" && steps.length > 0 ? sqliteInvalidatedTables({ steps, from: physical }) : { rebuilt: new Set, dropped: new Set };
|
|
10036
10372
|
if (dialect === "sqlite")
|
|
10037
10373
|
await assertSqliteRebuildTableOptions({ executor, tables: invalid.rebuilt });
|
|
10038
10374
|
for (const object of inventory) {
|
|
10039
|
-
const key =
|
|
10375
|
+
const key = sqlObjectKey18({ identity: object.identity });
|
|
10040
10376
|
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 && entry.identity.table.namespace === object.identity.table.namespace);
|
|
10041
|
-
const ownership = migrationOwned.has(key) ? "migration" : known.has(key) || normalizedOwner !== undefined && known.has(
|
|
10377
|
+
const ownership = migrationOwned.has(key) ? "migration" : known.has(key) || normalizedOwner !== undefined && known.has(sqlObjectKey18({ identity: normalizedOwner.identity })) ? "schema" : "unknown";
|
|
10042
10378
|
const externalOwner = external === undefined ? undefined : externalObjectOwner({ identity: object.identity, external });
|
|
10043
10379
|
if (object.inspectionUnavailable !== undefined)
|
|
10044
10380
|
states.push({ identity: object.identity, ownership, state: "unavailable", reason: object.inspectionUnavailable, differences: [] });
|
|
10045
10381
|
if (object.inspectionUnavailable !== undefined && invalid.rebuilt.has("table" in object.identity ? object.identity.table.name : typeof object.identity.name === "object" ? object.identity.name.name : object.identity.name))
|
|
10046
|
-
throw new
|
|
10382
|
+
throw new VibeError50({ 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 } });
|
|
10047
10383
|
if (ownership === "unknown" && comparisonInventory.includes(object))
|
|
10048
10384
|
states.push({ identity: object.identity, ownership, state: "unavailable", reason: "unmanaged", differences: [] });
|
|
10049
10385
|
let collateral = "table" in object.identity && droppedTables.has(physicalNameKey({ ...object.identity.table, dialect }));
|
|
10050
|
-
const declared = protectedObjects.find((entry) =>
|
|
10051
|
-
if (declared?.dependsOn.some((dependency) => removals.some((identity) =>
|
|
10386
|
+
const declared = protectedObjects.find((entry) => sqlObjectKey18({ identity: entry.identity }) === key);
|
|
10387
|
+
if (declared?.dependsOn.some((dependency) => removals.some((identity) => sqlObjectKey18({ identity }) === sqlObjectKey18({ identity: dependency })) || ("table" in dependency) && droppedTables.has(physicalNameKey({ ...dependency.table, dialect }))))
|
|
10052
10388
|
collateral = true;
|
|
10053
10389
|
if (ownership === "unknown" && object.identity.kind === "function" && steps.some((step) => step.kind === "dropTable" || step.kind === "dropColumn" || step.kind === "dropJoinTable" || step.kind === "dropEnum"))
|
|
10054
10390
|
states.push({ identity: object.identity, ownership, state: "unavailable", reason: "dependency-inspection-unavailable", differences: [] });
|
|
@@ -10062,7 +10398,7 @@ async function planSchemaMigration(params) {
|
|
|
10062
10398
|
for (const address of addresses) {
|
|
10063
10399
|
const value = JSON.parse(String(address));
|
|
10064
10400
|
if (!Array.isArray(value) || !Array.isArray(value[2]))
|
|
10065
|
-
throw new
|
|
10401
|
+
throw new VibeError50({ code: "VIBE_MIGRATION", message: "Malformed catalog dependency address" });
|
|
10066
10402
|
const names = value[2];
|
|
10067
10403
|
if (typeof names[0] !== "string" || typeof names[1] !== "string")
|
|
10068
10404
|
continue;
|
|
@@ -10074,22 +10410,23 @@ async function planSchemaMigration(params) {
|
|
|
10074
10410
|
}
|
|
10075
10411
|
}
|
|
10076
10412
|
if (dialect === "sqlite" && ownership === "unknown" && object.identity.kind === "generatedColumn" && invalid.rebuilt.has(object.identity.table.name))
|
|
10077
|
-
throw new
|
|
10078
|
-
const removed = collateral || removals.some((identity) =>
|
|
10413
|
+
throw new VibeError50({ code: "VIBE_MIGRATION", message: "SQLite generated-column preservation requires an independent ownership declaration", meta: { reason: "sqlite-preservation-unavailable", identity: object.identity } });
|
|
10414
|
+
const removed = collateral || removals.some((identity) => sqlObjectKey18({ identity }) === key);
|
|
10079
10415
|
if (removed && externalOwner !== undefined)
|
|
10080
|
-
throw new
|
|
10416
|
+
throw new VibeError50({ code: "VIBE_MIGRATION", message: "Automatic or collateral DDL cannot remove externally owned objects", meta: { reason: "external-owned-removal", owner: externalOwner, identity: object.identity } });
|
|
10081
10417
|
if (removed && ownership === "migration")
|
|
10082
|
-
throw new
|
|
10418
|
+
throw new VibeError50({ code: "VIBE_MIGRATION", message: "Automatic DDL would remove a migration-owned object", meta: { reason: "migration-owned-removal", identity: object.identity } });
|
|
10083
10419
|
if (removed && ownership === "unknown" && comparisonInventory.includes(object) && object.backingConstraint === undefined)
|
|
10084
10420
|
unmanagedRemovals.push(object.identity);
|
|
10085
10421
|
}
|
|
10086
10422
|
const invalidated = new Set([...invalid.rebuilt, ...invalid.dropped]);
|
|
10087
|
-
const installed = await probeInstalledExtensionState({ executor, dialect, artifacts });
|
|
10088
10423
|
const triggers = installed.triggers === undefined ? undefined : new Set(installed.triggers);
|
|
10089
10424
|
for (const artifact of artifacts)
|
|
10090
10425
|
if (artifact.owns.kind === "trigger" && invalidated.has(artifact.owns.table))
|
|
10091
10426
|
triggers?.delete(artifact.owns.name);
|
|
10092
|
-
const
|
|
10427
|
+
const absentArtifacts = planMissingArtifacts({ introspected: physical, artifacts, installed: { ...installed, ...triggers === undefined ? {} : { triggers } } });
|
|
10428
|
+
const absentIds = new Set(absentArtifacts.map((artifact) => artifact.id));
|
|
10429
|
+
const missing = artifacts.filter((artifact) => absentIds.has(artifact.id) || sqliteFts?.forcedArtifacts.has(artifact.id));
|
|
10093
10430
|
const declaredPrevious = params.previousDeclarations ?? previous?.schema;
|
|
10094
10431
|
const previousDeclarations = declaredPrevious === undefined || external === undefined ? declaredPrevious : projectExternalSchema({ schema: declaredPrevious, external });
|
|
10095
10432
|
const previousArtifacts = previousDeclarations === undefined ? undefined : collectExtensionArtifacts({ schema: previousDeclarations, dialect, extensions: params.extensions ?? [] });
|
|
@@ -10127,11 +10464,11 @@ async function planSchemaMigration(params) {
|
|
|
10127
10464
|
continue;
|
|
10128
10465
|
const live = await inspectSqlObject({ executor, dialect, identity });
|
|
10129
10466
|
if (live.state !== "present" || identity.kind !== "columnDefault" && typeof live.definition.facts.columnDefinition !== "string")
|
|
10130
|
-
throw new
|
|
10467
|
+
throw new VibeError50({ code: "VIBE_MIGRATION", message: "Cannot read the verified SQLite column definition for preservation", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
10131
10468
|
if (identity.kind === "columnDefault") {
|
|
10132
10469
|
const definition = live.definition.facts.definition;
|
|
10133
10470
|
if (definition !== null && typeof definition !== "string")
|
|
10134
|
-
throw new
|
|
10471
|
+
throw new VibeError50({ code: "VIBE_MIGRATION", message: "Cannot preserve SQLite default", meta: { identity } });
|
|
10135
10472
|
const defaults = preservedDefaults.get(identity.table.name) ?? new Map;
|
|
10136
10473
|
defaults.set(identity.column, definition);
|
|
10137
10474
|
preservedDefaults.set(identity.table.name, defaults);
|
|
@@ -10152,25 +10489,31 @@ async function planSchemaMigration(params) {
|
|
|
10152
10489
|
const expression = defaults.get(column);
|
|
10153
10490
|
return { ...rest, ...typeof expression === "string" ? { default: { kind: "dbgenerated", sql: expression } } : {} };
|
|
10154
10491
|
}) })) };
|
|
10155
|
-
const core = renderSteps({ steps, dialect, from: dialect === "sqlite" ? physical : from, to: dialect === "mysql" ? { ...target, migrationObjects: protectedObjects } : target, sqliteRestore: restore, sqliteColumns: preservedColumns });
|
|
10492
|
+
const core = steps.length === 0 ? [] : renderSteps({ steps, dialect, from: dialect === "sqlite" ? physical : from, to: dialect === "mysql" ? { ...target, migrationObjects: protectedObjects } : target, sqliteRestore: restore, sqliteColumns: preservedColumns });
|
|
10156
10493
|
const planned = new Set([...missing, ...rebuilt].map((artifact) => artifact.id));
|
|
10157
|
-
const
|
|
10158
|
-
...
|
|
10159
|
-
...core,
|
|
10494
|
+
const extensionSql = [
|
|
10495
|
+
...sqliteFts?.before ?? [],
|
|
10160
10496
|
...artifactDropSql({ artifacts: rebuilt }),
|
|
10161
|
-
...artifactCreateSql({ artifacts: collected.objects.filter((artifact) => planned.has(artifact.id) && !(artifact.owns.kind === "trigger" && invalid.rebuilt.has(artifact.owns.table))) }),
|
|
10497
|
+
...artifactCreateSql({ artifacts: collected.objects.filter((artifact) => planned.has(artifact.id) && (!(artifact.owns.kind === "trigger" && invalid.rebuilt.has(artifact.owns.table)) || sqliteFts?.forcedArtifacts.has(artifact.id))) }),
|
|
10498
|
+
...sqliteFts?.after ?? [],
|
|
10162
10499
|
...creates
|
|
10163
10500
|
];
|
|
10501
|
+
const atomicFts = dialect === "sqlite" && sqliteFts?.checks.some((check) => check.changes) === true && usesSelfManagedTransactions({ sql: core });
|
|
10502
|
+
const setupSql = artifactCreateSql({ artifacts: missing.filter((artifact) => artifact.owns.kind === "postgresExtension") });
|
|
10503
|
+
const sql = [...setupSql, ...core, ...atomicFts ? ["BEGIN;"] : [], ...extensionSql, ...atomicFts ? ["COMMIT;"] : []];
|
|
10164
10504
|
if (sql.length === 0) {
|
|
10165
10505
|
const definitions = [...previous?.definitions ?? []];
|
|
10166
10506
|
for (const object of objects)
|
|
10167
|
-
if (object.expected !== undefined && !definitions.some((definition) =>
|
|
10507
|
+
if (object.expected !== undefined && !definitions.some((definition) => sqlObjectKey18({ identity: definition.identity }) === sqlObjectKey18({ identity: object.identity })))
|
|
10168
10508
|
definitions.push(object.expected);
|
|
10169
10509
|
const report = await verifyMigrationState({ executor, dialect, expected: { schema, definitions }, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
10170
10510
|
states.push(...report.objects);
|
|
10171
10511
|
}
|
|
10172
|
-
const blocked = states.some((state) => state.reason === "dependency-inspection-unavailable" || state.ownership !== "unknown" && state.reason !== "legacy-definition-required" && (state.state === "unavailable" || state.state === "changed" || state.state === "missing" && sql.length === 0));
|
|
10173
|
-
|
|
10512
|
+
const blocked = states.some((state) => state.reason === "dependency-inspection-unavailable" || state.ownership !== "unknown" && state.reason !== "legacy-definition-required" && state.reason !== "sqlite-fts-update-upgrade" && (state.state === "unavailable" || state.state === "changed" || state.state === "missing" && sql.length === 0));
|
|
10513
|
+
const plan = { physical, from, to: schema, namespace, steps, objects: states, unmanagedRemovals, blocked, sql: blocked ? [] : sql, rebuiltArtifacts };
|
|
10514
|
+
if (sqliteFts !== undefined)
|
|
10515
|
+
retainSqliteFtsPlan({ plan, fts: sqliteFts, ...atomicFts ? { transaction: { begin: setupSql.length + core.length, commit: sql.length - 1 } } : {} });
|
|
10516
|
+
return plan;
|
|
10174
10517
|
}
|
|
10175
10518
|
|
|
10176
10519
|
// packages/migrate/src/replay-plan.ts
|
|
@@ -10196,13 +10539,13 @@ async function planReplayMigration(params) {
|
|
|
10196
10539
|
moduleHeads.set(migration.moduleOrigin.moduleId, migration);
|
|
10197
10540
|
const immutableObjects = new Map([...moduleHeads.values()].flatMap((migration) => {
|
|
10198
10541
|
const state = migration.moduleOrigin.source.desiredState.to;
|
|
10199
|
-
return (state.schema.migrationObjects ?? []).map((object) => [
|
|
10542
|
+
return (state.schema.migrationObjects ?? []).map((object) => [sqlObjectKey19({ identity: object.identity }), object.expected ?? state.definitions.find((definition) => sqlObjectKey19({ identity: definition.identity }) === sqlObjectKey19({ identity: object.identity }))]);
|
|
10200
10543
|
}));
|
|
10201
10544
|
const authored = reference.dialect === "postgres" ? (await resolvePostgresTypeIdentities({ executor, schema: params.schema, previous: reference.state.schema, mode: "verify" })).schema : params.schema;
|
|
10202
|
-
const compared = await compareReplayMigrationObjects({ executor, reference, schema: { ...authored, migrationObjects: (authored.migrationObjects ?? []).filter((object) => !immutableObjects.has(
|
|
10203
|
-
const comparedObjects = new Map((compared.migrationObjects ?? []).map((object) => [
|
|
10545
|
+
const compared = await compareReplayMigrationObjects({ executor, reference, schema: { ...authored, migrationObjects: (authored.migrationObjects ?? []).filter((object) => !immutableObjects.has(sqlObjectKey19({ identity: object.identity }))) } });
|
|
10546
|
+
const comparedObjects = new Map((compared.migrationObjects ?? []).map((object) => [sqlObjectKey19({ identity: object.identity }), object]));
|
|
10204
10547
|
const schema = { ...compared, migrationObjects: (params.schema.migrationObjects ?? []).map((object) => {
|
|
10205
|
-
const key =
|
|
10548
|
+
const key = sqlObjectKey19({ identity: object.identity });
|
|
10206
10549
|
if (!immutableObjects.has(key))
|
|
10207
10550
|
return comparedObjects.get(key);
|
|
10208
10551
|
const expected = immutableObjects.get(key);
|
|
@@ -10221,57 +10564,57 @@ async function planReplayMigration(params) {
|
|
|
10221
10564
|
const identity = removalIdentity({ step, namespace: plan.namespace });
|
|
10222
10565
|
return identity === undefined ? [] : [identity];
|
|
10223
10566
|
})])
|
|
10224
|
-
removals.set(
|
|
10225
|
-
const declared = new Set((schema.migrationObjects ?? []).map((object) =>
|
|
10567
|
+
removals.set(sqlObjectKey19({ identity }), identity);
|
|
10568
|
+
const declared = new Set((schema.migrationObjects ?? []).map((object) => sqlObjectKey19({ identity: object.identity })));
|
|
10226
10569
|
const historicalObjects = new Map;
|
|
10227
10570
|
for (const migration of params.migrations)
|
|
10228
10571
|
for (const object of migration.desiredState?.to.schema.migrationObjects ?? []) {
|
|
10229
10572
|
if (external === undefined || externalObjectOwner({ identity: object.identity, external }) === undefined)
|
|
10230
|
-
historicalObjects.set(
|
|
10573
|
+
historicalObjects.set(sqlObjectKey19({ identity: object.identity }), object.identity);
|
|
10231
10574
|
}
|
|
10232
|
-
const present = new Set(reference.inventory.map((object) =>
|
|
10233
|
-
const retainedObjects = reference.inventory.filter((object) => !declared.has(
|
|
10575
|
+
const present = new Set(reference.inventory.map((object) => sqlObjectKey19({ identity: object.identity })));
|
|
10576
|
+
const retainedObjects = reference.inventory.filter((object) => !declared.has(sqlObjectKey19({ identity: object.identity })) && !removals.has(sqlObjectKey19({ identity: object.identity })) && (object.identity.kind === "function" || object.identity.kind === "trigger" || historicalObjects.has(sqlObjectKey19({ identity: object.identity })))).map((object) => object.identity);
|
|
10234
10577
|
const removedRegistrations = [...historicalObjects].filter(([key]) => present.has(key) && !declared.has(key)).map(([, identity]) => identity);
|
|
10235
10578
|
if (removedRegistrations.length > 0)
|
|
10236
|
-
throw new
|
|
10579
|
+
throw new VibeError51({ code: "VIBE_MIGRATION", message: "Custom registrations removed from declarations still exist in replayed history. Restore the registration or supply reviewed SQL removing the exact named objects before generating again.", meta: { reason: "migration-object-removal-review-required", shadowReusable: true, objects: removedRegistrations } });
|
|
10237
10580
|
if (external !== undefined)
|
|
10238
10581
|
assertExternalOwnershipClaims({ external, identities: [...removals.values()] });
|
|
10239
10582
|
return { ...plan, requiredRemovals: [...removals.values()], retainedObjects, securityChanges: securityChangesOf({ steps: plan.steps }), replayRemovals: params.migrations.flatMap((migration) => migration.desiredState?.unmanagedRemovals ?? []).filter((identity) => external === undefined || externalObjectOwner({ identity, external }) === undefined) };
|
|
10240
10583
|
}
|
|
10241
10584
|
function assertReplayRemovals(params) {
|
|
10242
|
-
const wanted = new Set([...params.plan.requiredRemovals, ...params.plan.replayRemovals].map((identity) =>
|
|
10243
|
-
const keys = params.approved.map((identity) =>
|
|
10585
|
+
const wanted = new Set([...params.plan.requiredRemovals, ...params.plan.replayRemovals].map((identity) => sqlObjectKey19({ identity })));
|
|
10586
|
+
const keys = params.approved.map((identity) => sqlObjectKey19({ identity }));
|
|
10244
10587
|
if (new Set(keys).size !== keys.length)
|
|
10245
|
-
throw new
|
|
10246
|
-
const unused = params.approved.filter((identity) => !wanted.has(
|
|
10588
|
+
throw new VibeError51({ code: "VIBE_MIGRATION", message: "Removal decisions contain duplicate identities", meta: { reason: "removal-decision-invalid", shadowReusable: true } });
|
|
10589
|
+
const unused = params.approved.filter((identity) => !wanted.has(sqlObjectKey19({ identity })));
|
|
10247
10590
|
if (unused.length > 0)
|
|
10248
|
-
throw new
|
|
10591
|
+
throw new VibeError51({ code: "VIBE_MIGRATION", message: "Removal decisions do not identify proposed removals. Custom objects without a generated removal require a reviewed hand-authored SQL migration.", meta: { reason: "removal-decision-unused", shadowReusable: true, objects: unused } });
|
|
10249
10592
|
assertNamedRemovals({ removals: params.plan.requiredRemovals, approved: params.approved, acceptIntegrityLoss: params.acceptIntegrityLoss });
|
|
10250
10593
|
}
|
|
10251
10594
|
// packages/migrate/src/baseline.ts
|
|
10252
|
-
import { VibeError as
|
|
10595
|
+
import { VibeError as VibeError54, sqlObjectKey as sqlObjectKey21 } from "@vibeorm/schema";
|
|
10253
10596
|
|
|
10254
10597
|
// packages/migrate/src/reference-state.ts
|
|
10255
|
-
import { VibeError as
|
|
10598
|
+
import { VibeError as VibeError52, sqlObjectKey as sqlObjectKey20 } from "@vibeorm/schema";
|
|
10256
10599
|
async function captureMigrationReference(params) {
|
|
10257
10600
|
const namespace = await migrationNamespace({ executor: params.referenceExecutor, dialect: params.dialect });
|
|
10258
10601
|
const identities = [...schemaObjectIdentities({ schema: params.schema, dialect: params.dialect, namespace }), ...(params.schema.migrationObjects ?? []).map((object) => object.identity)];
|
|
10259
10602
|
const definitions = [];
|
|
10260
|
-
for (const identity of new Map(identities.map((identity) => [
|
|
10603
|
+
for (const identity of new Map(identities.map((identity) => [sqlObjectKey20({ identity }), identity])).values()) {
|
|
10261
10604
|
if (identity.kind === "namespace")
|
|
10262
10605
|
continue;
|
|
10263
|
-
if (!params.schema.migrationObjects?.some((object) =>
|
|
10606
|
+
if (!params.schema.migrationObjects?.some((object) => sqlObjectKey20({ identity: object.identity }) === sqlObjectKey20({ identity })) && !(identity.kind === "constraint" && (identity.constraintType === "check" || identity.constraintType === "exclusion")))
|
|
10264
10607
|
continue;
|
|
10265
10608
|
const result = await inspectSqlObject({ executor: params.referenceExecutor, dialect: params.dialect, identity });
|
|
10266
10609
|
if (result.state !== "present")
|
|
10267
|
-
throw new
|
|
10610
|
+
throw new VibeError52({ code: "VIBE_MIGRATION", message: "Independent reference object is missing or unavailable", meta: { reason: "reference-unavailable", identity, state: result.state } });
|
|
10268
10611
|
definitions.push(result.definition);
|
|
10269
10612
|
}
|
|
10270
10613
|
let schema = params.schema;
|
|
10271
10614
|
if (params.dialect === "postgres" && schema.models.some((model) => model.fields.some((field) => field.isUnsupported === true))) {
|
|
10272
10615
|
const resolved = await resolvePostgresTypeIdentities({ executor: params.referenceExecutor, schema, mode: "reference" });
|
|
10273
10616
|
if (resolved.unavailable.length > 0)
|
|
10274
|
-
throw new
|
|
10617
|
+
throw new VibeError52({ code: "VIBE_MIGRATION", message: "Resolve unsupported types in the independent reference before capture", meta: { reason: "type-reference-required", columns: resolved.unavailable } });
|
|
10275
10618
|
const physical = await introspectPhysicalPostgres({ executor: params.referenceExecutor, namespaces: ["public", ...schemaNamespaces({ schema })] });
|
|
10276
10619
|
for (const model of resolved.schema.models)
|
|
10277
10620
|
for (const field of model.fields) {
|
|
@@ -10279,7 +10622,7 @@ async function captureMigrationReference(params) {
|
|
|
10279
10622
|
continue;
|
|
10280
10623
|
const actual = physical.models.find((entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace }) === physicalNameKey({ name: model.dbName ?? model.name, namespace: model.namespace }))?.fields.find((entry) => entry.name === (field.dbName ?? field.name));
|
|
10281
10624
|
if (actual === undefined || columnTypeKey({ field }) !== columnTypeKey({ field: actual }))
|
|
10282
|
-
throw new
|
|
10625
|
+
throw new VibeError52({ code: "VIBE_MIGRATION", message: "Independent reference column does not match its declared PostgreSQL type", meta: { reason: "reference-type-mismatch", table: model.dbName ?? model.name, column: field.dbName ?? field.name } });
|
|
10283
10626
|
}
|
|
10284
10627
|
schema = resolved.schema;
|
|
10285
10628
|
}
|
|
@@ -10287,13 +10630,13 @@ async function captureMigrationReference(params) {
|
|
|
10287
10630
|
}
|
|
10288
10631
|
|
|
10289
10632
|
// packages/migrate/src/compile-desired-state.ts
|
|
10290
|
-
import { VibeError as
|
|
10633
|
+
import { VibeError as VibeError53, qualifiedSqlName as qualifiedSqlName14 } from "@vibeorm/schema";
|
|
10291
10634
|
import { renderExclusionSetup as renderExclusionSetup2, getSqlDialect as getSqlDialect16, renderMigrationServerVersion as renderMigrationServerVersion2 } from "@vibeorm/sql";
|
|
10292
10635
|
async function compileDesiredState(params) {
|
|
10293
10636
|
if (params.dialect !== "postgres")
|
|
10294
|
-
throw new
|
|
10637
|
+
throw new VibeError53({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Rollback declaration compilation requires PostgreSQL", meta: { feature: "desiredStateCompilation", dialect: params.dialect } });
|
|
10295
10638
|
if (params.steps.some((step) => step.kind === "alterColumn" || step.kind === "addColumn" || step.kind === "addEnumValue"))
|
|
10296
|
-
throw new
|
|
10639
|
+
throw new VibeError53({ 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" } });
|
|
10297
10640
|
const definitions = (params.schema.migrationObjects ?? []).flatMap((object) => object.expected === undefined ? [] : [object.expected]);
|
|
10298
10641
|
await params.executor({ text: "SAVEPOINT vibeorm_desired_state" });
|
|
10299
10642
|
try {
|
|
@@ -10302,7 +10645,7 @@ async function compileDesiredState(params) {
|
|
|
10302
10645
|
await params.executor({ text });
|
|
10303
10646
|
const models = [];
|
|
10304
10647
|
for (const model of params.schema.models) {
|
|
10305
|
-
const table =
|
|
10648
|
+
const table = qualifiedSqlName14({ name: model.dbName ?? model.name, namespace: model.namespace });
|
|
10306
10649
|
const exclusions = [];
|
|
10307
10650
|
for (const exclusion of model.exclusions ?? []) {
|
|
10308
10651
|
for (const text of renderExclusionSetup2({ dialect: getSqlDialect16({ dialect: "postgres" }), exclusion }))
|
|
@@ -10316,7 +10659,7 @@ async function compileDesiredState(params) {
|
|
|
10316
10659
|
const query = renderMigrationServerVersion2({ dialect: getSqlDialect16({ dialect: params.dialect }) });
|
|
10317
10660
|
const version = (await params.executor({ text: query.text, values: [] }))[0]?.version;
|
|
10318
10661
|
if (typeof version !== "string")
|
|
10319
|
-
throw new
|
|
10662
|
+
throw new VibeError53({ code: "VIBE_MIGRATION", message: "Cannot determine definition normalization version" });
|
|
10320
10663
|
return { ...createDesiredState({ schema: { ...params.schema, models }, dialect: params.dialect, definitions }), normalization: { format: "catalog-v1", dialect: params.dialect, serverVersion: version } };
|
|
10321
10664
|
} finally {
|
|
10322
10665
|
await params.executor({ text: "ROLLBACK TO SAVEPOINT vibeorm_desired_state" });
|
|
@@ -10329,19 +10672,19 @@ function generateBaselineMigration(params) {
|
|
|
10329
10672
|
const objects = orderedMigrationObjects({ objects: params.schema.migrationObjects ?? [] });
|
|
10330
10673
|
for (const object of objects)
|
|
10331
10674
|
if (object.dialect !== params.dialect)
|
|
10332
|
-
throw new
|
|
10675
|
+
throw new VibeError54({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Baseline object ${object.id} requires ${object.dialect}`, meta: { identity: object.identity, dialect: params.dialect } });
|
|
10333
10676
|
const prerequisites = objects.filter((object) => object.identity.kind === "postgresExtension" || object.identity.kind === "enum" || object.identity.kind === "sequence");
|
|
10334
|
-
const prerequisiteKeys = new Set(prerequisites.map((object) =>
|
|
10677
|
+
const prerequisiteKeys = new Set(prerequisites.map((object) => sqlObjectKey21({ identity: object.identity })));
|
|
10335
10678
|
for (const object of prerequisites) {
|
|
10336
|
-
const unresolved = object.dependsOn.filter((identity) => !prerequisiteKeys.has(
|
|
10679
|
+
const unresolved = object.dependsOn.filter((identity) => !prerequisiteKeys.has(sqlObjectKey21({ identity })));
|
|
10337
10680
|
if (unresolved.length > 0)
|
|
10338
|
-
throw new
|
|
10681
|
+
throw new VibeError54({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Baseline prerequisite depends on objects created later. Supply reviewed initial SQL with an explicit dependency order.", meta: { reason: "baseline-prerequisite-order", identity: object.identity, dependencies: unresolved } });
|
|
10339
10682
|
}
|
|
10340
|
-
const schema = { ...params.schema, migrationObjects: objects.filter((object) => !prerequisiteKeys.has(
|
|
10683
|
+
const schema = { ...params.schema, migrationObjects: objects.filter((object) => !prerequisiteKeys.has(sqlObjectKey21({ identity: object.identity }))) };
|
|
10341
10684
|
const generated = generateMigration({ from: { datasource: schema.datasource, models: [], enums: [], extensions: [] }, to: schema, name: params.name, dialect: params.dialect, extensions: params.extensions ?? [] });
|
|
10342
10685
|
const sql = splitSqlStatements({ sql: joinSqlStatements({ sql: [...prerequisites.flatMap((object) => object.create), ...generated.sql] }), dialect: params.dialect });
|
|
10343
10686
|
if (sql.length === 0)
|
|
10344
|
-
throw new
|
|
10687
|
+
throw new VibeError54({ code: "VIBE_MIGRATION", message: "A baseline must create declared database objects", meta: { reason: "baseline-empty" } });
|
|
10345
10688
|
return {
|
|
10346
10689
|
name: generated.name,
|
|
10347
10690
|
sql,
|
|
@@ -10353,8 +10696,8 @@ function generateBaselineMigration(params) {
|
|
|
10353
10696
|
async function verifyBaselineDeclarations(params) {
|
|
10354
10697
|
const captured = await captureMigrationReference({ referenceExecutor: params.executor, schema: params.schema, dialect: params.dialect });
|
|
10355
10698
|
const authored = (params.schema.migrationObjects ?? []).flatMap((object) => object.expected === undefined ? [] : [object.expected]);
|
|
10356
|
-
const customKeys = new Set((params.schema.migrationObjects ?? []).map((object) =>
|
|
10357
|
-
const customDefinitions = captured.definitions.filter((definition) => customKeys.has(
|
|
10699
|
+
const customKeys = new Set((params.schema.migrationObjects ?? []).map((object) => sqlObjectKey21({ identity: object.identity })));
|
|
10700
|
+
const customDefinitions = captured.definitions.filter((definition) => customKeys.has(sqlObjectKey21({ identity: definition.identity })) && !authored.some((entry) => sqlObjectKey21({ identity: entry.identity }) === sqlObjectKey21({ identity: definition.identity })));
|
|
10358
10701
|
const verify = async () => {
|
|
10359
10702
|
const compiled = params.dialect === "postgres" ? await compileDesiredState({ ...params, schema: captured.schema, steps: [], probe: "rollback" }) : { schema: captured.schema, definitions: authored };
|
|
10360
10703
|
const expected = { ...compiled, definitions: [...compiled.definitions, ...customDefinitions] };
|
|
@@ -10370,48 +10713,48 @@ async function verifyBaselineDeclarations(params) {
|
|
|
10370
10713
|
async function verifyBaselineTarget(params) {
|
|
10371
10714
|
const { reference } = params;
|
|
10372
10715
|
if (reference.unavailable.length > 0 || reference.inventory.some((object) => object.inspectionUnavailable !== undefined))
|
|
10373
|
-
throw new
|
|
10716
|
+
throw new VibeError54({ code: "VIBE_MIGRATION", message: "Baseline reference contains objects that cannot be inspected. Supply supported reviewed declarations before adoption.", meta: { reason: "baseline-reference-unavailable", objects: reference.unavailable, inventory: reference.inventory.filter((object) => object.inspectionUnavailable !== undefined) } });
|
|
10374
10717
|
const comparison = await verifyMigrationReference({ executor: params.executor, dialect: reference.dialect, reference, referenceExecutor: params.referenceExecutor }).catch((cause) => {
|
|
10375
|
-
if (cause instanceof
|
|
10376
|
-
throw new
|
|
10718
|
+
if (cause instanceof VibeError54 && cause.meta.reason === "verification-rls-reference-required")
|
|
10719
|
+
throw new VibeError54({ code: cause.code, message: cause.message, meta: { reason: "baseline-rls-reference-required" }, cause });
|
|
10377
10720
|
throw cause;
|
|
10378
10721
|
});
|
|
10379
10722
|
const security = comparison.objects.filter((object) => object.reason === "native-rls-mismatch");
|
|
10380
10723
|
if (security.length > 0)
|
|
10381
|
-
throw new
|
|
10724
|
+
throw new VibeError54({ code: "VIBE_MIGRATION", message: "Target native RLS policies or flags differ from the independent baseline. Restore the reviewed security declarations before recording; no target policies were changed.", meta: { reason: "baseline-rls-mismatch", shadowReusable: true, expected: security.flatMap((object) => object.differences.flatMap((difference) => difference.expected === undefined ? [] : [difference.expected])), actual: security.flatMap((object) => object.differences.flatMap((difference) => difference.actual === undefined ? [] : [difference.actual])) } });
|
|
10382
10725
|
if (comparison.unavailable.length > 0)
|
|
10383
|
-
throw new
|
|
10726
|
+
throw new VibeError54({ code: "VIBE_MIGRATION", message: "Baseline comparison contains objects whose definitions cannot be verified in the target context; use a supported reference before adoption.", meta: { reason: "baseline-target-unavailable", shadowReusable: true, objects: comparison.unavailable } });
|
|
10384
10727
|
const report = { inSync: comparison.objects.every((object) => object.state === "matching"), objects: comparison.objects };
|
|
10385
10728
|
assertMigrationState({ report });
|
|
10386
10729
|
const unmanaged = comparison.unmanaged.map((object) => object.identity);
|
|
10387
|
-
const extras = new Set(unmanaged.map((identity) =>
|
|
10730
|
+
const extras = new Set(unmanaged.map((identity) => sqlObjectKey21({ identity })));
|
|
10388
10731
|
const exclusions = params.excludeUnmanaged ?? [];
|
|
10389
|
-
const excluded = new Set(exclusions.map((identity) =>
|
|
10390
|
-
if (excluded.size !== exclusions.length || exclusions.some((identity) => !extras.has(
|
|
10391
|
-
throw new
|
|
10392
|
-
const unreviewed = unmanaged.filter((identity) => !excluded.has(
|
|
10732
|
+
const excluded = new Set(exclusions.map((identity) => sqlObjectKey21({ identity })));
|
|
10733
|
+
if (excluded.size !== exclusions.length || exclusions.some((identity) => !extras.has(sqlObjectKey21({ identity }))))
|
|
10734
|
+
throw new VibeError54({ code: "VIBE_CONFIG", message: "Baseline exclusions must uniquely name actual unmanaged target objects, never declared objects", meta: { reason: "baseline-exclusion-invalid", shadowReusable: true, exclusions } });
|
|
10735
|
+
const unreviewed = unmanaged.filter((identity) => !excluded.has(sqlObjectKey21({ identity })));
|
|
10393
10736
|
if (unreviewed.length > 0)
|
|
10394
|
-
throw new
|
|
10737
|
+
throw new VibeError54({ code: "VIBE_MIGRATION", message: "Target has unmanaged objects. Register their SQL or explicitly exclude each identity with --exclude-unmanaged; no target objects were removed.", meta: { reason: "baseline-unmanaged-objects", shadowReusable: true, objects: unreviewed } });
|
|
10395
10738
|
return { ...report, unmanaged };
|
|
10396
10739
|
}
|
|
10397
10740
|
// packages/migrate/src/baseline-record.ts
|
|
10398
|
-
import { VibeError as
|
|
10741
|
+
import { VibeError as VibeError55 } from "@vibeorm/schema";
|
|
10399
10742
|
import { getSqlDialect as getSqlDialect17, renderStatement } from "@vibeorm/sql";
|
|
10400
10743
|
function transactionFailure2(params) {
|
|
10401
|
-
if (params.cause instanceof
|
|
10744
|
+
if (params.cause instanceof VibeError55)
|
|
10402
10745
|
return params.cause;
|
|
10403
|
-
return new
|
|
10746
|
+
return new VibeError55({ code: "VIBE_MIGRATION", message: "SQLite baseline recording failed; inspect the journal and transaction outcome before retrying.", meta: { reason: "baseline-record-transaction-failed" }, cause: params.cause });
|
|
10404
10747
|
}
|
|
10405
10748
|
async function recordBaselineMigration(params) {
|
|
10406
10749
|
if (params.migration.moduleOrigin !== undefined)
|
|
10407
|
-
throw new
|
|
10750
|
+
throw new VibeError55({ code: "VIBE_MIGRATION", message: "Package module migrations must use the immutable module installation workflow, not baseline adoption", meta: { reason: "baseline-module-unsupported" } });
|
|
10408
10751
|
validateMigrationReplay({ dialect: params.dialect, migrations: [params.migration], acceptSecurityChanges: params.acceptSecurityChanges });
|
|
10409
10752
|
await withMigrationLock({ executor: params.executor, dialect: params.dialect, run: async (executor) => {
|
|
10410
10753
|
const record = async () => {
|
|
10411
10754
|
const assertEmpty = async () => {
|
|
10412
10755
|
const history = await readMigrationHistory({ executor, migrations: [params.migration], dialect: params.dialect });
|
|
10413
10756
|
if (history.recorded.length > 0)
|
|
10414
|
-
throw new
|
|
10757
|
+
throw new VibeError55({ code: "VIBE_MIGRATION", message: "Baseline recording requires an absent or empty migration journal. Use normal apply or explicit recovery for existing history.", meta: { reason: "baseline-journal-not-empty", shadowReusable: true, recorded: history.recorded.map((row) => ({ name: row.name, state: row.state })) } });
|
|
10415
10758
|
};
|
|
10416
10759
|
await assertEmpty();
|
|
10417
10760
|
await params.verify({ executor });
|
|
@@ -10436,14 +10779,14 @@ async function recordBaselineMigration(params) {
|
|
|
10436
10779
|
await executor({ text: "COMMIT" });
|
|
10437
10780
|
}).then(undefined, async (cause) => {
|
|
10438
10781
|
await executor({ text: "ROLLBACK" }).then(undefined, (rollback) => {
|
|
10439
|
-
throw new
|
|
10782
|
+
throw new VibeError55({ code: "VIBE_MIGRATION", message: "Baseline recording failed and its SQLite transaction could not be rolled back. Inspect the journal and connection state before retrying; the outcome is uncertain.", meta: { reason: "baseline-record-rollback-failed" }, cause: new AggregateError([cause, rollback], "Baseline recording and rollback failed") });
|
|
10440
10783
|
});
|
|
10441
10784
|
throw transactionFailure2({ cause });
|
|
10442
10785
|
});
|
|
10443
10786
|
} });
|
|
10444
10787
|
}
|
|
10445
10788
|
// packages/migrate/src/down.ts
|
|
10446
|
-
import { VibeError as
|
|
10789
|
+
import { VibeError as VibeError56, sqlObjectKey as sqlObjectKey22 } from "@vibeorm/schema";
|
|
10447
10790
|
function buildRollbackTarget(params) {
|
|
10448
10791
|
const { from, to, dialect } = params;
|
|
10449
10792
|
const skipped = [];
|
|
@@ -10547,10 +10890,10 @@ function generateDownMigration(params) {
|
|
|
10547
10890
|
}
|
|
10548
10891
|
restoreSql.push(...entry.restore);
|
|
10549
10892
|
}
|
|
10550
|
-
const previousKeys = new Set((from.migrationObjects ?? []).map((object) =>
|
|
10893
|
+
const previousKeys = new Set((from.migrationObjects ?? []).map((object) => sqlObjectKey22({ identity: object.identity })));
|
|
10551
10894
|
const ownedDropSql = [];
|
|
10552
10895
|
for (const object of orderedMigrationObjects({ objects: to.migrationObjects ?? [] }).reverse()) {
|
|
10553
|
-
if (previousKeys.has(
|
|
10896
|
+
if (previousKeys.has(sqlObjectKey22({ identity: object.identity })))
|
|
10554
10897
|
continue;
|
|
10555
10898
|
if (object.identity.kind === "namespace") {
|
|
10556
10899
|
skipped.push(`namespace ${object.identity.name} remains installed`);
|
|
@@ -10561,7 +10904,7 @@ function generateDownMigration(params) {
|
|
|
10561
10904
|
continue;
|
|
10562
10905
|
}
|
|
10563
10906
|
if (object.drop.length === 0)
|
|
10564
|
-
throw new
|
|
10907
|
+
throw new VibeError56({ code: "VIBE_MIGRATION", message: `Migration object ${object.id} needs reviewed reverse SQL`, meta: { reason: "migration-object-reverse-required", id: object.id } });
|
|
10565
10908
|
ownedDropSql.push(...object.drop);
|
|
10566
10909
|
}
|
|
10567
10910
|
return {
|
|
@@ -10574,11 +10917,11 @@ function generateDownMigration(params) {
|
|
|
10574
10917
|
};
|
|
10575
10918
|
}
|
|
10576
10919
|
// packages/migrate/src/push.ts
|
|
10577
|
-
import { VibeError as
|
|
10920
|
+
import { VibeError as VibeError57, assertValidSchemaIR as assertValidSchemaIR4, providerToDialect as providerToDialect7, sqlObjectKey as sqlObjectKey23, unsupportedCapability as unsupportedCapability14 } from "@vibeorm/schema";
|
|
10578
10921
|
import { DIALECT_CAPABILITIES as DIALECT_CAPABILITIES4 } from "@vibeorm/sql";
|
|
10579
10922
|
async function push(params) {
|
|
10580
10923
|
assertValidSchemaIR4({ schema: params.schema });
|
|
10581
|
-
const dialect =
|
|
10924
|
+
const dialect = providerToDialect7({ provider: params.schema.datasource.provider });
|
|
10582
10925
|
if (params.rebuildArtifacts === true && dialect !== "postgres")
|
|
10583
10926
|
throw unsupportedCapability14({ dialect, feature: "rebuilding extension artifacts on push (rebuildArtifacts)", hint: "drop the extension's objects by hand and push again; the transactional rebuild exists on PostgreSQL only" });
|
|
10584
10927
|
if (dialect !== "postgres")
|
|
@@ -10618,11 +10961,11 @@ async function pushChanges(params) {
|
|
|
10618
10961
|
if (params.dryRun === true)
|
|
10619
10962
|
return { steps, applied: [], planned: sql, securityChanges, objects: plan.objects, blocked: plan.blocked, unmanagedRemovals: plan.unmanagedRemovals };
|
|
10620
10963
|
if (plan.blocked)
|
|
10621
|
-
throw new
|
|
10964
|
+
throw new VibeError57({ code: "VIBE_MIGRATION", message: "Schema push is blocked by unverified object definitions", meta: { reason: "migration-state-unverified", objects: plan.objects } });
|
|
10622
10965
|
assertSecurityAcknowledgement({ securityChanges, ...params.acceptSecurityChanges === undefined ? {} : { acceptSecurityChanges: params.acceptSecurityChanges } });
|
|
10623
10966
|
const destructive = steps.filter((step) => step.destructive);
|
|
10624
10967
|
if (destructive.length > 0 && params.acceptDataLoss !== true)
|
|
10625
|
-
throw new
|
|
10968
|
+
throw new VibeError57({ 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 })) } });
|
|
10626
10969
|
assertRemovalAuthorization({
|
|
10627
10970
|
steps,
|
|
10628
10971
|
dialect,
|
|
@@ -10631,16 +10974,26 @@ async function pushChanges(params) {
|
|
|
10631
10974
|
...params.acceptIntegrityLoss === undefined ? {} : { acceptIntegrityLoss: params.acceptIntegrityLoss },
|
|
10632
10975
|
...params.removeUnmanaged === undefined ? {} : { removeUnmanaged: params.removeUnmanaged }
|
|
10633
10976
|
});
|
|
10634
|
-
const approved = new Set((params.removeUnmanaged ?? []).map((identity) =>
|
|
10635
|
-
const unapproved = plan.unmanagedRemovals.filter((identity) => !approved.has(
|
|
10977
|
+
const approved = new Set((params.removeUnmanaged ?? []).map((identity) => sqlObjectKey23({ identity })));
|
|
10978
|
+
const unapproved = plan.unmanagedRemovals.filter((identity) => !approved.has(sqlObjectKey23({ identity })));
|
|
10636
10979
|
if (unapproved.length > 0)
|
|
10637
|
-
throw new
|
|
10980
|
+
throw new VibeError57({ code: "VIBE_MIGRATION", message: "DDL would remove unmanaged dependent objects; approve their exact identities", meta: { reason: "unmanaged-removal-required", objects: unapproved } });
|
|
10638
10981
|
const wrap = dialect !== "postgres" && DIALECT_CAPABILITIES4[dialect].transactionalDdl && !usesSelfManagedTransactions({ sql });
|
|
10982
|
+
const ftsTransaction = dialect === "sqlite" ? sqliteFtsTransaction({ plan }) : undefined;
|
|
10639
10983
|
if (wrap)
|
|
10640
10984
|
await executor({ text: "BEGIN" });
|
|
10641
10985
|
try {
|
|
10642
|
-
|
|
10986
|
+
if (dialect === "sqlite" && ftsTransaction === undefined)
|
|
10987
|
+
await assertSqliteFtsApplyState({ plan, executor });
|
|
10988
|
+
for (const [index, statement] of sql.entries()) {
|
|
10989
|
+
if (ftsTransaction?.commit === index)
|
|
10990
|
+
await verifySqliteFtsApplyState({ plan, executor });
|
|
10643
10991
|
await executeStatement({ executor, statement, dialect });
|
|
10992
|
+
if (ftsTransaction?.begin === index)
|
|
10993
|
+
await assertSqliteFtsApplyState({ plan, executor });
|
|
10994
|
+
}
|
|
10995
|
+
if (dialect === "sqlite" && ftsTransaction === undefined)
|
|
10996
|
+
await verifySqliteFtsApplyState({ plan, executor });
|
|
10644
10997
|
if ((schema.migrationObjects?.length ?? 0) > 0)
|
|
10645
10998
|
assertMigrationState({ report: await verifyMigrationState({ executor, dialect, expected: createDesiredState({ schema: { ...schema, models: [] }, dialect }), ...params.external === undefined ? {} : { external: params.external } }) });
|
|
10646
10999
|
if (wrap)
|
|
@@ -10652,7 +11005,7 @@ async function pushChanges(params) {
|
|
|
10652
11005
|
} catch {}
|
|
10653
11006
|
} else if (dialect === "sqlite")
|
|
10654
11007
|
await cleanupSelfManagedFailure({ executor });
|
|
10655
|
-
throw new
|
|
11008
|
+
throw new VibeError57({ code: "VIBE_MIGRATION", message: `push failed while applying schema changes: ${error instanceof Error ? error.message : String(error)}`, meta: { statements: sql }, cause: error });
|
|
10656
11009
|
}
|
|
10657
11010
|
return { steps, applied: sql, securityChanges, objects: plan.objects, desiredState: createDesiredState({ schema: plan.to, dialect }) };
|
|
10658
11011
|
}
|
|
@@ -10667,11 +11020,11 @@ async function pull(params) {
|
|
|
10667
11020
|
}
|
|
10668
11021
|
}
|
|
10669
11022
|
// packages/migrate/src/module-export.ts
|
|
10670
|
-
import { VibeError as
|
|
11023
|
+
import { VibeError as VibeError58, providerToDialect as providerToDialect8 } from "@vibeorm/schema";
|
|
10671
11024
|
function runtimeSchema(params) {
|
|
10672
11025
|
const byName = (left, right) => left.name.localeCompare(right.name);
|
|
10673
11026
|
return {
|
|
10674
|
-
datasource: { provider:
|
|
11027
|
+
datasource: { provider: providerToDialect8({ provider: params.schema.datasource.provider }) },
|
|
10675
11028
|
models: [...params.schema.models].sort(byName),
|
|
10676
11029
|
enums: [...params.schema.enums].sort(byName),
|
|
10677
11030
|
extensions: [...params.schema.extensions].sort(byName),
|
|
@@ -10682,13 +11035,13 @@ function emitModuleMigrationExport(params) {
|
|
|
10682
11035
|
moduleMigrationFiles({ bundle: params.bundle, prefix: "export" });
|
|
10683
11036
|
const origin = params.bundle.migrations.at(-1).moduleOrigin;
|
|
10684
11037
|
if (!origin.contracts.includes(params.contract))
|
|
10685
|
-
throw new
|
|
11038
|
+
throw new VibeError58({ code: "VIBE_CONFIG", message: "The package runtime contract is not supported by its migration head", meta: { reason: "module-contract-mismatch", moduleId: origin.moduleId } });
|
|
10686
11039
|
if (params.client !== undefined) {
|
|
10687
11040
|
const { module, schema } = params.client;
|
|
10688
|
-
if (module.id !== origin.moduleId || module.revision !== origin.revision || module.contract !== params.contract ||
|
|
10689
|
-
throw new
|
|
11041
|
+
if (module.id !== origin.moduleId || module.revision !== origin.revision || module.contract !== params.contract || providerToDialect8({ provider: schema.datasource.provider }) !== origin.dialect)
|
|
11042
|
+
throw new VibeError58({ code: "VIBE_CONFIG", message: "The generated module identity, revision or contract differs from its migration head", meta: { reason: "module-client-head-mismatch", moduleId: origin.moduleId } });
|
|
10690
11043
|
if (canonicalMigrationJson({ value: runtimeSchema({ schema }) }) !== canonicalMigrationJson({ value: runtimeSchema({ schema: origin.source.desiredState.to.schema }) }))
|
|
10691
|
-
throw new
|
|
11044
|
+
throw new VibeError58({ code: "VIBE_CONFIG", message: "The generated module schema differs from its immutable migration head", meta: { reason: "module-client-schema-mismatch", moduleId: origin.moduleId } });
|
|
10692
11045
|
}
|
|
10693
11046
|
const expectation = { moduleId: origin.moduleId, revision: origin.revision, artifactChecksum: origin.artifactChecksum, contract: params.contract };
|
|
10694
11047
|
return [
|
|
@@ -10707,16 +11060,16 @@ export const moduleReadiness = Object.freeze(${JSON.stringify(expectation)});
|
|
|
10707
11060
|
];
|
|
10708
11061
|
}
|
|
10709
11062
|
// packages/migrate/src/module-readiness.ts
|
|
10710
|
-
import { VibeError as
|
|
11063
|
+
import { VibeError as VibeError59 } from "@vibeorm/schema";
|
|
10711
11064
|
async function inspectModuleReadiness(params) {
|
|
10712
11065
|
const ids = new Set;
|
|
10713
11066
|
for (const expected of params.modules) {
|
|
10714
11067
|
if (ids.has(expected.moduleId) || !expected.moduleId || !expected.revision || !expected.contract || !/^[a-f0-9]{64}$/.test(expected.artifactChecksum))
|
|
10715
|
-
throw new
|
|
11068
|
+
throw new VibeError59({ code: "VIBE_CONFIG", message: "Module readiness needs a unique module id, revision, contract and immutable artifact checksum", meta: { reason: "module-readiness-expectation", moduleId: expected.moduleId } });
|
|
10716
11069
|
ids.add(expected.moduleId);
|
|
10717
11070
|
}
|
|
10718
11071
|
if (params.compatibility !== undefined && params.compatibility !== "exact" && params.compatibility !== "contract")
|
|
10719
|
-
throw new
|
|
11072
|
+
throw new VibeError59({ code: "VIBE_CONFIG", message: "Unknown module compatibility mode", meta: { reason: "module-readiness-expectation" } });
|
|
10720
11073
|
const records = await readModuleJournal(params);
|
|
10721
11074
|
const heads = moduleHistoryHeads({ records });
|
|
10722
11075
|
const unresolved = [...records.values()].some(({ row, entry }) => row.error !== undefined || row.statementIndex < 0 || entry !== undefined && (!entry.complete || row.statementIndex !== entry.migration.sql.length));
|
|
@@ -10751,7 +11104,7 @@ async function inspectModuleReadiness(params) {
|
|
|
10751
11104
|
return { ready: !unresolved && modules.every((module) => (module.revision === "matching" || module.revision === "compatible") && module.structural.status !== "drifted"), modules };
|
|
10752
11105
|
}
|
|
10753
11106
|
// packages/migrate/src/adoption.ts
|
|
10754
|
-
import { VibeError as
|
|
11107
|
+
import { VibeError as VibeError60 } from "@vibeorm/schema";
|
|
10755
11108
|
async function adoptMigration(params) {
|
|
10756
11109
|
const report = await verifyMigrationState({ executor: params.executor, expected: params.reference, dialect: params.dialect });
|
|
10757
11110
|
assertMigrationState({ report });
|
|
@@ -10767,11 +11120,11 @@ function deployedDesiredState(params) {
|
|
|
10767
11120
|
for (const [index, row] of params.recorded.entries()) {
|
|
10768
11121
|
const file = params.migrations[index];
|
|
10769
11122
|
if (row.state !== "applied" || row.checksum === "" || file?.name !== row.name || migrationChecksum(file) !== row.checksum)
|
|
10770
|
-
throw new
|
|
11123
|
+
throw new VibeError60({ code: "VIBE_MIGRATION", message: "Desired state requires complete, checksum-verified migration history", meta: { reason: "desired-state-history-unverified", migration: row.name, state: row.state } });
|
|
10771
11124
|
}
|
|
10772
11125
|
const applied = new Set(params.recorded.map((row) => row.name));
|
|
10773
11126
|
if (params.migrations.some((migration) => applied.has(migration.name) && migration.desiredStatePending === true))
|
|
10774
|
-
throw new
|
|
11127
|
+
throw new VibeError60({ code: "VIBE_MIGRATION", message: "Applied unfinalized metadata cannot establish desired state", meta: { reason: "desired-state-history-unverified" } });
|
|
10775
11128
|
const last = [...params.migrations].reverse().find((migration) => applied.has(migration.name));
|
|
10776
11129
|
return last?.desiredState?.to;
|
|
10777
11130
|
}
|
|
@@ -10886,4 +11239,4 @@ export {
|
|
|
10886
11239
|
withMigrationReference
|
|
10887
11240
|
};
|
|
10888
11241
|
|
|
10889
|
-
//# debugId=
|
|
11242
|
+
//# debugId=F91BB3120BEADA3064756E2164756E21
|