@vibeorm/migrate 2.5.0 → 2.7.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/README.md +2 -0
- package/dist/compare-sqlite-primary-key.d.ts +1 -1
- package/dist/compare-sqlite-primary-key.d.ts.map +1 -1
- package/dist/ddl/mysql.d.ts.map +1 -1
- package/dist/ddl/postgres.d.ts +1 -1
- package/dist/ddl/rls.d.ts.map +1 -1
- package/dist/ddl/sqlite.d.ts.map +1 -1
- package/dist/differ.d.ts.map +1 -1
- package/dist/down.d.ts.map +1 -1
- package/dist/enum-values.d.ts +1 -0
- package/dist/enum-values.d.ts.map +1 -1
- package/dist/exclusions.d.ts +1 -0
- package/dist/exclusions.d.ts.map +1 -1
- package/dist/external-dependencies.d.ts +37 -0
- package/dist/external-dependencies.d.ts.map +1 -0
- package/dist/external-objects.d.ts +122 -0
- package/dist/external-objects.d.ts.map +1 -0
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +1303 -662
- package/dist/fs.js.map +34 -30
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3791 -2738
- package/dist/index.js.map +55 -49
- package/dist/inspect-objects.d.ts +5 -0
- package/dist/inspect-objects.d.ts.map +1 -1
- package/dist/introspect/postgres-rls.d.ts.map +1 -1
- package/dist/introspect/postgres.d.ts +4 -2
- package/dist/introspect/postgres.d.ts.map +1 -1
- package/dist/introspect/shared.d.ts +2 -1
- package/dist/introspect/shared.d.ts.map +1 -1
- package/dist/inventory.d.ts.map +1 -1
- package/dist/migration-checksum.d.ts +2 -0
- package/dist/migration-checksum.d.ts.map +1 -1
- package/dist/module-adoption-record.d.ts +22 -0
- package/dist/module-adoption-record.d.ts.map +1 -0
- package/dist/module-adoption-reference.d.ts +27 -0
- package/dist/module-adoption-reference.d.ts.map +1 -0
- package/dist/module-adoption.d.ts +26 -0
- package/dist/module-adoption.d.ts.map +1 -0
- package/dist/module-bundle.d.ts.map +1 -1
- package/dist/module-export.d.ts +6 -0
- package/dist/module-export.d.ts.map +1 -1
- package/dist/module-history.d.ts.map +1 -1
- package/dist/module-origin.d.ts +1 -1
- package/dist/module-origin.d.ts.map +1 -1
- package/dist/module-ownership.d.ts.map +1 -1
- package/dist/module-pending.d.ts +6 -0
- package/dist/module-pending.d.ts.map +1 -1
- package/dist/module-readiness.d.ts +5 -0
- package/dist/module-readiness.d.ts.map +1 -1
- package/dist/normalize.d.ts +34 -3
- package/dist/normalize.d.ts.map +1 -1
- package/dist/object-ownership.d.ts.map +1 -1
- package/dist/operator-classes.d.ts +9 -0
- package/dist/operator-classes.d.ts.map +1 -0
- package/dist/planner.d.ts +5 -0
- package/dist/planner.d.ts.map +1 -1
- package/dist/postgres-types.d.ts +24 -0
- package/dist/postgres-types.d.ts.map +1 -1
- package/dist/push.d.ts +3 -0
- package/dist/push.d.ts.map +1 -1
- package/dist/reference-state.d.ts.map +1 -1
- package/dist/relations.d.ts +1 -0
- package/dist/relations.d.ts.map +1 -1
- package/dist/renames.d.ts.map +1 -1
- package/dist/replay-plan.d.ts.map +1 -1
- package/dist/replay-reference.d.ts +10 -0
- package/dist/replay-reference.d.ts.map +1 -1
- package/dist/runner.d.ts +3 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/shadow-dedicated.d.ts.map +1 -1
- package/dist/shadow-guard.d.ts +4 -0
- package/dist/shadow-guard.d.ts.map +1 -1
- package/dist/shadow-types.d.ts +10 -0
- package/dist/shadow-types.d.ts.map +1 -1
- package/dist/shadow.d.ts.map +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/verify-history.d.ts +4 -0
- package/dist/verify-history.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/fs.js
CHANGED
|
@@ -15,8 +15,8 @@ function hasMysqlGlobalCatalogVisibility(params) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// packages/migrate/src/module-ownership.ts
|
|
18
|
-
import { VibeError as
|
|
19
|
-
import { getSqlDialect as
|
|
18
|
+
import { VibeError as VibeError18, sqlObjectKey as sqlObjectKey7, sqlObjectNamespace as sqlObjectNamespace3 } from "@vibeorm/schema";
|
|
19
|
+
import { getSqlDialect as getSqlDialect4, renderModuleDependencyQuery } from "@vibeorm/sql";
|
|
20
20
|
|
|
21
21
|
// packages/migrate/src/desired-state.ts
|
|
22
22
|
import { VibeError, assertValidSchemaIR, isSqlObjectDefinition, isSqlObjectIdentity, providerToDialect, sqlObjectKey } from "@vibeorm/schema";
|
|
@@ -115,14 +115,11 @@ function parseMigrationMetadata(params) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
// packages/migrate/src/object-ownership.ts
|
|
119
|
-
import { VibeError as VibeError6, defaultPrimaryKeyName, enumStorageOf as enumStorageOf2, implicitJoinTableObjectNames, postgresGeneratedObjectName, sqlObjectKey as sqlObjectKey2 } from "@vibeorm/schema";
|
|
120
|
-
|
|
121
118
|
// packages/migrate/src/normalize.ts
|
|
122
|
-
import { VibeError as VibeError5, defaultForeignKeyName, defaultIndexName, enumStorageOf, isEnumType as isEnumType5, unsupportedCapability as
|
|
119
|
+
import { assertIndexOperatorClasses, VibeError as VibeError5, defaultForeignKeyName, defaultIndexName, enumStorageOf, isEnumType as isEnumType5, qualifiedSqlName as qualifiedSqlName3, assertSqlNamespace, unsupportedCapability as unsupportedCapability5 } from "@vibeorm/schema";
|
|
123
120
|
|
|
124
121
|
// packages/migrate/src/relations.ts
|
|
125
|
-
import { implicitJoinTable, isImplicitManyToManyRelation } from "@vibeorm/schema";
|
|
122
|
+
import { implicitJoinTable, isImplicitManyToManyRelation, qualifiedSqlName, unsupportedCapability, providerToDialect as providerToDialect2 } from "@vibeorm/schema";
|
|
126
123
|
function detectImplicitM2MPairs(params) {
|
|
127
124
|
const { schema } = params;
|
|
128
125
|
const pairs = [];
|
|
@@ -138,14 +135,20 @@ function detectImplicitM2MPairs(params) {
|
|
|
138
135
|
modelB: relation.targetModel,
|
|
139
136
|
...relation.relationName === undefined ? {} : { relationName: relation.relationName }
|
|
140
137
|
});
|
|
141
|
-
|
|
138
|
+
const target = schema.models.find((entry) => entry.name === relation.targetModel);
|
|
139
|
+
const namespace = qualifiedSqlName({ name: model.name, namespace: model.namespace }).namespace;
|
|
140
|
+
if (namespace !== qualifiedSqlName({ name: target.name, namespace: target.namespace }).namespace)
|
|
141
|
+
throw unsupportedCapability({ dialect: providerToDialect2({ provider: schema.datasource.provider }), feature: "implicitJoinNamespaces", hint: "Implicit join parents must share one physical namespace" });
|
|
142
|
+
const key = JSON.stringify([namespace, joinTable.tableName]);
|
|
143
|
+
if (seen.has(key))
|
|
142
144
|
continue;
|
|
143
|
-
seen.add(
|
|
145
|
+
seen.add(key);
|
|
144
146
|
pairs.push({
|
|
145
147
|
modelA: joinTable.modelA,
|
|
146
148
|
modelB: joinTable.modelB,
|
|
147
149
|
...relation.relationName === undefined ? {} : { relationName: relation.relationName },
|
|
148
|
-
tableName: joinTable.tableName
|
|
150
|
+
tableName: joinTable.tableName,
|
|
151
|
+
...model.namespace === undefined && target.namespace === undefined ? {} : { namespace }
|
|
149
152
|
});
|
|
150
153
|
}
|
|
151
154
|
}
|
|
@@ -153,7 +156,7 @@ function detectImplicitM2MPairs(params) {
|
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
// packages/migrate/src/postgres-types.ts
|
|
156
|
-
import { VibeError as VibeError2, isEnumType, scalarListDefault, serializeJsonDefault } from "@vibeorm/schema";
|
|
159
|
+
import { VibeError as VibeError2, isEnumType, scalarListDefault, serializeJsonDefault, qualifiedSqlName as qualifiedSqlName2 } from "@vibeorm/schema";
|
|
157
160
|
import { POSTGRES_CODECS, postgresDialect, toPgArrayLiteral, parsePostgresTypeName, renderPostgresTypeName } from "@vibeorm/sql";
|
|
158
161
|
var SCALAR_TO_PG = {
|
|
159
162
|
String: "text",
|
|
@@ -169,6 +172,9 @@ var SCALAR_TO_PG = {
|
|
|
169
172
|
var CANONICAL_EQUIVALENTS = {
|
|
170
173
|
DateTime: ["timestamptz(3)", "timestamptz(6)"]
|
|
171
174
|
};
|
|
175
|
+
var EXACT_CANONICAL_EQUIVALENTS = {
|
|
176
|
+
DateTime: ["timestamptz(6)"]
|
|
177
|
+
};
|
|
172
178
|
var CANONICAL_UDT = {
|
|
173
179
|
text: "String",
|
|
174
180
|
bool: "Boolean",
|
|
@@ -237,13 +243,15 @@ var NATIVE_ALIASES = {
|
|
|
237
243
|
var IMPLICIT_MODIFIERS = {
|
|
238
244
|
bit: "(1)"
|
|
239
245
|
};
|
|
246
|
+
var DEFAULT_PRECISION_SIX = new Set(["timestamp", "time", "timetz"]);
|
|
240
247
|
function normalizeNativeType(params) {
|
|
241
248
|
const compact = params.raw.toLowerCase().replace(/\s+/g, "");
|
|
242
249
|
const match = compact.match(/^([a-z_0-9]+)(\(.*\))?(\[\])?$/);
|
|
243
250
|
if (match === null)
|
|
244
251
|
return compact;
|
|
245
252
|
const base = NATIVE_ALIASES[match[1]] ?? match[1];
|
|
246
|
-
|
|
253
|
+
const modifier = DEFAULT_PRECISION_SIX.has(base) && match[2] === "(6)" ? "" : match[2] ?? IMPLICIT_MODIFIERS[base] ?? "";
|
|
254
|
+
return `${base}${modifier}${match[3] ?? ""}`;
|
|
247
255
|
}
|
|
248
256
|
function parseUnsupportedPostgresType(params) {
|
|
249
257
|
const temporal = /^\s*(time(?:stamp)?)\s*(\(\s*\d+\s*\))?\s+(with|without)\s+time\s+zone\s*((?:\[\s*\]\s*)*)$/i.exec(params.raw);
|
|
@@ -256,7 +264,7 @@ function parseUnsupportedPostgresType(params) {
|
|
|
256
264
|
return NATIVE_ALIASES[base] === undefined ? undefined : parsePostgresTypeName({ raw: normalizeNativeType(params) });
|
|
257
265
|
}
|
|
258
266
|
function columnTypeKey(params) {
|
|
259
|
-
const { field } = params;
|
|
267
|
+
const { field, exact } = params;
|
|
260
268
|
const listSuffix = field.isList ? "[]" : "";
|
|
261
269
|
if (field.isUnsupported === true) {
|
|
262
270
|
const parsed = parseUnsupportedPostgresType({ raw: field.nativeType ?? "" });
|
|
@@ -267,22 +275,48 @@ function columnTypeKey(params) {
|
|
|
267
275
|
const scalar = namespace === undefined || namespace === "pg_catalog" ? scalarForBuiltinType({ name: parsed.name }) : undefined;
|
|
268
276
|
if (scalar !== undefined) {
|
|
269
277
|
const { isUnsupported: _unsupported, nativeTypeIdentity: _identity, ...rest } = field;
|
|
270
|
-
return columnTypeKey({ field: { ...rest, type: scalar, nativeType: `${parsed.name}${parsed.modifier ?? ""}`, isList: parsed.array || field.isList } });
|
|
278
|
+
return columnTypeKey({ field: { ...rest, type: scalar, nativeType: `${parsed.name}${parsed.modifier ?? ""}`, isList: parsed.array || field.isList }, ...exact === undefined ? {} : { exact } });
|
|
271
279
|
}
|
|
272
280
|
return `u:${JSON.stringify([namespace ?? null, parsed.name, parsed.modifier, parsed.array || field.isList])}`;
|
|
273
281
|
}
|
|
274
282
|
if (isEnumType(field.type)) {
|
|
275
|
-
|
|
283
|
+
const identity = qualifiedSqlName2({ name: field.type.enum, namespace: field.type.namespace });
|
|
284
|
+
return `e:${JSON.stringify([identity.namespace, identity.name])}${listSuffix}`;
|
|
276
285
|
}
|
|
277
286
|
if (field.nativeType !== undefined) {
|
|
278
287
|
const normalized = normalizeNativeType({ raw: field.nativeType });
|
|
279
288
|
const canonical = normalizeNativeType({ raw: SCALAR_TO_PG[field.type] });
|
|
280
|
-
const
|
|
289
|
+
const equivalents = exact === true ? EXACT_CANONICAL_EQUIVALENTS : CANONICAL_EQUIVALENTS;
|
|
290
|
+
const isCanonical = normalized === canonical || (equivalents[field.type]?.includes(normalized) ?? false);
|
|
281
291
|
if (!isCanonical)
|
|
282
292
|
return `n:${normalized}${listSuffix}`;
|
|
283
293
|
}
|
|
284
294
|
return `s:${field.type}${listSuffix}`;
|
|
285
295
|
}
|
|
296
|
+
function declaresExactColumnType(params) {
|
|
297
|
+
const { field } = params;
|
|
298
|
+
if (field.isUnsupported === true || field.nativeType === undefined || isEnumType(field.type))
|
|
299
|
+
return false;
|
|
300
|
+
return CANONICAL_EQUIVALENTS[field.type]?.includes(normalizeNativeType({ raw: field.nativeType })) ?? false;
|
|
301
|
+
}
|
|
302
|
+
function timestamptzPrecision(params) {
|
|
303
|
+
const { field } = params;
|
|
304
|
+
if (field.type !== "DateTime" || field.isUnsupported === true)
|
|
305
|
+
return;
|
|
306
|
+
const normalized = field.nativeType === undefined ? "timestamptz" : normalizeNativeType({ raw: field.nativeType });
|
|
307
|
+
if (normalized === "timestamptz")
|
|
308
|
+
return 6;
|
|
309
|
+
const match = /^timestamptz\((\d)\)$/.exec(normalized);
|
|
310
|
+
return match === null ? undefined : Number(match[1]);
|
|
311
|
+
}
|
|
312
|
+
function isTimestamptzPrecisionWidening(params) {
|
|
313
|
+
const { from, to } = params;
|
|
314
|
+
if (from.isList !== to.isList)
|
|
315
|
+
return false;
|
|
316
|
+
const before = timestamptzPrecision({ field: from });
|
|
317
|
+
const after = timestamptzPrecision({ field: to });
|
|
318
|
+
return before !== undefined && after !== undefined && after >= before;
|
|
319
|
+
}
|
|
286
320
|
function isScalarToArrayChange(params) {
|
|
287
321
|
const { from, to } = params;
|
|
288
322
|
if (from.isList || !to.isList)
|
|
@@ -309,7 +343,7 @@ function columnTypeSql(params) {
|
|
|
309
343
|
}
|
|
310
344
|
const listSuffix = field.isList ? "[]" : "";
|
|
311
345
|
if (isEnumType(field.type)) {
|
|
312
|
-
return `${postgresDialect.quoteIdent({ name: "public" })}.${postgresDialect.quoteIdent({ name: field.type.enum })}${listSuffix}`;
|
|
346
|
+
return `${postgresDialect.quoteIdent({ name: field.type.namespace ?? "public" })}.${postgresDialect.quoteIdent({ name: field.type.enum })}${listSuffix}`;
|
|
313
347
|
}
|
|
314
348
|
if (field.nativeType !== undefined) {
|
|
315
349
|
const normalized = normalizeNativeType({ raw: field.nativeType });
|
|
@@ -426,18 +460,18 @@ function quotePgString(params) {
|
|
|
426
460
|
}
|
|
427
461
|
|
|
428
462
|
// packages/migrate/src/sqlite-types.ts
|
|
429
|
-
import { VibeError as VibeError3, isEnumType as isEnumType3, unsupportedCapability as
|
|
463
|
+
import { VibeError as VibeError3, isEnumType as isEnumType3, unsupportedCapability as unsupportedCapability3 } from "@vibeorm/schema";
|
|
430
464
|
import { ParamCollector, sqliteDialect } from "@vibeorm/sql";
|
|
431
465
|
|
|
432
466
|
// packages/migrate/src/enum-values.ts
|
|
433
|
-
import { isEnumType as isEnumType2, unsupportedCapability } from "@vibeorm/schema";
|
|
467
|
+
import { isEnumType as isEnumType2, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
|
|
434
468
|
function resolveEnumValues(params) {
|
|
435
469
|
const { name, enums } = params;
|
|
436
470
|
const direct = enums.get(name);
|
|
437
471
|
if (direct !== undefined)
|
|
438
472
|
return direct.values;
|
|
439
473
|
for (const enumDef of enums.values()) {
|
|
440
|
-
if (enumDef.name === name)
|
|
474
|
+
if ((enumDef.name === name || enumDef.dbName === name) && (params.namespace === undefined || (enumDef.namespace ?? "public") === params.namespace))
|
|
441
475
|
return enumDef.values;
|
|
442
476
|
}
|
|
443
477
|
return;
|
|
@@ -452,7 +486,7 @@ function assertEnumStringTransition(params) {
|
|
|
452
486
|
return;
|
|
453
487
|
const reason = from.isList !== to.isList ? "change scalar/list shape separately from enum storage" : from.default?.kind === "dbgenerated" || to.default?.kind === "dbgenerated" ? "database-expression defaults may depend on the native enum type \u2014 migrate the default explicitly first" : dialect === "mysql" && isEnumType2(to.type) ? "non-strict mysql can silently replace out-of-list strings with '' \u2014 validate and migrate this column manually in strict mode" : undefined;
|
|
454
488
|
if (reason !== undefined) {
|
|
455
|
-
throw
|
|
489
|
+
throw unsupportedCapability2({ dialect, feature: `enum storage conversion on "${table}"."${column}"`, hint: reason });
|
|
456
490
|
}
|
|
457
491
|
}
|
|
458
492
|
|
|
@@ -469,7 +503,7 @@ var SCALAR_TO_SQLITE = {
|
|
|
469
503
|
Bytes: "BLOB"
|
|
470
504
|
};
|
|
471
505
|
function refuseSqliteScalarList(params) {
|
|
472
|
-
throw
|
|
506
|
+
throw unsupportedCapability3({
|
|
473
507
|
dialect: "sqlite",
|
|
474
508
|
feature: `scalar list column "${params.field.name}"`,
|
|
475
509
|
hint: "sqlite has no array column type \u2014 model the list as a separate table or a Json column"
|
|
@@ -605,7 +639,7 @@ function parseSqliteEnumChecks(params) {
|
|
|
605
639
|
}
|
|
606
640
|
|
|
607
641
|
// packages/migrate/src/mysql-types.ts
|
|
608
|
-
import { VibeError as VibeError4, isEnumType as isEnumType4, unsupportedCapability as
|
|
642
|
+
import { VibeError as VibeError4, isEnumType as isEnumType4, unsupportedCapability as unsupportedCapability4 } from "@vibeorm/schema";
|
|
609
643
|
import { mysqlDialect } from "@vibeorm/sql";
|
|
610
644
|
var SCALAR_TO_MYSQL = {
|
|
611
645
|
String: `varchar(${mysqlDialect.conflictKeyEquality.defaultStringMaxLength})`,
|
|
@@ -619,7 +653,7 @@ var SCALAR_TO_MYSQL = {
|
|
|
619
653
|
Bytes: "longblob"
|
|
620
654
|
};
|
|
621
655
|
function refuseMysqlScalarList(params) {
|
|
622
|
-
throw
|
|
656
|
+
throw unsupportedCapability4({
|
|
623
657
|
dialect: "mysql",
|
|
624
658
|
feature: `scalar list column "${params.field.name}"`,
|
|
625
659
|
hint: "mysql has no array column type \u2014 model the list as a separate table or a JSON column"
|
|
@@ -691,7 +725,7 @@ function mysqlColumnTypeSql(params) {
|
|
|
691
725
|
});
|
|
692
726
|
}
|
|
693
727
|
if (values.some((value) => /[\\\x00-\x1f\x7f]/.test(value)))
|
|
694
|
-
throw
|
|
728
|
+
throw unsupportedCapability4({ dialect: "mysql", feature: "mysqlEnumLiteralEscaping", hint: "ENUM labels containing backslashes or control characters require a reviewed SQL-mode-specific migration" });
|
|
695
729
|
return `ENUM(${values.map((value) => quotePgString({ value })).join(", ")})`;
|
|
696
730
|
}
|
|
697
731
|
if (field.nativeType !== undefined) {
|
|
@@ -750,25 +784,31 @@ function parseMysqlEnumColumnType(params) {
|
|
|
750
784
|
function normalizeSchema(params) {
|
|
751
785
|
const { schema } = params;
|
|
752
786
|
const dialect = params.dialect ?? "postgres";
|
|
787
|
+
assertIndexOperatorClasses({ schema, dialect, physical: true });
|
|
753
788
|
const enums = new Map;
|
|
754
789
|
for (const enumDef of schema.enums) {
|
|
790
|
+
if (enumDef.namespace !== undefined)
|
|
791
|
+
assertSqlNamespace({ namespace: enumDef.namespace, dialect });
|
|
755
792
|
if (enumStorageOf({ enumDef }) === "string")
|
|
756
793
|
continue;
|
|
757
|
-
enums.set(enumDef.dbName ?? enumDef.name, enumDef);
|
|
794
|
+
enums.set(physicalNameKey({ name: enumDef.dbName ?? enumDef.name, namespace: enumDef.namespace, dialect }), enumDef);
|
|
758
795
|
}
|
|
759
796
|
const tables = new Map;
|
|
760
797
|
const views = new Map;
|
|
761
798
|
for (const model of schema.models) {
|
|
799
|
+
if (model.namespace !== undefined)
|
|
800
|
+
assertSqlNamespace({ namespace: model.namespace, dialect });
|
|
762
801
|
if (model.isView) {
|
|
763
|
-
views.set(model.dbName ?? model.name, model);
|
|
802
|
+
views.set(physicalNameKey({ name: model.dbName ?? model.name, namespace: model.namespace, dialect }), model);
|
|
764
803
|
continue;
|
|
765
804
|
}
|
|
766
805
|
const table = normalizeModel({ model, schema, dialect, enums });
|
|
767
|
-
tables.set(table.table, table);
|
|
806
|
+
tables.set(physicalNameKey({ name: table.table, namespace: table.namespace, dialect }), table);
|
|
768
807
|
}
|
|
769
808
|
const joinTables = new Map;
|
|
770
809
|
for (const pair of detectImplicitM2MPairs({ schema })) {
|
|
771
|
-
|
|
810
|
+
const join = normalizeJoinTable({ pair, schema, dialect });
|
|
811
|
+
joinTables.set(physicalNameKey({ name: join.table, namespace: join.namespace, dialect }), join);
|
|
772
812
|
}
|
|
773
813
|
return { tables, joinTables, enums, views };
|
|
774
814
|
}
|
|
@@ -776,12 +816,15 @@ function physicalEnumField(params) {
|
|
|
776
816
|
const { field, schema, dialect, table } = params;
|
|
777
817
|
if (!isEnumType5(field.type))
|
|
778
818
|
return field;
|
|
779
|
-
const
|
|
780
|
-
|
|
819
|
+
const type = field.type;
|
|
820
|
+
if (type.namespace !== undefined)
|
|
821
|
+
assertSqlNamespace({ namespace: type.namespace, dialect });
|
|
822
|
+
const name = type.enum;
|
|
823
|
+
const enumDef = schema.enums.find((candidate) => (candidate.name === name || candidate.dbName === name) && (type.namespace === undefined || qualifiedSqlName3({ name, namespace: candidate.namespace }).namespace === type.namespace));
|
|
781
824
|
if (enumDef === undefined)
|
|
782
825
|
return field;
|
|
783
826
|
if (enumStorageOf({ enumDef }) !== "string") {
|
|
784
|
-
return dialect === "postgres" ? { ...field, type: { enum: enumDef.dbName ?? enumDef.name } } : field;
|
|
827
|
+
return dialect === "postgres" ? { ...field, type: { enum: enumDef.dbName ?? enumDef.name, ...enumDef.namespace === undefined ? {} : { namespace: enumDef.namespace } } } : field;
|
|
785
828
|
}
|
|
786
829
|
const physical = {
|
|
787
830
|
...field,
|
|
@@ -808,7 +851,7 @@ function assertStringEnumLabelsFit(params) {
|
|
|
808
851
|
const charLimit = varchar?.[1] === undefined ? undefined : Number(varchar[1]);
|
|
809
852
|
const tooLong = values.some((value) => charLimit !== undefined && [...value].length > charLimit || textBytes !== undefined && new TextEncoder().encode(value).length > textBytes);
|
|
810
853
|
if (!supported || tooLong) {
|
|
811
|
-
throw
|
|
854
|
+
throw unsupportedCapability5({
|
|
812
855
|
dialect,
|
|
813
856
|
feature: `string enum column "${table}"."${field.dbName ?? field.name}" stored as ${type}`,
|
|
814
857
|
hint: supported ? "an enum label exceeds the physical column limit \u2014 choose a wider varchar or text type" : "use text or varchar storage; other types cannot preserve arbitrary enum labels verbatim"
|
|
@@ -816,16 +859,20 @@ function assertStringEnumLabelsFit(params) {
|
|
|
816
859
|
}
|
|
817
860
|
}
|
|
818
861
|
function columnTypeKeyFor(params) {
|
|
819
|
-
const { field, dialect, enums } = params;
|
|
862
|
+
const { field, dialect, enums, exact } = params;
|
|
820
863
|
switch (dialect) {
|
|
821
864
|
case "postgres":
|
|
822
|
-
return columnTypeKey({ field });
|
|
865
|
+
return columnTypeKey({ field, ...exact === undefined ? {} : { exact } });
|
|
823
866
|
case "sqlite":
|
|
824
867
|
return sqliteColumnTypeKey({ field, enums });
|
|
825
868
|
case "mysql":
|
|
826
869
|
return mysqlColumnTypeKey({ field, enums });
|
|
827
870
|
}
|
|
828
871
|
}
|
|
872
|
+
function columnTypeChanged(params) {
|
|
873
|
+
const { declared, actual } = params;
|
|
874
|
+
return declared.pinsExactType ? declared.exactTypeKey !== actual.exactTypeKey : declared.typeKey !== actual.typeKey;
|
|
875
|
+
}
|
|
829
876
|
function defaultFingerprintFor(params) {
|
|
830
877
|
const { field, dialect } = params;
|
|
831
878
|
switch (dialect) {
|
|
@@ -848,6 +895,8 @@ function normalizeModel(params) {
|
|
|
848
895
|
name,
|
|
849
896
|
field,
|
|
850
897
|
typeKey: columnTypeKeyFor({ field, dialect, enums }),
|
|
898
|
+
exactTypeKey: columnTypeKeyFor({ field, dialect, enums, exact: true }),
|
|
899
|
+
pinsExactType: dialect === "postgres" && declaresExactColumnType({ field }),
|
|
851
900
|
nullable: field.isOptional,
|
|
852
901
|
defaultKey: defaultFingerprintFor({ field, dialect })
|
|
853
902
|
});
|
|
@@ -858,7 +907,7 @@ function normalizeModel(params) {
|
|
|
858
907
|
const checks = new Map;
|
|
859
908
|
for (const check of model.checks ?? [])
|
|
860
909
|
checks.set(check.name, check.expression);
|
|
861
|
-
return { model, table, columns, pk, indexes, fks, checks };
|
|
910
|
+
return { model, table, ...model.namespace === undefined ? {} : { namespace: model.namespace }, columns, pk, indexes, fks, checks };
|
|
862
911
|
}
|
|
863
912
|
function normalizePk(params) {
|
|
864
913
|
const { model } = params;
|
|
@@ -879,7 +928,8 @@ function normalizeIndexes(params) {
|
|
|
879
928
|
return {
|
|
880
929
|
name: fieldDbName({ model, name: field.name }),
|
|
881
930
|
desc: field.sort === "desc",
|
|
882
|
-
...decimal ? { decimal: true } : {}
|
|
931
|
+
...decimal ? { decimal: true } : {},
|
|
932
|
+
...field.operatorClass === undefined ? {} : { operatorClass: field.operatorClass }
|
|
883
933
|
};
|
|
884
934
|
});
|
|
885
935
|
const where = index.where;
|
|
@@ -910,7 +960,7 @@ function normalizeIndexes(params) {
|
|
|
910
960
|
}
|
|
911
961
|
function indexComparisonKey(params) {
|
|
912
962
|
const { unique, kind, columns, where, nullsNotDistinct } = params;
|
|
913
|
-
const cols = columns.map((column) => `${column.name}${column.desc ? " desc" : ""}`).join(",");
|
|
963
|
+
const cols = columns.map((column) => `${column.name}${column.desc ? " desc" : ""}${column.operatorClass === undefined ? "" : ` ops:${JSON.stringify([column.operatorClass.namespace, column.operatorClass.name])}`}`).join(",");
|
|
914
964
|
return `${unique ? "u" : "i"}${nullsNotDistinct === true ? "n" : ""}:${kind}:${cols}:${normalizeWhere({ where })}`;
|
|
915
965
|
}
|
|
916
966
|
function normalizeWhere(params) {
|
|
@@ -1088,7 +1138,7 @@ function normalizeFks(params) {
|
|
|
1088
1138
|
const field = model.fields.find((candidate) => candidate.name === name);
|
|
1089
1139
|
return field === undefined ? true : !field.isOptional;
|
|
1090
1140
|
});
|
|
1091
|
-
const key = `${columns.join(",")}\u2192${refTable}.${refColumns.join(",")}`;
|
|
1141
|
+
const key = `${columns.join(",")}\u2192${physicalNameKey({ name: refTable, namespace: target?.namespace, dialect: params.dialect })}.${refColumns.join(",")}`;
|
|
1092
1142
|
if (result.has(key))
|
|
1093
1143
|
continue;
|
|
1094
1144
|
result.set(key, {
|
|
@@ -1096,6 +1146,7 @@ function normalizeFks(params) {
|
|
|
1096
1146
|
constraintName: relation.constraintName ?? defaultForeignKeyName({ table, columns, dialect: params.dialect }),
|
|
1097
1147
|
columns,
|
|
1098
1148
|
refTable,
|
|
1149
|
+
...target?.namespace === undefined ? {} : { refNamespace: target.namespace },
|
|
1099
1150
|
refColumns,
|
|
1100
1151
|
onDelete: resolveOnDelete({ explicit: relation.onDelete, required }),
|
|
1101
1152
|
onUpdate: resolveOnUpdate({ explicit: relation.onUpdate })
|
|
@@ -1107,6 +1158,7 @@ function normalizeJoinTable(params) {
|
|
|
1107
1158
|
const { pair, schema, dialect } = params;
|
|
1108
1159
|
return {
|
|
1109
1160
|
table: pair.tableName,
|
|
1161
|
+
...pair.namespace === undefined ? {} : { namespace: pair.namespace },
|
|
1110
1162
|
a: joinTableEnd({ modelName: pair.modelA, schema, dialect, joinTable: pair.tableName }),
|
|
1111
1163
|
b: joinTableEnd({ modelName: pair.modelB, schema, dialect, joinTable: pair.tableName })
|
|
1112
1164
|
};
|
|
@@ -1132,10 +1184,24 @@ function joinTableEnd(params) {
|
|
|
1132
1184
|
}
|
|
1133
1185
|
return {
|
|
1134
1186
|
table: model.dbName ?? model.name,
|
|
1187
|
+
...model.namespace === undefined ? {} : { namespace: model.namespace },
|
|
1135
1188
|
column: pkField.dbName ?? pkField.name,
|
|
1136
1189
|
field: physicalEnumField({ field: pkField, schema, dialect: params.dialect, table: model.dbName ?? model.name })
|
|
1137
1190
|
};
|
|
1138
1191
|
}
|
|
1192
|
+
function physicalNameKey(params) {
|
|
1193
|
+
if (params.dialect !== undefined && params.dialect !== "postgres")
|
|
1194
|
+
return params.name;
|
|
1195
|
+
const identity = qualifiedSqlName3(params);
|
|
1196
|
+
return JSON.stringify([identity.namespace, identity.name]);
|
|
1197
|
+
}
|
|
1198
|
+
function schemaNamespaces(params) {
|
|
1199
|
+
return [...new Set([
|
|
1200
|
+
...params.schema.models.flatMap((model) => model.namespace === undefined || model.namespace === "public" ? [] : [model.namespace]),
|
|
1201
|
+
...params.schema.enums.flatMap((entry) => enumStorageOf({ enumDef: entry }) === "string" || entry.namespace === undefined || entry.namespace === "public" ? [] : [entry.namespace]),
|
|
1202
|
+
...(params.schema.migrationObjects ?? []).flatMap((object) => object.identity.kind === "namespace" ? [object.identity.name] : [])
|
|
1203
|
+
])];
|
|
1204
|
+
}
|
|
1139
1205
|
function fieldDbName(params) {
|
|
1140
1206
|
const { model, name } = params;
|
|
1141
1207
|
const field = model.fields.find((candidate) => candidate.name === name);
|
|
@@ -1143,6 +1209,7 @@ function fieldDbName(params) {
|
|
|
1143
1209
|
}
|
|
1144
1210
|
|
|
1145
1211
|
// packages/migrate/src/object-ownership.ts
|
|
1212
|
+
import { VibeError as VibeError6, defaultPrimaryKeyName, enumStorageOf as enumStorageOf2, implicitJoinTableObjectNames, postgresGeneratedObjectName, qualifiedSqlName as qualifiedSqlName4, sqlObjectKey as sqlObjectKey2 } from "@vibeorm/schema";
|
|
1146
1213
|
function legacyMigrationObjects(params) {
|
|
1147
1214
|
return params.artifacts.map((artifact) => {
|
|
1148
1215
|
const owner = artifact.owns;
|
|
@@ -1166,13 +1233,14 @@ function legacyMigrationObjects(params) {
|
|
|
1166
1233
|
}
|
|
1167
1234
|
function preserveMigrationObjects(params) {
|
|
1168
1235
|
const namespace = params.namespace ?? (params.to.datasource.provider === "sqlite" ? "main" : "public");
|
|
1169
|
-
const objects = params.objects
|
|
1236
|
+
const objects = params.objects;
|
|
1170
1237
|
const models = params.from.models.flatMap((model) => {
|
|
1171
1238
|
const table = model.dbName ?? model.name;
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1239
|
+
const placement = qualifiedSqlName4({ name: table, namespace: model.namespace, defaultNamespace: namespace });
|
|
1240
|
+
const target = params.to.models.find((entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect: params.to.datasource.provider === "sqlite" ? "sqlite" : params.to.datasource.provider === "mysql" ? "mysql" : "postgres" }) === physicalNameKey({ ...placement, dialect: params.to.datasource.provider === "sqlite" ? "sqlite" : params.to.datasource.provider === "mysql" ? "mysql" : "postgres" }));
|
|
1241
|
+
if (objects.some((object) => (object.identity.kind === "table" || object.identity.kind === "view") && object.identity.name.name === table && object.identity.name.namespace === placement.namespace))
|
|
1174
1242
|
return [];
|
|
1175
|
-
const owners = objects.map((object) => object.identity).filter((identity) => ("table" in identity) && identity.table.name === table);
|
|
1243
|
+
const owners = objects.map((object) => object.identity).filter((identity) => ("table" in identity) && identity.table.name === table && identity.table.namespace === placement.namespace);
|
|
1176
1244
|
return [{
|
|
1177
1245
|
...model,
|
|
1178
1246
|
fields: model.fields.flatMap((field) => {
|
|
@@ -1194,7 +1262,7 @@ function preserveMigrationObjects(params) {
|
|
|
1194
1262
|
relations: model.relations.filter((relation) => !owners.some((owner) => owner.kind === "constraint" && owner.constraintType === "foreignKey" && owner.name === relation.constraintName))
|
|
1195
1263
|
}];
|
|
1196
1264
|
});
|
|
1197
|
-
const enums = params.from.enums.filter((enumDef) => enumStorageOf2({ enumDef }) === "string" || !objects.some((object) => object.identity.kind === "enum" && object.identity.name.name === (enumDef.dbName ?? enumDef.name)));
|
|
1265
|
+
const enums = params.from.enums.filter((enumDef) => enumStorageOf2({ enumDef }) === "string" || !objects.some((object) => object.identity.kind === "enum" && object.identity.name.name === (enumDef.dbName ?? enumDef.name) && object.identity.name.namespace === (enumDef.namespace ?? namespace)));
|
|
1198
1266
|
return { ...params.from, models, enums };
|
|
1199
1267
|
}
|
|
1200
1268
|
function sqlitePreservedSchema(params) {
|
|
@@ -1213,7 +1281,8 @@ function sqlitePreservedSchema(params) {
|
|
|
1213
1281
|
}) };
|
|
1214
1282
|
}
|
|
1215
1283
|
function removalIdentity(params) {
|
|
1216
|
-
const { step
|
|
1284
|
+
const { step } = params;
|
|
1285
|
+
const namespace = step.namespace ?? params.namespace;
|
|
1217
1286
|
const table = "table" in step && step.table !== undefined ? { namespace, name: step.table } : undefined;
|
|
1218
1287
|
switch (step.kind) {
|
|
1219
1288
|
case "dropEnum":
|
|
@@ -1244,18 +1313,22 @@ function removalIdentity(params) {
|
|
|
1244
1313
|
function schemaObjectIdentities(params) {
|
|
1245
1314
|
const normalized = normalizeSchema({ schema: params.schema, dialect: params.dialect });
|
|
1246
1315
|
const result = [];
|
|
1247
|
-
if (params.dialect === "postgres")
|
|
1248
|
-
for (const
|
|
1249
|
-
result.push({ kind: "
|
|
1250
|
-
|
|
1251
|
-
|
|
1316
|
+
if (params.dialect === "postgres") {
|
|
1317
|
+
for (const namespace of schemaNamespaces({ schema: params.schema }))
|
|
1318
|
+
result.push({ kind: "namespace", name: namespace });
|
|
1319
|
+
for (const enumDef of normalized.enums.values())
|
|
1320
|
+
result.push({ kind: "enum", name: qualifiedSqlName4({ name: enumDef.dbName ?? enumDef.name, namespace: enumDef.namespace, defaultNamespace: params.namespace }) });
|
|
1321
|
+
}
|
|
1322
|
+
for (const model of normalized.tables.values()) {
|
|
1323
|
+
const name = model.table;
|
|
1324
|
+
const table = qualifiedSqlName4({ name, namespace: model.namespace, defaultNamespace: params.namespace });
|
|
1252
1325
|
result.push({ kind: "table", name: table });
|
|
1253
1326
|
for (const [column, definition] of model.columns) {
|
|
1254
1327
|
result.push({ kind: "column", table, column });
|
|
1255
1328
|
if (definition.defaultKey !== null)
|
|
1256
1329
|
result.push({ kind: "columnDefault", table, column });
|
|
1257
1330
|
if (params.dialect === "postgres" && definition.field.default?.kind === "autoincrement")
|
|
1258
|
-
result.push({ kind: "sequence", name: { namespace:
|
|
1331
|
+
result.push({ kind: "sequence", name: { namespace: table.namespace, name: postgresGeneratedObjectName({ name1: name, name2: column, label: "seq" }) } });
|
|
1259
1332
|
}
|
|
1260
1333
|
for (const exclusion of model.model.exclusions ?? [])
|
|
1261
1334
|
result.push({ kind: "constraint", constraintType: "exclusion", table, name: exclusion.name });
|
|
@@ -1271,16 +1344,17 @@ function schemaObjectIdentities(params) {
|
|
|
1271
1344
|
if (model.pk.length > 0)
|
|
1272
1345
|
result.push({ kind: "constraint", constraintType: "primaryKey", table, name: model.model.primaryKeyConstraintName ?? (params.dialect === "mysql" ? "PRIMARY" : defaultPrimaryKeyName({ table: name, dialect: params.dialect })) });
|
|
1273
1346
|
}
|
|
1274
|
-
for (const
|
|
1275
|
-
const
|
|
1347
|
+
for (const join of normalized.joinTables.values()) {
|
|
1348
|
+
const name = join.table;
|
|
1349
|
+
const table = qualifiedSqlName4({ name, namespace: join.namespace, defaultNamespace: params.namespace });
|
|
1276
1350
|
result.push({ kind: "table", name: table });
|
|
1277
1351
|
const derived = implicitJoinTableObjectNames({ table: name });
|
|
1278
1352
|
result.push({ kind: "column", table, column: "A" }, { kind: "constraint", constraintType: "foreignKey", table, name: derived.foreignKeyA });
|
|
1279
1353
|
result.push({ kind: "column", table, column: "B" }, { kind: "constraint", constraintType: "foreignKey", table, name: derived.foreignKeyB });
|
|
1280
1354
|
result.push({ kind: "index", table, name: derived.uniqueIndex }, { kind: "index", table, name: derived.indexB });
|
|
1281
1355
|
}
|
|
1282
|
-
for (const
|
|
1283
|
-
result.push({ kind: "view", name: { namespace:
|
|
1356
|
+
for (const view of normalized.views.values())
|
|
1357
|
+
result.push({ kind: "view", name: qualifiedSqlName4({ name: view.dbName ?? view.name, namespace: view.namespace, defaultNamespace: params.namespace }) });
|
|
1284
1358
|
return result;
|
|
1285
1359
|
}
|
|
1286
1360
|
function assertRetainedMigrationObjects(params) {
|
|
@@ -1340,7 +1414,7 @@ function assertRemovalAuthorization(params) {
|
|
|
1340
1414
|
integrity.push(identity);
|
|
1341
1415
|
if (step.kind === "dropTable" || step.kind === "dropJoinTable") {
|
|
1342
1416
|
for (const child of [...declared, ...params.removeUnmanaged ?? []]) {
|
|
1343
|
-
if ((child.kind === "constraint" || child.kind === "index") && child.table.namespace === params.namespace && child.table.name === step.table)
|
|
1417
|
+
if ((child.kind === "constraint" || child.kind === "index") && child.table.namespace === (step.namespace ?? params.namespace) && child.table.name === step.table)
|
|
1344
1418
|
integrity.push(child);
|
|
1345
1419
|
}
|
|
1346
1420
|
}
|
|
@@ -1352,22 +1426,311 @@ function assertRemovalAuthorization(params) {
|
|
|
1352
1426
|
}
|
|
1353
1427
|
|
|
1354
1428
|
// packages/migrate/src/inventory.ts
|
|
1355
|
-
import { VibeError as
|
|
1356
|
-
import { getSqlDialect as
|
|
1429
|
+
import { VibeError as VibeError17, defaultForeignKeyName as defaultForeignKeyName2, defaultPrimaryKeyName as defaultPrimaryKeyName2, isSqlObjectIdentity as isSqlObjectIdentity2, sqlObjectKey as sqlObjectKey6 } from "@vibeorm/schema";
|
|
1430
|
+
import { getSqlDialect as getSqlDialect3, renderMigrationInventory, renderSqliteColumnInventory as renderSqliteColumnInventory2, renderMysqlObjectMembers as renderMysqlObjectMembers2, sqliteTableConstraints as sqliteTableConstraints3, renderSqliteForeignKeyInventory } from "@vibeorm/sql";
|
|
1431
|
+
|
|
1432
|
+
// packages/migrate/src/external-objects.ts
|
|
1433
|
+
import { IDENTIFIER_LIMITS, VibeError as VibeError7, identifierLength, sqlNamespaceIssue, sqlObjectKey as sqlObjectKey3, sqlObjectNamespace, qualifiedSqlName as qualifiedSqlName5, enumStorageOf as enumStorageOf3, unsupportedCapability as unsupportedCapability6 } from "@vibeorm/schema";
|
|
1434
|
+
import { getSqlDialect, renderExternalOwnershipCatalog, renderReplayCoverageGaps } from "@vibeorm/sql";
|
|
1435
|
+
var partitionPatterns = new WeakMap;
|
|
1436
|
+
function invalid2(params) {
|
|
1437
|
+
return new VibeError7({ 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 } });
|
|
1438
|
+
}
|
|
1439
|
+
function record2(value) {
|
|
1440
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1441
|
+
}
|
|
1442
|
+
function identifier(value) {
|
|
1443
|
+
return typeof value === "string" && value.trim().length > 0 && !value.includes("\x00") && identifierLength({ name: value, dialect: "postgres" }) <= IDENTIFIER_LIMITS.postgres.max;
|
|
1444
|
+
}
|
|
1445
|
+
function names(params) {
|
|
1446
|
+
if (params.value === undefined)
|
|
1447
|
+
return;
|
|
1448
|
+
if (!Array.isArray(params.value) || !params.value.every(identifier) || new Set(params.value).size !== params.value.length)
|
|
1449
|
+
throw invalid2({ detail: `${params.key} must contain unique non-empty PostgreSQL identifiers` });
|
|
1450
|
+
return [...params.value];
|
|
1451
|
+
}
|
|
1452
|
+
async function parseExternalObjects(params) {
|
|
1453
|
+
return Promise.resolve().then(() => {
|
|
1454
|
+
if (!Array.isArray(params.value))
|
|
1455
|
+
throw invalid2({ detail: "external must be an array" });
|
|
1456
|
+
if (params.value.length > 0 && params.dialect !== undefined && params.dialect !== "postgres")
|
|
1457
|
+
throw unsupportedCapability6({ dialect: params.dialect, feature: "externalObjects" });
|
|
1458
|
+
const owners = new Set;
|
|
1459
|
+
const claims = new Set;
|
|
1460
|
+
const compiled = [];
|
|
1461
|
+
const tableClaims = [];
|
|
1462
|
+
const result = [];
|
|
1463
|
+
for (const value of params.value) {
|
|
1464
|
+
if (!record2(value) || typeof value.owner !== "string" || value.owner.trim().length === 0 || value.owner.includes("\x00"))
|
|
1465
|
+
throw invalid2({ detail: "each entry needs a non-empty owner" });
|
|
1466
|
+
const owner = value.owner;
|
|
1467
|
+
if (owners.has(owner))
|
|
1468
|
+
throw invalid2({ detail: "an owner must have one declaration", owner });
|
|
1469
|
+
owners.add(owner);
|
|
1470
|
+
const allowed = value.schema === undefined ? ["owner", "tables", "partitions", "functions"] : ["owner", "schema"];
|
|
1471
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
1472
|
+
throw invalid2({ detail: "whole-schema and named-object declarations are exclusive; unknown keys refuse", owner });
|
|
1473
|
+
const claim = (options) => {
|
|
1474
|
+
const { kind, name } = options;
|
|
1475
|
+
const key = JSON.stringify([kind, name]);
|
|
1476
|
+
if (claims.has(key))
|
|
1477
|
+
throw invalid2({ detail: `${kind} ${JSON.stringify(name)} has conflicting owners`, owner });
|
|
1478
|
+
claims.add(key);
|
|
1479
|
+
};
|
|
1480
|
+
if (value.schema !== undefined) {
|
|
1481
|
+
const issue = sqlNamespaceIssue({ namespace: value.schema, allowPublic: false });
|
|
1482
|
+
if (issue !== undefined || typeof value.schema !== "string")
|
|
1483
|
+
throw invalid2({ detail: issue ?? "schema must be a string", owner });
|
|
1484
|
+
claim({ kind: "namespace", name: value.schema });
|
|
1485
|
+
result.push(Object.freeze({ owner, schema: value.schema }));
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
const tables = names({ value: value.tables, key: "tables" });
|
|
1489
|
+
const functions = names({ value: value.functions, key: "functions" });
|
|
1490
|
+
const partitions = [];
|
|
1491
|
+
const ownerPatterns = [];
|
|
1492
|
+
if (value.partitions !== undefined) {
|
|
1493
|
+
if (!Array.isArray(value.partitions))
|
|
1494
|
+
throw invalid2({ detail: "partitions must be an array", owner });
|
|
1495
|
+
for (const partition of value.partitions) {
|
|
1496
|
+
if (!record2(partition) || Object.keys(partition).some((key) => key !== "parent" && key !== "pattern") || !identifier(partition.parent) || typeof partition.pattern !== "string" || partition.pattern.length === 0 || !tables?.includes(partition.parent))
|
|
1497
|
+
throw invalid2({ detail: "partition patterns require a parent in the same owner's tables", owner });
|
|
1498
|
+
const pattern = new RegExp(`^(?:${partition.pattern})$`);
|
|
1499
|
+
compiled.push({ owner, pattern });
|
|
1500
|
+
ownerPatterns.push({ parent: partition.parent, pattern });
|
|
1501
|
+
partitions.push(Object.freeze({ parent: partition.parent, pattern: partition.pattern }));
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if ((tables?.length ?? 0) + (functions?.length ?? 0) + partitions.length === 0)
|
|
1505
|
+
throw invalid2({ detail: "named ownership needs at least one table or function", owner });
|
|
1506
|
+
for (const name of tables ?? []) {
|
|
1507
|
+
if (name === "_vibe_migrations")
|
|
1508
|
+
throw invalid2({ detail: "the migration journal cannot be claimed externally", owner });
|
|
1509
|
+
claim({ kind: "table", name });
|
|
1510
|
+
tableClaims.push({ owner, name });
|
|
1511
|
+
}
|
|
1512
|
+
for (const name of functions ?? [])
|
|
1513
|
+
claim({ kind: "function", name });
|
|
1514
|
+
const declaration = Object.freeze({ owner, ...tables === undefined ? {} : { tables: Object.freeze(tables) }, ...functions === undefined ? {} : { functions: Object.freeze(functions) }, ...value.partitions === undefined ? {} : { partitions: Object.freeze(partitions) } });
|
|
1515
|
+
partitionPatterns.set(declaration, ownerPatterns);
|
|
1516
|
+
result.push(declaration);
|
|
1517
|
+
}
|
|
1518
|
+
for (const table of tableClaims)
|
|
1519
|
+
for (const pattern of compiled) {
|
|
1520
|
+
if (table.owner !== pattern.owner && pattern.pattern.exec(table.name)?.[0] === table.name)
|
|
1521
|
+
throw invalid2({ detail: "a full-name pattern claims another owner's named table", owner: pattern.owner });
|
|
1522
|
+
}
|
|
1523
|
+
return Object.freeze(result);
|
|
1524
|
+
}).catch((cause) => {
|
|
1525
|
+
if (cause instanceof VibeError7)
|
|
1526
|
+
throw cause;
|
|
1527
|
+
throw invalid2({ detail: "regular-expression pattern cannot be compiled", cause });
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
function nameKey(name) {
|
|
1531
|
+
return sqlObjectKey3({ identity: { kind: "table", name } });
|
|
1532
|
+
}
|
|
1533
|
+
function schemaOwner(params) {
|
|
1534
|
+
return params.external.find((entry) => entry.schema === params.namespace)?.owner;
|
|
1535
|
+
}
|
|
1536
|
+
function declaredTableOwner(params) {
|
|
1537
|
+
const whole = schemaOwner({ external: params.external, namespace: params.name.namespace });
|
|
1538
|
+
if (whole !== undefined || params.name.namespace !== "public")
|
|
1539
|
+
return whole;
|
|
1540
|
+
const matches = params.external.filter((entry) => {
|
|
1541
|
+
if (entry.tables?.includes(params.name.name))
|
|
1542
|
+
return true;
|
|
1543
|
+
const patterns = partitionPatterns.get(entry);
|
|
1544
|
+
if ((entry.partitions?.length ?? 0) > 0 && patterns === undefined)
|
|
1545
|
+
throw invalid2({ detail: "resolve partition declarations returned by parseExternalObjects", owner: entry.owner });
|
|
1546
|
+
return patterns?.some(({ pattern }) => pattern.exec(params.name.name)?.[0] === params.name.name) === true;
|
|
1547
|
+
});
|
|
1548
|
+
if (matches.length > 1)
|
|
1549
|
+
throw new VibeError7({ 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) } });
|
|
1550
|
+
return matches[0]?.owner;
|
|
1551
|
+
}
|
|
1552
|
+
function externalObjectOwner(params) {
|
|
1553
|
+
const { identity, external } = params;
|
|
1554
|
+
const whole = schemaOwner({ external: external.declarations, namespace: sqlObjectNamespace({ identity }) });
|
|
1555
|
+
if (whole !== undefined)
|
|
1556
|
+
return whole;
|
|
1557
|
+
if (identity.kind === "table" || "table" in identity) {
|
|
1558
|
+
const name = "table" in identity ? identity.table : identity.name;
|
|
1559
|
+
return external.tables.find((entry) => nameKey(entry.name) === nameKey(name))?.owner ?? declaredTableOwner({ external: external.declarations, name });
|
|
1560
|
+
}
|
|
1561
|
+
if (identity.kind === "sequence")
|
|
1562
|
+
return external.sequences.find((entry) => nameKey(entry.name) === nameKey(identity.name))?.owner;
|
|
1563
|
+
if (identity.kind === "function" && identity.name.namespace === "public")
|
|
1564
|
+
return external.declarations.find((entry) => entry.functions?.includes(identity.name.name))?.owner;
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
function resolveExternalObjectOwnership(params) {
|
|
1568
|
+
if (params.external.length > 0 && params.dialect !== "postgres")
|
|
1569
|
+
throw unsupportedCapability6({ dialect: params.dialect, feature: "externalObjects" });
|
|
1570
|
+
if (params.catalog.inventory.some((object) => object.inspectionUnavailable !== undefined))
|
|
1571
|
+
throw new VibeError7({ code: "VIBE_MIGRATION", message: "External ownership requires complete catalog visibility", meta: { reason: "external-inventory-unavailable", objects: params.catalog.inventory.filter((object) => object.inspectionUnavailable !== undefined) } });
|
|
1572
|
+
const tables = new Map;
|
|
1573
|
+
const assign = (entry) => {
|
|
1574
|
+
const key = nameKey(entry.name);
|
|
1575
|
+
const prior = tables.get(key)?.owner ?? declaredTableOwner({ external: params.external, name: entry.name });
|
|
1576
|
+
if (prior !== undefined && prior !== entry.owner)
|
|
1577
|
+
throw new VibeError7({ 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] } });
|
|
1578
|
+
if (tables.has(key))
|
|
1579
|
+
return false;
|
|
1580
|
+
tables.set(key, entry);
|
|
1581
|
+
return true;
|
|
1582
|
+
};
|
|
1583
|
+
for (const declaration of params.external)
|
|
1584
|
+
for (const name of declaration.tables ?? [])
|
|
1585
|
+
assign({ owner: declaration.owner, name: { namespace: "public", name } });
|
|
1586
|
+
for (const object of params.catalog.inventory) {
|
|
1587
|
+
if (object.identity.kind !== "table")
|
|
1588
|
+
continue;
|
|
1589
|
+
const owner = declaredTableOwner({ external: params.external, name: object.identity.name });
|
|
1590
|
+
if (owner !== undefined)
|
|
1591
|
+
assign({ owner, name: object.identity.name });
|
|
1592
|
+
}
|
|
1593
|
+
let changed = true;
|
|
1594
|
+
while (changed) {
|
|
1595
|
+
changed = false;
|
|
1596
|
+
for (const edge of params.catalog.inheritance) {
|
|
1597
|
+
const owner = tables.get(nameKey(edge.parent))?.owner;
|
|
1598
|
+
if (owner !== undefined && assign({ owner, name: edge.child }))
|
|
1599
|
+
changed = true;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
const sequences = params.catalog.ownedSequences.flatMap((edge) => {
|
|
1603
|
+
const owner = tables.get(nameKey(edge.table))?.owner;
|
|
1604
|
+
if (owner === undefined)
|
|
1605
|
+
return [];
|
|
1606
|
+
const whole = schemaOwner({ external: params.external, namespace: edge.sequence.namespace });
|
|
1607
|
+
if (whole !== undefined && whole !== owner)
|
|
1608
|
+
throw new VibeError7({ 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] } });
|
|
1609
|
+
return [{ owner, name: edge.sequence }];
|
|
1610
|
+
});
|
|
1611
|
+
const closure = { declarations: params.external, catalog: params.catalog, inventory: params.catalog.inventory, tables: [...tables.values()], sequences, owners: [], identities: [] };
|
|
1612
|
+
for (const edge of params.catalog.foreignKeys) {
|
|
1613
|
+
const owner = externalObjectOwner({ external: closure, identity: { kind: "table", name: edge.table } });
|
|
1614
|
+
const targetOwner = externalObjectOwner({ external: closure, identity: { kind: "table", name: edge.target } });
|
|
1615
|
+
if (owner !== targetOwner && (owner !== undefined || targetOwner !== undefined))
|
|
1616
|
+
throw new VibeError7({ 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" } });
|
|
1617
|
+
}
|
|
1618
|
+
const observed = new Map;
|
|
1619
|
+
for (const namespace of params.catalog.namespaces) {
|
|
1620
|
+
if (schemaOwner({ external: params.external, namespace }) !== undefined)
|
|
1621
|
+
observed.set(sqlObjectKey3({ identity: { kind: "namespace", name: namespace } }), { kind: "namespace", name: namespace });
|
|
1622
|
+
}
|
|
1623
|
+
for (const object of params.catalog.inventory)
|
|
1624
|
+
observed.set(sqlObjectKey3({ identity: object.identity }), object.identity);
|
|
1625
|
+
const owners = params.external.map((entry) => {
|
|
1626
|
+
const identities = [...observed.values()].filter((identity) => externalObjectOwner({ identity, external: closure }) === entry.owner);
|
|
1627
|
+
const missing = [];
|
|
1628
|
+
if (entry.schema !== undefined && !params.catalog.namespaces.includes(entry.schema))
|
|
1629
|
+
missing.push({ kind: "namespace", name: entry.schema });
|
|
1630
|
+
for (const name of entry.tables ?? [])
|
|
1631
|
+
if (!params.catalog.inventory.some(({ identity }) => identity.kind === "table" && identity.name.namespace === "public" && identity.name.name === name))
|
|
1632
|
+
missing.push({ kind: "table", name: { namespace: "public", name } });
|
|
1633
|
+
for (const name of entry.functions ?? [])
|
|
1634
|
+
if (!params.catalog.inventory.some(({ identity }) => identity.kind === "function" && identity.name.namespace === "public" && identity.name.name === name))
|
|
1635
|
+
missing.push({ kind: "function", name: { namespace: "public", name } });
|
|
1636
|
+
return { owner: entry.owner, state: "excluded", count: identities.length, identities, missing };
|
|
1637
|
+
});
|
|
1638
|
+
return { ...closure, owners, identities: owners.flatMap((owner) => owner.identities) };
|
|
1639
|
+
}
|
|
1640
|
+
function catalogName(params) {
|
|
1641
|
+
const namespace = params.row[params.namespace];
|
|
1642
|
+
const name = params.row[params.name];
|
|
1643
|
+
if (!identifier(namespace) || !identifier(name))
|
|
1644
|
+
throw new VibeError7({ code: "VIBE_MIGRATION", message: "Malformed external ownership catalog endpoint", meta: { reason: "external-inventory-unavailable" } });
|
|
1645
|
+
return { namespace, name };
|
|
1646
|
+
}
|
|
1647
|
+
async function inspectExternalObjects(params) {
|
|
1648
|
+
const external = await parseExternalObjects({ value: params.external, dialect: params.dialect });
|
|
1649
|
+
if (params.dialect !== "postgres")
|
|
1650
|
+
return resolveExternalObjectOwnership({ external, dialect: params.dialect, catalog: { namespaces: [], inventory: params.inventory ?? [], inheritance: [], ownedSequences: [], foreignKeys: [] } });
|
|
1651
|
+
const read = async (operation) => {
|
|
1652
|
+
const query = renderExternalOwnershipCatalog({ dialect: getSqlDialect({ dialect: params.dialect }), operation });
|
|
1653
|
+
return params.executor({ text: query.text, values: [...query.values] });
|
|
1654
|
+
};
|
|
1655
|
+
const namespaceRows = await read("namespaces");
|
|
1656
|
+
if (namespaceRows.some((row) => !identifier(row.namespace) || row.visible !== true))
|
|
1657
|
+
throw new VibeError7({ code: "VIBE_MIGRATION", message: "External ownership cannot prove visibility of every inspected namespace", meta: { reason: "external-inventory-unavailable" } });
|
|
1658
|
+
const namespaces = namespaceRows.map((row) => String(row.namespace));
|
|
1659
|
+
const [inherited, owned, foreignKeys] = await Promise.all([read("inheritance"), read("ownedSequences"), read("foreignKeys")]);
|
|
1660
|
+
const inventory = params.inventory ?? (await Promise.all(namespaces.map((namespace) => inspectMigrationInventory({ executor: params.executor, dialect: params.dialect, namespace })))).flat();
|
|
1661
|
+
const coverageGaps = (await Promise.all(namespaces.map(async (namespace) => {
|
|
1662
|
+
const query = renderReplayCoverageGaps({ dialect: getSqlDialect({ dialect: params.dialect }), namespace });
|
|
1663
|
+
return params.executor({ text: query.text, values: [...query.values] });
|
|
1664
|
+
}))).flat();
|
|
1665
|
+
return resolveExternalObjectOwnership({ external, dialect: params.dialect, catalog: {
|
|
1666
|
+
namespaces,
|
|
1667
|
+
inventory,
|
|
1668
|
+
coverageGaps,
|
|
1669
|
+
inheritance: inherited.map((row) => ({ parent: catalogName({ row, namespace: "parentNamespace", name: "parentName" }), child: catalogName({ row, namespace: "namespace", name: "name" }) })),
|
|
1670
|
+
ownedSequences: owned.map((row) => ({ table: catalogName({ row, namespace: "tableNamespace", name: "tableName" }), sequence: catalogName({ row, namespace: "namespace", name: "name" }) })),
|
|
1671
|
+
foreignKeys: foreignKeys.map((row) => ({ table: catalogName({ row, namespace: "namespace", name: "table" }), target: catalogName({ row, namespace: "refNamespace", name: "refTable" }), name: catalogName({ row, namespace: "namespace", name: "name" }).name }))
|
|
1672
|
+
} });
|
|
1673
|
+
}
|
|
1674
|
+
function assertExternalOwnershipClaims(params) {
|
|
1675
|
+
for (const identity of params.identities) {
|
|
1676
|
+
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);
|
|
1677
|
+
if (owner !== undefined)
|
|
1678
|
+
throw new VibeError7({ code: "VIBE_CONFIG", message: "Application or module declarations collide with external ownership", meta: { reason: "external-owned-object-collision", owner, identity } });
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
function projectExternalSchema(params) {
|
|
1682
|
+
const { schema, external } = params;
|
|
1683
|
+
const models = schema.models.filter((model) => externalObjectOwner({ external, identity: { kind: model.isView ? "view" : "table", name: qualifiedSqlName5({ name: model.dbName ?? model.name, namespace: model.namespace }) } }) === undefined);
|
|
1684
|
+
const removed = new Set(schema.models.filter((model) => !models.includes(model)).map((model) => model.name));
|
|
1685
|
+
for (const model of models)
|
|
1686
|
+
for (const relation of model.relations)
|
|
1687
|
+
if (removed.has(relation.targetModel))
|
|
1688
|
+
throw new VibeError7({ 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 } });
|
|
1689
|
+
return {
|
|
1690
|
+
...schema,
|
|
1691
|
+
models,
|
|
1692
|
+
enums: schema.enums.filter((entry) => enumStorageOf3({ enumDef: entry }) === "string" || externalObjectOwner({ external, identity: { kind: "enum", name: qualifiedSqlName5({ name: entry.dbName ?? entry.name, namespace: entry.namespace }) } }) === undefined),
|
|
1693
|
+
...schema.migrationObjects === undefined ? {} : { migrationObjects: schema.migrationObjects.filter((object) => externalObjectOwner({ external, identity: object.identity }) === undefined) }
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function projectExternalState(params) {
|
|
1697
|
+
return { ...params.state, schema: projectExternalSchema({ schema: params.state.schema, external: params.external }), definitions: params.state.definitions.filter((definition) => externalObjectOwner({ external: params.external, identity: definition.identity }) === undefined) };
|
|
1698
|
+
}
|
|
1699
|
+
function projectExternalInventory(params) {
|
|
1700
|
+
return params.inventory.filter((object) => externalObjectOwner({ identity: object.identity, external: params.external }) === undefined);
|
|
1701
|
+
}
|
|
1702
|
+
function migrationCoverageGaps(params) {
|
|
1703
|
+
const recognized = new Set([...params.namespaces, ...params.external?.tables.map((entry) => entry.name.namespace) ?? [], ...params.external?.sequences.map((entry) => entry.name.namespace) ?? []]);
|
|
1704
|
+
const kept = params.gaps.filter((gap) => {
|
|
1705
|
+
const namespace = typeof gap.namespace === "string" ? gap.namespace : params.namespace;
|
|
1706
|
+
if (gap.kind === "namespace" && typeof gap.name === "string" && recognized.has(gap.name))
|
|
1707
|
+
return false;
|
|
1708
|
+
const external = params.external;
|
|
1709
|
+
if (external === undefined)
|
|
1710
|
+
return true;
|
|
1711
|
+
if (external.declarations.some((entry) => entry.schema === namespace || gap.kind === "namespace" && entry.schema === gap.name))
|
|
1712
|
+
return false;
|
|
1713
|
+
const name = gap.kind === "rule" ? gap.parent : gap.name;
|
|
1714
|
+
if ((gap.kind === "relation-layout" || gap.kind === "rule") && typeof name === "string" && externalObjectOwner({ external, identity: { kind: "table", name: { namespace, name } } }) !== undefined)
|
|
1715
|
+
return false;
|
|
1716
|
+
return true;
|
|
1717
|
+
});
|
|
1718
|
+
return [...new Map(kept.map((gap) => [JSON.stringify(gap), gap])).values()];
|
|
1719
|
+
}
|
|
1357
1720
|
|
|
1358
1721
|
// packages/migrate/src/exclusions.ts
|
|
1359
1722
|
import { randomUUID } from "crypto";
|
|
1360
|
-
import { VibeError as
|
|
1723
|
+
import { VibeError as VibeError8, sqlObjectKey as sqlObjectKey4 } from "@vibeorm/schema";
|
|
1361
1724
|
import { postgresDialect as postgresDialect2, renderExclusionCatalog, renderExclusionProbe } from "@vibeorm/sql";
|
|
1362
1725
|
async function deparseExclusion(params) {
|
|
1363
1726
|
const probe = `vibe_exclusion_${randomUUID().replaceAll("-", "")}`;
|
|
1364
1727
|
await params.executor({ text: "SAVEPOINT vibe_exclusion_probe" });
|
|
1365
1728
|
try {
|
|
1366
|
-
for (const text of renderExclusionProbe({ dialect: postgresDialect2, table: params.table, probe, definition: params.definition }))
|
|
1729
|
+
for (const text of renderExclusionProbe({ dialect: postgresDialect2, table: params.table, ...params.namespace === undefined ? {} : { namespace: params.namespace }, probe, definition: params.definition }))
|
|
1367
1730
|
await params.executor({ text });
|
|
1368
1731
|
const rows = await params.executor({ text: renderExclusionCatalog({ dialect: postgresDialect2, probe }) });
|
|
1369
1732
|
if (typeof rows[0]?.definition !== "string")
|
|
1370
|
-
throw new
|
|
1733
|
+
throw new VibeError8({ code: "VIBE_MIGRATION", message: "Exclusion probe unavailable", meta: { reason: "exclusion-inspection-unavailable" } });
|
|
1371
1734
|
return rows[0].definition;
|
|
1372
1735
|
} finally {
|
|
1373
1736
|
await params.executor({ text: "ROLLBACK TO SAVEPOINT vibe_exclusion_probe" });
|
|
@@ -1378,7 +1741,7 @@ async function prepareExclusionDiff(params) {
|
|
|
1378
1741
|
const models = [];
|
|
1379
1742
|
for (const model of params.from.models) {
|
|
1380
1743
|
const table = model.dbName ?? model.name;
|
|
1381
|
-
const target = params.to.models.find((entry) => (entry.dbName ?? entry.name) === table);
|
|
1744
|
+
const target = params.to.models.find((entry) => (entry.dbName ?? entry.name) === table && (entry.namespace ?? "public") === (model.namespace ?? "public"));
|
|
1382
1745
|
const exclusions = [];
|
|
1383
1746
|
for (const before of model.exclusions ?? []) {
|
|
1384
1747
|
const next = target?.exclusions?.find((entry) => entry.name === before.name);
|
|
@@ -1386,22 +1749,22 @@ async function prepareExclusionDiff(params) {
|
|
|
1386
1749
|
exclusions.push(before);
|
|
1387
1750
|
continue;
|
|
1388
1751
|
}
|
|
1389
|
-
const identity = { kind: "constraint", constraintType: "exclusion", table: { namespace: "public", name: table }, name: before.name };
|
|
1752
|
+
const identity = { kind: "constraint", constraintType: "exclusion", table: { namespace: model.namespace ?? "public", name: table }, name: before.name };
|
|
1390
1753
|
if (params.probe === "rollback") {
|
|
1391
|
-
const normalized = await deparseExclusion({ executor: params.executor, table, definition: next.definition });
|
|
1754
|
+
const normalized = await deparseExclusion({ executor: params.executor, table, ...model.namespace === undefined ? {} : { namespace: model.namespace }, definition: next.definition });
|
|
1392
1755
|
exclusions.push(normalized === before.definition ? next : { name: before.name, definition: before.definition, requiredExtensions: before.requiredExtensions });
|
|
1393
1756
|
continue;
|
|
1394
1757
|
}
|
|
1395
|
-
const previous = params.previous?.schema.models.find((entry) => (entry.dbName ?? entry.name) === table)?.exclusions?.find((entry) => entry.name === before.name);
|
|
1758
|
+
const previous = params.previous?.schema.models.find((entry) => (entry.dbName ?? entry.name) === table && (entry.namespace ?? "public") === (model.namespace ?? "public"))?.exclusions?.find((entry) => entry.name === before.name);
|
|
1396
1759
|
if (previous !== undefined) {
|
|
1397
1760
|
exclusions.push(previous);
|
|
1398
1761
|
continue;
|
|
1399
1762
|
}
|
|
1400
|
-
if (params.replaceChecks?.some((entry) =>
|
|
1763
|
+
if (params.replaceChecks?.some((entry) => sqlObjectKey4({ identity: entry }) === sqlObjectKey4({ identity }))) {
|
|
1401
1764
|
exclusions.push({ name: before.name, definition: before.definition, requiredExtensions: before.requiredExtensions });
|
|
1402
1765
|
continue;
|
|
1403
1766
|
}
|
|
1404
|
-
throw new
|
|
1767
|
+
throw new VibeError8({ code: "VIBE_MIGRATION", message: "Exclusion equivalence requires a rollback probe or explicit replacement", meta: { reason: "explicit-replacement-required", identity } });
|
|
1405
1768
|
}
|
|
1406
1769
|
models.push({ ...model, exclusions });
|
|
1407
1770
|
}
|
|
@@ -1410,20 +1773,20 @@ async function prepareExclusionDiff(params) {
|
|
|
1410
1773
|
async function verifyExclusions(params) {
|
|
1411
1774
|
if (!params.expected.schema.models.some((model) => (model.exclusions?.length ?? 0) > 0))
|
|
1412
1775
|
return [];
|
|
1413
|
-
const rows = await params.executor({ text: renderExclusionCatalog({ dialect: postgresDialect2 }) });
|
|
1776
|
+
const rows = (await Promise.all([...new Set(params.expected.schema.models.map((model) => model.namespace ?? "public"))].map(async (namespace) => (await params.executor({ text: renderExclusionCatalog({ dialect: postgresDialect2, namespace }) })).map((row) => ({ ...row, namespace }))))).flat();
|
|
1414
1777
|
const objects = [];
|
|
1415
1778
|
for (const model of params.expected.schema.models)
|
|
1416
1779
|
for (const exclusion of model.exclusions ?? []) {
|
|
1417
1780
|
const table = model.dbName ?? model.name;
|
|
1418
|
-
const identity = { kind: "constraint", constraintType: "exclusion", table: { namespace: "public", name: table }, name: exclusion.name };
|
|
1419
|
-
const row = rows.find((entry) => entry.table_name === table && entry.constraint_name === exclusion.name);
|
|
1781
|
+
const identity = { kind: "constraint", constraintType: "exclusion", table: { namespace: model.namespace ?? "public", name: table }, name: exclusion.name };
|
|
1782
|
+
const row = rows.find((entry) => entry.namespace === (model.namespace ?? "public") && entry.table_name === table && entry.constraint_name === exclusion.name);
|
|
1420
1783
|
if (row === undefined) {
|
|
1421
1784
|
objects.push({ identity, ownership: "schema", state: "missing", differences: [] });
|
|
1422
1785
|
continue;
|
|
1423
1786
|
}
|
|
1424
1787
|
const differences = ["validated", "valid", "ready"].flatMap((attribute) => row[attribute] === true ? [] : [{ attribute, expected: true, actual: row[attribute] }]);
|
|
1425
|
-
const independentlyVerified = params.expected.definitions.some((entry) =>
|
|
1426
|
-
const definition = independentlyVerified ? row.definition : params.probe === "rollback" ? await deparseExclusion({ executor: params.executor, table, definition: exclusion.definition }) : (exclusion.normalizationSource === exclusion.definition ? exclusion.normalizedDefinition : undefined) ?? exclusion.definition;
|
|
1788
|
+
const independentlyVerified = params.expected.definitions.some((entry) => sqlObjectKey4({ identity: entry.identity }) === sqlObjectKey4({ identity }));
|
|
1789
|
+
const definition = independentlyVerified ? row.definition : params.probe === "rollback" ? await deparseExclusion({ executor: params.executor, table, ...model.namespace === undefined ? {} : { namespace: model.namespace }, definition: exclusion.definition }) : (exclusion.normalizationSource === exclusion.definition ? exclusion.normalizedDefinition : undefined) ?? exclusion.definition;
|
|
1427
1790
|
if (definition !== row.definition)
|
|
1428
1791
|
differences.push({ attribute: "definition", expected: definition, actual: row.definition });
|
|
1429
1792
|
objects.push({ identity, ownership: "schema", state: differences.length === 0 ? "matching" : params.probe === "rollback" ? "changed" : "unavailable", differences });
|
|
@@ -1433,22 +1796,19 @@ async function verifyExclusions(params) {
|
|
|
1433
1796
|
|
|
1434
1797
|
// packages/migrate/src/inspect-objects.ts
|
|
1435
1798
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1436
|
-
import { VibeError as
|
|
1437
|
-
import { getSqlDialect, renderMigrationObjectBatch, renderMigrationObjectQuery, renderCheckProbe, renderMigrationNamespace, renderMigrationDependencies, renderMysqlObjectMembers, renderMysqlCheckProbe, sqliteColumnClauses as sqliteColumnClauses2, renderMigrationVisibility, sqliteTableConstraints as sqliteTableConstraints2, renderSqliteColumnInventory } from "@vibeorm/sql";
|
|
1799
|
+
import { VibeError as VibeError16, sqlObjectKey as sqlObjectKey5, sqlObjectNamespace as sqlObjectNamespace2, DIALECT_CAPABILITIES as DIALECT_CAPABILITIES2, qualifiedSqlName as qualifiedSqlName8 } from "@vibeorm/schema";
|
|
1800
|
+
import { getSqlDialect as getSqlDialect2, renderMigrationObjectBatch, renderMigrationObjectQuery as renderMigrationObjectQuery2, renderCheckProbe, renderMigrationNamespace, renderMigrationDependencies, renderMysqlObjectMembers, renderMysqlCheckProbe, sqliteColumnClauses as sqliteColumnClauses2, renderMigrationVisibility, sqliteTableConstraints as sqliteTableConstraints2, renderSqliteColumnInventory } from "@vibeorm/sql";
|
|
1438
1801
|
|
|
1439
1802
|
// packages/migrate/src/introspect/postgres.ts
|
|
1440
1803
|
import { renderExclusionCatalog as renderExclusionCatalog2 } from "@vibeorm/sql";
|
|
1441
1804
|
import { parsePgArrayLiteral } from "@vibeorm/sql";
|
|
1442
1805
|
import { scalarListDefault as scalarListDefault2 } from "@vibeorm/schema";
|
|
1443
1806
|
import { exactIntegerLiteral } from "@vibeorm/schema";
|
|
1444
|
-
import { postgresDialect as postgresDialect6, renderRlsReadinessQuery } from "@vibeorm/sql";
|
|
1445
|
-
import { VibeError as
|
|
1446
|
-
|
|
1447
|
-
// packages/migrate/src/journal-table.ts
|
|
1448
|
-
var MIGRATIONS_TABLE_NAME = "_vibe_migrations";
|
|
1807
|
+
import { postgresDialect as postgresDialect6, renderRlsReadinessQuery, renderPostgresPhysicalCatalog, renderPostgresIndexKeys, renderMigrationObjectQuery } from "@vibeorm/sql";
|
|
1808
|
+
import { VibeError as VibeError13, assertSqlNamespace as assertSqlNamespace2, isReservedClientName } from "@vibeorm/schema";
|
|
1449
1809
|
|
|
1450
1810
|
// packages/migrate/src/introspect/shared.ts
|
|
1451
|
-
import { VibeError as
|
|
1811
|
+
import { VibeError as VibeError9 } from "@vibeorm/schema";
|
|
1452
1812
|
function detectJoinTable(params) {
|
|
1453
1813
|
const { table, columnNames, fkGroups, indexes } = params;
|
|
1454
1814
|
if (!table.startsWith("_"))
|
|
@@ -1466,8 +1826,8 @@ function detectJoinTable(params) {
|
|
|
1466
1826
|
if (fkA.refTable > fkB.refTable || tableFks.some((fk) => !fk.required || fk.onDelete !== "Cascade" || fk.onUpdate !== "Cascade"))
|
|
1467
1827
|
return null;
|
|
1468
1828
|
const tableIndexes = indexes.filter((index) => index.table === table);
|
|
1469
|
-
const canonicalAb = (index) => index.unique && index.kind === "btree" && index.where === undefined && index.nullsNotDistinct !== true && index.columns.length === 2 && index.columns[0]?.name === "A" && index.columns[1]?.name === "B" && index.columns.every((column) => !column.desc);
|
|
1470
|
-
const canonicalB = (index) => !index.unique && index.kind === "btree" && index.where === undefined && index.columns.length === 1 && index.columns[0]?.name === "B" && !index.columns[0].desc;
|
|
1829
|
+
const canonicalAb = (index) => index.unique && index.kind === "btree" && index.where === undefined && index.nullsNotDistinct !== true && index.columns.length === 2 && index.columns[0]?.name === "A" && index.columns[1]?.name === "B" && index.columns.every((column) => !column.desc && column.operatorClass === undefined);
|
|
1830
|
+
const canonicalB = (index) => !index.unique && index.kind === "btree" && index.where === undefined && index.columns.length === 1 && index.columns[0]?.name === "B" && !index.columns[0].desc && index.columns[0].operatorClass === undefined;
|
|
1471
1831
|
if (tableIndexes.length !== 2 || tableIndexes.filter(canonicalAb).length !== 1 || tableIndexes.filter(canonicalB).length !== 1)
|
|
1472
1832
|
return null;
|
|
1473
1833
|
const sorted = [fkA.refTable, fkB.refTable].sort();
|
|
@@ -1487,7 +1847,7 @@ function addFkRelations(params) {
|
|
|
1487
1847
|
const primary = target?.primaryKey?.fields ?? target?.fields.filter((field) => field.isId).map((field) => field.name) ?? [];
|
|
1488
1848
|
const columns = primary.map((name) => target?.fields.find((field) => field.name === name)?.dbName ?? name);
|
|
1489
1849
|
if (columns.length !== 1 || fk.refColumns.length !== 1 || columns[0] !== fk.refColumns[0])
|
|
1490
|
-
throw new
|
|
1850
|
+
throw new VibeError9({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Join table "${table}" does not reference the single-column primary key of "${fk.refTable}"; automatic join compression is unavailable`, meta: { feature: "implicitJoinReferencedKey", table, target: fk.refTable } });
|
|
1491
1851
|
}
|
|
1492
1852
|
const relationsByModel = new Map;
|
|
1493
1853
|
const takeName = nameAllocator({ models });
|
|
@@ -1499,9 +1859,9 @@ function addFkRelations(params) {
|
|
|
1499
1859
|
const target = modelsByName.get(group.refTable);
|
|
1500
1860
|
if (owner === undefined || target === undefined)
|
|
1501
1861
|
continue;
|
|
1502
|
-
const relationName = ambiguousPairs.has(modelPairKey({ a: group.table, b: group.refTable })) ? `${
|
|
1862
|
+
const relationName = ambiguousPairs.has(modelPairKey({ a: group.table, b: group.refTable })) ? `${owner.dbName ?? owner.name}_${group.columns.join("_")}To${target.dbName ?? target.name}` : undefined;
|
|
1503
1863
|
const fkSide = {
|
|
1504
|
-
name: takeName({ model: owner.name, base: lowerFirst(
|
|
1864
|
+
name: takeName({ model: owner.name, base: lowerFirst(target.dbName ?? target.name) }),
|
|
1505
1865
|
model: owner.name,
|
|
1506
1866
|
targetModel: group.refTable,
|
|
1507
1867
|
type: "manyToOne",
|
|
@@ -1516,7 +1876,7 @@ function addFkRelations(params) {
|
|
|
1516
1876
|
};
|
|
1517
1877
|
pushRelation({ relationsByModel, relation: fkSide });
|
|
1518
1878
|
const listSide = {
|
|
1519
|
-
name: takeName({ model: target.name, base: lowerFirst(
|
|
1879
|
+
name: takeName({ model: target.name, base: lowerFirst(owner.dbName ?? owner.name) }),
|
|
1520
1880
|
model: target.name,
|
|
1521
1881
|
targetModel: group.table,
|
|
1522
1882
|
type: "oneToMany",
|
|
@@ -1542,7 +1902,7 @@ function addJoinTableRelations(params) {
|
|
|
1542
1902
|
pushRelation({
|
|
1543
1903
|
relationsByModel,
|
|
1544
1904
|
relation: {
|
|
1545
|
-
name: takeName({ model: modelA.name, base: lowerFirst(modelB.name) }),
|
|
1905
|
+
name: takeName({ model: modelA.name, base: lowerFirst(modelB.dbName ?? modelB.name) }),
|
|
1546
1906
|
model: modelA.name,
|
|
1547
1907
|
targetModel: modelB.name,
|
|
1548
1908
|
type: "oneToMany",
|
|
@@ -1554,7 +1914,7 @@ function addJoinTableRelations(params) {
|
|
|
1554
1914
|
pushRelation({
|
|
1555
1915
|
relationsByModel,
|
|
1556
1916
|
relation: {
|
|
1557
|
-
name: takeName({ model: modelB.name, base: lowerFirst(modelA.name) }),
|
|
1917
|
+
name: takeName({ model: modelB.name, base: lowerFirst(modelA.dbName ?? modelA.name) }),
|
|
1558
1918
|
model: modelB.name,
|
|
1559
1919
|
targetModel: modelA.name,
|
|
1560
1920
|
type: "oneToMany",
|
|
@@ -1635,14 +1995,14 @@ function lowerFirst(value) {
|
|
|
1635
1995
|
}
|
|
1636
1996
|
|
|
1637
1997
|
// packages/migrate/src/postgres-type-identity.ts
|
|
1638
|
-
import { VibeError as
|
|
1998
|
+
import { VibeError as VibeError10, qualifiedSqlName as qualifiedSqlName6 } from "@vibeorm/schema";
|
|
1639
1999
|
import { postgresDialect as postgresDialect3, renderPostgresTypeIdentity } from "@vibeorm/sql";
|
|
1640
2000
|
async function resolvePostgresTypeIdentities(params) {
|
|
1641
2001
|
const unavailable = [];
|
|
1642
2002
|
const models = [];
|
|
1643
2003
|
for (const model of params.schema.models) {
|
|
1644
|
-
const table = {
|
|
1645
|
-
const previous = params.previous?.models.find((entry) => (entry.dbName ?? entry.name) === table.name);
|
|
2004
|
+
const table = qualifiedSqlName6({ name: model.dbName ?? model.name, namespace: model.namespace });
|
|
2005
|
+
const previous = params.previous?.models.find((entry) => (entry.dbName ?? entry.name) === table.name && (entry.namespace ?? "public") === table.namespace);
|
|
1646
2006
|
const fields = [];
|
|
1647
2007
|
for (const field of model.fields) {
|
|
1648
2008
|
if (field.isUnsupported !== true) {
|
|
@@ -1653,7 +2013,7 @@ async function resolvePostgresTypeIdentities(params) {
|
|
|
1653
2013
|
const parsed = parseUnsupportedPostgresType({ raw: field.nativeType ?? "" });
|
|
1654
2014
|
const evidence = field.nativeTypeIdentity;
|
|
1655
2015
|
if (evidence !== undefined && (evidence.source !== field.nativeType || parsed?.namespace !== undefined && parsed.namespace !== evidence.namespace)) {
|
|
1656
|
-
throw new
|
|
2016
|
+
throw new VibeError10({ code: "VIBE_MIGRATION", message: "PostgreSQL type evidence no longer matches its declaration; recapture the independent reference", meta: { reason: "type-reference-invalid", shadowReusable: true, table: table.name, column } });
|
|
1657
2017
|
}
|
|
1658
2018
|
const prior = previous?.fields.find((entry) => (entry.dbName ?? entry.name) === column && entry.isUnsupported === true);
|
|
1659
2019
|
const priorType = parseUnsupportedPostgresType({ raw: prior?.nativeType ?? "" });
|
|
@@ -1663,7 +2023,7 @@ async function resolvePostgresTypeIdentities(params) {
|
|
|
1663
2023
|
const source = params.mode === "catalog" ? { table, column } : { type: { name: parsed?.name ?? "", ...namespace === undefined && params.mode === "reference" ? {} : { namespace: namespace ?? "pg_catalog" } } };
|
|
1664
2024
|
const query = renderPostgresTypeIdentity({ dialect: postgresDialect3, source });
|
|
1665
2025
|
const rows = parsed === undefined && params.mode !== "catalog" ? [] : await params.executor({ text: query.text, values: [...query.values] }).then((rows) => rows, (cause) => {
|
|
1666
|
-
throw new
|
|
2026
|
+
throw new VibeError10({ code: "VIBE_MIGRATION", message: "PostgreSQL type identity inspection failed", meta: { reason: "type-inspection-unavailable", table: table.name, column }, cause });
|
|
1667
2027
|
});
|
|
1668
2028
|
const resolved = rows.length === 1 ? rows[0] : undefined;
|
|
1669
2029
|
namespace = typeof resolved?.namespace === "string" && (resolved.name === parsed?.name || parsed === undefined && params.mode === "catalog") ? resolved.namespace : undefined;
|
|
@@ -1681,11 +2041,11 @@ async function resolvePostgresTypeIdentities(params) {
|
|
|
1681
2041
|
}
|
|
1682
2042
|
|
|
1683
2043
|
// packages/migrate/src/introspect/postgres-rls.ts
|
|
1684
|
-
import { VibeError as
|
|
2044
|
+
import { VibeError as VibeError12, assertValidSchemaIR as assertValidSchemaIR3 } from "@vibeorm/schema";
|
|
1685
2045
|
import { NATIVE_RLS, postgresDialect as postgresDialect5, renderRlsPolicy as renderRlsPolicy2 } from "@vibeorm/sql";
|
|
1686
2046
|
|
|
1687
2047
|
// packages/migrate/src/ddl/rls.ts
|
|
1688
|
-
import { VibeError as
|
|
2048
|
+
import { VibeError as VibeError11, assertValidSchemaIR as assertValidSchemaIR2, unsupportedCapability as unsupportedCapability7 } from "@vibeorm/schema";
|
|
1689
2049
|
import * as sql from "@vibeorm/sql";
|
|
1690
2050
|
function isRlsProtected(params) {
|
|
1691
2051
|
return params.model.rls !== undefined && params.model.rls !== "unprotected";
|
|
@@ -1715,7 +2075,18 @@ function assertRlsDialect(params) {
|
|
|
1715
2075
|
if (params.schema.rls === undefined && !params.schema.models.some((model) => model.rls !== undefined))
|
|
1716
2076
|
return;
|
|
1717
2077
|
if (!sql.DIALECT_CAPABILITIES[params.dialect].nativeRowLevelSecurity) {
|
|
1718
|
-
throw
|
|
2078
|
+
throw unsupportedCapability7({ dialect: params.dialect, feature: "nativeRowLevelSecurity" });
|
|
2079
|
+
}
|
|
2080
|
+
for (const model of params.schema.models) {
|
|
2081
|
+
if (model.namespace !== undefined && model.namespace !== "public" && isRlsProtected({ model }))
|
|
2082
|
+
throw unsupportedCapability7({ dialect: params.dialect, feature: "fixedNamespaceNativeRls", hint: "Named package schemas do not support native RLS placement in this contract" });
|
|
2083
|
+
const policy = model.rls;
|
|
2084
|
+
if (isRlsProtected({ model }) && typeof policy === "object" && "through" in policy) {
|
|
2085
|
+
const relation = model.relations.find((entry) => entry.name === policy.through);
|
|
2086
|
+
const parent = params.schema.models.find((entry) => entry.name === relation?.targetModel);
|
|
2087
|
+
if (parent?.namespace !== undefined && parent.namespace !== "public")
|
|
2088
|
+
throw unsupportedCapability7({ dialect: params.dialect, feature: "fixedNamespaceNativeRls" });
|
|
2089
|
+
}
|
|
1719
2090
|
}
|
|
1720
2091
|
assertValidSchemaIR2({ schema: params.schema });
|
|
1721
2092
|
}
|
|
@@ -1723,8 +2094,9 @@ function withRlsSteps(params) {
|
|
|
1723
2094
|
const { from, to, steps } = params;
|
|
1724
2095
|
const before = orderedRlsModels({ schema: from });
|
|
1725
2096
|
const after = orderedRlsModels({ schema: to });
|
|
1726
|
-
const
|
|
1727
|
-
const
|
|
2097
|
+
const tableKey = (params) => physicalNameKey(params);
|
|
2098
|
+
const beforeByTable = new Map(before.map((model) => [tableKey({ name: model.dbName ?? model.name, namespace: model.namespace }), model]));
|
|
2099
|
+
const afterByTable = new Map(after.map((model) => [tableKey({ name: model.dbName ?? model.name, namespace: model.namespace }), model]));
|
|
1728
2100
|
const changed = new Set;
|
|
1729
2101
|
const policyChanges = new Set;
|
|
1730
2102
|
for (const [table, model] of beforeByTable) {
|
|
@@ -1736,7 +2108,7 @@ function withRlsSteps(params) {
|
|
|
1736
2108
|
}
|
|
1737
2109
|
for (const step of steps) {
|
|
1738
2110
|
if (step.kind === "dropTable" || step.kind === "dropColumn" || step.kind === "alterColumn" || step.kind === "dropForeignKey" || step.kind === "dropPrimaryKey")
|
|
1739
|
-
changed.add(step.table);
|
|
2111
|
+
changed.add(tableKey({ name: step.table, namespace: step.namespace }));
|
|
1740
2112
|
}
|
|
1741
2113
|
for (const schema of [from, to]) {
|
|
1742
2114
|
for (const model of orderedRlsModels({ schema })) {
|
|
@@ -1745,9 +2117,9 @@ function withRlsSteps(params) {
|
|
|
1745
2117
|
const through = model.rls.through;
|
|
1746
2118
|
const relation = model.relations.find((entry) => entry.name === through);
|
|
1747
2119
|
const parent = schema.models.find((entry) => entry.name === relation?.targetModel);
|
|
1748
|
-
if (parent !== undefined && changed.has(parent.dbName ?? parent.name)) {
|
|
1749
|
-
changed.add(model.dbName ?? model.name);
|
|
1750
|
-
policyChanges.add(model.dbName ?? model.name);
|
|
2120
|
+
if (parent !== undefined && changed.has(tableKey({ name: parent.dbName ?? parent.name, namespace: parent.namespace }))) {
|
|
2121
|
+
changed.add(tableKey({ name: model.dbName ?? model.name, namespace: model.namespace }));
|
|
2122
|
+
policyChanges.add(tableKey({ name: model.dbName ?? model.name, namespace: model.namespace }));
|
|
1751
2123
|
}
|
|
1752
2124
|
}
|
|
1753
2125
|
}
|
|
@@ -1756,20 +2128,22 @@ function withRlsSteps(params) {
|
|
|
1756
2128
|
const removals = [];
|
|
1757
2129
|
for (const model of [...before].reverse()) {
|
|
1758
2130
|
const table = model.dbName ?? model.name;
|
|
1759
|
-
|
|
2131
|
+
const key = tableKey({ name: table, namespace: model.namespace });
|
|
2132
|
+
if (!changed.has(key))
|
|
1760
2133
|
continue;
|
|
1761
|
-
const target = afterByTable.get(
|
|
1762
|
-
const securityChange = target === undefined ? `remove native RLS protection from "${table}"` : policyChanges.has(
|
|
2134
|
+
const target = afterByTable.get(key);
|
|
2135
|
+
const securityChange = target === undefined ? `remove native RLS protection from "${table}"` : policyChanges.has(key) && target.rls !== "deny" ? `replace native RLS policy on "${table}" (access may widen)` : undefined;
|
|
1763
2136
|
drops.push({ kind: "dropPolicy", table, destructive: false, ...securityChange === undefined ? {} : { securityChange } });
|
|
1764
|
-
if (target === undefined && to.models.some((entry) => (entry.dbName ?? entry.name) ===
|
|
2137
|
+
if (target === undefined && to.models.some((entry) => tableKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace }) === key)) {
|
|
1765
2138
|
removals.push({ kind: "noForceRls", table, destructive: false }, { kind: "disableRls", table, destructive: false });
|
|
1766
2139
|
}
|
|
1767
2140
|
}
|
|
1768
2141
|
for (const model of after) {
|
|
1769
2142
|
const table = model.dbName ?? model.name;
|
|
1770
|
-
|
|
2143
|
+
const key = tableKey({ name: table, namespace: model.namespace });
|
|
2144
|
+
if (beforeByTable.has(key) && !changed.has(key))
|
|
1771
2145
|
continue;
|
|
1772
|
-
if (!beforeByTable.has(
|
|
2146
|
+
if (!beforeByTable.has(key))
|
|
1773
2147
|
installs.push({ kind: "enableRls", table, destructive: false }, { kind: "forceRls", table, destructive: false });
|
|
1774
2148
|
installs.push({ kind: "createPolicy", table, schema: to, model: model.name, destructive: false });
|
|
1775
2149
|
}
|
|
@@ -1781,7 +2155,7 @@ function isRlsStep(params) {
|
|
|
1781
2155
|
function renderRlsStep(params) {
|
|
1782
2156
|
const { step, dialect } = params;
|
|
1783
2157
|
if (!sql.DIALECT_CAPABILITIES[dialect].nativeRowLevelSecurity)
|
|
1784
|
-
throw
|
|
2158
|
+
throw unsupportedCapability7({ dialect, feature: "nativeRowLevelSecurity" });
|
|
1785
2159
|
const common = { dialect: sql.postgresDialect, table: "table" in step ? step.table ?? "" : "" };
|
|
1786
2160
|
switch (step.kind) {
|
|
1787
2161
|
case "enableRls":
|
|
@@ -1797,11 +2171,11 @@ function renderRlsStep(params) {
|
|
|
1797
2171
|
const renderers = sql;
|
|
1798
2172
|
const renderer = renderers[name];
|
|
1799
2173
|
if (renderer === undefined)
|
|
1800
|
-
throw
|
|
2174
|
+
throw unsupportedCapability7({ dialect, feature: "nativeRowLevelSecurity", hint: `The SQL package must provide ${name} before this migration can execute` });
|
|
1801
2175
|
return [renderer(common).text];
|
|
1802
2176
|
}
|
|
1803
2177
|
default:
|
|
1804
|
-
throw new
|
|
2178
|
+
throw new VibeError11({ code: "VIBE_MIGRATION", message: "Expected a native RLS migration step" });
|
|
1805
2179
|
}
|
|
1806
2180
|
}
|
|
1807
2181
|
function securityChangesOf(params) {
|
|
@@ -1810,7 +2184,7 @@ function securityChangesOf(params) {
|
|
|
1810
2184
|
function assertSecurityAcknowledgement(params) {
|
|
1811
2185
|
if (params.securityChanges.length === 0 || params.acceptSecurityChanges === true)
|
|
1812
2186
|
return;
|
|
1813
|
-
throw new
|
|
2187
|
+
throw new VibeError11({
|
|
1814
2188
|
code: "VIBE_MIGRATION",
|
|
1815
2189
|
message: `Security changes require acceptSecurityChanges: true; acceptDataLoss does not authorize them:
|
|
1816
2190
|
${params.securityChanges.join(`
|
|
@@ -1849,7 +2223,7 @@ async function readPostgresRls(params) {
|
|
|
1849
2223
|
return [...tables.values()];
|
|
1850
2224
|
}
|
|
1851
2225
|
function conflict(params) {
|
|
1852
|
-
throw new
|
|
2226
|
+
throw new VibeError12({
|
|
1853
2227
|
code: "VIBE_MIGRATION",
|
|
1854
2228
|
message: `Native RLS on public.${params.table} cannot be managed safely (${params.attributes.join(", ")}). Review the live policies and restore the deployed declaration, or remove conflicting policies explicitly with administrator credentials. Supply rlsSchema/previousSchema for the deployed policy; no policy is deleted or adopted automatically.`,
|
|
1855
2229
|
meta: params
|
|
@@ -1860,10 +2234,11 @@ async function verifyPostgresRls(params) {
|
|
|
1860
2234
|
if (schema !== undefined && schema.rls !== undefined)
|
|
1861
2235
|
assertValidSchemaIR3({ schema });
|
|
1862
2236
|
await executor({ text: "SET LOCAL search_path = ''" });
|
|
1863
|
-
const declared =
|
|
2237
|
+
const declared = (schema?.models ?? []).filter((model) => model.namespace === undefined || model.namespace === "public");
|
|
2238
|
+
const byTable = new Map(declared.map((model) => [model.dbName ?? model.name, model]));
|
|
1864
2239
|
let tables = await readPostgresRls({ executor });
|
|
1865
2240
|
for (const state of tables) {
|
|
1866
|
-
const model =
|
|
2241
|
+
const model = byTable.get(state.table);
|
|
1867
2242
|
if (!state.enabled && !state.force && state.policies.length === 0 && (model === undefined || !isRlsProtected({ model })))
|
|
1868
2243
|
continue;
|
|
1869
2244
|
await executor({ text: `LOCK TABLE "public".${postgresDialect5.quoteIdent({ name: state.table })} IN ACCESS EXCLUSIVE MODE` });
|
|
@@ -1871,7 +2246,7 @@ async function verifyPostgresRls(params) {
|
|
|
1871
2246
|
tables = await readPostgresRls({ executor });
|
|
1872
2247
|
const matched = new Set;
|
|
1873
2248
|
for (const state of tables) {
|
|
1874
|
-
const model =
|
|
2249
|
+
const model = byTable.get(state.table);
|
|
1875
2250
|
const active = state.enabled || state.force || state.policies.length > 0;
|
|
1876
2251
|
if (schema === undefined && active)
|
|
1877
2252
|
conflict({ table: state.table, reason: "rls-unmanaged", attributes: ["security metadata cannot be exported losslessly without a trusted declaration"] });
|
|
@@ -1918,7 +2293,7 @@ async function verifyPostgresRls(params) {
|
|
|
1918
2293
|
matched.add(state.table);
|
|
1919
2294
|
}
|
|
1920
2295
|
if (params.requireInstalled === true) {
|
|
1921
|
-
for (const [table, model] of
|
|
2296
|
+
for (const [table, model] of byTable) {
|
|
1922
2297
|
if (isRlsProtected({ model }) && !matched.has(table))
|
|
1923
2298
|
conflict({ table, reason: "rls-drift", attributes: ["missing table"] });
|
|
1924
2299
|
}
|
|
@@ -1930,8 +2305,9 @@ function projectPostgresRls(params) {
|
|
|
1930
2305
|
if (declared?.rls === undefined || matched.size === 0)
|
|
1931
2306
|
return physical;
|
|
1932
2307
|
const models = physical.models.map((model) => {
|
|
1933
|
-
const
|
|
1934
|
-
|
|
2308
|
+
const physicalName = model.dbName ?? model.name;
|
|
2309
|
+
const source = declared.models.find((entry) => (entry.dbName ?? entry.name) === physicalName && (entry.namespace ?? "public") === (model.namespace ?? "public"));
|
|
2310
|
+
if (source === undefined || model.namespace !== undefined && model.namespace !== "public" || !matched.has(physicalName))
|
|
1935
2311
|
return { ...model, rls: "unprotected" };
|
|
1936
2312
|
const sourcePolicy = source.rls;
|
|
1937
2313
|
let rls = "deny";
|
|
@@ -1942,7 +2318,8 @@ function projectPostgresRls(params) {
|
|
|
1942
2318
|
const target = declared.models.find((entry) => entry.name === sourceRelation.targetModel);
|
|
1943
2319
|
const fields = sourceRelation.fields.map((name) => source.fields.find((entry) => entry.name === name)?.dbName ?? name);
|
|
1944
2320
|
const references = sourceRelation.references.map((name) => target.fields.find((entry) => entry.name === name)?.dbName ?? name);
|
|
1945
|
-
const
|
|
2321
|
+
const physicalTarget = physical.models.find((entry) => (entry.dbName ?? entry.name) === (target.dbName ?? target.name) && (entry.namespace ?? "public") === (target.namespace ?? "public"));
|
|
2322
|
+
const relation = model.relations.find((entry) => entry.targetModel === physicalTarget?.name && JSON.stringify(entry.fields) === JSON.stringify(fields) && JSON.stringify(entry.references) === JSON.stringify(references));
|
|
1946
2323
|
if (relation === undefined)
|
|
1947
2324
|
conflict({ table: model.name, reason: "rls-drift", attributes: ["through foreign key"] });
|
|
1948
2325
|
rls = { through: relation.name };
|
|
@@ -1953,146 +2330,6 @@ function projectPostgresRls(params) {
|
|
|
1953
2330
|
}
|
|
1954
2331
|
|
|
1955
2332
|
// packages/migrate/src/introspect/postgres.ts
|
|
1956
|
-
var EXTENSION_OWNED_FILTER = `NOT EXISTS (
|
|
1957
|
-
SELECT 1
|
|
1958
|
-
FROM pg_catalog.pg_depend d
|
|
1959
|
-
JOIN pg_catalog.pg_class c ON c.oid = d.objid
|
|
1960
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
1961
|
-
WHERE d.classid = 'pg_class'::regclass
|
|
1962
|
-
AND d.deptype = 'e'
|
|
1963
|
-
AND n.nspname = 'public'
|
|
1964
|
-
AND c.relname = table_name
|
|
1965
|
-
)`;
|
|
1966
|
-
var TABLES_QUERY = `
|
|
1967
|
-
SELECT table_name
|
|
1968
|
-
FROM information_schema.tables
|
|
1969
|
-
WHERE table_schema = 'public'
|
|
1970
|
-
AND table_type = 'BASE TABLE'
|
|
1971
|
-
AND table_name <> '${MIGRATIONS_TABLE_NAME}'
|
|
1972
|
-
AND ${EXTENSION_OWNED_FILTER}
|
|
1973
|
-
ORDER BY table_name;
|
|
1974
|
-
`;
|
|
1975
|
-
var VIEWS_QUERY = `
|
|
1976
|
-
SELECT table_name
|
|
1977
|
-
FROM information_schema.views
|
|
1978
|
-
WHERE table_schema = 'public'
|
|
1979
|
-
AND ${EXTENSION_OWNED_FILTER}
|
|
1980
|
-
ORDER BY table_name;
|
|
1981
|
-
`;
|
|
1982
|
-
var COLUMNS_QUERY = `
|
|
1983
|
-
SELECT
|
|
1984
|
-
table_name,
|
|
1985
|
-
column_name,
|
|
1986
|
-
data_type,
|
|
1987
|
-
udt_name,
|
|
1988
|
-
is_nullable,
|
|
1989
|
-
is_identity,
|
|
1990
|
-
is_generated,
|
|
1991
|
-
collation_name,
|
|
1992
|
-
column_default,
|
|
1993
|
-
datetime_precision,
|
|
1994
|
-
character_maximum_length,
|
|
1995
|
-
numeric_precision,
|
|
1996
|
-
numeric_scale,
|
|
1997
|
-
ordinal_position,
|
|
1998
|
-
(
|
|
1999
|
-
SELECT format_type(a.atttypid, a.atttypmod)
|
|
2000
|
-
FROM pg_attribute a
|
|
2001
|
-
JOIN pg_class c ON c.oid = a.attrelid
|
|
2002
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
2003
|
-
WHERE n.nspname = 'public'
|
|
2004
|
-
AND c.relname = columns.table_name
|
|
2005
|
-
AND a.attname = columns.column_name
|
|
2006
|
-
) AS full_type
|
|
2007
|
-
FROM information_schema.columns AS columns
|
|
2008
|
-
WHERE table_schema = 'public'
|
|
2009
|
-
ORDER BY table_name, ordinal_position;
|
|
2010
|
-
`;
|
|
2011
|
-
var CONSTRAINTS_QUERY = `
|
|
2012
|
-
SELECT
|
|
2013
|
-
c.conname AS constraint_name,
|
|
2014
|
-
CASE c.contype WHEN 'p' THEN 'PRIMARY KEY' ELSE 'UNIQUE' END AS constraint_type,
|
|
2015
|
-
cl.relname AS table_name,
|
|
2016
|
-
att.attname AS column_name
|
|
2017
|
-
FROM pg_catalog.pg_constraint c
|
|
2018
|
-
JOIN pg_catalog.pg_class cl ON cl.oid = c.conrelid
|
|
2019
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace
|
|
2020
|
-
JOIN unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord) ON true
|
|
2021
|
-
JOIN pg_catalog.pg_attribute att ON att.attrelid = c.conrelid AND att.attnum = k.attnum
|
|
2022
|
-
WHERE c.contype IN ('p', 'u')
|
|
2023
|
-
AND n.nspname = 'public'
|
|
2024
|
-
ORDER BY cl.relname, c.conname, k.ord;
|
|
2025
|
-
`;
|
|
2026
|
-
var CHECKS_QUERY = `
|
|
2027
|
-
SELECT
|
|
2028
|
-
cl.relname AS table_name,
|
|
2029
|
-
c.conname AS constraint_name,
|
|
2030
|
-
pg_get_constraintdef(c.oid) AS definition
|
|
2031
|
-
FROM pg_catalog.pg_constraint c
|
|
2032
|
-
JOIN pg_catalog.pg_class cl ON cl.oid = c.conrelid
|
|
2033
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace
|
|
2034
|
-
WHERE c.contype = 'c'
|
|
2035
|
-
AND n.nspname = 'public'
|
|
2036
|
-
ORDER BY cl.relname, c.conname;
|
|
2037
|
-
`;
|
|
2038
|
-
var FOREIGN_KEYS_QUERY = `
|
|
2039
|
-
SELECT
|
|
2040
|
-
c.conname AS constraint_name,
|
|
2041
|
-
cl.relname AS table_name,
|
|
2042
|
-
att.attname AS column_name,
|
|
2043
|
-
fcl.relname AS foreign_table_name,
|
|
2044
|
-
fatt.attname AS foreign_column_name,
|
|
2045
|
-
c.confdeltype AS on_delete_char,
|
|
2046
|
-
c.confupdtype AS on_update_char,
|
|
2047
|
-
c.convalidated AS validated,
|
|
2048
|
-
c.condeferrable AS deferrable,
|
|
2049
|
-
(SELECT bool_and(t.tgenabled IN ('O', 'A')) FROM pg_catalog.pg_trigger t WHERE t.tgconstraint = c.oid) AS triggers_enabled
|
|
2050
|
-
FROM pg_catalog.pg_constraint c
|
|
2051
|
-
JOIN pg_catalog.pg_class cl ON cl.oid = c.conrelid
|
|
2052
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace
|
|
2053
|
-
JOIN pg_catalog.pg_class fcl ON fcl.oid = c.confrelid
|
|
2054
|
-
JOIN pg_catalog.pg_namespace fn ON fn.oid = fcl.relnamespace
|
|
2055
|
-
JOIN unnest(c.conkey) WITH ORDINALITY AS lk(attnum, ord) ON true
|
|
2056
|
-
JOIN unnest(c.confkey) WITH ORDINALITY AS rk(attnum, ord) ON lk.ord = rk.ord
|
|
2057
|
-
JOIN pg_catalog.pg_attribute att ON att.attrelid = c.conrelid AND att.attnum = lk.attnum
|
|
2058
|
-
JOIN pg_catalog.pg_attribute fatt ON fatt.attrelid = c.confrelid AND fatt.attnum = rk.attnum
|
|
2059
|
-
WHERE c.contype = 'f'
|
|
2060
|
-
AND n.nspname = 'public'
|
|
2061
|
-
AND fn.nspname = 'public'
|
|
2062
|
-
ORDER BY cl.relname, c.conname, lk.ord;
|
|
2063
|
-
`;
|
|
2064
|
-
var ENUMS_QUERY = `
|
|
2065
|
-
SELECT
|
|
2066
|
-
t.typname AS type_name,
|
|
2067
|
-
e.enumlabel AS enum_value,
|
|
2068
|
-
e.enumsortorder AS sort_order
|
|
2069
|
-
FROM pg_type t
|
|
2070
|
-
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
2071
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
2072
|
-
WHERE n.nspname = 'public'
|
|
2073
|
-
ORDER BY t.typname, e.enumsortorder;
|
|
2074
|
-
`;
|
|
2075
|
-
var INDEXES_QUERY = `
|
|
2076
|
-
SELECT
|
|
2077
|
-
tablename AS table_name,
|
|
2078
|
-
indexname AS index_name,
|
|
2079
|
-
indexdef AS index_def,
|
|
2080
|
-
con.conname AS backing_constraint,
|
|
2081
|
-
con.contype AS backing_constraint_type,
|
|
2082
|
-
health.indisvalid AS valid,
|
|
2083
|
-
health.indisready AS ready,
|
|
2084
|
-
health.indimmediate AS immediate,
|
|
2085
|
-
health.indexprs IS NULL AS plain_columns,
|
|
2086
|
-
health.indnkeyatts AS key_count,
|
|
2087
|
-
health.indnatts AS attribute_count
|
|
2088
|
-
FROM pg_indexes ix
|
|
2089
|
-
JOIN pg_catalog.pg_namespace ns ON ns.nspname = ix.schemaname
|
|
2090
|
-
JOIN pg_catalog.pg_class idx ON idx.relnamespace = ns.oid AND idx.relname = ix.indexname
|
|
2091
|
-
JOIN pg_catalog.pg_index health ON health.indexrelid = idx.oid
|
|
2092
|
-
LEFT JOIN pg_catalog.pg_constraint con ON con.conindid = idx.oid AND con.contype IN ('p', 'u', 'x')
|
|
2093
|
-
WHERE schemaname = 'public'
|
|
2094
|
-
ORDER BY tablename, indexname;
|
|
2095
|
-
`;
|
|
2096
2333
|
async function introspectPostgres(params) {
|
|
2097
2334
|
const { executor } = params;
|
|
2098
2335
|
if (params.inTransaction !== true)
|
|
@@ -2101,14 +2338,14 @@ async function introspectPostgres(params) {
|
|
|
2101
2338
|
const savedPath = await executor(renderRlsReadinessQuery({ dialect: postgresDialect6, query: "readSearchPath" }));
|
|
2102
2339
|
const searchPath = savedPath[0]?.searchPath;
|
|
2103
2340
|
if (typeof searchPath !== "string")
|
|
2104
|
-
throw new
|
|
2341
|
+
throw new VibeError13({ code: "VIBE_MIGRATION", message: "Cannot read the migration search_path" });
|
|
2105
2342
|
const matched = await verifyPostgresRls({ executor, ...params.rlsSchema === undefined ? {} : { schema: params.rlsSchema }, ...params.requireInstalledRls === undefined ? {} : { requireInstalled: params.requireInstalledRls } }).then((verified) => verified, (error) => {
|
|
2106
|
-
if (error instanceof
|
|
2343
|
+
if (error instanceof VibeError13)
|
|
2107
2344
|
throw error;
|
|
2108
|
-
throw new
|
|
2345
|
+
throw new VibeError13({ code: "VIBE_MIGRATION", message: "Native RLS verification could not complete. Check migration privileges and supply the deployed schema as previousSchema/rlsSchema before changing referenced columns.", meta: { reason: "rls-verification-unavailable" }, cause: error });
|
|
2109
2346
|
});
|
|
2110
2347
|
await executor(renderRlsReadinessQuery({ dialect: postgresDialect6, query: "setSearchPath", searchPath }));
|
|
2111
|
-
const physical = await introspectPhysicalPostgres({ executor });
|
|
2348
|
+
const physical = await introspectPhysicalPostgres({ executor, namespaces: params.namespaces ?? ["public", ...params.rlsSchema?.models.flatMap((model) => model.namespace === undefined ? [] : [model.namespace]) ?? []] });
|
|
2112
2349
|
return projectPostgresRls({ physical, ...params.rlsSchema === undefined ? {} : { declared: params.rlsSchema }, matched });
|
|
2113
2350
|
} finally {
|
|
2114
2351
|
if (params.inTransaction !== true)
|
|
@@ -2117,17 +2354,51 @@ async function introspectPostgres(params) {
|
|
|
2117
2354
|
}
|
|
2118
2355
|
async function introspectPhysicalPostgres(params) {
|
|
2119
2356
|
const { executor } = params;
|
|
2357
|
+
const namespaces = [...new Set(params.namespaces ?? ["public"])];
|
|
2358
|
+
const physicalNames = new Map;
|
|
2359
|
+
const enumIdentities = new Map;
|
|
2360
|
+
const namespaceObjects = [];
|
|
2361
|
+
const read = async (query) => {
|
|
2362
|
+
const groups = await Promise.all(namespaces.map(async (namespace) => {
|
|
2363
|
+
assertSqlNamespace2({ namespace, dialect: "postgres" });
|
|
2364
|
+
const sql = renderPostgresPhysicalCatalog({ dialect: postgresDialect6, namespace, query });
|
|
2365
|
+
const rows = await executor({ text: sql.text, values: [...sql.values] });
|
|
2366
|
+
return rows.map((row) => {
|
|
2367
|
+
const name = typeof row.table_name === "string" ? row.table_name : undefined;
|
|
2368
|
+
const table = name === undefined ? undefined : physicalNameKey({ namespace, name });
|
|
2369
|
+
if (table !== undefined && name !== undefined)
|
|
2370
|
+
physicalNames.set(table, { namespace, name });
|
|
2371
|
+
const typeName = typeof row.type_name === "string" ? row.type_name : undefined;
|
|
2372
|
+
const enumKey = typeName === undefined ? undefined : physicalNameKey({ namespace, name: typeName });
|
|
2373
|
+
if (enumKey !== undefined && typeName !== undefined)
|
|
2374
|
+
enumIdentities.set(enumKey, { namespace, name: typeName });
|
|
2375
|
+
return { ...row, ...table === undefined ? {} : { table_name: table }, ...enumKey === undefined ? {} : { type_name: enumKey }, ...typeof row.foreign_table_name === "string" ? { foreign_table_name: physicalNameKey({ namespace: typeof row.foreign_namespace === "string" ? row.foreign_namespace : namespace, name: row.foreign_table_name }) } : {} };
|
|
2376
|
+
});
|
|
2377
|
+
}));
|
|
2378
|
+
return groups.flat();
|
|
2379
|
+
};
|
|
2380
|
+
for (const namespace of namespaces) {
|
|
2381
|
+
if (namespace === "public")
|
|
2382
|
+
continue;
|
|
2383
|
+
const query = renderMigrationObjectQuery({ dialect: postgresDialect6, identity: { kind: "namespace", name: namespace } });
|
|
2384
|
+
if ((await executor({ text: query.text, values: [...query.values] })).length > 0)
|
|
2385
|
+
namespaceObjects.push({ id: `namespace:${namespace}`, dialect: "postgres", identity: { kind: "namespace", name: namespace }, create: [], drop: [], dependsOn: [] });
|
|
2386
|
+
}
|
|
2120
2387
|
const [tableRows, viewRows, columnRows, constraintRows, fkRows, enumRows, indexRows, checkRows, exclusionRows] = await Promise.all([
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
executor({ text: renderExclusionCatalog2({ dialect: postgresDialect6 }) })
|
|
2388
|
+
read("tables"),
|
|
2389
|
+
read("views"),
|
|
2390
|
+
read("columns"),
|
|
2391
|
+
read("constraints"),
|
|
2392
|
+
read("foreignKeys"),
|
|
2393
|
+
read("enums"),
|
|
2394
|
+
read("indexes"),
|
|
2395
|
+
read("checks"),
|
|
2396
|
+
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())
|
|
2130
2397
|
]);
|
|
2398
|
+
const keysQuery = renderPostgresIndexKeys({ dialect: postgresDialect6 });
|
|
2399
|
+
const indexKeys = await executor({ text: keysQuery.text, values: [...keysQuery.values] });
|
|
2400
|
+
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))
|
|
2401
|
+
throw new VibeError13({ code: "VIBE_MIGRATION", message: "PostgreSQL index-key identity is incomplete", meta: { reason: "index-inspection-unavailable" } });
|
|
2131
2402
|
const tables = tableRows.map((row) => row.table_name);
|
|
2132
2403
|
const views = viewRows.map((row) => row.table_name);
|
|
2133
2404
|
const columns = columnRows.map((row) => ({
|
|
@@ -2135,6 +2406,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2135
2406
|
name: row.column_name,
|
|
2136
2407
|
dataType: row.data_type,
|
|
2137
2408
|
udtName: row.udt_name,
|
|
2409
|
+
udtNamespace: typeof row.udt_schema === "string" ? row.udt_schema : "public",
|
|
2138
2410
|
nullable: row.is_nullable === "YES",
|
|
2139
2411
|
identity: row.is_identity === "YES",
|
|
2140
2412
|
columnDefault: row.column_default ?? null,
|
|
@@ -2167,7 +2439,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2167
2439
|
const table = row.table_name;
|
|
2168
2440
|
const constraintName = row.constraint_name;
|
|
2169
2441
|
const columnName = row.column_name;
|
|
2170
|
-
constraintNames.add(constraintName);
|
|
2442
|
+
constraintNames.add(JSON.stringify([table, constraintName]));
|
|
2171
2443
|
if (row.constraint_type === "PRIMARY KEY") {
|
|
2172
2444
|
pkNames.set(table, constraintName);
|
|
2173
2445
|
const list = pkByTable.get(table);
|
|
@@ -2176,9 +2448,10 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2176
2448
|
else
|
|
2177
2449
|
list.push(columnName);
|
|
2178
2450
|
} else {
|
|
2179
|
-
const
|
|
2451
|
+
const constraintKey = JSON.stringify([table, constraintName]);
|
|
2452
|
+
const entry = uniquesByConstraint.get(constraintKey);
|
|
2180
2453
|
if (entry === undefined)
|
|
2181
|
-
uniquesByConstraint.set(
|
|
2454
|
+
uniquesByConstraint.set(constraintKey, { table, name: constraintName, columns: [columnName] });
|
|
2182
2455
|
else
|
|
2183
2456
|
entry.columns.push(columnName);
|
|
2184
2457
|
}
|
|
@@ -2225,17 +2498,35 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2225
2498
|
for (const row of indexRows) {
|
|
2226
2499
|
if (row.backing_constraint_type === "x")
|
|
2227
2500
|
continue;
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2230
|
-
name: row.index_name
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2501
|
+
const operands = indexKeys.filter((key) => physicalNameKey({ namespace: String(key.table_namespace), name: String(key.table_name) }) === row.table_name && key.index_name === row.index_name);
|
|
2502
|
+
if (row.plain_columns === true && operands.length !== Number(row.key_count))
|
|
2503
|
+
throw new VibeError13({ 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 } });
|
|
2504
|
+
if (operands.length > 0) {
|
|
2505
|
+
if (operands.every((key) => typeof key.column_name === "string"))
|
|
2506
|
+
indexes.push({
|
|
2507
|
+
table: String(row.table_name),
|
|
2508
|
+
name: String(row.index_name),
|
|
2509
|
+
unique: operands[0].unique === true,
|
|
2510
|
+
kind: String(operands[0].method),
|
|
2511
|
+
columns: operands.map((key) => ({ name: String(key.column_name), desc: key.descending === true, ...key.exact_default === true ? {} : { operatorClass: { namespace: String(key.operator_namespace), name: String(key.operator_name) } } })),
|
|
2512
|
+
...typeof operands[0].predicate === "string" ? { where: operands[0].predicate } : {},
|
|
2513
|
+
...operands[0].nulls_not_distinct === true ? { nullsNotDistinct: true } : {}
|
|
2514
|
+
});
|
|
2515
|
+
else {
|
|
2516
|
+
const parsed = parseIndexDef({ table: String(row.table_name), name: String(row.index_name), def: String(row.index_def) });
|
|
2517
|
+
if (parsed !== null)
|
|
2518
|
+
indexes.push(parsed);
|
|
2519
|
+
}
|
|
2520
|
+
} else {
|
|
2521
|
+
const parsed = parseIndexDef({ table: String(row.table_name), name: String(row.index_name), def: String(row.index_def) });
|
|
2522
|
+
if (parsed !== null)
|
|
2523
|
+
indexes.push(parsed);
|
|
2524
|
+
}
|
|
2235
2525
|
}
|
|
2236
2526
|
const joinTables = new Map;
|
|
2237
2527
|
for (const table of tables) {
|
|
2238
|
-
|
|
2528
|
+
const physicalTable = physicalNames.get(table);
|
|
2529
|
+
if (!physicalTable.name.startsWith("_"))
|
|
2239
2530
|
continue;
|
|
2240
2531
|
const tableFks = fkRows.filter((row) => row.table_name === table);
|
|
2241
2532
|
const tableIndexes = indexRows.filter((row) => row.table_name === table);
|
|
@@ -2251,14 +2542,16 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2251
2542
|
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;
|
|
2252
2543
|
}))
|
|
2253
2544
|
continue;
|
|
2545
|
+
if (tableFks.some((row) => row.foreign_namespace !== undefined && row.foreign_namespace !== physicalTable.namespace))
|
|
2546
|
+
continue;
|
|
2254
2547
|
const detected = detectJoinTable({
|
|
2255
|
-
table,
|
|
2548
|
+
table: physicalTable.name,
|
|
2256
2549
|
columnNames: columns.filter((column) => column.table === table).map((column) => column.name),
|
|
2257
|
-
fkGroups,
|
|
2258
|
-
indexes
|
|
2550
|
+
fkGroups: fkGroups.filter((group) => group.table === table).map((group) => ({ ...group, table: physicalTable.name, refTable: physicalNames.get(group.refTable)?.name ?? group.refTable })),
|
|
2551
|
+
indexes: indexes.filter((index) => index.table === table).map((index) => ({ ...index, table: physicalTable.name }))
|
|
2259
2552
|
});
|
|
2260
2553
|
if (detected !== null)
|
|
2261
|
-
joinTables.set(table, detected);
|
|
2554
|
+
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 }) });
|
|
2262
2555
|
}
|
|
2263
2556
|
const models = [];
|
|
2264
2557
|
for (const table of tables) {
|
|
@@ -2266,6 +2559,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2266
2559
|
continue;
|
|
2267
2560
|
models.push(buildModel({
|
|
2268
2561
|
table,
|
|
2562
|
+
physicalTable: physicalNames.get(table).name,
|
|
2269
2563
|
isView: false,
|
|
2270
2564
|
columns,
|
|
2271
2565
|
pkByTable,
|
|
@@ -2281,6 +2575,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2281
2575
|
for (const view of views) {
|
|
2282
2576
|
models.push(buildModel({
|
|
2283
2577
|
table: view,
|
|
2578
|
+
physicalTable: physicalNames.get(view).name,
|
|
2284
2579
|
isView: true,
|
|
2285
2580
|
columns,
|
|
2286
2581
|
pkByTable,
|
|
@@ -2294,14 +2589,59 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2294
2589
|
}
|
|
2295
2590
|
addFkRelations({ models, fkGroups, joinTables });
|
|
2296
2591
|
addJoinTableRelations({ models, joinTables });
|
|
2592
|
+
const logicalNames = new Set([...physicalNames.values(), ...enumIdentities.values()].map((entry) => entry.name));
|
|
2593
|
+
const logicalName = (params) => {
|
|
2594
|
+
if (!params.duplicates && /^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u.test(params.name) && !isReservedClientName({ name: params.name.charAt(0).toLowerCase() + params.name.slice(1) }))
|
|
2595
|
+
return params.name;
|
|
2596
|
+
const hex = Array.from(new TextEncoder().encode(params.key), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2597
|
+
let name = `VibePhysical_${hex}`;
|
|
2598
|
+
while (logicalNames.has(name))
|
|
2599
|
+
name += "_";
|
|
2600
|
+
logicalNames.add(name);
|
|
2601
|
+
return name;
|
|
2602
|
+
};
|
|
2603
|
+
const modelNames = new Map(models.map((model) => {
|
|
2604
|
+
const identity = physicalNames.get(model.name);
|
|
2605
|
+
const duplicates = [...physicalNames.values()].filter((entry) => entry.name === identity.name).length > 1;
|
|
2606
|
+
return [model.name, logicalName({ key: model.name, name: identity.name, duplicates })];
|
|
2607
|
+
}));
|
|
2608
|
+
const enumLogicalNames = new Map(enums.map((entry) => {
|
|
2609
|
+
const identity = enumIdentities.get(entry.name);
|
|
2610
|
+
const duplicates = [...enumIdentities.values()].filter((candidate) => candidate.name === identity.name).length > 1;
|
|
2611
|
+
return [entry.name, logicalName({ key: entry.name, name: identity.name, duplicates })];
|
|
2612
|
+
}));
|
|
2613
|
+
const qualifiedModels = models.map((model) => {
|
|
2614
|
+
const identity = physicalNames.get(model.name);
|
|
2615
|
+
const name = modelNames.get(model.name);
|
|
2616
|
+
const { dbName: _physicalName, ...metadata } = model;
|
|
2617
|
+
return {
|
|
2618
|
+
...metadata,
|
|
2619
|
+
name,
|
|
2620
|
+
...name === identity.name ? {} : { dbName: identity.name },
|
|
2621
|
+
...identity.namespace === "public" ? {} : { namespace: identity.namespace },
|
|
2622
|
+
fields: model.fields.map((field) => {
|
|
2623
|
+
if (typeof field.type === "string")
|
|
2624
|
+
return field;
|
|
2625
|
+
const identity = enumIdentities.get(field.type.enum);
|
|
2626
|
+
return { ...field, type: { enum: enumLogicalNames.get(field.type.enum), ...identity.namespace === "public" ? {} : { namespace: identity.namespace } } };
|
|
2627
|
+
}),
|
|
2628
|
+
relations: model.relations.map((relation) => ({ ...relation, name: /^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u.test(relation.name) ? relation.name : `relation_${Array.from(new TextEncoder().encode(relation.name), (byte) => byte.toString(16).padStart(2, "0")).join("")}`, model: name, targetModel: modelNames.get(relation.targetModel) ?? relation.targetModel }))
|
|
2629
|
+
};
|
|
2630
|
+
});
|
|
2631
|
+
const qualifiedEnums = enums.map((entry) => {
|
|
2632
|
+
const identity = enumIdentities.get(entry.name);
|
|
2633
|
+
const name = enumLogicalNames.get(entry.name);
|
|
2634
|
+
return { ...entry, name, ...name === identity.name ? {} : { dbName: identity.name }, ...identity.namespace === "public" ? {} : { namespace: identity.namespace } };
|
|
2635
|
+
});
|
|
2297
2636
|
const resolved = await resolvePostgresTypeIdentities({ executor, mode: "catalog", schema: {
|
|
2298
2637
|
datasource: { provider: "postgresql" },
|
|
2299
|
-
models,
|
|
2300
|
-
enums,
|
|
2301
|
-
extensions: []
|
|
2638
|
+
models: qualifiedModels,
|
|
2639
|
+
enums: qualifiedEnums,
|
|
2640
|
+
extensions: [],
|
|
2641
|
+
...namespaceObjects.length === 0 ? {} : { migrationObjects: namespaceObjects }
|
|
2302
2642
|
} });
|
|
2303
2643
|
if (resolved.unavailable.length > 0)
|
|
2304
|
-
throw new
|
|
2644
|
+
throw new VibeError13({ code: "VIBE_MIGRATION", message: "PostgreSQL column type identity could not be read", meta: { reason: "type-inspection-unavailable", columns: resolved.unavailable } });
|
|
2305
2645
|
return resolved.schema;
|
|
2306
2646
|
}
|
|
2307
2647
|
function buildModel(params) {
|
|
@@ -2310,18 +2650,19 @@ function buildModel(params) {
|
|
|
2310
2650
|
const pk = pkByTable.get(table) ?? [];
|
|
2311
2651
|
const uniqueSingles = new Set;
|
|
2312
2652
|
const multiUniques = [];
|
|
2313
|
-
for (const
|
|
2653
|
+
for (const unique of uniquesByConstraint.values()) {
|
|
2654
|
+
const name = unique.name;
|
|
2314
2655
|
if (unique.table !== table)
|
|
2315
2656
|
continue;
|
|
2316
2657
|
if (unique.columns.length === 1)
|
|
2317
2658
|
uniqueSingles.add(unique.columns[0]);
|
|
2318
2659
|
multiUniques.push({ name, columns: unique.columns });
|
|
2319
2660
|
}
|
|
2320
|
-
const tableIndexes = indexes.filter((index) => index.table === table && !constraintNames.has(index.name));
|
|
2661
|
+
const tableIndexes = indexes.filter((index) => index.table === table && !constraintNames.has(JSON.stringify([table, index.name])));
|
|
2321
2662
|
for (const index of tableIndexes) {
|
|
2322
2663
|
if (!index.unique)
|
|
2323
2664
|
continue;
|
|
2324
|
-
if (index.columns.length === 1 && index.where === undefined && index.nullsNotDistinct !== true) {
|
|
2665
|
+
if (index.columns.length === 1 && index.where === undefined && index.nullsNotDistinct !== true && index.columns[0]?.operatorClass === undefined) {
|
|
2325
2666
|
uniqueSingles.add(index.columns[0].name);
|
|
2326
2667
|
}
|
|
2327
2668
|
}
|
|
@@ -2336,7 +2677,7 @@ function buildModel(params) {
|
|
|
2336
2677
|
});
|
|
2337
2678
|
}
|
|
2338
2679
|
for (const index of tableIndexes) {
|
|
2339
|
-
if (index.unique && index.columns.length === 1 && index.where === undefined && index.nullsNotDistinct !== true && index.name === `${
|
|
2680
|
+
if (index.unique && index.columns.length === 1 && index.where === undefined && index.nullsNotDistinct !== true && index.columns[0]?.operatorClass === undefined && index.name === `${params.physicalTable}_${index.columns[0].name}_key`) {
|
|
2340
2681
|
continue;
|
|
2341
2682
|
}
|
|
2342
2683
|
modelIndexes.push({
|
|
@@ -2344,7 +2685,8 @@ function buildModel(params) {
|
|
|
2344
2685
|
kind: index.unique ? "unique" : index.kind,
|
|
2345
2686
|
fields: index.columns.map((column) => ({
|
|
2346
2687
|
name: column.name,
|
|
2347
|
-
...column.desc ? { sort: "desc" } : {}
|
|
2688
|
+
...column.desc ? { sort: "desc" } : {},
|
|
2689
|
+
...column.operatorClass === undefined ? {} : { operatorClass: column.operatorClass }
|
|
2348
2690
|
})),
|
|
2349
2691
|
...index.where === undefined ? {} : { where: index.where },
|
|
2350
2692
|
...index.nullsNotDistinct === true ? { nullsNotDistinct: true } : {}
|
|
@@ -2352,6 +2694,7 @@ function buildModel(params) {
|
|
|
2352
2694
|
}
|
|
2353
2695
|
return {
|
|
2354
2696
|
name: table,
|
|
2697
|
+
dbName: params.physicalTable,
|
|
2355
2698
|
fields,
|
|
2356
2699
|
relations: [],
|
|
2357
2700
|
indexes: modelIndexes,
|
|
@@ -2389,8 +2732,9 @@ function reverseMapType(params) {
|
|
|
2389
2732
|
const { column, enumNames } = params;
|
|
2390
2733
|
const isList = column.dataType === "ARRAY";
|
|
2391
2734
|
const udt = isList ? column.udtName.replace(/^_/, "") : column.udtName;
|
|
2392
|
-
|
|
2393
|
-
|
|
2735
|
+
const enumKey = physicalNameKey({ namespace: column.udtNamespace, name: udt });
|
|
2736
|
+
if (enumNames.has(enumKey)) {
|
|
2737
|
+
return { type: { enum: enumKey }, isList };
|
|
2394
2738
|
}
|
|
2395
2739
|
const canonical = CANONICAL_UDT[udt];
|
|
2396
2740
|
if (canonical !== undefined) {
|
|
@@ -2412,8 +2756,9 @@ function reverseMapType(params) {
|
|
|
2412
2756
|
function nonCanonicalModifier(params) {
|
|
2413
2757
|
const { column, udt, scalar } = params;
|
|
2414
2758
|
if (column.dataType === "ARRAY" && column.fullType !== null) {
|
|
2415
|
-
const
|
|
2416
|
-
|
|
2759
|
+
const temporal = parseUnsupportedPostgresType({ raw: column.fullType });
|
|
2760
|
+
const element = temporal?.name === "timestamptz" ? `timestamptz${temporal.modifier}` : normalizeNativeType({ raw: column.fullType }).replace(/\[\]$/, "");
|
|
2761
|
+
if (element !== "timestamptz(6)" && /^(numeric|timestamptz)\([\d,]+\)$/.test(element))
|
|
2417
2762
|
return element;
|
|
2418
2763
|
}
|
|
2419
2764
|
if (scalar === "DateTime" && udt === "timestamptz") {
|
|
@@ -2430,9 +2775,9 @@ function nonCanonicalModifier(params) {
|
|
|
2430
2775
|
}
|
|
2431
2776
|
function withModifier(params) {
|
|
2432
2777
|
const { column, udt } = params;
|
|
2433
|
-
if (udt === "varchar" && column.dataType === "ARRAY" && column.fullType !== null) {
|
|
2778
|
+
if ((udt === "varchar" || udt === "bpchar" || udt === "bit" || udt === "varbit") && column.dataType === "ARRAY" && column.fullType !== null) {
|
|
2434
2779
|
const elementType = normalizeNativeType({ raw: column.fullType }).replace(/\[\]$/, "");
|
|
2435
|
-
if (/^varchar\(\d+\)$/.test(elementType))
|
|
2780
|
+
if (/^(varchar|char|bit|varbit)\(\d+\)$/.test(elementType))
|
|
2436
2781
|
return elementType;
|
|
2437
2782
|
}
|
|
2438
2783
|
if ((udt === "varchar" || udt === "bpchar") && column.charMaxLength !== null) {
|
|
@@ -2442,6 +2787,10 @@ function withModifier(params) {
|
|
|
2442
2787
|
return "char";
|
|
2443
2788
|
if ((udt === "bit" || udt === "varbit") && column.charMaxLength !== null)
|
|
2444
2789
|
return `${udt}(${column.charMaxLength})`;
|
|
2790
|
+
if ((udt === "timestamp" || udt === "time" || udt === "timetz") && column.dataType === "ARRAY" && column.fullType !== null) {
|
|
2791
|
+
const element = parseUnsupportedPostgresType({ raw: column.fullType });
|
|
2792
|
+
return element === undefined || element.modifier === "" || element.modifier === "(6)" ? udt : `${udt}${element.modifier}`;
|
|
2793
|
+
}
|
|
2445
2794
|
if ((udt === "timestamp" || udt === "time" || udt === "timetz") && column.datetimePrecision !== null && column.datetimePrecision !== 6) {
|
|
2446
2795
|
return `${udt}(${column.datetimePrecision})`;
|
|
2447
2796
|
}
|
|
@@ -2596,7 +2945,12 @@ function toNumberOrNull(value) {
|
|
|
2596
2945
|
// packages/migrate/src/introspect/mysql.ts
|
|
2597
2946
|
import { exactIntegerLiteral as exactIntegerLiteral2 } from "@vibeorm/schema";
|
|
2598
2947
|
import { mysqlDialect as mysqlDialect2 } from "@vibeorm/sql";
|
|
2599
|
-
|
|
2948
|
+
|
|
2949
|
+
// packages/migrate/src/journal-table.ts
|
|
2950
|
+
var MIGRATIONS_TABLE_NAME = "_vibe_migrations";
|
|
2951
|
+
|
|
2952
|
+
// packages/migrate/src/introspect/mysql.ts
|
|
2953
|
+
var TABLES_QUERY = `
|
|
2600
2954
|
SELECT table_name, engine
|
|
2601
2955
|
FROM information_schema.tables
|
|
2602
2956
|
WHERE table_schema = DATABASE()
|
|
@@ -2604,13 +2958,13 @@ var TABLES_QUERY2 = `
|
|
|
2604
2958
|
AND table_name <> '${MIGRATIONS_TABLE_NAME}'
|
|
2605
2959
|
ORDER BY table_name;
|
|
2606
2960
|
`;
|
|
2607
|
-
var
|
|
2961
|
+
var VIEWS_QUERY = `
|
|
2608
2962
|
SELECT table_name
|
|
2609
2963
|
FROM information_schema.views
|
|
2610
2964
|
WHERE table_schema = DATABASE()
|
|
2611
2965
|
ORDER BY table_name;
|
|
2612
2966
|
`;
|
|
2613
|
-
var
|
|
2967
|
+
var COLUMNS_QUERY = `
|
|
2614
2968
|
SELECT
|
|
2615
2969
|
table_name,
|
|
2616
2970
|
column_name,
|
|
@@ -2641,7 +2995,7 @@ var STATISTICS_QUERY = `
|
|
|
2641
2995
|
WHERE table_schema = DATABASE()
|
|
2642
2996
|
ORDER BY table_name, index_name, seq_in_index;
|
|
2643
2997
|
`;
|
|
2644
|
-
var
|
|
2998
|
+
var FOREIGN_KEYS_QUERY = `
|
|
2645
2999
|
SELECT
|
|
2646
3000
|
constraint_name,
|
|
2647
3001
|
table_name,
|
|
@@ -2661,7 +3015,7 @@ var REFERENTIAL_QUERY = `
|
|
|
2661
3015
|
FROM information_schema.referential_constraints
|
|
2662
3016
|
WHERE constraint_schema = DATABASE();
|
|
2663
3017
|
`;
|
|
2664
|
-
var
|
|
3018
|
+
var CHECKS_QUERY = `
|
|
2665
3019
|
SELECT
|
|
2666
3020
|
tc.table_name AS table_name,
|
|
2667
3021
|
tc.constraint_name AS constraint_name,
|
|
@@ -2688,13 +3042,13 @@ function catalogField(params) {
|
|
|
2688
3042
|
async function introspectMysql(params) {
|
|
2689
3043
|
const { executor } = params;
|
|
2690
3044
|
const [tableRows, viewRows, columnRows, statisticRows, fkRows, refRows, checkRows] = await Promise.all([
|
|
2691
|
-
executor({ text:
|
|
2692
|
-
executor({ text:
|
|
2693
|
-
executor({ text:
|
|
3045
|
+
executor({ text: TABLES_QUERY }),
|
|
3046
|
+
executor({ text: VIEWS_QUERY }),
|
|
3047
|
+
executor({ text: COLUMNS_QUERY }),
|
|
2694
3048
|
executor({ text: STATISTICS_QUERY }),
|
|
2695
|
-
executor({ text:
|
|
3049
|
+
executor({ text: FOREIGN_KEYS_QUERY }),
|
|
2696
3050
|
executor({ text: REFERENTIAL_QUERY }),
|
|
2697
|
-
executor({ text:
|
|
3051
|
+
executor({ text: CHECKS_QUERY })
|
|
2698
3052
|
]);
|
|
2699
3053
|
const checksByTable = new Map;
|
|
2700
3054
|
for (const row of checkRows) {
|
|
@@ -3448,7 +3802,7 @@ function q(name) {
|
|
|
3448
3802
|
}
|
|
3449
3803
|
|
|
3450
3804
|
// packages/migrate/src/execute.ts
|
|
3451
|
-
import { VibeError as
|
|
3805
|
+
import { VibeError as VibeError14 } from "@vibeorm/schema";
|
|
3452
3806
|
var SELF_MANAGED_MARKER = "PRAGMA foreign_keys=OFF";
|
|
3453
3807
|
var FK_CHECK_PREFIX = "PRAGMA foreign_key_check";
|
|
3454
3808
|
function usesSelfManagedTransactions(params) {
|
|
@@ -3489,7 +3843,7 @@ function assertSelfManagedResumeSafe(params) {
|
|
|
3489
3843
|
const completedIndex = params.direction === "up" ? params.start + 1 : -(params.start + 2);
|
|
3490
3844
|
const rewind = selfManagedResumeStart({ sql: params.sql, start: params.start });
|
|
3491
3845
|
const retryIndex = params.direction === "up" ? rewind : -(rewind + 1);
|
|
3492
|
-
throw new
|
|
3846
|
+
throw new VibeError14({
|
|
3493
3847
|
code: "VIBE_MIGRATION",
|
|
3494
3848
|
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.`,
|
|
3495
3849
|
meta: { reason: "uncertain-statement", migration: params.migration, statementIndex: params.start, direction: params.direction, dialect: "sqlite" }
|
|
@@ -3518,7 +3872,7 @@ function unsupportedScript(params) {
|
|
|
3518
3872
|
const { migration, direction, index, problem, found } = params;
|
|
3519
3873
|
const subject = direction === "down" ? `the down script of migration "${migration}"` : `migration "${migration}"`;
|
|
3520
3874
|
const position = `${direction === "down" ? "down statement" : "statement"} ${index + 1} of ${params.statements}`;
|
|
3521
|
-
return new
|
|
3875
|
+
return new VibeError14({
|
|
3522
3876
|
code: "VIBE_MIGRATION",
|
|
3523
3877
|
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.',
|
|
3524
3878
|
meta: { reason: "unsupported-self-managed-script", problem, migration, statementIndex: index, direction }
|
|
@@ -3574,7 +3928,7 @@ async function executeStatement(params) {
|
|
|
3574
3928
|
const rows = await executor({ text: statement, values: [] });
|
|
3575
3929
|
if (rows.length > 0) {
|
|
3576
3930
|
const sample = rows.slice(0, 5).map((row) => `${String(row.table)} rowid ${String(row.rowid)} \u2192 ${String(row.parent)}`).join("; ");
|
|
3577
|
-
throw new
|
|
3931
|
+
throw new VibeError14({
|
|
3578
3932
|
code: "VIBE_MIGRATION",
|
|
3579
3933
|
message: `foreign key check failed after table rebuild: ${rows.length} violation(s) \u2014 ${sample}`,
|
|
3580
3934
|
meta: { violations: rows }
|
|
@@ -3593,9 +3947,44 @@ async function cleanupSelfManagedFailure(params) {
|
|
|
3593
3947
|
} catch {}
|
|
3594
3948
|
}
|
|
3595
3949
|
|
|
3950
|
+
// packages/migrate/src/operator-classes.ts
|
|
3951
|
+
import { defaultIndexName as defaultIndexName2, qualifiedSqlName as qualifiedSqlName7 } from "@vibeorm/schema";
|
|
3952
|
+
import { postgresDialect as postgresDialect7, renderPostgresIndexKeys as renderPostgresIndexKeys2 } from "@vibeorm/sql";
|
|
3953
|
+
async function prepareOperatorClassDiff(params) {
|
|
3954
|
+
const models = [];
|
|
3955
|
+
for (const live of params.from.models) {
|
|
3956
|
+
const table = qualifiedSqlName7({ name: live.dbName ?? live.name, namespace: live.namespace });
|
|
3957
|
+
const authored = params.to.models.find((entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace }) === physicalNameKey(table));
|
|
3958
|
+
if (authored === undefined || !authored.indexes.some((index) => index.fields.some((field) => field.operatorClass !== undefined))) {
|
|
3959
|
+
models.push(live);
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
const query = renderPostgresIndexKeys2({ dialect: postgresDialect7, table });
|
|
3963
|
+
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
3964
|
+
const indexes = live.indexes.map((index) => {
|
|
3965
|
+
const declared = authored.indexes.find((entry) => (entry.name ?? defaultIndexName2({ table: table.name, columns: entry.fields.map((field) => authored.fields.find((candidate) => candidate.name === field.name)?.dbName ?? field.name), unique: entry.kind === "unique", dialect: "postgres" })) === index.name && entry.kind === index.kind);
|
|
3966
|
+
if (declared === undefined)
|
|
3967
|
+
return index;
|
|
3968
|
+
return { ...index, fields: index.fields.map((field, position) => {
|
|
3969
|
+
const requested = declared.fields[position];
|
|
3970
|
+
const operatorClass = requested?.operatorClass;
|
|
3971
|
+
const physicalColumn = authored.fields.find((entry) => entry.name === requested?.name)?.dbName ?? requested?.name;
|
|
3972
|
+
if (field.operatorClass !== undefined || operatorClass === undefined || physicalColumn !== field.name)
|
|
3973
|
+
return field;
|
|
3974
|
+
const row = rows.find((entry) => entry.index_name === index.name && Number(entry.ordinal) === position + 1 && entry.column_name === field.name);
|
|
3975
|
+
if (row?.exact_default !== true || row.operator_namespace !== operatorClass.namespace || row.operator_name !== operatorClass.name || row.method !== (index.kind === "unique" ? "btree" : index.kind))
|
|
3976
|
+
return field;
|
|
3977
|
+
return { ...field, operatorClass };
|
|
3978
|
+
}) };
|
|
3979
|
+
});
|
|
3980
|
+
models.push({ ...live, indexes });
|
|
3981
|
+
}
|
|
3982
|
+
return { ...params.from, models };
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3596
3985
|
// packages/migrate/src/postgres-default-equivalence.ts
|
|
3597
|
-
import { VibeError as
|
|
3598
|
-
import { postgresDialect as
|
|
3986
|
+
import { VibeError as VibeError15 } from "@vibeorm/schema";
|
|
3987
|
+
import { postgresDialect as postgresDialect8, renderPostgresDefaultFunctionIdentity } from "@vibeorm/sql";
|
|
3599
3988
|
var IDENTIFIER = '(?:"(?:[^"\\u0000]|"")+"|[A-Za-z_][A-Za-z_0-9$]*)';
|
|
3600
3989
|
var CALL = new RegExp(`^\\s*(${IDENTIFIER})(?:\\s*\\.\\s*(${IDENTIFIER}))?\\s*\\(\\s*\\)\\s*$`);
|
|
3601
3990
|
function parseCall(params) {
|
|
@@ -3611,9 +4000,9 @@ async function equivalentPostgresDefaultFunction(params) {
|
|
|
3611
4000
|
const observed = parseCall({ expression: params.actual });
|
|
3612
4001
|
if (expected?.namespace === undefined || observed === undefined)
|
|
3613
4002
|
return false;
|
|
3614
|
-
const query = renderPostgresDefaultFunctionIdentity({ dialect:
|
|
4003
|
+
const query = renderPostgresDefaultFunctionIdentity({ dialect: postgresDialect8, table: params.table, column: params.column });
|
|
3615
4004
|
const rows = await params.executor({ text: query.text, values: [...query.values] }).catch((cause) => {
|
|
3616
|
-
throw new
|
|
4005
|
+
throw new VibeError15({ code: "VIBE_MIGRATION", message: "Cannot inspect the stored default function identity", meta: { reason: "default-inspection-unavailable", table: params.table, column: params.column }, cause });
|
|
3617
4006
|
});
|
|
3618
4007
|
if (rows.length !== 1)
|
|
3619
4008
|
return false;
|
|
@@ -3641,8 +4030,8 @@ function factsOf(params) {
|
|
|
3641
4030
|
}
|
|
3642
4031
|
async function inspectSqlObject(params) {
|
|
3643
4032
|
return Promise.resolve().then(async () => {
|
|
3644
|
-
const namespace =
|
|
3645
|
-
const query =
|
|
4033
|
+
const namespace = sqlObjectNamespace2({ identity: params.identity });
|
|
4034
|
+
const query = renderMigrationObjectQuery2({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: params.identity });
|
|
3646
4035
|
if (params.dialect === "mysql")
|
|
3647
4036
|
await assertCatalogVisibility({ executor: params.executor, dialect: params.dialect, namespace });
|
|
3648
4037
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
@@ -3666,7 +4055,7 @@ async function inspectSqlObject(params) {
|
|
|
3666
4055
|
const constraintType = params.identity.constraintType;
|
|
3667
4056
|
const constraint = sqliteTableConstraints2({ sql: String(facts.definition ?? "") }).find((entry) => entry.name === name && entry.type === constraintType);
|
|
3668
4057
|
if (constraint === undefined && constraintType === "primaryKey" && name === `${params.identity.table.name}_pkey`) {
|
|
3669
|
-
const query = renderSqliteColumnInventory({ dialect:
|
|
4058
|
+
const query = renderSqliteColumnInventory({ dialect: getSqlDialect2({ dialect: "sqlite" }), namespace: params.identity.table.namespace, table: params.identity.table.name });
|
|
3670
4059
|
const columns = (await params.executor({ text: query.text, values: [...query.values] })).filter((column) => Number(column.pk) > 0).sort((a, b) => Number(a.pk) - Number(b.pk));
|
|
3671
4060
|
if (columns.length === 0)
|
|
3672
4061
|
return { state: "missing" };
|
|
@@ -3689,12 +4078,12 @@ async function inspectSqlObject(params) {
|
|
|
3689
4078
|
if (params.identity.kind === "columnDefault")
|
|
3690
4079
|
facts = { definition: facts.definition ?? null };
|
|
3691
4080
|
if (params.dialect === "postgres") {
|
|
3692
|
-
const query = renderMigrationDependencies({ dialect:
|
|
4081
|
+
const query = renderMigrationDependencies({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: params.identity });
|
|
3693
4082
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
3694
4083
|
return postgresInspection({ identity: params.identity, facts, addresses: rows.map((row) => row.address) });
|
|
3695
4084
|
}
|
|
3696
4085
|
if (params.dialect === "mysql") {
|
|
3697
|
-
const query = renderMysqlObjectMembers({ dialect:
|
|
4086
|
+
const query = renderMysqlObjectMembers({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: params.identity });
|
|
3698
4087
|
if (query !== undefined) {
|
|
3699
4088
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
3700
4089
|
if (params.identity.kind === "function" && rows.length === 0)
|
|
@@ -3708,7 +4097,7 @@ async function inspectSqlObject(params) {
|
|
|
3708
4097
|
}
|
|
3709
4098
|
}
|
|
3710
4099
|
return { state: "present", definition: { identity: params.identity, facts, dependencies: [], format: "catalog-v2" } };
|
|
3711
|
-
}).catch((error) => ({ state: "unavailable", reason: error instanceof
|
|
4100
|
+
}).catch((error) => ({ state: "unavailable", reason: error instanceof VibeError16 ? String(error.meta.feature ?? error.code) : "catalog-query-failed" }));
|
|
3712
4101
|
}
|
|
3713
4102
|
function postgresInspection(params) {
|
|
3714
4103
|
if (!params.addresses.every((address) => typeof address === "string"))
|
|
@@ -3723,7 +4112,7 @@ function postgresInspection(params) {
|
|
|
3723
4112
|
return { state: "present", definition: { identity: params.identity, facts, dependencies, format: "catalog-v2" } };
|
|
3724
4113
|
}
|
|
3725
4114
|
function unavailableReason(params) {
|
|
3726
|
-
return params.error instanceof
|
|
4115
|
+
return params.error instanceof VibeError16 ? String(params.error.meta.feature ?? params.error.meta.reason ?? params.error.code) : "catalog-query-failed";
|
|
3727
4116
|
}
|
|
3728
4117
|
async function readBatch(params) {
|
|
3729
4118
|
const positions = new Set(params.batch.positions);
|
|
@@ -3731,7 +4120,7 @@ async function readBatch(params) {
|
|
|
3731
4120
|
const rows = new Map;
|
|
3732
4121
|
for (const row of await params.executor({ text: params.batch.text, values: [...params.batch.values] })) {
|
|
3733
4122
|
if (typeof row.position !== "number" || !Number.isInteger(row.position) || !positions.has(row.position))
|
|
3734
|
-
throw new
|
|
4123
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Invalid batched catalog identity", meta: { reason: "catalog-query-failed" } });
|
|
3735
4124
|
const matches = rows.get(row.position) ?? [];
|
|
3736
4125
|
matches.push(row);
|
|
3737
4126
|
rows.set(row.position, matches);
|
|
@@ -3759,7 +4148,7 @@ async function inspectSqlObjects(params) {
|
|
|
3759
4148
|
}
|
|
3760
4149
|
return Promise.resolve().then(async () => {
|
|
3761
4150
|
const results = params.identities.map(() => ({ state: "unavailable", reason: "catalog-query-failed" }));
|
|
3762
|
-
for (const batch of renderMigrationObjectBatch({ dialect:
|
|
4151
|
+
for (const batch of renderMigrationObjectBatch({ dialect: getSqlDialect2({ dialect: params.dialect }), identities: params.identities })) {
|
|
3763
4152
|
const outcome = await readBatch({ executor: params.executor, batch });
|
|
3764
4153
|
for (const position of batch.positions) {
|
|
3765
4154
|
results[position] = outcome.ok ? batchedInspection({ identity: params.identities[position], matches: outcome.rows.get(position) ?? [] }) : { state: "unavailable", reason: outcome.reason };
|
|
@@ -3771,12 +4160,12 @@ async function inspectSqlObjects(params) {
|
|
|
3771
4160
|
async function assertCatalogVisibility(params) {
|
|
3772
4161
|
if (params.dialect !== "mysql")
|
|
3773
4162
|
return;
|
|
3774
|
-
const query = renderMigrationVisibility({ dialect:
|
|
4163
|
+
const query = renderMigrationVisibility({ dialect: getSqlDialect2({ dialect: params.dialect }), namespace: params.namespace });
|
|
3775
4164
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
3776
4165
|
const grants = new Set(rows.map((row) => row.privilege));
|
|
3777
4166
|
const missing = ["SELECT", "TRIGGER", "SHOW VIEW", "EXECUTE"].filter((privilege) => !grants.has(privilege));
|
|
3778
4167
|
if (missing.length > 0)
|
|
3779
|
-
throw new
|
|
4168
|
+
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 } });
|
|
3780
4169
|
}
|
|
3781
4170
|
var CATALOG_V1_OMITTED_FACTS = Object.freeze({
|
|
3782
4171
|
function: ["definerOwner"],
|
|
@@ -3794,27 +4183,29 @@ function compare(params) {
|
|
|
3794
4183
|
if (expected === undefined !== (actual === undefined) || canonicalMigrationJson({ value: expected }) !== canonicalMigrationJson({ value: actual }))
|
|
3795
4184
|
differences.push({ attribute: key, expected, actual });
|
|
3796
4185
|
}
|
|
3797
|
-
const expected = params.expected.dependencies.map((identity) =>
|
|
3798
|
-
const actual = params.live.dependencies.map((identity) =>
|
|
4186
|
+
const expected = params.expected.dependencies.map((identity) => sqlObjectKey5({ identity })).sort();
|
|
4187
|
+
const actual = params.live.dependencies.map((identity) => sqlObjectKey5({ identity })).sort();
|
|
3799
4188
|
if (JSON.stringify(expected) !== JSON.stringify(actual))
|
|
3800
4189
|
differences.push({ attribute: "dependencies", expected, actual });
|
|
3801
4190
|
return differences;
|
|
3802
4191
|
}
|
|
3803
4192
|
function assertProbeMode(params) {
|
|
3804
4193
|
if (params.probe !== undefined && params.probe !== "none" && DIALECT_CAPABILITIES2[params.dialect].checkNormalization !== params.probe)
|
|
3805
|
-
throw new
|
|
4194
|
+
throw new VibeError16({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `${params.probe} CHECK probes are unavailable on ${params.dialect}`, meta: { feature: "checkProbe", dialect: params.dialect } });
|
|
3806
4195
|
}
|
|
3807
4196
|
async function verifyMigrationState(params) {
|
|
3808
4197
|
return verifyState(params);
|
|
3809
4198
|
}
|
|
3810
4199
|
async function verifyState(params) {
|
|
4200
|
+
if (params.external !== undefined)
|
|
4201
|
+
params = { ...params, expected: projectExternalState({ state: params.expected, external: params.external }) };
|
|
3811
4202
|
assertProbeMode({ dialect: params.dialect, probe: params.probe });
|
|
3812
4203
|
const declarations = params.expected.schema.migrationObjects ?? [];
|
|
3813
|
-
const owned = new Set(declarations.map((object) =>
|
|
3814
|
-
const definitions = new Map(params.expected.definitions.map((definition) => [
|
|
3815
|
-
const identities = new Map(params.expected.definitions.map((definition) => [
|
|
4204
|
+
const owned = new Set(declarations.map((object) => sqlObjectKey5({ identity: object.identity })));
|
|
4205
|
+
const definitions = new Map(params.expected.definitions.map((definition) => [sqlObjectKey5({ identity: definition.identity }), definition]));
|
|
4206
|
+
const identities = new Map(params.expected.definitions.map((definition) => [sqlObjectKey5({ identity: definition.identity }), definition.identity]));
|
|
3816
4207
|
for (const object of declarations)
|
|
3817
|
-
identities.set(
|
|
4208
|
+
identities.set(sqlObjectKey5({ identity: object.identity }), object.identity);
|
|
3818
4209
|
const objects = [];
|
|
3819
4210
|
const inspections = params.batch === true ? await inspectSqlObjects({ ...params, identities: [...identities.values()] }) : undefined;
|
|
3820
4211
|
let position = 0;
|
|
@@ -3825,6 +4216,8 @@ async function verifyState(params) {
|
|
|
3825
4216
|
position++;
|
|
3826
4217
|
if (live.state !== "present") {
|
|
3827
4218
|
objects.push({ identity, ownership, state: live.state, differences: [], ...live.state === "unavailable" ? { reason: live.reason } : {} });
|
|
4219
|
+
} else if (expected === undefined && identity.kind === "namespace") {
|
|
4220
|
+
objects.push({ identity, ownership, state: "matching", differences: [] });
|
|
3828
4221
|
} else if (expected === undefined) {
|
|
3829
4222
|
objects.push({ identity, ownership, state: "unavailable", reason: "independent-definition-required", differences: [] });
|
|
3830
4223
|
} else if (params.dialect === "postgres" && identity.kind === "table" && (expected.facts.persistence === undefined || expected.facts.options === undefined)) {
|
|
@@ -3834,23 +4227,31 @@ async function verifyState(params) {
|
|
|
3834
4227
|
objects.push({ identity, ownership, state: differences.length === 0 ? "matching" : "changed", differences });
|
|
3835
4228
|
}
|
|
3836
4229
|
}
|
|
3837
|
-
if (params.expected.schema.models.length > 0 || params.expected.schema.enums.length > 0)
|
|
4230
|
+
if (params.dialect === "postgres" && schemaNamespaces({ schema: params.expected.schema }).length > 0 || params.expected.schema.models.length > 0 || params.expected.schema.enums.length > 0)
|
|
3838
4231
|
objects.push(...await verifySchemaObjects(params));
|
|
3839
4232
|
if (params.dialect === "postgres")
|
|
3840
4233
|
objects.push(...await verifyExclusions(params));
|
|
3841
4234
|
return { inSync: objects.every((object) => object.state === "matching"), objects };
|
|
3842
4235
|
}
|
|
3843
4236
|
async function verifySchemaObjects(params) {
|
|
3844
|
-
const physical = params.dialect === "postgres" ? await introspectPhysicalPostgres({ executor: params.executor }) : params.dialect === "mysql" ? await introspectMysql({ executor: params.executor }) : params.sqliteRestorePending === true ? await introspectSqliteBeforeForeignKeyRestore({ executor: params.executor }) : await introspectSqlite({ executor: params.executor });
|
|
4237
|
+
const physical = params.dialect === "postgres" ? await introspectPhysicalPostgres({ executor: params.executor, namespaces: ["public", ...params.namespaces ?? [], ...schemaNamespaces({ schema: params.expected.schema })] }) : params.dialect === "mysql" ? await introspectMysql({ executor: params.executor }) : params.sqliteRestorePending === true ? await introspectSqliteBeforeForeignKeyRestore({ executor: params.executor }) : await introspectSqlite({ executor: params.executor });
|
|
3845
4238
|
const resolved = params.dialect === "postgres" ? await resolvePostgresTypeIdentities({ executor: params.executor, schema: params.expected.schema, mode: "verify" }) : { schema: params.expected.schema, unavailable: [] };
|
|
3846
4239
|
const expected = normalizeSchema({ schema: resolved.schema, dialect: params.dialect });
|
|
3847
|
-
const
|
|
4240
|
+
const physicalComparison = params.external === undefined ? physical : projectExternalSchema({ schema: physical, external: params.external });
|
|
4241
|
+
const comparable = params.dialect === "postgres" ? await prepareOperatorClassDiff({ executor: params.executor, from: physicalComparison, to: resolved.schema }) : physicalComparison;
|
|
4242
|
+
const actual = normalizeSchema({ schema: comparable, dialect: params.dialect });
|
|
3848
4243
|
const objects = resolved.unavailable.map((identity) => ({ identity, ownership: "schema", state: "unavailable", reason: "type-reference-required", differences: [] }));
|
|
3849
4244
|
const namespace = await migrationNamespace({ executor: params.executor, dialect: params.dialect });
|
|
3850
4245
|
const sqliteTableOptions = params.dialect === "sqlite" && expected.tables.size > 0 ? await params.executor({ text: "PRAGMA main.table_list;", values: [] }) : undefined;
|
|
3851
4246
|
if (params.dialect === "postgres")
|
|
3852
|
-
for (const
|
|
3853
|
-
const identity = { kind: "
|
|
4247
|
+
for (const name of schemaNamespaces({ schema: params.expected.schema })) {
|
|
4248
|
+
const identity = { kind: "namespace", name };
|
|
4249
|
+
const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
4250
|
+
objects.push({ identity, ownership: "schema", state: inspection.state === "present" ? "matching" : inspection.state, differences: [], ...inspection.state === "unavailable" ? { reason: inspection.reason } : {} });
|
|
4251
|
+
}
|
|
4252
|
+
if (params.dialect === "postgres")
|
|
4253
|
+
for (const desired of expected.enums.values()) {
|
|
4254
|
+
const identity = { kind: "enum", name: qualifiedSqlName8({ name: desired.dbName ?? desired.name, namespace: desired.namespace, defaultNamespace: namespace }) };
|
|
3854
4255
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
3855
4256
|
if (live.state !== "present") {
|
|
3856
4257
|
objects.push({ identity, ownership: "schema", state: live.state, differences: [], ...live.state === "unavailable" ? { reason: live.reason } : {} });
|
|
@@ -3864,9 +4265,10 @@ async function verifySchemaObjects(params) {
|
|
|
3864
4265
|
const matching = canonicalMigrationJson({ value: desired.values }) === canonicalMigrationJson({ value: values });
|
|
3865
4266
|
objects.push({ identity, ownership: "schema", state: matching ? "matching" : "changed", differences: matching ? [] : [{ attribute: "values", expected: desired.values, actual: values }] });
|
|
3866
4267
|
}
|
|
3867
|
-
for (const [
|
|
3868
|
-
const
|
|
3869
|
-
const
|
|
4268
|
+
for (const [key, desired] of expected.tables) {
|
|
4269
|
+
const name = desired.table;
|
|
4270
|
+
const table = qualifiedSqlName8({ name, namespace: desired.namespace, defaultNamespace: namespace });
|
|
4271
|
+
const live = actual.tables.get(key);
|
|
3870
4272
|
if (live === undefined) {
|
|
3871
4273
|
const presence = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity: { kind: "table", name: table } });
|
|
3872
4274
|
objects.push({ identity: { kind: "table", name: table }, ownership: "schema", state: presence.state === "missing" ? "missing" : "unavailable", ...presence.state === "missing" ? {} : { reason: "schema-definition-not-visible" }, differences: [] });
|
|
@@ -3887,12 +4289,13 @@ async function verifySchemaObjects(params) {
|
|
|
3887
4289
|
pin("primaryKey", desired.pk, live.pk);
|
|
3888
4290
|
for (const [column, field] of desired.columns) {
|
|
3889
4291
|
const stored = live.columns.get(column);
|
|
3890
|
-
|
|
4292
|
+
const exactType = field.pinsExactType;
|
|
4293
|
+
pin(`columns.${column}.type`, exactType ? field.exactTypeKey : field.typeKey, exactType ? stored?.exactTypeKey : stored?.typeKey);
|
|
3891
4294
|
pin(`columns.${column}.nullable`, field.nullable, stored?.nullable);
|
|
3892
4295
|
if (params.dialect === "mysql" && field.field.collation !== undefined) {
|
|
3893
4296
|
pin(`columns.${column}.collation`, field.field.collation.toLowerCase(), stored?.field.collation?.toLowerCase());
|
|
3894
4297
|
}
|
|
3895
|
-
const ownedDefault = params.expected.schema.migrationObjects?.some((object) => (object.identity.kind === "columnDefault" || object.identity.kind === "generatedColumn") && object.identity.table.namespace === namespace && object.identity.table.name === name && object.identity.column === column);
|
|
4298
|
+
const ownedDefault = params.expected.schema.migrationObjects?.some((object) => (object.identity.kind === "columnDefault" || object.identity.kind === "generatedColumn") && object.identity.table.namespace === table.namespace && object.identity.table.name === name && object.identity.column === column);
|
|
3896
4299
|
if (!ownedDefault) {
|
|
3897
4300
|
const desiredDefault = field.field.default;
|
|
3898
4301
|
const actualDefault = stored?.field.default;
|
|
@@ -3903,10 +4306,10 @@ async function verifySchemaObjects(params) {
|
|
|
3903
4306
|
}
|
|
3904
4307
|
for (const [key, fk] of desired.fks) {
|
|
3905
4308
|
const stored = live.fks.get(key);
|
|
3906
|
-
pin(`foreignKeys.${fk.constraintName}`, { columns: fk.columns, refTable: fk.refTable, refColumns: fk.refColumns, onDelete: fk.onDelete, onUpdate: fk.onUpdate }, stored === undefined ? undefined : { columns: stored.columns, refTable: stored.refTable, refColumns: stored.refColumns, onDelete: stored.onDelete, onUpdate: stored.onUpdate });
|
|
4309
|
+
pin(`foreignKeys.${fk.constraintName}`, { columns: fk.columns, refTable: fk.refTable, refNamespace: fk.refNamespace ?? namespace, refColumns: fk.refColumns, onDelete: fk.onDelete, onUpdate: fk.onUpdate }, stored === undefined ? undefined : { columns: stored.columns, refTable: stored.refTable, refNamespace: stored.refNamespace ?? namespace, refColumns: stored.refColumns, onDelete: stored.onDelete, onUpdate: stored.onUpdate });
|
|
3907
4310
|
if (params.dialect === "postgres" && stored !== undefined) {
|
|
3908
4311
|
const identity = { kind: "constraint", constraintType: "foreignKey", table, name: stored.constraintName };
|
|
3909
|
-
if (params.expected.definitions.some((definition) =>
|
|
4312
|
+
if (params.expected.definitions.some((definition) => sqlObjectKey5({ identity: definition.identity }) === sqlObjectKey5({ identity })))
|
|
3910
4313
|
continue;
|
|
3911
4314
|
const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
3912
4315
|
if (inspection.state !== "present") {
|
|
@@ -3922,7 +4325,7 @@ async function verifySchemaObjects(params) {
|
|
|
3922
4325
|
if (params.dialect !== "postgres" || !live.indexes.has(key))
|
|
3923
4326
|
continue;
|
|
3924
4327
|
const identity = { kind: "index", table, name: live.indexes.get(key).name };
|
|
3925
|
-
if (params.expected.definitions.some((definition) =>
|
|
4328
|
+
if (params.expected.definitions.some((definition) => sqlObjectKey5({ identity: definition.identity }) === sqlObjectKey5({ identity })))
|
|
3926
4329
|
continue;
|
|
3927
4330
|
const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
3928
4331
|
if (inspection.state !== "present") {
|
|
@@ -3935,7 +4338,7 @@ async function verifySchemaObjects(params) {
|
|
|
3935
4338
|
objects.push({ identity: { kind: "table", name: table }, ownership: "schema", state: differences.length === 0 ? "matching" : "changed", differences });
|
|
3936
4339
|
for (const [check, expression] of desired.checks) {
|
|
3937
4340
|
const identity = { kind: "constraint", constraintType: "check", table, name: check };
|
|
3938
|
-
if (params.expected.definitions.some((definition) =>
|
|
4341
|
+
if (params.expected.definitions.some((definition) => sqlObjectKey5({ identity: definition.identity }) === sqlObjectKey5({ identity })))
|
|
3939
4342
|
continue;
|
|
3940
4343
|
const stored = live.checks.get(check);
|
|
3941
4344
|
if (stored === undefined) {
|
|
@@ -3975,13 +4378,14 @@ async function verifySchemaObjects(params) {
|
|
|
3975
4378
|
}
|
|
3976
4379
|
for (const [name, join] of expected.joinTables) {
|
|
3977
4380
|
const stored = actual.joinTables.get(name);
|
|
3978
|
-
const shape = (value) => value === undefined ? undefined : { a: { table: value.a.table, column: value.a.column }, b: { table: value.b.table, column: value.b.column } };
|
|
4381
|
+
const shape = (value) => value === undefined ? undefined : { a: { table: value.a.table, namespace: value.a.namespace ?? namespace, column: value.a.column }, b: { table: value.b.table, namespace: value.b.namespace ?? namespace, column: value.b.column } };
|
|
3979
4382
|
const matching = canonicalMigrationJson({ value: shape(join) }) === canonicalMigrationJson({ value: shape(stored) });
|
|
3980
|
-
objects.push({ identity: { kind: "table", name: { namespace,
|
|
4383
|
+
objects.push({ identity: { kind: "table", name: qualifiedSqlName8({ name: join.table, namespace: join.namespace, defaultNamespace: namespace }) }, ownership: "schema", state: stored === undefined ? "missing" : matching ? "matching" : "changed", differences: matching ? [] : [{ attribute: "joinTable", expected: shape(join), actual: shape(stored) }] });
|
|
3981
4384
|
}
|
|
3982
|
-
for (const [
|
|
3983
|
-
|
|
3984
|
-
|
|
4385
|
+
for (const [key, view] of expected.views) {
|
|
4386
|
+
const name = qualifiedSqlName8({ name: view.dbName ?? view.name, namespace: view.namespace, defaultNamespace: namespace });
|
|
4387
|
+
if (!params.expected.definitions.some((definition) => definition.identity.kind === "view" && definition.identity.name.namespace === name.namespace && definition.identity.name.name === name.name))
|
|
4388
|
+
objects.push({ identity: { kind: "view", name }, ownership: "schema", state: actual.views.has(key) ? "unavailable" : "missing", reason: "independent-definition-required", differences: [] });
|
|
3985
4389
|
}
|
|
3986
4390
|
return objects;
|
|
3987
4391
|
}
|
|
@@ -3990,16 +4394,25 @@ async function verifyMigrationTransition(params) {
|
|
|
3990
4394
|
}
|
|
3991
4395
|
async function verifySqliteTransitionBeforeRestore(params) {
|
|
3992
4396
|
if (params.dialect !== "sqlite")
|
|
3993
|
-
throw new
|
|
4397
|
+
throw new VibeError16({ code: "VIBE_INTERNAL", message: "SQLite rebuild verification requires the SQLite dialect" });
|
|
3994
4398
|
await executeStatement({ executor: params.executor, dialect: "sqlite", statement: "PRAGMA foreign_key_check;" });
|
|
3995
4399
|
return verifyTransition({ ...params, sqliteRestorePending: true });
|
|
3996
4400
|
}
|
|
3997
4401
|
async function verifyTransition(params) {
|
|
3998
|
-
const
|
|
4402
|
+
const external = params.external;
|
|
4403
|
+
if (external !== undefined) {
|
|
4404
|
+
for (const identity of params.removals ?? []) {
|
|
4405
|
+
const owner = externalObjectOwner({ identity, external });
|
|
4406
|
+
if (owner !== undefined)
|
|
4407
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Transition removals cannot target externally owned objects", meta: { reason: "external-owned-removal", owner, identity } });
|
|
4408
|
+
}
|
|
4409
|
+
params = { ...params, from: projectExternalState({ state: params.from, external }), to: projectExternalState({ state: params.to, external }) };
|
|
4410
|
+
}
|
|
4411
|
+
const report = await verifyState({ executor: params.executor, dialect: params.dialect, expected: params.to, ...params.namespaces === undefined ? {} : { namespaces: params.namespaces }, ...external === undefined ? {} : { external }, ...params.sqliteRestorePending === true ? { sqliteRestorePending: true } : {}, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
3999
4412
|
const namespace = await migrationNamespace(params);
|
|
4000
4413
|
const identities = (state) => [...schemaObjectIdentities({ schema: state.schema, dialect: params.dialect, namespace }), ...(state.schema.migrationObjects ?? []).map((object) => object.identity), ...state.definitions.map((definition) => definition.identity)];
|
|
4001
|
-
const retained = new Set(identities(params.to).map((identity) =>
|
|
4002
|
-
const removed = new Map([...identities(params.from).filter((identity) => !retained.has(
|
|
4414
|
+
const retained = new Set(identities(params.to).map((identity) => sqlObjectKey5({ identity })));
|
|
4415
|
+
const removed = new Map([...identities(params.from).filter((identity) => identity.kind !== "namespace" && !retained.has(sqlObjectKey5({ identity }))), ...params.removals ?? []].map((identity) => [sqlObjectKey5({ identity }), identity]));
|
|
4003
4416
|
const objects = [...report.objects];
|
|
4004
4417
|
for (const identity of removed.values()) {
|
|
4005
4418
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
@@ -4012,16 +4425,16 @@ async function verifyTransition(params) {
|
|
|
4012
4425
|
async function migrationNamespace(params) {
|
|
4013
4426
|
if (params.dialect !== "mysql")
|
|
4014
4427
|
return params.dialect === "sqlite" ? "main" : "public";
|
|
4015
|
-
const query = renderMigrationNamespace({ dialect:
|
|
4428
|
+
const query = renderMigrationNamespace({ dialect: getSqlDialect2({ dialect: params.dialect }) });
|
|
4016
4429
|
const rows = await params.executor({ text: query.text, values: [] });
|
|
4017
4430
|
const namespace = rows[0]?.namespace;
|
|
4018
4431
|
if (typeof namespace !== "string" || namespace.length === 0)
|
|
4019
|
-
throw new
|
|
4432
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Cannot determine the migration namespace", meta: { reason: "inspection-unavailable" } });
|
|
4020
4433
|
return namespace;
|
|
4021
4434
|
}
|
|
4022
4435
|
function assertMigrationState(params) {
|
|
4023
4436
|
if (!params.report.inSync)
|
|
4024
|
-
throw new
|
|
4437
|
+
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") } });
|
|
4025
4438
|
}
|
|
4026
4439
|
async function deparsePostgresCheck(params) {
|
|
4027
4440
|
const definition = await deparsePostgresCheckDefinition({ ...params, constraint: "vibeorm_check_probe" });
|
|
@@ -4031,11 +4444,11 @@ async function deparsePostgresCheckDefinition(params) {
|
|
|
4031
4444
|
const name = "vibeorm_check_probe";
|
|
4032
4445
|
await params.executor({ text: "SAVEPOINT vibeorm_check_probe" });
|
|
4033
4446
|
try {
|
|
4034
|
-
const query = renderCheckProbe({ dialect:
|
|
4447
|
+
const query = renderCheckProbe({ dialect: getSqlDialect2({ dialect: "postgres" }), table: params.table, expression: params.expression, name });
|
|
4035
4448
|
await params.executor({ text: query.text });
|
|
4036
4449
|
const result = await inspectSqlObject({ executor: params.executor, dialect: "postgres", identity: { kind: "constraint", constraintType: "check", table: params.table, name } });
|
|
4037
4450
|
if (result.state !== "present" || typeof result.definition.facts.definition !== "string")
|
|
4038
|
-
throw new
|
|
4451
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "CHECK normalization probe unavailable", meta: { reason: "check-inspection-unavailable" } });
|
|
4039
4452
|
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 } };
|
|
4040
4453
|
} finally {
|
|
4041
4454
|
await params.executor({ text: "ROLLBACK TO SAVEPOINT vibeorm_check_probe" });
|
|
@@ -4044,7 +4457,7 @@ async function deparsePostgresCheckDefinition(params) {
|
|
|
4044
4457
|
}
|
|
4045
4458
|
async function deparseMysqlCheck(params) {
|
|
4046
4459
|
const probe = `vibe_check_${randomUUID2().replaceAll("-", "")}`;
|
|
4047
|
-
const dialect =
|
|
4460
|
+
const dialect = getSqlDialect2({ dialect: "mysql" });
|
|
4048
4461
|
const clone = renderMysqlCheckProbe({ dialect, probe, operation: "clone", table: params.table });
|
|
4049
4462
|
await params.executor({ text: clone.text });
|
|
4050
4463
|
let created = true;
|
|
@@ -4053,7 +4466,7 @@ async function deparseMysqlCheck(params) {
|
|
|
4053
4466
|
const source = await params.executor({ text: read.text, values: [] });
|
|
4054
4467
|
const definition = source[0]?.["Create Table"];
|
|
4055
4468
|
if (typeof definition !== "string")
|
|
4056
|
-
throw new
|
|
4469
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Cannot read temporary clone definition" });
|
|
4057
4470
|
const drop = renderMysqlCheckProbe({ dialect, probe, operation: "drop" });
|
|
4058
4471
|
await params.executor({ text: drop.text });
|
|
4059
4472
|
created = false;
|
|
@@ -4064,7 +4477,7 @@ async function deparseMysqlCheck(params) {
|
|
|
4064
4477
|
const normalizedDefinition = rows[0]?.["Create Table"];
|
|
4065
4478
|
const check = typeof normalizedDefinition === "string" ? parseSqliteNamedChecks({ tableSql: normalizedDefinition }).find((entry) => entry.name === probe) : undefined;
|
|
4066
4479
|
if (check === undefined)
|
|
4067
|
-
throw new
|
|
4480
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "MySQL temporary CHECK deparse unavailable", meta: { reason: "check-inspection-unavailable" } });
|
|
4068
4481
|
return check.expression;
|
|
4069
4482
|
} finally {
|
|
4070
4483
|
if (created) {
|
|
@@ -4078,7 +4491,7 @@ async function prepareCheckDiff(params) {
|
|
|
4078
4491
|
const models = [];
|
|
4079
4492
|
for (const model of params.from.models) {
|
|
4080
4493
|
const table = model.dbName ?? model.name;
|
|
4081
|
-
const target = params.to.models.find((entry) => (entry.dbName ?? entry.name) === table);
|
|
4494
|
+
const target = params.to.models.find((entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect: params.dialect }) === physicalNameKey({ name: table, namespace: model.namespace, dialect: params.dialect }));
|
|
4082
4495
|
const checks = [];
|
|
4083
4496
|
for (const check of model.checks ?? []) {
|
|
4084
4497
|
const next = target?.checks?.find((entry) => entry.name === check.name);
|
|
@@ -4086,12 +4499,12 @@ async function prepareCheckDiff(params) {
|
|
|
4086
4499
|
checks.push({ name: check.name, expression: check.expression });
|
|
4087
4500
|
continue;
|
|
4088
4501
|
}
|
|
4089
|
-
const identity = { kind: "constraint", constraintType: "check", table: { namespace: await migrationNamespace({ executor: params.executor, dialect: params.dialect })
|
|
4502
|
+
const identity = { kind: "constraint", constraintType: "check", table: qualifiedSqlName8({ name: table, namespace: model.namespace, defaultNamespace: await migrationNamespace({ executor: params.executor, dialect: params.dialect }) }), name: check.name };
|
|
4090
4503
|
if (params.dialect === "postgres" && params.probe === "rollback") {
|
|
4091
4504
|
const desired = await deparsePostgresCheck({ executor: params.executor, table: identity.table, expression: next.expression, probe: "rollback" });
|
|
4092
4505
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
4093
4506
|
if (live.state !== "present")
|
|
4094
|
-
throw new
|
|
4507
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Live CHECK inspection unavailable", meta: { reason: "check-inspection-unavailable", identity } });
|
|
4095
4508
|
if (live.definition.facts.definition === desired) {
|
|
4096
4509
|
checks.push(next);
|
|
4097
4510
|
continue;
|
|
@@ -4103,20 +4516,20 @@ async function prepareCheckDiff(params) {
|
|
|
4103
4516
|
const desired = await deparseMysqlCheck({ executor: params.executor, table: identity.table, expression: next.expression, probe: "temporary" });
|
|
4104
4517
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
4105
4518
|
if (live.state !== "present")
|
|
4106
|
-
throw new
|
|
4519
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Live CHECK inspection unavailable", meta: { reason: "check-inspection-unavailable", identity } });
|
|
4107
4520
|
checks.push(desired === live.definition.facts.definition ? next : { name: check.name, expression: check.expression });
|
|
4108
4521
|
continue;
|
|
4109
4522
|
}
|
|
4110
|
-
const previous = params.previous?.schema.models.find((entry) => (entry.dbName ?? entry.name) === table)?.checks?.find((entry) => entry.name === check.name);
|
|
4523
|
+
const previous = params.previous?.schema.models.find((entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect: params.dialect }) === physicalNameKey({ name: table, namespace: model.namespace, dialect: params.dialect }))?.checks?.find((entry) => entry.name === check.name);
|
|
4111
4524
|
if (previous !== undefined) {
|
|
4112
4525
|
checks.push(previous.expression === next.expression ? next : { name: check.name, expression: check.expression });
|
|
4113
4526
|
continue;
|
|
4114
4527
|
}
|
|
4115
|
-
if (params.replaceChecks?.some((entry) =>
|
|
4528
|
+
if (params.replaceChecks?.some((entry) => sqlObjectKey5({ identity: entry }) === sqlObjectKey5({ identity }))) {
|
|
4116
4529
|
checks.push({ name: check.name, expression: check.expression });
|
|
4117
4530
|
continue;
|
|
4118
4531
|
}
|
|
4119
|
-
throw new
|
|
4532
|
+
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 } });
|
|
4120
4533
|
}
|
|
4121
4534
|
models.push({ ...model, checks });
|
|
4122
4535
|
}
|
|
@@ -4126,7 +4539,7 @@ async function prepareCheckDiff(params) {
|
|
|
4126
4539
|
// packages/migrate/src/inventory.ts
|
|
4127
4540
|
async function inspectMigrationInventory(params) {
|
|
4128
4541
|
await assertCatalogVisibility(params);
|
|
4129
|
-
const query = renderMigrationInventory({ dialect:
|
|
4542
|
+
const query = renderMigrationInventory({ dialect: getSqlDialect3({ dialect: params.dialect }), namespace: params.namespace });
|
|
4130
4543
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
4131
4544
|
const objects = [];
|
|
4132
4545
|
const kinds = {
|
|
@@ -4143,14 +4556,18 @@ async function inspectMigrationInventory(params) {
|
|
|
4143
4556
|
for (const row of rows) {
|
|
4144
4557
|
const name = row.name;
|
|
4145
4558
|
if (typeof name !== "string" || typeof row.kind !== "string")
|
|
4146
|
-
throw new
|
|
4559
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "Malformed catalog identity", meta: { reason: "inventory-unavailable" } });
|
|
4147
4560
|
const qualified = { namespace: params.namespace, name };
|
|
4148
4561
|
const table = { namespace: params.namespace, name: String(row.parent ?? "") };
|
|
4149
4562
|
let identity;
|
|
4150
4563
|
switch (row.kind) {
|
|
4564
|
+
case "namespace":
|
|
4565
|
+
identity = { kind: "namespace", name };
|
|
4566
|
+
break;
|
|
4151
4567
|
case "table":
|
|
4152
4568
|
case "view":
|
|
4153
4569
|
case "sequence":
|
|
4570
|
+
case "enum":
|
|
4154
4571
|
identity = { kind: row.kind, name: qualified };
|
|
4155
4572
|
break;
|
|
4156
4573
|
case "trigger":
|
|
@@ -4165,27 +4582,27 @@ async function inspectMigrationInventory(params) {
|
|
|
4165
4582
|
case "constraint": {
|
|
4166
4583
|
const constraintType = kinds[String(row.subtype)];
|
|
4167
4584
|
if (constraintType === undefined)
|
|
4168
|
-
throw new
|
|
4585
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "Unknown constraint catalog kind", meta: { reason: "inventory-unavailable" } });
|
|
4169
4586
|
identity = { kind: "constraint", table, name, constraintType };
|
|
4170
4587
|
break;
|
|
4171
4588
|
}
|
|
4172
4589
|
case "function": {
|
|
4173
4590
|
let argumentTypes = JSON.parse(String(row.signature));
|
|
4174
4591
|
if (params.dialect === "mysql") {
|
|
4175
|
-
const query = renderMysqlObjectMembers2({ dialect:
|
|
4592
|
+
const query = renderMysqlObjectMembers2({ dialect: getSqlDialect3({ dialect: params.dialect }), identity: { kind: "function", name: qualified, argumentTypes: [] } });
|
|
4176
4593
|
const members = await params.executor({ text: query.text, values: [...query.values] });
|
|
4177
4594
|
if (members.length === 0)
|
|
4178
|
-
throw new
|
|
4595
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "MySQL function signature unavailable", meta: { reason: "inventory-unavailable", name } });
|
|
4179
4596
|
argumentTypes = members.filter((member) => member.mode !== null).map((member) => ({ namespace: "mysql", name: String(member.type) }));
|
|
4180
4597
|
}
|
|
4181
4598
|
const candidate = { kind: "function", name: qualified, argumentTypes };
|
|
4182
4599
|
if (!isSqlObjectIdentity2(candidate))
|
|
4183
|
-
throw new
|
|
4600
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "Invalid function signature in catalog", meta: { reason: "inventory-unavailable" } });
|
|
4184
4601
|
identity = candidate;
|
|
4185
4602
|
break;
|
|
4186
4603
|
}
|
|
4187
4604
|
default:
|
|
4188
|
-
throw new
|
|
4605
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: `Unknown catalog object kind ${row.kind}`, meta: { reason: "inventory-unavailable" } });
|
|
4189
4606
|
}
|
|
4190
4607
|
objects.push({ identity, ...typeof row.backing === "string" ? { backingConstraint: row.backing } : {} });
|
|
4191
4608
|
if (params.dialect === "sqlite" && row.kind === "table" && typeof row.definition === "string" && !/^\s*CREATE\s+VIRTUAL\s+TABLE\b/i.test(row.definition)) {
|
|
@@ -4201,11 +4618,11 @@ async function inspectMigrationInventory(params) {
|
|
|
4201
4618
|
objects.push({ identity: { kind: "constraint", constraintType: "check", table: qualified, name: check.name } });
|
|
4202
4619
|
if ([...row.definition.matchAll(/\bCHECK\s*\(/gi)].length > checks.length)
|
|
4203
4620
|
objects.push({ identity: { kind: "table", name: qualified }, inspectionUnavailable: "sqlite-unnamed-check-definition-unavailable" });
|
|
4204
|
-
const query = renderSqliteColumnInventory2({ dialect:
|
|
4621
|
+
const query = renderSqliteColumnInventory2({ dialect: getSqlDialect3({ dialect: params.dialect }), namespace: params.namespace, table: name });
|
|
4205
4622
|
const columns = await params.executor({ text: query.text, values: [...query.values] });
|
|
4206
4623
|
if (columns.some((column) => Number(column.pk) > 0) && !clauses.some((clause) => clause.type === "primaryKey"))
|
|
4207
4624
|
objects.push({ identity: { kind: "constraint", constraintType: "primaryKey", table: qualified, name: defaultPrimaryKeyName2({ table: qualified.name, dialect: "sqlite" }) } });
|
|
4208
|
-
const fkQuery = renderSqliteForeignKeyInventory({ dialect:
|
|
4625
|
+
const fkQuery = renderSqliteForeignKeyInventory({ dialect: getSqlDialect3({ dialect: params.dialect }), namespace: params.namespace, table: name });
|
|
4209
4626
|
const foreignKeys = await params.executor({ text: fkQuery.text, values: [...fkQuery.values] });
|
|
4210
4627
|
const groups = new Map;
|
|
4211
4628
|
for (const fk of foreignKeys)
|
|
@@ -4224,48 +4641,47 @@ async function inspectMigrationInventory(params) {
|
|
|
4224
4641
|
}
|
|
4225
4642
|
}
|
|
4226
4643
|
}
|
|
4227
|
-
return [...new Map(objects.map((object) => [
|
|
4644
|
+
return [...new Map(objects.map((object) => [sqlObjectKey6({ identity: object.identity }), object])).values()];
|
|
4228
4645
|
}
|
|
4229
4646
|
|
|
4230
4647
|
// packages/migrate/src/module-ownership.ts
|
|
4231
4648
|
function refusal(params) {
|
|
4232
|
-
return new
|
|
4649
|
+
return new VibeError18({ code: "VIBE_MIGRATION", message: "Module ownership or dependencies cannot be reconciled with the declared and live schema; no package SQL was authorized", meta: params });
|
|
4233
4650
|
}
|
|
4234
4651
|
function identities(params) {
|
|
4235
4652
|
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)];
|
|
4236
4653
|
}
|
|
4237
4654
|
function moduleNamespace(params) {
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
const first = params.origin.owned[0];
|
|
4241
|
-
if (first === undefined)
|
|
4655
|
+
const namespaces = [...new Set(params.origin.owned.map((identity) => sqlObjectNamespace3({ identity })))];
|
|
4656
|
+
if (namespaces.length > 1 || namespaces.length === 0 && params.origin.dialect === "mysql")
|
|
4242
4657
|
throw refusal({ reason: "module-namespace-required", moduleId: params.origin.moduleId });
|
|
4243
|
-
return "
|
|
4658
|
+
return namespaces[0] ?? (params.origin.dialect === "sqlite" ? "main" : "public");
|
|
4244
4659
|
}
|
|
4245
4660
|
function moduleOwnedObjects(params) {
|
|
4246
4661
|
const { desiredState } = params;
|
|
4662
|
+
if (params.namespace === undefined && desiredState.dialect === "mysql")
|
|
4663
|
+
throw refusal({ reason: "module-namespace-required" });
|
|
4247
4664
|
const namespace = params.namespace ?? (desiredState.dialect === "sqlite" ? "main" : "public");
|
|
4248
4665
|
const all = [desiredState.from, desiredState.to, ...desiredState.down === undefined ? [] : [desiredState.down]].flatMap((state) => identities({ state, dialect: desiredState.dialect, namespace }));
|
|
4249
|
-
return [...new Map(all.map((identity) => [
|
|
4666
|
+
return [...new Map(all.map((identity) => [sqlObjectKey7({ identity }), identity])).values()].sort((a, b) => sqlObjectKey7({ identity: a }).localeCompare(sqlObjectKey7({ identity: b })));
|
|
4250
4667
|
}
|
|
4251
4668
|
function assertModuleOwnership(params) {
|
|
4252
4669
|
const { origin } = params;
|
|
4253
4670
|
const state = origin.source.desiredState;
|
|
4254
4671
|
const namespace = moduleNamespace({ origin });
|
|
4255
4672
|
const expected = moduleOwnedObjects({ desiredState: state, namespace });
|
|
4256
|
-
const owned = new Set(origin.owned.map((identity) =>
|
|
4257
|
-
if (expected.length !== owned.size || expected.some((identity) => !owned.has(
|
|
4673
|
+
const owned = new Set(origin.owned.map((identity) => sqlObjectKey7({ identity })));
|
|
4674
|
+
if (expected.length !== owned.size || expected.some((identity) => !owned.has(sqlObjectKey7({ identity }))))
|
|
4258
4675
|
throw refusal({ reason: "module-ownership-invalid", moduleId: origin.moduleId });
|
|
4259
4676
|
for (const identity of expected) {
|
|
4260
|
-
|
|
4261
|
-
if (name?.namespace !== namespace || name.name === "_vibe_migrations")
|
|
4677
|
+
if (sqlObjectNamespace3({ identity }) !== namespace || ("table" in identity ? identity.table.name : typeof identity.name === "object" ? identity.name.name : undefined) === "_vibe_migrations")
|
|
4262
4678
|
throw refusal({ reason: "module-ownership-invalid", moduleId: origin.moduleId, identity });
|
|
4263
4679
|
}
|
|
4264
4680
|
for (const target of [state.from, state.to, ...state.down === undefined ? [] : [state.down]]) {
|
|
4265
4681
|
if (target.schema.rls !== undefined || target.schema.extensions.length > 0)
|
|
4266
4682
|
throw refusal({ reason: "module-schema-unsupported", moduleId: origin.moduleId });
|
|
4267
4683
|
for (const dependency of [...target.definitions.flatMap((definition) => definition.dependencies), ...(target.schema.migrationObjects ?? []).flatMap((object) => object.dependsOn), ...state.unmanagedRemovals ?? []]) {
|
|
4268
|
-
if (!owned.has(
|
|
4684
|
+
if (!owned.has(sqlObjectKey7({ identity: dependency })))
|
|
4269
4685
|
throw refusal({ reason: "module-external-dependency", moduleId: origin.moduleId, identity: dependency });
|
|
4270
4686
|
}
|
|
4271
4687
|
}
|
|
@@ -4273,28 +4689,29 @@ function assertModuleOwnership(params) {
|
|
|
4273
4689
|
return;
|
|
4274
4690
|
const local = params.desiredState;
|
|
4275
4691
|
const rest = (actual, source) => {
|
|
4276
|
-
const
|
|
4277
|
-
const
|
|
4278
|
-
const
|
|
4279
|
-
const
|
|
4692
|
+
const keyOf = (entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect: origin.dialect });
|
|
4693
|
+
const tables = new Set(source.schema.models.map(keyOf));
|
|
4694
|
+
const enums = new Set(source.schema.enums.map(keyOf));
|
|
4695
|
+
const selected = actual.schema.models.filter((model) => tables.has(keyOf(model)));
|
|
4696
|
+
const selectedEnums = actual.schema.enums.filter((entry) => enums.has(keyOf(entry)));
|
|
4280
4697
|
if (canonicalMigrationJson({ value: selected }) !== canonicalMigrationJson({ value: source.schema.models }) || canonicalMigrationJson({ value: selectedEnums }) !== canonicalMigrationJson({ value: source.schema.enums }))
|
|
4281
4698
|
throw refusal({ reason: "module-source-state-mismatch", moduleId: origin.moduleId });
|
|
4282
|
-
const selectedObjects = (actual.schema.migrationObjects ?? []).filter((object) => owned.has(
|
|
4283
|
-
const selectedDefinitions = actual.definitions.filter((definition) => owned.has(
|
|
4699
|
+
const selectedObjects = (actual.schema.migrationObjects ?? []).filter((object) => owned.has(sqlObjectKey7({ identity: object.identity })));
|
|
4700
|
+
const selectedDefinitions = actual.definitions.filter((definition) => owned.has(sqlObjectKey7({ identity: definition.identity })));
|
|
4284
4701
|
if (canonicalMigrationJson({ value: selectedObjects }) !== canonicalMigrationJson({ value: source.schema.migrationObjects ?? [] }) || canonicalMigrationJson({ value: selectedDefinitions }) !== canonicalMigrationJson({ value: source.definitions }))
|
|
4285
4702
|
throw refusal({ reason: "module-source-state-mismatch", moduleId: origin.moduleId });
|
|
4286
|
-
return { ...actual, schema: { ...actual.schema, models: actual.schema.models.filter((model) => !tables.has(model
|
|
4703
|
+
return { ...actual, schema: { ...actual.schema, models: actual.schema.models.filter((model) => !tables.has(keyOf(model))), enums: actual.schema.enums.filter((entry) => !enums.has(keyOf(entry))), migrationObjects: (actual.schema.migrationObjects ?? []).filter((object) => !owned.has(sqlObjectKey7({ identity: object.identity }))) }, definitions: actual.definitions.filter((definition) => !owned.has(sqlObjectKey7({ identity: definition.identity }))) };
|
|
4287
4704
|
};
|
|
4288
4705
|
const before = canonicalMigrationJson({ value: rest(local.from, state.from) });
|
|
4289
4706
|
if (before !== canonicalMigrationJson({ value: rest(local.to, state.to) }) || before !== canonicalMigrationJson({ value: rest(local.down ?? local.from, state.down ?? state.from) }))
|
|
4290
4707
|
throw refusal({ reason: "module-mutates-application-state", moduleId: origin.moduleId });
|
|
4291
4708
|
}
|
|
4292
4709
|
function dependencyUnavailable(params) {
|
|
4293
|
-
return new
|
|
4710
|
+
return new VibeError18({ 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 } });
|
|
4294
4711
|
}
|
|
4295
4712
|
async function assertCrossNamespaceModuleDependencies(params) {
|
|
4296
4713
|
const { executor, origin } = params;
|
|
4297
|
-
const dialect =
|
|
4714
|
+
const dialect = getSqlDialect4({ dialect: origin.dialect });
|
|
4298
4715
|
const query = renderModuleDependencyQuery({ dialect, operation: { kind: "foreignKeys", namespace: params.namespace, tables: [...params.tables] } });
|
|
4299
4716
|
if (query === undefined)
|
|
4300
4717
|
return;
|
|
@@ -4321,15 +4738,15 @@ async function assertCrossNamespaceModuleDependencies(params) {
|
|
|
4321
4738
|
async function assertLiveModuleOwnership(params) {
|
|
4322
4739
|
const { executor, origin } = params;
|
|
4323
4740
|
const namespace = moduleNamespace({ origin });
|
|
4324
|
-
if (await migrationNamespace({ executor, dialect: origin.dialect }) !== namespace)
|
|
4741
|
+
if (origin.dialect !== "postgres" && await migrationNamespace({ executor, dialect: origin.dialect }) !== namespace)
|
|
4325
4742
|
throw refusal({ reason: "module-placement-mismatch", moduleId: origin.moduleId });
|
|
4326
4743
|
const inventory = await inspectMigrationInventory({ executor, dialect: origin.dialect, namespace });
|
|
4327
|
-
const before = new Set(identities({ state: origin.source.desiredState.from, dialect: origin.dialect, namespace }).map((identity) =>
|
|
4328
|
-
const owned = new Set(origin.owned.map((identity) =>
|
|
4744
|
+
const before = new Set(identities({ state: origin.source.desiredState.from, dialect: origin.dialect, namespace }).map((identity) => sqlObjectKey7({ identity })));
|
|
4745
|
+
const owned = new Set(origin.owned.map((identity) => sqlObjectKey7({ identity })));
|
|
4329
4746
|
const tables = new Set(origin.owned.flatMap((identity) => identity.kind === "table" ? [identity.name.name] : []));
|
|
4330
4747
|
await assertCrossNamespaceModuleDependencies({ executor, origin, namespace, tables });
|
|
4331
4748
|
for (const object of inventory) {
|
|
4332
|
-
const key =
|
|
4749
|
+
const key = sqlObjectKey7({ identity: object.identity });
|
|
4333
4750
|
if (params.resume !== true && owned.has(key) && !before.has(key))
|
|
4334
4751
|
throw refusal({ reason: "module-object-collision", moduleId: origin.moduleId, identity: object.identity });
|
|
4335
4752
|
if (!owned.has(key) && (["function", "view", "trigger"].includes(object.identity.kind) || ("table" in object.identity) && tables.has(object.identity.table.name) && object.backingConstraint === undefined))
|
|
@@ -4337,7 +4754,7 @@ async function assertLiveModuleOwnership(params) {
|
|
|
4337
4754
|
if (object.inspectionUnavailable !== undefined && (owned.has(key) || ("table" in object.identity) && tables.has(object.identity.table.name)))
|
|
4338
4755
|
throw refusal({ reason: "module-dependency-unavailable", moduleId: origin.moduleId, identity: object.identity });
|
|
4339
4756
|
}
|
|
4340
|
-
const physical = origin.dialect === "postgres" ? await introspectPhysicalPostgres({ executor }) : origin.dialect === "mysql" ? await introspectMysql({ executor }) : params.sqliteRestorePending === true ? await introspectSqliteBeforeForeignKeyRestore({ executor }) : await introspectSqlite({ executor });
|
|
4757
|
+
const physical = origin.dialect === "postgres" ? await introspectPhysicalPostgres({ executor, namespaces: [namespace] }) : origin.dialect === "mysql" ? await introspectMysql({ executor }) : params.sqliteRestorePending === true ? await introspectSqliteBeforeForeignKeyRestore({ executor }) : await introspectSqlite({ executor });
|
|
4341
4758
|
for (const model of physical.models)
|
|
4342
4759
|
for (const relation of model.relations) {
|
|
4343
4760
|
const target = physical.models.find((candidate) => candidate.name === relation.targetModel);
|
|
@@ -4352,12 +4769,51 @@ async function assertApplicationModuleState(params) {
|
|
|
4352
4769
|
const options = { executor: params.executor, dialect: origin.dialect, from: state.from, to: state.to, removals: state.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } };
|
|
4353
4770
|
const report = params.sqliteRestorePending === true ? await verifySqliteTransitionBeforeRestore(options) : await verifyMigrationTransition(options);
|
|
4354
4771
|
if (!report.inSync)
|
|
4355
|
-
throw new
|
|
4772
|
+
throw new VibeError18({ 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 } });
|
|
4356
4773
|
}
|
|
4357
4774
|
}
|
|
4358
4775
|
|
|
4776
|
+
// packages/migrate/src/module-adoption.ts
|
|
4777
|
+
import { VibeError as VibeError19 } from "@vibeorm/schema";
|
|
4778
|
+
var MODULE_ADOPTION_METADATA = Object.freeze({ filename: "module-adoption.json", version: 1 });
|
|
4779
|
+
function invalid3() {
|
|
4780
|
+
return new VibeError19({ 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" } });
|
|
4781
|
+
}
|
|
4782
|
+
function assertMarker(params) {
|
|
4783
|
+
const value = params.value;
|
|
4784
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== 3 || !("version" in value) || value.version !== 1 || !("reverse" in value) || value.reverse !== "refuse" || !("referenceFingerprint" in value) || typeof value.referenceFingerprint !== "string" || !/^[a-f0-9]{64}$/.test(value.referenceFingerprint))
|
|
4785
|
+
throw invalid3();
|
|
4786
|
+
}
|
|
4787
|
+
function assertModuleAdoption(params) {
|
|
4788
|
+
if (params.migration.moduleAdoption === undefined)
|
|
4789
|
+
return;
|
|
4790
|
+
assertMarker({ value: params.migration.moduleAdoption });
|
|
4791
|
+
if (params.migration.moduleOrigin === undefined)
|
|
4792
|
+
throw invalid3();
|
|
4793
|
+
assertModuleOrigin({ value: params.migration.moduleOrigin });
|
|
4794
|
+
}
|
|
4795
|
+
function parseModuleAdoption(params) {
|
|
4796
|
+
return parseMigrationMetadata({ text: params.text, failure: invalid3, parse: (value) => {
|
|
4797
|
+
const checked = { value };
|
|
4798
|
+
assertMarker(checked);
|
|
4799
|
+
return Object.freeze(checked.value);
|
|
4800
|
+
} });
|
|
4801
|
+
}
|
|
4802
|
+
function moduleAdoptionChecksumSuffix(params) {
|
|
4803
|
+
if (params.moduleAdoption === undefined)
|
|
4804
|
+
return "";
|
|
4805
|
+
assertModuleAdoption({ migration: params });
|
|
4806
|
+
return `
|
|
4807
|
+
-- vibeorm:module-adoption ${canonicalMigrationJson({ value: params.moduleAdoption })}`;
|
|
4808
|
+
}
|
|
4809
|
+
function serializeModuleAdoption(params) {
|
|
4810
|
+
assertModuleAdoption(params);
|
|
4811
|
+
return params.migration.moduleAdoption === undefined ? undefined : JSON.stringify(params.migration.moduleAdoption, null, 2) + `
|
|
4812
|
+
`;
|
|
4813
|
+
}
|
|
4814
|
+
|
|
4359
4815
|
// packages/migrate/src/sql-statements.ts
|
|
4360
|
-
import { VibeError as
|
|
4816
|
+
import { VibeError as VibeError20 } from "@vibeorm/schema";
|
|
4361
4817
|
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;
|
|
4362
4818
|
var BLOCK_OPENERS = new Set(["BEGIN", "CASE", "IF", "LOOP", "WHILE", "REPEAT"]);
|
|
4363
4819
|
function splitSqlStatements(params) {
|
|
@@ -4433,7 +4889,7 @@ function splitSqlStatements(params) {
|
|
|
4433
4889
|
continue;
|
|
4434
4890
|
}
|
|
4435
4891
|
if (dialect === "mysql" || dialect === undefined) {
|
|
4436
|
-
throw new
|
|
4892
|
+
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 } });
|
|
4437
4893
|
}
|
|
4438
4894
|
}
|
|
4439
4895
|
if (value === endQuote) {
|
|
@@ -4507,15 +4963,15 @@ function joinSqlStatements(params) {
|
|
|
4507
4963
|
|
|
4508
4964
|
// packages/migrate/src/module-origin.ts
|
|
4509
4965
|
import { createHash } from "crypto";
|
|
4510
|
-
import { VibeError as
|
|
4966
|
+
import { VibeError as VibeError21, isSqlObjectIdentity as isSqlObjectIdentity3, sqlObjectKey as sqlObjectKey8 } from "@vibeorm/schema";
|
|
4511
4967
|
var MODULE_ORIGIN_METADATA = Object.freeze({ filename: "module-origin.json", version: 1 });
|
|
4512
|
-
function
|
|
4513
|
-
return new
|
|
4968
|
+
function invalid4() {
|
|
4969
|
+
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" } });
|
|
4514
4970
|
}
|
|
4515
|
-
function
|
|
4971
|
+
function record3(value) {
|
|
4516
4972
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4517
4973
|
}
|
|
4518
|
-
function
|
|
4974
|
+
function identifier2(value) {
|
|
4519
4975
|
return typeof value === "string" && value.trim().length > 0 && !value.includes("\x00");
|
|
4520
4976
|
}
|
|
4521
4977
|
function strings(value) {
|
|
@@ -4533,17 +4989,17 @@ function freezeData(params) {
|
|
|
4533
4989
|
}
|
|
4534
4990
|
function assertModuleOrigin(params) {
|
|
4535
4991
|
const value = params.value;
|
|
4536
|
-
if (!
|
|
4537
|
-
throw
|
|
4538
|
-
if (new Set(value.owned.map((identity) =>
|
|
4539
|
-
throw
|
|
4992
|
+
if (!record3(value) || value.version !== 1 || !identifier2(value.moduleId) || !identifier2(value.revision) || !(value.previousRevision === null || identifier2(value.previousRevision)) || value.previousRevision === value.revision || !["postgres", "sqlite", "mysql"].includes(String(value.dialect)) || !strings(value.contracts) || value.contracts.length === 0 || !value.contracts.every(identifier2) || new Set(value.contracts).size !== value.contracts.length || !Array.isArray(value.owned) || !value.owned.every(isSqlObjectIdentity3) || !record3(value.source) || "moduleOrigin" in value.source || "moduleAdoption" in value.source || !identifier2(value.source.name) || !strings(value.source.sql) || typeof value.source.destructive !== "boolean" || value.source.downSql !== undefined && !strings(value.source.downSql) || value.source.desiredState === undefined || value.source.desiredStatePending !== undefined || typeof value.artifactChecksum !== "string" || !/^[a-f0-9]{64}$/.test(value.artifactChecksum))
|
|
4993
|
+
throw invalid4();
|
|
4994
|
+
if (new Set(value.owned.map((identity) => sqlObjectKey8({ identity }))).size !== value.owned.length)
|
|
4995
|
+
throw invalid4();
|
|
4540
4996
|
const desired = { value: value.source.desiredState, migration: value.source.name };
|
|
4541
4997
|
assertDesiredState(desired);
|
|
4542
4998
|
if (desired.value.dialect !== value.dialect)
|
|
4543
|
-
throw
|
|
4999
|
+
throw invalid4();
|
|
4544
5000
|
const { artifactChecksum, ...payload } = value;
|
|
4545
5001
|
if (checksum({ value: payload }) !== artifactChecksum)
|
|
4546
|
-
throw
|
|
5002
|
+
throw invalid4();
|
|
4547
5003
|
assertModuleOwnership({ origin: value });
|
|
4548
5004
|
}
|
|
4549
5005
|
function createModuleOrigin(params) {
|
|
@@ -4566,7 +5022,7 @@ function parseModuleOrigin(params) {
|
|
|
4566
5022
|
const checked = { value };
|
|
4567
5023
|
assertModuleOrigin(checked);
|
|
4568
5024
|
return checked.value;
|
|
4569
|
-
}, failure:
|
|
5025
|
+
}, failure: invalid4 });
|
|
4570
5026
|
}
|
|
4571
5027
|
function assertMaterializedModuleMigration(params) {
|
|
4572
5028
|
const { migration } = params;
|
|
@@ -4574,7 +5030,7 @@ function assertMaterializedModuleMigration(params) {
|
|
|
4574
5030
|
return;
|
|
4575
5031
|
const restored = restoreModuleMigration({ migration, origin: migration.moduleOrigin });
|
|
4576
5032
|
if (migration.desiredState === undefined || canonicalMigrationJson({ value: restored }) !== canonicalMigrationJson({ value: migration }))
|
|
4577
|
-
throw new
|
|
5033
|
+
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 } });
|
|
4578
5034
|
}
|
|
4579
5035
|
function restoreModuleMigration(params) {
|
|
4580
5036
|
const { migration, origin } = params;
|
|
@@ -4585,25 +5041,27 @@ function restoreModuleMigration(params) {
|
|
|
4585
5041
|
continue;
|
|
4586
5042
|
const normalized = (sql) => sql.flatMap((text) => splitSqlStatements({ sql: text, dialect: origin.dialect }));
|
|
4587
5043
|
if (canonicalMigrationJson({ value: normalized(actual) }) !== canonicalMigrationJson({ value: normalized(origin.source[key] ?? []) }))
|
|
4588
|
-
throw
|
|
5044
|
+
throw invalid4();
|
|
4589
5045
|
}
|
|
4590
5046
|
for (const key of ["execution", "downExecution", "rls", "securityChanges", "downSecurityChanges"]) {
|
|
4591
5047
|
if (migration[key] !== undefined && canonicalMigrationJson({ value: migration[key] }) !== canonicalMigrationJson({ value: origin.source[key] }))
|
|
4592
|
-
throw
|
|
5048
|
+
throw invalid4();
|
|
4593
5049
|
}
|
|
4594
|
-
|
|
5050
|
+
const restored = { ...origin.source, name: migration.name, ...migration.desiredState === undefined ? {} : { desiredState: migration.desiredState }, moduleOrigin: origin, ...migration.moduleAdoption === undefined ? {} : { moduleAdoption: migration.moduleAdoption } };
|
|
5051
|
+
assertModuleAdoption({ migration: restored });
|
|
5052
|
+
return restored;
|
|
4595
5053
|
}
|
|
4596
5054
|
|
|
4597
5055
|
// packages/migrate/src/fs.ts
|
|
4598
5056
|
import { readFile, readdir } from "fs/promises";
|
|
4599
5057
|
import { join } from "path";
|
|
4600
|
-
import { VibeError as
|
|
5058
|
+
import { VibeError as VibeError36, assertValidSchemaIR as assertValidSchemaIR4 } from "@vibeorm/schema";
|
|
4601
5059
|
|
|
4602
5060
|
// packages/migrate/src/migration-checksum.ts
|
|
4603
5061
|
import { createHash as createHash2 } from "crypto";
|
|
4604
5062
|
|
|
4605
5063
|
// packages/migrate/src/execution.ts
|
|
4606
|
-
import { VibeError as
|
|
5064
|
+
import { VibeError as VibeError22 } from "@vibeorm/schema";
|
|
4607
5065
|
var EXECUTION_SIDECAR_FILENAME = "execution.json";
|
|
4608
5066
|
var EXECUTION_SIDECAR_VERSION = 1;
|
|
4609
5067
|
function isDefaultExecution(params) {
|
|
@@ -4641,8 +5099,8 @@ function executionChecksumSuffix(params) {
|
|
|
4641
5099
|
${lines.join(`
|
|
4642
5100
|
`)}`;
|
|
4643
5101
|
}
|
|
4644
|
-
function
|
|
4645
|
-
return new
|
|
5102
|
+
function invalid5(params) {
|
|
5103
|
+
return new VibeError22({
|
|
4646
5104
|
code: "VIBE_MIGRATION",
|
|
4647
5105
|
message: `invalid execution metadata for migration "${params.migration}": ${params.detail}`,
|
|
4648
5106
|
meta: { reason: "execution-metadata-invalid", migration: params.migration }
|
|
@@ -4653,22 +5111,22 @@ function assertValidExecution(params) {
|
|
|
4653
5111
|
if (execution === undefined)
|
|
4654
5112
|
return;
|
|
4655
5113
|
if (execution.transaction !== "default" && execution.transaction !== "none") {
|
|
4656
|
-
throw
|
|
5114
|
+
throw invalid5({ migration, detail: `unknown transaction mode ${JSON.stringify(execution.transaction)}` });
|
|
4657
5115
|
}
|
|
4658
5116
|
for (const [label, value] of [["lockTimeoutMs", execution.lockTimeoutMs], ["statementTimeoutMs", execution.statementTimeoutMs]]) {
|
|
4659
5117
|
if (value === undefined)
|
|
4660
5118
|
continue;
|
|
4661
5119
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
4662
|
-
throw
|
|
5120
|
+
throw invalid5({ migration, detail: `${label} must be a non-negative integer number of milliseconds` });
|
|
4663
5121
|
}
|
|
4664
5122
|
}
|
|
4665
5123
|
const seen = new Set;
|
|
4666
5124
|
for (const binding of execution.operations ?? []) {
|
|
4667
5125
|
if (!Number.isSafeInteger(binding.statement) || binding.statement < 0 || binding.statement >= statements) {
|
|
4668
|
-
throw
|
|
5126
|
+
throw invalid5({ migration, detail: `operation refers to statement ${String(binding.statement)}, which is outside the migration's ${String(statements)} statement(s)` });
|
|
4669
5127
|
}
|
|
4670
5128
|
if (seen.has(binding.statement)) {
|
|
4671
|
-
throw
|
|
5129
|
+
throw invalid5({ migration, detail: `statement ${String(binding.statement)} carries more than one declared operation` });
|
|
4672
5130
|
}
|
|
4673
5131
|
seen.add(binding.statement);
|
|
4674
5132
|
}
|
|
@@ -4693,7 +5151,7 @@ function parseExecutionSidecar(params) {
|
|
|
4693
5151
|
try {
|
|
4694
5152
|
parsed = JSON.parse(params.text);
|
|
4695
5153
|
} catch (error) {
|
|
4696
|
-
throw new
|
|
5154
|
+
throw new VibeError22({
|
|
4697
5155
|
code: "VIBE_MIGRATION",
|
|
4698
5156
|
message: `invalid execution metadata for migration "${migration}": not valid JSON`,
|
|
4699
5157
|
meta: { reason: "execution-metadata-invalid", migration },
|
|
@@ -4701,10 +5159,10 @@ function parseExecutionSidecar(params) {
|
|
|
4701
5159
|
});
|
|
4702
5160
|
}
|
|
4703
5161
|
if (parsed === null || typeof parsed !== "object")
|
|
4704
|
-
throw
|
|
5162
|
+
throw invalid5({ migration, detail: "expected a JSON object" });
|
|
4705
5163
|
const body = parsed;
|
|
4706
5164
|
if (body.version !== EXECUTION_SIDECAR_VERSION) {
|
|
4707
|
-
throw
|
|
5165
|
+
throw invalid5({ migration, detail: `unsupported sidecar version ${String(body.version)} (this build reads version ${String(EXECUTION_SIDECAR_VERSION)})` });
|
|
4708
5166
|
}
|
|
4709
5167
|
const up = body.up === undefined ? undefined : body.up;
|
|
4710
5168
|
const down = body.down === undefined ? undefined : body.down;
|
|
@@ -4786,22 +5244,25 @@ function describeMigrationRisks(params) {
|
|
|
4786
5244
|
function migrationChecksum(params) {
|
|
4787
5245
|
const suffix = executionChecksumSuffix({ ...params.execution === undefined ? {} : { execution: params.execution } });
|
|
4788
5246
|
return createHash2("sha256").update(params.sql.join(`
|
|
4789
|
-
`) + suffix + moduleOriginChecksumSuffix(params) + (params.desiredStatePending === true ? `
|
|
5247
|
+
`) + suffix + moduleOriginChecksumSuffix(params) + moduleAdoptionChecksumSuffix(params) + (params.desiredStatePending === true ? `
|
|
4790
5248
|
-- vibeorm:desired-state unfinalized-v1` : "") + desiredStateChecksumSuffix({ ...params.desiredState === undefined ? {} : { desiredState: params.desiredState } }) + (params.desiredState === undefined ? "" : `
|
|
4791
5249
|
-- vibeorm:down ${JSON.stringify(params.downSql ?? null)}${executionChecksumSuffix({ ...params.downExecution === undefined ? {} : { execution: params.downExecution } })}`)).digest("hex");
|
|
4792
5250
|
}
|
|
4793
5251
|
|
|
4794
5252
|
// packages/migrate/src/shadow-guard.ts
|
|
4795
|
-
import { VibeError as
|
|
4796
|
-
import { getSqlDialect as
|
|
5253
|
+
import { VibeError as VibeError23 } from "@vibeorm/schema";
|
|
5254
|
+
import { getSqlDialect as getSqlDialect5, renderShadowOperation } from "@vibeorm/sql";
|
|
4797
5255
|
var replayExecutors = new WeakSet;
|
|
5256
|
+
function isShadowReplayExecutor(params) {
|
|
5257
|
+
return replayExecutors.has(params.executor);
|
|
5258
|
+
}
|
|
4798
5259
|
async function assertDatabaseNotShadow(params) {
|
|
4799
5260
|
if (params.dialect === "sqlite" || replayExecutors.has(params.executor))
|
|
4800
5261
|
return;
|
|
4801
|
-
const [query] = renderShadowOperation({ dialect:
|
|
5262
|
+
const [query] = renderShadowOperation({ dialect: getSqlDialect5({ dialect: params.dialect }), operation: { kind: "markerExists" } });
|
|
4802
5263
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
4803
5264
|
if (rows.length !== 0)
|
|
4804
|
-
throw new
|
|
5265
|
+
throw new VibeError23({ 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" } });
|
|
4805
5266
|
}
|
|
4806
5267
|
function runShadowReplay(params) {
|
|
4807
5268
|
replayExecutors.add(params.executor);
|
|
@@ -4810,10 +5271,95 @@ function runShadowReplay(params) {
|
|
|
4810
5271
|
});
|
|
4811
5272
|
}
|
|
4812
5273
|
|
|
5274
|
+
// packages/migrate/src/external-dependencies.ts
|
|
5275
|
+
import { VibeError as VibeError24, unsupportedCapability as unsupportedCapability8 } from "@vibeorm/schema";
|
|
5276
|
+
import { getSqlDialect as getSqlDialect6, renderExternalDependents } from "@vibeorm/sql";
|
|
5277
|
+
function externalDependencyTargets(params) {
|
|
5278
|
+
return params.steps.flatMap((step) => {
|
|
5279
|
+
const namespace = step.namespace ?? params.namespace;
|
|
5280
|
+
switch (step.kind) {
|
|
5281
|
+
case "dropEnum":
|
|
5282
|
+
return [{ kind: "type", namespace, name: step.enumName, dependency: { kind: "enum", name: { namespace, name: step.enumName } } }];
|
|
5283
|
+
case "dropTable":
|
|
5284
|
+
case "dropJoinTable":
|
|
5285
|
+
return [{ kind: "relation", namespace, name: step.table, dependency: { kind: "table", name: { namespace, name: step.table } } }];
|
|
5286
|
+
case "dropView":
|
|
5287
|
+
return [{ kind: "relation", namespace, name: step.table, dependency: { kind: "view", name: { namespace, name: step.table } } }];
|
|
5288
|
+
case "dropColumn":
|
|
5289
|
+
return [{ kind: "column", namespace, name: step.table, column: step.column, dependency: { kind: "column", table: { namespace, name: step.table }, column: step.column } }];
|
|
5290
|
+
case "alterColumn":
|
|
5291
|
+
return step.changes.includes("type") ? [{ kind: "column", namespace, name: step.table, column: step.column, dependency: { kind: "column", table: { namespace, name: step.table }, column: step.column } }] : [];
|
|
5292
|
+
default:
|
|
5293
|
+
return [];
|
|
5294
|
+
}
|
|
5295
|
+
});
|
|
5296
|
+
}
|
|
5297
|
+
function unavailable() {
|
|
5298
|
+
return new VibeError24({ code: "VIBE_MIGRATION", message: "Malformed catalog dependency row", meta: { reason: "external-inventory-unavailable" } });
|
|
5299
|
+
}
|
|
5300
|
+
function text(params) {
|
|
5301
|
+
const value = params.row[params.key];
|
|
5302
|
+
if (value === null || value === undefined)
|
|
5303
|
+
return;
|
|
5304
|
+
if (typeof value !== "string")
|
|
5305
|
+
throw unavailable();
|
|
5306
|
+
return value;
|
|
5307
|
+
}
|
|
5308
|
+
function dependentOwner(params) {
|
|
5309
|
+
const { row, external } = params;
|
|
5310
|
+
const relationNamespace = text({ row, key: "relationNamespace" });
|
|
5311
|
+
const relationName = text({ row, key: "relationName" });
|
|
5312
|
+
const relationKind = text({ row, key: "relationKind" });
|
|
5313
|
+
const column = text({ row, key: "columnName" });
|
|
5314
|
+
const dependentClass = text({ row, key: "dependentClass" });
|
|
5315
|
+
const objectNamespace = text({ row, key: "objectNamespace" });
|
|
5316
|
+
const objectName = text({ row, key: "objectName" });
|
|
5317
|
+
if (relationNamespace !== undefined && relationName !== undefined) {
|
|
5318
|
+
const name = { namespace: relationNamespace, name: relationName };
|
|
5319
|
+
const anchor = relationKind === "r" || relationKind === "p" || relationKind === "f" ? { kind: "table", name } : relationKind === "v" || relationKind === "m" ? { kind: "view", name } : relationKind === "S" ? { kind: "sequence", name } : { kind: "namespace", name: relationNamespace };
|
|
5320
|
+
const owner = externalObjectOwner({ identity: anchor, external });
|
|
5321
|
+
const identity = anchor.kind === "table" && column !== undefined && dependentClass === "pg_attrdef" ? { kind: "columnDefault", table: name, column } : anchor.kind === "table" && column !== undefined ? { kind: "column", table: name, column } : anchor.kind === "namespace" ? undefined : anchor;
|
|
5322
|
+
return { ...owner === undefined ? {} : { owner }, ...identity === undefined ? {} : { identity } };
|
|
5323
|
+
}
|
|
5324
|
+
const anchor = dependentClass === "pg_proc" && objectNamespace !== undefined && objectName !== undefined ? { kind: "function", name: { namespace: objectNamespace, name: objectName }, argumentTypes: [] } : objectNamespace === undefined ? undefined : { kind: "namespace", name: objectNamespace };
|
|
5325
|
+
const owner = anchor === undefined ? undefined : externalObjectOwner({ identity: anchor, external });
|
|
5326
|
+
return owner === undefined ? {} : { owner };
|
|
5327
|
+
}
|
|
5328
|
+
function assertNoExternalDependents(params) {
|
|
5329
|
+
for (const row of params.rows) {
|
|
5330
|
+
const target = typeof row.position === "number" ? params.targets[row.position] : undefined;
|
|
5331
|
+
const type = text({ row, key: "dependentType" });
|
|
5332
|
+
const identity = text({ row, key: "dependentIdentity" });
|
|
5333
|
+
const referenced = text({ row, key: "referenced" });
|
|
5334
|
+
if (target === undefined || type === undefined || identity === undefined || referenced === undefined)
|
|
5335
|
+
throw unavailable();
|
|
5336
|
+
const dependent = dependentOwner({ row, external: params.external });
|
|
5337
|
+
if (dependent.owner === undefined)
|
|
5338
|
+
continue;
|
|
5339
|
+
throw new VibeError24({
|
|
5340
|
+
code: "VIBE_MIGRATION",
|
|
5341
|
+
message: "An externally owned object depends on an application object this change drops or retypes; automatic DDL is refused",
|
|
5342
|
+
meta: { reason: "external-owned-dependency", owner: dependent.owner, ...dependent.identity === undefined ? {} : { identity: dependent.identity }, dependent: { type, identity }, dependency: target.dependency, referenced }
|
|
5343
|
+
});
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
async function refuseExternalDependents(params) {
|
|
5347
|
+
if (params.external.declarations.length === 0)
|
|
5348
|
+
return;
|
|
5349
|
+
if (params.dialect !== "postgres")
|
|
5350
|
+
throw unsupportedCapability8({ dialect: params.dialect, feature: "externalObjects" });
|
|
5351
|
+
const targets = externalDependencyTargets({ steps: params.steps, namespace: params.namespace });
|
|
5352
|
+
if (targets.length === 0)
|
|
5353
|
+
return;
|
|
5354
|
+
const query = renderExternalDependents({ dialect: getSqlDialect6({ dialect: params.dialect }), targets: targets.map(({ dependency, ...target }) => target) });
|
|
5355
|
+
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
5356
|
+
assertNoExternalDependents({ external: params.external, targets, rows });
|
|
5357
|
+
}
|
|
5358
|
+
|
|
4813
5359
|
// packages/migrate/src/module-history.ts
|
|
4814
|
-
import { VibeError as
|
|
5360
|
+
import { VibeError as VibeError25, sqlObjectKey as sqlObjectKey9, providerToDialect as providerToDialect3 } from "@vibeorm/schema";
|
|
4815
5361
|
function refusal2(params) {
|
|
4816
|
-
return new
|
|
5362
|
+
return new VibeError25({ code: "VIBE_MIGRATION", message: `Module ${params.moduleId} history, ownership or predecessor state is inconsistent; restore or complete the reviewed upgrade first`, meta: params });
|
|
4817
5363
|
}
|
|
4818
5364
|
function moduleHistoryHeads(params) {
|
|
4819
5365
|
const heads = new Map;
|
|
@@ -4848,20 +5394,22 @@ function applicationModuleOrigins(params) {
|
|
|
4848
5394
|
return origins;
|
|
4849
5395
|
}
|
|
4850
5396
|
function preserves(params) {
|
|
4851
|
-
const
|
|
4852
|
-
const
|
|
4853
|
-
const
|
|
4854
|
-
const
|
|
5397
|
+
const dialect = providerToDialect3({ provider: params.expected.schema.datasource.provider });
|
|
5398
|
+
const keyOf = (entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect });
|
|
5399
|
+
const tables = new Set(params.expected.schema.models.map(keyOf));
|
|
5400
|
+
const enums = new Set(params.expected.schema.enums.map(keyOf));
|
|
5401
|
+
const models = params.actual.schema.models.filter((model) => tables.has(keyOf(model)));
|
|
5402
|
+
const declaredEnums = params.actual.schema.enums.filter((entry) => enums.has(keyOf(entry)));
|
|
4855
5403
|
if (canonicalMigrationJson({ value: models }) !== canonicalMigrationJson({ value: params.expected.schema.models }) || canonicalMigrationJson({ value: declaredEnums }) !== canonicalMigrationJson({ value: params.expected.schema.enums }))
|
|
4856
5404
|
throw refusal2({ reason: "module-application-mutation", moduleId: params.moduleId });
|
|
4857
|
-
const objectKeys = new Set((params.expected.schema.migrationObjects ?? []).map((object) =>
|
|
4858
|
-
const definitionKeys = new Set(params.expected.definitions.map((definition) =>
|
|
4859
|
-
if (canonicalMigrationJson({ value: (params.actual.schema.migrationObjects ?? []).filter((object) => objectKeys.has(
|
|
5405
|
+
const objectKeys = new Set((params.expected.schema.migrationObjects ?? []).map((object) => sqlObjectKey9({ identity: object.identity })));
|
|
5406
|
+
const definitionKeys = new Set(params.expected.definitions.map((definition) => sqlObjectKey9({ identity: definition.identity })));
|
|
5407
|
+
if (canonicalMigrationJson({ value: (params.actual.schema.migrationObjects ?? []).filter((object) => objectKeys.has(sqlObjectKey9({ identity: object.identity }))) }) !== canonicalMigrationJson({ value: params.expected.schema.migrationObjects ?? [] }) || canonicalMigrationJson({ value: params.actual.definitions.filter((definition) => definitionKeys.has(sqlObjectKey9({ identity: definition.identity }))) }) !== canonicalMigrationJson({ value: params.expected.definitions }))
|
|
4860
5408
|
throw refusal2({ reason: "module-application-mutation", moduleId: params.moduleId });
|
|
4861
5409
|
for (const model of params.actual.schema.models)
|
|
4862
5410
|
for (const relation of model.relations) {
|
|
4863
5411
|
const target = params.actual.schema.models.find((candidate) => candidate.name === relation.targetModel);
|
|
4864
|
-
if (target !== undefined && tables.has(model
|
|
5412
|
+
if (target !== undefined && tables.has(keyOf(model)) !== tables.has(keyOf(target)))
|
|
4865
5413
|
throw refusal2({ reason: "module-external-dependency", moduleId: params.moduleId });
|
|
4866
5414
|
}
|
|
4867
5415
|
}
|
|
@@ -4871,7 +5419,7 @@ function assertModuleUpgradeHistory(params) {
|
|
|
4871
5419
|
const register = (migration) => {
|
|
4872
5420
|
const origin = migration.moduleOrigin;
|
|
4873
5421
|
for (const identity of origin.owned) {
|
|
4874
|
-
const key =
|
|
5422
|
+
const key = sqlObjectKey9({ identity });
|
|
4875
5423
|
if (owners.has(key) && owners.get(key) !== origin.moduleId)
|
|
4876
5424
|
throw refusal2({ reason: "module-object-collision", moduleId: origin.moduleId });
|
|
4877
5425
|
owners.set(key, origin.moduleId);
|
|
@@ -4918,11 +5466,11 @@ function assertModuleUpgradeHistory(params) {
|
|
|
4918
5466
|
}
|
|
4919
5467
|
|
|
4920
5468
|
// packages/migrate/src/module-journal.ts
|
|
4921
|
-
import { VibeError as
|
|
4922
|
-
import { getSqlDialect as
|
|
5469
|
+
import { VibeError as VibeError27 } from "@vibeorm/schema";
|
|
5470
|
+
import { getSqlDialect as getSqlDialect7, MODULE_JOURNAL_COLUMN, renderModuleJournal } from "@vibeorm/sql";
|
|
4923
5471
|
|
|
4924
5472
|
// packages/migrate/src/journal-read.ts
|
|
4925
|
-
import { VibeError as
|
|
5473
|
+
import { VibeError as VibeError26 } from "@vibeorm/schema";
|
|
4926
5474
|
var MISSING_TABLE_IDENTIFIERS = new Set(["42P01", "42S02", "ER_NO_SUCH_TABLE"]);
|
|
4927
5475
|
function isMissingTable(params) {
|
|
4928
5476
|
let current = params.cause;
|
|
@@ -4946,7 +5494,7 @@ async function readJournalRows(params) {
|
|
|
4946
5494
|
if (presence.state === "missing")
|
|
4947
5495
|
return;
|
|
4948
5496
|
}
|
|
4949
|
-
throw new
|
|
5497
|
+
throw new VibeError26({ code: "VIBE_MIGRATION", message: "Cannot inspect migration history; check SELECT privileges", meta: { reason: "history-unavailable" }, cause });
|
|
4950
5498
|
});
|
|
4951
5499
|
if (rows === undefined)
|
|
4952
5500
|
return;
|
|
@@ -4954,7 +5502,7 @@ async function readJournalRows(params) {
|
|
|
4954
5502
|
for (const row of rows) {
|
|
4955
5503
|
const index = row.statement_index;
|
|
4956
5504
|
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)) {
|
|
4957
|
-
throw new
|
|
5505
|
+
throw new VibeError26({ code: "VIBE_MIGRATION", message: "Malformed migration journal row", meta: { reason: "history-unavailable" } });
|
|
4958
5506
|
}
|
|
4959
5507
|
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 } : {} });
|
|
4960
5508
|
}
|
|
@@ -4962,41 +5510,41 @@ async function readJournalRows(params) {
|
|
|
4962
5510
|
}
|
|
4963
5511
|
|
|
4964
5512
|
// packages/migrate/src/module-journal.ts
|
|
4965
|
-
function
|
|
4966
|
-
return new
|
|
5513
|
+
function invalid6() {
|
|
5514
|
+
return new VibeError27({ 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" } });
|
|
4967
5515
|
}
|
|
4968
5516
|
function parseModuleJournalEntry(params) {
|
|
4969
|
-
return parseMigrationMetadata({ text: params.text, failure:
|
|
5517
|
+
return parseMigrationMetadata({ text: params.text, failure: invalid6, parse: (value) => {
|
|
4970
5518
|
if (value === null || typeof value !== "object" || !("version" in value) || value.version !== 1 || !("complete" in value) || typeof value.complete !== "boolean" || !("migration" in value) || value.migration === null || typeof value.migration !== "object")
|
|
4971
|
-
throw
|
|
5519
|
+
throw invalid6();
|
|
4972
5520
|
const migration = value.migration;
|
|
4973
5521
|
assertModuleOrigin({ value: migration.moduleOrigin });
|
|
4974
5522
|
if (migration.name !== params.name || migrationChecksum(migration) !== params.checksum)
|
|
4975
|
-
throw
|
|
5523
|
+
throw invalid6();
|
|
4976
5524
|
assertMaterializedModuleMigration({ migration });
|
|
4977
5525
|
assertModuleOwnership({ origin: migration.moduleOrigin, desiredState: migration.desiredState });
|
|
4978
5526
|
return { version: 1, complete: value.complete, migration };
|
|
4979
5527
|
} });
|
|
4980
5528
|
}
|
|
4981
5529
|
async function readModuleJournal(params) {
|
|
4982
|
-
const query = renderModuleJournal({ dialect:
|
|
5530
|
+
const query = renderModuleJournal({ dialect: getSqlDialect7({ dialect: params.dialect }), table: MIGRATIONS_TABLE_NAME, operation: { kind: "read" } });
|
|
4983
5531
|
const rows = await readJournalRows({ ...params, query });
|
|
4984
5532
|
return new Map([...rows ?? []].map(([name, row]) => {
|
|
4985
5533
|
const entry = row.moduleEntry === undefined ? undefined : parseModuleJournalEntry({ text: row.moduleEntry, name, checksum: row.checksum });
|
|
4986
5534
|
if (entry !== undefined && entry.migration.moduleOrigin.dialect !== params.dialect)
|
|
4987
|
-
throw
|
|
5535
|
+
throw invalid6();
|
|
4988
5536
|
return [name, { row, ...entry === undefined ? {} : { entry } }];
|
|
4989
5537
|
}));
|
|
4990
5538
|
}
|
|
4991
5539
|
async function ensureModuleJournal(params) {
|
|
4992
|
-
const dialect =
|
|
5540
|
+
const dialect = getSqlDialect7({ dialect: params.dialect });
|
|
4993
5541
|
const columns = await params.executor(renderModuleJournal({ dialect, table: MIGRATIONS_TABLE_NAME, operation: { kind: "columns" } }));
|
|
4994
5542
|
if (columns.some((row) => row.name === MODULE_JOURNAL_COLUMN))
|
|
4995
5543
|
return;
|
|
4996
5544
|
await params.executor(renderModuleJournal({ dialect, table: MIGRATIONS_TABLE_NAME, operation: { kind: "addColumn" } }));
|
|
4997
5545
|
}
|
|
4998
5546
|
async function insertModuleJournal(params) {
|
|
4999
|
-
await params.executor(renderModuleJournal({ dialect:
|
|
5547
|
+
await params.executor(renderModuleJournal({ dialect: getSqlDialect7({ dialect: params.dialect }), table: MIGRATIONS_TABLE_NAME, operation: {
|
|
5000
5548
|
kind: "insert",
|
|
5001
5549
|
name: params.migration.name,
|
|
5002
5550
|
checksum: migrationChecksum(params.migration),
|
|
@@ -5005,7 +5553,7 @@ async function insertModuleJournal(params) {
|
|
|
5005
5553
|
} }));
|
|
5006
5554
|
}
|
|
5007
5555
|
async function completeModuleJournal(params) {
|
|
5008
|
-
await params.executor(renderModuleJournal({ dialect:
|
|
5556
|
+
await params.executor(renderModuleJournal({ dialect: getSqlDialect7({ dialect: params.dialect }), table: MIGRATIONS_TABLE_NAME, operation: {
|
|
5009
5557
|
kind: "complete",
|
|
5010
5558
|
name: params.migration.name,
|
|
5011
5559
|
entry: canonicalMigrationJson({ value: { version: 1, migration: params.migration, complete: true } })
|
|
@@ -5014,27 +5562,27 @@ async function completeModuleJournal(params) {
|
|
|
5014
5562
|
function moduleRecordComplete(params) {
|
|
5015
5563
|
const { migration, stored } = params;
|
|
5016
5564
|
if (stored.moduleEntry === undefined || stored.checksum === "")
|
|
5017
|
-
throw
|
|
5565
|
+
throw invalid6();
|
|
5018
5566
|
const entry = parseModuleJournalEntry({ text: stored.moduleEntry, name: migration.name, checksum: stored.checksum });
|
|
5019
5567
|
if (migrationChecksum(migration) !== stored.checksum)
|
|
5020
|
-
throw
|
|
5568
|
+
throw invalid6();
|
|
5021
5569
|
return entry.complete && stored.error === undefined && stored.statementIndex === migration.sql.length;
|
|
5022
5570
|
}
|
|
5023
5571
|
|
|
5024
5572
|
// packages/migrate/src/runner.ts
|
|
5025
|
-
import { VibeError as
|
|
5573
|
+
import { VibeError as VibeError35 } from "@vibeorm/schema";
|
|
5026
5574
|
import {
|
|
5027
5575
|
DIALECT_CAPABILITIES as DIALECT_CAPABILITIES3,
|
|
5028
|
-
getSqlDialect as
|
|
5576
|
+
getSqlDialect as getSqlDialect9,
|
|
5029
5577
|
renderMigrationSessionRead,
|
|
5030
5578
|
renderMigrationSessionRestore,
|
|
5031
5579
|
renderMigrationSessionSet
|
|
5032
5580
|
} from "@vibeorm/sql";
|
|
5033
5581
|
|
|
5034
5582
|
// packages/migrate/src/recovery.ts
|
|
5035
|
-
import { VibeError as
|
|
5583
|
+
import { VibeError as VibeError28 } from "@vibeorm/schema";
|
|
5036
5584
|
import {
|
|
5037
|
-
getSqlDialect as
|
|
5585
|
+
getSqlDialect as getSqlDialect8,
|
|
5038
5586
|
renderColumnStateQuery,
|
|
5039
5587
|
renderConstraintStateQuery,
|
|
5040
5588
|
renderIndexStateQuery,
|
|
@@ -5048,7 +5596,7 @@ function constraintUnverifiable(params) {
|
|
|
5048
5596
|
});
|
|
5049
5597
|
}
|
|
5050
5598
|
function conflict2(params) {
|
|
5051
|
-
return new
|
|
5599
|
+
return new VibeError28({
|
|
5052
5600
|
code: "VIBE_MIGRATION",
|
|
5053
5601
|
message: params.message,
|
|
5054
5602
|
meta: { reason: params.reason, ...params.meta }
|
|
@@ -5073,7 +5621,7 @@ function sameShape(params) {
|
|
|
5073
5621
|
}
|
|
5074
5622
|
async function establishOperationState(params) {
|
|
5075
5623
|
const { executor, operation } = params;
|
|
5076
|
-
const dialect =
|
|
5624
|
+
const dialect = getSqlDialect8({ dialect: params.dialect });
|
|
5077
5625
|
const run = async (query) => executor({ text: query.text, values: [...query.values] });
|
|
5078
5626
|
switch (operation.kind) {
|
|
5079
5627
|
case "createIndexConcurrently": {
|
|
@@ -5152,11 +5700,11 @@ async function establishOperationState(params) {
|
|
|
5152
5700
|
}
|
|
5153
5701
|
|
|
5154
5702
|
// packages/migrate/src/differ.ts
|
|
5155
|
-
import { isEnumType as isEnumType6, unsupportedCapability as
|
|
5156
|
-
import { VibeError as
|
|
5703
|
+
import { assertIndexOperatorClasses as assertIndexOperatorClasses2, isEnumType as isEnumType6, qualifiedSqlName as qualifiedSqlName9, unsupportedCapability as unsupportedCapability10 } from "@vibeorm/schema";
|
|
5704
|
+
import { VibeError as VibeError30 } from "@vibeorm/schema";
|
|
5157
5705
|
|
|
5158
5706
|
// packages/migrate/src/renames.ts
|
|
5159
|
-
import { VibeError as
|
|
5707
|
+
import { VibeError as VibeError29, unsupportedCapability as unsupportedCapability9 } from "@vibeorm/schema";
|
|
5160
5708
|
function tableOf(model) {
|
|
5161
5709
|
return model.dbName ?? model.name;
|
|
5162
5710
|
}
|
|
@@ -5167,7 +5715,7 @@ function hasColumn(params) {
|
|
|
5167
5715
|
return params.model.fields.some((field) => (field.dbName ?? field.name) === params.column);
|
|
5168
5716
|
}
|
|
5169
5717
|
function ambiguous(params) {
|
|
5170
|
-
return new
|
|
5718
|
+
return new VibeError29({
|
|
5171
5719
|
code: "VIBE_MIGRATION",
|
|
5172
5720
|
message: `rename refused: ${params.detail}`,
|
|
5173
5721
|
meta: { reason: "rename-ambiguous", ...params.meta }
|
|
@@ -5177,6 +5725,8 @@ function planRenames(params) {
|
|
|
5177
5725
|
const { to, dialect } = params;
|
|
5178
5726
|
if (params.renames.length === 0)
|
|
5179
5727
|
return { steps: [], from: params.from };
|
|
5728
|
+
if ([params.from, params.to].some((schema) => schema.models.some((model) => model.namespace !== undefined && model.namespace !== "public" && params.renames.some((intent) => intent.kind === "table" ? [intent.from, intent.to].includes(tableOf(model)) : intent.table === tableOf(model)))))
|
|
5729
|
+
throw unsupportedCapability9({ dialect, feature: "qualifiedRenameIntent", hint: "Use reviewed namespace-qualified SQL for a physical rename in a fixed namespace" });
|
|
5180
5730
|
const renamedTables = new Map;
|
|
5181
5731
|
const renamedColumns = new Map;
|
|
5182
5732
|
const steps = [];
|
|
@@ -5268,9 +5818,19 @@ function planRenames(params) {
|
|
|
5268
5818
|
// packages/migrate/src/differ.ts
|
|
5269
5819
|
function diffSchemas(params) {
|
|
5270
5820
|
const dialect = params.dialect ?? "postgres";
|
|
5821
|
+
assertIndexOperatorClasses2({ schema: params.to, dialect });
|
|
5822
|
+
for (const kind of ["models", "enums"])
|
|
5823
|
+
for (const entry of params.to[kind]) {
|
|
5824
|
+
const key = physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect });
|
|
5825
|
+
const before = params.from[kind].find((candidate) => candidate.name === entry.name) ?? params.from[kind].find((candidate) => (candidate.dbName ?? candidate.name) === (entry.dbName ?? entry.name) && !params.to[kind].some((target) => physicalNameKey({ name: target.dbName ?? target.name, namespace: target.namespace, dialect }) === physicalNameKey({ name: candidate.dbName ?? candidate.name, namespace: candidate.namespace, dialect })));
|
|
5826
|
+
if (before !== undefined && qualifiedSqlName9({ name: entry.name, namespace: before.namespace }).namespace !== qualifiedSqlName9({ name: entry.name, namespace: entry.namespace }).namespace && !params.from[kind].some((candidate) => physicalNameKey({ name: candidate.dbName ?? candidate.name, namespace: candidate.namespace, dialect }) === key))
|
|
5827
|
+
throw unsupportedCapability10({ dialect, feature: "automaticNamespaceMove", hint: "Moving populated objects to a fixed namespace requires an explicit reviewed migration" });
|
|
5828
|
+
}
|
|
5829
|
+
for (const schema of [params.from, params.to])
|
|
5830
|
+
normalizeSchema({ schema, dialect });
|
|
5271
5831
|
for (const schema of [params.from, params.to])
|
|
5272
5832
|
if (dialect !== "postgres" && schema.models.some((model) => (model.exclusions?.length ?? 0) > 0))
|
|
5273
|
-
throw
|
|
5833
|
+
throw unsupportedCapability10({ dialect, feature: "exclusionConstraints" });
|
|
5274
5834
|
assertRlsDialect({ schema: params.from, dialect });
|
|
5275
5835
|
assertRlsDialect({ schema: params.to, dialect });
|
|
5276
5836
|
if (dialect !== "postgres")
|
|
@@ -5280,6 +5840,7 @@ function diffSchemas(params) {
|
|
|
5280
5840
|
const from = normalizeSchema({ schema: params.from, dialect });
|
|
5281
5841
|
const to = normalizeSchema({ schema: params.to, dialect });
|
|
5282
5842
|
const buckets = {
|
|
5843
|
+
createNamespace: dialect === "postgres" ? schemaNamespaces({ schema: params.to }).filter((namespace) => !schemaNamespaces({ schema: params.from }).includes(namespace)).map((namespace) => ({ kind: "createNamespace", namespace, destructive: false })) : [],
|
|
5283
5844
|
dropExclusion: [],
|
|
5284
5845
|
addExclusion: [],
|
|
5285
5846
|
dropView: [],
|
|
@@ -5309,16 +5870,17 @@ function diffSchemas(params) {
|
|
|
5309
5870
|
diffTables({ from, to, buckets, dialect });
|
|
5310
5871
|
for (const model of params.to.models) {
|
|
5311
5872
|
const table = model.dbName ?? model.name;
|
|
5312
|
-
const
|
|
5873
|
+
const placement = model.namespace === undefined ? {} : { namespace: model.namespace };
|
|
5874
|
+
const previous = params.from.models.find((entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect }) === physicalNameKey({ name: table, namespace: model.namespace, dialect }));
|
|
5313
5875
|
for (const exclusion of model.exclusions ?? []) {
|
|
5314
5876
|
const before = previous?.exclusions?.find((entry) => entry.name === exclusion.name);
|
|
5315
5877
|
if (before?.definition === exclusion.definition)
|
|
5316
5878
|
continue;
|
|
5317
5879
|
if (before?.catalogDefinition)
|
|
5318
|
-
throw new
|
|
5880
|
+
throw new VibeError30({ code: "VIBE_MIGRATION", message: "Exclusion comparison requires normalized evidence", meta: { reason: "exclusion-inspection-unavailable", table, name: exclusion.name } });
|
|
5319
5881
|
if (before !== undefined)
|
|
5320
|
-
buckets.dropExclusion.push({ kind: "dropExclusion", table, exclusion: before, destructive: false });
|
|
5321
|
-
buckets.addExclusion.push({ kind: "addExclusion", table, exclusion, destructive: false });
|
|
5882
|
+
buckets.dropExclusion.push({ kind: "dropExclusion", table, ...placement, exclusion: before, destructive: false });
|
|
5883
|
+
buckets.addExclusion.push({ kind: "addExclusion", table, ...placement, exclusion, destructive: false });
|
|
5322
5884
|
}
|
|
5323
5885
|
for (const exclusion of previous?.exclusions ?? [])
|
|
5324
5886
|
if (!model.exclusions?.some((entry) => entry.name === exclusion.name))
|
|
@@ -5326,6 +5888,7 @@ function diffSchemas(params) {
|
|
|
5326
5888
|
}
|
|
5327
5889
|
refuseDependentEnumStorageChanges({ from, to, buckets, dialect });
|
|
5328
5890
|
return withRlsSteps({ from: params.from, to: params.to, steps: [
|
|
5891
|
+
...buckets.createNamespace,
|
|
5329
5892
|
...renamed.steps,
|
|
5330
5893
|
...buckets.dropView,
|
|
5331
5894
|
...buckets.createEnum,
|
|
@@ -5358,7 +5921,7 @@ function refuseScalarListColumns(params) {
|
|
|
5358
5921
|
for (const field of model.fields) {
|
|
5359
5922
|
if (!field.isList || field.isUnsupported === true)
|
|
5360
5923
|
continue;
|
|
5361
|
-
throw
|
|
5924
|
+
throw unsupportedCapability10({
|
|
5362
5925
|
dialect,
|
|
5363
5926
|
feature: `scalar list column "${model.name}.${field.name}"`,
|
|
5364
5927
|
hint: `${dialect} has no array column type \u2014 model the list as a separate table or a ${dialect === "mysql" ? "JSON" : "Json"} column`
|
|
@@ -5378,12 +5941,12 @@ function diffEnums(params) {
|
|
|
5378
5941
|
const currentValues = new Set(enumDef.values);
|
|
5379
5942
|
for (const value of enumDef.values) {
|
|
5380
5943
|
if (!previousValues.has(value)) {
|
|
5381
|
-
buckets.addEnumValue.push({ kind: "addEnumValue", enumName: name, value, destructive: false });
|
|
5944
|
+
buckets.addEnumValue.push({ kind: "addEnumValue", enumName: enumDef.dbName ?? enumDef.name, ...enumDef.namespace === undefined ? {} : { namespace: enumDef.namespace }, value, destructive: false });
|
|
5382
5945
|
}
|
|
5383
5946
|
}
|
|
5384
5947
|
for (const value of previous.values) {
|
|
5385
5948
|
if (!currentValues.has(value)) {
|
|
5386
|
-
throw
|
|
5949
|
+
throw unsupportedCapability10({
|
|
5387
5950
|
dialect: "postgres",
|
|
5388
5951
|
feature: `removing value "${value}" from enum "${name}"`,
|
|
5389
5952
|
hint: "postgres cannot drop enum values \u2014 recreate the type manually (create new type, ALTER COLUMN ... USING, drop old type)"
|
|
@@ -5391,9 +5954,9 @@ function diffEnums(params) {
|
|
|
5391
5954
|
}
|
|
5392
5955
|
}
|
|
5393
5956
|
}
|
|
5394
|
-
for (const [name] of from.enums) {
|
|
5957
|
+
for (const [name, enumDef] of from.enums) {
|
|
5395
5958
|
if (!to.enums.has(name)) {
|
|
5396
|
-
buckets.dropEnum.push({ kind: "dropEnum", enumName: name, destructive: true });
|
|
5959
|
+
buckets.dropEnum.push({ kind: "dropEnum", enumName: enumDef.dbName ?? enumDef.name, ...enumDef.namespace === undefined ? {} : { namespace: enumDef.namespace }, destructive: true });
|
|
5397
5960
|
}
|
|
5398
5961
|
}
|
|
5399
5962
|
}
|
|
@@ -5403,10 +5966,10 @@ function refuseDependentEnumStorageChanges(params) {
|
|
|
5403
5966
|
if (step.kind !== "alterColumn" || !isEnumStringTransition({ from: step.from, to: step.to }))
|
|
5404
5967
|
continue;
|
|
5405
5968
|
for (const normalized of [from, to]) {
|
|
5406
|
-
const hasFk = [...normalized.tables.values()].some((table) => [...table.fks.values()].some((fk) => table.table === step.table && fk.columns.includes(step.column) || fk.refTable === step.table && fk.refColumns.includes(step.column)));
|
|
5407
|
-
const hasJoin = [...normalized.joinTables.values()].some((join) => [join.a, join.b].some((end) => end.table === step.table && end.column === step.column));
|
|
5969
|
+
const hasFk = [...normalized.tables.values()].some((table) => [...table.fks.values()].some((fk) => table.table === step.table && (table.namespace ?? "public") === (step.namespace ?? "public") && fk.columns.includes(step.column) || fk.refTable === step.table && (fk.refNamespace ?? "public") === (step.namespace ?? "public") && fk.refColumns.includes(step.column)));
|
|
5970
|
+
const hasJoin = [...normalized.joinTables.values()].some((join) => [join.a, join.b].some((end) => end.table === step.table && (end.namespace ?? "public") === (step.namespace ?? "public") && end.column === step.column));
|
|
5408
5971
|
if (hasFk || hasJoin) {
|
|
5409
|
-
throw
|
|
5972
|
+
throw unsupportedCapability10({
|
|
5410
5973
|
dialect,
|
|
5411
5974
|
feature: `enum storage conversion on "${step.table}"."${step.column}" with foreign-key dependencies`,
|
|
5412
5975
|
hint: "migrate the referencing columns and constraints together in a reviewed manual migration"
|
|
@@ -5417,28 +5980,31 @@ function refuseDependentEnumStorageChanges(params) {
|
|
|
5417
5980
|
}
|
|
5418
5981
|
function diffViews(params) {
|
|
5419
5982
|
const { from, to, buckets } = params;
|
|
5420
|
-
for (const [name] of from.views) {
|
|
5983
|
+
for (const [name, view] of from.views) {
|
|
5421
5984
|
if (!to.views.has(name)) {
|
|
5422
|
-
buckets.dropView.push({ kind: "dropView", table: name, destructive: true });
|
|
5985
|
+
buckets.dropView.push({ kind: "dropView", table: view.dbName ?? view.name, ...view.namespace === undefined ? {} : { namespace: view.namespace }, destructive: true });
|
|
5423
5986
|
}
|
|
5424
5987
|
}
|
|
5425
5988
|
}
|
|
5426
5989
|
function diffJoinTables(params) {
|
|
5427
5990
|
const { from, to, buckets } = params;
|
|
5428
|
-
for (const [
|
|
5429
|
-
if (!from.joinTables.has(
|
|
5991
|
+
for (const [key, joinTable] of to.joinTables) {
|
|
5992
|
+
if (!from.joinTables.has(key)) {
|
|
5993
|
+
const table = joinTable.table;
|
|
5994
|
+
const placement = joinTable.namespace === undefined ? {} : { namespace: joinTable.namespace };
|
|
5430
5995
|
buckets.createJoinTable.push({
|
|
5431
5996
|
kind: "createJoinTable",
|
|
5432
5997
|
table,
|
|
5998
|
+
...placement,
|
|
5433
5999
|
a: joinTable.a,
|
|
5434
6000
|
b: joinTable.b,
|
|
5435
6001
|
destructive: false
|
|
5436
6002
|
});
|
|
5437
6003
|
}
|
|
5438
6004
|
}
|
|
5439
|
-
for (const [
|
|
5440
|
-
if (!to.joinTables.has(
|
|
5441
|
-
buckets.dropJoinTable.push({ kind: "dropJoinTable", table, destructive: true });
|
|
6005
|
+
for (const [key, joinTable] of from.joinTables) {
|
|
6006
|
+
if (!to.joinTables.has(key)) {
|
|
6007
|
+
buckets.dropJoinTable.push({ kind: "dropJoinTable", table: joinTable.table, ...joinTable.namespace === undefined ? {} : { namespace: joinTable.namespace }, destructive: true });
|
|
5442
6008
|
}
|
|
5443
6009
|
}
|
|
5444
6010
|
}
|
|
@@ -5463,9 +6029,9 @@ function diffTables(params) {
|
|
|
5463
6029
|
continue;
|
|
5464
6030
|
if (dialect !== "sqlite")
|
|
5465
6031
|
for (const fk of table.fks.values()) {
|
|
5466
|
-
buckets.dropForeignKey.push({ kind: "dropForeignKey", table:
|
|
6032
|
+
buckets.dropForeignKey.push({ kind: "dropForeignKey", table: table.table, ...table.namespace === undefined ? {} : { namespace: table.namespace }, constraintName: fk.constraintName, destructive: false });
|
|
5467
6033
|
}
|
|
5468
|
-
buckets.dropTable.push({ kind: "dropTable", table:
|
|
6034
|
+
buckets.dropTable.push({ kind: "dropTable", table: table.table, ...table.namespace === undefined ? {} : { namespace: table.namespace }, destructive: true });
|
|
5469
6035
|
}
|
|
5470
6036
|
}
|
|
5471
6037
|
function createWholeTable(params) {
|
|
@@ -5473,6 +6039,7 @@ function createWholeTable(params) {
|
|
|
5473
6039
|
buckets.createTable.push({
|
|
5474
6040
|
kind: "createTable",
|
|
5475
6041
|
table: table.table,
|
|
6042
|
+
...table.namespace === undefined ? {} : { namespace: table.namespace },
|
|
5476
6043
|
columns: [...table.columns.values()].map((column) => column.field),
|
|
5477
6044
|
primaryKey: table.pk,
|
|
5478
6045
|
...table.model.primaryKeyConstraintName === undefined ? {} : { primaryKeyConstraintName: table.model.primaryKeyConstraintName },
|
|
@@ -5483,6 +6050,7 @@ function createWholeTable(params) {
|
|
|
5483
6050
|
buckets.createIndex.push({
|
|
5484
6051
|
kind: "createIndex",
|
|
5485
6052
|
table: table.table,
|
|
6053
|
+
...table.namespace === undefined ? {} : { namespace: table.namespace },
|
|
5486
6054
|
name: index.name,
|
|
5487
6055
|
indexKind: index.kind,
|
|
5488
6056
|
unique: index.unique,
|
|
@@ -5496,6 +6064,7 @@ function createWholeTable(params) {
|
|
|
5496
6064
|
buckets.addForeignKey.push({
|
|
5497
6065
|
kind: "addForeignKey",
|
|
5498
6066
|
table: table.table,
|
|
6067
|
+
...table.namespace === undefined ? {} : { namespace: table.namespace },
|
|
5499
6068
|
constraintName: fk.constraintName,
|
|
5500
6069
|
columns: fk.columns,
|
|
5501
6070
|
refTable: fk.refTable,
|
|
@@ -5509,14 +6078,15 @@ function createWholeTable(params) {
|
|
|
5509
6078
|
function diffOneTable(params) {
|
|
5510
6079
|
const { previous, current, buckets, dialect, fromEnums, toEnums } = params;
|
|
5511
6080
|
const table = current.table;
|
|
6081
|
+
const placement = current.namespace === undefined ? {} : { namespace: current.namespace };
|
|
5512
6082
|
for (const [name, column] of current.columns) {
|
|
5513
6083
|
const before = previous.columns.get(name);
|
|
5514
6084
|
if (before === undefined) {
|
|
5515
|
-
buckets.addColumn.push({ kind: "addColumn", table, column: column.field, destructive: false });
|
|
6085
|
+
buckets.addColumn.push({ kind: "addColumn", table, ...placement, column: column.field, destructive: false });
|
|
5516
6086
|
continue;
|
|
5517
6087
|
}
|
|
5518
6088
|
const changes = [];
|
|
5519
|
-
if (
|
|
6089
|
+
if (columnTypeChanged({ declared: column, actual: before }))
|
|
5520
6090
|
changes.push("type");
|
|
5521
6091
|
if (before.nullable !== column.nullable)
|
|
5522
6092
|
changes.push("nullability");
|
|
@@ -5528,25 +6098,25 @@ function diffOneTable(params) {
|
|
|
5528
6098
|
if (changes.length === 0)
|
|
5529
6099
|
continue;
|
|
5530
6100
|
if (changes.includes("type") && [...previous.checks].some(([name, expression]) => current.checks.get(name) === expression)) {
|
|
5531
|
-
throw
|
|
6101
|
+
throw unsupportedCapability10({ dialect, feature: "alterColumnWithRetainedCheck", hint: "CHECK dependencies and type compatibility are not proven; use a reviewed migration that replaces and revalidates the affected constraints" });
|
|
5532
6102
|
}
|
|
5533
6103
|
const storageChange = isEnumStringTransition({ from: before.field, to: column.field });
|
|
5534
6104
|
if (storageChange) {
|
|
5535
6105
|
assertEnumStringTransition({ from: before.field, to: column.field, dialect, table, column: name });
|
|
5536
6106
|
if (isEnumType6(before.field.type) && column.field.type === "String") {
|
|
5537
|
-
const values = resolveEnumValues({ name: before.field.type.enum, enums: fromEnums });
|
|
6107
|
+
const values = resolveEnumValues({ name: before.field.type.enum, namespace: before.field.type.namespace, enums: fromEnums });
|
|
5538
6108
|
if (values !== undefined)
|
|
5539
6109
|
assertStringEnumLabelsFit({ field: column.field, values, dialect, table });
|
|
5540
6110
|
}
|
|
5541
6111
|
}
|
|
5542
|
-
let narrowingType = changes.includes("type") && !isScalarToArrayChange({ from: before.field, to: column.field }) && !(storageChange && column.field.type === "String");
|
|
6112
|
+
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");
|
|
5543
6113
|
if (dialect !== "postgres" && changes.includes("type") && isEnumType6(before.field.type) && isEnumType6(column.field.type)) {
|
|
5544
6114
|
const fromValues = resolveEnumValues({ name: before.field.type.enum, enums: fromEnums });
|
|
5545
6115
|
const toValues = resolveEnumValues({ name: column.field.type.enum, enums: toEnums });
|
|
5546
6116
|
if (fromValues !== undefined && toValues !== undefined) {
|
|
5547
6117
|
const removed = fromValues.filter((value) => !toValues.includes(value));
|
|
5548
6118
|
if (removed.length > 0) {
|
|
5549
|
-
throw
|
|
6119
|
+
throw unsupportedCapability10({
|
|
5550
6120
|
dialect,
|
|
5551
6121
|
feature: `removing value(s) ${removed.map((value) => `"${value}"`).join(", ")} from enum column "${table}"."${name}"`,
|
|
5552
6122
|
hint: "migrate the stored values away first, then recreate the column with the narrower enum"
|
|
@@ -5558,6 +6128,7 @@ function diffOneTable(params) {
|
|
|
5558
6128
|
buckets.alterColumn.push({
|
|
5559
6129
|
kind: "alterColumn",
|
|
5560
6130
|
table,
|
|
6131
|
+
...placement,
|
|
5561
6132
|
column: name,
|
|
5562
6133
|
from: before.field,
|
|
5563
6134
|
to: column.field,
|
|
@@ -5567,19 +6138,20 @@ function diffOneTable(params) {
|
|
|
5567
6138
|
}
|
|
5568
6139
|
for (const [name] of previous.columns) {
|
|
5569
6140
|
if (!current.columns.has(name)) {
|
|
5570
|
-
buckets.dropColumn.push({ kind: "dropColumn", table, column: name, destructive: true });
|
|
6141
|
+
buckets.dropColumn.push({ kind: "dropColumn", table, ...placement, column: name, destructive: true });
|
|
5571
6142
|
}
|
|
5572
6143
|
}
|
|
5573
6144
|
const pkBefore = previous.pk.join(",");
|
|
5574
6145
|
const pkAfter = current.pk.join(",");
|
|
5575
6146
|
if (pkBefore !== pkAfter) {
|
|
5576
6147
|
if (previous.pk.length > 0) {
|
|
5577
|
-
buckets.dropPrimaryKey.push({ kind: "dropPrimaryKey", table, destructive: false, ...previous.model.primaryKeyConstraintName === undefined ? {} : { constraintName: previous.model.primaryKeyConstraintName } });
|
|
6148
|
+
buckets.dropPrimaryKey.push({ kind: "dropPrimaryKey", table, ...placement, destructive: false, ...previous.model.primaryKeyConstraintName === undefined ? {} : { constraintName: previous.model.primaryKeyConstraintName } });
|
|
5578
6149
|
}
|
|
5579
6150
|
if (current.pk.length > 0) {
|
|
5580
6151
|
buckets.addPrimaryKey.push({
|
|
5581
6152
|
kind: "addPrimaryKey",
|
|
5582
6153
|
table,
|
|
6154
|
+
...placement,
|
|
5583
6155
|
columns: current.pk,
|
|
5584
6156
|
...current.model.primaryKeyConstraintName === undefined ? {} : { constraintName: current.model.primaryKeyConstraintName },
|
|
5585
6157
|
destructive: false
|
|
@@ -5591,15 +6163,15 @@ function diffOneTable(params) {
|
|
|
5591
6163
|
if (before === expression)
|
|
5592
6164
|
continue;
|
|
5593
6165
|
if (before !== undefined && previous.model.checks?.some((check) => check.name === name && check.catalogExpression === true))
|
|
5594
|
-
throw new
|
|
6166
|
+
throw new VibeError30({ 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 } });
|
|
5595
6167
|
if (before !== undefined)
|
|
5596
|
-
buckets.dropCheck.push({ kind: "dropCheck", table, name, destructive: false });
|
|
5597
|
-
buckets.addCheck.push({ kind: "addCheck", table, name, expression, destructive: false });
|
|
6168
|
+
buckets.dropCheck.push({ kind: "dropCheck", table, ...placement, name, destructive: false });
|
|
6169
|
+
buckets.addCheck.push({ kind: "addCheck", table, ...placement, name, expression, destructive: false });
|
|
5598
6170
|
}
|
|
5599
6171
|
for (const [name] of previous.checks) {
|
|
5600
6172
|
if (current.checks.has(name))
|
|
5601
6173
|
continue;
|
|
5602
|
-
buckets.dropCheck.push({ kind: "dropCheck", table, name, destructive: false });
|
|
6174
|
+
buckets.dropCheck.push({ kind: "dropCheck", table, ...placement, name, destructive: false });
|
|
5603
6175
|
}
|
|
5604
6176
|
for (const [key, index] of current.indexes) {
|
|
5605
6177
|
if (previous.indexes.has(key))
|
|
@@ -5607,6 +6179,7 @@ function diffOneTable(params) {
|
|
|
5607
6179
|
buckets.createIndex.push({
|
|
5608
6180
|
kind: "createIndex",
|
|
5609
6181
|
table,
|
|
6182
|
+
...placement,
|
|
5610
6183
|
name: index.name,
|
|
5611
6184
|
indexKind: index.kind,
|
|
5612
6185
|
unique: index.unique,
|
|
@@ -5619,7 +6192,7 @@ function diffOneTable(params) {
|
|
|
5619
6192
|
for (const [key, index] of previous.indexes) {
|
|
5620
6193
|
if (current.indexes.has(key))
|
|
5621
6194
|
continue;
|
|
5622
|
-
buckets.dropIndex.push({ kind: "dropIndex", name: index.name, table, destructive: false, ...index.constraintName === undefined ? {} : { constraintName: index.constraintName } });
|
|
6195
|
+
buckets.dropIndex.push({ kind: "dropIndex", name: index.name, table, ...placement, destructive: false, ...index.constraintName === undefined ? {} : { constraintName: index.constraintName } });
|
|
5623
6196
|
}
|
|
5624
6197
|
for (const [key, fk] of current.fks) {
|
|
5625
6198
|
const before = previous.fks.get(key);
|
|
@@ -5627,9 +6200,11 @@ function diffOneTable(params) {
|
|
|
5627
6200
|
buckets.addForeignKey.push({
|
|
5628
6201
|
kind: "addForeignKey",
|
|
5629
6202
|
table,
|
|
6203
|
+
...placement,
|
|
5630
6204
|
constraintName: fk.constraintName,
|
|
5631
6205
|
columns: fk.columns,
|
|
5632
6206
|
refTable: fk.refTable,
|
|
6207
|
+
...fk.refNamespace === undefined ? {} : { refNamespace: fk.refNamespace },
|
|
5633
6208
|
refColumns: fk.refColumns,
|
|
5634
6209
|
onDelete: fk.onDelete,
|
|
5635
6210
|
onUpdate: fk.onUpdate,
|
|
@@ -5641,15 +6216,18 @@ function diffOneTable(params) {
|
|
|
5641
6216
|
buckets.dropForeignKey.push({
|
|
5642
6217
|
kind: "dropForeignKey",
|
|
5643
6218
|
table,
|
|
6219
|
+
...placement,
|
|
5644
6220
|
constraintName: before.constraintName,
|
|
5645
6221
|
destructive: false
|
|
5646
6222
|
});
|
|
5647
6223
|
buckets.addForeignKey.push({
|
|
5648
6224
|
kind: "addForeignKey",
|
|
5649
6225
|
table,
|
|
6226
|
+
...placement,
|
|
5650
6227
|
constraintName: fk.constraintName,
|
|
5651
6228
|
columns: fk.columns,
|
|
5652
6229
|
refTable: fk.refTable,
|
|
6230
|
+
...fk.refNamespace === undefined ? {} : { refNamespace: fk.refNamespace },
|
|
5653
6231
|
refColumns: fk.refColumns,
|
|
5654
6232
|
onDelete: fk.onDelete,
|
|
5655
6233
|
onUpdate: fk.onUpdate,
|
|
@@ -5663,6 +6241,7 @@ function diffOneTable(params) {
|
|
|
5663
6241
|
buckets.dropForeignKey.push({
|
|
5664
6242
|
kind: "dropForeignKey",
|
|
5665
6243
|
table,
|
|
6244
|
+
...placement,
|
|
5666
6245
|
constraintName: fk.constraintName,
|
|
5667
6246
|
destructive: false
|
|
5668
6247
|
});
|
|
@@ -5670,12 +6249,12 @@ function diffOneTable(params) {
|
|
|
5670
6249
|
}
|
|
5671
6250
|
|
|
5672
6251
|
// packages/migrate/src/ddl/render.ts
|
|
5673
|
-
import { VibeError as
|
|
6252
|
+
import { VibeError as VibeError34, sqlObjectKey as sqlObjectKey10 } from "@vibeorm/schema";
|
|
5674
6253
|
|
|
5675
6254
|
// packages/migrate/src/ddl/postgres.ts
|
|
5676
6255
|
import { renderExclusionSetup } from "@vibeorm/sql";
|
|
5677
|
-
import { VibeError as
|
|
5678
|
-
import { postgresDialect as
|
|
6256
|
+
import { VibeError as VibeError31, assertSqlNamespace as assertSqlNamespace3, defaultPrimaryKeyName as defaultPrimaryKeyName3, implicitJoinTableObjectNames as implicitJoinTableObjectNames2, isEnumType as isEnumType7 } from "@vibeorm/schema";
|
|
6257
|
+
import { postgresDialect as postgresDialect9, renderRlsPolicyPath, renderIndexColumns } from "@vibeorm/sql";
|
|
5679
6258
|
function renderPostgresSteps(params) {
|
|
5680
6259
|
refuseSameTransactionEnumUse({ steps: params.steps });
|
|
5681
6260
|
return params.steps.flatMap((step) => renderStep({ step }));
|
|
@@ -5685,19 +6264,21 @@ function refuseSameTransactionEnumUse(params) {
|
|
|
5685
6264
|
for (const step of params.steps) {
|
|
5686
6265
|
if (step.kind !== "addEnumValue")
|
|
5687
6266
|
continue;
|
|
5688
|
-
const
|
|
6267
|
+
const key = JSON.stringify([step.namespace ?? "public", step.enumName]);
|
|
6268
|
+
const values = added.get(key) ?? new Set;
|
|
5689
6269
|
values.add(step.value);
|
|
5690
|
-
added.set(
|
|
6270
|
+
added.set(key, values);
|
|
5691
6271
|
}
|
|
5692
6272
|
if (added.size === 0)
|
|
5693
6273
|
return;
|
|
5694
6274
|
const use = (field) => {
|
|
5695
6275
|
if (!isEnumType7(field.type))
|
|
5696
6276
|
return;
|
|
5697
|
-
const
|
|
6277
|
+
const type = field.type;
|
|
6278
|
+
const enumName = type.enum;
|
|
5698
6279
|
const def = field.default;
|
|
5699
6280
|
const candidates = def?.kind === "list" ? def.values : def?.kind === "enumValue" || def?.kind === "literal" ? [def.value] : [];
|
|
5700
|
-
const value = candidates.find((entry) => typeof entry === "string" && added.get(enumName)?.has(entry) === true);
|
|
6281
|
+
const value = candidates.find((entry) => typeof entry === "string" && added.get(JSON.stringify([type.namespace ?? "public", enumName]))?.has(entry) === true);
|
|
5701
6282
|
return value === undefined ? undefined : { enumName, value };
|
|
5702
6283
|
};
|
|
5703
6284
|
for (const step of params.steps) {
|
|
@@ -5709,7 +6290,7 @@ function refuseSameTransactionEnumUse(params) {
|
|
|
5709
6290
|
const found = use(field);
|
|
5710
6291
|
if (found === undefined)
|
|
5711
6292
|
continue;
|
|
5712
|
-
throw new
|
|
6293
|
+
throw new VibeError31({
|
|
5713
6294
|
code: "VIBE_MIGRATION",
|
|
5714
6295
|
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`,
|
|
5715
6296
|
meta: { enum: found.enumName, value: found.value, table: step.table, column: field.dbName ?? field.name, rule: "enum-value-used-in-adding-transaction" }
|
|
@@ -5720,27 +6301,30 @@ function refuseSameTransactionEnumUse(params) {
|
|
|
5720
6301
|
function renderStep(params) {
|
|
5721
6302
|
const { step } = params;
|
|
5722
6303
|
switch (step.kind) {
|
|
6304
|
+
case "createNamespace":
|
|
6305
|
+
assertSqlNamespace3({ namespace: step.namespace, dialect: "postgres", allowPublic: false });
|
|
6306
|
+
return [`CREATE SCHEMA ${q2(step.namespace)};`];
|
|
5723
6307
|
case "addExclusion":
|
|
5724
|
-
return [...renderExclusionSetup({ dialect:
|
|
6308
|
+
return [...renderExclusionSetup({ dialect: postgresDialect9, exclusion: step.exclusion }), `ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.exclusion.name)} ${step.exclusion.definition};`];
|
|
5725
6309
|
case "dropExclusion":
|
|
5726
|
-
return [`ALTER TABLE ${
|
|
6310
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.exclusion.name)};`];
|
|
5727
6311
|
case "enableRls":
|
|
5728
6312
|
case "forceRls":
|
|
5729
6313
|
case "disableRls":
|
|
5730
6314
|
case "noForceRls":
|
|
5731
6315
|
case "createPolicy":
|
|
5732
6316
|
case "dropPolicy":
|
|
5733
|
-
return step.kind === "createPolicy" ? [renderRlsPolicyPath({ dialect:
|
|
6317
|
+
return step.kind === "createPolicy" ? [renderRlsPolicyPath({ dialect: postgresDialect9, stage: "save" }).text, renderRlsPolicyPath({ dialect: postgresDialect9, stage: "empty" }).text, ...renderRlsStep({ step, dialect: "postgres" }), renderRlsPolicyPath({ dialect: postgresDialect9, stage: "restore" }).text] : renderRlsStep({ step, dialect: "postgres" });
|
|
5734
6318
|
case "createEnum": {
|
|
5735
6319
|
const name = step.enumDef.dbName ?? step.enumDef.name;
|
|
5736
6320
|
const values = step.enumDef.values.map((value) => quotePgString({ value })).join(", ");
|
|
5737
|
-
return [`CREATE TYPE ${
|
|
6321
|
+
return [`CREATE TYPE ${qualifiedName(name, step.enumDef.namespace)} AS ENUM (${values});`];
|
|
5738
6322
|
}
|
|
5739
6323
|
case "dropEnum":
|
|
5740
|
-
return [`DROP TYPE ${
|
|
6324
|
+
return [`DROP TYPE ${qualifiedName(step.enumName, step.namespace)};`];
|
|
5741
6325
|
case "addEnumValue":
|
|
5742
6326
|
return [
|
|
5743
|
-
`ALTER TYPE ${
|
|
6327
|
+
`ALTER TYPE ${qualifiedName(step.enumName, step.namespace)} ADD VALUE IF NOT EXISTS ${quotePgString({ value: step.value })};`
|
|
5744
6328
|
];
|
|
5745
6329
|
case "createTable": {
|
|
5746
6330
|
const lines = step.columns.map((column) => ` ${columnDefSql({ field: column })}`);
|
|
@@ -5751,101 +6335,101 @@ function renderStep(params) {
|
|
|
5751
6335
|
for (const check of step.checks ?? []) {
|
|
5752
6336
|
lines.push(` CONSTRAINT ${q2(check.name)} CHECK (${check.expression})`);
|
|
5753
6337
|
}
|
|
5754
|
-
return [`CREATE TABLE ${
|
|
6338
|
+
return [`CREATE TABLE ${qualifiedName(step.table, step.namespace)} (
|
|
5755
6339
|
${lines.join(`,
|
|
5756
6340
|
`)}
|
|
5757
6341
|
);`];
|
|
5758
6342
|
}
|
|
5759
6343
|
case "dropTable":
|
|
5760
6344
|
case "dropJoinTable":
|
|
5761
|
-
return [`DROP TABLE ${
|
|
6345
|
+
return [`DROP TABLE ${qualifiedName(step.table, step.namespace)};`];
|
|
5762
6346
|
case "dropView":
|
|
5763
|
-
return [`DROP VIEW IF EXISTS ${
|
|
6347
|
+
return [`DROP VIEW IF EXISTS ${qualifiedName(step.table, step.namespace)};`];
|
|
5764
6348
|
case "addColumn":
|
|
5765
|
-
return [`ALTER TABLE ${
|
|
6349
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD COLUMN ${columnDefSql({ field: step.column })};`];
|
|
5766
6350
|
case "dropColumn":
|
|
5767
|
-
return [`ALTER TABLE ${
|
|
6351
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP COLUMN ${q2(step.column)};`];
|
|
5768
6352
|
case "alterColumn":
|
|
5769
6353
|
return renderAlterColumn({ step });
|
|
5770
6354
|
case "createIndex": {
|
|
5771
6355
|
const unique = step.unique ? "UNIQUE " : "";
|
|
5772
6356
|
const using = !step.unique && step.indexKind !== "btree" ? ` USING ${step.indexKind}` : "";
|
|
5773
|
-
const cols =
|
|
6357
|
+
const cols = renderIndexColumns({ dialect: postgresDialect9, columns: step.columns, unique: step.unique }).join(", ");
|
|
5774
6358
|
const nulls = step.nullsNotDistinct === true ? " NULLS NOT DISTINCT" : "";
|
|
5775
6359
|
const where = step.where === undefined ? "" : ` WHERE ${step.where}`;
|
|
5776
6360
|
return [
|
|
5777
|
-
`CREATE ${unique}INDEX ${q2(step.name)} ON ${
|
|
6361
|
+
`CREATE ${unique}INDEX ${q2(step.name)} ON ${qualifiedName(step.table, step.namespace)}${using} (${cols})${nulls}${where};`
|
|
5778
6362
|
];
|
|
5779
6363
|
}
|
|
5780
6364
|
case "dropIndex":
|
|
5781
6365
|
if (step.constraintName !== undefined) {
|
|
5782
6366
|
if (step.table === undefined)
|
|
5783
|
-
throw new
|
|
5784
|
-
return [`ALTER TABLE ${
|
|
6367
|
+
throw new VibeError31({ code: "VIBE_MIGRATION", message: "Constraint-backed index removal requires its owning table" });
|
|
6368
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.constraintName)};`];
|
|
5785
6369
|
}
|
|
5786
|
-
return [`DROP INDEX ${
|
|
6370
|
+
return [`DROP INDEX ${qualifiedName(step.name, step.namespace)};`];
|
|
5787
6371
|
case "addCheck":
|
|
5788
6372
|
return [
|
|
5789
|
-
`ALTER TABLE ${
|
|
6373
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.name)} CHECK (${step.expression});`
|
|
5790
6374
|
];
|
|
5791
6375
|
case "dropCheck":
|
|
5792
|
-
return [`ALTER TABLE ${
|
|
6376
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.name)};`];
|
|
5793
6377
|
case "addForeignKey": {
|
|
5794
6378
|
const cols = step.columns.map((column) => q2(column)).join(", ");
|
|
5795
6379
|
const refCols = step.refColumns.map((column) => q2(column)).join(", ");
|
|
5796
6380
|
return [
|
|
5797
|
-
`ALTER TABLE ${
|
|
6381
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.constraintName)} FOREIGN KEY (${cols}) REFERENCES ${qualifiedName(step.refTable, step.refNamespace)} (${refCols}) ON DELETE ${CASCADE_ACTION_SQL[step.onDelete]} ON UPDATE ${CASCADE_ACTION_SQL[step.onUpdate]};`
|
|
5798
6382
|
];
|
|
5799
6383
|
}
|
|
5800
6384
|
case "addForeignKeyNotValid": {
|
|
5801
6385
|
const cols = step.columns.map((column) => q2(column)).join(", ");
|
|
5802
6386
|
const refCols = step.refColumns.map((column) => q2(column)).join(", ");
|
|
5803
6387
|
return [
|
|
5804
|
-
`ALTER TABLE ${
|
|
6388
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.constraintName)} FOREIGN KEY (${cols}) REFERENCES ${qualifiedName(step.refTable, step.refNamespace)} (${refCols}) ON DELETE ${CASCADE_ACTION_SQL[step.onDelete]} ON UPDATE ${CASCADE_ACTION_SQL[step.onUpdate]} NOT VALID;`
|
|
5805
6389
|
];
|
|
5806
6390
|
}
|
|
5807
6391
|
case "addCheckNotValid":
|
|
5808
6392
|
return [
|
|
5809
|
-
`ALTER TABLE ${
|
|
6393
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.name)} CHECK (${step.expression}) NOT VALID;`
|
|
5810
6394
|
];
|
|
5811
6395
|
case "validateConstraint":
|
|
5812
|
-
return [`ALTER TABLE ${
|
|
6396
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} VALIDATE CONSTRAINT ${q2(step.name)};`];
|
|
5813
6397
|
case "renameTable":
|
|
5814
|
-
return [`ALTER TABLE ${
|
|
6398
|
+
return [`ALTER TABLE ${qualifiedName(step.from, step.namespace)} RENAME TO ${q2(step.to)};`];
|
|
5815
6399
|
case "renameColumn":
|
|
5816
|
-
return [`ALTER TABLE ${
|
|
6400
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} RENAME COLUMN ${q2(step.from)} TO ${q2(step.to)};`];
|
|
5817
6401
|
case "dropForeignKey":
|
|
5818
|
-
return [`ALTER TABLE ${
|
|
6402
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.constraintName)};`];
|
|
5819
6403
|
case "createJoinTable": {
|
|
5820
6404
|
const aType = columnTypeSql({ field: { ...step.a.field, isList: false } });
|
|
5821
6405
|
const bType = columnTypeSql({ field: { ...step.b.field, isList: false } });
|
|
5822
6406
|
const names = implicitJoinTableObjectNames2({ table: step.table });
|
|
5823
6407
|
return [
|
|
5824
|
-
`CREATE TABLE ${
|
|
6408
|
+
`CREATE TABLE ${qualifiedName(step.table, step.namespace)} (
|
|
5825
6409
|
"A" ${aType} NOT NULL,
|
|
5826
6410
|
"B" ${bType} NOT NULL
|
|
5827
6411
|
);`,
|
|
5828
|
-
`CREATE UNIQUE INDEX ${q2(names.uniqueIndex)} ON ${
|
|
5829
|
-
`CREATE INDEX ${q2(names.indexB)} ON ${
|
|
5830
|
-
`ALTER TABLE ${
|
|
5831
|
-
`ALTER TABLE ${
|
|
6412
|
+
`CREATE UNIQUE INDEX ${q2(names.uniqueIndex)} ON ${qualifiedName(step.table, step.namespace)} ("A", "B");`,
|
|
6413
|
+
`CREATE INDEX ${q2(names.indexB)} ON ${qualifiedName(step.table, step.namespace)} ("B");`,
|
|
6414
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(names.foreignKeyA)} FOREIGN KEY ("A") REFERENCES ${qualifiedName(step.a.table, step.a.namespace)} (${q2(step.a.column)}) ON DELETE CASCADE ON UPDATE CASCADE;`,
|
|
6415
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(names.foreignKeyB)} FOREIGN KEY ("B") REFERENCES ${qualifiedName(step.b.table, step.b.namespace)} (${q2(step.b.column)}) ON DELETE CASCADE ON UPDATE CASCADE;`
|
|
5832
6416
|
];
|
|
5833
6417
|
}
|
|
5834
6418
|
case "addPrimaryKey": {
|
|
5835
6419
|
const cols = step.columns.map((column) => q2(column)).join(", ");
|
|
5836
6420
|
return [
|
|
5837
|
-
`ALTER TABLE ${
|
|
6421
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.constraintName ?? defaultPrimaryKeyName3({ table: step.table, dialect: "postgres" }))} PRIMARY KEY (${cols});`
|
|
5838
6422
|
];
|
|
5839
6423
|
}
|
|
5840
6424
|
case "dropPrimaryKey":
|
|
5841
|
-
return [`ALTER TABLE ${
|
|
6425
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.constraintName ?? defaultPrimaryKeyName3({ table: step.table, dialect: "postgres" }))};`];
|
|
5842
6426
|
}
|
|
5843
6427
|
}
|
|
5844
6428
|
function columnDefSql(params) {
|
|
5845
6429
|
const { field } = params;
|
|
5846
6430
|
const name = q2(field.dbName ?? field.name);
|
|
5847
6431
|
if (field.default?.kind === "autoincrement" && !isIdentityColumn({ field })) {
|
|
5848
|
-
throw new
|
|
6432
|
+
throw new VibeError31({
|
|
5849
6433
|
code: "VIBE_MIGRATION",
|
|
5850
6434
|
message: `autoincrement default on "${field.name}" requires a non-list Int or BigInt column`,
|
|
5851
6435
|
meta: { field: field.name }
|
|
@@ -5865,7 +6449,7 @@ function renderAlterColumn(params) {
|
|
|
5865
6449
|
const { step } = params;
|
|
5866
6450
|
const statements = [];
|
|
5867
6451
|
const column = q2(step.column);
|
|
5868
|
-
const alterPrefix = `ALTER TABLE ${
|
|
6452
|
+
const alterPrefix = `ALTER TABLE ${qualifiedName(step.table, step.namespace)} ALTER COLUMN ${column}`;
|
|
5869
6453
|
const storageChange = step.changes.includes("type") && isEnumStringTransition({ from: step.from, to: step.to });
|
|
5870
6454
|
if (storageChange)
|
|
5871
6455
|
assertEnumStringTransition({ from: step.from, to: step.to, dialect: "postgres", table: step.table, column: step.column });
|
|
@@ -5881,7 +6465,7 @@ function renderAlterColumn(params) {
|
|
|
5881
6465
|
}
|
|
5882
6466
|
if (step.changes.includes("default") || droppedDefault) {
|
|
5883
6467
|
if (step.to.default?.kind === "autoincrement") {
|
|
5884
|
-
throw new
|
|
6468
|
+
throw new VibeError31({
|
|
5885
6469
|
code: "VIBE_MIGRATION",
|
|
5886
6470
|
message: `cannot add autoincrement to existing column "${step.table}"."${step.column}" \u2014 drop and recreate the column instead`,
|
|
5887
6471
|
meta: { table: step.table, column: step.column }
|
|
@@ -5892,15 +6476,16 @@ function renderAlterColumn(params) {
|
|
|
5892
6476
|
}
|
|
5893
6477
|
return statements;
|
|
5894
6478
|
}
|
|
5895
|
-
function
|
|
5896
|
-
|
|
6479
|
+
function qualifiedName(name, namespace = "public") {
|
|
6480
|
+
assertSqlNamespace3({ namespace, dialect: "postgres" });
|
|
6481
|
+
return `${q2(namespace)}.${q2(name)}`;
|
|
5897
6482
|
}
|
|
5898
6483
|
function q2(name) {
|
|
5899
|
-
return
|
|
6484
|
+
return postgresDialect9.quoteIdent({ name });
|
|
5900
6485
|
}
|
|
5901
6486
|
|
|
5902
6487
|
// packages/migrate/src/ddl/sqlite.ts
|
|
5903
|
-
import { VibeError as
|
|
6488
|
+
import { VibeError as VibeError32, implicitJoinTableObjectNames as implicitJoinTableObjectNames3, isEnumType as isEnumType8, unsupportedCapability as unsupportedCapability11 } from "@vibeorm/schema";
|
|
5904
6489
|
import { sqliteDialect as sqliteDialect3 } from "@vibeorm/sql";
|
|
5905
6490
|
|
|
5906
6491
|
// packages/migrate/src/ddl/drop-order.ts
|
|
@@ -5950,6 +6535,8 @@ function orderDropsChildrenFirst(params) {
|
|
|
5950
6535
|
function describeStep(params) {
|
|
5951
6536
|
const { step } = params;
|
|
5952
6537
|
switch (step.kind) {
|
|
6538
|
+
case "createNamespace":
|
|
6539
|
+
return `create namespace ${JSON.stringify(step.namespace)}`;
|
|
5953
6540
|
case "addExclusion":
|
|
5954
6541
|
return `add exclusion "${step.exclusion.name}" on "${step.table}"`;
|
|
5955
6542
|
case "dropExclusion":
|
|
@@ -6020,6 +6607,12 @@ function describeStep(params) {
|
|
|
6020
6607
|
// packages/migrate/src/ddl/sqlite.ts
|
|
6021
6608
|
function renderSqliteSteps(params) {
|
|
6022
6609
|
const { steps } = params;
|
|
6610
|
+
for (const step of steps) {
|
|
6611
|
+
if (step.kind === "createIndex" && step.columns.some((column) => column.operatorClass !== undefined))
|
|
6612
|
+
throw unsupportedCapability11({ dialect: "sqlite", feature: "indexOperatorClasses" });
|
|
6613
|
+
if (step.kind === "createNamespace" || step.namespace !== undefined || step.refNamespace !== undefined)
|
|
6614
|
+
throw unsupportedCapability11({ dialect: "sqlite", feature: "fixedNamespaces" });
|
|
6615
|
+
}
|
|
6023
6616
|
for (const step of steps)
|
|
6024
6617
|
if (isRlsStep({ step }))
|
|
6025
6618
|
renderRlsStep({ step, dialect: "sqlite" });
|
|
@@ -6040,7 +6633,7 @@ function renderSqliteSteps(params) {
|
|
|
6040
6633
|
continue;
|
|
6041
6634
|
const awaitsNewParent = [...toNorm?.tables.get(table)?.fks.values() ?? []].some((fk) => createdTables.has(fk.refTable));
|
|
6042
6635
|
if (awaitsNewParent) {
|
|
6043
|
-
throw new
|
|
6636
|
+
throw new VibeError32({
|
|
6044
6637
|
code: "VIBE_MIGRATION",
|
|
6045
6638
|
message: `cannot order this sqlite migration safely: rebuilding "${table}" must happen BEFORE the dropped table(s) it references (${[
|
|
6046
6639
|
...droppedTables
|
|
@@ -6069,7 +6662,7 @@ function renderSqliteSteps(params) {
|
|
|
6069
6662
|
switch (step.kind) {
|
|
6070
6663
|
case "addExclusion":
|
|
6071
6664
|
case "dropExclusion":
|
|
6072
|
-
throw new
|
|
6665
|
+
throw new VibeError32({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "sqlite", feature: "exclusionConstraints" } });
|
|
6073
6666
|
case "createEnum":
|
|
6074
6667
|
case "dropEnum":
|
|
6075
6668
|
case "addEnumValue":
|
|
@@ -6155,7 +6748,7 @@ function renderSqliteSteps(params) {
|
|
|
6155
6748
|
case "renameColumn": {
|
|
6156
6749
|
const renamed = step.kind === "renameTable" ? step.from : step.table;
|
|
6157
6750
|
if (rebuildSet.has(renamed) || step.kind === "renameTable" && rebuildSet.has(step.to)) {
|
|
6158
|
-
throw new
|
|
6751
|
+
throw new VibeError32({
|
|
6159
6752
|
code: "VIBE_MIGRATION",
|
|
6160
6753
|
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`,
|
|
6161
6754
|
meta: { reason: "rename-ambiguous", table: renamed, dialect: "sqlite" }
|
|
@@ -6167,7 +6760,7 @@ function renderSqliteSteps(params) {
|
|
|
6167
6760
|
case "addForeignKeyNotValid":
|
|
6168
6761
|
case "addCheckNotValid":
|
|
6169
6762
|
case "validateConstraint":
|
|
6170
|
-
throw
|
|
6763
|
+
throw unsupportedCapability11({
|
|
6171
6764
|
dialect: "sqlite",
|
|
6172
6765
|
feature: "stagedConstraints",
|
|
6173
6766
|
hint: "sqlite applies constraint changes by rebuilding the table; add the constraint in one step"
|
|
@@ -6209,7 +6802,7 @@ function classifyRebuilds(params) {
|
|
|
6209
6802
|
switch (step.kind) {
|
|
6210
6803
|
case "addExclusion":
|
|
6211
6804
|
case "dropExclusion":
|
|
6212
|
-
throw new
|
|
6805
|
+
throw new VibeError32({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "sqlite", feature: "exclusionConstraints" } });
|
|
6213
6806
|
case "alterColumn":
|
|
6214
6807
|
case "dropForeignKey":
|
|
6215
6808
|
case "addPrimaryKey":
|
|
@@ -6261,7 +6854,7 @@ async function assertSqliteRebuildTableOptions(params) {
|
|
|
6261
6854
|
for (const table of params.tables) {
|
|
6262
6855
|
const row = rows.find((entry) => entry.schema === "main" && entry.name === table);
|
|
6263
6856
|
if (row === undefined || Number(row.strict) !== 0 || Number(row.wr) !== 0) {
|
|
6264
|
-
throw
|
|
6857
|
+
throw unsupportedCapability11({ dialect: "sqlite", feature: "sqliteRebuildTableOptions", hint: `Cannot reproduce the existing table options of "${table}"; use a reviewed migration that preserves STRICT and WITHOUT ROWID` });
|
|
6265
6858
|
}
|
|
6266
6859
|
}
|
|
6267
6860
|
}
|
|
@@ -6270,7 +6863,7 @@ function renderTableRebuild(params) {
|
|
|
6270
6863
|
const source = from.tables.get(table);
|
|
6271
6864
|
const target = to.tables.get(table);
|
|
6272
6865
|
if (source === undefined || target === undefined) {
|
|
6273
|
-
throw new
|
|
6866
|
+
throw new VibeError32({
|
|
6274
6867
|
code: "VIBE_MIGRATION",
|
|
6275
6868
|
message: `cannot rebuild "${table}" \u2014 the table is missing from the ${source === undefined ? "from" : "to"} schema`,
|
|
6276
6869
|
meta: { table }
|
|
@@ -6297,7 +6890,7 @@ function renderTableRebuild(params) {
|
|
|
6297
6890
|
}));
|
|
6298
6891
|
const common = [...new Set([...target.columns.keys(), ...params.preservedColumns.keys()])].filter((name) => params.preservedColumns.get(name)?.generated !== true && (source.columns.has(name) || params.preservedColumns.has(name)));
|
|
6299
6892
|
if (common.length === 0) {
|
|
6300
|
-
throw
|
|
6893
|
+
throw unsupportedCapability11({ dialect: "sqlite", feature: "sqliteRebuildWithoutRowTransfer", hint: `Rebuilding "${table}" has no transferable columns and would discard every row; use explicit renames or a reviewed data migration` });
|
|
6301
6894
|
}
|
|
6302
6895
|
const cols = common.map((name) => q3(name)).join(", ");
|
|
6303
6896
|
statements.push(`INSERT INTO ${q3(tmp)} (${cols}) SELECT ${cols} FROM ${q3(table)};`);
|
|
@@ -6405,7 +6998,7 @@ function columnDefSql2(params) {
|
|
|
6405
6998
|
const { field, constraintTable, enums, inlinePkAutoincrement } = params;
|
|
6406
6999
|
const name = field.dbName ?? field.name;
|
|
6407
7000
|
if (field.default?.kind === "autoincrement" && !inlinePkAutoincrement) {
|
|
6408
|
-
throw new
|
|
7001
|
+
throw new VibeError32({
|
|
6409
7002
|
code: "VIBE_MIGRATION",
|
|
6410
7003
|
message: `autoincrement on sqlite requires "${field.name}" to be the single-column Int or BigInt primary key`,
|
|
6411
7004
|
meta: { field: field.name }
|
|
@@ -6424,7 +7017,7 @@ function columnDefSql2(params) {
|
|
|
6424
7017
|
const enumName = field.type.enum;
|
|
6425
7018
|
const values = resolveEnumValues({ name: enumName, enums });
|
|
6426
7019
|
if (values === undefined) {
|
|
6427
|
-
throw new
|
|
7020
|
+
throw new VibeError32({
|
|
6428
7021
|
code: "VIBE_MIGRATION",
|
|
6429
7022
|
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`,
|
|
6430
7023
|
meta: { field: field.name, enum: enumName }
|
|
@@ -6438,14 +7031,14 @@ function columnDefSql2(params) {
|
|
|
6438
7031
|
function createIndexSql(params) {
|
|
6439
7032
|
const { table, name, unique, kind, columns, where } = params;
|
|
6440
7033
|
if (params.nullsNotDistinct === true) {
|
|
6441
|
-
throw
|
|
7034
|
+
throw unsupportedCapability11({
|
|
6442
7035
|
dialect: "sqlite",
|
|
6443
7036
|
feature: `UNIQUE NULLS NOT DISTINCT (index "${name}")`,
|
|
6444
7037
|
hint: "sqlite treats NULLs as distinct in unique indexes and has no switch \u2014 gate the flag per dialect"
|
|
6445
7038
|
});
|
|
6446
7039
|
}
|
|
6447
7040
|
if (!unique && kind !== "btree") {
|
|
6448
|
-
throw
|
|
7041
|
+
throw unsupportedCapability11({
|
|
6449
7042
|
dialect: "sqlite",
|
|
6450
7043
|
feature: `index kind "${kind}" (index "${name}")`,
|
|
6451
7044
|
hint: "sqlite indexes are btree only; use an extension-specific virtual table for others"
|
|
@@ -6459,7 +7052,7 @@ function requireSchemas(params) {
|
|
|
6459
7052
|
const { step, fromNorm, toNorm } = params;
|
|
6460
7053
|
if (fromNorm !== undefined && toNorm !== undefined)
|
|
6461
7054
|
return;
|
|
6462
|
-
throw new
|
|
7055
|
+
throw new VibeError32({
|
|
6463
7056
|
code: "VIBE_MIGRATION",
|
|
6464
7057
|
message: `sqlite step "${describeStep({ step })}" requires a table rebuild \u2014 renderSteps needs the { from, to } schemas to construct it`,
|
|
6465
7058
|
meta: { step: step.kind }
|
|
@@ -6470,10 +7063,14 @@ function q3(name) {
|
|
|
6470
7063
|
}
|
|
6471
7064
|
|
|
6472
7065
|
// packages/migrate/src/ddl/mysql.ts
|
|
6473
|
-
import { VibeError as
|
|
7066
|
+
import { VibeError as VibeError33, implicitJoinTableObjectNames as implicitJoinTableObjectNames4, unsupportedCapability as unsupportedCapability12 } from "@vibeorm/schema";
|
|
6474
7067
|
import { mysqlDialect as mysqlDialect3 } from "@vibeorm/sql";
|
|
6475
7068
|
function renderMysqlSteps(params) {
|
|
6476
7069
|
for (const step of params.steps) {
|
|
7070
|
+
if (step.namespace !== undefined || step.refNamespace !== undefined || step.kind === "createNamespace")
|
|
7071
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "fixedNamespaces" });
|
|
7072
|
+
if (step.kind === "createIndex" && step.columns.some((column) => column.operatorClass !== undefined))
|
|
7073
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "indexOperatorClasses" });
|
|
6477
7074
|
if (isRlsStep({ step }))
|
|
6478
7075
|
renderRlsStep({ step, dialect: "mysql" });
|
|
6479
7076
|
if (step.kind !== "alterColumn" || !step.changes.some((change) => change === "type" || change === "nullability" || change === "collation"))
|
|
@@ -6483,7 +7080,7 @@ function renderMysqlSteps(params) {
|
|
|
6483
7080
|
return object.dialect === "mysql" && (identity.kind === "columnDefault" || identity.kind === "generatedColumn" || identity.kind === "column") && identity.table.name === step.table && identity.column === step.column;
|
|
6484
7081
|
});
|
|
6485
7082
|
if (owner !== undefined)
|
|
6486
|
-
throw
|
|
7083
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "mysqlOwnedColumnRewrite", hint: `MODIFY COLUMN would overwrite migration-owned ${owner.id}; use a reviewed migration that preserves the complete column definition` });
|
|
6487
7084
|
}
|
|
6488
7085
|
const enums = new Map;
|
|
6489
7086
|
for (const enumDef of params.to?.enums ?? []) {
|
|
@@ -6503,7 +7100,7 @@ function renderMysqlSteps(params) {
|
|
|
6503
7100
|
continue;
|
|
6504
7101
|
const retainedIndex = [...table.indexes.values()].some((index) => index.columns[0]?.name === auto && !params.steps.some((candidate) => candidate.kind === "dropIndex" && candidate.table === step.table && candidate.name === index.name));
|
|
6505
7102
|
if (!retainedIndex)
|
|
6506
|
-
throw
|
|
7103
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "mysqlAutoIncrementKeyRemoval", hint: `Removing the last leading key of AUTO_INCREMENT column "${step.table}.${auto}" requires a reviewed combined ALTER` });
|
|
6507
7104
|
}
|
|
6508
7105
|
let dropCursor = 0;
|
|
6509
7106
|
const inlineIndexes = new Map;
|
|
@@ -6512,7 +7109,7 @@ function renderMysqlSteps(params) {
|
|
|
6512
7109
|
const columns = step.kind === "createTable" ? step.columns : step.kind === "addColumn" ? [step.column] : [];
|
|
6513
7110
|
const counters = columns.filter((field) => field.default?.kind === "autoincrement");
|
|
6514
7111
|
if (counters.length > 1)
|
|
6515
|
-
throw
|
|
7112
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "multiple auto-increment columns" });
|
|
6516
7113
|
for (const column of counters) {
|
|
6517
7114
|
const name = column.dbName ?? column.name;
|
|
6518
7115
|
if (step.kind === "createTable" && step.primaryKey[0] === name)
|
|
@@ -6521,7 +7118,7 @@ function renderMysqlSteps(params) {
|
|
|
6521
7118
|
continue;
|
|
6522
7119
|
const key = params.steps.find((candidate) => candidate.kind === "createIndex" && candidate.table === step.table && candidate.columns[0]?.name === name && !candidate.columns[0]?.desc && candidate.where === undefined && candidate.nullsNotDistinct !== true && (candidate.unique || candidate.indexKind === "btree"));
|
|
6523
7120
|
if (key === undefined)
|
|
6524
|
-
throw
|
|
7121
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: `auto-increment column ${step.table}.${name} without an accompanying leading index` });
|
|
6525
7122
|
inlineIndexes.set(step, key);
|
|
6526
7123
|
consumed.add(key);
|
|
6527
7124
|
}
|
|
@@ -6540,6 +7137,8 @@ function renderMysqlSteps(params) {
|
|
|
6540
7137
|
function renderStep2(params) {
|
|
6541
7138
|
const { step, enums } = params;
|
|
6542
7139
|
switch (step.kind) {
|
|
7140
|
+
case "createNamespace":
|
|
7141
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "fixedNamespaces" });
|
|
6543
7142
|
case "enableRls":
|
|
6544
7143
|
case "forceRls":
|
|
6545
7144
|
case "disableRls":
|
|
@@ -6581,21 +7180,21 @@ ${lines.join(`,
|
|
|
6581
7180
|
return renderAlterColumn2({ step, enums });
|
|
6582
7181
|
case "createIndex": {
|
|
6583
7182
|
if (step.where !== undefined) {
|
|
6584
|
-
throw
|
|
7183
|
+
throw unsupportedCapability12({
|
|
6585
7184
|
dialect: "mysql",
|
|
6586
7185
|
feature: `partial index "${step.name}"`,
|
|
6587
7186
|
hint: "mysql has no partial indexes \u2014 drop the WHERE predicate or gate the index per dialect"
|
|
6588
7187
|
});
|
|
6589
7188
|
}
|
|
6590
7189
|
if (step.nullsNotDistinct === true) {
|
|
6591
|
-
throw
|
|
7190
|
+
throw unsupportedCapability12({
|
|
6592
7191
|
dialect: "mysql",
|
|
6593
7192
|
feature: `UNIQUE NULLS NOT DISTINCT (index "${step.name}")`,
|
|
6594
7193
|
hint: "mysql treats NULLs as distinct in unique indexes and has no switch \u2014 gate the flag per dialect"
|
|
6595
7194
|
});
|
|
6596
7195
|
}
|
|
6597
7196
|
if (!step.unique && step.indexKind !== "btree") {
|
|
6598
|
-
throw
|
|
7197
|
+
throw unsupportedCapability12({
|
|
6599
7198
|
dialect: "mysql",
|
|
6600
7199
|
feature: `index kind "${step.indexKind}" (index "${step.name}")`,
|
|
6601
7200
|
hint: "mysql secondary indexes are btree only"
|
|
@@ -6612,12 +7211,12 @@ ${lines.join(`,
|
|
|
6612
7211
|
];
|
|
6613
7212
|
case "addExclusion":
|
|
6614
7213
|
case "dropExclusion":
|
|
6615
|
-
throw new
|
|
7214
|
+
throw new VibeError33({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "mysql", feature: "exclusionConstraints" } });
|
|
6616
7215
|
case "dropCheck":
|
|
6617
7216
|
return [`ALTER TABLE ${q4(step.table)} DROP CHECK ${q4(step.name)};`];
|
|
6618
7217
|
case "dropIndex": {
|
|
6619
7218
|
if (step.table === undefined) {
|
|
6620
|
-
throw new
|
|
7219
|
+
throw new VibeError33({
|
|
6621
7220
|
code: "VIBE_MIGRATION",
|
|
6622
7221
|
message: `dropIndex "${step.name}" carries no table \u2014 mysql's DROP INDEX requires one`,
|
|
6623
7222
|
meta: { index: step.name }
|
|
@@ -6639,7 +7238,7 @@ ${lines.join(`,
|
|
|
6639
7238
|
case "addForeignKeyNotValid":
|
|
6640
7239
|
case "addCheckNotValid":
|
|
6641
7240
|
case "validateConstraint":
|
|
6642
|
-
throw
|
|
7241
|
+
throw unsupportedCapability12({
|
|
6643
7242
|
dialect: "mysql",
|
|
6644
7243
|
feature: "stagedConstraints",
|
|
6645
7244
|
hint: "MySQL validates a constraint when it is added; add it in one step, or take the outage window that scan needs"
|
|
@@ -6674,7 +7273,7 @@ function columnDefSql3(params) {
|
|
|
6674
7273
|
const name = q4(field.dbName ?? field.name);
|
|
6675
7274
|
const autoincrement = field.default?.kind === "autoincrement";
|
|
6676
7275
|
if (autoincrement && field.type !== "Int" && field.type !== "BigInt") {
|
|
6677
|
-
throw new
|
|
7276
|
+
throw new VibeError33({
|
|
6678
7277
|
code: "VIBE_MIGRATION",
|
|
6679
7278
|
message: `autoincrement default on "${field.name}" requires a non-list Int or BigInt column`,
|
|
6680
7279
|
meta: { field: field.name }
|
|
@@ -6696,7 +7295,7 @@ function renderAlterColumn2(params) {
|
|
|
6696
7295
|
const { step, enums } = params;
|
|
6697
7296
|
assertEnumStringTransition({ from: step.from, to: step.to, dialect: "mysql", table: step.table, column: step.column });
|
|
6698
7297
|
if (step.to.default?.kind === "autoincrement" && step.from.default?.kind !== "autoincrement") {
|
|
6699
|
-
throw new
|
|
7298
|
+
throw new VibeError33({
|
|
6700
7299
|
code: "VIBE_MIGRATION",
|
|
6701
7300
|
message: `cannot add autoincrement to existing column "${step.table}"."${step.column}" \u2014 drop and recreate the column instead`,
|
|
6702
7301
|
meta: { table: step.table, column: step.column }
|
|
@@ -6729,31 +7328,31 @@ function declaredSqlitePreservation(params) {
|
|
|
6729
7328
|
if (params.from === undefined || to === undefined || (params.from.migrationObjects?.length ?? 0) === 0)
|
|
6730
7329
|
return { to, restore, columns };
|
|
6731
7330
|
const invalid = sqliteInvalidatedTables({ steps: params.steps, from: params.from });
|
|
6732
|
-
const retained = new Set((to.migrationObjects ?? []).map((object) =>
|
|
7331
|
+
const retained = new Set((to.migrationObjects ?? []).map((object) => sqlObjectKey10({ identity: object.identity })));
|
|
6733
7332
|
for (const object of params.from.migrationObjects ?? []) {
|
|
6734
7333
|
const identity = object.identity;
|
|
6735
|
-
if (!("table" in identity) || identity.table.namespace !== "main" || !invalid.rebuilt.has(identity.table.name) || !retained.has(
|
|
7334
|
+
if (!("table" in identity) || identity.table.namespace !== "main" || !invalid.rebuilt.has(identity.table.name) || !retained.has(sqlObjectKey10({ identity })))
|
|
6736
7335
|
continue;
|
|
6737
7336
|
const facts = object.expected?.facts;
|
|
6738
7337
|
if (facts === undefined)
|
|
6739
|
-
throw new
|
|
7338
|
+
throw new VibeError34({ code: "VIBE_MIGRATION", message: "SQLite rebuild requires independently declared preservation definitions", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
6740
7339
|
if (identity.kind === "trigger" || identity.kind === "index") {
|
|
6741
7340
|
if (identity.kind === "index" && to.models.some((model) => (model.dbName ?? model.name) === identity.table.name && model.indexes.some((index) => index.name === identity.name)))
|
|
6742
7341
|
continue;
|
|
6743
7342
|
if (typeof facts.definition !== "string")
|
|
6744
|
-
throw new
|
|
7343
|
+
throw new VibeError34({ code: "VIBE_MIGRATION", message: "Missing SQLite restoration SQL", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
6745
7344
|
const statements = restore.get(identity.table.name) ?? [];
|
|
6746
7345
|
statements.push(facts.definition);
|
|
6747
7346
|
restore.set(identity.table.name, statements);
|
|
6748
7347
|
} else if (identity.kind === "column" || identity.kind === "generatedColumn") {
|
|
6749
7348
|
if (typeof facts.columnDefinition !== "string")
|
|
6750
|
-
throw new
|
|
7349
|
+
throw new VibeError34({ code: "VIBE_MIGRATION", message: "Missing SQLite column clause", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
6751
7350
|
const fields = columns.get(identity.table.name) ?? new Map;
|
|
6752
7351
|
fields.set(identity.column, { sql: facts.columnDefinition, generated: Number(facts.generated) > 1 });
|
|
6753
7352
|
columns.set(identity.table.name, fields);
|
|
6754
7353
|
} else if (identity.kind === "columnDefault") {
|
|
6755
7354
|
if (facts.definition !== null && typeof facts.definition !== "string")
|
|
6756
|
-
throw new
|
|
7355
|
+
throw new VibeError34({ code: "VIBE_MIGRATION", message: "Missing SQLite default clause", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
6757
7356
|
const expression = facts.definition;
|
|
6758
7357
|
to = { ...to, models: to.models.map((model) => (model.dbName ?? model.name) !== identity.table.name ? model : { ...model, fields: model.fields.map((field) => {
|
|
6759
7358
|
if ((field.dbName ?? field.name) !== identity.column)
|
|
@@ -7038,10 +7637,10 @@ function assertNativeMigrationMetadata(params) {
|
|
|
7038
7637
|
if (!native)
|
|
7039
7638
|
return;
|
|
7040
7639
|
if (!DIALECT_CAPABILITIES3[params.dialect].nativeRowLevelSecurity) {
|
|
7041
|
-
throw new
|
|
7640
|
+
throw new VibeError35({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Native RLS migrations are unsupported on ${params.dialect}`, meta: { feature: "nativeRowLevelSecurity", dialect: params.dialect } });
|
|
7042
7641
|
}
|
|
7043
7642
|
if (params.migration.rls === undefined) {
|
|
7044
|
-
throw new
|
|
7643
|
+
throw new VibeError35({ 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 } });
|
|
7045
7644
|
}
|
|
7046
7645
|
}
|
|
7047
7646
|
var PG_ADVISORY_LOCK_KEY = 1447645253;
|
|
@@ -7086,11 +7685,11 @@ async function assertForwardStart(params) {
|
|
|
7086
7685
|
const state = params.migration.desiredState;
|
|
7087
7686
|
if (state === undefined)
|
|
7088
7687
|
return;
|
|
7089
|
-
const report = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state.from, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7688
|
+
const report = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state.from, external: params.external, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7090
7689
|
if (report.inSync)
|
|
7091
7690
|
return;
|
|
7092
7691
|
if (state.down !== undefined) {
|
|
7093
|
-
const restored = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state.down, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7692
|
+
const restored = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state.down, external: params.external, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7094
7693
|
if (restored.inSync)
|
|
7095
7694
|
return;
|
|
7096
7695
|
}
|
|
@@ -7122,12 +7721,12 @@ function generateMigration(params) {
|
|
|
7122
7721
|
}),
|
|
7123
7722
|
...orderedMigrationObjects({ objects: to.migrationObjects ?? [] }).flatMap((object) => {
|
|
7124
7723
|
if (object.dialect !== dialect)
|
|
7125
|
-
throw new
|
|
7724
|
+
throw new VibeError35({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Migration object ${object.id} requires ${object.dialect}` });
|
|
7126
7725
|
const previous = (params.desiredState?.from.schema.migrationObjects ?? from.migrationObjects)?.find((entry) => entry.id === object.id);
|
|
7127
7726
|
if (previous === undefined)
|
|
7128
7727
|
return [...object.create];
|
|
7129
7728
|
if (JSON.stringify(previous) !== JSON.stringify(object))
|
|
7130
|
-
throw new
|
|
7729
|
+
throw new VibeError35({ 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 } });
|
|
7131
7730
|
return [];
|
|
7132
7731
|
})
|
|
7133
7732
|
];
|
|
@@ -7153,8 +7752,8 @@ async function withMigrationLock(params) {
|
|
|
7153
7752
|
}, (cause) => releaseLock({ executor, dialect: params.dialect }).then(() => {
|
|
7154
7753
|
throw cause;
|
|
7155
7754
|
}, (cleanupCause) => {
|
|
7156
|
-
if (cause instanceof
|
|
7157
|
-
throw new
|
|
7755
|
+
if (cause instanceof VibeError35)
|
|
7756
|
+
throw new VibeError35({ code: cause.code, message: cause.message, meta: { ...cause.meta, cleanupCause }, cause });
|
|
7158
7757
|
throw cause;
|
|
7159
7758
|
}));
|
|
7160
7759
|
};
|
|
@@ -7178,16 +7777,17 @@ function validateMigrationReplay(params) {
|
|
|
7178
7777
|
}
|
|
7179
7778
|
function assertMigrationMetadata(params) {
|
|
7180
7779
|
const { migration, dialect } = params;
|
|
7780
|
+
assertModuleAdoption({ migration });
|
|
7181
7781
|
if (migration.moduleOrigin !== undefined) {
|
|
7182
7782
|
assertMaterializedModuleMigration({ migration });
|
|
7183
7783
|
assertModuleOwnership({ origin: migration.moduleOrigin, desiredState: migration.desiredState });
|
|
7184
7784
|
}
|
|
7185
7785
|
if (migration.desiredStatePending === true)
|
|
7186
|
-
throw new
|
|
7786
|
+
throw new VibeError35({ 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 } });
|
|
7187
7787
|
if (migration.desiredState !== undefined) {
|
|
7188
7788
|
assertDesiredState({ value: migration.desiredState, migration: migration.name });
|
|
7189
7789
|
if (migration.desiredState.dialect !== dialect)
|
|
7190
|
-
throw new
|
|
7790
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Migration desired-state dialect mismatch", meta: { reason: "desired-state-dialect-mismatch", migration: migration.name } });
|
|
7191
7791
|
}
|
|
7192
7792
|
assertNativeMigrationMetadata({ migration, sql: migration.sql, dialect });
|
|
7193
7793
|
assertExecutable({ migration, execution: migration.execution, sql: migration.sql, dialect });
|
|
@@ -7195,20 +7795,25 @@ function assertMigrationMetadata(params) {
|
|
|
7195
7795
|
async function applyMigrations(params) {
|
|
7196
7796
|
const { executor, migrations } = params;
|
|
7197
7797
|
const dialect = params.dialect ?? "postgres";
|
|
7798
|
+
const trustedReplay = isShadowReplayExecutor({ executor });
|
|
7198
7799
|
if (params.history !== undefined && params.history !== "complete" && params.history !== "subset")
|
|
7199
|
-
throw new
|
|
7800
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Unknown migration history validation mode", meta: { reason: "history-mode-invalid" } });
|
|
7200
7801
|
if (params.history === "complete")
|
|
7201
7802
|
assertDistinctMigrationNames({ migrations });
|
|
7202
7803
|
for (const migration of migrations)
|
|
7203
7804
|
assertMigrationMetadata({ migration, dialect });
|
|
7805
|
+
const externalDeclarations = await parseExternalObjects({ value: params.external ?? [], dialect });
|
|
7204
7806
|
return withMigrationLock({ executor, dialect, run: async (executor) => {
|
|
7807
|
+
const external = externalDeclarations.length === 0 ? undefined : await inspectExternalObjects({ executor, dialect, external: externalDeclarations });
|
|
7205
7808
|
let moduleRecords = await readModuleJournal({ executor, dialect });
|
|
7206
7809
|
const recorded = new Map([...moduleRecords].map(([name, record]) => [name, record.row]));
|
|
7810
|
+
if (external !== undefined)
|
|
7811
|
+
assertExternalOwnershipClaims({ external, identities: [...migrations.flatMap((migration) => migration.moduleOrigin?.owned ?? []), ...[...moduleRecords.values()].flatMap((record) => record.entry?.migration.moduleOrigin?.owned ?? [])] });
|
|
7207
7812
|
if (migrations.some((migration) => migration.moduleOrigin !== undefined) || [...moduleRecords.values()].some((record) => record.entry !== undefined)) {
|
|
7208
7813
|
assertModuleUpgradeHistory({ migrations, records: moduleRecords });
|
|
7209
7814
|
const namespace = await migrationNamespace({ executor, dialect });
|
|
7210
|
-
if (migrations.some((migration) => migration.moduleOrigin !== undefined && moduleNamespace({ origin: migration.moduleOrigin }) !== namespace))
|
|
7211
|
-
throw new
|
|
7815
|
+
if (dialect !== "postgres" && migrations.some((migration) => migration.moduleOrigin !== undefined && moduleNamespace({ origin: migration.moduleOrigin }) !== namespace))
|
|
7816
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Module placement differs from the active database namespace", meta: { reason: "module-placement-mismatch" } });
|
|
7212
7817
|
}
|
|
7213
7818
|
if (params.history === "complete")
|
|
7214
7819
|
assertCompleteHistory({ migrations, recorded, dialect });
|
|
@@ -7217,6 +7822,12 @@ async function applyMigrations(params) {
|
|
|
7217
7822
|
const stored = recorded.get(migration.name);
|
|
7218
7823
|
return stored === undefined || !isCompleteRecord({ dialect, migration, stored });
|
|
7219
7824
|
});
|
|
7825
|
+
const adopted = pending.find((migration) => migration.moduleAdoption !== undefined);
|
|
7826
|
+
if (adopted !== undefined && !trustedReplay)
|
|
7827
|
+
throw new VibeError35({ 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 } });
|
|
7828
|
+
if (external !== undefined && !trustedReplay)
|
|
7829
|
+
for (const migration of pending)
|
|
7830
|
+
await assertExternalMigrationMutation({ executor, migration, dialect, namespace: await migrationNamespace({ executor, dialect }), external });
|
|
7220
7831
|
validateMigrationReplay({ ...params, migrations: pending, dialect });
|
|
7221
7832
|
await ensureMigrationsTable({ executor, dialect });
|
|
7222
7833
|
if (migrations.some((migration) => migration.moduleOrigin !== undefined))
|
|
@@ -7229,7 +7840,7 @@ async function applyMigrations(params) {
|
|
|
7229
7840
|
await assertLiveModuleOwnership({ executor, origin: migration.moduleOrigin, resume: stored !== undefined });
|
|
7230
7841
|
if (stored !== undefined) {
|
|
7231
7842
|
if (stored.statementIndex < 0) {
|
|
7232
|
-
throw new
|
|
7843
|
+
throw new VibeError35({
|
|
7233
7844
|
code: "VIBE_MIGRATION",
|
|
7234
7845
|
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`,
|
|
7235
7846
|
meta: { migration: migration.name, statementIndex: stored.statementIndex }
|
|
@@ -7251,6 +7862,7 @@ async function applyMigrations(params) {
|
|
|
7251
7862
|
migration,
|
|
7252
7863
|
start: stored.statementIndex,
|
|
7253
7864
|
moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }),
|
|
7865
|
+
external,
|
|
7254
7866
|
...params.probe === undefined ? {} : { probe: params.probe },
|
|
7255
7867
|
insertRecord: false,
|
|
7256
7868
|
...stored.error === undefined ? {} : { storedError: stored.error }
|
|
@@ -7266,10 +7878,10 @@ async function applyMigrations(params) {
|
|
|
7266
7878
|
executor,
|
|
7267
7879
|
dialect,
|
|
7268
7880
|
...migration.execution === undefined ? {} : { execution: migration.execution },
|
|
7269
|
-
run: () => runNonTransactional({ executor, dialect, migration, moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }), start: 0, insertRecord: true, ...params.probe === undefined ? {} : { probe: params.probe } })
|
|
7881
|
+
run: () => runNonTransactional({ executor, dialect, migration, external, moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }), start: 0, insertRecord: true, ...params.probe === undefined ? {} : { probe: params.probe } })
|
|
7270
7882
|
});
|
|
7271
7883
|
} else {
|
|
7272
|
-
await runTransactional({ executor, dialect, migration, moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }), ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7884
|
+
await runTransactional({ executor, dialect, migration, external, moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }), ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7273
7885
|
}
|
|
7274
7886
|
if (migration.moduleOrigin !== undefined)
|
|
7275
7887
|
moduleRecords = await readModuleJournal({ executor, dialect });
|
|
@@ -7277,10 +7889,11 @@ async function applyMigrations(params) {
|
|
|
7277
7889
|
}
|
|
7278
7890
|
const final = migrations.at(-1);
|
|
7279
7891
|
if (final?.desiredState !== undefined) {
|
|
7892
|
+
const finalExternal = externalDeclarations.length === 0 ? undefined : await inspectExternalObjects({ executor, dialect, external: externalDeclarations });
|
|
7280
7893
|
if (params.probe === "rollback")
|
|
7281
7894
|
await executor({ text: "BEGIN" });
|
|
7282
7895
|
try {
|
|
7283
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: final.desiredState.from, to: final.desiredState.to, removals: final
|
|
7896
|
+
assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: final.desiredState.from, to: final.desiredState.to, external: finalExternal, removals: comparisonRemovals({ migration: final, external: finalExternal }), ...params.probe === undefined ? {} : { probe: params.probe } }) });
|
|
7284
7897
|
} finally {
|
|
7285
7898
|
if (params.probe === "rollback")
|
|
7286
7899
|
await executor({ text: "ROLLBACK" });
|
|
@@ -7289,6 +7902,27 @@ async function applyMigrations(params) {
|
|
|
7289
7902
|
return { applied, skipped };
|
|
7290
7903
|
} });
|
|
7291
7904
|
}
|
|
7905
|
+
function comparisonRemovals(params) {
|
|
7906
|
+
return params.migration.desiredState?.unmanagedRemovals?.filter((identity) => params.external === undefined || externalObjectOwner({ identity, external: params.external }) === undefined);
|
|
7907
|
+
}
|
|
7908
|
+
async function assertExternalMigrationMutation(params) {
|
|
7909
|
+
assertExternalOwnershipClaims({ external: params.external, identities: params.migration.desiredState?.unmanagedRemovals ?? [] });
|
|
7910
|
+
const state = params.migration.desiredState;
|
|
7911
|
+
if (state === undefined)
|
|
7912
|
+
return;
|
|
7913
|
+
const identities = [];
|
|
7914
|
+
const steps = diffSchemas({ from: state.from.schema, to: state.to.schema, dialect: params.dialect });
|
|
7915
|
+
for (const step of steps) {
|
|
7916
|
+
if (step.kind === "createNamespace")
|
|
7917
|
+
identities.push({ kind: "namespace", name: step.namespace });
|
|
7918
|
+
else if ("table" in step && typeof step.table === "string")
|
|
7919
|
+
identities.push({ kind: "table", name: { namespace: step.namespace ?? params.namespace, name: step.table } });
|
|
7920
|
+
else if ("enumName" in step)
|
|
7921
|
+
identities.push({ kind: "enum", name: { namespace: step.namespace ?? params.namespace, name: step.enumName } });
|
|
7922
|
+
}
|
|
7923
|
+
assertExternalOwnershipClaims({ external: params.external, identities });
|
|
7924
|
+
await refuseExternalDependents({ executor: params.executor, dialect: params.dialect, external: params.external, steps, namespace: params.namespace });
|
|
7925
|
+
}
|
|
7292
7926
|
async function migrationStatus(params) {
|
|
7293
7927
|
const { executor, migrations } = params;
|
|
7294
7928
|
const dialect = params.dialect ?? "postgres";
|
|
@@ -7364,7 +7998,7 @@ async function repairMigration(params) {
|
|
|
7364
7998
|
const { executor, name, dialect } = params;
|
|
7365
7999
|
const moduleRecord = (await readModuleJournal({ executor, dialect })).get(name);
|
|
7366
8000
|
if (moduleRecord?.entry !== undefined)
|
|
7367
|
-
throw new
|
|
8001
|
+
throw new VibeError35({ 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 } });
|
|
7368
8002
|
await ensureMigrationsTable({ executor, dialect });
|
|
7369
8003
|
if (params.as === "applied") {
|
|
7370
8004
|
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})`;
|
|
@@ -7386,7 +8020,7 @@ async function rollbackMigration(params) {
|
|
|
7386
8020
|
const stored = assertRollbackRecord({ migration, stored: recorded.get(migration.name), dialect });
|
|
7387
8021
|
const newest = [...recorded.keys()].at(-1);
|
|
7388
8022
|
if (newest !== migration.name)
|
|
7389
|
-
throw new
|
|
8023
|
+
throw new VibeError35({ 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 } });
|
|
7390
8024
|
await validateRollbackRecovery({ executor, migration, downSql, stored, dialect });
|
|
7391
8025
|
await executeRollback({ ...params, executor, dialect, downSql, stored });
|
|
7392
8026
|
} });
|
|
@@ -7404,7 +8038,7 @@ async function rollbackMigrations(params) {
|
|
|
7404
8038
|
const target = journal.findIndex((row) => row.name === params.to);
|
|
7405
8039
|
const row = journal[target];
|
|
7406
8040
|
if (row?.state !== "applied")
|
|
7407
|
-
throw new
|
|
8041
|
+
throw new VibeError35({ 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 } });
|
|
7408
8042
|
assertMigrationMetadata({ migration: byName.get(row.name), dialect });
|
|
7409
8043
|
start = target + 1;
|
|
7410
8044
|
}
|
|
@@ -7412,7 +8046,7 @@ async function rollbackMigrations(params) {
|
|
|
7412
8046
|
for (const row of journal.slice(start).reverse()) {
|
|
7413
8047
|
const migration = byName.get(row.name);
|
|
7414
8048
|
if (migration === undefined)
|
|
7415
|
-
throw new
|
|
8049
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Cannot roll back "${row.name}" without its migration file`, meta: { reason: "rollback-file-missing", migration: row.name } });
|
|
7416
8050
|
const stored = assertRollbackRecord({ migration, stored: recorded.get(row.name), dialect });
|
|
7417
8051
|
const downSql = validateRollbackFile({ ...params, migration, dialect });
|
|
7418
8052
|
plan.push({ migration, downSql, stored });
|
|
@@ -7429,20 +8063,23 @@ async function rollbackMigrations(params) {
|
|
|
7429
8063
|
}
|
|
7430
8064
|
function validateRollbackFile(params) {
|
|
7431
8065
|
const { migration, dialect } = params;
|
|
8066
|
+
assertModuleAdoption({ migration });
|
|
8067
|
+
if (migration.moduleAdoption !== undefined)
|
|
8068
|
+
throw new VibeError35({ 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 } });
|
|
7432
8069
|
if (migration.desiredStatePending === true)
|
|
7433
|
-
throw new
|
|
8070
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Unfinalized desired state cannot be rolled back", meta: { reason: "desired-state-unfinalized", migration: migration.name } });
|
|
7434
8071
|
if (migration.moduleOrigin !== undefined) {
|
|
7435
8072
|
assertMaterializedModuleMigration({ migration });
|
|
7436
8073
|
assertModuleOwnership({ origin: migration.moduleOrigin, desiredState: migration.desiredState });
|
|
7437
8074
|
}
|
|
7438
8075
|
const downSql = migration.downSql;
|
|
7439
8076
|
if (downSql === undefined)
|
|
7440
|
-
throw new
|
|
8077
|
+
throw new VibeError35({ 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 } });
|
|
7441
8078
|
assertProbeMode({ dialect, probe: params.probe });
|
|
7442
8079
|
if (migration.desiredState !== undefined) {
|
|
7443
8080
|
assertDesiredState({ value: migration.desiredState, migration: migration.name });
|
|
7444
8081
|
if (migration.desiredState.dialect !== dialect)
|
|
7445
|
-
throw new
|
|
8082
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Migration desired-state dialect mismatch", meta: { reason: "desired-state-dialect-mismatch" } });
|
|
7446
8083
|
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 } });
|
|
7447
8084
|
}
|
|
7448
8085
|
assertNativeMigrationMetadata({ migration, sql: downSql, dialect });
|
|
@@ -7455,10 +8092,10 @@ function validateRollbackFile(params) {
|
|
|
7455
8092
|
function assertRollbackRecord(params) {
|
|
7456
8093
|
const { migration, stored, dialect } = params;
|
|
7457
8094
|
if (stored === undefined)
|
|
7458
|
-
throw new
|
|
8095
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `migration "${migration.name}" is not recorded as applied; nothing to roll back`, meta: { migration: migration.name } });
|
|
7459
8096
|
assertChecksumMatches({ migration, stored: stored.checksum });
|
|
7460
8097
|
if (stored.statementIndex >= 0 && (!isCompleteRecord({ dialect, migration, stored }) || stored.error !== undefined))
|
|
7461
|
-
throw new
|
|
8098
|
+
throw new VibeError35({ 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 } });
|
|
7462
8099
|
return stored;
|
|
7463
8100
|
}
|
|
7464
8101
|
async function validateRollbackRecovery(params) {
|
|
@@ -7467,7 +8104,7 @@ async function validateRollbackRecovery(params) {
|
|
|
7467
8104
|
return;
|
|
7468
8105
|
const start = -stored.statementIndex - 1;
|
|
7469
8106
|
if (start > downSql.length)
|
|
7470
|
-
throw new
|
|
8107
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Rollback progress exceeds its statement count", meta: { reason: "rollback-progress-invalid", migration: migration.name } });
|
|
7471
8108
|
if (dialect === "sqlite" && usesSelfManagedTransactions({ sql: downSql }))
|
|
7472
8109
|
assertSelfManagedResumeSafe({ sql: downSql, start, migration: migration.name, direction: "down" });
|
|
7473
8110
|
if (!marksInflight({ dialect, execution: migration.downExecution }) || start >= downSql.length)
|
|
@@ -7502,9 +8139,9 @@ async function runTransactionalDown(params) {
|
|
|
7502
8139
|
const limits = migration.downExecution;
|
|
7503
8140
|
if (wrap && dialect === "postgres" && limits !== undefined) {
|
|
7504
8141
|
if ((limits.lockTimeoutMs ?? 0) > 0)
|
|
7505
|
-
await executor({ text: renderMigrationSessionSet({ dialect:
|
|
8142
|
+
await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect9({ dialect }), setting: "lock_timeout", milliseconds: limits.lockTimeoutMs, local: true }).text });
|
|
7506
8143
|
if ((limits.statementTimeoutMs ?? 0) > 0)
|
|
7507
|
-
await executor({ text: renderMigrationSessionSet({ dialect:
|
|
8144
|
+
await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect9({ dialect }), setting: "statement_timeout", milliseconds: limits.statementTimeoutMs, local: true }).text });
|
|
7508
8145
|
}
|
|
7509
8146
|
if (migration.desiredState !== undefined)
|
|
7510
8147
|
assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.desiredState.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } }) });
|
|
@@ -7533,7 +8170,7 @@ async function runTransactionalDown(params) {
|
|
|
7533
8170
|
} else {
|
|
7534
8171
|
await cleanupSelfManagedFailure({ executor });
|
|
7535
8172
|
}
|
|
7536
|
-
throw new
|
|
8173
|
+
throw new VibeError35({
|
|
7537
8174
|
code: "VIBE_MIGRATION",
|
|
7538
8175
|
message: `rollback of "${migration.name}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
7539
8176
|
meta: { migration: migration.name, ...timeoutDiagnostic({ error }) },
|
|
@@ -7616,7 +8253,7 @@ async function runNonTransactionalDown(params) {
|
|
|
7616
8253
|
values: [marks ? `${INFLIGHT_PREFIX}${index}` : error instanceof Error ? error.message : String(error), migration.name]
|
|
7617
8254
|
});
|
|
7618
8255
|
} catch {}
|
|
7619
|
-
throw new
|
|
8256
|
+
throw new VibeError35({
|
|
7620
8257
|
code: "VIBE_MIGRATION",
|
|
7621
8258
|
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."}`,
|
|
7622
8259
|
meta: { migration: migration.name, statementIndex: index, ...marks ? { outcome: "unknown" } : {}, ...timeoutDiagnostic({ error }) },
|
|
@@ -7643,11 +8280,11 @@ async function runTransactional(params) {
|
|
|
7643
8280
|
const limits = migration.execution;
|
|
7644
8281
|
if (wrap && dialect === "postgres" && limits !== undefined) {
|
|
7645
8282
|
if ((limits.lockTimeoutMs ?? 0) > 0)
|
|
7646
|
-
await executor({ text: renderMigrationSessionSet({ dialect:
|
|
8283
|
+
await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect9({ dialect }), setting: "lock_timeout", milliseconds: limits.lockTimeoutMs, local: true }).text });
|
|
7647
8284
|
if ((limits.statementTimeoutMs ?? 0) > 0)
|
|
7648
|
-
await executor({ text: renderMigrationSessionSet({ dialect:
|
|
8285
|
+
await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect9({ dialect }), setting: "statement_timeout", milliseconds: limits.statementTimeoutMs, local: true }).text });
|
|
7649
8286
|
}
|
|
7650
|
-
await assertForwardStart({ executor, dialect, migration, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
8287
|
+
await assertForwardStart({ executor, dialect, migration, external: params.external, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7651
8288
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
7652
8289
|
if (migration.rls !== undefined)
|
|
7653
8290
|
await introspectPostgres({ executor, rlsSchema: migration.rls.from, inTransaction: true, requireInstalledRls: true });
|
|
@@ -7657,7 +8294,7 @@ async function runTransactional(params) {
|
|
|
7657
8294
|
statementIndex += 1;
|
|
7658
8295
|
}
|
|
7659
8296
|
if (migration.desiredState !== undefined)
|
|
7660
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.
|
|
8297
|
+
assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, external: params.external, removals: comparisonRemovals({ migration, external: params.external }), dialect, ...params.probe === undefined ? {} : { probe: params.probe } }) });
|
|
7661
8298
|
if (migration.rls !== undefined)
|
|
7662
8299
|
await introspectPostgres({ executor, rlsSchema: migration.rls.to, inTransaction: true, requireInstalledRls: true });
|
|
7663
8300
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
@@ -7678,7 +8315,7 @@ async function runTransactional(params) {
|
|
|
7678
8315
|
} else {
|
|
7679
8316
|
await cleanupSelfManagedFailure({ executor });
|
|
7680
8317
|
}
|
|
7681
|
-
throw new
|
|
8318
|
+
throw new VibeError35({
|
|
7682
8319
|
code: "VIBE_MIGRATION",
|
|
7683
8320
|
message: `migration "${migration.name}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
7684
8321
|
meta: { migration: migration.name },
|
|
@@ -7701,9 +8338,9 @@ async function runNonTransactional(params) {
|
|
|
7701
8338
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
7702
8339
|
if (insertRecord) {
|
|
7703
8340
|
if (migration.desiredState !== undefined) {
|
|
7704
|
-
await assertForwardStart({ executor, dialect, migration, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
8341
|
+
await assertForwardStart({ executor, dialect, migration, external: params.external, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7705
8342
|
if (migration.sql.length === 0)
|
|
7706
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.
|
|
8343
|
+
assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, external: params.external, removals: comparisonRemovals({ migration, external: params.external }), dialect, ...params.probe === undefined ? {} : { probe: params.probe } }) });
|
|
7707
8344
|
}
|
|
7708
8345
|
if (migration.moduleOrigin !== undefined)
|
|
7709
8346
|
await insertModuleJournal({ executor, dialect, migration, index: 0, complete: false });
|
|
@@ -7726,7 +8363,7 @@ async function runNonTransactional(params) {
|
|
|
7726
8363
|
const state = await establishOperationState({ executor, dialect, operation });
|
|
7727
8364
|
if (state === "satisfied") {
|
|
7728
8365
|
if (start + 1 === migration.sql.length && migration.desiredState !== undefined)
|
|
7729
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.
|
|
8366
|
+
assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.from, to: migration.desiredState.to, external: params.external, removals: comparisonRemovals({ migration, external: params.external }), ...params.probe === undefined ? {} : { probe: params.probe } }) });
|
|
7730
8367
|
if (start + 1 === migration.sql.length)
|
|
7731
8368
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
7732
8369
|
await checkpoint(start + 1);
|
|
@@ -7756,7 +8393,7 @@ async function runNonTransactional(params) {
|
|
|
7756
8393
|
}
|
|
7757
8394
|
await executeStatement({ executor, statement: migration.sql[index], dialect });
|
|
7758
8395
|
if (index === migration.sql.length - 1 && migration.desiredState !== undefined)
|
|
7759
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.
|
|
8396
|
+
assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, external: params.external, removals: comparisonRemovals({ migration, external: params.external }), dialect, ...params.probe === undefined ? {} : { probe: params.probe } }) });
|
|
7760
8397
|
if (index === migration.sql.length - 1)
|
|
7761
8398
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
7762
8399
|
if (atomic) {
|
|
@@ -7774,7 +8411,7 @@ async function runNonTransactional(params) {
|
|
|
7774
8411
|
} catch {}
|
|
7775
8412
|
const sqlState = sqlStateOf(error);
|
|
7776
8413
|
const timeout = sqlState === "55P03" ? "lock-timeout" : sqlState === "57014" ? "statement-timeout" : undefined;
|
|
7777
|
-
throw new
|
|
8414
|
+
throw new VibeError35({
|
|
7778
8415
|
code: "VIBE_MIGRATION",
|
|
7779
8416
|
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."}`,
|
|
7780
8417
|
meta: { migration: migration.name, statementIndex: index, ...marks ? { outcome: "unknown" } : {}, ...timeout === undefined ? {} : { reason: timeout, sqlState } },
|
|
@@ -7785,7 +8422,7 @@ async function runNonTransactional(params) {
|
|
|
7785
8422
|
await checkpoint(index + 1);
|
|
7786
8423
|
}
|
|
7787
8424
|
if (migration.moduleOrigin !== undefined) {
|
|
7788
|
-
assertMigrationState({ report: await verifyMigrationState({ executor, dialect, expected: migration.desiredState.to }) });
|
|
8425
|
+
assertMigrationState({ report: await verifyMigrationState({ executor, dialect, expected: migration.desiredState.to, external: params.external }) });
|
|
7789
8426
|
await completeModuleJournal({ executor, dialect, migration });
|
|
7790
8427
|
}
|
|
7791
8428
|
}
|
|
@@ -7802,7 +8439,7 @@ async function acquireLock(params) {
|
|
|
7802
8439
|
});
|
|
7803
8440
|
const acquired = Number(rows[0]?.vibe_lock) === 1;
|
|
7804
8441
|
if (!acquired) {
|
|
7805
|
-
throw new
|
|
8442
|
+
throw new VibeError35({
|
|
7806
8443
|
code: "VIBE_MIGRATION",
|
|
7807
8444
|
message: `could not acquire the migration lock ('${MYSQL_LOCK_NAME}') within ${MYSQL_LOCK_TIMEOUT_SECONDS}s \u2014 another migration run is in progress`,
|
|
7808
8445
|
meta: { lock: MYSQL_LOCK_NAME }
|
|
@@ -7837,7 +8474,7 @@ function uncertainStatement(params) {
|
|
|
7837
8474
|
const subject = direction === "down" ? `rollback of "${migration}"` : `migration "${migration}"`;
|
|
7838
8475
|
const position = `${direction === "down" ? "down statement" : "statement"} ${index + 1} of ${statements.length}`;
|
|
7839
8476
|
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.";
|
|
7840
|
-
return new
|
|
8477
|
+
return new VibeError35({
|
|
7841
8478
|
code: "VIBE_MIGRATION",
|
|
7842
8479
|
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)}`,
|
|
7843
8480
|
meta: {
|
|
@@ -7855,7 +8492,7 @@ function assertExecutable(params) {
|
|
|
7855
8492
|
if (execution === undefined || isDefaultExecution({ execution }))
|
|
7856
8493
|
return;
|
|
7857
8494
|
if (dialect !== "postgres") {
|
|
7858
|
-
throw new
|
|
8495
|
+
throw new VibeError35({
|
|
7859
8496
|
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
7860
8497
|
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`,
|
|
7861
8498
|
meta: { reason: "nontransactional-unsupported", dialect, migration: migration.name, feature: "nontransactionalMigrations" }
|
|
@@ -7878,7 +8515,7 @@ async function withSessionLimits(params) {
|
|
|
7878
8515
|
const statement = execution?.statementTimeoutMs ?? 0;
|
|
7879
8516
|
if (dialect !== "postgres" || lock === 0 && statement === 0)
|
|
7880
8517
|
return params.run();
|
|
7881
|
-
const sqlDialect =
|
|
8518
|
+
const sqlDialect = getSqlDialect9({ dialect });
|
|
7882
8519
|
const previous = (await executor({ text: renderMigrationSessionRead({ dialect: sqlDialect }).text, values: [] }))[0];
|
|
7883
8520
|
if (lock > 0)
|
|
7884
8521
|
await executor({ text: renderMigrationSessionSet({ dialect: sqlDialect, setting: "lock_timeout", milliseconds: lock }).text });
|
|
@@ -7931,7 +8568,7 @@ function assertChecksumMatches(params) {
|
|
|
7931
8568
|
const actual = checksumOf({ migration });
|
|
7932
8569
|
if (stored === actual)
|
|
7933
8570
|
return;
|
|
7934
|
-
throw new
|
|
8571
|
+
throw new VibeError35({
|
|
7935
8572
|
code: "VIBE_MIGRATION",
|
|
7936
8573
|
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)`,
|
|
7937
8574
|
meta: { migration: migration.name, recorded: stored, current: actual }
|
|
@@ -7942,7 +8579,7 @@ function assertResumable(params) {
|
|
|
7942
8579
|
const actual = checksumOf({ migration });
|
|
7943
8580
|
if (stored.checksum === actual)
|
|
7944
8581
|
return;
|
|
7945
|
-
throw new
|
|
8582
|
+
throw new VibeError35({
|
|
7946
8583
|
code: "VIBE_MIGRATION",
|
|
7947
8584
|
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`,
|
|
7948
8585
|
meta: {
|
|
@@ -7962,7 +8599,7 @@ function assertDistinctMigrationNames(params) {
|
|
|
7962
8599
|
const names = new Set;
|
|
7963
8600
|
for (const migration of params.migrations) {
|
|
7964
8601
|
if (names.has(migration.name))
|
|
7965
|
-
throw new
|
|
8602
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Duplicate migration file identity "${migration.name}"`, meta: { reason: "history-duplicate-file", migration: migration.name } });
|
|
7966
8603
|
names.add(migration.name);
|
|
7967
8604
|
}
|
|
7968
8605
|
}
|
|
@@ -7970,15 +8607,15 @@ function assertCompleteHistory(params) {
|
|
|
7970
8607
|
const byName = new Map(params.migrations.map((migration) => [migration.name, migration]));
|
|
7971
8608
|
for (const name of params.recorded.keys()) {
|
|
7972
8609
|
if (!byName.has(name))
|
|
7973
|
-
throw new
|
|
8610
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Recorded migration "${name}" has no file in the complete chain`, meta: { reason: "history-unknown-file", migration: name } });
|
|
7974
8611
|
}
|
|
7975
8612
|
let index = 0;
|
|
7976
8613
|
for (const [name, stored] of params.recorded) {
|
|
7977
8614
|
const migration = params.migrations[index];
|
|
7978
8615
|
if (migration?.name !== name)
|
|
7979
|
-
throw new
|
|
8616
|
+
throw new VibeError35({ 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 } });
|
|
7980
8617
|
if (stored.statementIndex >= 0 && !isCompleteRecord({ dialect: params.dialect, migration, stored }) && index !== params.recorded.size - 1)
|
|
7981
|
-
throw new
|
|
8618
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Partial migration "${name}" precedes later recorded work`, meta: { reason: "history-partial-not-last", migration: name } });
|
|
7982
8619
|
index++;
|
|
7983
8620
|
}
|
|
7984
8621
|
}
|
|
@@ -7987,16 +8624,16 @@ async function assertApplyHistory(params) {
|
|
|
7987
8624
|
const byName = new Map(migrations.map((migration) => [migration.name, migration]));
|
|
7988
8625
|
for (const [name, stored] of recorded) {
|
|
7989
8626
|
if (stored.statementIndex < 0) {
|
|
7990
|
-
throw new
|
|
8627
|
+
throw new VibeError35({ 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 } });
|
|
7991
8628
|
}
|
|
7992
8629
|
const migration = byName.get(name);
|
|
7993
8630
|
if (migration === undefined) {
|
|
7994
8631
|
if (stored.error !== undefined)
|
|
7995
|
-
throw new
|
|
8632
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Migration "${name}" has an unresolved journal error but no supplied file`, meta: { reason: "history-unknown-dirty", migration: name } });
|
|
7996
8633
|
continue;
|
|
7997
8634
|
}
|
|
7998
8635
|
if (isCompleteRecord({ dialect, migration, stored }) && stored.error !== undefined)
|
|
7999
|
-
throw new
|
|
8636
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Migration "${name}" has an unresolved journal error`, meta: { reason: "history-dirty", migration: name } });
|
|
8000
8637
|
if (isCompleteRecord({ dialect, migration, stored }))
|
|
8001
8638
|
assertChecksumMatches({ migration, stored: stored.checksum });
|
|
8002
8639
|
else
|
|
@@ -8021,10 +8658,10 @@ async function assertApplyHistory(params) {
|
|
|
8021
8658
|
}
|
|
8022
8659
|
}
|
|
8023
8660
|
function q5(dialect, name) {
|
|
8024
|
-
return
|
|
8661
|
+
return getSqlDialect9({ dialect }).quoteIdent({ name });
|
|
8025
8662
|
}
|
|
8026
8663
|
function ph(dialect, index) {
|
|
8027
|
-
return
|
|
8664
|
+
return getSqlDialect9({ dialect }).placeholder({ index });
|
|
8028
8665
|
}
|
|
8029
8666
|
|
|
8030
8667
|
// packages/migrate/src/fs.ts
|
|
@@ -8032,7 +8669,7 @@ function absent(error) {
|
|
|
8032
8669
|
return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
8033
8670
|
}
|
|
8034
8671
|
function failure(params) {
|
|
8035
|
-
return new
|
|
8672
|
+
return new VibeError36({ code: "VIBE_MIGRATION", message: `Cannot read migration file ${params.path}`, meta: { reason: "migration-file-unreadable", path: params.path }, cause: params.cause });
|
|
8036
8673
|
}
|
|
8037
8674
|
async function optionalFile(params) {
|
|
8038
8675
|
return readFile(params.path, "utf-8").catch((cause) => {
|
|
@@ -8046,13 +8683,13 @@ function rlsMetadata(params) {
|
|
|
8046
8683
|
const metadata = JSON.parse(params.text);
|
|
8047
8684
|
const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
8048
8685
|
if (metadata?.version !== 1 || typeof metadata.destructive !== "boolean" || metadata.rls === undefined || !strings(metadata.securityChanges) || !strings(metadata.downSecurityChanges) || metadata.sqlChecksum !== migrationChecksum({ sql: params.migration.sql }) || metadata.downChecksum !== migrationChecksum({ sql: params.migration.downSql ?? [] })) {
|
|
8049
|
-
throw new
|
|
8686
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Invalid native RLS metadata" });
|
|
8050
8687
|
}
|
|
8051
8688
|
assertValidSchemaIR4({ schema: metadata.rls.from });
|
|
8052
8689
|
assertValidSchemaIR4({ schema: metadata.rls.to });
|
|
8053
8690
|
return { ...params.migration, rls: metadata.rls, destructive: metadata.destructive, securityChanges: metadata.securityChanges, downSecurityChanges: metadata.downSecurityChanges };
|
|
8054
8691
|
}).catch((cause) => {
|
|
8055
|
-
throw new
|
|
8692
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Invalid native RLS metadata for migration "${params.migration.name}"`, meta: { reason: "rls-migration-metadata-invalid", migration: params.migration.name }, cause });
|
|
8056
8693
|
});
|
|
8057
8694
|
}
|
|
8058
8695
|
async function loadMigrationFiles(params) {
|
|
@@ -8085,13 +8722,17 @@ async function loadMigrationFiles(params) {
|
|
|
8085
8722
|
const desired = await optionalFile({ path: join(directory, DESIRED_STATE_METADATA.filename) });
|
|
8086
8723
|
if (desired !== undefined) {
|
|
8087
8724
|
const pending = await Promise.resolve().then(() => isUnfinalizedDesiredState({ text: desired })).catch((cause) => {
|
|
8088
|
-
throw new
|
|
8725
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Malformed desired state for ${name}`, meta: { reason: "desired-state-invalid", migration: name }, cause });
|
|
8089
8726
|
});
|
|
8090
8727
|
migration = pending ? { ...migration, desiredStatePending: true } : { ...migration, desiredState: parseDesiredStateSidecar({ text: desired, migration: name }) };
|
|
8091
8728
|
}
|
|
8092
8729
|
const origin = await optionalFile({ path: join(directory, MODULE_ORIGIN_METADATA.filename) });
|
|
8093
8730
|
if (origin !== undefined)
|
|
8094
8731
|
migration = restoreModuleMigration({ migration, origin: parseModuleOrigin({ text: origin }) });
|
|
8732
|
+
const adoption = await optionalFile({ path: join(directory, MODULE_ADOPTION_METADATA.filename) });
|
|
8733
|
+
if (adoption !== undefined)
|
|
8734
|
+
migration = { ...migration, moduleAdoption: parseModuleAdoption({ text: adoption }) };
|
|
8735
|
+
assertModuleAdoption({ migration });
|
|
8095
8736
|
migrations.push(migration);
|
|
8096
8737
|
}
|
|
8097
8738
|
return migrations;
|
|
@@ -8102,4 +8743,4 @@ export {
|
|
|
8102
8743
|
splitSqlStatements
|
|
8103
8744
|
};
|
|
8104
8745
|
|
|
8105
|
-
//# debugId=
|
|
8746
|
+
//# debugId=000F8CFDEDF957F264756E2164756E21
|