@smartive/graphql-magic 23.7.0-next.5 → 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 +152 -291
- package/dist/cjs/index.cjs +153 -294
- 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 -18
- package/dist/esm/migrations/generate.js +150 -286
- 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 +186 -351
- 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
|
-
* constraint
|
|
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 -> { normalized, raw } */
|
|
1107
|
-
existingExcludeConstraints = {};
|
|
1108
|
-
/** table name -> constraint name -> { normalized, raw } */
|
|
1109
|
-
existingConstraintTriggers = {};
|
|
1110
1087
|
uuidUsed;
|
|
1111
1088
|
nowUsed;
|
|
1112
1089
|
needsMigration = false;
|
|
@@ -1134,56 +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, {
|
|
1150
|
-
normalized: this.normalizeExcludeDef(row.constraint_def),
|
|
1151
|
-
raw: row.constraint_def
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
const triggerResult = await schema.knex.raw(
|
|
1155
|
-
`SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_triggerdef(t.oid) as trigger_def
|
|
1156
|
-
FROM pg_constraint c
|
|
1157
|
-
JOIN pg_trigger t ON t.tgconstraint = c.oid
|
|
1158
|
-
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
1159
|
-
WHERE n.nspname = 'public' AND c.contype = 't'`
|
|
1160
|
-
);
|
|
1161
|
-
const triggerRows = "rows" in triggerResult && Array.isArray(triggerResult.rows) ? triggerResult.rows : [];
|
|
1162
|
-
for (const row of triggerRows) {
|
|
1163
|
-
const tableName = row.table_name.split(".").pop()?.replace(/^"|"$/g, "") ?? row.table_name;
|
|
1164
|
-
if (!this.existingConstraintTriggers[tableName]) {
|
|
1165
|
-
this.existingConstraintTriggers[tableName] = /* @__PURE__ */ new Map();
|
|
1166
|
-
}
|
|
1167
|
-
this.existingConstraintTriggers[tableName].set(row.constraint_name, {
|
|
1168
|
-
normalized: this.normalizeTriggerDef(row.trigger_def),
|
|
1169
|
-
raw: row.trigger_def
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
1114
|
const up = [];
|
|
1173
1115
|
const down = [];
|
|
1174
|
-
const wantsBtreeGist = models.entities.some(
|
|
1175
|
-
(model) => model.constraints?.some((c) => c.kind === "exclude" && c.elements.some((el) => "column" in el && el.operator === "="))
|
|
1176
|
-
);
|
|
1177
|
-
if (wantsBtreeGist) {
|
|
1178
|
-
const extResult = await schema.knex("pg_extension").where("extname", "btree_gist").select("oid").first();
|
|
1179
|
-
const btreeGistInstalled = !!extResult;
|
|
1180
|
-
if (!btreeGistInstalled) {
|
|
1181
|
-
up.unshift(() => {
|
|
1182
|
-
this.writer.writeLine(`await knex.raw('CREATE EXTENSION IF NOT EXISTS btree_gist');`);
|
|
1183
|
-
this.writer.blankLine();
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
1116
|
this.createEnums(
|
|
1188
1117
|
this.models.enums.filter((enm2) => !enums.includes((0, import_lowerFirst.default)(enm2.name))),
|
|
1189
1118
|
up,
|
|
@@ -1262,22 +1191,10 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1262
1191
|
if (entry.kind === "check") {
|
|
1263
1192
|
validateCheckConstraint(model, entry);
|
|
1264
1193
|
const table = model.name;
|
|
1265
|
-
const constraintName = this.
|
|
1266
|
-
|
|
1267
|
-
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
1268
|
-
});
|
|
1269
|
-
} else if (entry.kind === "exclude") {
|
|
1270
|
-
validateExcludeConstraint(model, entry);
|
|
1271
|
-
const table = model.name;
|
|
1272
|
-
const constraintName = this.getConstraintName(model, entry, i);
|
|
1273
|
-
up.push(() => {
|
|
1274
|
-
this.addExcludeConstraint(table, constraintName, entry);
|
|
1275
|
-
});
|
|
1276
|
-
} else if (entry.kind === "constraint_trigger") {
|
|
1277
|
-
const table = model.name;
|
|
1278
|
-
const constraintName = this.getConstraintName(model, entry, i);
|
|
1194
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
1195
|
+
const expression = entry.expression;
|
|
1279
1196
|
up.push(() => {
|
|
1280
|
-
this.
|
|
1197
|
+
this.addCheckConstraint(table, constraintName, expression);
|
|
1281
1198
|
});
|
|
1282
1199
|
}
|
|
1283
1200
|
}
|
|
@@ -1317,81 +1234,36 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1317
1234
|
);
|
|
1318
1235
|
this.updateFields(model, existingFields, up, down);
|
|
1319
1236
|
if (model.constraints?.length) {
|
|
1320
|
-
const existingCheckMap = this.existingCheckConstraints[model.name];
|
|
1321
|
-
const existingExcludeMap = this.existingExcludeConstraints[model.name];
|
|
1322
|
-
const existingTriggerMap = this.existingConstraintTriggers[model.name];
|
|
1323
1237
|
for (let i = 0; i < model.constraints.length; i++) {
|
|
1324
1238
|
const entry = model.constraints[i];
|
|
1239
|
+
if (entry.kind !== "check") {
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
validateCheckConstraint(model, entry);
|
|
1325
1243
|
const table = model.name;
|
|
1326
|
-
const constraintName = this.
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
validateExcludeConstraint(model, entry);
|
|
1350
|
-
const newDef = this.normalizeExcludeDef(this.buildExcludeDef(entry));
|
|
1351
|
-
const existing = existingExcludeMap?.get(constraintName);
|
|
1352
|
-
if (existing === void 0) {
|
|
1353
|
-
up.push(() => {
|
|
1354
|
-
this.addExcludeConstraint(table, constraintName, entry);
|
|
1355
|
-
});
|
|
1356
|
-
down.push(() => {
|
|
1357
|
-
this.dropExcludeConstraint(table, constraintName);
|
|
1358
|
-
});
|
|
1359
|
-
} else if (existing.normalized !== newDef) {
|
|
1360
|
-
up.push(() => {
|
|
1361
|
-
this.dropExcludeConstraint(table, constraintName);
|
|
1362
|
-
this.addExcludeConstraint(table, constraintName, entry);
|
|
1363
|
-
});
|
|
1364
|
-
down.push(() => {
|
|
1365
|
-
this.dropExcludeConstraint(table, constraintName);
|
|
1366
|
-
const escaped = this.escapeExpressionForRaw(existing.raw);
|
|
1367
|
-
this.writer.writeLine(
|
|
1368
|
-
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`
|
|
1369
|
-
);
|
|
1370
|
-
this.writer.blankLine();
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
} else if (entry.kind === "constraint_trigger") {
|
|
1374
|
-
const newDef = this.normalizeTriggerDef(this.buildConstraintTriggerDef(table, constraintName, entry));
|
|
1375
|
-
const existing = existingTriggerMap?.get(constraintName);
|
|
1376
|
-
if (existing === void 0) {
|
|
1377
|
-
up.push(() => {
|
|
1378
|
-
this.addConstraintTrigger(table, constraintName, entry);
|
|
1379
|
-
});
|
|
1380
|
-
down.push(() => {
|
|
1381
|
-
this.dropConstraintTrigger(table, constraintName);
|
|
1382
|
-
});
|
|
1383
|
-
} else if (existing.normalized !== newDef) {
|
|
1384
|
-
up.push(() => {
|
|
1385
|
-
this.dropConstraintTrigger(table, constraintName);
|
|
1386
|
-
this.addConstraintTrigger(table, constraintName, entry);
|
|
1387
|
-
});
|
|
1388
|
-
down.push(() => {
|
|
1389
|
-
this.dropConstraintTrigger(table, constraintName);
|
|
1390
|
-
const escaped = existing.raw.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1391
|
-
this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
|
|
1392
|
-
this.writer.blankLine();
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
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
|
+
});
|
|
1395
1267
|
}
|
|
1396
1268
|
}
|
|
1397
1269
|
}
|
|
@@ -1766,122 +1638,148 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1766
1638
|
renameColumn(from, to) {
|
|
1767
1639
|
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
1768
1640
|
}
|
|
1769
|
-
|
|
1641
|
+
getCheckConstraintName(model, entry, index) {
|
|
1770
1642
|
return `${model.name}_${entry.name}_${entry.kind}_${index}`;
|
|
1771
1643
|
}
|
|
1772
|
-
|
|
1773
|
-
"
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
"in",
|
|
1777
|
-
"is",
|
|
1778
|
-
"null",
|
|
1779
|
-
"true",
|
|
1780
|
-
"false",
|
|
1781
|
-
"between",
|
|
1782
|
-
"like",
|
|
1783
|
-
"exists",
|
|
1784
|
-
"all",
|
|
1785
|
-
"any",
|
|
1786
|
-
"asc",
|
|
1787
|
-
"desc",
|
|
1788
|
-
"with",
|
|
1789
|
-
"using",
|
|
1790
|
-
"as",
|
|
1791
|
-
"on",
|
|
1792
|
-
"infinity",
|
|
1793
|
-
"extract",
|
|
1794
|
-
"current_date",
|
|
1795
|
-
"current_timestamp"
|
|
1796
|
-
]);
|
|
1797
|
-
static LITERAL_PLACEHOLDER = "\uE000";
|
|
1798
|
-
normalizeSqlIdentifiers(s) {
|
|
1799
|
-
const literals = [];
|
|
1800
|
-
let result = s.replace(/'([^']|'')*'/g, (lit) => {
|
|
1801
|
-
literals.push(lit);
|
|
1802
|
-
return `${_MigrationGenerator.LITERAL_PLACEHOLDER}${literals.length - 1}${_MigrationGenerator.LITERAL_PLACEHOLDER}`;
|
|
1803
|
-
});
|
|
1804
|
-
result = result.replace(/"([^"]*)"/g, (_, ident) => `"${ident.toLowerCase()}"`);
|
|
1805
|
-
result = result.replace(
|
|
1806
|
-
/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g,
|
|
1807
|
-
(match) => _MigrationGenerator.SQL_KEYWORDS.has(match.toLowerCase()) ? match : `"${match.toLowerCase()}"`
|
|
1808
|
-
);
|
|
1809
|
-
for (let i = 0; i < literals.length; i++) {
|
|
1810
|
-
result = result.replace(
|
|
1811
|
-
new RegExp(`${_MigrationGenerator.LITERAL_PLACEHOLDER}${i}${_MigrationGenerator.LITERAL_PLACEHOLDER}`, "g"),
|
|
1812
|
-
literals[i]
|
|
1813
|
-
);
|
|
1644
|
+
normalizeCheckExpression(expr) {
|
|
1645
|
+
let normalized = expr.replace(/\s+/g, " ").trim();
|
|
1646
|
+
while (this.isWrappedByOuterParentheses(normalized)) {
|
|
1647
|
+
normalized = normalized.slice(1, -1).trim();
|
|
1814
1648
|
}
|
|
1815
|
-
return
|
|
1649
|
+
return normalized;
|
|
1816
1650
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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;
|
|
1826
1664
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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;
|
|
1830
1680
|
}
|
|
1831
1681
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
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;
|
|
1834
1705
|
}
|
|
1835
|
-
|
|
1706
|
+
return { constraintName, expression };
|
|
1836
1707
|
}
|
|
1837
|
-
return
|
|
1708
|
+
return null;
|
|
1838
1709
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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}`);
|
|
1849
1752
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1753
|
+
return this.normalizeCheckExpression(this.extractCheckExpressionFromDefinition(definition));
|
|
1754
|
+
} finally {
|
|
1755
|
+
try {
|
|
1756
|
+
await trx.rollback();
|
|
1757
|
+
} catch {
|
|
1854
1758
|
}
|
|
1855
1759
|
}
|
|
1856
|
-
parts.push(s.slice(start).trim());
|
|
1857
|
-
return parts.filter(Boolean);
|
|
1858
1760
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
return this.
|
|
1872
|
-
}
|
|
1873
|
-
normalizeTriggerDef(def) {
|
|
1874
|
-
return def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\bON\s+[a-zA-Z_][a-zA-Z0-9_]*\./gi, "ON ").trim();
|
|
1761
|
+
extractCheckExpressionFromDefinition(definition) {
|
|
1762
|
+
const trimmed = definition.trim();
|
|
1763
|
+
const match = trimmed.match(/^CHECK\s*\(([\s\S]*)\)$/i);
|
|
1764
|
+
if (!match) {
|
|
1765
|
+
return trimmed;
|
|
1766
|
+
}
|
|
1767
|
+
return match[1];
|
|
1768
|
+
}
|
|
1769
|
+
quoteIdentifier(identifier) {
|
|
1770
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1771
|
+
}
|
|
1772
|
+
quoteQualifiedIdentifier(identifier) {
|
|
1773
|
+
return identifier.split(".").map((part) => this.quoteIdentifier(part)).join(".");
|
|
1875
1774
|
}
|
|
1876
1775
|
/** Escape expression for embedding inside a template literal in generated code */
|
|
1877
1776
|
escapeExpressionForRaw(expr) {
|
|
1878
1777
|
return expr.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1879
1778
|
}
|
|
1880
|
-
addCheckConstraint(table, constraintName, expression
|
|
1779
|
+
addCheckConstraint(table, constraintName, expression) {
|
|
1881
1780
|
const escaped = this.escapeExpressionForRaw(expression);
|
|
1882
|
-
const deferrableClause = deferrable ? ` DEFERRABLE ${deferrable}` : "";
|
|
1883
1781
|
this.writer.writeLine(
|
|
1884
|
-
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})
|
|
1782
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`
|
|
1885
1783
|
);
|
|
1886
1784
|
this.writer.blankLine();
|
|
1887
1785
|
}
|
|
@@ -1889,43 +1787,6 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1889
1787
|
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1890
1788
|
this.writer.blankLine();
|
|
1891
1789
|
}
|
|
1892
|
-
buildExcludeDef(entry) {
|
|
1893
|
-
const elementsStr = entry.elements.map((el) => "column" in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`).join(", ");
|
|
1894
|
-
const whereClause = entry.where ? ` WHERE (${entry.where})` : "";
|
|
1895
|
-
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
1896
|
-
return `EXCLUDE USING ${entry.using} (${elementsStr})${whereClause}${deferrableClause}`;
|
|
1897
|
-
}
|
|
1898
|
-
addExcludeConstraint(table, constraintName, entry) {
|
|
1899
|
-
const def = this.buildExcludeDef(entry);
|
|
1900
|
-
const escaped = this.escapeExpressionForRaw(def);
|
|
1901
|
-
this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
|
|
1902
|
-
this.writer.blankLine();
|
|
1903
|
-
}
|
|
1904
|
-
dropExcludeConstraint(table, constraintName) {
|
|
1905
|
-
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1906
|
-
this.writer.blankLine();
|
|
1907
|
-
}
|
|
1908
|
-
buildConstraintTriggerDef(table, constraintName, entry) {
|
|
1909
|
-
const eventsStr = entry.events.join(" OR ");
|
|
1910
|
-
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
1911
|
-
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
1912
|
-
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
1913
|
-
return `CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}`;
|
|
1914
|
-
}
|
|
1915
|
-
addConstraintTrigger(table, constraintName, entry) {
|
|
1916
|
-
const eventsStr = entry.events.join(" OR ");
|
|
1917
|
-
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
1918
|
-
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
1919
|
-
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
1920
|
-
this.writer.writeLine(
|
|
1921
|
-
`await knex.raw(\`CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}\`);`
|
|
1922
|
-
);
|
|
1923
|
-
this.writer.blankLine();
|
|
1924
|
-
}
|
|
1925
|
-
dropConstraintTrigger(table, constraintName) {
|
|
1926
|
-
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1927
|
-
this.writer.blankLine();
|
|
1928
|
-
}
|
|
1929
1790
|
value(value2) {
|
|
1930
1791
|
if (typeof value2 === "string") {
|
|
1931
1792
|
return `'${value2}'`;
|