@smartive/graphql-magic 23.7.0-next.4 → 23.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -3
- package/dist/bin/gqm.cjs +155 -256
- package/dist/cjs/index.cjs +155 -258
- package/dist/esm/migrations/generate-functions.js +0 -2
- package/dist/esm/migrations/generate-functions.js.map +1 -1
- package/dist/esm/migrations/generate.d.ts +8 -15
- package/dist/esm/migrations/generate.js +156 -253
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/migrations/update-functions.js +0 -2
- package/dist/esm/migrations/update-functions.js.map +1 -1
- package/dist/esm/models/model-definitions.d.ts +2 -27
- package/dist/esm/models/models.d.ts +5 -1
- package/dist/esm/models/models.js +1 -4
- package/dist/esm/models/models.js.map +1 -1
- package/dist/esm/models/utils.d.ts +0 -10
- package/dist/esm/models/utils.js +0 -11
- package/dist/esm/models/utils.js.map +1 -1
- package/docs/docs/2-models.md +4 -18
- package/docs/docs/5-migrations.md +7 -9
- package/package.json +1 -1
- package/src/bin/gqm/parse-knexfile.ts +0 -1
- package/src/bin/gqm/settings.ts +0 -4
- package/src/migrations/generate-functions.ts +0 -2
- package/src/migrations/generate.ts +191 -310
- package/src/migrations/update-functions.ts +0 -2
- package/src/models/model-definitions.ts +1 -20
- package/src/models/models.ts +1 -4
- package/src/models/utils.ts +0 -20
- package/tests/unit/constraints.spec.ts +2 -98
- package/tests/unit/migration-constraints.spec.ts +300 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
## [23.7.0
|
|
1
|
+
## [23.7.0](https://github.com/smartive/graphql-magic/compare/v23.6.1...v23.7.0) (2026-03-04)
|
|
2
2
|
|
|
3
|
-
###
|
|
3
|
+
### Features
|
|
4
4
|
|
|
5
|
-
*
|
|
5
|
+
* Enhance check constraint handling in migration generator ([#430](https://github.com/smartive/graphql-magic/issues/430)) ([b23a885](https://github.com/smartive/graphql-magic/commit/b23a8858ec46182b90cdb220e444c782f46201fc))
|
package/dist/bin/gqm.cjs
CHANGED
|
@@ -389,8 +389,6 @@ var EntityModel = class extends Model {
|
|
|
389
389
|
for (const constraint of this.constraints) {
|
|
390
390
|
if (constraint.kind === "check") {
|
|
391
391
|
validateCheckConstraint(this, constraint);
|
|
392
|
-
} else if (constraint.kind === "exclude") {
|
|
393
|
-
validateExcludeConstraint(this, constraint);
|
|
394
392
|
}
|
|
395
393
|
}
|
|
396
394
|
}
|
|
@@ -646,19 +644,6 @@ var validateCheckConstraint = (model, constraint) => {
|
|
|
646
644
|
}
|
|
647
645
|
}
|
|
648
646
|
};
|
|
649
|
-
var validateExcludeConstraint = (model, constraint) => {
|
|
650
|
-
const validColumnNames = new Set(model.fields.map((f) => getColumnName(f)));
|
|
651
|
-
for (const el of constraint.elements) {
|
|
652
|
-
if ("column" in el) {
|
|
653
|
-
if (!validColumnNames.has(el.column)) {
|
|
654
|
-
const validList = [...validColumnNames].sort().join(", ");
|
|
655
|
-
throw new Error(
|
|
656
|
-
`Exclude constraint "${constraint.name}" references column "${el.column}" which does not exist on model ${model.name}. Valid columns: ${validList}`
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
647
|
|
|
663
648
|
// src/db/generate.ts
|
|
664
649
|
var import_code_block_writer = __toESM(require("code-block-writer"), 1);
|
|
@@ -807,7 +792,6 @@ var generateFunctionsFromDatabase = async (knex2) => {
|
|
|
807
792
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
808
793
|
WHERE n.nspname = 'public'
|
|
809
794
|
AND NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid)
|
|
810
|
-
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
|
|
811
795
|
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
812
796
|
`);
|
|
813
797
|
const aggregateFunctions = await knex2.raw(`
|
|
@@ -822,7 +806,6 @@ var generateFunctionsFromDatabase = async (knex2) => {
|
|
|
822
806
|
JOIN pg_aggregate a ON p.oid = a.aggfnoid
|
|
823
807
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
824
808
|
WHERE n.nspname = 'public'
|
|
825
|
-
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
|
|
826
809
|
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
827
810
|
`);
|
|
828
811
|
const functions = [];
|
|
@@ -927,7 +910,6 @@ var getDatabaseFunctions = async (knex2) => {
|
|
|
927
910
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
928
911
|
WHERE n.nspname = 'public'
|
|
929
912
|
AND NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid)
|
|
930
|
-
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
|
|
931
913
|
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
932
914
|
`);
|
|
933
915
|
const aggregateFunctions = await knex2.raw(`
|
|
@@ -943,7 +925,6 @@ var getDatabaseFunctions = async (knex2) => {
|
|
|
943
925
|
JOIN pg_aggregate a ON p.oid = a.aggfnoid
|
|
944
926
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
945
927
|
WHERE n.nspname = 'public'
|
|
946
|
-
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
|
|
947
928
|
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
948
929
|
`);
|
|
949
930
|
const result = [];
|
|
@@ -1087,7 +1068,7 @@ Summary: ${updatedCount} updated, ${skippedCount} skipped`);
|
|
|
1087
1068
|
};
|
|
1088
1069
|
|
|
1089
1070
|
// src/migrations/generate.ts
|
|
1090
|
-
var MigrationGenerator = class
|
|
1071
|
+
var MigrationGenerator = class {
|
|
1091
1072
|
constructor(knex2, models, parsedFunctions) {
|
|
1092
1073
|
this.models = models;
|
|
1093
1074
|
this.parsedFunctions = parsedFunctions;
|
|
@@ -1103,10 +1084,6 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1103
1084
|
columns = {};
|
|
1104
1085
|
/** table name -> constraint name -> check clause expression */
|
|
1105
1086
|
existingCheckConstraints = {};
|
|
1106
|
-
/** table name -> constraint name -> exclude definition (normalized) */
|
|
1107
|
-
existingExcludeConstraints = {};
|
|
1108
|
-
/** table name -> constraint name -> trigger definition (normalized) */
|
|
1109
|
-
existingConstraintTriggers = {};
|
|
1110
1087
|
uuidUsed;
|
|
1111
1088
|
nowUsed;
|
|
1112
1089
|
needsMigration = false;
|
|
@@ -1134,50 +1111,8 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1134
1111
|
}
|
|
1135
1112
|
this.existingCheckConstraints[tableName].set(row.constraint_name, row.check_clause);
|
|
1136
1113
|
}
|
|
1137
|
-
const excludeResult = await schema.knex.raw(
|
|
1138
|
-
`SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_constraintdef(c.oid) as constraint_def
|
|
1139
|
-
FROM pg_constraint c
|
|
1140
|
-
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
1141
|
-
WHERE n.nspname = 'public' AND c.contype = 'x'`
|
|
1142
|
-
);
|
|
1143
|
-
const excludeRows = "rows" in excludeResult && Array.isArray(excludeResult.rows) ? excludeResult.rows : [];
|
|
1144
|
-
for (const row of excludeRows) {
|
|
1145
|
-
const tableName = row.table_name.split(".").pop()?.replace(/^"|"$/g, "") ?? row.table_name;
|
|
1146
|
-
if (!this.existingExcludeConstraints[tableName]) {
|
|
1147
|
-
this.existingExcludeConstraints[tableName] = /* @__PURE__ */ new Map();
|
|
1148
|
-
}
|
|
1149
|
-
this.existingExcludeConstraints[tableName].set(row.constraint_name, this.normalizeExcludeDef(row.constraint_def));
|
|
1150
|
-
}
|
|
1151
|
-
const triggerResult = await schema.knex.raw(
|
|
1152
|
-
`SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_triggerdef(t.oid) as trigger_def
|
|
1153
|
-
FROM pg_constraint c
|
|
1154
|
-
JOIN pg_trigger t ON t.tgconstraint = c.oid
|
|
1155
|
-
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
1156
|
-
WHERE n.nspname = 'public' AND c.contype = 't'`
|
|
1157
|
-
);
|
|
1158
|
-
const triggerRows = "rows" in triggerResult && Array.isArray(triggerResult.rows) ? triggerResult.rows : [];
|
|
1159
|
-
for (const row of triggerRows) {
|
|
1160
|
-
const tableName = row.table_name.split(".").pop()?.replace(/^"|"$/g, "") ?? row.table_name;
|
|
1161
|
-
if (!this.existingConstraintTriggers[tableName]) {
|
|
1162
|
-
this.existingConstraintTriggers[tableName] = /* @__PURE__ */ new Map();
|
|
1163
|
-
}
|
|
1164
|
-
this.existingConstraintTriggers[tableName].set(row.constraint_name, this.normalizeTriggerDef(row.trigger_def));
|
|
1165
|
-
}
|
|
1166
1114
|
const up = [];
|
|
1167
1115
|
const down = [];
|
|
1168
|
-
const wantsBtreeGist = models.entities.some(
|
|
1169
|
-
(model) => model.constraints?.some((c) => c.kind === "exclude" && c.elements.some((el) => "column" in el && el.operator === "="))
|
|
1170
|
-
);
|
|
1171
|
-
if (wantsBtreeGist) {
|
|
1172
|
-
const extResult = await schema.knex("pg_extension").where("extname", "btree_gist").select("oid").first();
|
|
1173
|
-
const btreeGistInstalled = !!extResult;
|
|
1174
|
-
if (!btreeGistInstalled) {
|
|
1175
|
-
up.unshift(() => {
|
|
1176
|
-
this.writer.writeLine(`await knex.raw('CREATE EXTENSION IF NOT EXISTS btree_gist');`);
|
|
1177
|
-
this.writer.blankLine();
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
1116
|
this.createEnums(
|
|
1182
1117
|
this.models.enums.filter((enm2) => !enums.includes((0, import_lowerFirst.default)(enm2.name))),
|
|
1183
1118
|
up,
|
|
@@ -1256,22 +1191,10 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1256
1191
|
if (entry.kind === "check") {
|
|
1257
1192
|
validateCheckConstraint(model, entry);
|
|
1258
1193
|
const table = model.name;
|
|
1259
|
-
const constraintName = this.
|
|
1260
|
-
|
|
1261
|
-
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
1262
|
-
});
|
|
1263
|
-
} else if (entry.kind === "exclude") {
|
|
1264
|
-
validateExcludeConstraint(model, entry);
|
|
1265
|
-
const table = model.name;
|
|
1266
|
-
const constraintName = this.getConstraintName(model, entry, i);
|
|
1194
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
1195
|
+
const expression = entry.expression;
|
|
1267
1196
|
up.push(() => {
|
|
1268
|
-
this.
|
|
1269
|
-
});
|
|
1270
|
-
} else if (entry.kind === "constraint_trigger") {
|
|
1271
|
-
const table = model.name;
|
|
1272
|
-
const constraintName = this.getConstraintName(model, entry, i);
|
|
1273
|
-
up.push(() => {
|
|
1274
|
-
this.addConstraintTrigger(table, constraintName, entry);
|
|
1197
|
+
this.addCheckConstraint(table, constraintName, expression);
|
|
1275
1198
|
});
|
|
1276
1199
|
}
|
|
1277
1200
|
}
|
|
@@ -1311,81 +1234,36 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1311
1234
|
);
|
|
1312
1235
|
this.updateFields(model, existingFields, up, down);
|
|
1313
1236
|
if (model.constraints?.length) {
|
|
1314
|
-
const existingCheckMap = this.existingCheckConstraints[model.name];
|
|
1315
|
-
const existingExcludeMap = this.existingExcludeConstraints[model.name];
|
|
1316
|
-
const existingTriggerMap = this.existingConstraintTriggers[model.name];
|
|
1317
1237
|
for (let i = 0; i < model.constraints.length; i++) {
|
|
1318
1238
|
const entry = model.constraints[i];
|
|
1239
|
+
if (entry.kind !== "check") {
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
validateCheckConstraint(model, entry);
|
|
1319
1243
|
const table = model.name;
|
|
1320
|
-
const constraintName = this.
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
validateExcludeConstraint(model, entry);
|
|
1344
|
-
const newDef = this.normalizeExcludeDef(this.buildExcludeDef(entry));
|
|
1345
|
-
const existingDef = existingExcludeMap?.get(constraintName);
|
|
1346
|
-
if (existingDef === void 0) {
|
|
1347
|
-
up.push(() => {
|
|
1348
|
-
this.addExcludeConstraint(table, constraintName, entry);
|
|
1349
|
-
});
|
|
1350
|
-
down.push(() => {
|
|
1351
|
-
this.dropExcludeConstraint(table, constraintName);
|
|
1352
|
-
});
|
|
1353
|
-
} else if (existingDef !== newDef) {
|
|
1354
|
-
up.push(() => {
|
|
1355
|
-
this.dropExcludeConstraint(table, constraintName);
|
|
1356
|
-
this.addExcludeConstraint(table, constraintName, entry);
|
|
1357
|
-
});
|
|
1358
|
-
down.push(() => {
|
|
1359
|
-
this.dropExcludeConstraint(table, constraintName);
|
|
1360
|
-
const escaped = this.escapeExpressionForRaw(existingDef);
|
|
1361
|
-
this.writer.writeLine(
|
|
1362
|
-
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`
|
|
1363
|
-
);
|
|
1364
|
-
this.writer.blankLine();
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1367
|
-
} else if (entry.kind === "constraint_trigger") {
|
|
1368
|
-
const newDef = this.normalizeTriggerDef(this.buildConstraintTriggerDef(table, constraintName, entry));
|
|
1369
|
-
const existingDef = existingTriggerMap?.get(constraintName);
|
|
1370
|
-
if (existingDef === void 0) {
|
|
1371
|
-
up.push(() => {
|
|
1372
|
-
this.addConstraintTrigger(table, constraintName, entry);
|
|
1373
|
-
});
|
|
1374
|
-
down.push(() => {
|
|
1375
|
-
this.dropConstraintTrigger(table, constraintName);
|
|
1376
|
-
});
|
|
1377
|
-
} else if (existingDef !== newDef) {
|
|
1378
|
-
up.push(() => {
|
|
1379
|
-
this.dropConstraintTrigger(table, constraintName);
|
|
1380
|
-
this.addConstraintTrigger(table, constraintName, entry);
|
|
1381
|
-
});
|
|
1382
|
-
down.push(() => {
|
|
1383
|
-
this.dropConstraintTrigger(table, constraintName);
|
|
1384
|
-
const escaped = existingDef.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1385
|
-
this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
|
|
1386
|
-
this.writer.blankLine();
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1244
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
1245
|
+
const existingConstraint = this.findExistingConstraint(table, entry, constraintName);
|
|
1246
|
+
if (!existingConstraint) {
|
|
1247
|
+
up.push(() => {
|
|
1248
|
+
this.addCheckConstraint(table, constraintName, entry.expression);
|
|
1249
|
+
});
|
|
1250
|
+
down.push(() => {
|
|
1251
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1252
|
+
});
|
|
1253
|
+
} else if (!await this.equalExpressions(
|
|
1254
|
+
table,
|
|
1255
|
+
existingConstraint.constraintName,
|
|
1256
|
+
existingConstraint.expression,
|
|
1257
|
+
entry.expression
|
|
1258
|
+
)) {
|
|
1259
|
+
up.push(() => {
|
|
1260
|
+
this.dropCheckConstraint(table, existingConstraint.constraintName);
|
|
1261
|
+
this.addCheckConstraint(table, constraintName, entry.expression);
|
|
1262
|
+
});
|
|
1263
|
+
down.push(() => {
|
|
1264
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1265
|
+
this.addCheckConstraint(table, existingConstraint.constraintName, existingConstraint.expression);
|
|
1266
|
+
});
|
|
1389
1267
|
}
|
|
1390
1268
|
}
|
|
1391
1269
|
}
|
|
@@ -1760,90 +1638,148 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1760
1638
|
renameColumn(from, to) {
|
|
1761
1639
|
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
1762
1640
|
}
|
|
1763
|
-
|
|
1641
|
+
getCheckConstraintName(model, entry, index) {
|
|
1764
1642
|
return `${model.name}_${entry.name}_${entry.kind}_${index}`;
|
|
1765
1643
|
}
|
|
1766
|
-
|
|
1767
|
-
"
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
"in",
|
|
1771
|
-
"is",
|
|
1772
|
-
"null",
|
|
1773
|
-
"true",
|
|
1774
|
-
"false",
|
|
1775
|
-
"between",
|
|
1776
|
-
"like",
|
|
1777
|
-
"exists",
|
|
1778
|
-
"all",
|
|
1779
|
-
"any",
|
|
1780
|
-
"asc",
|
|
1781
|
-
"desc",
|
|
1782
|
-
"with",
|
|
1783
|
-
"using",
|
|
1784
|
-
"as",
|
|
1785
|
-
"on",
|
|
1786
|
-
"infinity",
|
|
1787
|
-
"extract",
|
|
1788
|
-
"current_date",
|
|
1789
|
-
"current_timestamp"
|
|
1790
|
-
]);
|
|
1791
|
-
normalizeSqlIdentifiers(s) {
|
|
1792
|
-
const literals = [];
|
|
1793
|
-
let result = s.replace(/'([^']|'')*'/g, (lit) => {
|
|
1794
|
-
literals.push(lit);
|
|
1795
|
-
return `\0L${literals.length - 1}\0`;
|
|
1796
|
-
});
|
|
1797
|
-
result = result.replace(/"([^"]*)"/g, (_, ident) => `"${ident.toLowerCase()}"`);
|
|
1798
|
-
result = result.replace(
|
|
1799
|
-
/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g,
|
|
1800
|
-
(match) => _MigrationGenerator.SQL_KEYWORDS.has(match.toLowerCase()) ? match : `"${match.toLowerCase()}"`
|
|
1801
|
-
);
|
|
1802
|
-
for (let i = 0; i < literals.length; i++) {
|
|
1803
|
-
result = result.replace(new RegExp(`\0L${i}\0`, "g"), literals[i]);
|
|
1644
|
+
normalizeCheckExpression(expr) {
|
|
1645
|
+
let normalized = expr.replace(/\s+/g, " ").trim();
|
|
1646
|
+
while (this.isWrappedByOuterParentheses(normalized)) {
|
|
1647
|
+
normalized = normalized.slice(1, -1).trim();
|
|
1804
1648
|
}
|
|
1805
|
-
return
|
|
1649
|
+
return normalized;
|
|
1806
1650
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1651
|
+
isWrappedByOuterParentheses(expr) {
|
|
1652
|
+
if (!expr.startsWith("(") || !expr.endsWith(")")) {
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
let depth = 0;
|
|
1656
|
+
let inSingleQuote = false;
|
|
1657
|
+
for (let i = 0; i < expr.length; i++) {
|
|
1658
|
+
const char = expr[i];
|
|
1659
|
+
const next = expr[i + 1];
|
|
1660
|
+
if (char === "'") {
|
|
1661
|
+
if (inSingleQuote && next === "'") {
|
|
1662
|
+
i++;
|
|
1663
|
+
continue;
|
|
1817
1664
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1665
|
+
inSingleQuote = !inSingleQuote;
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
if (inSingleQuote) {
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
if (char === "(") {
|
|
1672
|
+
depth++;
|
|
1673
|
+
} else if (char === ")") {
|
|
1674
|
+
depth--;
|
|
1675
|
+
if (depth === 0 && i !== expr.length - 1) {
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
if (depth < 0) {
|
|
1679
|
+
return false;
|
|
1821
1680
|
}
|
|
1822
1681
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1682
|
+
}
|
|
1683
|
+
return depth === 0;
|
|
1684
|
+
}
|
|
1685
|
+
findExistingConstraint(table, entry, preferredConstraintName) {
|
|
1686
|
+
const existingMap = this.existingCheckConstraints[table];
|
|
1687
|
+
if (!existingMap) {
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
const preferredExpression = existingMap.get(preferredConstraintName);
|
|
1691
|
+
if (preferredExpression !== void 0) {
|
|
1692
|
+
return {
|
|
1693
|
+
constraintName: preferredConstraintName,
|
|
1694
|
+
expression: preferredExpression
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
const normalizedNewExpression = this.normalizeCheckExpression(entry.expression);
|
|
1698
|
+
const constraintPrefix = `${table}_${entry.name}_${entry.kind}_`;
|
|
1699
|
+
for (const [constraintName, expression] of existingMap.entries()) {
|
|
1700
|
+
if (!constraintName.startsWith(constraintPrefix)) {
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
if (this.normalizeCheckExpression(expression) !== normalizedNewExpression) {
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
return { constraintName, expression };
|
|
1707
|
+
}
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1710
|
+
async equalExpressions(table, constraintName, existingExpression, newExpression) {
|
|
1711
|
+
try {
|
|
1712
|
+
const [canonicalExisting, canonicalNew] = await Promise.all([
|
|
1713
|
+
this.canonicalizeCheckExpressionWithPostgres(table, existingExpression),
|
|
1714
|
+
this.canonicalizeCheckExpressionWithPostgres(table, newExpression)
|
|
1715
|
+
]);
|
|
1716
|
+
return canonicalExisting === canonicalNew;
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
console.warn(
|
|
1719
|
+
`Failed to canonicalize check constraint "${constraintName}" on table "${table}". Treating it as changed.`,
|
|
1720
|
+
error
|
|
1721
|
+
);
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
async canonicalizeCheckExpressionWithPostgres(table, expression) {
|
|
1726
|
+
const sourceTableIdentifier = table.split(".").map((part) => this.quoteIdentifier(part)).join(".");
|
|
1727
|
+
const uniqueSuffix = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1728
|
+
const tableSlug = table.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
1729
|
+
const tempTableName = `gqm_tmp_check_${tableSlug}_${uniqueSuffix}`;
|
|
1730
|
+
const tempTableIdentifier = this.quoteIdentifier(tempTableName);
|
|
1731
|
+
const constraintName = `gqm_tmp_check_${uniqueSuffix}`;
|
|
1732
|
+
const constraintIdentifier = this.quoteIdentifier(constraintName);
|
|
1733
|
+
const trx = await this.knex.transaction();
|
|
1734
|
+
try {
|
|
1735
|
+
await trx.raw(`CREATE TEMP TABLE ${tempTableIdentifier} (LIKE ${sourceTableIdentifier}) ON COMMIT DROP`);
|
|
1736
|
+
await trx.raw(`ALTER TABLE ${tempTableIdentifier} ADD CONSTRAINT ${constraintIdentifier} CHECK (${expression})`);
|
|
1737
|
+
const result = await trx.raw(
|
|
1738
|
+
`SELECT pg_get_constraintdef(c.oid, true) AS constraint_definition
|
|
1739
|
+
FROM pg_constraint c
|
|
1740
|
+
JOIN pg_class t
|
|
1741
|
+
ON t.oid = c.conrelid
|
|
1742
|
+
WHERE t.relname = ?
|
|
1743
|
+
AND c.conname = ?
|
|
1744
|
+
ORDER BY c.oid DESC
|
|
1745
|
+
LIMIT 1`,
|
|
1746
|
+
[tempTableName, constraintName]
|
|
1747
|
+
);
|
|
1748
|
+
const rows = "rows" in result && Array.isArray(result.rows) ? result.rows : [];
|
|
1749
|
+
const definition = rows[0]?.constraint_definition;
|
|
1750
|
+
if (!definition) {
|
|
1751
|
+
throw new Error(`Could not read canonical check definition for expression: ${expression}`);
|
|
1825
1752
|
}
|
|
1826
|
-
|
|
1753
|
+
return this.normalizeCheckExpression(this.extractCheckExpressionFromDefinition(definition));
|
|
1754
|
+
} finally {
|
|
1755
|
+
try {
|
|
1756
|
+
await trx.rollback();
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
extractCheckExpressionFromDefinition(definition) {
|
|
1762
|
+
const trimmed = definition.trim();
|
|
1763
|
+
const match = trimmed.match(/^CHECK\s*\(([\s\S]*)\)$/i);
|
|
1764
|
+
if (!match) {
|
|
1765
|
+
return trimmed;
|
|
1827
1766
|
}
|
|
1828
|
-
|
|
1829
|
-
return this.normalizeSqlIdentifiers(s);
|
|
1767
|
+
return match[1];
|
|
1830
1768
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
return this.normalizeSqlIdentifiers(s);
|
|
1769
|
+
quoteIdentifier(identifier) {
|
|
1770
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1834
1771
|
}
|
|
1835
|
-
|
|
1836
|
-
return
|
|
1772
|
+
quoteQualifiedIdentifier(identifier) {
|
|
1773
|
+
return identifier.split(".").map((part) => this.quoteIdentifier(part)).join(".");
|
|
1837
1774
|
}
|
|
1838
1775
|
/** Escape expression for embedding inside a template literal in generated code */
|
|
1839
1776
|
escapeExpressionForRaw(expr) {
|
|
1840
1777
|
return expr.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1841
1778
|
}
|
|
1842
|
-
addCheckConstraint(table, constraintName, expression
|
|
1779
|
+
addCheckConstraint(table, constraintName, expression) {
|
|
1843
1780
|
const escaped = this.escapeExpressionForRaw(expression);
|
|
1844
|
-
const deferrableClause = deferrable ? ` DEFERRABLE ${deferrable}` : "";
|
|
1845
1781
|
this.writer.writeLine(
|
|
1846
|
-
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})
|
|
1782
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`
|
|
1847
1783
|
);
|
|
1848
1784
|
this.writer.blankLine();
|
|
1849
1785
|
}
|
|
@@ -1851,43 +1787,6 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1851
1787
|
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1852
1788
|
this.writer.blankLine();
|
|
1853
1789
|
}
|
|
1854
|
-
buildExcludeDef(entry) {
|
|
1855
|
-
const elementsStr = entry.elements.map((el) => "column" in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`).join(", ");
|
|
1856
|
-
const whereClause = entry.where ? ` WHERE (${entry.where})` : "";
|
|
1857
|
-
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
1858
|
-
return `EXCLUDE USING ${entry.using} (${elementsStr})${whereClause}${deferrableClause}`;
|
|
1859
|
-
}
|
|
1860
|
-
addExcludeConstraint(table, constraintName, entry) {
|
|
1861
|
-
const def = this.buildExcludeDef(entry);
|
|
1862
|
-
const escaped = this.escapeExpressionForRaw(def);
|
|
1863
|
-
this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
|
|
1864
|
-
this.writer.blankLine();
|
|
1865
|
-
}
|
|
1866
|
-
dropExcludeConstraint(table, constraintName) {
|
|
1867
|
-
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1868
|
-
this.writer.blankLine();
|
|
1869
|
-
}
|
|
1870
|
-
buildConstraintTriggerDef(table, constraintName, entry) {
|
|
1871
|
-
const eventsStr = entry.events.join(" OR ");
|
|
1872
|
-
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
1873
|
-
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
1874
|
-
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
1875
|
-
return `CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}`;
|
|
1876
|
-
}
|
|
1877
|
-
addConstraintTrigger(table, constraintName, entry) {
|
|
1878
|
-
const eventsStr = entry.events.join(" OR ");
|
|
1879
|
-
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
1880
|
-
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
1881
|
-
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
1882
|
-
this.writer.writeLine(
|
|
1883
|
-
`await knex.raw(\`CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}\`);`
|
|
1884
|
-
);
|
|
1885
|
-
this.writer.blankLine();
|
|
1886
|
-
}
|
|
1887
|
-
dropConstraintTrigger(table, constraintName) {
|
|
1888
|
-
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1889
|
-
this.writer.blankLine();
|
|
1890
|
-
}
|
|
1891
1790
|
value(value2) {
|
|
1892
1791
|
if (typeof value2 === "string") {
|
|
1893
1792
|
return `'${value2}'`;
|