@smartive/graphql-magic 23.7.0 → 23.8.0-next.2
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 +307 -34
- package/dist/cjs/index.cjs +309 -34
- package/dist/esm/migrations/generate-functions.js +2 -0
- package/dist/esm/migrations/generate-functions.js.map +1 -1
- package/dist/esm/migrations/generate.d.ts +18 -1
- package/dist/esm/migrations/generate.js +292 -29
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/migrations/update-functions.js +2 -0
- package/dist/esm/migrations/update-functions.js.map +1 -1
- package/dist/esm/models/model-definitions.d.ts +27 -2
- package/dist/esm/models/models.d.ts +1 -5
- package/dist/esm/models/models.js +4 -1
- package/dist/esm/models/models.js.map +1 -1
- package/dist/esm/models/utils.d.ts +10 -0
- package/dist/esm/models/utils.js +11 -0
- package/dist/esm/models/utils.js.map +1 -1
- package/docs/docs/2-models.md +18 -4
- package/docs/docs/5-migrations.md +11 -5
- package/package.json +1 -1
- package/src/bin/gqm/parse-knexfile.ts +1 -0
- package/src/bin/gqm/settings.ts +4 -0
- package/src/migrations/generate-functions.ts +2 -0
- package/src/migrations/generate.ts +379 -35
- package/src/migrations/update-functions.ts +2 -0
- package/src/models/model-definitions.ts +20 -1
- package/src/models/models.ts +4 -1
- package/src/models/utils.ts +20 -0
- package/tests/unit/constraints.spec.ts +98 -2
- package/tests/unit/migration-constraints.spec.ts +47 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
## [23.
|
|
1
|
+
## [23.8.0-next.2](https://github.com/smartive/graphql-magic/compare/v23.8.0-next.1...v23.8.0-next.2) (2026-03-04)
|
|
2
2
|
|
|
3
|
-
###
|
|
3
|
+
### Bug Fixes
|
|
4
4
|
|
|
5
|
-
*
|
|
5
|
+
* normalize EXCLUDE defs so identical constraints produce empty migration ([0f42eb8](https://github.com/smartive/graphql-magic/commit/0f42eb8b9df1d1ce40a1905b6f52332165540943))
|
package/dist/bin/gqm.cjs
CHANGED
|
@@ -389,6 +389,8 @@ 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);
|
|
392
394
|
}
|
|
393
395
|
}
|
|
394
396
|
}
|
|
@@ -644,6 +646,19 @@ var validateCheckConstraint = (model, constraint) => {
|
|
|
644
646
|
}
|
|
645
647
|
}
|
|
646
648
|
};
|
|
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
|
+
};
|
|
647
662
|
|
|
648
663
|
// src/db/generate.ts
|
|
649
664
|
var import_code_block_writer = __toESM(require("code-block-writer"), 1);
|
|
@@ -792,6 +807,7 @@ var generateFunctionsFromDatabase = async (knex2) => {
|
|
|
792
807
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
793
808
|
WHERE n.nspname = 'public'
|
|
794
809
|
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')
|
|
795
811
|
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
796
812
|
`);
|
|
797
813
|
const aggregateFunctions = await knex2.raw(`
|
|
@@ -806,6 +822,7 @@ var generateFunctionsFromDatabase = async (knex2) => {
|
|
|
806
822
|
JOIN pg_aggregate a ON p.oid = a.aggfnoid
|
|
807
823
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
808
824
|
WHERE n.nspname = 'public'
|
|
825
|
+
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
|
|
809
826
|
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
810
827
|
`);
|
|
811
828
|
const functions = [];
|
|
@@ -910,6 +927,7 @@ var getDatabaseFunctions = async (knex2) => {
|
|
|
910
927
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
911
928
|
WHERE n.nspname = 'public'
|
|
912
929
|
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')
|
|
913
931
|
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
914
932
|
`);
|
|
915
933
|
const aggregateFunctions = await knex2.raw(`
|
|
@@ -925,6 +943,7 @@ var getDatabaseFunctions = async (knex2) => {
|
|
|
925
943
|
JOIN pg_aggregate a ON p.oid = a.aggfnoid
|
|
926
944
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
927
945
|
WHERE n.nspname = 'public'
|
|
946
|
+
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
|
|
928
947
|
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
929
948
|
`);
|
|
930
949
|
const result = [];
|
|
@@ -1068,7 +1087,7 @@ Summary: ${updatedCount} updated, ${skippedCount} skipped`);
|
|
|
1068
1087
|
};
|
|
1069
1088
|
|
|
1070
1089
|
// src/migrations/generate.ts
|
|
1071
|
-
var MigrationGenerator = class {
|
|
1090
|
+
var MigrationGenerator = class _MigrationGenerator {
|
|
1072
1091
|
constructor(knex2, models, parsedFunctions) {
|
|
1073
1092
|
this.models = models;
|
|
1074
1093
|
this.parsedFunctions = parsedFunctions;
|
|
@@ -1084,6 +1103,10 @@ var MigrationGenerator = class {
|
|
|
1084
1103
|
columns = {};
|
|
1085
1104
|
/** table name -> constraint name -> check clause expression */
|
|
1086
1105
|
existingCheckConstraints = {};
|
|
1106
|
+
/** table name -> constraint name -> { normalized, raw } */
|
|
1107
|
+
existingExcludeConstraints = {};
|
|
1108
|
+
/** table name -> constraint name -> { normalized, raw } */
|
|
1109
|
+
existingConstraintTriggers = {};
|
|
1087
1110
|
uuidUsed;
|
|
1088
1111
|
nowUsed;
|
|
1089
1112
|
needsMigration = false;
|
|
@@ -1111,8 +1134,56 @@ var MigrationGenerator = class {
|
|
|
1111
1134
|
}
|
|
1112
1135
|
this.existingCheckConstraints[tableName].set(row.constraint_name, row.check_clause);
|
|
1113
1136
|
}
|
|
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
|
+
}
|
|
1114
1172
|
const up = [];
|
|
1115
1173
|
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
|
+
}
|
|
1116
1187
|
this.createEnums(
|
|
1117
1188
|
this.models.enums.filter((enm2) => !enums.includes((0, import_lowerFirst.default)(enm2.name))),
|
|
1118
1189
|
up,
|
|
@@ -1191,10 +1262,22 @@ var MigrationGenerator = class {
|
|
|
1191
1262
|
if (entry.kind === "check") {
|
|
1192
1263
|
validateCheckConstraint(model, entry);
|
|
1193
1264
|
const table = model.name;
|
|
1194
|
-
const constraintName = this.
|
|
1195
|
-
|
|
1265
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
1266
|
+
up.push(() => {
|
|
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);
|
|
1196
1279
|
up.push(() => {
|
|
1197
|
-
this.
|
|
1280
|
+
this.addConstraintTrigger(table, constraintName, entry);
|
|
1198
1281
|
});
|
|
1199
1282
|
}
|
|
1200
1283
|
}
|
|
@@ -1234,36 +1317,84 @@ var MigrationGenerator = class {
|
|
|
1234
1317
|
);
|
|
1235
1318
|
this.updateFields(model, existingFields, up, down);
|
|
1236
1319
|
if (model.constraints?.length) {
|
|
1320
|
+
const existingExcludeMap = this.existingExcludeConstraints[model.name];
|
|
1321
|
+
const existingTriggerMap = this.existingConstraintTriggers[model.name];
|
|
1237
1322
|
for (let i = 0; i < model.constraints.length; i++) {
|
|
1238
1323
|
const entry = model.constraints[i];
|
|
1239
|
-
if (entry.kind !== "check") {
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
validateCheckConstraint(model, entry);
|
|
1243
1324
|
const table = model.name;
|
|
1244
|
-
const constraintName = this.
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1325
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
1326
|
+
if (entry.kind === "check") {
|
|
1327
|
+
validateCheckConstraint(model, entry);
|
|
1328
|
+
const existingConstraint = this.findExistingConstraint(table, entry, constraintName);
|
|
1329
|
+
if (!existingConstraint) {
|
|
1330
|
+
up.push(() => {
|
|
1331
|
+
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
1332
|
+
});
|
|
1333
|
+
down.push(() => {
|
|
1334
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1335
|
+
});
|
|
1336
|
+
} else if (!await this.equalExpressions(
|
|
1337
|
+
table,
|
|
1338
|
+
existingConstraint.constraintName,
|
|
1339
|
+
existingConstraint.expression,
|
|
1340
|
+
entry.expression
|
|
1341
|
+
)) {
|
|
1342
|
+
up.push(() => {
|
|
1343
|
+
this.dropCheckConstraint(table, existingConstraint.constraintName);
|
|
1344
|
+
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
1345
|
+
});
|
|
1346
|
+
down.push(() => {
|
|
1347
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1348
|
+
this.addCheckConstraint(table, existingConstraint.constraintName, existingConstraint.expression);
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
} else if (entry.kind === "exclude") {
|
|
1352
|
+
validateExcludeConstraint(model, entry);
|
|
1353
|
+
const newDef = this.normalizeExcludeDef(this.buildExcludeDef(entry));
|
|
1354
|
+
const existing = existingExcludeMap?.get(constraintName);
|
|
1355
|
+
if (existing === void 0) {
|
|
1356
|
+
up.push(() => {
|
|
1357
|
+
this.addExcludeConstraint(table, constraintName, entry);
|
|
1358
|
+
});
|
|
1359
|
+
down.push(() => {
|
|
1360
|
+
this.dropExcludeConstraint(table, constraintName);
|
|
1361
|
+
});
|
|
1362
|
+
} else if (existing.normalized !== newDef) {
|
|
1363
|
+
up.push(() => {
|
|
1364
|
+
this.dropExcludeConstraint(table, constraintName);
|
|
1365
|
+
this.addExcludeConstraint(table, constraintName, entry);
|
|
1366
|
+
});
|
|
1367
|
+
down.push(() => {
|
|
1368
|
+
this.dropExcludeConstraint(table, constraintName);
|
|
1369
|
+
const escaped = this.escapeExpressionForRaw(existing.raw);
|
|
1370
|
+
this.writer.writeLine(
|
|
1371
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`
|
|
1372
|
+
);
|
|
1373
|
+
this.writer.blankLine();
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
} else if (entry.kind === "constraint_trigger") {
|
|
1377
|
+
const newDef = this.normalizeTriggerDef(this.buildConstraintTriggerDef(table, constraintName, entry));
|
|
1378
|
+
const existing = existingTriggerMap?.get(constraintName);
|
|
1379
|
+
if (existing === void 0) {
|
|
1380
|
+
up.push(() => {
|
|
1381
|
+
this.addConstraintTrigger(table, constraintName, entry);
|
|
1382
|
+
});
|
|
1383
|
+
down.push(() => {
|
|
1384
|
+
this.dropConstraintTrigger(table, constraintName);
|
|
1385
|
+
});
|
|
1386
|
+
} else if (existing.normalized !== newDef) {
|
|
1387
|
+
up.push(() => {
|
|
1388
|
+
this.dropConstraintTrigger(table, constraintName);
|
|
1389
|
+
this.addConstraintTrigger(table, constraintName, entry);
|
|
1390
|
+
});
|
|
1391
|
+
down.push(() => {
|
|
1392
|
+
this.dropConstraintTrigger(table, constraintName);
|
|
1393
|
+
const escaped = existing.raw.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1394
|
+
this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
|
|
1395
|
+
this.writer.blankLine();
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1267
1398
|
}
|
|
1268
1399
|
}
|
|
1269
1400
|
}
|
|
@@ -1638,9 +1769,113 @@ var MigrationGenerator = class {
|
|
|
1638
1769
|
renameColumn(from, to) {
|
|
1639
1770
|
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
1640
1771
|
}
|
|
1641
|
-
|
|
1772
|
+
getConstraintName(model, entry, index) {
|
|
1642
1773
|
return `${model.name}_${entry.name}_${entry.kind}_${index}`;
|
|
1643
1774
|
}
|
|
1775
|
+
static SQL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1776
|
+
"and",
|
|
1777
|
+
"or",
|
|
1778
|
+
"not",
|
|
1779
|
+
"in",
|
|
1780
|
+
"is",
|
|
1781
|
+
"null",
|
|
1782
|
+
"true",
|
|
1783
|
+
"false",
|
|
1784
|
+
"between",
|
|
1785
|
+
"like",
|
|
1786
|
+
"exists",
|
|
1787
|
+
"all",
|
|
1788
|
+
"any",
|
|
1789
|
+
"asc",
|
|
1790
|
+
"desc",
|
|
1791
|
+
"with",
|
|
1792
|
+
"using",
|
|
1793
|
+
"as",
|
|
1794
|
+
"on",
|
|
1795
|
+
"infinity",
|
|
1796
|
+
"extract",
|
|
1797
|
+
"current_date",
|
|
1798
|
+
"current_timestamp"
|
|
1799
|
+
]);
|
|
1800
|
+
static LITERAL_PLACEHOLDER = "\uE000";
|
|
1801
|
+
static IDENT_PLACEHOLDER = "\uE001";
|
|
1802
|
+
normalizeSqlIdentifiers(s) {
|
|
1803
|
+
const literals = [];
|
|
1804
|
+
let result = s.replace(/'([^']|'')*'/g, (lit) => {
|
|
1805
|
+
literals.push(lit);
|
|
1806
|
+
return `${_MigrationGenerator.LITERAL_PLACEHOLDER}${literals.length - 1}${_MigrationGenerator.LITERAL_PLACEHOLDER}`;
|
|
1807
|
+
});
|
|
1808
|
+
const quotedIdents = [];
|
|
1809
|
+
result = result.replace(/"([^"]*)"/g, (_, ident) => {
|
|
1810
|
+
quotedIdents.push(`"${ident.toLowerCase()}"`);
|
|
1811
|
+
return `${_MigrationGenerator.IDENT_PLACEHOLDER}${quotedIdents.length - 1}${_MigrationGenerator.IDENT_PLACEHOLDER}`;
|
|
1812
|
+
});
|
|
1813
|
+
result = result.replace(
|
|
1814
|
+
/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g,
|
|
1815
|
+
(match) => _MigrationGenerator.SQL_KEYWORDS.has(match.toLowerCase()) ? match : `"${match.toLowerCase()}"`
|
|
1816
|
+
);
|
|
1817
|
+
for (let i = 0; i < quotedIdents.length; i++) {
|
|
1818
|
+
result = result.replace(
|
|
1819
|
+
new RegExp(`${_MigrationGenerator.IDENT_PLACEHOLDER}${i}${_MigrationGenerator.IDENT_PLACEHOLDER}`, "g"),
|
|
1820
|
+
quotedIdents[i]
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
for (let i = 0; i < literals.length; i++) {
|
|
1824
|
+
result = result.replace(
|
|
1825
|
+
new RegExp(`${_MigrationGenerator.LITERAL_PLACEHOLDER}${i}${_MigrationGenerator.LITERAL_PLACEHOLDER}`, "g"),
|
|
1826
|
+
literals[i]
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
return result;
|
|
1830
|
+
}
|
|
1831
|
+
normalizeExcludeDef(def) {
|
|
1832
|
+
let s = def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/::\s*timestamp\s+with\s+time\s+zone\b/gi, "::timestamptz").replace(/::\s*timestamp\s+without\s+time\s+zone\b/gi, "::timestamp").trim();
|
|
1833
|
+
const whereMatch = s.match(/\bWHERE\s*\(/i);
|
|
1834
|
+
if (whereMatch) {
|
|
1835
|
+
const openParen = (whereMatch.index ?? 0) + whereMatch[0].length - 1;
|
|
1836
|
+
const closeParen = this.findMatchingParen(s, openParen);
|
|
1837
|
+
if (closeParen !== -1) {
|
|
1838
|
+
let cond = s.slice(openParen + 1, closeParen).trim();
|
|
1839
|
+
while (this.isWrappedByOuterParentheses(cond)) {
|
|
1840
|
+
cond = cond.slice(1, -1).trim();
|
|
1841
|
+
}
|
|
1842
|
+
s = s.slice(0, openParen + 1) + cond + s.slice(closeParen);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return this.normalizeSqlIdentifiers(s);
|
|
1846
|
+
}
|
|
1847
|
+
findMatchingParen(s, openIndex) {
|
|
1848
|
+
let depth = 1;
|
|
1849
|
+
let inSingleQuote = false;
|
|
1850
|
+
for (let i = openIndex + 1; i < s.length; i++) {
|
|
1851
|
+
const char = s[i];
|
|
1852
|
+
const next = s[i + 1];
|
|
1853
|
+
if (char === "'") {
|
|
1854
|
+
if (inSingleQuote && next === "'") {
|
|
1855
|
+
i++;
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
inSingleQuote = !inSingleQuote;
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (inSingleQuote) {
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
if (char === "(") {
|
|
1865
|
+
depth++;
|
|
1866
|
+
} else if (char === ")") {
|
|
1867
|
+
depth--;
|
|
1868
|
+
if (depth === 0) {
|
|
1869
|
+
return i;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return -1;
|
|
1874
|
+
}
|
|
1875
|
+
normalizeTriggerDef(def) {
|
|
1876
|
+
const s = 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();
|
|
1877
|
+
return this.normalizeSqlIdentifiers(s);
|
|
1878
|
+
}
|
|
1644
1879
|
normalizeCheckExpression(expr) {
|
|
1645
1880
|
let normalized = expr.replace(/\s+/g, " ").trim();
|
|
1646
1881
|
while (this.isWrappedByOuterParentheses(normalized)) {
|
|
@@ -1776,10 +2011,11 @@ var MigrationGenerator = class {
|
|
|
1776
2011
|
escapeExpressionForRaw(expr) {
|
|
1777
2012
|
return expr.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1778
2013
|
}
|
|
1779
|
-
addCheckConstraint(table, constraintName, expression) {
|
|
2014
|
+
addCheckConstraint(table, constraintName, expression, deferrable) {
|
|
1780
2015
|
const escaped = this.escapeExpressionForRaw(expression);
|
|
2016
|
+
const deferrableClause = deferrable ? ` DEFERRABLE ${deferrable}` : "";
|
|
1781
2017
|
this.writer.writeLine(
|
|
1782
|
-
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`
|
|
2018
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})${deferrableClause}\`);`
|
|
1783
2019
|
);
|
|
1784
2020
|
this.writer.blankLine();
|
|
1785
2021
|
}
|
|
@@ -1787,6 +2023,43 @@ var MigrationGenerator = class {
|
|
|
1787
2023
|
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1788
2024
|
this.writer.blankLine();
|
|
1789
2025
|
}
|
|
2026
|
+
buildExcludeDef(entry) {
|
|
2027
|
+
const elementsStr = entry.elements.map((el) => "column" in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`).join(", ");
|
|
2028
|
+
const whereClause = entry.where ? ` WHERE (${entry.where})` : "";
|
|
2029
|
+
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
2030
|
+
return `EXCLUDE USING ${entry.using} (${elementsStr})${whereClause}${deferrableClause}`;
|
|
2031
|
+
}
|
|
2032
|
+
addExcludeConstraint(table, constraintName, entry) {
|
|
2033
|
+
const def = this.buildExcludeDef(entry);
|
|
2034
|
+
const escaped = this.escapeExpressionForRaw(def);
|
|
2035
|
+
this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
|
|
2036
|
+
this.writer.blankLine();
|
|
2037
|
+
}
|
|
2038
|
+
dropExcludeConstraint(table, constraintName) {
|
|
2039
|
+
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
2040
|
+
this.writer.blankLine();
|
|
2041
|
+
}
|
|
2042
|
+
buildConstraintTriggerDef(table, constraintName, entry) {
|
|
2043
|
+
const eventsStr = entry.events.join(" OR ");
|
|
2044
|
+
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
2045
|
+
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
2046
|
+
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
2047
|
+
return `CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}`;
|
|
2048
|
+
}
|
|
2049
|
+
addConstraintTrigger(table, constraintName, entry) {
|
|
2050
|
+
const eventsStr = entry.events.join(" OR ");
|
|
2051
|
+
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
2052
|
+
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
2053
|
+
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
2054
|
+
this.writer.writeLine(
|
|
2055
|
+
`await knex.raw(\`CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}\`);`
|
|
2056
|
+
);
|
|
2057
|
+
this.writer.blankLine();
|
|
2058
|
+
}
|
|
2059
|
+
dropConstraintTrigger(table, constraintName) {
|
|
2060
|
+
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
2061
|
+
this.writer.blankLine();
|
|
2062
|
+
}
|
|
1790
2063
|
value(value2) {
|
|
1791
2064
|
if (typeof value2 === "string") {
|
|
1792
2065
|
return `'${value2}'`;
|