@vibeorm/migrate 2.4.0 → 2.6.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/artifact-probe.d.ts +41 -0
- package/dist/artifact-probe.d.ts.map +1 -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 +21 -3
- 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/extensions.d.ts +52 -0
- package/dist/extensions.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 +1293 -651
- package/dist/fs.js.map +35 -31
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5424 -4300
- package/dist/index.js.map +57 -50
- 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 +15 -2
- 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 +33 -0
- package/dist/planner.d.ts.map +1 -1
- package/dist/postgres-types.d.ts.map +1 -1
- package/dist/push.d.ts +14 -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",
|
|
@@ -272,7 +275,8 @@ function columnTypeKey(params) {
|
|
|
272
275
|
return `u:${JSON.stringify([namespace ?? null, parsed.name, parsed.modifier, parsed.array || field.isList])}`;
|
|
273
276
|
}
|
|
274
277
|
if (isEnumType(field.type)) {
|
|
275
|
-
|
|
278
|
+
const identity = qualifiedSqlName2({ name: field.type.enum, namespace: field.type.namespace });
|
|
279
|
+
return `e:${JSON.stringify([identity.namespace, identity.name])}${listSuffix}`;
|
|
276
280
|
}
|
|
277
281
|
if (field.nativeType !== undefined) {
|
|
278
282
|
const normalized = normalizeNativeType({ raw: field.nativeType });
|
|
@@ -309,7 +313,7 @@ function columnTypeSql(params) {
|
|
|
309
313
|
}
|
|
310
314
|
const listSuffix = field.isList ? "[]" : "";
|
|
311
315
|
if (isEnumType(field.type)) {
|
|
312
|
-
return `${postgresDialect.quoteIdent({ name: "public" })}.${postgresDialect.quoteIdent({ name: field.type.enum })}${listSuffix}`;
|
|
316
|
+
return `${postgresDialect.quoteIdent({ name: field.type.namespace ?? "public" })}.${postgresDialect.quoteIdent({ name: field.type.enum })}${listSuffix}`;
|
|
313
317
|
}
|
|
314
318
|
if (field.nativeType !== undefined) {
|
|
315
319
|
const normalized = normalizeNativeType({ raw: field.nativeType });
|
|
@@ -426,18 +430,18 @@ function quotePgString(params) {
|
|
|
426
430
|
}
|
|
427
431
|
|
|
428
432
|
// packages/migrate/src/sqlite-types.ts
|
|
429
|
-
import { VibeError as VibeError3, isEnumType as isEnumType3, unsupportedCapability as
|
|
433
|
+
import { VibeError as VibeError3, isEnumType as isEnumType3, unsupportedCapability as unsupportedCapability3 } from "@vibeorm/schema";
|
|
430
434
|
import { ParamCollector, sqliteDialect } from "@vibeorm/sql";
|
|
431
435
|
|
|
432
436
|
// packages/migrate/src/enum-values.ts
|
|
433
|
-
import { isEnumType as isEnumType2, unsupportedCapability } from "@vibeorm/schema";
|
|
437
|
+
import { isEnumType as isEnumType2, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
|
|
434
438
|
function resolveEnumValues(params) {
|
|
435
439
|
const { name, enums } = params;
|
|
436
440
|
const direct = enums.get(name);
|
|
437
441
|
if (direct !== undefined)
|
|
438
442
|
return direct.values;
|
|
439
443
|
for (const enumDef of enums.values()) {
|
|
440
|
-
if (enumDef.name === name)
|
|
444
|
+
if ((enumDef.name === name || enumDef.dbName === name) && (params.namespace === undefined || (enumDef.namespace ?? "public") === params.namespace))
|
|
441
445
|
return enumDef.values;
|
|
442
446
|
}
|
|
443
447
|
return;
|
|
@@ -452,7 +456,7 @@ function assertEnumStringTransition(params) {
|
|
|
452
456
|
return;
|
|
453
457
|
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
458
|
if (reason !== undefined) {
|
|
455
|
-
throw
|
|
459
|
+
throw unsupportedCapability2({ dialect, feature: `enum storage conversion on "${table}"."${column}"`, hint: reason });
|
|
456
460
|
}
|
|
457
461
|
}
|
|
458
462
|
|
|
@@ -469,7 +473,7 @@ var SCALAR_TO_SQLITE = {
|
|
|
469
473
|
Bytes: "BLOB"
|
|
470
474
|
};
|
|
471
475
|
function refuseSqliteScalarList(params) {
|
|
472
|
-
throw
|
|
476
|
+
throw unsupportedCapability3({
|
|
473
477
|
dialect: "sqlite",
|
|
474
478
|
feature: `scalar list column "${params.field.name}"`,
|
|
475
479
|
hint: "sqlite has no array column type \u2014 model the list as a separate table or a Json column"
|
|
@@ -605,7 +609,7 @@ function parseSqliteEnumChecks(params) {
|
|
|
605
609
|
}
|
|
606
610
|
|
|
607
611
|
// packages/migrate/src/mysql-types.ts
|
|
608
|
-
import { VibeError as VibeError4, isEnumType as isEnumType4, unsupportedCapability as
|
|
612
|
+
import { VibeError as VibeError4, isEnumType as isEnumType4, unsupportedCapability as unsupportedCapability4 } from "@vibeorm/schema";
|
|
609
613
|
import { mysqlDialect } from "@vibeorm/sql";
|
|
610
614
|
var SCALAR_TO_MYSQL = {
|
|
611
615
|
String: `varchar(${mysqlDialect.conflictKeyEquality.defaultStringMaxLength})`,
|
|
@@ -619,7 +623,7 @@ var SCALAR_TO_MYSQL = {
|
|
|
619
623
|
Bytes: "longblob"
|
|
620
624
|
};
|
|
621
625
|
function refuseMysqlScalarList(params) {
|
|
622
|
-
throw
|
|
626
|
+
throw unsupportedCapability4({
|
|
623
627
|
dialect: "mysql",
|
|
624
628
|
feature: `scalar list column "${params.field.name}"`,
|
|
625
629
|
hint: "mysql has no array column type \u2014 model the list as a separate table or a JSON column"
|
|
@@ -691,7 +695,7 @@ function mysqlColumnTypeSql(params) {
|
|
|
691
695
|
});
|
|
692
696
|
}
|
|
693
697
|
if (values.some((value) => /[\\\x00-\x1f\x7f]/.test(value)))
|
|
694
|
-
throw
|
|
698
|
+
throw unsupportedCapability4({ dialect: "mysql", feature: "mysqlEnumLiteralEscaping", hint: "ENUM labels containing backslashes or control characters require a reviewed SQL-mode-specific migration" });
|
|
695
699
|
return `ENUM(${values.map((value) => quotePgString({ value })).join(", ")})`;
|
|
696
700
|
}
|
|
697
701
|
if (field.nativeType !== undefined) {
|
|
@@ -750,25 +754,31 @@ function parseMysqlEnumColumnType(params) {
|
|
|
750
754
|
function normalizeSchema(params) {
|
|
751
755
|
const { schema } = params;
|
|
752
756
|
const dialect = params.dialect ?? "postgres";
|
|
757
|
+
assertIndexOperatorClasses({ schema, dialect, physical: true });
|
|
753
758
|
const enums = new Map;
|
|
754
759
|
for (const enumDef of schema.enums) {
|
|
760
|
+
if (enumDef.namespace !== undefined)
|
|
761
|
+
assertSqlNamespace({ namespace: enumDef.namespace, dialect });
|
|
755
762
|
if (enumStorageOf({ enumDef }) === "string")
|
|
756
763
|
continue;
|
|
757
|
-
enums.set(enumDef.dbName ?? enumDef.name, enumDef);
|
|
764
|
+
enums.set(physicalNameKey({ name: enumDef.dbName ?? enumDef.name, namespace: enumDef.namespace, dialect }), enumDef);
|
|
758
765
|
}
|
|
759
766
|
const tables = new Map;
|
|
760
767
|
const views = new Map;
|
|
761
768
|
for (const model of schema.models) {
|
|
769
|
+
if (model.namespace !== undefined)
|
|
770
|
+
assertSqlNamespace({ namespace: model.namespace, dialect });
|
|
762
771
|
if (model.isView) {
|
|
763
|
-
views.set(model.dbName ?? model.name, model);
|
|
772
|
+
views.set(physicalNameKey({ name: model.dbName ?? model.name, namespace: model.namespace, dialect }), model);
|
|
764
773
|
continue;
|
|
765
774
|
}
|
|
766
775
|
const table = normalizeModel({ model, schema, dialect, enums });
|
|
767
|
-
tables.set(table.table, table);
|
|
776
|
+
tables.set(physicalNameKey({ name: table.table, namespace: table.namespace, dialect }), table);
|
|
768
777
|
}
|
|
769
778
|
const joinTables = new Map;
|
|
770
779
|
for (const pair of detectImplicitM2MPairs({ schema })) {
|
|
771
|
-
|
|
780
|
+
const join = normalizeJoinTable({ pair, schema, dialect });
|
|
781
|
+
joinTables.set(physicalNameKey({ name: join.table, namespace: join.namespace, dialect }), join);
|
|
772
782
|
}
|
|
773
783
|
return { tables, joinTables, enums, views };
|
|
774
784
|
}
|
|
@@ -776,12 +786,15 @@ function physicalEnumField(params) {
|
|
|
776
786
|
const { field, schema, dialect, table } = params;
|
|
777
787
|
if (!isEnumType5(field.type))
|
|
778
788
|
return field;
|
|
779
|
-
const
|
|
780
|
-
|
|
789
|
+
const type = field.type;
|
|
790
|
+
if (type.namespace !== undefined)
|
|
791
|
+
assertSqlNamespace({ namespace: type.namespace, dialect });
|
|
792
|
+
const name = type.enum;
|
|
793
|
+
const enumDef = schema.enums.find((candidate) => (candidate.name === name || candidate.dbName === name) && (type.namespace === undefined || qualifiedSqlName3({ name, namespace: candidate.namespace }).namespace === type.namespace));
|
|
781
794
|
if (enumDef === undefined)
|
|
782
795
|
return field;
|
|
783
796
|
if (enumStorageOf({ enumDef }) !== "string") {
|
|
784
|
-
return dialect === "postgres" ? { ...field, type: { enum: enumDef.dbName ?? enumDef.name } } : field;
|
|
797
|
+
return dialect === "postgres" ? { ...field, type: { enum: enumDef.dbName ?? enumDef.name, ...enumDef.namespace === undefined ? {} : { namespace: enumDef.namespace } } } : field;
|
|
785
798
|
}
|
|
786
799
|
const physical = {
|
|
787
800
|
...field,
|
|
@@ -808,7 +821,7 @@ function assertStringEnumLabelsFit(params) {
|
|
|
808
821
|
const charLimit = varchar?.[1] === undefined ? undefined : Number(varchar[1]);
|
|
809
822
|
const tooLong = values.some((value) => charLimit !== undefined && [...value].length > charLimit || textBytes !== undefined && new TextEncoder().encode(value).length > textBytes);
|
|
810
823
|
if (!supported || tooLong) {
|
|
811
|
-
throw
|
|
824
|
+
throw unsupportedCapability5({
|
|
812
825
|
dialect,
|
|
813
826
|
feature: `string enum column "${table}"."${field.dbName ?? field.name}" stored as ${type}`,
|
|
814
827
|
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"
|
|
@@ -858,7 +871,7 @@ function normalizeModel(params) {
|
|
|
858
871
|
const checks = new Map;
|
|
859
872
|
for (const check of model.checks ?? [])
|
|
860
873
|
checks.set(check.name, check.expression);
|
|
861
|
-
return { model, table, columns, pk, indexes, fks, checks };
|
|
874
|
+
return { model, table, ...model.namespace === undefined ? {} : { namespace: model.namespace }, columns, pk, indexes, fks, checks };
|
|
862
875
|
}
|
|
863
876
|
function normalizePk(params) {
|
|
864
877
|
const { model } = params;
|
|
@@ -879,7 +892,8 @@ function normalizeIndexes(params) {
|
|
|
879
892
|
return {
|
|
880
893
|
name: fieldDbName({ model, name: field.name }),
|
|
881
894
|
desc: field.sort === "desc",
|
|
882
|
-
...decimal ? { decimal: true } : {}
|
|
895
|
+
...decimal ? { decimal: true } : {},
|
|
896
|
+
...field.operatorClass === undefined ? {} : { operatorClass: field.operatorClass }
|
|
883
897
|
};
|
|
884
898
|
});
|
|
885
899
|
const where = index.where;
|
|
@@ -910,7 +924,7 @@ function normalizeIndexes(params) {
|
|
|
910
924
|
}
|
|
911
925
|
function indexComparisonKey(params) {
|
|
912
926
|
const { unique, kind, columns, where, nullsNotDistinct } = params;
|
|
913
|
-
const cols = columns.map((column) => `${column.name}${column.desc ? " desc" : ""}`).join(",");
|
|
927
|
+
const cols = columns.map((column) => `${column.name}${column.desc ? " desc" : ""}${column.operatorClass === undefined ? "" : ` ops:${JSON.stringify([column.operatorClass.namespace, column.operatorClass.name])}`}`).join(",");
|
|
914
928
|
return `${unique ? "u" : "i"}${nullsNotDistinct === true ? "n" : ""}:${kind}:${cols}:${normalizeWhere({ where })}`;
|
|
915
929
|
}
|
|
916
930
|
function normalizeWhere(params) {
|
|
@@ -1088,7 +1102,7 @@ function normalizeFks(params) {
|
|
|
1088
1102
|
const field = model.fields.find((candidate) => candidate.name === name);
|
|
1089
1103
|
return field === undefined ? true : !field.isOptional;
|
|
1090
1104
|
});
|
|
1091
|
-
const key = `${columns.join(",")}\u2192${refTable}.${refColumns.join(",")}`;
|
|
1105
|
+
const key = `${columns.join(",")}\u2192${physicalNameKey({ name: refTable, namespace: target?.namespace, dialect: params.dialect })}.${refColumns.join(",")}`;
|
|
1092
1106
|
if (result.has(key))
|
|
1093
1107
|
continue;
|
|
1094
1108
|
result.set(key, {
|
|
@@ -1096,6 +1110,7 @@ function normalizeFks(params) {
|
|
|
1096
1110
|
constraintName: relation.constraintName ?? defaultForeignKeyName({ table, columns, dialect: params.dialect }),
|
|
1097
1111
|
columns,
|
|
1098
1112
|
refTable,
|
|
1113
|
+
...target?.namespace === undefined ? {} : { refNamespace: target.namespace },
|
|
1099
1114
|
refColumns,
|
|
1100
1115
|
onDelete: resolveOnDelete({ explicit: relation.onDelete, required }),
|
|
1101
1116
|
onUpdate: resolveOnUpdate({ explicit: relation.onUpdate })
|
|
@@ -1107,6 +1122,7 @@ function normalizeJoinTable(params) {
|
|
|
1107
1122
|
const { pair, schema, dialect } = params;
|
|
1108
1123
|
return {
|
|
1109
1124
|
table: pair.tableName,
|
|
1125
|
+
...pair.namespace === undefined ? {} : { namespace: pair.namespace },
|
|
1110
1126
|
a: joinTableEnd({ modelName: pair.modelA, schema, dialect, joinTable: pair.tableName }),
|
|
1111
1127
|
b: joinTableEnd({ modelName: pair.modelB, schema, dialect, joinTable: pair.tableName })
|
|
1112
1128
|
};
|
|
@@ -1132,10 +1148,24 @@ function joinTableEnd(params) {
|
|
|
1132
1148
|
}
|
|
1133
1149
|
return {
|
|
1134
1150
|
table: model.dbName ?? model.name,
|
|
1151
|
+
...model.namespace === undefined ? {} : { namespace: model.namespace },
|
|
1135
1152
|
column: pkField.dbName ?? pkField.name,
|
|
1136
1153
|
field: physicalEnumField({ field: pkField, schema, dialect: params.dialect, table: model.dbName ?? model.name })
|
|
1137
1154
|
};
|
|
1138
1155
|
}
|
|
1156
|
+
function physicalNameKey(params) {
|
|
1157
|
+
if (params.dialect !== undefined && params.dialect !== "postgres")
|
|
1158
|
+
return params.name;
|
|
1159
|
+
const identity = qualifiedSqlName3(params);
|
|
1160
|
+
return JSON.stringify([identity.namespace, identity.name]);
|
|
1161
|
+
}
|
|
1162
|
+
function schemaNamespaces(params) {
|
|
1163
|
+
return [...new Set([
|
|
1164
|
+
...params.schema.models.flatMap((model) => model.namespace === undefined || model.namespace === "public" ? [] : [model.namespace]),
|
|
1165
|
+
...params.schema.enums.flatMap((entry) => enumStorageOf({ enumDef: entry }) === "string" || entry.namespace === undefined || entry.namespace === "public" ? [] : [entry.namespace]),
|
|
1166
|
+
...(params.schema.migrationObjects ?? []).flatMap((object) => object.identity.kind === "namespace" ? [object.identity.name] : [])
|
|
1167
|
+
])];
|
|
1168
|
+
}
|
|
1139
1169
|
function fieldDbName(params) {
|
|
1140
1170
|
const { model, name } = params;
|
|
1141
1171
|
const field = model.fields.find((candidate) => candidate.name === name);
|
|
@@ -1143,6 +1173,7 @@ function fieldDbName(params) {
|
|
|
1143
1173
|
}
|
|
1144
1174
|
|
|
1145
1175
|
// packages/migrate/src/object-ownership.ts
|
|
1176
|
+
import { VibeError as VibeError6, defaultPrimaryKeyName, enumStorageOf as enumStorageOf2, implicitJoinTableObjectNames, postgresGeneratedObjectName, qualifiedSqlName as qualifiedSqlName4, sqlObjectKey as sqlObjectKey2 } from "@vibeorm/schema";
|
|
1146
1177
|
function legacyMigrationObjects(params) {
|
|
1147
1178
|
return params.artifacts.map((artifact) => {
|
|
1148
1179
|
const owner = artifact.owns;
|
|
@@ -1166,13 +1197,14 @@ function legacyMigrationObjects(params) {
|
|
|
1166
1197
|
}
|
|
1167
1198
|
function preserveMigrationObjects(params) {
|
|
1168
1199
|
const namespace = params.namespace ?? (params.to.datasource.provider === "sqlite" ? "main" : "public");
|
|
1169
|
-
const objects = params.objects
|
|
1200
|
+
const objects = params.objects;
|
|
1170
1201
|
const models = params.from.models.flatMap((model) => {
|
|
1171
1202
|
const table = model.dbName ?? model.name;
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1203
|
+
const placement = qualifiedSqlName4({ name: table, namespace: model.namespace, defaultNamespace: namespace });
|
|
1204
|
+
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" }));
|
|
1205
|
+
if (objects.some((object) => (object.identity.kind === "table" || object.identity.kind === "view") && object.identity.name.name === table && object.identity.name.namespace === placement.namespace))
|
|
1174
1206
|
return [];
|
|
1175
|
-
const owners = objects.map((object) => object.identity).filter((identity) => ("table" in identity) && identity.table.name === table);
|
|
1207
|
+
const owners = objects.map((object) => object.identity).filter((identity) => ("table" in identity) && identity.table.name === table && identity.table.namespace === placement.namespace);
|
|
1176
1208
|
return [{
|
|
1177
1209
|
...model,
|
|
1178
1210
|
fields: model.fields.flatMap((field) => {
|
|
@@ -1194,7 +1226,7 @@ function preserveMigrationObjects(params) {
|
|
|
1194
1226
|
relations: model.relations.filter((relation) => !owners.some((owner) => owner.kind === "constraint" && owner.constraintType === "foreignKey" && owner.name === relation.constraintName))
|
|
1195
1227
|
}];
|
|
1196
1228
|
});
|
|
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)));
|
|
1229
|
+
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
1230
|
return { ...params.from, models, enums };
|
|
1199
1231
|
}
|
|
1200
1232
|
function sqlitePreservedSchema(params) {
|
|
@@ -1213,7 +1245,8 @@ function sqlitePreservedSchema(params) {
|
|
|
1213
1245
|
}) };
|
|
1214
1246
|
}
|
|
1215
1247
|
function removalIdentity(params) {
|
|
1216
|
-
const { step
|
|
1248
|
+
const { step } = params;
|
|
1249
|
+
const namespace = step.namespace ?? params.namespace;
|
|
1217
1250
|
const table = "table" in step && step.table !== undefined ? { namespace, name: step.table } : undefined;
|
|
1218
1251
|
switch (step.kind) {
|
|
1219
1252
|
case "dropEnum":
|
|
@@ -1244,18 +1277,22 @@ function removalIdentity(params) {
|
|
|
1244
1277
|
function schemaObjectIdentities(params) {
|
|
1245
1278
|
const normalized = normalizeSchema({ schema: params.schema, dialect: params.dialect });
|
|
1246
1279
|
const result = [];
|
|
1247
|
-
if (params.dialect === "postgres")
|
|
1248
|
-
for (const
|
|
1249
|
-
result.push({ kind: "
|
|
1250
|
-
|
|
1251
|
-
|
|
1280
|
+
if (params.dialect === "postgres") {
|
|
1281
|
+
for (const namespace of schemaNamespaces({ schema: params.schema }))
|
|
1282
|
+
result.push({ kind: "namespace", name: namespace });
|
|
1283
|
+
for (const enumDef of normalized.enums.values())
|
|
1284
|
+
result.push({ kind: "enum", name: qualifiedSqlName4({ name: enumDef.dbName ?? enumDef.name, namespace: enumDef.namespace, defaultNamespace: params.namespace }) });
|
|
1285
|
+
}
|
|
1286
|
+
for (const model of normalized.tables.values()) {
|
|
1287
|
+
const name = model.table;
|
|
1288
|
+
const table = qualifiedSqlName4({ name, namespace: model.namespace, defaultNamespace: params.namespace });
|
|
1252
1289
|
result.push({ kind: "table", name: table });
|
|
1253
1290
|
for (const [column, definition] of model.columns) {
|
|
1254
1291
|
result.push({ kind: "column", table, column });
|
|
1255
1292
|
if (definition.defaultKey !== null)
|
|
1256
1293
|
result.push({ kind: "columnDefault", table, column });
|
|
1257
1294
|
if (params.dialect === "postgres" && definition.field.default?.kind === "autoincrement")
|
|
1258
|
-
result.push({ kind: "sequence", name: { namespace:
|
|
1295
|
+
result.push({ kind: "sequence", name: { namespace: table.namespace, name: postgresGeneratedObjectName({ name1: name, name2: column, label: "seq" }) } });
|
|
1259
1296
|
}
|
|
1260
1297
|
for (const exclusion of model.model.exclusions ?? [])
|
|
1261
1298
|
result.push({ kind: "constraint", constraintType: "exclusion", table, name: exclusion.name });
|
|
@@ -1271,16 +1308,17 @@ function schemaObjectIdentities(params) {
|
|
|
1271
1308
|
if (model.pk.length > 0)
|
|
1272
1309
|
result.push({ kind: "constraint", constraintType: "primaryKey", table, name: model.model.primaryKeyConstraintName ?? (params.dialect === "mysql" ? "PRIMARY" : defaultPrimaryKeyName({ table: name, dialect: params.dialect })) });
|
|
1273
1310
|
}
|
|
1274
|
-
for (const
|
|
1275
|
-
const
|
|
1311
|
+
for (const join of normalized.joinTables.values()) {
|
|
1312
|
+
const name = join.table;
|
|
1313
|
+
const table = qualifiedSqlName4({ name, namespace: join.namespace, defaultNamespace: params.namespace });
|
|
1276
1314
|
result.push({ kind: "table", name: table });
|
|
1277
1315
|
const derived = implicitJoinTableObjectNames({ table: name });
|
|
1278
1316
|
result.push({ kind: "column", table, column: "A" }, { kind: "constraint", constraintType: "foreignKey", table, name: derived.foreignKeyA });
|
|
1279
1317
|
result.push({ kind: "column", table, column: "B" }, { kind: "constraint", constraintType: "foreignKey", table, name: derived.foreignKeyB });
|
|
1280
1318
|
result.push({ kind: "index", table, name: derived.uniqueIndex }, { kind: "index", table, name: derived.indexB });
|
|
1281
1319
|
}
|
|
1282
|
-
for (const
|
|
1283
|
-
result.push({ kind: "view", name: { namespace:
|
|
1320
|
+
for (const view of normalized.views.values())
|
|
1321
|
+
result.push({ kind: "view", name: qualifiedSqlName4({ name: view.dbName ?? view.name, namespace: view.namespace, defaultNamespace: params.namespace }) });
|
|
1284
1322
|
return result;
|
|
1285
1323
|
}
|
|
1286
1324
|
function assertRetainedMigrationObjects(params) {
|
|
@@ -1340,7 +1378,7 @@ function assertRemovalAuthorization(params) {
|
|
|
1340
1378
|
integrity.push(identity);
|
|
1341
1379
|
if (step.kind === "dropTable" || step.kind === "dropJoinTable") {
|
|
1342
1380
|
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)
|
|
1381
|
+
if ((child.kind === "constraint" || child.kind === "index") && child.table.namespace === (step.namespace ?? params.namespace) && child.table.name === step.table)
|
|
1344
1382
|
integrity.push(child);
|
|
1345
1383
|
}
|
|
1346
1384
|
}
|
|
@@ -1352,22 +1390,311 @@ function assertRemovalAuthorization(params) {
|
|
|
1352
1390
|
}
|
|
1353
1391
|
|
|
1354
1392
|
// packages/migrate/src/inventory.ts
|
|
1355
|
-
import { VibeError as
|
|
1356
|
-
import { getSqlDialect as
|
|
1393
|
+
import { VibeError as VibeError17, defaultForeignKeyName as defaultForeignKeyName2, defaultPrimaryKeyName as defaultPrimaryKeyName2, isSqlObjectIdentity as isSqlObjectIdentity2, sqlObjectKey as sqlObjectKey6 } from "@vibeorm/schema";
|
|
1394
|
+
import { getSqlDialect as getSqlDialect3, renderMigrationInventory, renderSqliteColumnInventory as renderSqliteColumnInventory2, renderMysqlObjectMembers as renderMysqlObjectMembers2, sqliteTableConstraints as sqliteTableConstraints3, renderSqliteForeignKeyInventory } from "@vibeorm/sql";
|
|
1395
|
+
|
|
1396
|
+
// packages/migrate/src/external-objects.ts
|
|
1397
|
+
import { IDENTIFIER_LIMITS, VibeError as VibeError7, identifierLength, sqlNamespaceIssue, sqlObjectKey as sqlObjectKey3, sqlObjectNamespace, qualifiedSqlName as qualifiedSqlName5, enumStorageOf as enumStorageOf3, unsupportedCapability as unsupportedCapability6 } from "@vibeorm/schema";
|
|
1398
|
+
import { getSqlDialect, renderExternalOwnershipCatalog, renderReplayCoverageGaps } from "@vibeorm/sql";
|
|
1399
|
+
var partitionPatterns = new WeakMap;
|
|
1400
|
+
function invalid2(params) {
|
|
1401
|
+
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 } });
|
|
1402
|
+
}
|
|
1403
|
+
function record2(value) {
|
|
1404
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1405
|
+
}
|
|
1406
|
+
function identifier(value) {
|
|
1407
|
+
return typeof value === "string" && value.trim().length > 0 && !value.includes("\x00") && identifierLength({ name: value, dialect: "postgres" }) <= IDENTIFIER_LIMITS.postgres.max;
|
|
1408
|
+
}
|
|
1409
|
+
function names(params) {
|
|
1410
|
+
if (params.value === undefined)
|
|
1411
|
+
return;
|
|
1412
|
+
if (!Array.isArray(params.value) || !params.value.every(identifier) || new Set(params.value).size !== params.value.length)
|
|
1413
|
+
throw invalid2({ detail: `${params.key} must contain unique non-empty PostgreSQL identifiers` });
|
|
1414
|
+
return [...params.value];
|
|
1415
|
+
}
|
|
1416
|
+
async function parseExternalObjects(params) {
|
|
1417
|
+
return Promise.resolve().then(() => {
|
|
1418
|
+
if (!Array.isArray(params.value))
|
|
1419
|
+
throw invalid2({ detail: "external must be an array" });
|
|
1420
|
+
if (params.value.length > 0 && params.dialect !== undefined && params.dialect !== "postgres")
|
|
1421
|
+
throw unsupportedCapability6({ dialect: params.dialect, feature: "externalObjects" });
|
|
1422
|
+
const owners = new Set;
|
|
1423
|
+
const claims = new Set;
|
|
1424
|
+
const compiled = [];
|
|
1425
|
+
const tableClaims = [];
|
|
1426
|
+
const result = [];
|
|
1427
|
+
for (const value of params.value) {
|
|
1428
|
+
if (!record2(value) || typeof value.owner !== "string" || value.owner.trim().length === 0 || value.owner.includes("\x00"))
|
|
1429
|
+
throw invalid2({ detail: "each entry needs a non-empty owner" });
|
|
1430
|
+
const owner = value.owner;
|
|
1431
|
+
if (owners.has(owner))
|
|
1432
|
+
throw invalid2({ detail: "an owner must have one declaration", owner });
|
|
1433
|
+
owners.add(owner);
|
|
1434
|
+
const allowed = value.schema === undefined ? ["owner", "tables", "partitions", "functions"] : ["owner", "schema"];
|
|
1435
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
1436
|
+
throw invalid2({ detail: "whole-schema and named-object declarations are exclusive; unknown keys refuse", owner });
|
|
1437
|
+
const claim = (options) => {
|
|
1438
|
+
const { kind, name } = options;
|
|
1439
|
+
const key = JSON.stringify([kind, name]);
|
|
1440
|
+
if (claims.has(key))
|
|
1441
|
+
throw invalid2({ detail: `${kind} ${JSON.stringify(name)} has conflicting owners`, owner });
|
|
1442
|
+
claims.add(key);
|
|
1443
|
+
};
|
|
1444
|
+
if (value.schema !== undefined) {
|
|
1445
|
+
const issue = sqlNamespaceIssue({ namespace: value.schema, allowPublic: false });
|
|
1446
|
+
if (issue !== undefined || typeof value.schema !== "string")
|
|
1447
|
+
throw invalid2({ detail: issue ?? "schema must be a string", owner });
|
|
1448
|
+
claim({ kind: "namespace", name: value.schema });
|
|
1449
|
+
result.push(Object.freeze({ owner, schema: value.schema }));
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
const tables = names({ value: value.tables, key: "tables" });
|
|
1453
|
+
const functions = names({ value: value.functions, key: "functions" });
|
|
1454
|
+
const partitions = [];
|
|
1455
|
+
const ownerPatterns = [];
|
|
1456
|
+
if (value.partitions !== undefined) {
|
|
1457
|
+
if (!Array.isArray(value.partitions))
|
|
1458
|
+
throw invalid2({ detail: "partitions must be an array", owner });
|
|
1459
|
+
for (const partition of value.partitions) {
|
|
1460
|
+
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))
|
|
1461
|
+
throw invalid2({ detail: "partition patterns require a parent in the same owner's tables", owner });
|
|
1462
|
+
const pattern = new RegExp(`^(?:${partition.pattern})$`);
|
|
1463
|
+
compiled.push({ owner, pattern });
|
|
1464
|
+
ownerPatterns.push({ parent: partition.parent, pattern });
|
|
1465
|
+
partitions.push(Object.freeze({ parent: partition.parent, pattern: partition.pattern }));
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if ((tables?.length ?? 0) + (functions?.length ?? 0) + partitions.length === 0)
|
|
1469
|
+
throw invalid2({ detail: "named ownership needs at least one table or function", owner });
|
|
1470
|
+
for (const name of tables ?? []) {
|
|
1471
|
+
if (name === "_vibe_migrations")
|
|
1472
|
+
throw invalid2({ detail: "the migration journal cannot be claimed externally", owner });
|
|
1473
|
+
claim({ kind: "table", name });
|
|
1474
|
+
tableClaims.push({ owner, name });
|
|
1475
|
+
}
|
|
1476
|
+
for (const name of functions ?? [])
|
|
1477
|
+
claim({ kind: "function", name });
|
|
1478
|
+
const declaration = Object.freeze({ owner, ...tables === undefined ? {} : { tables: Object.freeze(tables) }, ...functions === undefined ? {} : { functions: Object.freeze(functions) }, ...value.partitions === undefined ? {} : { partitions: Object.freeze(partitions) } });
|
|
1479
|
+
partitionPatterns.set(declaration, ownerPatterns);
|
|
1480
|
+
result.push(declaration);
|
|
1481
|
+
}
|
|
1482
|
+
for (const table of tableClaims)
|
|
1483
|
+
for (const pattern of compiled) {
|
|
1484
|
+
if (table.owner !== pattern.owner && pattern.pattern.exec(table.name)?.[0] === table.name)
|
|
1485
|
+
throw invalid2({ detail: "a full-name pattern claims another owner's named table", owner: pattern.owner });
|
|
1486
|
+
}
|
|
1487
|
+
return Object.freeze(result);
|
|
1488
|
+
}).catch((cause) => {
|
|
1489
|
+
if (cause instanceof VibeError7)
|
|
1490
|
+
throw cause;
|
|
1491
|
+
throw invalid2({ detail: "regular-expression pattern cannot be compiled", cause });
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
function nameKey(name) {
|
|
1495
|
+
return sqlObjectKey3({ identity: { kind: "table", name } });
|
|
1496
|
+
}
|
|
1497
|
+
function schemaOwner(params) {
|
|
1498
|
+
return params.external.find((entry) => entry.schema === params.namespace)?.owner;
|
|
1499
|
+
}
|
|
1500
|
+
function declaredTableOwner(params) {
|
|
1501
|
+
const whole = schemaOwner({ external: params.external, namespace: params.name.namespace });
|
|
1502
|
+
if (whole !== undefined || params.name.namespace !== "public")
|
|
1503
|
+
return whole;
|
|
1504
|
+
const matches = params.external.filter((entry) => {
|
|
1505
|
+
if (entry.tables?.includes(params.name.name))
|
|
1506
|
+
return true;
|
|
1507
|
+
const patterns = partitionPatterns.get(entry);
|
|
1508
|
+
if ((entry.partitions?.length ?? 0) > 0 && patterns === undefined)
|
|
1509
|
+
throw invalid2({ detail: "resolve partition declarations returned by parseExternalObjects", owner: entry.owner });
|
|
1510
|
+
return patterns?.some(({ pattern }) => pattern.exec(params.name.name)?.[0] === params.name.name) === true;
|
|
1511
|
+
});
|
|
1512
|
+
if (matches.length > 1)
|
|
1513
|
+
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) } });
|
|
1514
|
+
return matches[0]?.owner;
|
|
1515
|
+
}
|
|
1516
|
+
function externalObjectOwner(params) {
|
|
1517
|
+
const { identity, external } = params;
|
|
1518
|
+
const whole = schemaOwner({ external: external.declarations, namespace: sqlObjectNamespace({ identity }) });
|
|
1519
|
+
if (whole !== undefined)
|
|
1520
|
+
return whole;
|
|
1521
|
+
if (identity.kind === "table" || "table" in identity) {
|
|
1522
|
+
const name = "table" in identity ? identity.table : identity.name;
|
|
1523
|
+
return external.tables.find((entry) => nameKey(entry.name) === nameKey(name))?.owner ?? declaredTableOwner({ external: external.declarations, name });
|
|
1524
|
+
}
|
|
1525
|
+
if (identity.kind === "sequence")
|
|
1526
|
+
return external.sequences.find((entry) => nameKey(entry.name) === nameKey(identity.name))?.owner;
|
|
1527
|
+
if (identity.kind === "function" && identity.name.namespace === "public")
|
|
1528
|
+
return external.declarations.find((entry) => entry.functions?.includes(identity.name.name))?.owner;
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
function resolveExternalObjectOwnership(params) {
|
|
1532
|
+
if (params.external.length > 0 && params.dialect !== "postgres")
|
|
1533
|
+
throw unsupportedCapability6({ dialect: params.dialect, feature: "externalObjects" });
|
|
1534
|
+
if (params.catalog.inventory.some((object) => object.inspectionUnavailable !== undefined))
|
|
1535
|
+
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) } });
|
|
1536
|
+
const tables = new Map;
|
|
1537
|
+
const assign = (entry) => {
|
|
1538
|
+
const key = nameKey(entry.name);
|
|
1539
|
+
const prior = tables.get(key)?.owner ?? declaredTableOwner({ external: params.external, name: entry.name });
|
|
1540
|
+
if (prior !== undefined && prior !== entry.owner)
|
|
1541
|
+
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] } });
|
|
1542
|
+
if (tables.has(key))
|
|
1543
|
+
return false;
|
|
1544
|
+
tables.set(key, entry);
|
|
1545
|
+
return true;
|
|
1546
|
+
};
|
|
1547
|
+
for (const declaration of params.external)
|
|
1548
|
+
for (const name of declaration.tables ?? [])
|
|
1549
|
+
assign({ owner: declaration.owner, name: { namespace: "public", name } });
|
|
1550
|
+
for (const object of params.catalog.inventory) {
|
|
1551
|
+
if (object.identity.kind !== "table")
|
|
1552
|
+
continue;
|
|
1553
|
+
const owner = declaredTableOwner({ external: params.external, name: object.identity.name });
|
|
1554
|
+
if (owner !== undefined)
|
|
1555
|
+
assign({ owner, name: object.identity.name });
|
|
1556
|
+
}
|
|
1557
|
+
let changed = true;
|
|
1558
|
+
while (changed) {
|
|
1559
|
+
changed = false;
|
|
1560
|
+
for (const edge of params.catalog.inheritance) {
|
|
1561
|
+
const owner = tables.get(nameKey(edge.parent))?.owner;
|
|
1562
|
+
if (owner !== undefined && assign({ owner, name: edge.child }))
|
|
1563
|
+
changed = true;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
const sequences = params.catalog.ownedSequences.flatMap((edge) => {
|
|
1567
|
+
const owner = tables.get(nameKey(edge.table))?.owner;
|
|
1568
|
+
if (owner === undefined)
|
|
1569
|
+
return [];
|
|
1570
|
+
const whole = schemaOwner({ external: params.external, namespace: edge.sequence.namespace });
|
|
1571
|
+
if (whole !== undefined && whole !== owner)
|
|
1572
|
+
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] } });
|
|
1573
|
+
return [{ owner, name: edge.sequence }];
|
|
1574
|
+
});
|
|
1575
|
+
const closure = { declarations: params.external, catalog: params.catalog, inventory: params.catalog.inventory, tables: [...tables.values()], sequences, owners: [], identities: [] };
|
|
1576
|
+
for (const edge of params.catalog.foreignKeys) {
|
|
1577
|
+
const owner = externalObjectOwner({ external: closure, identity: { kind: "table", name: edge.table } });
|
|
1578
|
+
const targetOwner = externalObjectOwner({ external: closure, identity: { kind: "table", name: edge.target } });
|
|
1579
|
+
if (owner !== targetOwner && (owner !== undefined || targetOwner !== undefined))
|
|
1580
|
+
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" } });
|
|
1581
|
+
}
|
|
1582
|
+
const observed = new Map;
|
|
1583
|
+
for (const namespace of params.catalog.namespaces) {
|
|
1584
|
+
if (schemaOwner({ external: params.external, namespace }) !== undefined)
|
|
1585
|
+
observed.set(sqlObjectKey3({ identity: { kind: "namespace", name: namespace } }), { kind: "namespace", name: namespace });
|
|
1586
|
+
}
|
|
1587
|
+
for (const object of params.catalog.inventory)
|
|
1588
|
+
observed.set(sqlObjectKey3({ identity: object.identity }), object.identity);
|
|
1589
|
+
const owners = params.external.map((entry) => {
|
|
1590
|
+
const identities = [...observed.values()].filter((identity) => externalObjectOwner({ identity, external: closure }) === entry.owner);
|
|
1591
|
+
const missing = [];
|
|
1592
|
+
if (entry.schema !== undefined && !params.catalog.namespaces.includes(entry.schema))
|
|
1593
|
+
missing.push({ kind: "namespace", name: entry.schema });
|
|
1594
|
+
for (const name of entry.tables ?? [])
|
|
1595
|
+
if (!params.catalog.inventory.some(({ identity }) => identity.kind === "table" && identity.name.namespace === "public" && identity.name.name === name))
|
|
1596
|
+
missing.push({ kind: "table", name: { namespace: "public", name } });
|
|
1597
|
+
for (const name of entry.functions ?? [])
|
|
1598
|
+
if (!params.catalog.inventory.some(({ identity }) => identity.kind === "function" && identity.name.namespace === "public" && identity.name.name === name))
|
|
1599
|
+
missing.push({ kind: "function", name: { namespace: "public", name } });
|
|
1600
|
+
return { owner: entry.owner, state: "excluded", count: identities.length, identities, missing };
|
|
1601
|
+
});
|
|
1602
|
+
return { ...closure, owners, identities: owners.flatMap((owner) => owner.identities) };
|
|
1603
|
+
}
|
|
1604
|
+
function catalogName(params) {
|
|
1605
|
+
const namespace = params.row[params.namespace];
|
|
1606
|
+
const name = params.row[params.name];
|
|
1607
|
+
if (!identifier(namespace) || !identifier(name))
|
|
1608
|
+
throw new VibeError7({ code: "VIBE_MIGRATION", message: "Malformed external ownership catalog endpoint", meta: { reason: "external-inventory-unavailable" } });
|
|
1609
|
+
return { namespace, name };
|
|
1610
|
+
}
|
|
1611
|
+
async function inspectExternalObjects(params) {
|
|
1612
|
+
const external = await parseExternalObjects({ value: params.external, dialect: params.dialect });
|
|
1613
|
+
if (params.dialect !== "postgres")
|
|
1614
|
+
return resolveExternalObjectOwnership({ external, dialect: params.dialect, catalog: { namespaces: [], inventory: params.inventory ?? [], inheritance: [], ownedSequences: [], foreignKeys: [] } });
|
|
1615
|
+
const read = async (operation) => {
|
|
1616
|
+
const query = renderExternalOwnershipCatalog({ dialect: getSqlDialect({ dialect: params.dialect }), operation });
|
|
1617
|
+
return params.executor({ text: query.text, values: [...query.values] });
|
|
1618
|
+
};
|
|
1619
|
+
const namespaceRows = await read("namespaces");
|
|
1620
|
+
if (namespaceRows.some((row) => !identifier(row.namespace) || row.visible !== true))
|
|
1621
|
+
throw new VibeError7({ code: "VIBE_MIGRATION", message: "External ownership cannot prove visibility of every inspected namespace", meta: { reason: "external-inventory-unavailable" } });
|
|
1622
|
+
const namespaces = namespaceRows.map((row) => String(row.namespace));
|
|
1623
|
+
const [inherited, owned, foreignKeys] = await Promise.all([read("inheritance"), read("ownedSequences"), read("foreignKeys")]);
|
|
1624
|
+
const inventory = params.inventory ?? (await Promise.all(namespaces.map((namespace) => inspectMigrationInventory({ executor: params.executor, dialect: params.dialect, namespace })))).flat();
|
|
1625
|
+
const coverageGaps = (await Promise.all(namespaces.map(async (namespace) => {
|
|
1626
|
+
const query = renderReplayCoverageGaps({ dialect: getSqlDialect({ dialect: params.dialect }), namespace });
|
|
1627
|
+
return params.executor({ text: query.text, values: [...query.values] });
|
|
1628
|
+
}))).flat();
|
|
1629
|
+
return resolveExternalObjectOwnership({ external, dialect: params.dialect, catalog: {
|
|
1630
|
+
namespaces,
|
|
1631
|
+
inventory,
|
|
1632
|
+
coverageGaps,
|
|
1633
|
+
inheritance: inherited.map((row) => ({ parent: catalogName({ row, namespace: "parentNamespace", name: "parentName" }), child: catalogName({ row, namespace: "namespace", name: "name" }) })),
|
|
1634
|
+
ownedSequences: owned.map((row) => ({ table: catalogName({ row, namespace: "tableNamespace", name: "tableName" }), sequence: catalogName({ row, namespace: "namespace", name: "name" }) })),
|
|
1635
|
+
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 }))
|
|
1636
|
+
} });
|
|
1637
|
+
}
|
|
1638
|
+
function assertExternalOwnershipClaims(params) {
|
|
1639
|
+
for (const identity of params.identities) {
|
|
1640
|
+
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);
|
|
1641
|
+
if (owner !== undefined)
|
|
1642
|
+
throw new VibeError7({ code: "VIBE_CONFIG", message: "Application or module declarations collide with external ownership", meta: { reason: "external-owned-object-collision", owner, identity } });
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function projectExternalSchema(params) {
|
|
1646
|
+
const { schema, external } = params;
|
|
1647
|
+
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);
|
|
1648
|
+
const removed = new Set(schema.models.filter((model) => !models.includes(model)).map((model) => model.name));
|
|
1649
|
+
for (const model of models)
|
|
1650
|
+
for (const relation of model.relations)
|
|
1651
|
+
if (removed.has(relation.targetModel))
|
|
1652
|
+
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 } });
|
|
1653
|
+
return {
|
|
1654
|
+
...schema,
|
|
1655
|
+
models,
|
|
1656
|
+
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),
|
|
1657
|
+
...schema.migrationObjects === undefined ? {} : { migrationObjects: schema.migrationObjects.filter((object) => externalObjectOwner({ external, identity: object.identity }) === undefined) }
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
function projectExternalState(params) {
|
|
1661
|
+
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) };
|
|
1662
|
+
}
|
|
1663
|
+
function projectExternalInventory(params) {
|
|
1664
|
+
return params.inventory.filter((object) => externalObjectOwner({ identity: object.identity, external: params.external }) === undefined);
|
|
1665
|
+
}
|
|
1666
|
+
function migrationCoverageGaps(params) {
|
|
1667
|
+
const recognized = new Set([...params.namespaces, ...params.external?.tables.map((entry) => entry.name.namespace) ?? [], ...params.external?.sequences.map((entry) => entry.name.namespace) ?? []]);
|
|
1668
|
+
const kept = params.gaps.filter((gap) => {
|
|
1669
|
+
const namespace = typeof gap.namespace === "string" ? gap.namespace : params.namespace;
|
|
1670
|
+
if (gap.kind === "namespace" && typeof gap.name === "string" && recognized.has(gap.name))
|
|
1671
|
+
return false;
|
|
1672
|
+
const external = params.external;
|
|
1673
|
+
if (external === undefined)
|
|
1674
|
+
return true;
|
|
1675
|
+
if (external.declarations.some((entry) => entry.schema === namespace || gap.kind === "namespace" && entry.schema === gap.name))
|
|
1676
|
+
return false;
|
|
1677
|
+
const name = gap.kind === "rule" ? gap.parent : gap.name;
|
|
1678
|
+
if ((gap.kind === "relation-layout" || gap.kind === "rule") && typeof name === "string" && externalObjectOwner({ external, identity: { kind: "table", name: { namespace, name } } }) !== undefined)
|
|
1679
|
+
return false;
|
|
1680
|
+
return true;
|
|
1681
|
+
});
|
|
1682
|
+
return [...new Map(kept.map((gap) => [JSON.stringify(gap), gap])).values()];
|
|
1683
|
+
}
|
|
1357
1684
|
|
|
1358
1685
|
// packages/migrate/src/exclusions.ts
|
|
1359
1686
|
import { randomUUID } from "crypto";
|
|
1360
|
-
import { VibeError as
|
|
1687
|
+
import { VibeError as VibeError8, sqlObjectKey as sqlObjectKey4 } from "@vibeorm/schema";
|
|
1361
1688
|
import { postgresDialect as postgresDialect2, renderExclusionCatalog, renderExclusionProbe } from "@vibeorm/sql";
|
|
1362
1689
|
async function deparseExclusion(params) {
|
|
1363
1690
|
const probe = `vibe_exclusion_${randomUUID().replaceAll("-", "")}`;
|
|
1364
1691
|
await params.executor({ text: "SAVEPOINT vibe_exclusion_probe" });
|
|
1365
1692
|
try {
|
|
1366
|
-
for (const text of renderExclusionProbe({ dialect: postgresDialect2, table: params.table, probe, definition: params.definition }))
|
|
1693
|
+
for (const text of renderExclusionProbe({ dialect: postgresDialect2, table: params.table, ...params.namespace === undefined ? {} : { namespace: params.namespace }, probe, definition: params.definition }))
|
|
1367
1694
|
await params.executor({ text });
|
|
1368
1695
|
const rows = await params.executor({ text: renderExclusionCatalog({ dialect: postgresDialect2, probe }) });
|
|
1369
1696
|
if (typeof rows[0]?.definition !== "string")
|
|
1370
|
-
throw new
|
|
1697
|
+
throw new VibeError8({ code: "VIBE_MIGRATION", message: "Exclusion probe unavailable", meta: { reason: "exclusion-inspection-unavailable" } });
|
|
1371
1698
|
return rows[0].definition;
|
|
1372
1699
|
} finally {
|
|
1373
1700
|
await params.executor({ text: "ROLLBACK TO SAVEPOINT vibe_exclusion_probe" });
|
|
@@ -1378,7 +1705,7 @@ async function prepareExclusionDiff(params) {
|
|
|
1378
1705
|
const models = [];
|
|
1379
1706
|
for (const model of params.from.models) {
|
|
1380
1707
|
const table = model.dbName ?? model.name;
|
|
1381
|
-
const target = params.to.models.find((entry) => (entry.dbName ?? entry.name) === table);
|
|
1708
|
+
const target = params.to.models.find((entry) => (entry.dbName ?? entry.name) === table && (entry.namespace ?? "public") === (model.namespace ?? "public"));
|
|
1382
1709
|
const exclusions = [];
|
|
1383
1710
|
for (const before of model.exclusions ?? []) {
|
|
1384
1711
|
const next = target?.exclusions?.find((entry) => entry.name === before.name);
|
|
@@ -1386,22 +1713,22 @@ async function prepareExclusionDiff(params) {
|
|
|
1386
1713
|
exclusions.push(before);
|
|
1387
1714
|
continue;
|
|
1388
1715
|
}
|
|
1389
|
-
const identity = { kind: "constraint", constraintType: "exclusion", table: { namespace: "public", name: table }, name: before.name };
|
|
1716
|
+
const identity = { kind: "constraint", constraintType: "exclusion", table: { namespace: model.namespace ?? "public", name: table }, name: before.name };
|
|
1390
1717
|
if (params.probe === "rollback") {
|
|
1391
|
-
const normalized = await deparseExclusion({ executor: params.executor, table, definition: next.definition });
|
|
1718
|
+
const normalized = await deparseExclusion({ executor: params.executor, table, ...model.namespace === undefined ? {} : { namespace: model.namespace }, definition: next.definition });
|
|
1392
1719
|
exclusions.push(normalized === before.definition ? next : { name: before.name, definition: before.definition, requiredExtensions: before.requiredExtensions });
|
|
1393
1720
|
continue;
|
|
1394
1721
|
}
|
|
1395
|
-
const previous = params.previous?.schema.models.find((entry) => (entry.dbName ?? entry.name) === table)?.exclusions?.find((entry) => entry.name === before.name);
|
|
1722
|
+
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
1723
|
if (previous !== undefined) {
|
|
1397
1724
|
exclusions.push(previous);
|
|
1398
1725
|
continue;
|
|
1399
1726
|
}
|
|
1400
|
-
if (params.replaceChecks?.some((entry) =>
|
|
1727
|
+
if (params.replaceChecks?.some((entry) => sqlObjectKey4({ identity: entry }) === sqlObjectKey4({ identity }))) {
|
|
1401
1728
|
exclusions.push({ name: before.name, definition: before.definition, requiredExtensions: before.requiredExtensions });
|
|
1402
1729
|
continue;
|
|
1403
1730
|
}
|
|
1404
|
-
throw new
|
|
1731
|
+
throw new VibeError8({ code: "VIBE_MIGRATION", message: "Exclusion equivalence requires a rollback probe or explicit replacement", meta: { reason: "explicit-replacement-required", identity } });
|
|
1405
1732
|
}
|
|
1406
1733
|
models.push({ ...model, exclusions });
|
|
1407
1734
|
}
|
|
@@ -1410,20 +1737,20 @@ async function prepareExclusionDiff(params) {
|
|
|
1410
1737
|
async function verifyExclusions(params) {
|
|
1411
1738
|
if (!params.expected.schema.models.some((model) => (model.exclusions?.length ?? 0) > 0))
|
|
1412
1739
|
return [];
|
|
1413
|
-
const rows = await params.executor({ text: renderExclusionCatalog({ dialect: postgresDialect2 }) });
|
|
1740
|
+
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
1741
|
const objects = [];
|
|
1415
1742
|
for (const model of params.expected.schema.models)
|
|
1416
1743
|
for (const exclusion of model.exclusions ?? []) {
|
|
1417
1744
|
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);
|
|
1745
|
+
const identity = { kind: "constraint", constraintType: "exclusion", table: { namespace: model.namespace ?? "public", name: table }, name: exclusion.name };
|
|
1746
|
+
const row = rows.find((entry) => entry.namespace === (model.namespace ?? "public") && entry.table_name === table && entry.constraint_name === exclusion.name);
|
|
1420
1747
|
if (row === undefined) {
|
|
1421
1748
|
objects.push({ identity, ownership: "schema", state: "missing", differences: [] });
|
|
1422
1749
|
continue;
|
|
1423
1750
|
}
|
|
1424
1751
|
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;
|
|
1752
|
+
const independentlyVerified = params.expected.definitions.some((entry) => sqlObjectKey4({ identity: entry.identity }) === sqlObjectKey4({ identity }));
|
|
1753
|
+
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
1754
|
if (definition !== row.definition)
|
|
1428
1755
|
differences.push({ attribute: "definition", expected: definition, actual: row.definition });
|
|
1429
1756
|
objects.push({ identity, ownership: "schema", state: differences.length === 0 ? "matching" : params.probe === "rollback" ? "changed" : "unavailable", differences });
|
|
@@ -1433,22 +1760,19 @@ async function verifyExclusions(params) {
|
|
|
1433
1760
|
|
|
1434
1761
|
// packages/migrate/src/inspect-objects.ts
|
|
1435
1762
|
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";
|
|
1763
|
+
import { VibeError as VibeError16, sqlObjectKey as sqlObjectKey5, sqlObjectNamespace as sqlObjectNamespace2, DIALECT_CAPABILITIES as DIALECT_CAPABILITIES2, qualifiedSqlName as qualifiedSqlName8 } from "@vibeorm/schema";
|
|
1764
|
+
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
1765
|
|
|
1439
1766
|
// packages/migrate/src/introspect/postgres.ts
|
|
1440
1767
|
import { renderExclusionCatalog as renderExclusionCatalog2 } from "@vibeorm/sql";
|
|
1441
1768
|
import { parsePgArrayLiteral } from "@vibeorm/sql";
|
|
1442
1769
|
import { scalarListDefault as scalarListDefault2 } from "@vibeorm/schema";
|
|
1443
1770
|
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";
|
|
1771
|
+
import { postgresDialect as postgresDialect6, renderRlsReadinessQuery, renderPostgresPhysicalCatalog, renderPostgresIndexKeys, renderMigrationObjectQuery } from "@vibeorm/sql";
|
|
1772
|
+
import { VibeError as VibeError13, assertSqlNamespace as assertSqlNamespace2, isReservedClientName } from "@vibeorm/schema";
|
|
1449
1773
|
|
|
1450
1774
|
// packages/migrate/src/introspect/shared.ts
|
|
1451
|
-
import { VibeError as
|
|
1775
|
+
import { VibeError as VibeError9 } from "@vibeorm/schema";
|
|
1452
1776
|
function detectJoinTable(params) {
|
|
1453
1777
|
const { table, columnNames, fkGroups, indexes } = params;
|
|
1454
1778
|
if (!table.startsWith("_"))
|
|
@@ -1466,8 +1790,8 @@ function detectJoinTable(params) {
|
|
|
1466
1790
|
if (fkA.refTable > fkB.refTable || tableFks.some((fk) => !fk.required || fk.onDelete !== "Cascade" || fk.onUpdate !== "Cascade"))
|
|
1467
1791
|
return null;
|
|
1468
1792
|
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;
|
|
1793
|
+
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);
|
|
1794
|
+
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
1795
|
if (tableIndexes.length !== 2 || tableIndexes.filter(canonicalAb).length !== 1 || tableIndexes.filter(canonicalB).length !== 1)
|
|
1472
1796
|
return null;
|
|
1473
1797
|
const sorted = [fkA.refTable, fkB.refTable].sort();
|
|
@@ -1487,7 +1811,7 @@ function addFkRelations(params) {
|
|
|
1487
1811
|
const primary = target?.primaryKey?.fields ?? target?.fields.filter((field) => field.isId).map((field) => field.name) ?? [];
|
|
1488
1812
|
const columns = primary.map((name) => target?.fields.find((field) => field.name === name)?.dbName ?? name);
|
|
1489
1813
|
if (columns.length !== 1 || fk.refColumns.length !== 1 || columns[0] !== fk.refColumns[0])
|
|
1490
|
-
throw new
|
|
1814
|
+
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
1815
|
}
|
|
1492
1816
|
const relationsByModel = new Map;
|
|
1493
1817
|
const takeName = nameAllocator({ models });
|
|
@@ -1499,9 +1823,9 @@ function addFkRelations(params) {
|
|
|
1499
1823
|
const target = modelsByName.get(group.refTable);
|
|
1500
1824
|
if (owner === undefined || target === undefined)
|
|
1501
1825
|
continue;
|
|
1502
|
-
const relationName = ambiguousPairs.has(modelPairKey({ a: group.table, b: group.refTable })) ? `${
|
|
1826
|
+
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
1827
|
const fkSide = {
|
|
1504
|
-
name: takeName({ model: owner.name, base: lowerFirst(
|
|
1828
|
+
name: takeName({ model: owner.name, base: lowerFirst(target.dbName ?? target.name) }),
|
|
1505
1829
|
model: owner.name,
|
|
1506
1830
|
targetModel: group.refTable,
|
|
1507
1831
|
type: "manyToOne",
|
|
@@ -1516,7 +1840,7 @@ function addFkRelations(params) {
|
|
|
1516
1840
|
};
|
|
1517
1841
|
pushRelation({ relationsByModel, relation: fkSide });
|
|
1518
1842
|
const listSide = {
|
|
1519
|
-
name: takeName({ model: target.name, base: lowerFirst(
|
|
1843
|
+
name: takeName({ model: target.name, base: lowerFirst(owner.dbName ?? owner.name) }),
|
|
1520
1844
|
model: target.name,
|
|
1521
1845
|
targetModel: group.table,
|
|
1522
1846
|
type: "oneToMany",
|
|
@@ -1542,7 +1866,7 @@ function addJoinTableRelations(params) {
|
|
|
1542
1866
|
pushRelation({
|
|
1543
1867
|
relationsByModel,
|
|
1544
1868
|
relation: {
|
|
1545
|
-
name: takeName({ model: modelA.name, base: lowerFirst(modelB.name) }),
|
|
1869
|
+
name: takeName({ model: modelA.name, base: lowerFirst(modelB.dbName ?? modelB.name) }),
|
|
1546
1870
|
model: modelA.name,
|
|
1547
1871
|
targetModel: modelB.name,
|
|
1548
1872
|
type: "oneToMany",
|
|
@@ -1554,7 +1878,7 @@ function addJoinTableRelations(params) {
|
|
|
1554
1878
|
pushRelation({
|
|
1555
1879
|
relationsByModel,
|
|
1556
1880
|
relation: {
|
|
1557
|
-
name: takeName({ model: modelB.name, base: lowerFirst(modelA.name) }),
|
|
1881
|
+
name: takeName({ model: modelB.name, base: lowerFirst(modelA.dbName ?? modelA.name) }),
|
|
1558
1882
|
model: modelB.name,
|
|
1559
1883
|
targetModel: modelA.name,
|
|
1560
1884
|
type: "oneToMany",
|
|
@@ -1635,14 +1959,14 @@ function lowerFirst(value) {
|
|
|
1635
1959
|
}
|
|
1636
1960
|
|
|
1637
1961
|
// packages/migrate/src/postgres-type-identity.ts
|
|
1638
|
-
import { VibeError as
|
|
1962
|
+
import { VibeError as VibeError10, qualifiedSqlName as qualifiedSqlName6 } from "@vibeorm/schema";
|
|
1639
1963
|
import { postgresDialect as postgresDialect3, renderPostgresTypeIdentity } from "@vibeorm/sql";
|
|
1640
1964
|
async function resolvePostgresTypeIdentities(params) {
|
|
1641
1965
|
const unavailable = [];
|
|
1642
1966
|
const models = [];
|
|
1643
1967
|
for (const model of params.schema.models) {
|
|
1644
|
-
const table = {
|
|
1645
|
-
const previous = params.previous?.models.find((entry) => (entry.dbName ?? entry.name) === table.name);
|
|
1968
|
+
const table = qualifiedSqlName6({ name: model.dbName ?? model.name, namespace: model.namespace });
|
|
1969
|
+
const previous = params.previous?.models.find((entry) => (entry.dbName ?? entry.name) === table.name && (entry.namespace ?? "public") === table.namespace);
|
|
1646
1970
|
const fields = [];
|
|
1647
1971
|
for (const field of model.fields) {
|
|
1648
1972
|
if (field.isUnsupported !== true) {
|
|
@@ -1653,7 +1977,7 @@ async function resolvePostgresTypeIdentities(params) {
|
|
|
1653
1977
|
const parsed = parseUnsupportedPostgresType({ raw: field.nativeType ?? "" });
|
|
1654
1978
|
const evidence = field.nativeTypeIdentity;
|
|
1655
1979
|
if (evidence !== undefined && (evidence.source !== field.nativeType || parsed?.namespace !== undefined && parsed.namespace !== evidence.namespace)) {
|
|
1656
|
-
throw new
|
|
1980
|
+
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
1981
|
}
|
|
1658
1982
|
const prior = previous?.fields.find((entry) => (entry.dbName ?? entry.name) === column && entry.isUnsupported === true);
|
|
1659
1983
|
const priorType = parseUnsupportedPostgresType({ raw: prior?.nativeType ?? "" });
|
|
@@ -1663,7 +1987,7 @@ async function resolvePostgresTypeIdentities(params) {
|
|
|
1663
1987
|
const source = params.mode === "catalog" ? { table, column } : { type: { name: parsed?.name ?? "", ...namespace === undefined && params.mode === "reference" ? {} : { namespace: namespace ?? "pg_catalog" } } };
|
|
1664
1988
|
const query = renderPostgresTypeIdentity({ dialect: postgresDialect3, source });
|
|
1665
1989
|
const rows = parsed === undefined && params.mode !== "catalog" ? [] : await params.executor({ text: query.text, values: [...query.values] }).then((rows) => rows, (cause) => {
|
|
1666
|
-
throw new
|
|
1990
|
+
throw new VibeError10({ code: "VIBE_MIGRATION", message: "PostgreSQL type identity inspection failed", meta: { reason: "type-inspection-unavailable", table: table.name, column }, cause });
|
|
1667
1991
|
});
|
|
1668
1992
|
const resolved = rows.length === 1 ? rows[0] : undefined;
|
|
1669
1993
|
namespace = typeof resolved?.namespace === "string" && (resolved.name === parsed?.name || parsed === undefined && params.mode === "catalog") ? resolved.namespace : undefined;
|
|
@@ -1681,11 +2005,11 @@ async function resolvePostgresTypeIdentities(params) {
|
|
|
1681
2005
|
}
|
|
1682
2006
|
|
|
1683
2007
|
// packages/migrate/src/introspect/postgres-rls.ts
|
|
1684
|
-
import { VibeError as
|
|
2008
|
+
import { VibeError as VibeError12, assertValidSchemaIR as assertValidSchemaIR3 } from "@vibeorm/schema";
|
|
1685
2009
|
import { NATIVE_RLS, postgresDialect as postgresDialect5, renderRlsPolicy as renderRlsPolicy2 } from "@vibeorm/sql";
|
|
1686
2010
|
|
|
1687
2011
|
// packages/migrate/src/ddl/rls.ts
|
|
1688
|
-
import { VibeError as
|
|
2012
|
+
import { VibeError as VibeError11, assertValidSchemaIR as assertValidSchemaIR2, unsupportedCapability as unsupportedCapability7 } from "@vibeorm/schema";
|
|
1689
2013
|
import * as sql from "@vibeorm/sql";
|
|
1690
2014
|
function isRlsProtected(params) {
|
|
1691
2015
|
return params.model.rls !== undefined && params.model.rls !== "unprotected";
|
|
@@ -1715,7 +2039,18 @@ function assertRlsDialect(params) {
|
|
|
1715
2039
|
if (params.schema.rls === undefined && !params.schema.models.some((model) => model.rls !== undefined))
|
|
1716
2040
|
return;
|
|
1717
2041
|
if (!sql.DIALECT_CAPABILITIES[params.dialect].nativeRowLevelSecurity) {
|
|
1718
|
-
throw
|
|
2042
|
+
throw unsupportedCapability7({ dialect: params.dialect, feature: "nativeRowLevelSecurity" });
|
|
2043
|
+
}
|
|
2044
|
+
for (const model of params.schema.models) {
|
|
2045
|
+
if (model.namespace !== undefined && model.namespace !== "public" && isRlsProtected({ model }))
|
|
2046
|
+
throw unsupportedCapability7({ dialect: params.dialect, feature: "fixedNamespaceNativeRls", hint: "Named package schemas do not support native RLS placement in this contract" });
|
|
2047
|
+
const policy = model.rls;
|
|
2048
|
+
if (isRlsProtected({ model }) && typeof policy === "object" && "through" in policy) {
|
|
2049
|
+
const relation = model.relations.find((entry) => entry.name === policy.through);
|
|
2050
|
+
const parent = params.schema.models.find((entry) => entry.name === relation?.targetModel);
|
|
2051
|
+
if (parent?.namespace !== undefined && parent.namespace !== "public")
|
|
2052
|
+
throw unsupportedCapability7({ dialect: params.dialect, feature: "fixedNamespaceNativeRls" });
|
|
2053
|
+
}
|
|
1719
2054
|
}
|
|
1720
2055
|
assertValidSchemaIR2({ schema: params.schema });
|
|
1721
2056
|
}
|
|
@@ -1723,8 +2058,9 @@ function withRlsSteps(params) {
|
|
|
1723
2058
|
const { from, to, steps } = params;
|
|
1724
2059
|
const before = orderedRlsModels({ schema: from });
|
|
1725
2060
|
const after = orderedRlsModels({ schema: to });
|
|
1726
|
-
const
|
|
1727
|
-
const
|
|
2061
|
+
const tableKey = (params) => physicalNameKey(params);
|
|
2062
|
+
const beforeByTable = new Map(before.map((model) => [tableKey({ name: model.dbName ?? model.name, namespace: model.namespace }), model]));
|
|
2063
|
+
const afterByTable = new Map(after.map((model) => [tableKey({ name: model.dbName ?? model.name, namespace: model.namespace }), model]));
|
|
1728
2064
|
const changed = new Set;
|
|
1729
2065
|
const policyChanges = new Set;
|
|
1730
2066
|
for (const [table, model] of beforeByTable) {
|
|
@@ -1736,7 +2072,7 @@ function withRlsSteps(params) {
|
|
|
1736
2072
|
}
|
|
1737
2073
|
for (const step of steps) {
|
|
1738
2074
|
if (step.kind === "dropTable" || step.kind === "dropColumn" || step.kind === "alterColumn" || step.kind === "dropForeignKey" || step.kind === "dropPrimaryKey")
|
|
1739
|
-
changed.add(step.table);
|
|
2075
|
+
changed.add(tableKey({ name: step.table, namespace: step.namespace }));
|
|
1740
2076
|
}
|
|
1741
2077
|
for (const schema of [from, to]) {
|
|
1742
2078
|
for (const model of orderedRlsModels({ schema })) {
|
|
@@ -1745,9 +2081,9 @@ function withRlsSteps(params) {
|
|
|
1745
2081
|
const through = model.rls.through;
|
|
1746
2082
|
const relation = model.relations.find((entry) => entry.name === through);
|
|
1747
2083
|
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);
|
|
2084
|
+
if (parent !== undefined && changed.has(tableKey({ name: parent.dbName ?? parent.name, namespace: parent.namespace }))) {
|
|
2085
|
+
changed.add(tableKey({ name: model.dbName ?? model.name, namespace: model.namespace }));
|
|
2086
|
+
policyChanges.add(tableKey({ name: model.dbName ?? model.name, namespace: model.namespace }));
|
|
1751
2087
|
}
|
|
1752
2088
|
}
|
|
1753
2089
|
}
|
|
@@ -1756,20 +2092,22 @@ function withRlsSteps(params) {
|
|
|
1756
2092
|
const removals = [];
|
|
1757
2093
|
for (const model of [...before].reverse()) {
|
|
1758
2094
|
const table = model.dbName ?? model.name;
|
|
1759
|
-
|
|
2095
|
+
const key = tableKey({ name: table, namespace: model.namespace });
|
|
2096
|
+
if (!changed.has(key))
|
|
1760
2097
|
continue;
|
|
1761
|
-
const target = afterByTable.get(
|
|
1762
|
-
const securityChange = target === undefined ? `remove native RLS protection from "${table}"` : policyChanges.has(
|
|
2098
|
+
const target = afterByTable.get(key);
|
|
2099
|
+
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
2100
|
drops.push({ kind: "dropPolicy", table, destructive: false, ...securityChange === undefined ? {} : { securityChange } });
|
|
1764
|
-
if (target === undefined && to.models.some((entry) => (entry.dbName ?? entry.name) ===
|
|
2101
|
+
if (target === undefined && to.models.some((entry) => tableKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace }) === key)) {
|
|
1765
2102
|
removals.push({ kind: "noForceRls", table, destructive: false }, { kind: "disableRls", table, destructive: false });
|
|
1766
2103
|
}
|
|
1767
2104
|
}
|
|
1768
2105
|
for (const model of after) {
|
|
1769
2106
|
const table = model.dbName ?? model.name;
|
|
1770
|
-
|
|
2107
|
+
const key = tableKey({ name: table, namespace: model.namespace });
|
|
2108
|
+
if (beforeByTable.has(key) && !changed.has(key))
|
|
1771
2109
|
continue;
|
|
1772
|
-
if (!beforeByTable.has(
|
|
2110
|
+
if (!beforeByTable.has(key))
|
|
1773
2111
|
installs.push({ kind: "enableRls", table, destructive: false }, { kind: "forceRls", table, destructive: false });
|
|
1774
2112
|
installs.push({ kind: "createPolicy", table, schema: to, model: model.name, destructive: false });
|
|
1775
2113
|
}
|
|
@@ -1781,7 +2119,7 @@ function isRlsStep(params) {
|
|
|
1781
2119
|
function renderRlsStep(params) {
|
|
1782
2120
|
const { step, dialect } = params;
|
|
1783
2121
|
if (!sql.DIALECT_CAPABILITIES[dialect].nativeRowLevelSecurity)
|
|
1784
|
-
throw
|
|
2122
|
+
throw unsupportedCapability7({ dialect, feature: "nativeRowLevelSecurity" });
|
|
1785
2123
|
const common = { dialect: sql.postgresDialect, table: "table" in step ? step.table ?? "" : "" };
|
|
1786
2124
|
switch (step.kind) {
|
|
1787
2125
|
case "enableRls":
|
|
@@ -1797,11 +2135,11 @@ function renderRlsStep(params) {
|
|
|
1797
2135
|
const renderers = sql;
|
|
1798
2136
|
const renderer = renderers[name];
|
|
1799
2137
|
if (renderer === undefined)
|
|
1800
|
-
throw
|
|
2138
|
+
throw unsupportedCapability7({ dialect, feature: "nativeRowLevelSecurity", hint: `The SQL package must provide ${name} before this migration can execute` });
|
|
1801
2139
|
return [renderer(common).text];
|
|
1802
2140
|
}
|
|
1803
2141
|
default:
|
|
1804
|
-
throw new
|
|
2142
|
+
throw new VibeError11({ code: "VIBE_MIGRATION", message: "Expected a native RLS migration step" });
|
|
1805
2143
|
}
|
|
1806
2144
|
}
|
|
1807
2145
|
function securityChangesOf(params) {
|
|
@@ -1810,7 +2148,7 @@ function securityChangesOf(params) {
|
|
|
1810
2148
|
function assertSecurityAcknowledgement(params) {
|
|
1811
2149
|
if (params.securityChanges.length === 0 || params.acceptSecurityChanges === true)
|
|
1812
2150
|
return;
|
|
1813
|
-
throw new
|
|
2151
|
+
throw new VibeError11({
|
|
1814
2152
|
code: "VIBE_MIGRATION",
|
|
1815
2153
|
message: `Security changes require acceptSecurityChanges: true; acceptDataLoss does not authorize them:
|
|
1816
2154
|
${params.securityChanges.join(`
|
|
@@ -1849,7 +2187,7 @@ async function readPostgresRls(params) {
|
|
|
1849
2187
|
return [...tables.values()];
|
|
1850
2188
|
}
|
|
1851
2189
|
function conflict(params) {
|
|
1852
|
-
throw new
|
|
2190
|
+
throw new VibeError12({
|
|
1853
2191
|
code: "VIBE_MIGRATION",
|
|
1854
2192
|
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
2193
|
meta: params
|
|
@@ -1860,10 +2198,11 @@ async function verifyPostgresRls(params) {
|
|
|
1860
2198
|
if (schema !== undefined && schema.rls !== undefined)
|
|
1861
2199
|
assertValidSchemaIR3({ schema });
|
|
1862
2200
|
await executor({ text: "SET LOCAL search_path = ''" });
|
|
1863
|
-
const declared =
|
|
2201
|
+
const declared = (schema?.models ?? []).filter((model) => model.namespace === undefined || model.namespace === "public");
|
|
2202
|
+
const byTable = new Map(declared.map((model) => [model.dbName ?? model.name, model]));
|
|
1864
2203
|
let tables = await readPostgresRls({ executor });
|
|
1865
2204
|
for (const state of tables) {
|
|
1866
|
-
const model =
|
|
2205
|
+
const model = byTable.get(state.table);
|
|
1867
2206
|
if (!state.enabled && !state.force && state.policies.length === 0 && (model === undefined || !isRlsProtected({ model })))
|
|
1868
2207
|
continue;
|
|
1869
2208
|
await executor({ text: `LOCK TABLE "public".${postgresDialect5.quoteIdent({ name: state.table })} IN ACCESS EXCLUSIVE MODE` });
|
|
@@ -1871,7 +2210,7 @@ async function verifyPostgresRls(params) {
|
|
|
1871
2210
|
tables = await readPostgresRls({ executor });
|
|
1872
2211
|
const matched = new Set;
|
|
1873
2212
|
for (const state of tables) {
|
|
1874
|
-
const model =
|
|
2213
|
+
const model = byTable.get(state.table);
|
|
1875
2214
|
const active = state.enabled || state.force || state.policies.length > 0;
|
|
1876
2215
|
if (schema === undefined && active)
|
|
1877
2216
|
conflict({ table: state.table, reason: "rls-unmanaged", attributes: ["security metadata cannot be exported losslessly without a trusted declaration"] });
|
|
@@ -1918,7 +2257,7 @@ async function verifyPostgresRls(params) {
|
|
|
1918
2257
|
matched.add(state.table);
|
|
1919
2258
|
}
|
|
1920
2259
|
if (params.requireInstalled === true) {
|
|
1921
|
-
for (const [table, model] of
|
|
2260
|
+
for (const [table, model] of byTable) {
|
|
1922
2261
|
if (isRlsProtected({ model }) && !matched.has(table))
|
|
1923
2262
|
conflict({ table, reason: "rls-drift", attributes: ["missing table"] });
|
|
1924
2263
|
}
|
|
@@ -1930,8 +2269,9 @@ function projectPostgresRls(params) {
|
|
|
1930
2269
|
if (declared?.rls === undefined || matched.size === 0)
|
|
1931
2270
|
return physical;
|
|
1932
2271
|
const models = physical.models.map((model) => {
|
|
1933
|
-
const
|
|
1934
|
-
|
|
2272
|
+
const physicalName = model.dbName ?? model.name;
|
|
2273
|
+
const source = declared.models.find((entry) => (entry.dbName ?? entry.name) === physicalName && (entry.namespace ?? "public") === (model.namespace ?? "public"));
|
|
2274
|
+
if (source === undefined || model.namespace !== undefined && model.namespace !== "public" || !matched.has(physicalName))
|
|
1935
2275
|
return { ...model, rls: "unprotected" };
|
|
1936
2276
|
const sourcePolicy = source.rls;
|
|
1937
2277
|
let rls = "deny";
|
|
@@ -1942,7 +2282,8 @@ function projectPostgresRls(params) {
|
|
|
1942
2282
|
const target = declared.models.find((entry) => entry.name === sourceRelation.targetModel);
|
|
1943
2283
|
const fields = sourceRelation.fields.map((name) => source.fields.find((entry) => entry.name === name)?.dbName ?? name);
|
|
1944
2284
|
const references = sourceRelation.references.map((name) => target.fields.find((entry) => entry.name === name)?.dbName ?? name);
|
|
1945
|
-
const
|
|
2285
|
+
const physicalTarget = physical.models.find((entry) => (entry.dbName ?? entry.name) === (target.dbName ?? target.name) && (entry.namespace ?? "public") === (target.namespace ?? "public"));
|
|
2286
|
+
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
2287
|
if (relation === undefined)
|
|
1947
2288
|
conflict({ table: model.name, reason: "rls-drift", attributes: ["through foreign key"] });
|
|
1948
2289
|
rls = { through: relation.name };
|
|
@@ -1953,146 +2294,6 @@ function projectPostgresRls(params) {
|
|
|
1953
2294
|
}
|
|
1954
2295
|
|
|
1955
2296
|
// 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
2297
|
async function introspectPostgres(params) {
|
|
2097
2298
|
const { executor } = params;
|
|
2098
2299
|
if (params.inTransaction !== true)
|
|
@@ -2101,14 +2302,14 @@ async function introspectPostgres(params) {
|
|
|
2101
2302
|
const savedPath = await executor(renderRlsReadinessQuery({ dialect: postgresDialect6, query: "readSearchPath" }));
|
|
2102
2303
|
const searchPath = savedPath[0]?.searchPath;
|
|
2103
2304
|
if (typeof searchPath !== "string")
|
|
2104
|
-
throw new
|
|
2305
|
+
throw new VibeError13({ code: "VIBE_MIGRATION", message: "Cannot read the migration search_path" });
|
|
2105
2306
|
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
|
|
2307
|
+
if (error instanceof VibeError13)
|
|
2107
2308
|
throw error;
|
|
2108
|
-
throw new
|
|
2309
|
+
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
2310
|
});
|
|
2110
2311
|
await executor(renderRlsReadinessQuery({ dialect: postgresDialect6, query: "setSearchPath", searchPath }));
|
|
2111
|
-
const physical = await introspectPhysicalPostgres({ executor });
|
|
2312
|
+
const physical = await introspectPhysicalPostgres({ executor, namespaces: params.namespaces ?? ["public", ...params.rlsSchema?.models.flatMap((model) => model.namespace === undefined ? [] : [model.namespace]) ?? []] });
|
|
2112
2313
|
return projectPostgresRls({ physical, ...params.rlsSchema === undefined ? {} : { declared: params.rlsSchema }, matched });
|
|
2113
2314
|
} finally {
|
|
2114
2315
|
if (params.inTransaction !== true)
|
|
@@ -2117,17 +2318,51 @@ async function introspectPostgres(params) {
|
|
|
2117
2318
|
}
|
|
2118
2319
|
async function introspectPhysicalPostgres(params) {
|
|
2119
2320
|
const { executor } = params;
|
|
2321
|
+
const namespaces = [...new Set(params.namespaces ?? ["public"])];
|
|
2322
|
+
const physicalNames = new Map;
|
|
2323
|
+
const enumIdentities = new Map;
|
|
2324
|
+
const namespaceObjects = [];
|
|
2325
|
+
const read = async (query) => {
|
|
2326
|
+
const groups = await Promise.all(namespaces.map(async (namespace) => {
|
|
2327
|
+
assertSqlNamespace2({ namespace, dialect: "postgres" });
|
|
2328
|
+
const sql = renderPostgresPhysicalCatalog({ dialect: postgresDialect6, namespace, query });
|
|
2329
|
+
const rows = await executor({ text: sql.text, values: [...sql.values] });
|
|
2330
|
+
return rows.map((row) => {
|
|
2331
|
+
const name = typeof row.table_name === "string" ? row.table_name : undefined;
|
|
2332
|
+
const table = name === undefined ? undefined : physicalNameKey({ namespace, name });
|
|
2333
|
+
if (table !== undefined && name !== undefined)
|
|
2334
|
+
physicalNames.set(table, { namespace, name });
|
|
2335
|
+
const typeName = typeof row.type_name === "string" ? row.type_name : undefined;
|
|
2336
|
+
const enumKey = typeName === undefined ? undefined : physicalNameKey({ namespace, name: typeName });
|
|
2337
|
+
if (enumKey !== undefined && typeName !== undefined)
|
|
2338
|
+
enumIdentities.set(enumKey, { namespace, name: typeName });
|
|
2339
|
+
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 }) } : {} };
|
|
2340
|
+
});
|
|
2341
|
+
}));
|
|
2342
|
+
return groups.flat();
|
|
2343
|
+
};
|
|
2344
|
+
for (const namespace of namespaces) {
|
|
2345
|
+
if (namespace === "public")
|
|
2346
|
+
continue;
|
|
2347
|
+
const query = renderMigrationObjectQuery({ dialect: postgresDialect6, identity: { kind: "namespace", name: namespace } });
|
|
2348
|
+
if ((await executor({ text: query.text, values: [...query.values] })).length > 0)
|
|
2349
|
+
namespaceObjects.push({ id: `namespace:${namespace}`, dialect: "postgres", identity: { kind: "namespace", name: namespace }, create: [], drop: [], dependsOn: [] });
|
|
2350
|
+
}
|
|
2120
2351
|
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 }) })
|
|
2352
|
+
read("tables"),
|
|
2353
|
+
read("views"),
|
|
2354
|
+
read("columns"),
|
|
2355
|
+
read("constraints"),
|
|
2356
|
+
read("foreignKeys"),
|
|
2357
|
+
read("enums"),
|
|
2358
|
+
read("indexes"),
|
|
2359
|
+
read("checks"),
|
|
2360
|
+
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
2361
|
]);
|
|
2362
|
+
const keysQuery = renderPostgresIndexKeys({ dialect: postgresDialect6 });
|
|
2363
|
+
const indexKeys = await executor({ text: keysQuery.text, values: [...keysQuery.values] });
|
|
2364
|
+
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))
|
|
2365
|
+
throw new VibeError13({ code: "VIBE_MIGRATION", message: "PostgreSQL index-key identity is incomplete", meta: { reason: "index-inspection-unavailable" } });
|
|
2131
2366
|
const tables = tableRows.map((row) => row.table_name);
|
|
2132
2367
|
const views = viewRows.map((row) => row.table_name);
|
|
2133
2368
|
const columns = columnRows.map((row) => ({
|
|
@@ -2135,6 +2370,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2135
2370
|
name: row.column_name,
|
|
2136
2371
|
dataType: row.data_type,
|
|
2137
2372
|
udtName: row.udt_name,
|
|
2373
|
+
udtNamespace: typeof row.udt_schema === "string" ? row.udt_schema : "public",
|
|
2138
2374
|
nullable: row.is_nullable === "YES",
|
|
2139
2375
|
identity: row.is_identity === "YES",
|
|
2140
2376
|
columnDefault: row.column_default ?? null,
|
|
@@ -2167,7 +2403,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2167
2403
|
const table = row.table_name;
|
|
2168
2404
|
const constraintName = row.constraint_name;
|
|
2169
2405
|
const columnName = row.column_name;
|
|
2170
|
-
constraintNames.add(constraintName);
|
|
2406
|
+
constraintNames.add(JSON.stringify([table, constraintName]));
|
|
2171
2407
|
if (row.constraint_type === "PRIMARY KEY") {
|
|
2172
2408
|
pkNames.set(table, constraintName);
|
|
2173
2409
|
const list = pkByTable.get(table);
|
|
@@ -2176,9 +2412,10 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2176
2412
|
else
|
|
2177
2413
|
list.push(columnName);
|
|
2178
2414
|
} else {
|
|
2179
|
-
const
|
|
2415
|
+
const constraintKey = JSON.stringify([table, constraintName]);
|
|
2416
|
+
const entry = uniquesByConstraint.get(constraintKey);
|
|
2180
2417
|
if (entry === undefined)
|
|
2181
|
-
uniquesByConstraint.set(
|
|
2418
|
+
uniquesByConstraint.set(constraintKey, { table, name: constraintName, columns: [columnName] });
|
|
2182
2419
|
else
|
|
2183
2420
|
entry.columns.push(columnName);
|
|
2184
2421
|
}
|
|
@@ -2225,17 +2462,35 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2225
2462
|
for (const row of indexRows) {
|
|
2226
2463
|
if (row.backing_constraint_type === "x")
|
|
2227
2464
|
continue;
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2230
|
-
name: row.index_name
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2465
|
+
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);
|
|
2466
|
+
if (row.plain_columns === true && operands.length !== Number(row.key_count))
|
|
2467
|
+
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 } });
|
|
2468
|
+
if (operands.length > 0) {
|
|
2469
|
+
if (operands.every((key) => typeof key.column_name === "string"))
|
|
2470
|
+
indexes.push({
|
|
2471
|
+
table: String(row.table_name),
|
|
2472
|
+
name: String(row.index_name),
|
|
2473
|
+
unique: operands[0].unique === true,
|
|
2474
|
+
kind: String(operands[0].method),
|
|
2475
|
+
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) } } })),
|
|
2476
|
+
...typeof operands[0].predicate === "string" ? { where: operands[0].predicate } : {},
|
|
2477
|
+
...operands[0].nulls_not_distinct === true ? { nullsNotDistinct: true } : {}
|
|
2478
|
+
});
|
|
2479
|
+
else {
|
|
2480
|
+
const parsed = parseIndexDef({ table: String(row.table_name), name: String(row.index_name), def: String(row.index_def) });
|
|
2481
|
+
if (parsed !== null)
|
|
2482
|
+
indexes.push(parsed);
|
|
2483
|
+
}
|
|
2484
|
+
} else {
|
|
2485
|
+
const parsed = parseIndexDef({ table: String(row.table_name), name: String(row.index_name), def: String(row.index_def) });
|
|
2486
|
+
if (parsed !== null)
|
|
2487
|
+
indexes.push(parsed);
|
|
2488
|
+
}
|
|
2235
2489
|
}
|
|
2236
2490
|
const joinTables = new Map;
|
|
2237
2491
|
for (const table of tables) {
|
|
2238
|
-
|
|
2492
|
+
const physicalTable = physicalNames.get(table);
|
|
2493
|
+
if (!physicalTable.name.startsWith("_"))
|
|
2239
2494
|
continue;
|
|
2240
2495
|
const tableFks = fkRows.filter((row) => row.table_name === table);
|
|
2241
2496
|
const tableIndexes = indexRows.filter((row) => row.table_name === table);
|
|
@@ -2251,14 +2506,16 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2251
2506
|
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
2507
|
}))
|
|
2253
2508
|
continue;
|
|
2509
|
+
if (tableFks.some((row) => row.foreign_namespace !== undefined && row.foreign_namespace !== physicalTable.namespace))
|
|
2510
|
+
continue;
|
|
2254
2511
|
const detected = detectJoinTable({
|
|
2255
|
-
table,
|
|
2512
|
+
table: physicalTable.name,
|
|
2256
2513
|
columnNames: columns.filter((column) => column.table === table).map((column) => column.name),
|
|
2257
|
-
fkGroups,
|
|
2258
|
-
indexes
|
|
2514
|
+
fkGroups: fkGroups.filter((group) => group.table === table).map((group) => ({ ...group, table: physicalTable.name, refTable: physicalNames.get(group.refTable)?.name ?? group.refTable })),
|
|
2515
|
+
indexes: indexes.filter((index) => index.table === table).map((index) => ({ ...index, table: physicalTable.name }))
|
|
2259
2516
|
});
|
|
2260
2517
|
if (detected !== null)
|
|
2261
|
-
joinTables.set(table, detected);
|
|
2518
|
+
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
2519
|
}
|
|
2263
2520
|
const models = [];
|
|
2264
2521
|
for (const table of tables) {
|
|
@@ -2266,6 +2523,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2266
2523
|
continue;
|
|
2267
2524
|
models.push(buildModel({
|
|
2268
2525
|
table,
|
|
2526
|
+
physicalTable: physicalNames.get(table).name,
|
|
2269
2527
|
isView: false,
|
|
2270
2528
|
columns,
|
|
2271
2529
|
pkByTable,
|
|
@@ -2281,6 +2539,7 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2281
2539
|
for (const view of views) {
|
|
2282
2540
|
models.push(buildModel({
|
|
2283
2541
|
table: view,
|
|
2542
|
+
physicalTable: physicalNames.get(view).name,
|
|
2284
2543
|
isView: true,
|
|
2285
2544
|
columns,
|
|
2286
2545
|
pkByTable,
|
|
@@ -2294,14 +2553,59 @@ async function introspectPhysicalPostgres(params) {
|
|
|
2294
2553
|
}
|
|
2295
2554
|
addFkRelations({ models, fkGroups, joinTables });
|
|
2296
2555
|
addJoinTableRelations({ models, joinTables });
|
|
2556
|
+
const logicalNames = new Set([...physicalNames.values(), ...enumIdentities.values()].map((entry) => entry.name));
|
|
2557
|
+
const logicalName = (params) => {
|
|
2558
|
+
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) }))
|
|
2559
|
+
return params.name;
|
|
2560
|
+
const hex = Array.from(new TextEncoder().encode(params.key), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2561
|
+
let name = `VibePhysical_${hex}`;
|
|
2562
|
+
while (logicalNames.has(name))
|
|
2563
|
+
name += "_";
|
|
2564
|
+
logicalNames.add(name);
|
|
2565
|
+
return name;
|
|
2566
|
+
};
|
|
2567
|
+
const modelNames = new Map(models.map((model) => {
|
|
2568
|
+
const identity = physicalNames.get(model.name);
|
|
2569
|
+
const duplicates = [...physicalNames.values()].filter((entry) => entry.name === identity.name).length > 1;
|
|
2570
|
+
return [model.name, logicalName({ key: model.name, name: identity.name, duplicates })];
|
|
2571
|
+
}));
|
|
2572
|
+
const enumLogicalNames = new Map(enums.map((entry) => {
|
|
2573
|
+
const identity = enumIdentities.get(entry.name);
|
|
2574
|
+
const duplicates = [...enumIdentities.values()].filter((candidate) => candidate.name === identity.name).length > 1;
|
|
2575
|
+
return [entry.name, logicalName({ key: entry.name, name: identity.name, duplicates })];
|
|
2576
|
+
}));
|
|
2577
|
+
const qualifiedModels = models.map((model) => {
|
|
2578
|
+
const identity = physicalNames.get(model.name);
|
|
2579
|
+
const name = modelNames.get(model.name);
|
|
2580
|
+
const { dbName: _physicalName, ...metadata } = model;
|
|
2581
|
+
return {
|
|
2582
|
+
...metadata,
|
|
2583
|
+
name,
|
|
2584
|
+
...name === identity.name ? {} : { dbName: identity.name },
|
|
2585
|
+
...identity.namespace === "public" ? {} : { namespace: identity.namespace },
|
|
2586
|
+
fields: model.fields.map((field) => {
|
|
2587
|
+
if (typeof field.type === "string")
|
|
2588
|
+
return field;
|
|
2589
|
+
const identity = enumIdentities.get(field.type.enum);
|
|
2590
|
+
return { ...field, type: { enum: enumLogicalNames.get(field.type.enum), ...identity.namespace === "public" ? {} : { namespace: identity.namespace } } };
|
|
2591
|
+
}),
|
|
2592
|
+
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 }))
|
|
2593
|
+
};
|
|
2594
|
+
});
|
|
2595
|
+
const qualifiedEnums = enums.map((entry) => {
|
|
2596
|
+
const identity = enumIdentities.get(entry.name);
|
|
2597
|
+
const name = enumLogicalNames.get(entry.name);
|
|
2598
|
+
return { ...entry, name, ...name === identity.name ? {} : { dbName: identity.name }, ...identity.namespace === "public" ? {} : { namespace: identity.namespace } };
|
|
2599
|
+
});
|
|
2297
2600
|
const resolved = await resolvePostgresTypeIdentities({ executor, mode: "catalog", schema: {
|
|
2298
2601
|
datasource: { provider: "postgresql" },
|
|
2299
|
-
models,
|
|
2300
|
-
enums,
|
|
2301
|
-
extensions: []
|
|
2602
|
+
models: qualifiedModels,
|
|
2603
|
+
enums: qualifiedEnums,
|
|
2604
|
+
extensions: [],
|
|
2605
|
+
...namespaceObjects.length === 0 ? {} : { migrationObjects: namespaceObjects }
|
|
2302
2606
|
} });
|
|
2303
2607
|
if (resolved.unavailable.length > 0)
|
|
2304
|
-
throw new
|
|
2608
|
+
throw new VibeError13({ code: "VIBE_MIGRATION", message: "PostgreSQL column type identity could not be read", meta: { reason: "type-inspection-unavailable", columns: resolved.unavailable } });
|
|
2305
2609
|
return resolved.schema;
|
|
2306
2610
|
}
|
|
2307
2611
|
function buildModel(params) {
|
|
@@ -2310,18 +2614,19 @@ function buildModel(params) {
|
|
|
2310
2614
|
const pk = pkByTable.get(table) ?? [];
|
|
2311
2615
|
const uniqueSingles = new Set;
|
|
2312
2616
|
const multiUniques = [];
|
|
2313
|
-
for (const
|
|
2617
|
+
for (const unique of uniquesByConstraint.values()) {
|
|
2618
|
+
const name = unique.name;
|
|
2314
2619
|
if (unique.table !== table)
|
|
2315
2620
|
continue;
|
|
2316
2621
|
if (unique.columns.length === 1)
|
|
2317
2622
|
uniqueSingles.add(unique.columns[0]);
|
|
2318
2623
|
multiUniques.push({ name, columns: unique.columns });
|
|
2319
2624
|
}
|
|
2320
|
-
const tableIndexes = indexes.filter((index) => index.table === table && !constraintNames.has(index.name));
|
|
2625
|
+
const tableIndexes = indexes.filter((index) => index.table === table && !constraintNames.has(JSON.stringify([table, index.name])));
|
|
2321
2626
|
for (const index of tableIndexes) {
|
|
2322
2627
|
if (!index.unique)
|
|
2323
2628
|
continue;
|
|
2324
|
-
if (index.columns.length === 1 && index.where === undefined && index.nullsNotDistinct !== true) {
|
|
2629
|
+
if (index.columns.length === 1 && index.where === undefined && index.nullsNotDistinct !== true && index.columns[0]?.operatorClass === undefined) {
|
|
2325
2630
|
uniqueSingles.add(index.columns[0].name);
|
|
2326
2631
|
}
|
|
2327
2632
|
}
|
|
@@ -2336,7 +2641,7 @@ function buildModel(params) {
|
|
|
2336
2641
|
});
|
|
2337
2642
|
}
|
|
2338
2643
|
for (const index of tableIndexes) {
|
|
2339
|
-
if (index.unique && index.columns.length === 1 && index.where === undefined && index.nullsNotDistinct !== true && index.name === `${
|
|
2644
|
+
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
2645
|
continue;
|
|
2341
2646
|
}
|
|
2342
2647
|
modelIndexes.push({
|
|
@@ -2344,7 +2649,8 @@ function buildModel(params) {
|
|
|
2344
2649
|
kind: index.unique ? "unique" : index.kind,
|
|
2345
2650
|
fields: index.columns.map((column) => ({
|
|
2346
2651
|
name: column.name,
|
|
2347
|
-
...column.desc ? { sort: "desc" } : {}
|
|
2652
|
+
...column.desc ? { sort: "desc" } : {},
|
|
2653
|
+
...column.operatorClass === undefined ? {} : { operatorClass: column.operatorClass }
|
|
2348
2654
|
})),
|
|
2349
2655
|
...index.where === undefined ? {} : { where: index.where },
|
|
2350
2656
|
...index.nullsNotDistinct === true ? { nullsNotDistinct: true } : {}
|
|
@@ -2352,6 +2658,7 @@ function buildModel(params) {
|
|
|
2352
2658
|
}
|
|
2353
2659
|
return {
|
|
2354
2660
|
name: table,
|
|
2661
|
+
dbName: params.physicalTable,
|
|
2355
2662
|
fields,
|
|
2356
2663
|
relations: [],
|
|
2357
2664
|
indexes: modelIndexes,
|
|
@@ -2389,8 +2696,9 @@ function reverseMapType(params) {
|
|
|
2389
2696
|
const { column, enumNames } = params;
|
|
2390
2697
|
const isList = column.dataType === "ARRAY";
|
|
2391
2698
|
const udt = isList ? column.udtName.replace(/^_/, "") : column.udtName;
|
|
2392
|
-
|
|
2393
|
-
|
|
2699
|
+
const enumKey = physicalNameKey({ namespace: column.udtNamespace, name: udt });
|
|
2700
|
+
if (enumNames.has(enumKey)) {
|
|
2701
|
+
return { type: { enum: enumKey }, isList };
|
|
2394
2702
|
}
|
|
2395
2703
|
const canonical = CANONICAL_UDT[udt];
|
|
2396
2704
|
if (canonical !== undefined) {
|
|
@@ -2596,7 +2904,12 @@ function toNumberOrNull(value) {
|
|
|
2596
2904
|
// packages/migrate/src/introspect/mysql.ts
|
|
2597
2905
|
import { exactIntegerLiteral as exactIntegerLiteral2 } from "@vibeorm/schema";
|
|
2598
2906
|
import { mysqlDialect as mysqlDialect2 } from "@vibeorm/sql";
|
|
2599
|
-
|
|
2907
|
+
|
|
2908
|
+
// packages/migrate/src/journal-table.ts
|
|
2909
|
+
var MIGRATIONS_TABLE_NAME = "_vibe_migrations";
|
|
2910
|
+
|
|
2911
|
+
// packages/migrate/src/introspect/mysql.ts
|
|
2912
|
+
var TABLES_QUERY = `
|
|
2600
2913
|
SELECT table_name, engine
|
|
2601
2914
|
FROM information_schema.tables
|
|
2602
2915
|
WHERE table_schema = DATABASE()
|
|
@@ -2604,13 +2917,13 @@ var TABLES_QUERY2 = `
|
|
|
2604
2917
|
AND table_name <> '${MIGRATIONS_TABLE_NAME}'
|
|
2605
2918
|
ORDER BY table_name;
|
|
2606
2919
|
`;
|
|
2607
|
-
var
|
|
2920
|
+
var VIEWS_QUERY = `
|
|
2608
2921
|
SELECT table_name
|
|
2609
2922
|
FROM information_schema.views
|
|
2610
2923
|
WHERE table_schema = DATABASE()
|
|
2611
2924
|
ORDER BY table_name;
|
|
2612
2925
|
`;
|
|
2613
|
-
var
|
|
2926
|
+
var COLUMNS_QUERY = `
|
|
2614
2927
|
SELECT
|
|
2615
2928
|
table_name,
|
|
2616
2929
|
column_name,
|
|
@@ -2641,7 +2954,7 @@ var STATISTICS_QUERY = `
|
|
|
2641
2954
|
WHERE table_schema = DATABASE()
|
|
2642
2955
|
ORDER BY table_name, index_name, seq_in_index;
|
|
2643
2956
|
`;
|
|
2644
|
-
var
|
|
2957
|
+
var FOREIGN_KEYS_QUERY = `
|
|
2645
2958
|
SELECT
|
|
2646
2959
|
constraint_name,
|
|
2647
2960
|
table_name,
|
|
@@ -2661,7 +2974,7 @@ var REFERENTIAL_QUERY = `
|
|
|
2661
2974
|
FROM information_schema.referential_constraints
|
|
2662
2975
|
WHERE constraint_schema = DATABASE();
|
|
2663
2976
|
`;
|
|
2664
|
-
var
|
|
2977
|
+
var CHECKS_QUERY = `
|
|
2665
2978
|
SELECT
|
|
2666
2979
|
tc.table_name AS table_name,
|
|
2667
2980
|
tc.constraint_name AS constraint_name,
|
|
@@ -2688,13 +3001,13 @@ function catalogField(params) {
|
|
|
2688
3001
|
async function introspectMysql(params) {
|
|
2689
3002
|
const { executor } = params;
|
|
2690
3003
|
const [tableRows, viewRows, columnRows, statisticRows, fkRows, refRows, checkRows] = await Promise.all([
|
|
2691
|
-
executor({ text:
|
|
2692
|
-
executor({ text:
|
|
2693
|
-
executor({ text:
|
|
3004
|
+
executor({ text: TABLES_QUERY }),
|
|
3005
|
+
executor({ text: VIEWS_QUERY }),
|
|
3006
|
+
executor({ text: COLUMNS_QUERY }),
|
|
2694
3007
|
executor({ text: STATISTICS_QUERY }),
|
|
2695
|
-
executor({ text:
|
|
3008
|
+
executor({ text: FOREIGN_KEYS_QUERY }),
|
|
2696
3009
|
executor({ text: REFERENTIAL_QUERY }),
|
|
2697
|
-
executor({ text:
|
|
3010
|
+
executor({ text: CHECKS_QUERY })
|
|
2698
3011
|
]);
|
|
2699
3012
|
const checksByTable = new Map;
|
|
2700
3013
|
for (const row of checkRows) {
|
|
@@ -3448,7 +3761,7 @@ function q(name) {
|
|
|
3448
3761
|
}
|
|
3449
3762
|
|
|
3450
3763
|
// packages/migrate/src/execute.ts
|
|
3451
|
-
import { VibeError as
|
|
3764
|
+
import { VibeError as VibeError14 } from "@vibeorm/schema";
|
|
3452
3765
|
var SELF_MANAGED_MARKER = "PRAGMA foreign_keys=OFF";
|
|
3453
3766
|
var FK_CHECK_PREFIX = "PRAGMA foreign_key_check";
|
|
3454
3767
|
function usesSelfManagedTransactions(params) {
|
|
@@ -3489,7 +3802,7 @@ function assertSelfManagedResumeSafe(params) {
|
|
|
3489
3802
|
const completedIndex = params.direction === "up" ? params.start + 1 : -(params.start + 2);
|
|
3490
3803
|
const rewind = selfManagedResumeStart({ sql: params.sql, start: params.start });
|
|
3491
3804
|
const retryIndex = params.direction === "up" ? rewind : -(rewind + 1);
|
|
3492
|
-
throw new
|
|
3805
|
+
throw new VibeError14({
|
|
3493
3806
|
code: "VIBE_MIGRATION",
|
|
3494
3807
|
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
3808
|
meta: { reason: "uncertain-statement", migration: params.migration, statementIndex: params.start, direction: params.direction, dialect: "sqlite" }
|
|
@@ -3518,7 +3831,7 @@ function unsupportedScript(params) {
|
|
|
3518
3831
|
const { migration, direction, index, problem, found } = params;
|
|
3519
3832
|
const subject = direction === "down" ? `the down script of migration "${migration}"` : `migration "${migration}"`;
|
|
3520
3833
|
const position = `${direction === "down" ? "down statement" : "statement"} ${index + 1} of ${params.statements}`;
|
|
3521
|
-
return new
|
|
3834
|
+
return new VibeError14({
|
|
3522
3835
|
code: "VIBE_MIGRATION",
|
|
3523
3836
|
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
3837
|
meta: { reason: "unsupported-self-managed-script", problem, migration, statementIndex: index, direction }
|
|
@@ -3574,7 +3887,7 @@ async function executeStatement(params) {
|
|
|
3574
3887
|
const rows = await executor({ text: statement, values: [] });
|
|
3575
3888
|
if (rows.length > 0) {
|
|
3576
3889
|
const sample = rows.slice(0, 5).map((row) => `${String(row.table)} rowid ${String(row.rowid)} \u2192 ${String(row.parent)}`).join("; ");
|
|
3577
|
-
throw new
|
|
3890
|
+
throw new VibeError14({
|
|
3578
3891
|
code: "VIBE_MIGRATION",
|
|
3579
3892
|
message: `foreign key check failed after table rebuild: ${rows.length} violation(s) \u2014 ${sample}`,
|
|
3580
3893
|
meta: { violations: rows }
|
|
@@ -3593,9 +3906,44 @@ async function cleanupSelfManagedFailure(params) {
|
|
|
3593
3906
|
} catch {}
|
|
3594
3907
|
}
|
|
3595
3908
|
|
|
3909
|
+
// packages/migrate/src/operator-classes.ts
|
|
3910
|
+
import { defaultIndexName as defaultIndexName2, qualifiedSqlName as qualifiedSqlName7 } from "@vibeorm/schema";
|
|
3911
|
+
import { postgresDialect as postgresDialect7, renderPostgresIndexKeys as renderPostgresIndexKeys2 } from "@vibeorm/sql";
|
|
3912
|
+
async function prepareOperatorClassDiff(params) {
|
|
3913
|
+
const models = [];
|
|
3914
|
+
for (const live of params.from.models) {
|
|
3915
|
+
const table = qualifiedSqlName7({ name: live.dbName ?? live.name, namespace: live.namespace });
|
|
3916
|
+
const authored = params.to.models.find((entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace }) === physicalNameKey(table));
|
|
3917
|
+
if (authored === undefined || !authored.indexes.some((index) => index.fields.some((field) => field.operatorClass !== undefined))) {
|
|
3918
|
+
models.push(live);
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
const query = renderPostgresIndexKeys2({ dialect: postgresDialect7, table });
|
|
3922
|
+
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
3923
|
+
const indexes = live.indexes.map((index) => {
|
|
3924
|
+
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);
|
|
3925
|
+
if (declared === undefined)
|
|
3926
|
+
return index;
|
|
3927
|
+
return { ...index, fields: index.fields.map((field, position) => {
|
|
3928
|
+
const requested = declared.fields[position];
|
|
3929
|
+
const operatorClass = requested?.operatorClass;
|
|
3930
|
+
const physicalColumn = authored.fields.find((entry) => entry.name === requested?.name)?.dbName ?? requested?.name;
|
|
3931
|
+
if (field.operatorClass !== undefined || operatorClass === undefined || physicalColumn !== field.name)
|
|
3932
|
+
return field;
|
|
3933
|
+
const row = rows.find((entry) => entry.index_name === index.name && Number(entry.ordinal) === position + 1 && entry.column_name === field.name);
|
|
3934
|
+
if (row?.exact_default !== true || row.operator_namespace !== operatorClass.namespace || row.operator_name !== operatorClass.name || row.method !== (index.kind === "unique" ? "btree" : index.kind))
|
|
3935
|
+
return field;
|
|
3936
|
+
return { ...field, operatorClass };
|
|
3937
|
+
}) };
|
|
3938
|
+
});
|
|
3939
|
+
models.push({ ...live, indexes });
|
|
3940
|
+
}
|
|
3941
|
+
return { ...params.from, models };
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3596
3944
|
// packages/migrate/src/postgres-default-equivalence.ts
|
|
3597
|
-
import { VibeError as
|
|
3598
|
-
import { postgresDialect as
|
|
3945
|
+
import { VibeError as VibeError15 } from "@vibeorm/schema";
|
|
3946
|
+
import { postgresDialect as postgresDialect8, renderPostgresDefaultFunctionIdentity } from "@vibeorm/sql";
|
|
3599
3947
|
var IDENTIFIER = '(?:"(?:[^"\\u0000]|"")+"|[A-Za-z_][A-Za-z_0-9$]*)';
|
|
3600
3948
|
var CALL = new RegExp(`^\\s*(${IDENTIFIER})(?:\\s*\\.\\s*(${IDENTIFIER}))?\\s*\\(\\s*\\)\\s*$`);
|
|
3601
3949
|
function parseCall(params) {
|
|
@@ -3611,9 +3959,9 @@ async function equivalentPostgresDefaultFunction(params) {
|
|
|
3611
3959
|
const observed = parseCall({ expression: params.actual });
|
|
3612
3960
|
if (expected?.namespace === undefined || observed === undefined)
|
|
3613
3961
|
return false;
|
|
3614
|
-
const query = renderPostgresDefaultFunctionIdentity({ dialect:
|
|
3962
|
+
const query = renderPostgresDefaultFunctionIdentity({ dialect: postgresDialect8, table: params.table, column: params.column });
|
|
3615
3963
|
const rows = await params.executor({ text: query.text, values: [...query.values] }).catch((cause) => {
|
|
3616
|
-
throw new
|
|
3964
|
+
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
3965
|
});
|
|
3618
3966
|
if (rows.length !== 1)
|
|
3619
3967
|
return false;
|
|
@@ -3641,8 +3989,8 @@ function factsOf(params) {
|
|
|
3641
3989
|
}
|
|
3642
3990
|
async function inspectSqlObject(params) {
|
|
3643
3991
|
return Promise.resolve().then(async () => {
|
|
3644
|
-
const namespace =
|
|
3645
|
-
const query =
|
|
3992
|
+
const namespace = sqlObjectNamespace2({ identity: params.identity });
|
|
3993
|
+
const query = renderMigrationObjectQuery2({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: params.identity });
|
|
3646
3994
|
if (params.dialect === "mysql")
|
|
3647
3995
|
await assertCatalogVisibility({ executor: params.executor, dialect: params.dialect, namespace });
|
|
3648
3996
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
@@ -3666,7 +4014,7 @@ async function inspectSqlObject(params) {
|
|
|
3666
4014
|
const constraintType = params.identity.constraintType;
|
|
3667
4015
|
const constraint = sqliteTableConstraints2({ sql: String(facts.definition ?? "") }).find((entry) => entry.name === name && entry.type === constraintType);
|
|
3668
4016
|
if (constraint === undefined && constraintType === "primaryKey" && name === `${params.identity.table.name}_pkey`) {
|
|
3669
|
-
const query = renderSqliteColumnInventory({ dialect:
|
|
4017
|
+
const query = renderSqliteColumnInventory({ dialect: getSqlDialect2({ dialect: "sqlite" }), namespace: params.identity.table.namespace, table: params.identity.table.name });
|
|
3670
4018
|
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
4019
|
if (columns.length === 0)
|
|
3672
4020
|
return { state: "missing" };
|
|
@@ -3689,12 +4037,12 @@ async function inspectSqlObject(params) {
|
|
|
3689
4037
|
if (params.identity.kind === "columnDefault")
|
|
3690
4038
|
facts = { definition: facts.definition ?? null };
|
|
3691
4039
|
if (params.dialect === "postgres") {
|
|
3692
|
-
const query = renderMigrationDependencies({ dialect:
|
|
4040
|
+
const query = renderMigrationDependencies({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: params.identity });
|
|
3693
4041
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
3694
4042
|
return postgresInspection({ identity: params.identity, facts, addresses: rows.map((row) => row.address) });
|
|
3695
4043
|
}
|
|
3696
4044
|
if (params.dialect === "mysql") {
|
|
3697
|
-
const query = renderMysqlObjectMembers({ dialect:
|
|
4045
|
+
const query = renderMysqlObjectMembers({ dialect: getSqlDialect2({ dialect: params.dialect }), identity: params.identity });
|
|
3698
4046
|
if (query !== undefined) {
|
|
3699
4047
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
3700
4048
|
if (params.identity.kind === "function" && rows.length === 0)
|
|
@@ -3708,7 +4056,7 @@ async function inspectSqlObject(params) {
|
|
|
3708
4056
|
}
|
|
3709
4057
|
}
|
|
3710
4058
|
return { state: "present", definition: { identity: params.identity, facts, dependencies: [], format: "catalog-v2" } };
|
|
3711
|
-
}).catch((error) => ({ state: "unavailable", reason: error instanceof
|
|
4059
|
+
}).catch((error) => ({ state: "unavailable", reason: error instanceof VibeError16 ? String(error.meta.feature ?? error.code) : "catalog-query-failed" }));
|
|
3712
4060
|
}
|
|
3713
4061
|
function postgresInspection(params) {
|
|
3714
4062
|
if (!params.addresses.every((address) => typeof address === "string"))
|
|
@@ -3723,7 +4071,7 @@ function postgresInspection(params) {
|
|
|
3723
4071
|
return { state: "present", definition: { identity: params.identity, facts, dependencies, format: "catalog-v2" } };
|
|
3724
4072
|
}
|
|
3725
4073
|
function unavailableReason(params) {
|
|
3726
|
-
return params.error instanceof
|
|
4074
|
+
return params.error instanceof VibeError16 ? String(params.error.meta.feature ?? params.error.meta.reason ?? params.error.code) : "catalog-query-failed";
|
|
3727
4075
|
}
|
|
3728
4076
|
async function readBatch(params) {
|
|
3729
4077
|
const positions = new Set(params.batch.positions);
|
|
@@ -3731,7 +4079,7 @@ async function readBatch(params) {
|
|
|
3731
4079
|
const rows = new Map;
|
|
3732
4080
|
for (const row of await params.executor({ text: params.batch.text, values: [...params.batch.values] })) {
|
|
3733
4081
|
if (typeof row.position !== "number" || !Number.isInteger(row.position) || !positions.has(row.position))
|
|
3734
|
-
throw new
|
|
4082
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Invalid batched catalog identity", meta: { reason: "catalog-query-failed" } });
|
|
3735
4083
|
const matches = rows.get(row.position) ?? [];
|
|
3736
4084
|
matches.push(row);
|
|
3737
4085
|
rows.set(row.position, matches);
|
|
@@ -3759,7 +4107,7 @@ async function inspectSqlObjects(params) {
|
|
|
3759
4107
|
}
|
|
3760
4108
|
return Promise.resolve().then(async () => {
|
|
3761
4109
|
const results = params.identities.map(() => ({ state: "unavailable", reason: "catalog-query-failed" }));
|
|
3762
|
-
for (const batch of renderMigrationObjectBatch({ dialect:
|
|
4110
|
+
for (const batch of renderMigrationObjectBatch({ dialect: getSqlDialect2({ dialect: params.dialect }), identities: params.identities })) {
|
|
3763
4111
|
const outcome = await readBatch({ executor: params.executor, batch });
|
|
3764
4112
|
for (const position of batch.positions) {
|
|
3765
4113
|
results[position] = outcome.ok ? batchedInspection({ identity: params.identities[position], matches: outcome.rows.get(position) ?? [] }) : { state: "unavailable", reason: outcome.reason };
|
|
@@ -3771,12 +4119,12 @@ async function inspectSqlObjects(params) {
|
|
|
3771
4119
|
async function assertCatalogVisibility(params) {
|
|
3772
4120
|
if (params.dialect !== "mysql")
|
|
3773
4121
|
return;
|
|
3774
|
-
const query = renderMigrationVisibility({ dialect:
|
|
4122
|
+
const query = renderMigrationVisibility({ dialect: getSqlDialect2({ dialect: params.dialect }), namespace: params.namespace });
|
|
3775
4123
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
3776
4124
|
const grants = new Set(rows.map((row) => row.privilege));
|
|
3777
4125
|
const missing = ["SELECT", "TRIGGER", "SHOW VIEW", "EXECUTE"].filter((privilege) => !grants.has(privilege));
|
|
3778
4126
|
if (missing.length > 0)
|
|
3779
|
-
throw new
|
|
4127
|
+
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
4128
|
}
|
|
3781
4129
|
var CATALOG_V1_OMITTED_FACTS = Object.freeze({
|
|
3782
4130
|
function: ["definerOwner"],
|
|
@@ -3794,27 +4142,29 @@ function compare(params) {
|
|
|
3794
4142
|
if (expected === undefined !== (actual === undefined) || canonicalMigrationJson({ value: expected }) !== canonicalMigrationJson({ value: actual }))
|
|
3795
4143
|
differences.push({ attribute: key, expected, actual });
|
|
3796
4144
|
}
|
|
3797
|
-
const expected = params.expected.dependencies.map((identity) =>
|
|
3798
|
-
const actual = params.live.dependencies.map((identity) =>
|
|
4145
|
+
const expected = params.expected.dependencies.map((identity) => sqlObjectKey5({ identity })).sort();
|
|
4146
|
+
const actual = params.live.dependencies.map((identity) => sqlObjectKey5({ identity })).sort();
|
|
3799
4147
|
if (JSON.stringify(expected) !== JSON.stringify(actual))
|
|
3800
4148
|
differences.push({ attribute: "dependencies", expected, actual });
|
|
3801
4149
|
return differences;
|
|
3802
4150
|
}
|
|
3803
4151
|
function assertProbeMode(params) {
|
|
3804
4152
|
if (params.probe !== undefined && params.probe !== "none" && DIALECT_CAPABILITIES2[params.dialect].checkNormalization !== params.probe)
|
|
3805
|
-
throw new
|
|
4153
|
+
throw new VibeError16({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `${params.probe} CHECK probes are unavailable on ${params.dialect}`, meta: { feature: "checkProbe", dialect: params.dialect } });
|
|
3806
4154
|
}
|
|
3807
4155
|
async function verifyMigrationState(params) {
|
|
3808
4156
|
return verifyState(params);
|
|
3809
4157
|
}
|
|
3810
4158
|
async function verifyState(params) {
|
|
4159
|
+
if (params.external !== undefined)
|
|
4160
|
+
params = { ...params, expected: projectExternalState({ state: params.expected, external: params.external }) };
|
|
3811
4161
|
assertProbeMode({ dialect: params.dialect, probe: params.probe });
|
|
3812
4162
|
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) => [
|
|
4163
|
+
const owned = new Set(declarations.map((object) => sqlObjectKey5({ identity: object.identity })));
|
|
4164
|
+
const definitions = new Map(params.expected.definitions.map((definition) => [sqlObjectKey5({ identity: definition.identity }), definition]));
|
|
4165
|
+
const identities = new Map(params.expected.definitions.map((definition) => [sqlObjectKey5({ identity: definition.identity }), definition.identity]));
|
|
3816
4166
|
for (const object of declarations)
|
|
3817
|
-
identities.set(
|
|
4167
|
+
identities.set(sqlObjectKey5({ identity: object.identity }), object.identity);
|
|
3818
4168
|
const objects = [];
|
|
3819
4169
|
const inspections = params.batch === true ? await inspectSqlObjects({ ...params, identities: [...identities.values()] }) : undefined;
|
|
3820
4170
|
let position = 0;
|
|
@@ -3825,6 +4175,8 @@ async function verifyState(params) {
|
|
|
3825
4175
|
position++;
|
|
3826
4176
|
if (live.state !== "present") {
|
|
3827
4177
|
objects.push({ identity, ownership, state: live.state, differences: [], ...live.state === "unavailable" ? { reason: live.reason } : {} });
|
|
4178
|
+
} else if (expected === undefined && identity.kind === "namespace") {
|
|
4179
|
+
objects.push({ identity, ownership, state: "matching", differences: [] });
|
|
3828
4180
|
} else if (expected === undefined) {
|
|
3829
4181
|
objects.push({ identity, ownership, state: "unavailable", reason: "independent-definition-required", differences: [] });
|
|
3830
4182
|
} else if (params.dialect === "postgres" && identity.kind === "table" && (expected.facts.persistence === undefined || expected.facts.options === undefined)) {
|
|
@@ -3834,23 +4186,31 @@ async function verifyState(params) {
|
|
|
3834
4186
|
objects.push({ identity, ownership, state: differences.length === 0 ? "matching" : "changed", differences });
|
|
3835
4187
|
}
|
|
3836
4188
|
}
|
|
3837
|
-
if (params.expected.schema.models.length > 0 || params.expected.schema.enums.length > 0)
|
|
4189
|
+
if (params.dialect === "postgres" && schemaNamespaces({ schema: params.expected.schema }).length > 0 || params.expected.schema.models.length > 0 || params.expected.schema.enums.length > 0)
|
|
3838
4190
|
objects.push(...await verifySchemaObjects(params));
|
|
3839
4191
|
if (params.dialect === "postgres")
|
|
3840
4192
|
objects.push(...await verifyExclusions(params));
|
|
3841
4193
|
return { inSync: objects.every((object) => object.state === "matching"), objects };
|
|
3842
4194
|
}
|
|
3843
4195
|
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 });
|
|
4196
|
+
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
4197
|
const resolved = params.dialect === "postgres" ? await resolvePostgresTypeIdentities({ executor: params.executor, schema: params.expected.schema, mode: "verify" }) : { schema: params.expected.schema, unavailable: [] };
|
|
3846
4198
|
const expected = normalizeSchema({ schema: resolved.schema, dialect: params.dialect });
|
|
3847
|
-
const
|
|
4199
|
+
const physicalComparison = params.external === undefined ? physical : projectExternalSchema({ schema: physical, external: params.external });
|
|
4200
|
+
const comparable = params.dialect === "postgres" ? await prepareOperatorClassDiff({ executor: params.executor, from: physicalComparison, to: resolved.schema }) : physicalComparison;
|
|
4201
|
+
const actual = normalizeSchema({ schema: comparable, dialect: params.dialect });
|
|
3848
4202
|
const objects = resolved.unavailable.map((identity) => ({ identity, ownership: "schema", state: "unavailable", reason: "type-reference-required", differences: [] }));
|
|
3849
4203
|
const namespace = await migrationNamespace({ executor: params.executor, dialect: params.dialect });
|
|
3850
4204
|
const sqliteTableOptions = params.dialect === "sqlite" && expected.tables.size > 0 ? await params.executor({ text: "PRAGMA main.table_list;", values: [] }) : undefined;
|
|
3851
4205
|
if (params.dialect === "postgres")
|
|
3852
|
-
for (const
|
|
3853
|
-
const identity = { kind: "
|
|
4206
|
+
for (const name of schemaNamespaces({ schema: params.expected.schema })) {
|
|
4207
|
+
const identity = { kind: "namespace", name };
|
|
4208
|
+
const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
4209
|
+
objects.push({ identity, ownership: "schema", state: inspection.state === "present" ? "matching" : inspection.state, differences: [], ...inspection.state === "unavailable" ? { reason: inspection.reason } : {} });
|
|
4210
|
+
}
|
|
4211
|
+
if (params.dialect === "postgres")
|
|
4212
|
+
for (const desired of expected.enums.values()) {
|
|
4213
|
+
const identity = { kind: "enum", name: qualifiedSqlName8({ name: desired.dbName ?? desired.name, namespace: desired.namespace, defaultNamespace: namespace }) };
|
|
3854
4214
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
3855
4215
|
if (live.state !== "present") {
|
|
3856
4216
|
objects.push({ identity, ownership: "schema", state: live.state, differences: [], ...live.state === "unavailable" ? { reason: live.reason } : {} });
|
|
@@ -3864,9 +4224,10 @@ async function verifySchemaObjects(params) {
|
|
|
3864
4224
|
const matching = canonicalMigrationJson({ value: desired.values }) === canonicalMigrationJson({ value: values });
|
|
3865
4225
|
objects.push({ identity, ownership: "schema", state: matching ? "matching" : "changed", differences: matching ? [] : [{ attribute: "values", expected: desired.values, actual: values }] });
|
|
3866
4226
|
}
|
|
3867
|
-
for (const [
|
|
3868
|
-
const
|
|
3869
|
-
const
|
|
4227
|
+
for (const [key, desired] of expected.tables) {
|
|
4228
|
+
const name = desired.table;
|
|
4229
|
+
const table = qualifiedSqlName8({ name, namespace: desired.namespace, defaultNamespace: namespace });
|
|
4230
|
+
const live = actual.tables.get(key);
|
|
3870
4231
|
if (live === undefined) {
|
|
3871
4232
|
const presence = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity: { kind: "table", name: table } });
|
|
3872
4233
|
objects.push({ identity: { kind: "table", name: table }, ownership: "schema", state: presence.state === "missing" ? "missing" : "unavailable", ...presence.state === "missing" ? {} : { reason: "schema-definition-not-visible" }, differences: [] });
|
|
@@ -3892,7 +4253,7 @@ async function verifySchemaObjects(params) {
|
|
|
3892
4253
|
if (params.dialect === "mysql" && field.field.collation !== undefined) {
|
|
3893
4254
|
pin(`columns.${column}.collation`, field.field.collation.toLowerCase(), stored?.field.collation?.toLowerCase());
|
|
3894
4255
|
}
|
|
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);
|
|
4256
|
+
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
4257
|
if (!ownedDefault) {
|
|
3897
4258
|
const desiredDefault = field.field.default;
|
|
3898
4259
|
const actualDefault = stored?.field.default;
|
|
@@ -3903,10 +4264,10 @@ async function verifySchemaObjects(params) {
|
|
|
3903
4264
|
}
|
|
3904
4265
|
for (const [key, fk] of desired.fks) {
|
|
3905
4266
|
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 });
|
|
4267
|
+
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
4268
|
if (params.dialect === "postgres" && stored !== undefined) {
|
|
3908
4269
|
const identity = { kind: "constraint", constraintType: "foreignKey", table, name: stored.constraintName };
|
|
3909
|
-
if (params.expected.definitions.some((definition) =>
|
|
4270
|
+
if (params.expected.definitions.some((definition) => sqlObjectKey5({ identity: definition.identity }) === sqlObjectKey5({ identity })))
|
|
3910
4271
|
continue;
|
|
3911
4272
|
const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
3912
4273
|
if (inspection.state !== "present") {
|
|
@@ -3922,7 +4283,7 @@ async function verifySchemaObjects(params) {
|
|
|
3922
4283
|
if (params.dialect !== "postgres" || !live.indexes.has(key))
|
|
3923
4284
|
continue;
|
|
3924
4285
|
const identity = { kind: "index", table, name: live.indexes.get(key).name };
|
|
3925
|
-
if (params.expected.definitions.some((definition) =>
|
|
4286
|
+
if (params.expected.definitions.some((definition) => sqlObjectKey5({ identity: definition.identity }) === sqlObjectKey5({ identity })))
|
|
3926
4287
|
continue;
|
|
3927
4288
|
const inspection = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
3928
4289
|
if (inspection.state !== "present") {
|
|
@@ -3935,7 +4296,7 @@ async function verifySchemaObjects(params) {
|
|
|
3935
4296
|
objects.push({ identity: { kind: "table", name: table }, ownership: "schema", state: differences.length === 0 ? "matching" : "changed", differences });
|
|
3936
4297
|
for (const [check, expression] of desired.checks) {
|
|
3937
4298
|
const identity = { kind: "constraint", constraintType: "check", table, name: check };
|
|
3938
|
-
if (params.expected.definitions.some((definition) =>
|
|
4299
|
+
if (params.expected.definitions.some((definition) => sqlObjectKey5({ identity: definition.identity }) === sqlObjectKey5({ identity })))
|
|
3939
4300
|
continue;
|
|
3940
4301
|
const stored = live.checks.get(check);
|
|
3941
4302
|
if (stored === undefined) {
|
|
@@ -3975,13 +4336,14 @@ async function verifySchemaObjects(params) {
|
|
|
3975
4336
|
}
|
|
3976
4337
|
for (const [name, join] of expected.joinTables) {
|
|
3977
4338
|
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 } };
|
|
4339
|
+
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
4340
|
const matching = canonicalMigrationJson({ value: shape(join) }) === canonicalMigrationJson({ value: shape(stored) });
|
|
3980
|
-
objects.push({ identity: { kind: "table", name: { namespace,
|
|
4341
|
+
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
4342
|
}
|
|
3982
|
-
for (const [
|
|
3983
|
-
|
|
3984
|
-
|
|
4343
|
+
for (const [key, view] of expected.views) {
|
|
4344
|
+
const name = qualifiedSqlName8({ name: view.dbName ?? view.name, namespace: view.namespace, defaultNamespace: namespace });
|
|
4345
|
+
if (!params.expected.definitions.some((definition) => definition.identity.kind === "view" && definition.identity.name.namespace === name.namespace && definition.identity.name.name === name.name))
|
|
4346
|
+
objects.push({ identity: { kind: "view", name }, ownership: "schema", state: actual.views.has(key) ? "unavailable" : "missing", reason: "independent-definition-required", differences: [] });
|
|
3985
4347
|
}
|
|
3986
4348
|
return objects;
|
|
3987
4349
|
}
|
|
@@ -3990,16 +4352,25 @@ async function verifyMigrationTransition(params) {
|
|
|
3990
4352
|
}
|
|
3991
4353
|
async function verifySqliteTransitionBeforeRestore(params) {
|
|
3992
4354
|
if (params.dialect !== "sqlite")
|
|
3993
|
-
throw new
|
|
4355
|
+
throw new VibeError16({ code: "VIBE_INTERNAL", message: "SQLite rebuild verification requires the SQLite dialect" });
|
|
3994
4356
|
await executeStatement({ executor: params.executor, dialect: "sqlite", statement: "PRAGMA foreign_key_check;" });
|
|
3995
4357
|
return verifyTransition({ ...params, sqliteRestorePending: true });
|
|
3996
4358
|
}
|
|
3997
4359
|
async function verifyTransition(params) {
|
|
3998
|
-
const
|
|
4360
|
+
const external = params.external;
|
|
4361
|
+
if (external !== undefined) {
|
|
4362
|
+
for (const identity of params.removals ?? []) {
|
|
4363
|
+
const owner = externalObjectOwner({ identity, external });
|
|
4364
|
+
if (owner !== undefined)
|
|
4365
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Transition removals cannot target externally owned objects", meta: { reason: "external-owned-removal", owner, identity } });
|
|
4366
|
+
}
|
|
4367
|
+
params = { ...params, from: projectExternalState({ state: params.from, external }), to: projectExternalState({ state: params.to, external }) };
|
|
4368
|
+
}
|
|
4369
|
+
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
4370
|
const namespace = await migrationNamespace(params);
|
|
4000
4371
|
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(
|
|
4372
|
+
const retained = new Set(identities(params.to).map((identity) => sqlObjectKey5({ identity })));
|
|
4373
|
+
const removed = new Map([...identities(params.from).filter((identity) => identity.kind !== "namespace" && !retained.has(sqlObjectKey5({ identity }))), ...params.removals ?? []].map((identity) => [sqlObjectKey5({ identity }), identity]));
|
|
4003
4374
|
const objects = [...report.objects];
|
|
4004
4375
|
for (const identity of removed.values()) {
|
|
4005
4376
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
@@ -4012,16 +4383,16 @@ async function verifyTransition(params) {
|
|
|
4012
4383
|
async function migrationNamespace(params) {
|
|
4013
4384
|
if (params.dialect !== "mysql")
|
|
4014
4385
|
return params.dialect === "sqlite" ? "main" : "public";
|
|
4015
|
-
const query = renderMigrationNamespace({ dialect:
|
|
4386
|
+
const query = renderMigrationNamespace({ dialect: getSqlDialect2({ dialect: params.dialect }) });
|
|
4016
4387
|
const rows = await params.executor({ text: query.text, values: [] });
|
|
4017
4388
|
const namespace = rows[0]?.namespace;
|
|
4018
4389
|
if (typeof namespace !== "string" || namespace.length === 0)
|
|
4019
|
-
throw new
|
|
4390
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Cannot determine the migration namespace", meta: { reason: "inspection-unavailable" } });
|
|
4020
4391
|
return namespace;
|
|
4021
4392
|
}
|
|
4022
4393
|
function assertMigrationState(params) {
|
|
4023
4394
|
if (!params.report.inSync)
|
|
4024
|
-
throw new
|
|
4395
|
+
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
4396
|
}
|
|
4026
4397
|
async function deparsePostgresCheck(params) {
|
|
4027
4398
|
const definition = await deparsePostgresCheckDefinition({ ...params, constraint: "vibeorm_check_probe" });
|
|
@@ -4031,11 +4402,11 @@ async function deparsePostgresCheckDefinition(params) {
|
|
|
4031
4402
|
const name = "vibeorm_check_probe";
|
|
4032
4403
|
await params.executor({ text: "SAVEPOINT vibeorm_check_probe" });
|
|
4033
4404
|
try {
|
|
4034
|
-
const query = renderCheckProbe({ dialect:
|
|
4405
|
+
const query = renderCheckProbe({ dialect: getSqlDialect2({ dialect: "postgres" }), table: params.table, expression: params.expression, name });
|
|
4035
4406
|
await params.executor({ text: query.text });
|
|
4036
4407
|
const result = await inspectSqlObject({ executor: params.executor, dialect: "postgres", identity: { kind: "constraint", constraintType: "check", table: params.table, name } });
|
|
4037
4408
|
if (result.state !== "present" || typeof result.definition.facts.definition !== "string")
|
|
4038
|
-
throw new
|
|
4409
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "CHECK normalization probe unavailable", meta: { reason: "check-inspection-unavailable" } });
|
|
4039
4410
|
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
4411
|
} finally {
|
|
4041
4412
|
await params.executor({ text: "ROLLBACK TO SAVEPOINT vibeorm_check_probe" });
|
|
@@ -4044,7 +4415,7 @@ async function deparsePostgresCheckDefinition(params) {
|
|
|
4044
4415
|
}
|
|
4045
4416
|
async function deparseMysqlCheck(params) {
|
|
4046
4417
|
const probe = `vibe_check_${randomUUID2().replaceAll("-", "")}`;
|
|
4047
|
-
const dialect =
|
|
4418
|
+
const dialect = getSqlDialect2({ dialect: "mysql" });
|
|
4048
4419
|
const clone = renderMysqlCheckProbe({ dialect, probe, operation: "clone", table: params.table });
|
|
4049
4420
|
await params.executor({ text: clone.text });
|
|
4050
4421
|
let created = true;
|
|
@@ -4053,7 +4424,7 @@ async function deparseMysqlCheck(params) {
|
|
|
4053
4424
|
const source = await params.executor({ text: read.text, values: [] });
|
|
4054
4425
|
const definition = source[0]?.["Create Table"];
|
|
4055
4426
|
if (typeof definition !== "string")
|
|
4056
|
-
throw new
|
|
4427
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Cannot read temporary clone definition" });
|
|
4057
4428
|
const drop = renderMysqlCheckProbe({ dialect, probe, operation: "drop" });
|
|
4058
4429
|
await params.executor({ text: drop.text });
|
|
4059
4430
|
created = false;
|
|
@@ -4064,7 +4435,7 @@ async function deparseMysqlCheck(params) {
|
|
|
4064
4435
|
const normalizedDefinition = rows[0]?.["Create Table"];
|
|
4065
4436
|
const check = typeof normalizedDefinition === "string" ? parseSqliteNamedChecks({ tableSql: normalizedDefinition }).find((entry) => entry.name === probe) : undefined;
|
|
4066
4437
|
if (check === undefined)
|
|
4067
|
-
throw new
|
|
4438
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "MySQL temporary CHECK deparse unavailable", meta: { reason: "check-inspection-unavailable" } });
|
|
4068
4439
|
return check.expression;
|
|
4069
4440
|
} finally {
|
|
4070
4441
|
if (created) {
|
|
@@ -4078,7 +4449,7 @@ async function prepareCheckDiff(params) {
|
|
|
4078
4449
|
const models = [];
|
|
4079
4450
|
for (const model of params.from.models) {
|
|
4080
4451
|
const table = model.dbName ?? model.name;
|
|
4081
|
-
const target = params.to.models.find((entry) => (entry.dbName ?? entry.name) === table);
|
|
4452
|
+
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
4453
|
const checks = [];
|
|
4083
4454
|
for (const check of model.checks ?? []) {
|
|
4084
4455
|
const next = target?.checks?.find((entry) => entry.name === check.name);
|
|
@@ -4086,12 +4457,12 @@ async function prepareCheckDiff(params) {
|
|
|
4086
4457
|
checks.push({ name: check.name, expression: check.expression });
|
|
4087
4458
|
continue;
|
|
4088
4459
|
}
|
|
4089
|
-
const identity = { kind: "constraint", constraintType: "check", table: { namespace: await migrationNamespace({ executor: params.executor, dialect: params.dialect })
|
|
4460
|
+
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
4461
|
if (params.dialect === "postgres" && params.probe === "rollback") {
|
|
4091
4462
|
const desired = await deparsePostgresCheck({ executor: params.executor, table: identity.table, expression: next.expression, probe: "rollback" });
|
|
4092
4463
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
4093
4464
|
if (live.state !== "present")
|
|
4094
|
-
throw new
|
|
4465
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Live CHECK inspection unavailable", meta: { reason: "check-inspection-unavailable", identity } });
|
|
4095
4466
|
if (live.definition.facts.definition === desired) {
|
|
4096
4467
|
checks.push(next);
|
|
4097
4468
|
continue;
|
|
@@ -4103,20 +4474,20 @@ async function prepareCheckDiff(params) {
|
|
|
4103
4474
|
const desired = await deparseMysqlCheck({ executor: params.executor, table: identity.table, expression: next.expression, probe: "temporary" });
|
|
4104
4475
|
const live = await inspectSqlObject({ executor: params.executor, dialect: params.dialect, identity });
|
|
4105
4476
|
if (live.state !== "present")
|
|
4106
|
-
throw new
|
|
4477
|
+
throw new VibeError16({ code: "VIBE_MIGRATION", message: "Live CHECK inspection unavailable", meta: { reason: "check-inspection-unavailable", identity } });
|
|
4107
4478
|
checks.push(desired === live.definition.facts.definition ? next : { name: check.name, expression: check.expression });
|
|
4108
4479
|
continue;
|
|
4109
4480
|
}
|
|
4110
|
-
const previous = params.previous?.schema.models.find((entry) => (entry.dbName ?? entry.name) === table)?.checks?.find((entry) => entry.name === check.name);
|
|
4481
|
+
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
4482
|
if (previous !== undefined) {
|
|
4112
4483
|
checks.push(previous.expression === next.expression ? next : { name: check.name, expression: check.expression });
|
|
4113
4484
|
continue;
|
|
4114
4485
|
}
|
|
4115
|
-
if (params.replaceChecks?.some((entry) =>
|
|
4486
|
+
if (params.replaceChecks?.some((entry) => sqlObjectKey5({ identity: entry }) === sqlObjectKey5({ identity }))) {
|
|
4116
4487
|
checks.push({ name: check.name, expression: check.expression });
|
|
4117
4488
|
continue;
|
|
4118
4489
|
}
|
|
4119
|
-
throw new
|
|
4490
|
+
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
4491
|
}
|
|
4121
4492
|
models.push({ ...model, checks });
|
|
4122
4493
|
}
|
|
@@ -4126,7 +4497,7 @@ async function prepareCheckDiff(params) {
|
|
|
4126
4497
|
// packages/migrate/src/inventory.ts
|
|
4127
4498
|
async function inspectMigrationInventory(params) {
|
|
4128
4499
|
await assertCatalogVisibility(params);
|
|
4129
|
-
const query = renderMigrationInventory({ dialect:
|
|
4500
|
+
const query = renderMigrationInventory({ dialect: getSqlDialect3({ dialect: params.dialect }), namespace: params.namespace });
|
|
4130
4501
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
4131
4502
|
const objects = [];
|
|
4132
4503
|
const kinds = {
|
|
@@ -4143,14 +4514,18 @@ async function inspectMigrationInventory(params) {
|
|
|
4143
4514
|
for (const row of rows) {
|
|
4144
4515
|
const name = row.name;
|
|
4145
4516
|
if (typeof name !== "string" || typeof row.kind !== "string")
|
|
4146
|
-
throw new
|
|
4517
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "Malformed catalog identity", meta: { reason: "inventory-unavailable" } });
|
|
4147
4518
|
const qualified = { namespace: params.namespace, name };
|
|
4148
4519
|
const table = { namespace: params.namespace, name: String(row.parent ?? "") };
|
|
4149
4520
|
let identity;
|
|
4150
4521
|
switch (row.kind) {
|
|
4522
|
+
case "namespace":
|
|
4523
|
+
identity = { kind: "namespace", name };
|
|
4524
|
+
break;
|
|
4151
4525
|
case "table":
|
|
4152
4526
|
case "view":
|
|
4153
4527
|
case "sequence":
|
|
4528
|
+
case "enum":
|
|
4154
4529
|
identity = { kind: row.kind, name: qualified };
|
|
4155
4530
|
break;
|
|
4156
4531
|
case "trigger":
|
|
@@ -4165,27 +4540,27 @@ async function inspectMigrationInventory(params) {
|
|
|
4165
4540
|
case "constraint": {
|
|
4166
4541
|
const constraintType = kinds[String(row.subtype)];
|
|
4167
4542
|
if (constraintType === undefined)
|
|
4168
|
-
throw new
|
|
4543
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "Unknown constraint catalog kind", meta: { reason: "inventory-unavailable" } });
|
|
4169
4544
|
identity = { kind: "constraint", table, name, constraintType };
|
|
4170
4545
|
break;
|
|
4171
4546
|
}
|
|
4172
4547
|
case "function": {
|
|
4173
4548
|
let argumentTypes = JSON.parse(String(row.signature));
|
|
4174
4549
|
if (params.dialect === "mysql") {
|
|
4175
|
-
const query = renderMysqlObjectMembers2({ dialect:
|
|
4550
|
+
const query = renderMysqlObjectMembers2({ dialect: getSqlDialect3({ dialect: params.dialect }), identity: { kind: "function", name: qualified, argumentTypes: [] } });
|
|
4176
4551
|
const members = await params.executor({ text: query.text, values: [...query.values] });
|
|
4177
4552
|
if (members.length === 0)
|
|
4178
|
-
throw new
|
|
4553
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "MySQL function signature unavailable", meta: { reason: "inventory-unavailable", name } });
|
|
4179
4554
|
argumentTypes = members.filter((member) => member.mode !== null).map((member) => ({ namespace: "mysql", name: String(member.type) }));
|
|
4180
4555
|
}
|
|
4181
4556
|
const candidate = { kind: "function", name: qualified, argumentTypes };
|
|
4182
4557
|
if (!isSqlObjectIdentity2(candidate))
|
|
4183
|
-
throw new
|
|
4558
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: "Invalid function signature in catalog", meta: { reason: "inventory-unavailable" } });
|
|
4184
4559
|
identity = candidate;
|
|
4185
4560
|
break;
|
|
4186
4561
|
}
|
|
4187
4562
|
default:
|
|
4188
|
-
throw new
|
|
4563
|
+
throw new VibeError17({ code: "VIBE_MIGRATION", message: `Unknown catalog object kind ${row.kind}`, meta: { reason: "inventory-unavailable" } });
|
|
4189
4564
|
}
|
|
4190
4565
|
objects.push({ identity, ...typeof row.backing === "string" ? { backingConstraint: row.backing } : {} });
|
|
4191
4566
|
if (params.dialect === "sqlite" && row.kind === "table" && typeof row.definition === "string" && !/^\s*CREATE\s+VIRTUAL\s+TABLE\b/i.test(row.definition)) {
|
|
@@ -4201,11 +4576,11 @@ async function inspectMigrationInventory(params) {
|
|
|
4201
4576
|
objects.push({ identity: { kind: "constraint", constraintType: "check", table: qualified, name: check.name } });
|
|
4202
4577
|
if ([...row.definition.matchAll(/\bCHECK\s*\(/gi)].length > checks.length)
|
|
4203
4578
|
objects.push({ identity: { kind: "table", name: qualified }, inspectionUnavailable: "sqlite-unnamed-check-definition-unavailable" });
|
|
4204
|
-
const query = renderSqliteColumnInventory2({ dialect:
|
|
4579
|
+
const query = renderSqliteColumnInventory2({ dialect: getSqlDialect3({ dialect: params.dialect }), namespace: params.namespace, table: name });
|
|
4205
4580
|
const columns = await params.executor({ text: query.text, values: [...query.values] });
|
|
4206
4581
|
if (columns.some((column) => Number(column.pk) > 0) && !clauses.some((clause) => clause.type === "primaryKey"))
|
|
4207
4582
|
objects.push({ identity: { kind: "constraint", constraintType: "primaryKey", table: qualified, name: defaultPrimaryKeyName2({ table: qualified.name, dialect: "sqlite" }) } });
|
|
4208
|
-
const fkQuery = renderSqliteForeignKeyInventory({ dialect:
|
|
4583
|
+
const fkQuery = renderSqliteForeignKeyInventory({ dialect: getSqlDialect3({ dialect: params.dialect }), namespace: params.namespace, table: name });
|
|
4209
4584
|
const foreignKeys = await params.executor({ text: fkQuery.text, values: [...fkQuery.values] });
|
|
4210
4585
|
const groups = new Map;
|
|
4211
4586
|
for (const fk of foreignKeys)
|
|
@@ -4224,48 +4599,47 @@ async function inspectMigrationInventory(params) {
|
|
|
4224
4599
|
}
|
|
4225
4600
|
}
|
|
4226
4601
|
}
|
|
4227
|
-
return [...new Map(objects.map((object) => [
|
|
4602
|
+
return [...new Map(objects.map((object) => [sqlObjectKey6({ identity: object.identity }), object])).values()];
|
|
4228
4603
|
}
|
|
4229
4604
|
|
|
4230
4605
|
// packages/migrate/src/module-ownership.ts
|
|
4231
4606
|
function refusal(params) {
|
|
4232
|
-
return new
|
|
4607
|
+
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
4608
|
}
|
|
4234
4609
|
function identities(params) {
|
|
4235
4610
|
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
4611
|
}
|
|
4237
4612
|
function moduleNamespace(params) {
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
const first = params.origin.owned[0];
|
|
4241
|
-
if (first === undefined)
|
|
4613
|
+
const namespaces = [...new Set(params.origin.owned.map((identity) => sqlObjectNamespace3({ identity })))];
|
|
4614
|
+
if (namespaces.length > 1 || namespaces.length === 0 && params.origin.dialect === "mysql")
|
|
4242
4615
|
throw refusal({ reason: "module-namespace-required", moduleId: params.origin.moduleId });
|
|
4243
|
-
return "
|
|
4616
|
+
return namespaces[0] ?? (params.origin.dialect === "sqlite" ? "main" : "public");
|
|
4244
4617
|
}
|
|
4245
4618
|
function moduleOwnedObjects(params) {
|
|
4246
4619
|
const { desiredState } = params;
|
|
4620
|
+
if (params.namespace === undefined && desiredState.dialect === "mysql")
|
|
4621
|
+
throw refusal({ reason: "module-namespace-required" });
|
|
4247
4622
|
const namespace = params.namespace ?? (desiredState.dialect === "sqlite" ? "main" : "public");
|
|
4248
4623
|
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) => [
|
|
4624
|
+
return [...new Map(all.map((identity) => [sqlObjectKey7({ identity }), identity])).values()].sort((a, b) => sqlObjectKey7({ identity: a }).localeCompare(sqlObjectKey7({ identity: b })));
|
|
4250
4625
|
}
|
|
4251
4626
|
function assertModuleOwnership(params) {
|
|
4252
4627
|
const { origin } = params;
|
|
4253
4628
|
const state = origin.source.desiredState;
|
|
4254
4629
|
const namespace = moduleNamespace({ origin });
|
|
4255
4630
|
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(
|
|
4631
|
+
const owned = new Set(origin.owned.map((identity) => sqlObjectKey7({ identity })));
|
|
4632
|
+
if (expected.length !== owned.size || expected.some((identity) => !owned.has(sqlObjectKey7({ identity }))))
|
|
4258
4633
|
throw refusal({ reason: "module-ownership-invalid", moduleId: origin.moduleId });
|
|
4259
4634
|
for (const identity of expected) {
|
|
4260
|
-
|
|
4261
|
-
if (name?.namespace !== namespace || name.name === "_vibe_migrations")
|
|
4635
|
+
if (sqlObjectNamespace3({ identity }) !== namespace || ("table" in identity ? identity.table.name : typeof identity.name === "object" ? identity.name.name : undefined) === "_vibe_migrations")
|
|
4262
4636
|
throw refusal({ reason: "module-ownership-invalid", moduleId: origin.moduleId, identity });
|
|
4263
4637
|
}
|
|
4264
4638
|
for (const target of [state.from, state.to, ...state.down === undefined ? [] : [state.down]]) {
|
|
4265
4639
|
if (target.schema.rls !== undefined || target.schema.extensions.length > 0)
|
|
4266
4640
|
throw refusal({ reason: "module-schema-unsupported", moduleId: origin.moduleId });
|
|
4267
4641
|
for (const dependency of [...target.definitions.flatMap((definition) => definition.dependencies), ...(target.schema.migrationObjects ?? []).flatMap((object) => object.dependsOn), ...state.unmanagedRemovals ?? []]) {
|
|
4268
|
-
if (!owned.has(
|
|
4642
|
+
if (!owned.has(sqlObjectKey7({ identity: dependency })))
|
|
4269
4643
|
throw refusal({ reason: "module-external-dependency", moduleId: origin.moduleId, identity: dependency });
|
|
4270
4644
|
}
|
|
4271
4645
|
}
|
|
@@ -4273,28 +4647,29 @@ function assertModuleOwnership(params) {
|
|
|
4273
4647
|
return;
|
|
4274
4648
|
const local = params.desiredState;
|
|
4275
4649
|
const rest = (actual, source) => {
|
|
4276
|
-
const
|
|
4277
|
-
const
|
|
4278
|
-
const
|
|
4279
|
-
const
|
|
4650
|
+
const keyOf = (entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect: origin.dialect });
|
|
4651
|
+
const tables = new Set(source.schema.models.map(keyOf));
|
|
4652
|
+
const enums = new Set(source.schema.enums.map(keyOf));
|
|
4653
|
+
const selected = actual.schema.models.filter((model) => tables.has(keyOf(model)));
|
|
4654
|
+
const selectedEnums = actual.schema.enums.filter((entry) => enums.has(keyOf(entry)));
|
|
4280
4655
|
if (canonicalMigrationJson({ value: selected }) !== canonicalMigrationJson({ value: source.schema.models }) || canonicalMigrationJson({ value: selectedEnums }) !== canonicalMigrationJson({ value: source.schema.enums }))
|
|
4281
4656
|
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(
|
|
4657
|
+
const selectedObjects = (actual.schema.migrationObjects ?? []).filter((object) => owned.has(sqlObjectKey7({ identity: object.identity })));
|
|
4658
|
+
const selectedDefinitions = actual.definitions.filter((definition) => owned.has(sqlObjectKey7({ identity: definition.identity })));
|
|
4284
4659
|
if (canonicalMigrationJson({ value: selectedObjects }) !== canonicalMigrationJson({ value: source.schema.migrationObjects ?? [] }) || canonicalMigrationJson({ value: selectedDefinitions }) !== canonicalMigrationJson({ value: source.definitions }))
|
|
4285
4660
|
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
|
|
4661
|
+
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
4662
|
};
|
|
4288
4663
|
const before = canonicalMigrationJson({ value: rest(local.from, state.from) });
|
|
4289
4664
|
if (before !== canonicalMigrationJson({ value: rest(local.to, state.to) }) || before !== canonicalMigrationJson({ value: rest(local.down ?? local.from, state.down ?? state.from) }))
|
|
4290
4665
|
throw refusal({ reason: "module-mutates-application-state", moduleId: origin.moduleId });
|
|
4291
4666
|
}
|
|
4292
4667
|
function dependencyUnavailable(params) {
|
|
4293
|
-
return new
|
|
4668
|
+
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
4669
|
}
|
|
4295
4670
|
async function assertCrossNamespaceModuleDependencies(params) {
|
|
4296
4671
|
const { executor, origin } = params;
|
|
4297
|
-
const dialect =
|
|
4672
|
+
const dialect = getSqlDialect4({ dialect: origin.dialect });
|
|
4298
4673
|
const query = renderModuleDependencyQuery({ dialect, operation: { kind: "foreignKeys", namespace: params.namespace, tables: [...params.tables] } });
|
|
4299
4674
|
if (query === undefined)
|
|
4300
4675
|
return;
|
|
@@ -4321,15 +4696,15 @@ async function assertCrossNamespaceModuleDependencies(params) {
|
|
|
4321
4696
|
async function assertLiveModuleOwnership(params) {
|
|
4322
4697
|
const { executor, origin } = params;
|
|
4323
4698
|
const namespace = moduleNamespace({ origin });
|
|
4324
|
-
if (await migrationNamespace({ executor, dialect: origin.dialect }) !== namespace)
|
|
4699
|
+
if (origin.dialect !== "postgres" && await migrationNamespace({ executor, dialect: origin.dialect }) !== namespace)
|
|
4325
4700
|
throw refusal({ reason: "module-placement-mismatch", moduleId: origin.moduleId });
|
|
4326
4701
|
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) =>
|
|
4702
|
+
const before = new Set(identities({ state: origin.source.desiredState.from, dialect: origin.dialect, namespace }).map((identity) => sqlObjectKey7({ identity })));
|
|
4703
|
+
const owned = new Set(origin.owned.map((identity) => sqlObjectKey7({ identity })));
|
|
4329
4704
|
const tables = new Set(origin.owned.flatMap((identity) => identity.kind === "table" ? [identity.name.name] : []));
|
|
4330
4705
|
await assertCrossNamespaceModuleDependencies({ executor, origin, namespace, tables });
|
|
4331
4706
|
for (const object of inventory) {
|
|
4332
|
-
const key =
|
|
4707
|
+
const key = sqlObjectKey7({ identity: object.identity });
|
|
4333
4708
|
if (params.resume !== true && owned.has(key) && !before.has(key))
|
|
4334
4709
|
throw refusal({ reason: "module-object-collision", moduleId: origin.moduleId, identity: object.identity });
|
|
4335
4710
|
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 +4712,7 @@ async function assertLiveModuleOwnership(params) {
|
|
|
4337
4712
|
if (object.inspectionUnavailable !== undefined && (owned.has(key) || ("table" in object.identity) && tables.has(object.identity.table.name)))
|
|
4338
4713
|
throw refusal({ reason: "module-dependency-unavailable", moduleId: origin.moduleId, identity: object.identity });
|
|
4339
4714
|
}
|
|
4340
|
-
const physical = origin.dialect === "postgres" ? await introspectPhysicalPostgres({ executor }) : origin.dialect === "mysql" ? await introspectMysql({ executor }) : params.sqliteRestorePending === true ? await introspectSqliteBeforeForeignKeyRestore({ executor }) : await introspectSqlite({ executor });
|
|
4715
|
+
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
4716
|
for (const model of physical.models)
|
|
4342
4717
|
for (const relation of model.relations) {
|
|
4343
4718
|
const target = physical.models.find((candidate) => candidate.name === relation.targetModel);
|
|
@@ -4352,12 +4727,51 @@ async function assertApplicationModuleState(params) {
|
|
|
4352
4727
|
const options = { executor: params.executor, dialect: origin.dialect, from: state.from, to: state.to, removals: state.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } };
|
|
4353
4728
|
const report = params.sqliteRestorePending === true ? await verifySqliteTransitionBeforeRestore(options) : await verifyMigrationTransition(options);
|
|
4354
4729
|
if (!report.inSync)
|
|
4355
|
-
throw new
|
|
4730
|
+
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
4731
|
}
|
|
4357
4732
|
}
|
|
4358
4733
|
|
|
4734
|
+
// packages/migrate/src/module-adoption.ts
|
|
4735
|
+
import { VibeError as VibeError19 } from "@vibeorm/schema";
|
|
4736
|
+
var MODULE_ADOPTION_METADATA = Object.freeze({ filename: "module-adoption.json", version: 1 });
|
|
4737
|
+
function invalid3() {
|
|
4738
|
+
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" } });
|
|
4739
|
+
}
|
|
4740
|
+
function assertMarker(params) {
|
|
4741
|
+
const value = params.value;
|
|
4742
|
+
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))
|
|
4743
|
+
throw invalid3();
|
|
4744
|
+
}
|
|
4745
|
+
function assertModuleAdoption(params) {
|
|
4746
|
+
if (params.migration.moduleAdoption === undefined)
|
|
4747
|
+
return;
|
|
4748
|
+
assertMarker({ value: params.migration.moduleAdoption });
|
|
4749
|
+
if (params.migration.moduleOrigin === undefined)
|
|
4750
|
+
throw invalid3();
|
|
4751
|
+
assertModuleOrigin({ value: params.migration.moduleOrigin });
|
|
4752
|
+
}
|
|
4753
|
+
function parseModuleAdoption(params) {
|
|
4754
|
+
return parseMigrationMetadata({ text: params.text, failure: invalid3, parse: (value) => {
|
|
4755
|
+
const checked = { value };
|
|
4756
|
+
assertMarker(checked);
|
|
4757
|
+
return Object.freeze(checked.value);
|
|
4758
|
+
} });
|
|
4759
|
+
}
|
|
4760
|
+
function moduleAdoptionChecksumSuffix(params) {
|
|
4761
|
+
if (params.moduleAdoption === undefined)
|
|
4762
|
+
return "";
|
|
4763
|
+
assertModuleAdoption({ migration: params });
|
|
4764
|
+
return `
|
|
4765
|
+
-- vibeorm:module-adoption ${canonicalMigrationJson({ value: params.moduleAdoption })}`;
|
|
4766
|
+
}
|
|
4767
|
+
function serializeModuleAdoption(params) {
|
|
4768
|
+
assertModuleAdoption(params);
|
|
4769
|
+
return params.migration.moduleAdoption === undefined ? undefined : JSON.stringify(params.migration.moduleAdoption, null, 2) + `
|
|
4770
|
+
`;
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4359
4773
|
// packages/migrate/src/sql-statements.ts
|
|
4360
|
-
import { VibeError as
|
|
4774
|
+
import { VibeError as VibeError20 } from "@vibeorm/schema";
|
|
4361
4775
|
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
4776
|
var BLOCK_OPENERS = new Set(["BEGIN", "CASE", "IF", "LOOP", "WHILE", "REPEAT"]);
|
|
4363
4777
|
function splitSqlStatements(params) {
|
|
@@ -4433,7 +4847,7 @@ function splitSqlStatements(params) {
|
|
|
4433
4847
|
continue;
|
|
4434
4848
|
}
|
|
4435
4849
|
if (dialect === "mysql" || dialect === undefined) {
|
|
4436
|
-
throw new
|
|
4850
|
+
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
4851
|
}
|
|
4438
4852
|
}
|
|
4439
4853
|
if (value === endQuote) {
|
|
@@ -4507,15 +4921,15 @@ function joinSqlStatements(params) {
|
|
|
4507
4921
|
|
|
4508
4922
|
// packages/migrate/src/module-origin.ts
|
|
4509
4923
|
import { createHash } from "crypto";
|
|
4510
|
-
import { VibeError as
|
|
4924
|
+
import { VibeError as VibeError21, isSqlObjectIdentity as isSqlObjectIdentity3, sqlObjectKey as sqlObjectKey8 } from "@vibeorm/schema";
|
|
4511
4925
|
var MODULE_ORIGIN_METADATA = Object.freeze({ filename: "module-origin.json", version: 1 });
|
|
4512
|
-
function
|
|
4513
|
-
return new
|
|
4926
|
+
function invalid4() {
|
|
4927
|
+
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
4928
|
}
|
|
4515
|
-
function
|
|
4929
|
+
function record3(value) {
|
|
4516
4930
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4517
4931
|
}
|
|
4518
|
-
function
|
|
4932
|
+
function identifier2(value) {
|
|
4519
4933
|
return typeof value === "string" && value.trim().length > 0 && !value.includes("\x00");
|
|
4520
4934
|
}
|
|
4521
4935
|
function strings(value) {
|
|
@@ -4533,17 +4947,17 @@ function freezeData(params) {
|
|
|
4533
4947
|
}
|
|
4534
4948
|
function assertModuleOrigin(params) {
|
|
4535
4949
|
const value = params.value;
|
|
4536
|
-
if (!
|
|
4537
|
-
throw
|
|
4538
|
-
if (new Set(value.owned.map((identity) =>
|
|
4539
|
-
throw
|
|
4950
|
+
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))
|
|
4951
|
+
throw invalid4();
|
|
4952
|
+
if (new Set(value.owned.map((identity) => sqlObjectKey8({ identity }))).size !== value.owned.length)
|
|
4953
|
+
throw invalid4();
|
|
4540
4954
|
const desired = { value: value.source.desiredState, migration: value.source.name };
|
|
4541
4955
|
assertDesiredState(desired);
|
|
4542
4956
|
if (desired.value.dialect !== value.dialect)
|
|
4543
|
-
throw
|
|
4957
|
+
throw invalid4();
|
|
4544
4958
|
const { artifactChecksum, ...payload } = value;
|
|
4545
4959
|
if (checksum({ value: payload }) !== artifactChecksum)
|
|
4546
|
-
throw
|
|
4960
|
+
throw invalid4();
|
|
4547
4961
|
assertModuleOwnership({ origin: value });
|
|
4548
4962
|
}
|
|
4549
4963
|
function createModuleOrigin(params) {
|
|
@@ -4566,7 +4980,7 @@ function parseModuleOrigin(params) {
|
|
|
4566
4980
|
const checked = { value };
|
|
4567
4981
|
assertModuleOrigin(checked);
|
|
4568
4982
|
return checked.value;
|
|
4569
|
-
}, failure:
|
|
4983
|
+
}, failure: invalid4 });
|
|
4570
4984
|
}
|
|
4571
4985
|
function assertMaterializedModuleMigration(params) {
|
|
4572
4986
|
const { migration } = params;
|
|
@@ -4574,7 +4988,7 @@ function assertMaterializedModuleMigration(params) {
|
|
|
4574
4988
|
return;
|
|
4575
4989
|
const restored = restoreModuleMigration({ migration, origin: migration.moduleOrigin });
|
|
4576
4990
|
if (migration.desiredState === undefined || canonicalMigrationJson({ value: restored }) !== canonicalMigrationJson({ value: migration }))
|
|
4577
|
-
throw new
|
|
4991
|
+
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
4992
|
}
|
|
4579
4993
|
function restoreModuleMigration(params) {
|
|
4580
4994
|
const { migration, origin } = params;
|
|
@@ -4585,25 +4999,27 @@ function restoreModuleMigration(params) {
|
|
|
4585
4999
|
continue;
|
|
4586
5000
|
const normalized = (sql) => sql.flatMap((text) => splitSqlStatements({ sql: text, dialect: origin.dialect }));
|
|
4587
5001
|
if (canonicalMigrationJson({ value: normalized(actual) }) !== canonicalMigrationJson({ value: normalized(origin.source[key] ?? []) }))
|
|
4588
|
-
throw
|
|
5002
|
+
throw invalid4();
|
|
4589
5003
|
}
|
|
4590
5004
|
for (const key of ["execution", "downExecution", "rls", "securityChanges", "downSecurityChanges"]) {
|
|
4591
5005
|
if (migration[key] !== undefined && canonicalMigrationJson({ value: migration[key] }) !== canonicalMigrationJson({ value: origin.source[key] }))
|
|
4592
|
-
throw
|
|
5006
|
+
throw invalid4();
|
|
4593
5007
|
}
|
|
4594
|
-
|
|
5008
|
+
const restored = { ...origin.source, name: migration.name, ...migration.desiredState === undefined ? {} : { desiredState: migration.desiredState }, moduleOrigin: origin, ...migration.moduleAdoption === undefined ? {} : { moduleAdoption: migration.moduleAdoption } };
|
|
5009
|
+
assertModuleAdoption({ migration: restored });
|
|
5010
|
+
return restored;
|
|
4595
5011
|
}
|
|
4596
5012
|
|
|
4597
5013
|
// packages/migrate/src/fs.ts
|
|
4598
5014
|
import { readFile, readdir } from "fs/promises";
|
|
4599
5015
|
import { join } from "path";
|
|
4600
|
-
import { VibeError as
|
|
5016
|
+
import { VibeError as VibeError36, assertValidSchemaIR as assertValidSchemaIR4 } from "@vibeorm/schema";
|
|
4601
5017
|
|
|
4602
5018
|
// packages/migrate/src/migration-checksum.ts
|
|
4603
5019
|
import { createHash as createHash2 } from "crypto";
|
|
4604
5020
|
|
|
4605
5021
|
// packages/migrate/src/execution.ts
|
|
4606
|
-
import { VibeError as
|
|
5022
|
+
import { VibeError as VibeError22 } from "@vibeorm/schema";
|
|
4607
5023
|
var EXECUTION_SIDECAR_FILENAME = "execution.json";
|
|
4608
5024
|
var EXECUTION_SIDECAR_VERSION = 1;
|
|
4609
5025
|
function isDefaultExecution(params) {
|
|
@@ -4641,8 +5057,8 @@ function executionChecksumSuffix(params) {
|
|
|
4641
5057
|
${lines.join(`
|
|
4642
5058
|
`)}`;
|
|
4643
5059
|
}
|
|
4644
|
-
function
|
|
4645
|
-
return new
|
|
5060
|
+
function invalid5(params) {
|
|
5061
|
+
return new VibeError22({
|
|
4646
5062
|
code: "VIBE_MIGRATION",
|
|
4647
5063
|
message: `invalid execution metadata for migration "${params.migration}": ${params.detail}`,
|
|
4648
5064
|
meta: { reason: "execution-metadata-invalid", migration: params.migration }
|
|
@@ -4653,22 +5069,22 @@ function assertValidExecution(params) {
|
|
|
4653
5069
|
if (execution === undefined)
|
|
4654
5070
|
return;
|
|
4655
5071
|
if (execution.transaction !== "default" && execution.transaction !== "none") {
|
|
4656
|
-
throw
|
|
5072
|
+
throw invalid5({ migration, detail: `unknown transaction mode ${JSON.stringify(execution.transaction)}` });
|
|
4657
5073
|
}
|
|
4658
5074
|
for (const [label, value] of [["lockTimeoutMs", execution.lockTimeoutMs], ["statementTimeoutMs", execution.statementTimeoutMs]]) {
|
|
4659
5075
|
if (value === undefined)
|
|
4660
5076
|
continue;
|
|
4661
5077
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
4662
|
-
throw
|
|
5078
|
+
throw invalid5({ migration, detail: `${label} must be a non-negative integer number of milliseconds` });
|
|
4663
5079
|
}
|
|
4664
5080
|
}
|
|
4665
5081
|
const seen = new Set;
|
|
4666
5082
|
for (const binding of execution.operations ?? []) {
|
|
4667
5083
|
if (!Number.isSafeInteger(binding.statement) || binding.statement < 0 || binding.statement >= statements) {
|
|
4668
|
-
throw
|
|
5084
|
+
throw invalid5({ migration, detail: `operation refers to statement ${String(binding.statement)}, which is outside the migration's ${String(statements)} statement(s)` });
|
|
4669
5085
|
}
|
|
4670
5086
|
if (seen.has(binding.statement)) {
|
|
4671
|
-
throw
|
|
5087
|
+
throw invalid5({ migration, detail: `statement ${String(binding.statement)} carries more than one declared operation` });
|
|
4672
5088
|
}
|
|
4673
5089
|
seen.add(binding.statement);
|
|
4674
5090
|
}
|
|
@@ -4693,7 +5109,7 @@ function parseExecutionSidecar(params) {
|
|
|
4693
5109
|
try {
|
|
4694
5110
|
parsed = JSON.parse(params.text);
|
|
4695
5111
|
} catch (error) {
|
|
4696
|
-
throw new
|
|
5112
|
+
throw new VibeError22({
|
|
4697
5113
|
code: "VIBE_MIGRATION",
|
|
4698
5114
|
message: `invalid execution metadata for migration "${migration}": not valid JSON`,
|
|
4699
5115
|
meta: { reason: "execution-metadata-invalid", migration },
|
|
@@ -4701,10 +5117,10 @@ function parseExecutionSidecar(params) {
|
|
|
4701
5117
|
});
|
|
4702
5118
|
}
|
|
4703
5119
|
if (parsed === null || typeof parsed !== "object")
|
|
4704
|
-
throw
|
|
5120
|
+
throw invalid5({ migration, detail: "expected a JSON object" });
|
|
4705
5121
|
const body = parsed;
|
|
4706
5122
|
if (body.version !== EXECUTION_SIDECAR_VERSION) {
|
|
4707
|
-
throw
|
|
5123
|
+
throw invalid5({ migration, detail: `unsupported sidecar version ${String(body.version)} (this build reads version ${String(EXECUTION_SIDECAR_VERSION)})` });
|
|
4708
5124
|
}
|
|
4709
5125
|
const up = body.up === undefined ? undefined : body.up;
|
|
4710
5126
|
const down = body.down === undefined ? undefined : body.down;
|
|
@@ -4786,22 +5202,25 @@ function describeMigrationRisks(params) {
|
|
|
4786
5202
|
function migrationChecksum(params) {
|
|
4787
5203
|
const suffix = executionChecksumSuffix({ ...params.execution === undefined ? {} : { execution: params.execution } });
|
|
4788
5204
|
return createHash2("sha256").update(params.sql.join(`
|
|
4789
|
-
`) + suffix + moduleOriginChecksumSuffix(params) + (params.desiredStatePending === true ? `
|
|
5205
|
+
`) + suffix + moduleOriginChecksumSuffix(params) + moduleAdoptionChecksumSuffix(params) + (params.desiredStatePending === true ? `
|
|
4790
5206
|
-- vibeorm:desired-state unfinalized-v1` : "") + desiredStateChecksumSuffix({ ...params.desiredState === undefined ? {} : { desiredState: params.desiredState } }) + (params.desiredState === undefined ? "" : `
|
|
4791
5207
|
-- vibeorm:down ${JSON.stringify(params.downSql ?? null)}${executionChecksumSuffix({ ...params.downExecution === undefined ? {} : { execution: params.downExecution } })}`)).digest("hex");
|
|
4792
5208
|
}
|
|
4793
5209
|
|
|
4794
5210
|
// packages/migrate/src/shadow-guard.ts
|
|
4795
|
-
import { VibeError as
|
|
4796
|
-
import { getSqlDialect as
|
|
5211
|
+
import { VibeError as VibeError23 } from "@vibeorm/schema";
|
|
5212
|
+
import { getSqlDialect as getSqlDialect5, renderShadowOperation } from "@vibeorm/sql";
|
|
4797
5213
|
var replayExecutors = new WeakSet;
|
|
5214
|
+
function isShadowReplayExecutor(params) {
|
|
5215
|
+
return replayExecutors.has(params.executor);
|
|
5216
|
+
}
|
|
4798
5217
|
async function assertDatabaseNotShadow(params) {
|
|
4799
5218
|
if (params.dialect === "sqlite" || replayExecutors.has(params.executor))
|
|
4800
5219
|
return;
|
|
4801
|
-
const [query] = renderShadowOperation({ dialect:
|
|
5220
|
+
const [query] = renderShadowOperation({ dialect: getSqlDialect5({ dialect: params.dialect }), operation: { kind: "markerExists" } });
|
|
4802
5221
|
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
4803
5222
|
if (rows.length !== 0)
|
|
4804
|
-
throw new
|
|
5223
|
+
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
5224
|
}
|
|
4806
5225
|
function runShadowReplay(params) {
|
|
4807
5226
|
replayExecutors.add(params.executor);
|
|
@@ -4810,10 +5229,95 @@ function runShadowReplay(params) {
|
|
|
4810
5229
|
});
|
|
4811
5230
|
}
|
|
4812
5231
|
|
|
5232
|
+
// packages/migrate/src/external-dependencies.ts
|
|
5233
|
+
import { VibeError as VibeError24, unsupportedCapability as unsupportedCapability8 } from "@vibeorm/schema";
|
|
5234
|
+
import { getSqlDialect as getSqlDialect6, renderExternalDependents } from "@vibeorm/sql";
|
|
5235
|
+
function externalDependencyTargets(params) {
|
|
5236
|
+
return params.steps.flatMap((step) => {
|
|
5237
|
+
const namespace = step.namespace ?? params.namespace;
|
|
5238
|
+
switch (step.kind) {
|
|
5239
|
+
case "dropEnum":
|
|
5240
|
+
return [{ kind: "type", namespace, name: step.enumName, dependency: { kind: "enum", name: { namespace, name: step.enumName } } }];
|
|
5241
|
+
case "dropTable":
|
|
5242
|
+
case "dropJoinTable":
|
|
5243
|
+
return [{ kind: "relation", namespace, name: step.table, dependency: { kind: "table", name: { namespace, name: step.table } } }];
|
|
5244
|
+
case "dropView":
|
|
5245
|
+
return [{ kind: "relation", namespace, name: step.table, dependency: { kind: "view", name: { namespace, name: step.table } } }];
|
|
5246
|
+
case "dropColumn":
|
|
5247
|
+
return [{ kind: "column", namespace, name: step.table, column: step.column, dependency: { kind: "column", table: { namespace, name: step.table }, column: step.column } }];
|
|
5248
|
+
case "alterColumn":
|
|
5249
|
+
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 } }] : [];
|
|
5250
|
+
default:
|
|
5251
|
+
return [];
|
|
5252
|
+
}
|
|
5253
|
+
});
|
|
5254
|
+
}
|
|
5255
|
+
function unavailable() {
|
|
5256
|
+
return new VibeError24({ code: "VIBE_MIGRATION", message: "Malformed catalog dependency row", meta: { reason: "external-inventory-unavailable" } });
|
|
5257
|
+
}
|
|
5258
|
+
function text(params) {
|
|
5259
|
+
const value = params.row[params.key];
|
|
5260
|
+
if (value === null || value === undefined)
|
|
5261
|
+
return;
|
|
5262
|
+
if (typeof value !== "string")
|
|
5263
|
+
throw unavailable();
|
|
5264
|
+
return value;
|
|
5265
|
+
}
|
|
5266
|
+
function dependentOwner(params) {
|
|
5267
|
+
const { row, external } = params;
|
|
5268
|
+
const relationNamespace = text({ row, key: "relationNamespace" });
|
|
5269
|
+
const relationName = text({ row, key: "relationName" });
|
|
5270
|
+
const relationKind = text({ row, key: "relationKind" });
|
|
5271
|
+
const column = text({ row, key: "columnName" });
|
|
5272
|
+
const dependentClass = text({ row, key: "dependentClass" });
|
|
5273
|
+
const objectNamespace = text({ row, key: "objectNamespace" });
|
|
5274
|
+
const objectName = text({ row, key: "objectName" });
|
|
5275
|
+
if (relationNamespace !== undefined && relationName !== undefined) {
|
|
5276
|
+
const name = { namespace: relationNamespace, name: relationName };
|
|
5277
|
+
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 };
|
|
5278
|
+
const owner = externalObjectOwner({ identity: anchor, external });
|
|
5279
|
+
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;
|
|
5280
|
+
return { ...owner === undefined ? {} : { owner }, ...identity === undefined ? {} : { identity } };
|
|
5281
|
+
}
|
|
5282
|
+
const anchor = dependentClass === "pg_proc" && objectNamespace !== undefined && objectName !== undefined ? { kind: "function", name: { namespace: objectNamespace, name: objectName }, argumentTypes: [] } : objectNamespace === undefined ? undefined : { kind: "namespace", name: objectNamespace };
|
|
5283
|
+
const owner = anchor === undefined ? undefined : externalObjectOwner({ identity: anchor, external });
|
|
5284
|
+
return owner === undefined ? {} : { owner };
|
|
5285
|
+
}
|
|
5286
|
+
function assertNoExternalDependents(params) {
|
|
5287
|
+
for (const row of params.rows) {
|
|
5288
|
+
const target = typeof row.position === "number" ? params.targets[row.position] : undefined;
|
|
5289
|
+
const type = text({ row, key: "dependentType" });
|
|
5290
|
+
const identity = text({ row, key: "dependentIdentity" });
|
|
5291
|
+
const referenced = text({ row, key: "referenced" });
|
|
5292
|
+
if (target === undefined || type === undefined || identity === undefined || referenced === undefined)
|
|
5293
|
+
throw unavailable();
|
|
5294
|
+
const dependent = dependentOwner({ row, external: params.external });
|
|
5295
|
+
if (dependent.owner === undefined)
|
|
5296
|
+
continue;
|
|
5297
|
+
throw new VibeError24({
|
|
5298
|
+
code: "VIBE_MIGRATION",
|
|
5299
|
+
message: "An externally owned object depends on an application object this change drops or retypes; automatic DDL is refused",
|
|
5300
|
+
meta: { reason: "external-owned-dependency", owner: dependent.owner, ...dependent.identity === undefined ? {} : { identity: dependent.identity }, dependent: { type, identity }, dependency: target.dependency, referenced }
|
|
5301
|
+
});
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
async function refuseExternalDependents(params) {
|
|
5305
|
+
if (params.external.declarations.length === 0)
|
|
5306
|
+
return;
|
|
5307
|
+
if (params.dialect !== "postgres")
|
|
5308
|
+
throw unsupportedCapability8({ dialect: params.dialect, feature: "externalObjects" });
|
|
5309
|
+
const targets = externalDependencyTargets({ steps: params.steps, namespace: params.namespace });
|
|
5310
|
+
if (targets.length === 0)
|
|
5311
|
+
return;
|
|
5312
|
+
const query = renderExternalDependents({ dialect: getSqlDialect6({ dialect: params.dialect }), targets: targets.map(({ dependency, ...target }) => target) });
|
|
5313
|
+
const rows = await params.executor({ text: query.text, values: [...query.values] });
|
|
5314
|
+
assertNoExternalDependents({ external: params.external, targets, rows });
|
|
5315
|
+
}
|
|
5316
|
+
|
|
4813
5317
|
// packages/migrate/src/module-history.ts
|
|
4814
|
-
import { VibeError as
|
|
5318
|
+
import { VibeError as VibeError25, sqlObjectKey as sqlObjectKey9, providerToDialect as providerToDialect3 } from "@vibeorm/schema";
|
|
4815
5319
|
function refusal2(params) {
|
|
4816
|
-
return new
|
|
5320
|
+
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
5321
|
}
|
|
4818
5322
|
function moduleHistoryHeads(params) {
|
|
4819
5323
|
const heads = new Map;
|
|
@@ -4848,20 +5352,22 @@ function applicationModuleOrigins(params) {
|
|
|
4848
5352
|
return origins;
|
|
4849
5353
|
}
|
|
4850
5354
|
function preserves(params) {
|
|
4851
|
-
const
|
|
4852
|
-
const
|
|
4853
|
-
const
|
|
4854
|
-
const
|
|
5355
|
+
const dialect = providerToDialect3({ provider: params.expected.schema.datasource.provider });
|
|
5356
|
+
const keyOf = (entry) => physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect });
|
|
5357
|
+
const tables = new Set(params.expected.schema.models.map(keyOf));
|
|
5358
|
+
const enums = new Set(params.expected.schema.enums.map(keyOf));
|
|
5359
|
+
const models = params.actual.schema.models.filter((model) => tables.has(keyOf(model)));
|
|
5360
|
+
const declaredEnums = params.actual.schema.enums.filter((entry) => enums.has(keyOf(entry)));
|
|
4855
5361
|
if (canonicalMigrationJson({ value: models }) !== canonicalMigrationJson({ value: params.expected.schema.models }) || canonicalMigrationJson({ value: declaredEnums }) !== canonicalMigrationJson({ value: params.expected.schema.enums }))
|
|
4856
5362
|
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(
|
|
5363
|
+
const objectKeys = new Set((params.expected.schema.migrationObjects ?? []).map((object) => sqlObjectKey9({ identity: object.identity })));
|
|
5364
|
+
const definitionKeys = new Set(params.expected.definitions.map((definition) => sqlObjectKey9({ identity: definition.identity })));
|
|
5365
|
+
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
5366
|
throw refusal2({ reason: "module-application-mutation", moduleId: params.moduleId });
|
|
4861
5367
|
for (const model of params.actual.schema.models)
|
|
4862
5368
|
for (const relation of model.relations) {
|
|
4863
5369
|
const target = params.actual.schema.models.find((candidate) => candidate.name === relation.targetModel);
|
|
4864
|
-
if (target !== undefined && tables.has(model
|
|
5370
|
+
if (target !== undefined && tables.has(keyOf(model)) !== tables.has(keyOf(target)))
|
|
4865
5371
|
throw refusal2({ reason: "module-external-dependency", moduleId: params.moduleId });
|
|
4866
5372
|
}
|
|
4867
5373
|
}
|
|
@@ -4871,7 +5377,7 @@ function assertModuleUpgradeHistory(params) {
|
|
|
4871
5377
|
const register = (migration) => {
|
|
4872
5378
|
const origin = migration.moduleOrigin;
|
|
4873
5379
|
for (const identity of origin.owned) {
|
|
4874
|
-
const key =
|
|
5380
|
+
const key = sqlObjectKey9({ identity });
|
|
4875
5381
|
if (owners.has(key) && owners.get(key) !== origin.moduleId)
|
|
4876
5382
|
throw refusal2({ reason: "module-object-collision", moduleId: origin.moduleId });
|
|
4877
5383
|
owners.set(key, origin.moduleId);
|
|
@@ -4918,11 +5424,11 @@ function assertModuleUpgradeHistory(params) {
|
|
|
4918
5424
|
}
|
|
4919
5425
|
|
|
4920
5426
|
// packages/migrate/src/module-journal.ts
|
|
4921
|
-
import { VibeError as
|
|
4922
|
-
import { getSqlDialect as
|
|
5427
|
+
import { VibeError as VibeError27 } from "@vibeorm/schema";
|
|
5428
|
+
import { getSqlDialect as getSqlDialect7, MODULE_JOURNAL_COLUMN, renderModuleJournal } from "@vibeorm/sql";
|
|
4923
5429
|
|
|
4924
5430
|
// packages/migrate/src/journal-read.ts
|
|
4925
|
-
import { VibeError as
|
|
5431
|
+
import { VibeError as VibeError26 } from "@vibeorm/schema";
|
|
4926
5432
|
var MISSING_TABLE_IDENTIFIERS = new Set(["42P01", "42S02", "ER_NO_SUCH_TABLE"]);
|
|
4927
5433
|
function isMissingTable(params) {
|
|
4928
5434
|
let current = params.cause;
|
|
@@ -4946,7 +5452,7 @@ async function readJournalRows(params) {
|
|
|
4946
5452
|
if (presence.state === "missing")
|
|
4947
5453
|
return;
|
|
4948
5454
|
}
|
|
4949
|
-
throw new
|
|
5455
|
+
throw new VibeError26({ code: "VIBE_MIGRATION", message: "Cannot inspect migration history; check SELECT privileges", meta: { reason: "history-unavailable" }, cause });
|
|
4950
5456
|
});
|
|
4951
5457
|
if (rows === undefined)
|
|
4952
5458
|
return;
|
|
@@ -4954,7 +5460,7 @@ async function readJournalRows(params) {
|
|
|
4954
5460
|
for (const row of rows) {
|
|
4955
5461
|
const index = row.statement_index;
|
|
4956
5462
|
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
|
|
5463
|
+
throw new VibeError26({ code: "VIBE_MIGRATION", message: "Malformed migration journal row", meta: { reason: "history-unavailable" } });
|
|
4958
5464
|
}
|
|
4959
5465
|
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
5466
|
}
|
|
@@ -4962,41 +5468,41 @@ async function readJournalRows(params) {
|
|
|
4962
5468
|
}
|
|
4963
5469
|
|
|
4964
5470
|
// packages/migrate/src/module-journal.ts
|
|
4965
|
-
function
|
|
4966
|
-
return new
|
|
5471
|
+
function invalid6() {
|
|
5472
|
+
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
5473
|
}
|
|
4968
5474
|
function parseModuleJournalEntry(params) {
|
|
4969
|
-
return parseMigrationMetadata({ text: params.text, failure:
|
|
5475
|
+
return parseMigrationMetadata({ text: params.text, failure: invalid6, parse: (value) => {
|
|
4970
5476
|
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
|
|
5477
|
+
throw invalid6();
|
|
4972
5478
|
const migration = value.migration;
|
|
4973
5479
|
assertModuleOrigin({ value: migration.moduleOrigin });
|
|
4974
5480
|
if (migration.name !== params.name || migrationChecksum(migration) !== params.checksum)
|
|
4975
|
-
throw
|
|
5481
|
+
throw invalid6();
|
|
4976
5482
|
assertMaterializedModuleMigration({ migration });
|
|
4977
5483
|
assertModuleOwnership({ origin: migration.moduleOrigin, desiredState: migration.desiredState });
|
|
4978
5484
|
return { version: 1, complete: value.complete, migration };
|
|
4979
5485
|
} });
|
|
4980
5486
|
}
|
|
4981
5487
|
async function readModuleJournal(params) {
|
|
4982
|
-
const query = renderModuleJournal({ dialect:
|
|
5488
|
+
const query = renderModuleJournal({ dialect: getSqlDialect7({ dialect: params.dialect }), table: MIGRATIONS_TABLE_NAME, operation: { kind: "read" } });
|
|
4983
5489
|
const rows = await readJournalRows({ ...params, query });
|
|
4984
5490
|
return new Map([...rows ?? []].map(([name, row]) => {
|
|
4985
5491
|
const entry = row.moduleEntry === undefined ? undefined : parseModuleJournalEntry({ text: row.moduleEntry, name, checksum: row.checksum });
|
|
4986
5492
|
if (entry !== undefined && entry.migration.moduleOrigin.dialect !== params.dialect)
|
|
4987
|
-
throw
|
|
5493
|
+
throw invalid6();
|
|
4988
5494
|
return [name, { row, ...entry === undefined ? {} : { entry } }];
|
|
4989
5495
|
}));
|
|
4990
5496
|
}
|
|
4991
5497
|
async function ensureModuleJournal(params) {
|
|
4992
|
-
const dialect =
|
|
5498
|
+
const dialect = getSqlDialect7({ dialect: params.dialect });
|
|
4993
5499
|
const columns = await params.executor(renderModuleJournal({ dialect, table: MIGRATIONS_TABLE_NAME, operation: { kind: "columns" } }));
|
|
4994
5500
|
if (columns.some((row) => row.name === MODULE_JOURNAL_COLUMN))
|
|
4995
5501
|
return;
|
|
4996
5502
|
await params.executor(renderModuleJournal({ dialect, table: MIGRATIONS_TABLE_NAME, operation: { kind: "addColumn" } }));
|
|
4997
5503
|
}
|
|
4998
5504
|
async function insertModuleJournal(params) {
|
|
4999
|
-
await params.executor(renderModuleJournal({ dialect:
|
|
5505
|
+
await params.executor(renderModuleJournal({ dialect: getSqlDialect7({ dialect: params.dialect }), table: MIGRATIONS_TABLE_NAME, operation: {
|
|
5000
5506
|
kind: "insert",
|
|
5001
5507
|
name: params.migration.name,
|
|
5002
5508
|
checksum: migrationChecksum(params.migration),
|
|
@@ -5005,7 +5511,7 @@ async function insertModuleJournal(params) {
|
|
|
5005
5511
|
} }));
|
|
5006
5512
|
}
|
|
5007
5513
|
async function completeModuleJournal(params) {
|
|
5008
|
-
await params.executor(renderModuleJournal({ dialect:
|
|
5514
|
+
await params.executor(renderModuleJournal({ dialect: getSqlDialect7({ dialect: params.dialect }), table: MIGRATIONS_TABLE_NAME, operation: {
|
|
5009
5515
|
kind: "complete",
|
|
5010
5516
|
name: params.migration.name,
|
|
5011
5517
|
entry: canonicalMigrationJson({ value: { version: 1, migration: params.migration, complete: true } })
|
|
@@ -5014,27 +5520,27 @@ async function completeModuleJournal(params) {
|
|
|
5014
5520
|
function moduleRecordComplete(params) {
|
|
5015
5521
|
const { migration, stored } = params;
|
|
5016
5522
|
if (stored.moduleEntry === undefined || stored.checksum === "")
|
|
5017
|
-
throw
|
|
5523
|
+
throw invalid6();
|
|
5018
5524
|
const entry = parseModuleJournalEntry({ text: stored.moduleEntry, name: migration.name, checksum: stored.checksum });
|
|
5019
5525
|
if (migrationChecksum(migration) !== stored.checksum)
|
|
5020
|
-
throw
|
|
5526
|
+
throw invalid6();
|
|
5021
5527
|
return entry.complete && stored.error === undefined && stored.statementIndex === migration.sql.length;
|
|
5022
5528
|
}
|
|
5023
5529
|
|
|
5024
5530
|
// packages/migrate/src/runner.ts
|
|
5025
|
-
import { VibeError as
|
|
5531
|
+
import { VibeError as VibeError35 } from "@vibeorm/schema";
|
|
5026
5532
|
import {
|
|
5027
5533
|
DIALECT_CAPABILITIES as DIALECT_CAPABILITIES3,
|
|
5028
|
-
getSqlDialect as
|
|
5534
|
+
getSqlDialect as getSqlDialect9,
|
|
5029
5535
|
renderMigrationSessionRead,
|
|
5030
5536
|
renderMigrationSessionRestore,
|
|
5031
5537
|
renderMigrationSessionSet
|
|
5032
5538
|
} from "@vibeorm/sql";
|
|
5033
5539
|
|
|
5034
5540
|
// packages/migrate/src/recovery.ts
|
|
5035
|
-
import { VibeError as
|
|
5541
|
+
import { VibeError as VibeError28 } from "@vibeorm/schema";
|
|
5036
5542
|
import {
|
|
5037
|
-
getSqlDialect as
|
|
5543
|
+
getSqlDialect as getSqlDialect8,
|
|
5038
5544
|
renderColumnStateQuery,
|
|
5039
5545
|
renderConstraintStateQuery,
|
|
5040
5546
|
renderIndexStateQuery,
|
|
@@ -5048,7 +5554,7 @@ function constraintUnverifiable(params) {
|
|
|
5048
5554
|
});
|
|
5049
5555
|
}
|
|
5050
5556
|
function conflict2(params) {
|
|
5051
|
-
return new
|
|
5557
|
+
return new VibeError28({
|
|
5052
5558
|
code: "VIBE_MIGRATION",
|
|
5053
5559
|
message: params.message,
|
|
5054
5560
|
meta: { reason: params.reason, ...params.meta }
|
|
@@ -5073,7 +5579,7 @@ function sameShape(params) {
|
|
|
5073
5579
|
}
|
|
5074
5580
|
async function establishOperationState(params) {
|
|
5075
5581
|
const { executor, operation } = params;
|
|
5076
|
-
const dialect =
|
|
5582
|
+
const dialect = getSqlDialect8({ dialect: params.dialect });
|
|
5077
5583
|
const run = async (query) => executor({ text: query.text, values: [...query.values] });
|
|
5078
5584
|
switch (operation.kind) {
|
|
5079
5585
|
case "createIndexConcurrently": {
|
|
@@ -5152,11 +5658,11 @@ async function establishOperationState(params) {
|
|
|
5152
5658
|
}
|
|
5153
5659
|
|
|
5154
5660
|
// packages/migrate/src/differ.ts
|
|
5155
|
-
import { isEnumType as isEnumType6, unsupportedCapability as
|
|
5156
|
-
import { VibeError as
|
|
5661
|
+
import { assertIndexOperatorClasses as assertIndexOperatorClasses2, isEnumType as isEnumType6, qualifiedSqlName as qualifiedSqlName9, unsupportedCapability as unsupportedCapability10 } from "@vibeorm/schema";
|
|
5662
|
+
import { VibeError as VibeError30 } from "@vibeorm/schema";
|
|
5157
5663
|
|
|
5158
5664
|
// packages/migrate/src/renames.ts
|
|
5159
|
-
import { VibeError as
|
|
5665
|
+
import { VibeError as VibeError29, unsupportedCapability as unsupportedCapability9 } from "@vibeorm/schema";
|
|
5160
5666
|
function tableOf(model) {
|
|
5161
5667
|
return model.dbName ?? model.name;
|
|
5162
5668
|
}
|
|
@@ -5167,7 +5673,7 @@ function hasColumn(params) {
|
|
|
5167
5673
|
return params.model.fields.some((field) => (field.dbName ?? field.name) === params.column);
|
|
5168
5674
|
}
|
|
5169
5675
|
function ambiguous(params) {
|
|
5170
|
-
return new
|
|
5676
|
+
return new VibeError29({
|
|
5171
5677
|
code: "VIBE_MIGRATION",
|
|
5172
5678
|
message: `rename refused: ${params.detail}`,
|
|
5173
5679
|
meta: { reason: "rename-ambiguous", ...params.meta }
|
|
@@ -5177,6 +5683,8 @@ function planRenames(params) {
|
|
|
5177
5683
|
const { to, dialect } = params;
|
|
5178
5684
|
if (params.renames.length === 0)
|
|
5179
5685
|
return { steps: [], from: params.from };
|
|
5686
|
+
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)))))
|
|
5687
|
+
throw unsupportedCapability9({ dialect, feature: "qualifiedRenameIntent", hint: "Use reviewed namespace-qualified SQL for a physical rename in a fixed namespace" });
|
|
5180
5688
|
const renamedTables = new Map;
|
|
5181
5689
|
const renamedColumns = new Map;
|
|
5182
5690
|
const steps = [];
|
|
@@ -5268,9 +5776,19 @@ function planRenames(params) {
|
|
|
5268
5776
|
// packages/migrate/src/differ.ts
|
|
5269
5777
|
function diffSchemas(params) {
|
|
5270
5778
|
const dialect = params.dialect ?? "postgres";
|
|
5779
|
+
assertIndexOperatorClasses2({ schema: params.to, dialect });
|
|
5780
|
+
for (const kind of ["models", "enums"])
|
|
5781
|
+
for (const entry of params.to[kind]) {
|
|
5782
|
+
const key = physicalNameKey({ name: entry.dbName ?? entry.name, namespace: entry.namespace, dialect });
|
|
5783
|
+
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 })));
|
|
5784
|
+
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))
|
|
5785
|
+
throw unsupportedCapability10({ dialect, feature: "automaticNamespaceMove", hint: "Moving populated objects to a fixed namespace requires an explicit reviewed migration" });
|
|
5786
|
+
}
|
|
5787
|
+
for (const schema of [params.from, params.to])
|
|
5788
|
+
normalizeSchema({ schema, dialect });
|
|
5271
5789
|
for (const schema of [params.from, params.to])
|
|
5272
5790
|
if (dialect !== "postgres" && schema.models.some((model) => (model.exclusions?.length ?? 0) > 0))
|
|
5273
|
-
throw
|
|
5791
|
+
throw unsupportedCapability10({ dialect, feature: "exclusionConstraints" });
|
|
5274
5792
|
assertRlsDialect({ schema: params.from, dialect });
|
|
5275
5793
|
assertRlsDialect({ schema: params.to, dialect });
|
|
5276
5794
|
if (dialect !== "postgres")
|
|
@@ -5280,6 +5798,7 @@ function diffSchemas(params) {
|
|
|
5280
5798
|
const from = normalizeSchema({ schema: params.from, dialect });
|
|
5281
5799
|
const to = normalizeSchema({ schema: params.to, dialect });
|
|
5282
5800
|
const buckets = {
|
|
5801
|
+
createNamespace: dialect === "postgres" ? schemaNamespaces({ schema: params.to }).filter((namespace) => !schemaNamespaces({ schema: params.from }).includes(namespace)).map((namespace) => ({ kind: "createNamespace", namespace, destructive: false })) : [],
|
|
5283
5802
|
dropExclusion: [],
|
|
5284
5803
|
addExclusion: [],
|
|
5285
5804
|
dropView: [],
|
|
@@ -5309,16 +5828,17 @@ function diffSchemas(params) {
|
|
|
5309
5828
|
diffTables({ from, to, buckets, dialect });
|
|
5310
5829
|
for (const model of params.to.models) {
|
|
5311
5830
|
const table = model.dbName ?? model.name;
|
|
5312
|
-
const
|
|
5831
|
+
const placement = model.namespace === undefined ? {} : { namespace: model.namespace };
|
|
5832
|
+
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
5833
|
for (const exclusion of model.exclusions ?? []) {
|
|
5314
5834
|
const before = previous?.exclusions?.find((entry) => entry.name === exclusion.name);
|
|
5315
5835
|
if (before?.definition === exclusion.definition)
|
|
5316
5836
|
continue;
|
|
5317
5837
|
if (before?.catalogDefinition)
|
|
5318
|
-
throw new
|
|
5838
|
+
throw new VibeError30({ code: "VIBE_MIGRATION", message: "Exclusion comparison requires normalized evidence", meta: { reason: "exclusion-inspection-unavailable", table, name: exclusion.name } });
|
|
5319
5839
|
if (before !== undefined)
|
|
5320
|
-
buckets.dropExclusion.push({ kind: "dropExclusion", table, exclusion: before, destructive: false });
|
|
5321
|
-
buckets.addExclusion.push({ kind: "addExclusion", table, exclusion, destructive: false });
|
|
5840
|
+
buckets.dropExclusion.push({ kind: "dropExclusion", table, ...placement, exclusion: before, destructive: false });
|
|
5841
|
+
buckets.addExclusion.push({ kind: "addExclusion", table, ...placement, exclusion, destructive: false });
|
|
5322
5842
|
}
|
|
5323
5843
|
for (const exclusion of previous?.exclusions ?? [])
|
|
5324
5844
|
if (!model.exclusions?.some((entry) => entry.name === exclusion.name))
|
|
@@ -5326,6 +5846,7 @@ function diffSchemas(params) {
|
|
|
5326
5846
|
}
|
|
5327
5847
|
refuseDependentEnumStorageChanges({ from, to, buckets, dialect });
|
|
5328
5848
|
return withRlsSteps({ from: params.from, to: params.to, steps: [
|
|
5849
|
+
...buckets.createNamespace,
|
|
5329
5850
|
...renamed.steps,
|
|
5330
5851
|
...buckets.dropView,
|
|
5331
5852
|
...buckets.createEnum,
|
|
@@ -5358,7 +5879,7 @@ function refuseScalarListColumns(params) {
|
|
|
5358
5879
|
for (const field of model.fields) {
|
|
5359
5880
|
if (!field.isList || field.isUnsupported === true)
|
|
5360
5881
|
continue;
|
|
5361
|
-
throw
|
|
5882
|
+
throw unsupportedCapability10({
|
|
5362
5883
|
dialect,
|
|
5363
5884
|
feature: `scalar list column "${model.name}.${field.name}"`,
|
|
5364
5885
|
hint: `${dialect} has no array column type \u2014 model the list as a separate table or a ${dialect === "mysql" ? "JSON" : "Json"} column`
|
|
@@ -5378,12 +5899,12 @@ function diffEnums(params) {
|
|
|
5378
5899
|
const currentValues = new Set(enumDef.values);
|
|
5379
5900
|
for (const value of enumDef.values) {
|
|
5380
5901
|
if (!previousValues.has(value)) {
|
|
5381
|
-
buckets.addEnumValue.push({ kind: "addEnumValue", enumName: name, value, destructive: false });
|
|
5902
|
+
buckets.addEnumValue.push({ kind: "addEnumValue", enumName: enumDef.dbName ?? enumDef.name, ...enumDef.namespace === undefined ? {} : { namespace: enumDef.namespace }, value, destructive: false });
|
|
5382
5903
|
}
|
|
5383
5904
|
}
|
|
5384
5905
|
for (const value of previous.values) {
|
|
5385
5906
|
if (!currentValues.has(value)) {
|
|
5386
|
-
throw
|
|
5907
|
+
throw unsupportedCapability10({
|
|
5387
5908
|
dialect: "postgres",
|
|
5388
5909
|
feature: `removing value "${value}" from enum "${name}"`,
|
|
5389
5910
|
hint: "postgres cannot drop enum values \u2014 recreate the type manually (create new type, ALTER COLUMN ... USING, drop old type)"
|
|
@@ -5391,9 +5912,9 @@ function diffEnums(params) {
|
|
|
5391
5912
|
}
|
|
5392
5913
|
}
|
|
5393
5914
|
}
|
|
5394
|
-
for (const [name] of from.enums) {
|
|
5915
|
+
for (const [name, enumDef] of from.enums) {
|
|
5395
5916
|
if (!to.enums.has(name)) {
|
|
5396
|
-
buckets.dropEnum.push({ kind: "dropEnum", enumName: name, destructive: true });
|
|
5917
|
+
buckets.dropEnum.push({ kind: "dropEnum", enumName: enumDef.dbName ?? enumDef.name, ...enumDef.namespace === undefined ? {} : { namespace: enumDef.namespace }, destructive: true });
|
|
5397
5918
|
}
|
|
5398
5919
|
}
|
|
5399
5920
|
}
|
|
@@ -5403,10 +5924,10 @@ function refuseDependentEnumStorageChanges(params) {
|
|
|
5403
5924
|
if (step.kind !== "alterColumn" || !isEnumStringTransition({ from: step.from, to: step.to }))
|
|
5404
5925
|
continue;
|
|
5405
5926
|
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));
|
|
5927
|
+
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)));
|
|
5928
|
+
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
5929
|
if (hasFk || hasJoin) {
|
|
5409
|
-
throw
|
|
5930
|
+
throw unsupportedCapability10({
|
|
5410
5931
|
dialect,
|
|
5411
5932
|
feature: `enum storage conversion on "${step.table}"."${step.column}" with foreign-key dependencies`,
|
|
5412
5933
|
hint: "migrate the referencing columns and constraints together in a reviewed manual migration"
|
|
@@ -5417,28 +5938,31 @@ function refuseDependentEnumStorageChanges(params) {
|
|
|
5417
5938
|
}
|
|
5418
5939
|
function diffViews(params) {
|
|
5419
5940
|
const { from, to, buckets } = params;
|
|
5420
|
-
for (const [name] of from.views) {
|
|
5941
|
+
for (const [name, view] of from.views) {
|
|
5421
5942
|
if (!to.views.has(name)) {
|
|
5422
|
-
buckets.dropView.push({ kind: "dropView", table: name, destructive: true });
|
|
5943
|
+
buckets.dropView.push({ kind: "dropView", table: view.dbName ?? view.name, ...view.namespace === undefined ? {} : { namespace: view.namespace }, destructive: true });
|
|
5423
5944
|
}
|
|
5424
5945
|
}
|
|
5425
5946
|
}
|
|
5426
5947
|
function diffJoinTables(params) {
|
|
5427
5948
|
const { from, to, buckets } = params;
|
|
5428
|
-
for (const [
|
|
5429
|
-
if (!from.joinTables.has(
|
|
5949
|
+
for (const [key, joinTable] of to.joinTables) {
|
|
5950
|
+
if (!from.joinTables.has(key)) {
|
|
5951
|
+
const table = joinTable.table;
|
|
5952
|
+
const placement = joinTable.namespace === undefined ? {} : { namespace: joinTable.namespace };
|
|
5430
5953
|
buckets.createJoinTable.push({
|
|
5431
5954
|
kind: "createJoinTable",
|
|
5432
5955
|
table,
|
|
5956
|
+
...placement,
|
|
5433
5957
|
a: joinTable.a,
|
|
5434
5958
|
b: joinTable.b,
|
|
5435
5959
|
destructive: false
|
|
5436
5960
|
});
|
|
5437
5961
|
}
|
|
5438
5962
|
}
|
|
5439
|
-
for (const [
|
|
5440
|
-
if (!to.joinTables.has(
|
|
5441
|
-
buckets.dropJoinTable.push({ kind: "dropJoinTable", table, destructive: true });
|
|
5963
|
+
for (const [key, joinTable] of from.joinTables) {
|
|
5964
|
+
if (!to.joinTables.has(key)) {
|
|
5965
|
+
buckets.dropJoinTable.push({ kind: "dropJoinTable", table: joinTable.table, ...joinTable.namespace === undefined ? {} : { namespace: joinTable.namespace }, destructive: true });
|
|
5442
5966
|
}
|
|
5443
5967
|
}
|
|
5444
5968
|
}
|
|
@@ -5463,9 +5987,9 @@ function diffTables(params) {
|
|
|
5463
5987
|
continue;
|
|
5464
5988
|
if (dialect !== "sqlite")
|
|
5465
5989
|
for (const fk of table.fks.values()) {
|
|
5466
|
-
buckets.dropForeignKey.push({ kind: "dropForeignKey", table:
|
|
5990
|
+
buckets.dropForeignKey.push({ kind: "dropForeignKey", table: table.table, ...table.namespace === undefined ? {} : { namespace: table.namespace }, constraintName: fk.constraintName, destructive: false });
|
|
5467
5991
|
}
|
|
5468
|
-
buckets.dropTable.push({ kind: "dropTable", table:
|
|
5992
|
+
buckets.dropTable.push({ kind: "dropTable", table: table.table, ...table.namespace === undefined ? {} : { namespace: table.namespace }, destructive: true });
|
|
5469
5993
|
}
|
|
5470
5994
|
}
|
|
5471
5995
|
function createWholeTable(params) {
|
|
@@ -5473,6 +5997,7 @@ function createWholeTable(params) {
|
|
|
5473
5997
|
buckets.createTable.push({
|
|
5474
5998
|
kind: "createTable",
|
|
5475
5999
|
table: table.table,
|
|
6000
|
+
...table.namespace === undefined ? {} : { namespace: table.namespace },
|
|
5476
6001
|
columns: [...table.columns.values()].map((column) => column.field),
|
|
5477
6002
|
primaryKey: table.pk,
|
|
5478
6003
|
...table.model.primaryKeyConstraintName === undefined ? {} : { primaryKeyConstraintName: table.model.primaryKeyConstraintName },
|
|
@@ -5483,6 +6008,7 @@ function createWholeTable(params) {
|
|
|
5483
6008
|
buckets.createIndex.push({
|
|
5484
6009
|
kind: "createIndex",
|
|
5485
6010
|
table: table.table,
|
|
6011
|
+
...table.namespace === undefined ? {} : { namespace: table.namespace },
|
|
5486
6012
|
name: index.name,
|
|
5487
6013
|
indexKind: index.kind,
|
|
5488
6014
|
unique: index.unique,
|
|
@@ -5496,6 +6022,7 @@ function createWholeTable(params) {
|
|
|
5496
6022
|
buckets.addForeignKey.push({
|
|
5497
6023
|
kind: "addForeignKey",
|
|
5498
6024
|
table: table.table,
|
|
6025
|
+
...table.namespace === undefined ? {} : { namespace: table.namespace },
|
|
5499
6026
|
constraintName: fk.constraintName,
|
|
5500
6027
|
columns: fk.columns,
|
|
5501
6028
|
refTable: fk.refTable,
|
|
@@ -5509,10 +6036,11 @@ function createWholeTable(params) {
|
|
|
5509
6036
|
function diffOneTable(params) {
|
|
5510
6037
|
const { previous, current, buckets, dialect, fromEnums, toEnums } = params;
|
|
5511
6038
|
const table = current.table;
|
|
6039
|
+
const placement = current.namespace === undefined ? {} : { namespace: current.namespace };
|
|
5512
6040
|
for (const [name, column] of current.columns) {
|
|
5513
6041
|
const before = previous.columns.get(name);
|
|
5514
6042
|
if (before === undefined) {
|
|
5515
|
-
buckets.addColumn.push({ kind: "addColumn", table, column: column.field, destructive: false });
|
|
6043
|
+
buckets.addColumn.push({ kind: "addColumn", table, ...placement, column: column.field, destructive: false });
|
|
5516
6044
|
continue;
|
|
5517
6045
|
}
|
|
5518
6046
|
const changes = [];
|
|
@@ -5528,13 +6056,13 @@ function diffOneTable(params) {
|
|
|
5528
6056
|
if (changes.length === 0)
|
|
5529
6057
|
continue;
|
|
5530
6058
|
if (changes.includes("type") && [...previous.checks].some(([name, expression]) => current.checks.get(name) === expression)) {
|
|
5531
|
-
throw
|
|
6059
|
+
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
6060
|
}
|
|
5533
6061
|
const storageChange = isEnumStringTransition({ from: before.field, to: column.field });
|
|
5534
6062
|
if (storageChange) {
|
|
5535
6063
|
assertEnumStringTransition({ from: before.field, to: column.field, dialect, table, column: name });
|
|
5536
6064
|
if (isEnumType6(before.field.type) && column.field.type === "String") {
|
|
5537
|
-
const values = resolveEnumValues({ name: before.field.type.enum, enums: fromEnums });
|
|
6065
|
+
const values = resolveEnumValues({ name: before.field.type.enum, namespace: before.field.type.namespace, enums: fromEnums });
|
|
5538
6066
|
if (values !== undefined)
|
|
5539
6067
|
assertStringEnumLabelsFit({ field: column.field, values, dialect, table });
|
|
5540
6068
|
}
|
|
@@ -5546,7 +6074,7 @@ function diffOneTable(params) {
|
|
|
5546
6074
|
if (fromValues !== undefined && toValues !== undefined) {
|
|
5547
6075
|
const removed = fromValues.filter((value) => !toValues.includes(value));
|
|
5548
6076
|
if (removed.length > 0) {
|
|
5549
|
-
throw
|
|
6077
|
+
throw unsupportedCapability10({
|
|
5550
6078
|
dialect,
|
|
5551
6079
|
feature: `removing value(s) ${removed.map((value) => `"${value}"`).join(", ")} from enum column "${table}"."${name}"`,
|
|
5552
6080
|
hint: "migrate the stored values away first, then recreate the column with the narrower enum"
|
|
@@ -5558,6 +6086,7 @@ function diffOneTable(params) {
|
|
|
5558
6086
|
buckets.alterColumn.push({
|
|
5559
6087
|
kind: "alterColumn",
|
|
5560
6088
|
table,
|
|
6089
|
+
...placement,
|
|
5561
6090
|
column: name,
|
|
5562
6091
|
from: before.field,
|
|
5563
6092
|
to: column.field,
|
|
@@ -5567,19 +6096,20 @@ function diffOneTable(params) {
|
|
|
5567
6096
|
}
|
|
5568
6097
|
for (const [name] of previous.columns) {
|
|
5569
6098
|
if (!current.columns.has(name)) {
|
|
5570
|
-
buckets.dropColumn.push({ kind: "dropColumn", table, column: name, destructive: true });
|
|
6099
|
+
buckets.dropColumn.push({ kind: "dropColumn", table, ...placement, column: name, destructive: true });
|
|
5571
6100
|
}
|
|
5572
6101
|
}
|
|
5573
6102
|
const pkBefore = previous.pk.join(",");
|
|
5574
6103
|
const pkAfter = current.pk.join(",");
|
|
5575
6104
|
if (pkBefore !== pkAfter) {
|
|
5576
6105
|
if (previous.pk.length > 0) {
|
|
5577
|
-
buckets.dropPrimaryKey.push({ kind: "dropPrimaryKey", table, destructive: false, ...previous.model.primaryKeyConstraintName === undefined ? {} : { constraintName: previous.model.primaryKeyConstraintName } });
|
|
6106
|
+
buckets.dropPrimaryKey.push({ kind: "dropPrimaryKey", table, ...placement, destructive: false, ...previous.model.primaryKeyConstraintName === undefined ? {} : { constraintName: previous.model.primaryKeyConstraintName } });
|
|
5578
6107
|
}
|
|
5579
6108
|
if (current.pk.length > 0) {
|
|
5580
6109
|
buckets.addPrimaryKey.push({
|
|
5581
6110
|
kind: "addPrimaryKey",
|
|
5582
6111
|
table,
|
|
6112
|
+
...placement,
|
|
5583
6113
|
columns: current.pk,
|
|
5584
6114
|
...current.model.primaryKeyConstraintName === undefined ? {} : { constraintName: current.model.primaryKeyConstraintName },
|
|
5585
6115
|
destructive: false
|
|
@@ -5591,15 +6121,15 @@ function diffOneTable(params) {
|
|
|
5591
6121
|
if (before === expression)
|
|
5592
6122
|
continue;
|
|
5593
6123
|
if (before !== undefined && previous.model.checks?.some((check) => check.name === name && check.catalogExpression === true))
|
|
5594
|
-
throw new
|
|
6124
|
+
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
6125
|
if (before !== undefined)
|
|
5596
|
-
buckets.dropCheck.push({ kind: "dropCheck", table, name, destructive: false });
|
|
5597
|
-
buckets.addCheck.push({ kind: "addCheck", table, name, expression, destructive: false });
|
|
6126
|
+
buckets.dropCheck.push({ kind: "dropCheck", table, ...placement, name, destructive: false });
|
|
6127
|
+
buckets.addCheck.push({ kind: "addCheck", table, ...placement, name, expression, destructive: false });
|
|
5598
6128
|
}
|
|
5599
6129
|
for (const [name] of previous.checks) {
|
|
5600
6130
|
if (current.checks.has(name))
|
|
5601
6131
|
continue;
|
|
5602
|
-
buckets.dropCheck.push({ kind: "dropCheck", table, name, destructive: false });
|
|
6132
|
+
buckets.dropCheck.push({ kind: "dropCheck", table, ...placement, name, destructive: false });
|
|
5603
6133
|
}
|
|
5604
6134
|
for (const [key, index] of current.indexes) {
|
|
5605
6135
|
if (previous.indexes.has(key))
|
|
@@ -5607,6 +6137,7 @@ function diffOneTable(params) {
|
|
|
5607
6137
|
buckets.createIndex.push({
|
|
5608
6138
|
kind: "createIndex",
|
|
5609
6139
|
table,
|
|
6140
|
+
...placement,
|
|
5610
6141
|
name: index.name,
|
|
5611
6142
|
indexKind: index.kind,
|
|
5612
6143
|
unique: index.unique,
|
|
@@ -5619,7 +6150,7 @@ function diffOneTable(params) {
|
|
|
5619
6150
|
for (const [key, index] of previous.indexes) {
|
|
5620
6151
|
if (current.indexes.has(key))
|
|
5621
6152
|
continue;
|
|
5622
|
-
buckets.dropIndex.push({ kind: "dropIndex", name: index.name, table, destructive: false, ...index.constraintName === undefined ? {} : { constraintName: index.constraintName } });
|
|
6153
|
+
buckets.dropIndex.push({ kind: "dropIndex", name: index.name, table, ...placement, destructive: false, ...index.constraintName === undefined ? {} : { constraintName: index.constraintName } });
|
|
5623
6154
|
}
|
|
5624
6155
|
for (const [key, fk] of current.fks) {
|
|
5625
6156
|
const before = previous.fks.get(key);
|
|
@@ -5627,9 +6158,11 @@ function diffOneTable(params) {
|
|
|
5627
6158
|
buckets.addForeignKey.push({
|
|
5628
6159
|
kind: "addForeignKey",
|
|
5629
6160
|
table,
|
|
6161
|
+
...placement,
|
|
5630
6162
|
constraintName: fk.constraintName,
|
|
5631
6163
|
columns: fk.columns,
|
|
5632
6164
|
refTable: fk.refTable,
|
|
6165
|
+
...fk.refNamespace === undefined ? {} : { refNamespace: fk.refNamespace },
|
|
5633
6166
|
refColumns: fk.refColumns,
|
|
5634
6167
|
onDelete: fk.onDelete,
|
|
5635
6168
|
onUpdate: fk.onUpdate,
|
|
@@ -5641,15 +6174,18 @@ function diffOneTable(params) {
|
|
|
5641
6174
|
buckets.dropForeignKey.push({
|
|
5642
6175
|
kind: "dropForeignKey",
|
|
5643
6176
|
table,
|
|
6177
|
+
...placement,
|
|
5644
6178
|
constraintName: before.constraintName,
|
|
5645
6179
|
destructive: false
|
|
5646
6180
|
});
|
|
5647
6181
|
buckets.addForeignKey.push({
|
|
5648
6182
|
kind: "addForeignKey",
|
|
5649
6183
|
table,
|
|
6184
|
+
...placement,
|
|
5650
6185
|
constraintName: fk.constraintName,
|
|
5651
6186
|
columns: fk.columns,
|
|
5652
6187
|
refTable: fk.refTable,
|
|
6188
|
+
...fk.refNamespace === undefined ? {} : { refNamespace: fk.refNamespace },
|
|
5653
6189
|
refColumns: fk.refColumns,
|
|
5654
6190
|
onDelete: fk.onDelete,
|
|
5655
6191
|
onUpdate: fk.onUpdate,
|
|
@@ -5663,6 +6199,7 @@ function diffOneTable(params) {
|
|
|
5663
6199
|
buckets.dropForeignKey.push({
|
|
5664
6200
|
kind: "dropForeignKey",
|
|
5665
6201
|
table,
|
|
6202
|
+
...placement,
|
|
5666
6203
|
constraintName: fk.constraintName,
|
|
5667
6204
|
destructive: false
|
|
5668
6205
|
});
|
|
@@ -5670,12 +6207,12 @@ function diffOneTable(params) {
|
|
|
5670
6207
|
}
|
|
5671
6208
|
|
|
5672
6209
|
// packages/migrate/src/ddl/render.ts
|
|
5673
|
-
import { VibeError as
|
|
6210
|
+
import { VibeError as VibeError34, sqlObjectKey as sqlObjectKey10 } from "@vibeorm/schema";
|
|
5674
6211
|
|
|
5675
6212
|
// packages/migrate/src/ddl/postgres.ts
|
|
5676
6213
|
import { renderExclusionSetup } from "@vibeorm/sql";
|
|
5677
|
-
import { VibeError as
|
|
5678
|
-
import { postgresDialect as
|
|
6214
|
+
import { VibeError as VibeError31, assertSqlNamespace as assertSqlNamespace3, defaultPrimaryKeyName as defaultPrimaryKeyName3, implicitJoinTableObjectNames as implicitJoinTableObjectNames2, isEnumType as isEnumType7 } from "@vibeorm/schema";
|
|
6215
|
+
import { postgresDialect as postgresDialect9, renderRlsPolicyPath, renderIndexColumns } from "@vibeorm/sql";
|
|
5679
6216
|
function renderPostgresSteps(params) {
|
|
5680
6217
|
refuseSameTransactionEnumUse({ steps: params.steps });
|
|
5681
6218
|
return params.steps.flatMap((step) => renderStep({ step }));
|
|
@@ -5685,19 +6222,21 @@ function refuseSameTransactionEnumUse(params) {
|
|
|
5685
6222
|
for (const step of params.steps) {
|
|
5686
6223
|
if (step.kind !== "addEnumValue")
|
|
5687
6224
|
continue;
|
|
5688
|
-
const
|
|
6225
|
+
const key = JSON.stringify([step.namespace ?? "public", step.enumName]);
|
|
6226
|
+
const values = added.get(key) ?? new Set;
|
|
5689
6227
|
values.add(step.value);
|
|
5690
|
-
added.set(
|
|
6228
|
+
added.set(key, values);
|
|
5691
6229
|
}
|
|
5692
6230
|
if (added.size === 0)
|
|
5693
6231
|
return;
|
|
5694
6232
|
const use = (field) => {
|
|
5695
6233
|
if (!isEnumType7(field.type))
|
|
5696
6234
|
return;
|
|
5697
|
-
const
|
|
6235
|
+
const type = field.type;
|
|
6236
|
+
const enumName = type.enum;
|
|
5698
6237
|
const def = field.default;
|
|
5699
6238
|
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);
|
|
6239
|
+
const value = candidates.find((entry) => typeof entry === "string" && added.get(JSON.stringify([type.namespace ?? "public", enumName]))?.has(entry) === true);
|
|
5701
6240
|
return value === undefined ? undefined : { enumName, value };
|
|
5702
6241
|
};
|
|
5703
6242
|
for (const step of params.steps) {
|
|
@@ -5709,7 +6248,7 @@ function refuseSameTransactionEnumUse(params) {
|
|
|
5709
6248
|
const found = use(field);
|
|
5710
6249
|
if (found === undefined)
|
|
5711
6250
|
continue;
|
|
5712
|
-
throw new
|
|
6251
|
+
throw new VibeError31({
|
|
5713
6252
|
code: "VIBE_MIGRATION",
|
|
5714
6253
|
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
6254
|
meta: { enum: found.enumName, value: found.value, table: step.table, column: field.dbName ?? field.name, rule: "enum-value-used-in-adding-transaction" }
|
|
@@ -5720,27 +6259,30 @@ function refuseSameTransactionEnumUse(params) {
|
|
|
5720
6259
|
function renderStep(params) {
|
|
5721
6260
|
const { step } = params;
|
|
5722
6261
|
switch (step.kind) {
|
|
6262
|
+
case "createNamespace":
|
|
6263
|
+
assertSqlNamespace3({ namespace: step.namespace, dialect: "postgres", allowPublic: false });
|
|
6264
|
+
return [`CREATE SCHEMA ${q2(step.namespace)};`];
|
|
5723
6265
|
case "addExclusion":
|
|
5724
|
-
return [...renderExclusionSetup({ dialect:
|
|
6266
|
+
return [...renderExclusionSetup({ dialect: postgresDialect9, exclusion: step.exclusion }), `ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.exclusion.name)} ${step.exclusion.definition};`];
|
|
5725
6267
|
case "dropExclusion":
|
|
5726
|
-
return [`ALTER TABLE ${
|
|
6268
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.exclusion.name)};`];
|
|
5727
6269
|
case "enableRls":
|
|
5728
6270
|
case "forceRls":
|
|
5729
6271
|
case "disableRls":
|
|
5730
6272
|
case "noForceRls":
|
|
5731
6273
|
case "createPolicy":
|
|
5732
6274
|
case "dropPolicy":
|
|
5733
|
-
return step.kind === "createPolicy" ? [renderRlsPolicyPath({ dialect:
|
|
6275
|
+
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
6276
|
case "createEnum": {
|
|
5735
6277
|
const name = step.enumDef.dbName ?? step.enumDef.name;
|
|
5736
6278
|
const values = step.enumDef.values.map((value) => quotePgString({ value })).join(", ");
|
|
5737
|
-
return [`CREATE TYPE ${
|
|
6279
|
+
return [`CREATE TYPE ${qualifiedName(name, step.enumDef.namespace)} AS ENUM (${values});`];
|
|
5738
6280
|
}
|
|
5739
6281
|
case "dropEnum":
|
|
5740
|
-
return [`DROP TYPE ${
|
|
6282
|
+
return [`DROP TYPE ${qualifiedName(step.enumName, step.namespace)};`];
|
|
5741
6283
|
case "addEnumValue":
|
|
5742
6284
|
return [
|
|
5743
|
-
`ALTER TYPE ${
|
|
6285
|
+
`ALTER TYPE ${qualifiedName(step.enumName, step.namespace)} ADD VALUE IF NOT EXISTS ${quotePgString({ value: step.value })};`
|
|
5744
6286
|
];
|
|
5745
6287
|
case "createTable": {
|
|
5746
6288
|
const lines = step.columns.map((column) => ` ${columnDefSql({ field: column })}`);
|
|
@@ -5751,101 +6293,101 @@ function renderStep(params) {
|
|
|
5751
6293
|
for (const check of step.checks ?? []) {
|
|
5752
6294
|
lines.push(` CONSTRAINT ${q2(check.name)} CHECK (${check.expression})`);
|
|
5753
6295
|
}
|
|
5754
|
-
return [`CREATE TABLE ${
|
|
6296
|
+
return [`CREATE TABLE ${qualifiedName(step.table, step.namespace)} (
|
|
5755
6297
|
${lines.join(`,
|
|
5756
6298
|
`)}
|
|
5757
6299
|
);`];
|
|
5758
6300
|
}
|
|
5759
6301
|
case "dropTable":
|
|
5760
6302
|
case "dropJoinTable":
|
|
5761
|
-
return [`DROP TABLE ${
|
|
6303
|
+
return [`DROP TABLE ${qualifiedName(step.table, step.namespace)};`];
|
|
5762
6304
|
case "dropView":
|
|
5763
|
-
return [`DROP VIEW IF EXISTS ${
|
|
6305
|
+
return [`DROP VIEW IF EXISTS ${qualifiedName(step.table, step.namespace)};`];
|
|
5764
6306
|
case "addColumn":
|
|
5765
|
-
return [`ALTER TABLE ${
|
|
6307
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD COLUMN ${columnDefSql({ field: step.column })};`];
|
|
5766
6308
|
case "dropColumn":
|
|
5767
|
-
return [`ALTER TABLE ${
|
|
6309
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP COLUMN ${q2(step.column)};`];
|
|
5768
6310
|
case "alterColumn":
|
|
5769
6311
|
return renderAlterColumn({ step });
|
|
5770
6312
|
case "createIndex": {
|
|
5771
6313
|
const unique = step.unique ? "UNIQUE " : "";
|
|
5772
6314
|
const using = !step.unique && step.indexKind !== "btree" ? ` USING ${step.indexKind}` : "";
|
|
5773
|
-
const cols =
|
|
6315
|
+
const cols = renderIndexColumns({ dialect: postgresDialect9, columns: step.columns, unique: step.unique }).join(", ");
|
|
5774
6316
|
const nulls = step.nullsNotDistinct === true ? " NULLS NOT DISTINCT" : "";
|
|
5775
6317
|
const where = step.where === undefined ? "" : ` WHERE ${step.where}`;
|
|
5776
6318
|
return [
|
|
5777
|
-
`CREATE ${unique}INDEX ${q2(step.name)} ON ${
|
|
6319
|
+
`CREATE ${unique}INDEX ${q2(step.name)} ON ${qualifiedName(step.table, step.namespace)}${using} (${cols})${nulls}${where};`
|
|
5778
6320
|
];
|
|
5779
6321
|
}
|
|
5780
6322
|
case "dropIndex":
|
|
5781
6323
|
if (step.constraintName !== undefined) {
|
|
5782
6324
|
if (step.table === undefined)
|
|
5783
|
-
throw new
|
|
5784
|
-
return [`ALTER TABLE ${
|
|
6325
|
+
throw new VibeError31({ code: "VIBE_MIGRATION", message: "Constraint-backed index removal requires its owning table" });
|
|
6326
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.constraintName)};`];
|
|
5785
6327
|
}
|
|
5786
|
-
return [`DROP INDEX ${
|
|
6328
|
+
return [`DROP INDEX ${qualifiedName(step.name, step.namespace)};`];
|
|
5787
6329
|
case "addCheck":
|
|
5788
6330
|
return [
|
|
5789
|
-
`ALTER TABLE ${
|
|
6331
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.name)} CHECK (${step.expression});`
|
|
5790
6332
|
];
|
|
5791
6333
|
case "dropCheck":
|
|
5792
|
-
return [`ALTER TABLE ${
|
|
6334
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.name)};`];
|
|
5793
6335
|
case "addForeignKey": {
|
|
5794
6336
|
const cols = step.columns.map((column) => q2(column)).join(", ");
|
|
5795
6337
|
const refCols = step.refColumns.map((column) => q2(column)).join(", ");
|
|
5796
6338
|
return [
|
|
5797
|
-
`ALTER TABLE ${
|
|
6339
|
+
`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
6340
|
];
|
|
5799
6341
|
}
|
|
5800
6342
|
case "addForeignKeyNotValid": {
|
|
5801
6343
|
const cols = step.columns.map((column) => q2(column)).join(", ");
|
|
5802
6344
|
const refCols = step.refColumns.map((column) => q2(column)).join(", ");
|
|
5803
6345
|
return [
|
|
5804
|
-
`ALTER TABLE ${
|
|
6346
|
+
`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
6347
|
];
|
|
5806
6348
|
}
|
|
5807
6349
|
case "addCheckNotValid":
|
|
5808
6350
|
return [
|
|
5809
|
-
`ALTER TABLE ${
|
|
6351
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.name)} CHECK (${step.expression}) NOT VALID;`
|
|
5810
6352
|
];
|
|
5811
6353
|
case "validateConstraint":
|
|
5812
|
-
return [`ALTER TABLE ${
|
|
6354
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} VALIDATE CONSTRAINT ${q2(step.name)};`];
|
|
5813
6355
|
case "renameTable":
|
|
5814
|
-
return [`ALTER TABLE ${
|
|
6356
|
+
return [`ALTER TABLE ${qualifiedName(step.from, step.namespace)} RENAME TO ${q2(step.to)};`];
|
|
5815
6357
|
case "renameColumn":
|
|
5816
|
-
return [`ALTER TABLE ${
|
|
6358
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} RENAME COLUMN ${q2(step.from)} TO ${q2(step.to)};`];
|
|
5817
6359
|
case "dropForeignKey":
|
|
5818
|
-
return [`ALTER TABLE ${
|
|
6360
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.constraintName)};`];
|
|
5819
6361
|
case "createJoinTable": {
|
|
5820
6362
|
const aType = columnTypeSql({ field: { ...step.a.field, isList: false } });
|
|
5821
6363
|
const bType = columnTypeSql({ field: { ...step.b.field, isList: false } });
|
|
5822
6364
|
const names = implicitJoinTableObjectNames2({ table: step.table });
|
|
5823
6365
|
return [
|
|
5824
|
-
`CREATE TABLE ${
|
|
6366
|
+
`CREATE TABLE ${qualifiedName(step.table, step.namespace)} (
|
|
5825
6367
|
"A" ${aType} NOT NULL,
|
|
5826
6368
|
"B" ${bType} NOT NULL
|
|
5827
6369
|
);`,
|
|
5828
|
-
`CREATE UNIQUE INDEX ${q2(names.uniqueIndex)} ON ${
|
|
5829
|
-
`CREATE INDEX ${q2(names.indexB)} ON ${
|
|
5830
|
-
`ALTER TABLE ${
|
|
5831
|
-
`ALTER TABLE ${
|
|
6370
|
+
`CREATE UNIQUE INDEX ${q2(names.uniqueIndex)} ON ${qualifiedName(step.table, step.namespace)} ("A", "B");`,
|
|
6371
|
+
`CREATE INDEX ${q2(names.indexB)} ON ${qualifiedName(step.table, step.namespace)} ("B");`,
|
|
6372
|
+
`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;`,
|
|
6373
|
+
`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
6374
|
];
|
|
5833
6375
|
}
|
|
5834
6376
|
case "addPrimaryKey": {
|
|
5835
6377
|
const cols = step.columns.map((column) => q2(column)).join(", ");
|
|
5836
6378
|
return [
|
|
5837
|
-
`ALTER TABLE ${
|
|
6379
|
+
`ALTER TABLE ${qualifiedName(step.table, step.namespace)} ADD CONSTRAINT ${q2(step.constraintName ?? defaultPrimaryKeyName3({ table: step.table, dialect: "postgres" }))} PRIMARY KEY (${cols});`
|
|
5838
6380
|
];
|
|
5839
6381
|
}
|
|
5840
6382
|
case "dropPrimaryKey":
|
|
5841
|
-
return [`ALTER TABLE ${
|
|
6383
|
+
return [`ALTER TABLE ${qualifiedName(step.table, step.namespace)} DROP CONSTRAINT ${q2(step.constraintName ?? defaultPrimaryKeyName3({ table: step.table, dialect: "postgres" }))};`];
|
|
5842
6384
|
}
|
|
5843
6385
|
}
|
|
5844
6386
|
function columnDefSql(params) {
|
|
5845
6387
|
const { field } = params;
|
|
5846
6388
|
const name = q2(field.dbName ?? field.name);
|
|
5847
6389
|
if (field.default?.kind === "autoincrement" && !isIdentityColumn({ field })) {
|
|
5848
|
-
throw new
|
|
6390
|
+
throw new VibeError31({
|
|
5849
6391
|
code: "VIBE_MIGRATION",
|
|
5850
6392
|
message: `autoincrement default on "${field.name}" requires a non-list Int or BigInt column`,
|
|
5851
6393
|
meta: { field: field.name }
|
|
@@ -5865,7 +6407,7 @@ function renderAlterColumn(params) {
|
|
|
5865
6407
|
const { step } = params;
|
|
5866
6408
|
const statements = [];
|
|
5867
6409
|
const column = q2(step.column);
|
|
5868
|
-
const alterPrefix = `ALTER TABLE ${
|
|
6410
|
+
const alterPrefix = `ALTER TABLE ${qualifiedName(step.table, step.namespace)} ALTER COLUMN ${column}`;
|
|
5869
6411
|
const storageChange = step.changes.includes("type") && isEnumStringTransition({ from: step.from, to: step.to });
|
|
5870
6412
|
if (storageChange)
|
|
5871
6413
|
assertEnumStringTransition({ from: step.from, to: step.to, dialect: "postgres", table: step.table, column: step.column });
|
|
@@ -5881,7 +6423,7 @@ function renderAlterColumn(params) {
|
|
|
5881
6423
|
}
|
|
5882
6424
|
if (step.changes.includes("default") || droppedDefault) {
|
|
5883
6425
|
if (step.to.default?.kind === "autoincrement") {
|
|
5884
|
-
throw new
|
|
6426
|
+
throw new VibeError31({
|
|
5885
6427
|
code: "VIBE_MIGRATION",
|
|
5886
6428
|
message: `cannot add autoincrement to existing column "${step.table}"."${step.column}" \u2014 drop and recreate the column instead`,
|
|
5887
6429
|
meta: { table: step.table, column: step.column }
|
|
@@ -5892,15 +6434,16 @@ function renderAlterColumn(params) {
|
|
|
5892
6434
|
}
|
|
5893
6435
|
return statements;
|
|
5894
6436
|
}
|
|
5895
|
-
function
|
|
5896
|
-
|
|
6437
|
+
function qualifiedName(name, namespace = "public") {
|
|
6438
|
+
assertSqlNamespace3({ namespace, dialect: "postgres" });
|
|
6439
|
+
return `${q2(namespace)}.${q2(name)}`;
|
|
5897
6440
|
}
|
|
5898
6441
|
function q2(name) {
|
|
5899
|
-
return
|
|
6442
|
+
return postgresDialect9.quoteIdent({ name });
|
|
5900
6443
|
}
|
|
5901
6444
|
|
|
5902
6445
|
// packages/migrate/src/ddl/sqlite.ts
|
|
5903
|
-
import { VibeError as
|
|
6446
|
+
import { VibeError as VibeError32, implicitJoinTableObjectNames as implicitJoinTableObjectNames3, isEnumType as isEnumType8, unsupportedCapability as unsupportedCapability11 } from "@vibeorm/schema";
|
|
5904
6447
|
import { sqliteDialect as sqliteDialect3 } from "@vibeorm/sql";
|
|
5905
6448
|
|
|
5906
6449
|
// packages/migrate/src/ddl/drop-order.ts
|
|
@@ -5950,6 +6493,8 @@ function orderDropsChildrenFirst(params) {
|
|
|
5950
6493
|
function describeStep(params) {
|
|
5951
6494
|
const { step } = params;
|
|
5952
6495
|
switch (step.kind) {
|
|
6496
|
+
case "createNamespace":
|
|
6497
|
+
return `create namespace ${JSON.stringify(step.namespace)}`;
|
|
5953
6498
|
case "addExclusion":
|
|
5954
6499
|
return `add exclusion "${step.exclusion.name}" on "${step.table}"`;
|
|
5955
6500
|
case "dropExclusion":
|
|
@@ -6020,6 +6565,12 @@ function describeStep(params) {
|
|
|
6020
6565
|
// packages/migrate/src/ddl/sqlite.ts
|
|
6021
6566
|
function renderSqliteSteps(params) {
|
|
6022
6567
|
const { steps } = params;
|
|
6568
|
+
for (const step of steps) {
|
|
6569
|
+
if (step.kind === "createIndex" && step.columns.some((column) => column.operatorClass !== undefined))
|
|
6570
|
+
throw unsupportedCapability11({ dialect: "sqlite", feature: "indexOperatorClasses" });
|
|
6571
|
+
if (step.kind === "createNamespace" || step.namespace !== undefined || step.refNamespace !== undefined)
|
|
6572
|
+
throw unsupportedCapability11({ dialect: "sqlite", feature: "fixedNamespaces" });
|
|
6573
|
+
}
|
|
6023
6574
|
for (const step of steps)
|
|
6024
6575
|
if (isRlsStep({ step }))
|
|
6025
6576
|
renderRlsStep({ step, dialect: "sqlite" });
|
|
@@ -6040,7 +6591,7 @@ function renderSqliteSteps(params) {
|
|
|
6040
6591
|
continue;
|
|
6041
6592
|
const awaitsNewParent = [...toNorm?.tables.get(table)?.fks.values() ?? []].some((fk) => createdTables.has(fk.refTable));
|
|
6042
6593
|
if (awaitsNewParent) {
|
|
6043
|
-
throw new
|
|
6594
|
+
throw new VibeError32({
|
|
6044
6595
|
code: "VIBE_MIGRATION",
|
|
6045
6596
|
message: `cannot order this sqlite migration safely: rebuilding "${table}" must happen BEFORE the dropped table(s) it references (${[
|
|
6046
6597
|
...droppedTables
|
|
@@ -6069,7 +6620,7 @@ function renderSqliteSteps(params) {
|
|
|
6069
6620
|
switch (step.kind) {
|
|
6070
6621
|
case "addExclusion":
|
|
6071
6622
|
case "dropExclusion":
|
|
6072
|
-
throw new
|
|
6623
|
+
throw new VibeError32({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "sqlite", feature: "exclusionConstraints" } });
|
|
6073
6624
|
case "createEnum":
|
|
6074
6625
|
case "dropEnum":
|
|
6075
6626
|
case "addEnumValue":
|
|
@@ -6155,7 +6706,7 @@ function renderSqliteSteps(params) {
|
|
|
6155
6706
|
case "renameColumn": {
|
|
6156
6707
|
const renamed = step.kind === "renameTable" ? step.from : step.table;
|
|
6157
6708
|
if (rebuildSet.has(renamed) || step.kind === "renameTable" && rebuildSet.has(step.to)) {
|
|
6158
|
-
throw new
|
|
6709
|
+
throw new VibeError32({
|
|
6159
6710
|
code: "VIBE_MIGRATION",
|
|
6160
6711
|
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
6712
|
meta: { reason: "rename-ambiguous", table: renamed, dialect: "sqlite" }
|
|
@@ -6167,7 +6718,7 @@ function renderSqliteSteps(params) {
|
|
|
6167
6718
|
case "addForeignKeyNotValid":
|
|
6168
6719
|
case "addCheckNotValid":
|
|
6169
6720
|
case "validateConstraint":
|
|
6170
|
-
throw
|
|
6721
|
+
throw unsupportedCapability11({
|
|
6171
6722
|
dialect: "sqlite",
|
|
6172
6723
|
feature: "stagedConstraints",
|
|
6173
6724
|
hint: "sqlite applies constraint changes by rebuilding the table; add the constraint in one step"
|
|
@@ -6209,7 +6760,7 @@ function classifyRebuilds(params) {
|
|
|
6209
6760
|
switch (step.kind) {
|
|
6210
6761
|
case "addExclusion":
|
|
6211
6762
|
case "dropExclusion":
|
|
6212
|
-
throw new
|
|
6763
|
+
throw new VibeError32({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "sqlite", feature: "exclusionConstraints" } });
|
|
6213
6764
|
case "alterColumn":
|
|
6214
6765
|
case "dropForeignKey":
|
|
6215
6766
|
case "addPrimaryKey":
|
|
@@ -6261,7 +6812,7 @@ async function assertSqliteRebuildTableOptions(params) {
|
|
|
6261
6812
|
for (const table of params.tables) {
|
|
6262
6813
|
const row = rows.find((entry) => entry.schema === "main" && entry.name === table);
|
|
6263
6814
|
if (row === undefined || Number(row.strict) !== 0 || Number(row.wr) !== 0) {
|
|
6264
|
-
throw
|
|
6815
|
+
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
6816
|
}
|
|
6266
6817
|
}
|
|
6267
6818
|
}
|
|
@@ -6270,7 +6821,7 @@ function renderTableRebuild(params) {
|
|
|
6270
6821
|
const source = from.tables.get(table);
|
|
6271
6822
|
const target = to.tables.get(table);
|
|
6272
6823
|
if (source === undefined || target === undefined) {
|
|
6273
|
-
throw new
|
|
6824
|
+
throw new VibeError32({
|
|
6274
6825
|
code: "VIBE_MIGRATION",
|
|
6275
6826
|
message: `cannot rebuild "${table}" \u2014 the table is missing from the ${source === undefined ? "from" : "to"} schema`,
|
|
6276
6827
|
meta: { table }
|
|
@@ -6297,7 +6848,7 @@ function renderTableRebuild(params) {
|
|
|
6297
6848
|
}));
|
|
6298
6849
|
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
6850
|
if (common.length === 0) {
|
|
6300
|
-
throw
|
|
6851
|
+
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
6852
|
}
|
|
6302
6853
|
const cols = common.map((name) => q3(name)).join(", ");
|
|
6303
6854
|
statements.push(`INSERT INTO ${q3(tmp)} (${cols}) SELECT ${cols} FROM ${q3(table)};`);
|
|
@@ -6405,7 +6956,7 @@ function columnDefSql2(params) {
|
|
|
6405
6956
|
const { field, constraintTable, enums, inlinePkAutoincrement } = params;
|
|
6406
6957
|
const name = field.dbName ?? field.name;
|
|
6407
6958
|
if (field.default?.kind === "autoincrement" && !inlinePkAutoincrement) {
|
|
6408
|
-
throw new
|
|
6959
|
+
throw new VibeError32({
|
|
6409
6960
|
code: "VIBE_MIGRATION",
|
|
6410
6961
|
message: `autoincrement on sqlite requires "${field.name}" to be the single-column Int or BigInt primary key`,
|
|
6411
6962
|
meta: { field: field.name }
|
|
@@ -6424,7 +6975,7 @@ function columnDefSql2(params) {
|
|
|
6424
6975
|
const enumName = field.type.enum;
|
|
6425
6976
|
const values = resolveEnumValues({ name: enumName, enums });
|
|
6426
6977
|
if (values === undefined) {
|
|
6427
|
-
throw new
|
|
6978
|
+
throw new VibeError32({
|
|
6428
6979
|
code: "VIBE_MIGRATION",
|
|
6429
6980
|
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
6981
|
meta: { field: field.name, enum: enumName }
|
|
@@ -6438,14 +6989,14 @@ function columnDefSql2(params) {
|
|
|
6438
6989
|
function createIndexSql(params) {
|
|
6439
6990
|
const { table, name, unique, kind, columns, where } = params;
|
|
6440
6991
|
if (params.nullsNotDistinct === true) {
|
|
6441
|
-
throw
|
|
6992
|
+
throw unsupportedCapability11({
|
|
6442
6993
|
dialect: "sqlite",
|
|
6443
6994
|
feature: `UNIQUE NULLS NOT DISTINCT (index "${name}")`,
|
|
6444
6995
|
hint: "sqlite treats NULLs as distinct in unique indexes and has no switch \u2014 gate the flag per dialect"
|
|
6445
6996
|
});
|
|
6446
6997
|
}
|
|
6447
6998
|
if (!unique && kind !== "btree") {
|
|
6448
|
-
throw
|
|
6999
|
+
throw unsupportedCapability11({
|
|
6449
7000
|
dialect: "sqlite",
|
|
6450
7001
|
feature: `index kind "${kind}" (index "${name}")`,
|
|
6451
7002
|
hint: "sqlite indexes are btree only; use an extension-specific virtual table for others"
|
|
@@ -6459,7 +7010,7 @@ function requireSchemas(params) {
|
|
|
6459
7010
|
const { step, fromNorm, toNorm } = params;
|
|
6460
7011
|
if (fromNorm !== undefined && toNorm !== undefined)
|
|
6461
7012
|
return;
|
|
6462
|
-
throw new
|
|
7013
|
+
throw new VibeError32({
|
|
6463
7014
|
code: "VIBE_MIGRATION",
|
|
6464
7015
|
message: `sqlite step "${describeStep({ step })}" requires a table rebuild \u2014 renderSteps needs the { from, to } schemas to construct it`,
|
|
6465
7016
|
meta: { step: step.kind }
|
|
@@ -6470,10 +7021,14 @@ function q3(name) {
|
|
|
6470
7021
|
}
|
|
6471
7022
|
|
|
6472
7023
|
// packages/migrate/src/ddl/mysql.ts
|
|
6473
|
-
import { VibeError as
|
|
7024
|
+
import { VibeError as VibeError33, implicitJoinTableObjectNames as implicitJoinTableObjectNames4, unsupportedCapability as unsupportedCapability12 } from "@vibeorm/schema";
|
|
6474
7025
|
import { mysqlDialect as mysqlDialect3 } from "@vibeorm/sql";
|
|
6475
7026
|
function renderMysqlSteps(params) {
|
|
6476
7027
|
for (const step of params.steps) {
|
|
7028
|
+
if (step.namespace !== undefined || step.refNamespace !== undefined || step.kind === "createNamespace")
|
|
7029
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "fixedNamespaces" });
|
|
7030
|
+
if (step.kind === "createIndex" && step.columns.some((column) => column.operatorClass !== undefined))
|
|
7031
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "indexOperatorClasses" });
|
|
6477
7032
|
if (isRlsStep({ step }))
|
|
6478
7033
|
renderRlsStep({ step, dialect: "mysql" });
|
|
6479
7034
|
if (step.kind !== "alterColumn" || !step.changes.some((change) => change === "type" || change === "nullability" || change === "collation"))
|
|
@@ -6483,7 +7038,7 @@ function renderMysqlSteps(params) {
|
|
|
6483
7038
|
return object.dialect === "mysql" && (identity.kind === "columnDefault" || identity.kind === "generatedColumn" || identity.kind === "column") && identity.table.name === step.table && identity.column === step.column;
|
|
6484
7039
|
});
|
|
6485
7040
|
if (owner !== undefined)
|
|
6486
|
-
throw
|
|
7041
|
+
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
7042
|
}
|
|
6488
7043
|
const enums = new Map;
|
|
6489
7044
|
for (const enumDef of params.to?.enums ?? []) {
|
|
@@ -6503,7 +7058,7 @@ function renderMysqlSteps(params) {
|
|
|
6503
7058
|
continue;
|
|
6504
7059
|
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
7060
|
if (!retainedIndex)
|
|
6506
|
-
throw
|
|
7061
|
+
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
7062
|
}
|
|
6508
7063
|
let dropCursor = 0;
|
|
6509
7064
|
const inlineIndexes = new Map;
|
|
@@ -6512,7 +7067,7 @@ function renderMysqlSteps(params) {
|
|
|
6512
7067
|
const columns = step.kind === "createTable" ? step.columns : step.kind === "addColumn" ? [step.column] : [];
|
|
6513
7068
|
const counters = columns.filter((field) => field.default?.kind === "autoincrement");
|
|
6514
7069
|
if (counters.length > 1)
|
|
6515
|
-
throw
|
|
7070
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "multiple auto-increment columns" });
|
|
6516
7071
|
for (const column of counters) {
|
|
6517
7072
|
const name = column.dbName ?? column.name;
|
|
6518
7073
|
if (step.kind === "createTable" && step.primaryKey[0] === name)
|
|
@@ -6521,7 +7076,7 @@ function renderMysqlSteps(params) {
|
|
|
6521
7076
|
continue;
|
|
6522
7077
|
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
7078
|
if (key === undefined)
|
|
6524
|
-
throw
|
|
7079
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: `auto-increment column ${step.table}.${name} without an accompanying leading index` });
|
|
6525
7080
|
inlineIndexes.set(step, key);
|
|
6526
7081
|
consumed.add(key);
|
|
6527
7082
|
}
|
|
@@ -6540,6 +7095,8 @@ function renderMysqlSteps(params) {
|
|
|
6540
7095
|
function renderStep2(params) {
|
|
6541
7096
|
const { step, enums } = params;
|
|
6542
7097
|
switch (step.kind) {
|
|
7098
|
+
case "createNamespace":
|
|
7099
|
+
throw unsupportedCapability12({ dialect: "mysql", feature: "fixedNamespaces" });
|
|
6543
7100
|
case "enableRls":
|
|
6544
7101
|
case "forceRls":
|
|
6545
7102
|
case "disableRls":
|
|
@@ -6581,21 +7138,21 @@ ${lines.join(`,
|
|
|
6581
7138
|
return renderAlterColumn2({ step, enums });
|
|
6582
7139
|
case "createIndex": {
|
|
6583
7140
|
if (step.where !== undefined) {
|
|
6584
|
-
throw
|
|
7141
|
+
throw unsupportedCapability12({
|
|
6585
7142
|
dialect: "mysql",
|
|
6586
7143
|
feature: `partial index "${step.name}"`,
|
|
6587
7144
|
hint: "mysql has no partial indexes \u2014 drop the WHERE predicate or gate the index per dialect"
|
|
6588
7145
|
});
|
|
6589
7146
|
}
|
|
6590
7147
|
if (step.nullsNotDistinct === true) {
|
|
6591
|
-
throw
|
|
7148
|
+
throw unsupportedCapability12({
|
|
6592
7149
|
dialect: "mysql",
|
|
6593
7150
|
feature: `UNIQUE NULLS NOT DISTINCT (index "${step.name}")`,
|
|
6594
7151
|
hint: "mysql treats NULLs as distinct in unique indexes and has no switch \u2014 gate the flag per dialect"
|
|
6595
7152
|
});
|
|
6596
7153
|
}
|
|
6597
7154
|
if (!step.unique && step.indexKind !== "btree") {
|
|
6598
|
-
throw
|
|
7155
|
+
throw unsupportedCapability12({
|
|
6599
7156
|
dialect: "mysql",
|
|
6600
7157
|
feature: `index kind "${step.indexKind}" (index "${step.name}")`,
|
|
6601
7158
|
hint: "mysql secondary indexes are btree only"
|
|
@@ -6612,12 +7169,12 @@ ${lines.join(`,
|
|
|
6612
7169
|
];
|
|
6613
7170
|
case "addExclusion":
|
|
6614
7171
|
case "dropExclusion":
|
|
6615
|
-
throw new
|
|
7172
|
+
throw new VibeError33({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Exclusion constraints require PostgreSQL", meta: { dialect: "mysql", feature: "exclusionConstraints" } });
|
|
6616
7173
|
case "dropCheck":
|
|
6617
7174
|
return [`ALTER TABLE ${q4(step.table)} DROP CHECK ${q4(step.name)};`];
|
|
6618
7175
|
case "dropIndex": {
|
|
6619
7176
|
if (step.table === undefined) {
|
|
6620
|
-
throw new
|
|
7177
|
+
throw new VibeError33({
|
|
6621
7178
|
code: "VIBE_MIGRATION",
|
|
6622
7179
|
message: `dropIndex "${step.name}" carries no table \u2014 mysql's DROP INDEX requires one`,
|
|
6623
7180
|
meta: { index: step.name }
|
|
@@ -6639,7 +7196,7 @@ ${lines.join(`,
|
|
|
6639
7196
|
case "addForeignKeyNotValid":
|
|
6640
7197
|
case "addCheckNotValid":
|
|
6641
7198
|
case "validateConstraint":
|
|
6642
|
-
throw
|
|
7199
|
+
throw unsupportedCapability12({
|
|
6643
7200
|
dialect: "mysql",
|
|
6644
7201
|
feature: "stagedConstraints",
|
|
6645
7202
|
hint: "MySQL validates a constraint when it is added; add it in one step, or take the outage window that scan needs"
|
|
@@ -6674,7 +7231,7 @@ function columnDefSql3(params) {
|
|
|
6674
7231
|
const name = q4(field.dbName ?? field.name);
|
|
6675
7232
|
const autoincrement = field.default?.kind === "autoincrement";
|
|
6676
7233
|
if (autoincrement && field.type !== "Int" && field.type !== "BigInt") {
|
|
6677
|
-
throw new
|
|
7234
|
+
throw new VibeError33({
|
|
6678
7235
|
code: "VIBE_MIGRATION",
|
|
6679
7236
|
message: `autoincrement default on "${field.name}" requires a non-list Int or BigInt column`,
|
|
6680
7237
|
meta: { field: field.name }
|
|
@@ -6696,7 +7253,7 @@ function renderAlterColumn2(params) {
|
|
|
6696
7253
|
const { step, enums } = params;
|
|
6697
7254
|
assertEnumStringTransition({ from: step.from, to: step.to, dialect: "mysql", table: step.table, column: step.column });
|
|
6698
7255
|
if (step.to.default?.kind === "autoincrement" && step.from.default?.kind !== "autoincrement") {
|
|
6699
|
-
throw new
|
|
7256
|
+
throw new VibeError33({
|
|
6700
7257
|
code: "VIBE_MIGRATION",
|
|
6701
7258
|
message: `cannot add autoincrement to existing column "${step.table}"."${step.column}" \u2014 drop and recreate the column instead`,
|
|
6702
7259
|
meta: { table: step.table, column: step.column }
|
|
@@ -6729,31 +7286,31 @@ function declaredSqlitePreservation(params) {
|
|
|
6729
7286
|
if (params.from === undefined || to === undefined || (params.from.migrationObjects?.length ?? 0) === 0)
|
|
6730
7287
|
return { to, restore, columns };
|
|
6731
7288
|
const invalid = sqliteInvalidatedTables({ steps: params.steps, from: params.from });
|
|
6732
|
-
const retained = new Set((to.migrationObjects ?? []).map((object) =>
|
|
7289
|
+
const retained = new Set((to.migrationObjects ?? []).map((object) => sqlObjectKey10({ identity: object.identity })));
|
|
6733
7290
|
for (const object of params.from.migrationObjects ?? []) {
|
|
6734
7291
|
const identity = object.identity;
|
|
6735
|
-
if (!("table" in identity) || identity.table.namespace !== "main" || !invalid.rebuilt.has(identity.table.name) || !retained.has(
|
|
7292
|
+
if (!("table" in identity) || identity.table.namespace !== "main" || !invalid.rebuilt.has(identity.table.name) || !retained.has(sqlObjectKey10({ identity })))
|
|
6736
7293
|
continue;
|
|
6737
7294
|
const facts = object.expected?.facts;
|
|
6738
7295
|
if (facts === undefined)
|
|
6739
|
-
throw new
|
|
7296
|
+
throw new VibeError34({ code: "VIBE_MIGRATION", message: "SQLite rebuild requires independently declared preservation definitions", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
6740
7297
|
if (identity.kind === "trigger" || identity.kind === "index") {
|
|
6741
7298
|
if (identity.kind === "index" && to.models.some((model) => (model.dbName ?? model.name) === identity.table.name && model.indexes.some((index) => index.name === identity.name)))
|
|
6742
7299
|
continue;
|
|
6743
7300
|
if (typeof facts.definition !== "string")
|
|
6744
|
-
throw new
|
|
7301
|
+
throw new VibeError34({ code: "VIBE_MIGRATION", message: "Missing SQLite restoration SQL", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
6745
7302
|
const statements = restore.get(identity.table.name) ?? [];
|
|
6746
7303
|
statements.push(facts.definition);
|
|
6747
7304
|
restore.set(identity.table.name, statements);
|
|
6748
7305
|
} else if (identity.kind === "column" || identity.kind === "generatedColumn") {
|
|
6749
7306
|
if (typeof facts.columnDefinition !== "string")
|
|
6750
|
-
throw new
|
|
7307
|
+
throw new VibeError34({ code: "VIBE_MIGRATION", message: "Missing SQLite column clause", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
6751
7308
|
const fields = columns.get(identity.table.name) ?? new Map;
|
|
6752
7309
|
fields.set(identity.column, { sql: facts.columnDefinition, generated: Number(facts.generated) > 1 });
|
|
6753
7310
|
columns.set(identity.table.name, fields);
|
|
6754
7311
|
} else if (identity.kind === "columnDefault") {
|
|
6755
7312
|
if (facts.definition !== null && typeof facts.definition !== "string")
|
|
6756
|
-
throw new
|
|
7313
|
+
throw new VibeError34({ code: "VIBE_MIGRATION", message: "Missing SQLite default clause", meta: { reason: "sqlite-preservation-unavailable", identity } });
|
|
6757
7314
|
const expression = facts.definition;
|
|
6758
7315
|
to = { ...to, models: to.models.map((model) => (model.dbName ?? model.name) !== identity.table.name ? model : { ...model, fields: model.fields.map((field) => {
|
|
6759
7316
|
if ((field.dbName ?? field.name) !== identity.column)
|
|
@@ -6795,6 +7352,7 @@ function collectExtensionArtifacts(params) {
|
|
|
6795
7352
|
const { schema, dialect, extensions } = params;
|
|
6796
7353
|
const setup = [];
|
|
6797
7354
|
const objects = [];
|
|
7355
|
+
const groups = [];
|
|
6798
7356
|
for (const extension of extensions) {
|
|
6799
7357
|
const artifactsHook = extension.migrate?.artifacts;
|
|
6800
7358
|
if (artifactsHook === undefined)
|
|
@@ -6802,6 +7360,7 @@ function collectExtensionArtifacts(params) {
|
|
|
6802
7360
|
const declarations = extensionDeclarations({ schema, name: extension.name });
|
|
6803
7361
|
if (Object.keys(declarations).length === 0 && extension.schema?.configOnly !== true)
|
|
6804
7362
|
continue;
|
|
7363
|
+
const contributed = [];
|
|
6805
7364
|
for (const artifact of artifactsHook({
|
|
6806
7365
|
schema,
|
|
6807
7366
|
dialect,
|
|
@@ -6809,10 +7368,19 @@ function collectExtensionArtifacts(params) {
|
|
|
6809
7368
|
options: extension.options,
|
|
6810
7369
|
declarations
|
|
6811
7370
|
})) {
|
|
6812
|
-
(artifact.owns.kind === "postgresExtension"
|
|
7371
|
+
if (artifact.owns.kind === "postgresExtension")
|
|
7372
|
+
setup.push(artifact);
|
|
7373
|
+
else {
|
|
7374
|
+
objects.push(artifact);
|
|
7375
|
+
contributed.push(artifact);
|
|
7376
|
+
}
|
|
6813
7377
|
}
|
|
7378
|
+
groups.push({ extension: extension.name, objects: contributed });
|
|
6814
7379
|
}
|
|
6815
|
-
return { setup, objects };
|
|
7380
|
+
return { setup, objects, groups };
|
|
7381
|
+
}
|
|
7382
|
+
function lastDeclaredSchema(params) {
|
|
7383
|
+
return [...params.migrations].reverse().find((migration) => migration.desiredState !== undefined)?.desiredState?.to.schema;
|
|
6816
7384
|
}
|
|
6817
7385
|
function ownedTableNames(params) {
|
|
6818
7386
|
const exact = new Set;
|
|
@@ -6914,6 +7482,36 @@ async function probeInstalledExtensionState(params) {
|
|
|
6914
7482
|
function artifactCreateSql(params) {
|
|
6915
7483
|
return params.artifacts.flatMap((artifact) => [...artifact.create]);
|
|
6916
7484
|
}
|
|
7485
|
+
function planChangedArtifacts(params) {
|
|
7486
|
+
const previousById = new Map((params.previous?.objects ?? []).map((artifact) => [artifact.id, artifact]));
|
|
7487
|
+
const probedIds = new Set((params.probed ?? []).map((artifact) => artifact.id));
|
|
7488
|
+
const missingIds = new Set(params.missing.map((artifact) => artifact.id));
|
|
7489
|
+
const rebuilt = new Set;
|
|
7490
|
+
for (const group of params.current.groups) {
|
|
7491
|
+
const changed = group.objects.filter((artifact) => {
|
|
7492
|
+
if (missingIds.has(artifact.id))
|
|
7493
|
+
return false;
|
|
7494
|
+
if (probedIds.has(artifact.id))
|
|
7495
|
+
return true;
|
|
7496
|
+
const before = previousById.get(artifact.id);
|
|
7497
|
+
return before !== undefined && JSON.stringify(before.owns) === JSON.stringify(artifact.owns) && JSON.stringify(before.create) !== JSON.stringify(artifact.create);
|
|
7498
|
+
});
|
|
7499
|
+
if (changed.length === 0)
|
|
7500
|
+
continue;
|
|
7501
|
+
const rebuiltColumnTables = new Set(changed.flatMap((artifact) => artifact.owns.kind === "column" ? [artifact.owns.table] : []));
|
|
7502
|
+
for (const artifact of group.objects) {
|
|
7503
|
+
if (missingIds.has(artifact.id))
|
|
7504
|
+
continue;
|
|
7505
|
+
const dependent = (artifact.owns.kind === "index" || artifact.owns.kind === "trigger") && rebuiltColumnTables.has(artifact.owns.table);
|
|
7506
|
+
if (changed.includes(artifact) || dependent)
|
|
7507
|
+
rebuilt.add(artifact.id);
|
|
7508
|
+
}
|
|
7509
|
+
}
|
|
7510
|
+
return params.current.objects.filter((artifact) => rebuilt.has(artifact.id));
|
|
7511
|
+
}
|
|
7512
|
+
function artifactDropSql(params) {
|
|
7513
|
+
return [...params.artifacts].reverse().flatMap((artifact) => [...artifact.drop]);
|
|
7514
|
+
}
|
|
6917
7515
|
// packages/migrate/src/runner.ts
|
|
6918
7516
|
function migrationRlsSecurityChanges(params) {
|
|
6919
7517
|
const source = params.migration.rls;
|
|
@@ -6997,10 +7595,10 @@ function assertNativeMigrationMetadata(params) {
|
|
|
6997
7595
|
if (!native)
|
|
6998
7596
|
return;
|
|
6999
7597
|
if (!DIALECT_CAPABILITIES3[params.dialect].nativeRowLevelSecurity) {
|
|
7000
|
-
throw new
|
|
7598
|
+
throw new VibeError35({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Native RLS migrations are unsupported on ${params.dialect}`, meta: { feature: "nativeRowLevelSecurity", dialect: params.dialect } });
|
|
7001
7599
|
}
|
|
7002
7600
|
if (params.migration.rls === undefined) {
|
|
7003
|
-
throw new
|
|
7601
|
+
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 } });
|
|
7004
7602
|
}
|
|
7005
7603
|
}
|
|
7006
7604
|
var PG_ADVISORY_LOCK_KEY = 1447645253;
|
|
@@ -7045,11 +7643,11 @@ async function assertForwardStart(params) {
|
|
|
7045
7643
|
const state = params.migration.desiredState;
|
|
7046
7644
|
if (state === undefined)
|
|
7047
7645
|
return;
|
|
7048
|
-
const report = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state.from, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7646
|
+
const report = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state.from, external: params.external, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7049
7647
|
if (report.inSync)
|
|
7050
7648
|
return;
|
|
7051
7649
|
if (state.down !== undefined) {
|
|
7052
|
-
const restored = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state.down, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7650
|
+
const restored = await verifyMigrationState({ executor: params.executor, dialect: params.dialect, expected: state.down, external: params.external, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7053
7651
|
if (restored.inSync)
|
|
7054
7652
|
return;
|
|
7055
7653
|
}
|
|
@@ -7064,6 +7662,7 @@ function generateMigration(params) {
|
|
|
7064
7662
|
extensions: params.extensions ?? []
|
|
7065
7663
|
});
|
|
7066
7664
|
const allArtifacts = [...artifacts.setup, ...artifacts.objects];
|
|
7665
|
+
const rebuilt = planChangedArtifacts({ previous: collectExtensionArtifacts({ schema: from, dialect, extensions: params.extensions ?? [] }), current: artifacts, missing: [] });
|
|
7067
7666
|
assertRetainedMigrationObjects({ from: from.migrationObjects ?? [], to: to.migrationObjects ?? [] });
|
|
7068
7667
|
const strippedFrom = preserveMigrationObjects({ from: stripExtensionArtifacts({ schema: from, artifacts: allArtifacts }), to, objects: [...from.migrationObjects ?? [], ...to.migrationObjects ?? []] });
|
|
7069
7668
|
const steps = diffSchemas({ from: strippedFrom, to, dialect, ...params.renames === undefined ? {} : { renames: params.renames } });
|
|
@@ -7074,17 +7673,18 @@ function generateMigration(params) {
|
|
|
7074
7673
|
artifacts: missing.filter((artifact) => artifact.owns.kind === "postgresExtension")
|
|
7075
7674
|
}),
|
|
7076
7675
|
...coreSql,
|
|
7676
|
+
...artifactDropSql({ artifacts: rebuilt }),
|
|
7077
7677
|
...artifactCreateSql({
|
|
7078
7678
|
artifacts: missing.filter((artifact) => artifact.owns.kind !== "postgresExtension")
|
|
7079
7679
|
}),
|
|
7080
7680
|
...orderedMigrationObjects({ objects: to.migrationObjects ?? [] }).flatMap((object) => {
|
|
7081
7681
|
if (object.dialect !== dialect)
|
|
7082
|
-
throw new
|
|
7682
|
+
throw new VibeError35({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Migration object ${object.id} requires ${object.dialect}` });
|
|
7083
7683
|
const previous = (params.desiredState?.from.schema.migrationObjects ?? from.migrationObjects)?.find((entry) => entry.id === object.id);
|
|
7084
7684
|
if (previous === undefined)
|
|
7085
7685
|
return [...object.create];
|
|
7086
7686
|
if (JSON.stringify(previous) !== JSON.stringify(object))
|
|
7087
|
-
throw new
|
|
7687
|
+
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 } });
|
|
7088
7688
|
return [];
|
|
7089
7689
|
})
|
|
7090
7690
|
];
|
|
@@ -7110,8 +7710,8 @@ async function withMigrationLock(params) {
|
|
|
7110
7710
|
}, (cause) => releaseLock({ executor, dialect: params.dialect }).then(() => {
|
|
7111
7711
|
throw cause;
|
|
7112
7712
|
}, (cleanupCause) => {
|
|
7113
|
-
if (cause instanceof
|
|
7114
|
-
throw new
|
|
7713
|
+
if (cause instanceof VibeError35)
|
|
7714
|
+
throw new VibeError35({ code: cause.code, message: cause.message, meta: { ...cause.meta, cleanupCause }, cause });
|
|
7115
7715
|
throw cause;
|
|
7116
7716
|
}));
|
|
7117
7717
|
};
|
|
@@ -7135,16 +7735,17 @@ function validateMigrationReplay(params) {
|
|
|
7135
7735
|
}
|
|
7136
7736
|
function assertMigrationMetadata(params) {
|
|
7137
7737
|
const { migration, dialect } = params;
|
|
7738
|
+
assertModuleAdoption({ migration });
|
|
7138
7739
|
if (migration.moduleOrigin !== undefined) {
|
|
7139
7740
|
assertMaterializedModuleMigration({ migration });
|
|
7140
7741
|
assertModuleOwnership({ origin: migration.moduleOrigin, desiredState: migration.desiredState });
|
|
7141
7742
|
}
|
|
7142
7743
|
if (migration.desiredStatePending === true)
|
|
7143
|
-
throw new
|
|
7744
|
+
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 } });
|
|
7144
7745
|
if (migration.desiredState !== undefined) {
|
|
7145
7746
|
assertDesiredState({ value: migration.desiredState, migration: migration.name });
|
|
7146
7747
|
if (migration.desiredState.dialect !== dialect)
|
|
7147
|
-
throw new
|
|
7748
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Migration desired-state dialect mismatch", meta: { reason: "desired-state-dialect-mismatch", migration: migration.name } });
|
|
7148
7749
|
}
|
|
7149
7750
|
assertNativeMigrationMetadata({ migration, sql: migration.sql, dialect });
|
|
7150
7751
|
assertExecutable({ migration, execution: migration.execution, sql: migration.sql, dialect });
|
|
@@ -7152,20 +7753,25 @@ function assertMigrationMetadata(params) {
|
|
|
7152
7753
|
async function applyMigrations(params) {
|
|
7153
7754
|
const { executor, migrations } = params;
|
|
7154
7755
|
const dialect = params.dialect ?? "postgres";
|
|
7756
|
+
const trustedReplay = isShadowReplayExecutor({ executor });
|
|
7155
7757
|
if (params.history !== undefined && params.history !== "complete" && params.history !== "subset")
|
|
7156
|
-
throw new
|
|
7758
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Unknown migration history validation mode", meta: { reason: "history-mode-invalid" } });
|
|
7157
7759
|
if (params.history === "complete")
|
|
7158
7760
|
assertDistinctMigrationNames({ migrations });
|
|
7159
7761
|
for (const migration of migrations)
|
|
7160
7762
|
assertMigrationMetadata({ migration, dialect });
|
|
7763
|
+
const externalDeclarations = await parseExternalObjects({ value: params.external ?? [], dialect });
|
|
7161
7764
|
return withMigrationLock({ executor, dialect, run: async (executor) => {
|
|
7765
|
+
const external = externalDeclarations.length === 0 ? undefined : await inspectExternalObjects({ executor, dialect, external: externalDeclarations });
|
|
7162
7766
|
let moduleRecords = await readModuleJournal({ executor, dialect });
|
|
7163
7767
|
const recorded = new Map([...moduleRecords].map(([name, record]) => [name, record.row]));
|
|
7768
|
+
if (external !== undefined)
|
|
7769
|
+
assertExternalOwnershipClaims({ external, identities: [...migrations.flatMap((migration) => migration.moduleOrigin?.owned ?? []), ...[...moduleRecords.values()].flatMap((record) => record.entry?.migration.moduleOrigin?.owned ?? [])] });
|
|
7164
7770
|
if (migrations.some((migration) => migration.moduleOrigin !== undefined) || [...moduleRecords.values()].some((record) => record.entry !== undefined)) {
|
|
7165
7771
|
assertModuleUpgradeHistory({ migrations, records: moduleRecords });
|
|
7166
7772
|
const namespace = await migrationNamespace({ executor, dialect });
|
|
7167
|
-
if (migrations.some((migration) => migration.moduleOrigin !== undefined && moduleNamespace({ origin: migration.moduleOrigin }) !== namespace))
|
|
7168
|
-
throw new
|
|
7773
|
+
if (dialect !== "postgres" && migrations.some((migration) => migration.moduleOrigin !== undefined && moduleNamespace({ origin: migration.moduleOrigin }) !== namespace))
|
|
7774
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Module placement differs from the active database namespace", meta: { reason: "module-placement-mismatch" } });
|
|
7169
7775
|
}
|
|
7170
7776
|
if (params.history === "complete")
|
|
7171
7777
|
assertCompleteHistory({ migrations, recorded, dialect });
|
|
@@ -7174,6 +7780,12 @@ async function applyMigrations(params) {
|
|
|
7174
7780
|
const stored = recorded.get(migration.name);
|
|
7175
7781
|
return stored === undefined || !isCompleteRecord({ dialect, migration, stored });
|
|
7176
7782
|
});
|
|
7783
|
+
const adopted = pending.find((migration) => migration.moduleAdoption !== undefined);
|
|
7784
|
+
if (adopted !== undefined && !trustedReplay)
|
|
7785
|
+
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 } });
|
|
7786
|
+
if (external !== undefined && !trustedReplay)
|
|
7787
|
+
for (const migration of pending)
|
|
7788
|
+
await assertExternalMigrationMutation({ executor, migration, dialect, namespace: await migrationNamespace({ executor, dialect }), external });
|
|
7177
7789
|
validateMigrationReplay({ ...params, migrations: pending, dialect });
|
|
7178
7790
|
await ensureMigrationsTable({ executor, dialect });
|
|
7179
7791
|
if (migrations.some((migration) => migration.moduleOrigin !== undefined))
|
|
@@ -7186,7 +7798,7 @@ async function applyMigrations(params) {
|
|
|
7186
7798
|
await assertLiveModuleOwnership({ executor, origin: migration.moduleOrigin, resume: stored !== undefined });
|
|
7187
7799
|
if (stored !== undefined) {
|
|
7188
7800
|
if (stored.statementIndex < 0) {
|
|
7189
|
-
throw new
|
|
7801
|
+
throw new VibeError35({
|
|
7190
7802
|
code: "VIBE_MIGRATION",
|
|
7191
7803
|
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`,
|
|
7192
7804
|
meta: { migration: migration.name, statementIndex: stored.statementIndex }
|
|
@@ -7208,6 +7820,7 @@ async function applyMigrations(params) {
|
|
|
7208
7820
|
migration,
|
|
7209
7821
|
start: stored.statementIndex,
|
|
7210
7822
|
moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }),
|
|
7823
|
+
external,
|
|
7211
7824
|
...params.probe === undefined ? {} : { probe: params.probe },
|
|
7212
7825
|
insertRecord: false,
|
|
7213
7826
|
...stored.error === undefined ? {} : { storedError: stored.error }
|
|
@@ -7223,10 +7836,10 @@ async function applyMigrations(params) {
|
|
|
7223
7836
|
executor,
|
|
7224
7837
|
dialect,
|
|
7225
7838
|
...migration.execution === undefined ? {} : { execution: migration.execution },
|
|
7226
|
-
run: () => runNonTransactional({ executor, dialect, migration, moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }), start: 0, insertRecord: true, ...params.probe === undefined ? {} : { probe: params.probe } })
|
|
7839
|
+
run: () => runNonTransactional({ executor, dialect, migration, external, moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }), start: 0, insertRecord: true, ...params.probe === undefined ? {} : { probe: params.probe } })
|
|
7227
7840
|
});
|
|
7228
7841
|
} else {
|
|
7229
|
-
await runTransactional({ executor, dialect, migration, moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }), ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7842
|
+
await runTransactional({ executor, dialect, migration, external, moduleOrigins: applicationModuleOrigins({ migration, records: moduleRecords }), ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7230
7843
|
}
|
|
7231
7844
|
if (migration.moduleOrigin !== undefined)
|
|
7232
7845
|
moduleRecords = await readModuleJournal({ executor, dialect });
|
|
@@ -7234,10 +7847,11 @@ async function applyMigrations(params) {
|
|
|
7234
7847
|
}
|
|
7235
7848
|
const final = migrations.at(-1);
|
|
7236
7849
|
if (final?.desiredState !== undefined) {
|
|
7850
|
+
const finalExternal = externalDeclarations.length === 0 ? undefined : await inspectExternalObjects({ executor, dialect, external: externalDeclarations });
|
|
7237
7851
|
if (params.probe === "rollback")
|
|
7238
7852
|
await executor({ text: "BEGIN" });
|
|
7239
7853
|
try {
|
|
7240
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: final.desiredState.from, to: final.desiredState.to, removals: final
|
|
7854
|
+
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 } }) });
|
|
7241
7855
|
} finally {
|
|
7242
7856
|
if (params.probe === "rollback")
|
|
7243
7857
|
await executor({ text: "ROLLBACK" });
|
|
@@ -7246,6 +7860,27 @@ async function applyMigrations(params) {
|
|
|
7246
7860
|
return { applied, skipped };
|
|
7247
7861
|
} });
|
|
7248
7862
|
}
|
|
7863
|
+
function comparisonRemovals(params) {
|
|
7864
|
+
return params.migration.desiredState?.unmanagedRemovals?.filter((identity) => params.external === undefined || externalObjectOwner({ identity, external: params.external }) === undefined);
|
|
7865
|
+
}
|
|
7866
|
+
async function assertExternalMigrationMutation(params) {
|
|
7867
|
+
assertExternalOwnershipClaims({ external: params.external, identities: params.migration.desiredState?.unmanagedRemovals ?? [] });
|
|
7868
|
+
const state = params.migration.desiredState;
|
|
7869
|
+
if (state === undefined)
|
|
7870
|
+
return;
|
|
7871
|
+
const identities = [];
|
|
7872
|
+
const steps = diffSchemas({ from: state.from.schema, to: state.to.schema, dialect: params.dialect });
|
|
7873
|
+
for (const step of steps) {
|
|
7874
|
+
if (step.kind === "createNamespace")
|
|
7875
|
+
identities.push({ kind: "namespace", name: step.namespace });
|
|
7876
|
+
else if ("table" in step && typeof step.table === "string")
|
|
7877
|
+
identities.push({ kind: "table", name: { namespace: step.namespace ?? params.namespace, name: step.table } });
|
|
7878
|
+
else if ("enumName" in step)
|
|
7879
|
+
identities.push({ kind: "enum", name: { namespace: step.namespace ?? params.namespace, name: step.enumName } });
|
|
7880
|
+
}
|
|
7881
|
+
assertExternalOwnershipClaims({ external: params.external, identities });
|
|
7882
|
+
await refuseExternalDependents({ executor: params.executor, dialect: params.dialect, external: params.external, steps, namespace: params.namespace });
|
|
7883
|
+
}
|
|
7249
7884
|
async function migrationStatus(params) {
|
|
7250
7885
|
const { executor, migrations } = params;
|
|
7251
7886
|
const dialect = params.dialect ?? "postgres";
|
|
@@ -7321,7 +7956,7 @@ async function repairMigration(params) {
|
|
|
7321
7956
|
const { executor, name, dialect } = params;
|
|
7322
7957
|
const moduleRecord = (await readModuleJournal({ executor, dialect })).get(name);
|
|
7323
7958
|
if (moduleRecord?.entry !== undefined)
|
|
7324
|
-
throw new
|
|
7959
|
+
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 } });
|
|
7325
7960
|
await ensureMigrationsTable({ executor, dialect });
|
|
7326
7961
|
if (params.as === "applied") {
|
|
7327
7962
|
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})`;
|
|
@@ -7343,7 +7978,7 @@ async function rollbackMigration(params) {
|
|
|
7343
7978
|
const stored = assertRollbackRecord({ migration, stored: recorded.get(migration.name), dialect });
|
|
7344
7979
|
const newest = [...recorded.keys()].at(-1);
|
|
7345
7980
|
if (newest !== migration.name)
|
|
7346
|
-
throw new
|
|
7981
|
+
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 } });
|
|
7347
7982
|
await validateRollbackRecovery({ executor, migration, downSql, stored, dialect });
|
|
7348
7983
|
await executeRollback({ ...params, executor, dialect, downSql, stored });
|
|
7349
7984
|
} });
|
|
@@ -7361,7 +7996,7 @@ async function rollbackMigrations(params) {
|
|
|
7361
7996
|
const target = journal.findIndex((row) => row.name === params.to);
|
|
7362
7997
|
const row = journal[target];
|
|
7363
7998
|
if (row?.state !== "applied")
|
|
7364
|
-
throw new
|
|
7999
|
+
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 } });
|
|
7365
8000
|
assertMigrationMetadata({ migration: byName.get(row.name), dialect });
|
|
7366
8001
|
start = target + 1;
|
|
7367
8002
|
}
|
|
@@ -7369,7 +8004,7 @@ async function rollbackMigrations(params) {
|
|
|
7369
8004
|
for (const row of journal.slice(start).reverse()) {
|
|
7370
8005
|
const migration = byName.get(row.name);
|
|
7371
8006
|
if (migration === undefined)
|
|
7372
|
-
throw new
|
|
8007
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Cannot roll back "${row.name}" without its migration file`, meta: { reason: "rollback-file-missing", migration: row.name } });
|
|
7373
8008
|
const stored = assertRollbackRecord({ migration, stored: recorded.get(row.name), dialect });
|
|
7374
8009
|
const downSql = validateRollbackFile({ ...params, migration, dialect });
|
|
7375
8010
|
plan.push({ migration, downSql, stored });
|
|
@@ -7386,20 +8021,23 @@ async function rollbackMigrations(params) {
|
|
|
7386
8021
|
}
|
|
7387
8022
|
function validateRollbackFile(params) {
|
|
7388
8023
|
const { migration, dialect } = params;
|
|
8024
|
+
assertModuleAdoption({ migration });
|
|
8025
|
+
if (migration.moduleAdoption !== undefined)
|
|
8026
|
+
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 } });
|
|
7389
8027
|
if (migration.desiredStatePending === true)
|
|
7390
|
-
throw new
|
|
8028
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Unfinalized desired state cannot be rolled back", meta: { reason: "desired-state-unfinalized", migration: migration.name } });
|
|
7391
8029
|
if (migration.moduleOrigin !== undefined) {
|
|
7392
8030
|
assertMaterializedModuleMigration({ migration });
|
|
7393
8031
|
assertModuleOwnership({ origin: migration.moduleOrigin, desiredState: migration.desiredState });
|
|
7394
8032
|
}
|
|
7395
8033
|
const downSql = migration.downSql;
|
|
7396
8034
|
if (downSql === undefined)
|
|
7397
|
-
throw new
|
|
8035
|
+
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 } });
|
|
7398
8036
|
assertProbeMode({ dialect, probe: params.probe });
|
|
7399
8037
|
if (migration.desiredState !== undefined) {
|
|
7400
8038
|
assertDesiredState({ value: migration.desiredState, migration: migration.name });
|
|
7401
8039
|
if (migration.desiredState.dialect !== dialect)
|
|
7402
|
-
throw new
|
|
8040
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Migration desired-state dialect mismatch", meta: { reason: "desired-state-dialect-mismatch" } });
|
|
7403
8041
|
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 } });
|
|
7404
8042
|
}
|
|
7405
8043
|
assertNativeMigrationMetadata({ migration, sql: downSql, dialect });
|
|
@@ -7412,10 +8050,10 @@ function validateRollbackFile(params) {
|
|
|
7412
8050
|
function assertRollbackRecord(params) {
|
|
7413
8051
|
const { migration, stored, dialect } = params;
|
|
7414
8052
|
if (stored === undefined)
|
|
7415
|
-
throw new
|
|
8053
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `migration "${migration.name}" is not recorded as applied; nothing to roll back`, meta: { migration: migration.name } });
|
|
7416
8054
|
assertChecksumMatches({ migration, stored: stored.checksum });
|
|
7417
8055
|
if (stored.statementIndex >= 0 && (!isCompleteRecord({ dialect, migration, stored }) || stored.error !== undefined))
|
|
7418
|
-
throw new
|
|
8056
|
+
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 } });
|
|
7419
8057
|
return stored;
|
|
7420
8058
|
}
|
|
7421
8059
|
async function validateRollbackRecovery(params) {
|
|
@@ -7424,7 +8062,7 @@ async function validateRollbackRecovery(params) {
|
|
|
7424
8062
|
return;
|
|
7425
8063
|
const start = -stored.statementIndex - 1;
|
|
7426
8064
|
if (start > downSql.length)
|
|
7427
|
-
throw new
|
|
8065
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: "Rollback progress exceeds its statement count", meta: { reason: "rollback-progress-invalid", migration: migration.name } });
|
|
7428
8066
|
if (dialect === "sqlite" && usesSelfManagedTransactions({ sql: downSql }))
|
|
7429
8067
|
assertSelfManagedResumeSafe({ sql: downSql, start, migration: migration.name, direction: "down" });
|
|
7430
8068
|
if (!marksInflight({ dialect, execution: migration.downExecution }) || start >= downSql.length)
|
|
@@ -7459,9 +8097,9 @@ async function runTransactionalDown(params) {
|
|
|
7459
8097
|
const limits = migration.downExecution;
|
|
7460
8098
|
if (wrap && dialect === "postgres" && limits !== undefined) {
|
|
7461
8099
|
if ((limits.lockTimeoutMs ?? 0) > 0)
|
|
7462
|
-
await executor({ text: renderMigrationSessionSet({ dialect:
|
|
8100
|
+
await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect9({ dialect }), setting: "lock_timeout", milliseconds: limits.lockTimeoutMs, local: true }).text });
|
|
7463
8101
|
if ((limits.statementTimeoutMs ?? 0) > 0)
|
|
7464
|
-
await executor({ text: renderMigrationSessionSet({ dialect:
|
|
8102
|
+
await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect9({ dialect }), setting: "statement_timeout", milliseconds: limits.statementTimeoutMs, local: true }).text });
|
|
7465
8103
|
}
|
|
7466
8104
|
if (migration.desiredState !== undefined)
|
|
7467
8105
|
assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.desiredState.unmanagedRemovals, ...params.probe === undefined ? {} : { probe: params.probe } }) });
|
|
@@ -7490,7 +8128,7 @@ async function runTransactionalDown(params) {
|
|
|
7490
8128
|
} else {
|
|
7491
8129
|
await cleanupSelfManagedFailure({ executor });
|
|
7492
8130
|
}
|
|
7493
|
-
throw new
|
|
8131
|
+
throw new VibeError35({
|
|
7494
8132
|
code: "VIBE_MIGRATION",
|
|
7495
8133
|
message: `rollback of "${migration.name}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
7496
8134
|
meta: { migration: migration.name, ...timeoutDiagnostic({ error }) },
|
|
@@ -7573,7 +8211,7 @@ async function runNonTransactionalDown(params) {
|
|
|
7573
8211
|
values: [marks ? `${INFLIGHT_PREFIX}${index}` : error instanceof Error ? error.message : String(error), migration.name]
|
|
7574
8212
|
});
|
|
7575
8213
|
} catch {}
|
|
7576
|
-
throw new
|
|
8214
|
+
throw new VibeError35({
|
|
7577
8215
|
code: "VIBE_MIGRATION",
|
|
7578
8216
|
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."}`,
|
|
7579
8217
|
meta: { migration: migration.name, statementIndex: index, ...marks ? { outcome: "unknown" } : {}, ...timeoutDiagnostic({ error }) },
|
|
@@ -7600,11 +8238,11 @@ async function runTransactional(params) {
|
|
|
7600
8238
|
const limits = migration.execution;
|
|
7601
8239
|
if (wrap && dialect === "postgres" && limits !== undefined) {
|
|
7602
8240
|
if ((limits.lockTimeoutMs ?? 0) > 0)
|
|
7603
|
-
await executor({ text: renderMigrationSessionSet({ dialect:
|
|
8241
|
+
await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect9({ dialect }), setting: "lock_timeout", milliseconds: limits.lockTimeoutMs, local: true }).text });
|
|
7604
8242
|
if ((limits.statementTimeoutMs ?? 0) > 0)
|
|
7605
|
-
await executor({ text: renderMigrationSessionSet({ dialect:
|
|
8243
|
+
await executor({ text: renderMigrationSessionSet({ dialect: getSqlDialect9({ dialect }), setting: "statement_timeout", milliseconds: limits.statementTimeoutMs, local: true }).text });
|
|
7606
8244
|
}
|
|
7607
|
-
await assertForwardStart({ executor, dialect, migration, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
8245
|
+
await assertForwardStart({ executor, dialect, migration, external: params.external, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7608
8246
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
7609
8247
|
if (migration.rls !== undefined)
|
|
7610
8248
|
await introspectPostgres({ executor, rlsSchema: migration.rls.from, inTransaction: true, requireInstalledRls: true });
|
|
@@ -7614,7 +8252,7 @@ async function runTransactional(params) {
|
|
|
7614
8252
|
statementIndex += 1;
|
|
7615
8253
|
}
|
|
7616
8254
|
if (migration.desiredState !== undefined)
|
|
7617
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.
|
|
8255
|
+
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 } }) });
|
|
7618
8256
|
if (migration.rls !== undefined)
|
|
7619
8257
|
await introspectPostgres({ executor, rlsSchema: migration.rls.to, inTransaction: true, requireInstalledRls: true });
|
|
7620
8258
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
@@ -7635,7 +8273,7 @@ async function runTransactional(params) {
|
|
|
7635
8273
|
} else {
|
|
7636
8274
|
await cleanupSelfManagedFailure({ executor });
|
|
7637
8275
|
}
|
|
7638
|
-
throw new
|
|
8276
|
+
throw new VibeError35({
|
|
7639
8277
|
code: "VIBE_MIGRATION",
|
|
7640
8278
|
message: `migration "${migration.name}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
7641
8279
|
meta: { migration: migration.name },
|
|
@@ -7658,9 +8296,9 @@ async function runNonTransactional(params) {
|
|
|
7658
8296
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
7659
8297
|
if (insertRecord) {
|
|
7660
8298
|
if (migration.desiredState !== undefined) {
|
|
7661
|
-
await assertForwardStart({ executor, dialect, migration, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
8299
|
+
await assertForwardStart({ executor, dialect, migration, external: params.external, ...params.probe === undefined ? {} : { probe: params.probe } });
|
|
7662
8300
|
if (migration.sql.length === 0)
|
|
7663
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.
|
|
8301
|
+
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 } }) });
|
|
7664
8302
|
}
|
|
7665
8303
|
if (migration.moduleOrigin !== undefined)
|
|
7666
8304
|
await insertModuleJournal({ executor, dialect, migration, index: 0, complete: false });
|
|
@@ -7683,7 +8321,7 @@ async function runNonTransactional(params) {
|
|
|
7683
8321
|
const state = await establishOperationState({ executor, dialect, operation });
|
|
7684
8322
|
if (state === "satisfied") {
|
|
7685
8323
|
if (start + 1 === migration.sql.length && migration.desiredState !== undefined)
|
|
7686
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, dialect, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.
|
|
8324
|
+
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 } }) });
|
|
7687
8325
|
if (start + 1 === migration.sql.length)
|
|
7688
8326
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
7689
8327
|
await checkpoint(start + 1);
|
|
@@ -7713,7 +8351,7 @@ async function runNonTransactional(params) {
|
|
|
7713
8351
|
}
|
|
7714
8352
|
await executeStatement({ executor, statement: migration.sql[index], dialect });
|
|
7715
8353
|
if (index === migration.sql.length - 1 && migration.desiredState !== undefined)
|
|
7716
|
-
assertMigrationState({ report: await verifyMigrationTransition({ executor, from: migration.desiredState.from, to: migration.desiredState.to, removals: migration.
|
|
8354
|
+
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 } }) });
|
|
7717
8355
|
if (index === migration.sql.length - 1)
|
|
7718
8356
|
await assertApplicationModuleState({ executor, origins: params.moduleOrigins, probe: params.probe });
|
|
7719
8357
|
if (atomic) {
|
|
@@ -7731,7 +8369,7 @@ async function runNonTransactional(params) {
|
|
|
7731
8369
|
} catch {}
|
|
7732
8370
|
const sqlState = sqlStateOf(error);
|
|
7733
8371
|
const timeout = sqlState === "55P03" ? "lock-timeout" : sqlState === "57014" ? "statement-timeout" : undefined;
|
|
7734
|
-
throw new
|
|
8372
|
+
throw new VibeError35({
|
|
7735
8373
|
code: "VIBE_MIGRATION",
|
|
7736
8374
|
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."}`,
|
|
7737
8375
|
meta: { migration: migration.name, statementIndex: index, ...marks ? { outcome: "unknown" } : {}, ...timeout === undefined ? {} : { reason: timeout, sqlState } },
|
|
@@ -7742,7 +8380,7 @@ async function runNonTransactional(params) {
|
|
|
7742
8380
|
await checkpoint(index + 1);
|
|
7743
8381
|
}
|
|
7744
8382
|
if (migration.moduleOrigin !== undefined) {
|
|
7745
|
-
assertMigrationState({ report: await verifyMigrationState({ executor, dialect, expected: migration.desiredState.to }) });
|
|
8383
|
+
assertMigrationState({ report: await verifyMigrationState({ executor, dialect, expected: migration.desiredState.to, external: params.external }) });
|
|
7746
8384
|
await completeModuleJournal({ executor, dialect, migration });
|
|
7747
8385
|
}
|
|
7748
8386
|
}
|
|
@@ -7759,7 +8397,7 @@ async function acquireLock(params) {
|
|
|
7759
8397
|
});
|
|
7760
8398
|
const acquired = Number(rows[0]?.vibe_lock) === 1;
|
|
7761
8399
|
if (!acquired) {
|
|
7762
|
-
throw new
|
|
8400
|
+
throw new VibeError35({
|
|
7763
8401
|
code: "VIBE_MIGRATION",
|
|
7764
8402
|
message: `could not acquire the migration lock ('${MYSQL_LOCK_NAME}') within ${MYSQL_LOCK_TIMEOUT_SECONDS}s \u2014 another migration run is in progress`,
|
|
7765
8403
|
meta: { lock: MYSQL_LOCK_NAME }
|
|
@@ -7794,7 +8432,7 @@ function uncertainStatement(params) {
|
|
|
7794
8432
|
const subject = direction === "down" ? `rollback of "${migration}"` : `migration "${migration}"`;
|
|
7795
8433
|
const position = `${direction === "down" ? "down statement" : "statement"} ${index + 1} of ${statements.length}`;
|
|
7796
8434
|
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.";
|
|
7797
|
-
return new
|
|
8435
|
+
return new VibeError35({
|
|
7798
8436
|
code: "VIBE_MIGRATION",
|
|
7799
8437
|
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)}`,
|
|
7800
8438
|
meta: {
|
|
@@ -7812,7 +8450,7 @@ function assertExecutable(params) {
|
|
|
7812
8450
|
if (execution === undefined || isDefaultExecution({ execution }))
|
|
7813
8451
|
return;
|
|
7814
8452
|
if (dialect !== "postgres") {
|
|
7815
|
-
throw new
|
|
8453
|
+
throw new VibeError35({
|
|
7816
8454
|
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
7817
8455
|
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`,
|
|
7818
8456
|
meta: { reason: "nontransactional-unsupported", dialect, migration: migration.name, feature: "nontransactionalMigrations" }
|
|
@@ -7835,7 +8473,7 @@ async function withSessionLimits(params) {
|
|
|
7835
8473
|
const statement = execution?.statementTimeoutMs ?? 0;
|
|
7836
8474
|
if (dialect !== "postgres" || lock === 0 && statement === 0)
|
|
7837
8475
|
return params.run();
|
|
7838
|
-
const sqlDialect =
|
|
8476
|
+
const sqlDialect = getSqlDialect9({ dialect });
|
|
7839
8477
|
const previous = (await executor({ text: renderMigrationSessionRead({ dialect: sqlDialect }).text, values: [] }))[0];
|
|
7840
8478
|
if (lock > 0)
|
|
7841
8479
|
await executor({ text: renderMigrationSessionSet({ dialect: sqlDialect, setting: "lock_timeout", milliseconds: lock }).text });
|
|
@@ -7888,7 +8526,7 @@ function assertChecksumMatches(params) {
|
|
|
7888
8526
|
const actual = checksumOf({ migration });
|
|
7889
8527
|
if (stored === actual)
|
|
7890
8528
|
return;
|
|
7891
|
-
throw new
|
|
8529
|
+
throw new VibeError35({
|
|
7892
8530
|
code: "VIBE_MIGRATION",
|
|
7893
8531
|
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)`,
|
|
7894
8532
|
meta: { migration: migration.name, recorded: stored, current: actual }
|
|
@@ -7899,7 +8537,7 @@ function assertResumable(params) {
|
|
|
7899
8537
|
const actual = checksumOf({ migration });
|
|
7900
8538
|
if (stored.checksum === actual)
|
|
7901
8539
|
return;
|
|
7902
|
-
throw new
|
|
8540
|
+
throw new VibeError35({
|
|
7903
8541
|
code: "VIBE_MIGRATION",
|
|
7904
8542
|
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`,
|
|
7905
8543
|
meta: {
|
|
@@ -7919,7 +8557,7 @@ function assertDistinctMigrationNames(params) {
|
|
|
7919
8557
|
const names = new Set;
|
|
7920
8558
|
for (const migration of params.migrations) {
|
|
7921
8559
|
if (names.has(migration.name))
|
|
7922
|
-
throw new
|
|
8560
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Duplicate migration file identity "${migration.name}"`, meta: { reason: "history-duplicate-file", migration: migration.name } });
|
|
7923
8561
|
names.add(migration.name);
|
|
7924
8562
|
}
|
|
7925
8563
|
}
|
|
@@ -7927,15 +8565,15 @@ function assertCompleteHistory(params) {
|
|
|
7927
8565
|
const byName = new Map(params.migrations.map((migration) => [migration.name, migration]));
|
|
7928
8566
|
for (const name of params.recorded.keys()) {
|
|
7929
8567
|
if (!byName.has(name))
|
|
7930
|
-
throw new
|
|
8568
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Recorded migration "${name}" has no file in the complete chain`, meta: { reason: "history-unknown-file", migration: name } });
|
|
7931
8569
|
}
|
|
7932
8570
|
let index = 0;
|
|
7933
8571
|
for (const [name, stored] of params.recorded) {
|
|
7934
8572
|
const migration = params.migrations[index];
|
|
7935
8573
|
if (migration?.name !== name)
|
|
7936
|
-
throw new
|
|
8574
|
+
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 } });
|
|
7937
8575
|
if (stored.statementIndex >= 0 && !isCompleteRecord({ dialect: params.dialect, migration, stored }) && index !== params.recorded.size - 1)
|
|
7938
|
-
throw new
|
|
8576
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Partial migration "${name}" precedes later recorded work`, meta: { reason: "history-partial-not-last", migration: name } });
|
|
7939
8577
|
index++;
|
|
7940
8578
|
}
|
|
7941
8579
|
}
|
|
@@ -7944,16 +8582,16 @@ async function assertApplyHistory(params) {
|
|
|
7944
8582
|
const byName = new Map(migrations.map((migration) => [migration.name, migration]));
|
|
7945
8583
|
for (const [name, stored] of recorded) {
|
|
7946
8584
|
if (stored.statementIndex < 0) {
|
|
7947
|
-
throw new
|
|
8585
|
+
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 } });
|
|
7948
8586
|
}
|
|
7949
8587
|
const migration = byName.get(name);
|
|
7950
8588
|
if (migration === undefined) {
|
|
7951
8589
|
if (stored.error !== undefined)
|
|
7952
|
-
throw new
|
|
8590
|
+
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 } });
|
|
7953
8591
|
continue;
|
|
7954
8592
|
}
|
|
7955
8593
|
if (isCompleteRecord({ dialect, migration, stored }) && stored.error !== undefined)
|
|
7956
|
-
throw new
|
|
8594
|
+
throw new VibeError35({ code: "VIBE_MIGRATION", message: `Migration "${name}" has an unresolved journal error`, meta: { reason: "history-dirty", migration: name } });
|
|
7957
8595
|
if (isCompleteRecord({ dialect, migration, stored }))
|
|
7958
8596
|
assertChecksumMatches({ migration, stored: stored.checksum });
|
|
7959
8597
|
else
|
|
@@ -7978,10 +8616,10 @@ async function assertApplyHistory(params) {
|
|
|
7978
8616
|
}
|
|
7979
8617
|
}
|
|
7980
8618
|
function q5(dialect, name) {
|
|
7981
|
-
return
|
|
8619
|
+
return getSqlDialect9({ dialect }).quoteIdent({ name });
|
|
7982
8620
|
}
|
|
7983
8621
|
function ph(dialect, index) {
|
|
7984
|
-
return
|
|
8622
|
+
return getSqlDialect9({ dialect }).placeholder({ index });
|
|
7985
8623
|
}
|
|
7986
8624
|
|
|
7987
8625
|
// packages/migrate/src/fs.ts
|
|
@@ -7989,7 +8627,7 @@ function absent(error) {
|
|
|
7989
8627
|
return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
7990
8628
|
}
|
|
7991
8629
|
function failure(params) {
|
|
7992
|
-
return new
|
|
8630
|
+
return new VibeError36({ code: "VIBE_MIGRATION", message: `Cannot read migration file ${params.path}`, meta: { reason: "migration-file-unreadable", path: params.path }, cause: params.cause });
|
|
7993
8631
|
}
|
|
7994
8632
|
async function optionalFile(params) {
|
|
7995
8633
|
return readFile(params.path, "utf-8").catch((cause) => {
|
|
@@ -8003,13 +8641,13 @@ function rlsMetadata(params) {
|
|
|
8003
8641
|
const metadata = JSON.parse(params.text);
|
|
8004
8642
|
const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
8005
8643
|
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 ?? [] })) {
|
|
8006
|
-
throw new
|
|
8644
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: "Invalid native RLS metadata" });
|
|
8007
8645
|
}
|
|
8008
8646
|
assertValidSchemaIR4({ schema: metadata.rls.from });
|
|
8009
8647
|
assertValidSchemaIR4({ schema: metadata.rls.to });
|
|
8010
8648
|
return { ...params.migration, rls: metadata.rls, destructive: metadata.destructive, securityChanges: metadata.securityChanges, downSecurityChanges: metadata.downSecurityChanges };
|
|
8011
8649
|
}).catch((cause) => {
|
|
8012
|
-
throw new
|
|
8650
|
+
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 });
|
|
8013
8651
|
});
|
|
8014
8652
|
}
|
|
8015
8653
|
async function loadMigrationFiles(params) {
|
|
@@ -8042,13 +8680,17 @@ async function loadMigrationFiles(params) {
|
|
|
8042
8680
|
const desired = await optionalFile({ path: join(directory, DESIRED_STATE_METADATA.filename) });
|
|
8043
8681
|
if (desired !== undefined) {
|
|
8044
8682
|
const pending = await Promise.resolve().then(() => isUnfinalizedDesiredState({ text: desired })).catch((cause) => {
|
|
8045
|
-
throw new
|
|
8683
|
+
throw new VibeError36({ code: "VIBE_MIGRATION", message: `Malformed desired state for ${name}`, meta: { reason: "desired-state-invalid", migration: name }, cause });
|
|
8046
8684
|
});
|
|
8047
8685
|
migration = pending ? { ...migration, desiredStatePending: true } : { ...migration, desiredState: parseDesiredStateSidecar({ text: desired, migration: name }) };
|
|
8048
8686
|
}
|
|
8049
8687
|
const origin = await optionalFile({ path: join(directory, MODULE_ORIGIN_METADATA.filename) });
|
|
8050
8688
|
if (origin !== undefined)
|
|
8051
8689
|
migration = restoreModuleMigration({ migration, origin: parseModuleOrigin({ text: origin }) });
|
|
8690
|
+
const adoption = await optionalFile({ path: join(directory, MODULE_ADOPTION_METADATA.filename) });
|
|
8691
|
+
if (adoption !== undefined)
|
|
8692
|
+
migration = { ...migration, moduleAdoption: parseModuleAdoption({ text: adoption }) };
|
|
8693
|
+
assertModuleAdoption({ migration });
|
|
8052
8694
|
migrations.push(migration);
|
|
8053
8695
|
}
|
|
8054
8696
|
return migrations;
|
|
@@ -8059,4 +8701,4 @@ export {
|
|
|
8059
8701
|
splitSqlStatements
|
|
8060
8702
|
};
|
|
8061
8703
|
|
|
8062
|
-
//# debugId=
|
|
8704
|
+
//# debugId=91A1E2329350564C64756E2164756E21
|