@smartive/graphql-magic 23.7.0 → 23.8.0-next.1

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 CHANGED
@@ -1,5 +1,12 @@
1
- ## [23.7.0](https://github.com/smartive/graphql-magic/compare/v23.6.1...v23.7.0) (2026-03-04)
1
+ ## [23.8.0-next.1](https://github.com/smartive/graphql-magic/compare/v23.7.0...v23.8.0-next.1) (2026-03-04)
2
2
 
3
3
  ### Features
4
4
 
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))
5
+ * Exclude constraints and triggers ([12f5d85](https://github.com/smartive/graphql-magic/commit/12f5d85470bdf1107e023700d5a11012df69d098))
6
+
7
+ ### Bug Fixes
8
+
9
+ * constraint normalization for empty migration when schema in sync ([77a8283](https://github.com/smartive/graphql-magic/commit/77a828367abc78c07708556d7d751d780b785a9a))
10
+ * gen ([80bb12d](https://github.com/smartive/graphql-magic/commit/80bb12da6f7eb0aa231fb13c22019995f5811a6c))
11
+ * normalize identifier quoting in constraint comparison ([689136b](https://github.com/smartive/graphql-magic/commit/689136b1ef07874b24986c4596aae37acc92d74b))
12
+ * produce empty migration when schema is in sync ([e8e8e88](https://github.com/smartive/graphql-magic/commit/e8e8e8825a1d4ac833886f993a702c187364ea6f))
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.getCheckConstraintName(model, entry, i);
1195
- const expression = entry.expression;
1265
+ const constraintName = this.getConstraintName(model, entry, i);
1196
1266
  up.push(() => {
1197
- this.addCheckConstraint(table, constraintName, expression);
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);
1279
+ up.push(() => {
1280
+ this.addConstraintTrigger(table, constraintName, entry);
1198
1281
  });
1199
1282
  }
1200
1283
  }
@@ -1234,36 +1317,88 @@ 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.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
- });
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(
1349
+ table,
1350
+ existingConstraint.constraintName,
1351
+ existingConstraint.expression
1352
+ );
1353
+ });
1354
+ }
1355
+ } else if (entry.kind === "exclude") {
1356
+ validateExcludeConstraint(model, entry);
1357
+ const newDef = this.normalizeExcludeDef(this.buildExcludeDef(entry));
1358
+ const existing = existingExcludeMap?.get(constraintName);
1359
+ if (existing === void 0) {
1360
+ up.push(() => {
1361
+ this.addExcludeConstraint(table, constraintName, entry);
1362
+ });
1363
+ down.push(() => {
1364
+ this.dropExcludeConstraint(table, constraintName);
1365
+ });
1366
+ } else if (existing.normalized !== newDef) {
1367
+ up.push(() => {
1368
+ this.dropExcludeConstraint(table, constraintName);
1369
+ this.addExcludeConstraint(table, constraintName, entry);
1370
+ });
1371
+ down.push(() => {
1372
+ this.dropExcludeConstraint(table, constraintName);
1373
+ const escaped = this.escapeExpressionForRaw(existing.raw);
1374
+ this.writer.writeLine(
1375
+ `await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`
1376
+ );
1377
+ this.writer.blankLine();
1378
+ });
1379
+ }
1380
+ } else if (entry.kind === "constraint_trigger") {
1381
+ const newDef = this.normalizeTriggerDef(this.buildConstraintTriggerDef(table, constraintName, entry));
1382
+ const existing = existingTriggerMap?.get(constraintName);
1383
+ if (existing === void 0) {
1384
+ up.push(() => {
1385
+ this.addConstraintTrigger(table, constraintName, entry);
1386
+ });
1387
+ down.push(() => {
1388
+ this.dropConstraintTrigger(table, constraintName);
1389
+ });
1390
+ } else if (existing.normalized !== newDef) {
1391
+ up.push(() => {
1392
+ this.dropConstraintTrigger(table, constraintName);
1393
+ this.addConstraintTrigger(table, constraintName, entry);
1394
+ });
1395
+ down.push(() => {
1396
+ this.dropConstraintTrigger(table, constraintName);
1397
+ const escaped = existing.raw.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
1398
+ this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
1399
+ this.writer.blankLine();
1400
+ });
1401
+ }
1267
1402
  }
1268
1403
  }
1269
1404
  }
@@ -1638,9 +1773,62 @@ var MigrationGenerator = class {
1638
1773
  renameColumn(from, to) {
1639
1774
  this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
1640
1775
  }
1641
- getCheckConstraintName(model, entry, index) {
1776
+ getConstraintName(model, entry, index) {
1642
1777
  return `${model.name}_${entry.name}_${entry.kind}_${index}`;
1643
1778
  }
1779
+ static SQL_KEYWORDS = /* @__PURE__ */ new Set([
1780
+ "and",
1781
+ "or",
1782
+ "not",
1783
+ "in",
1784
+ "is",
1785
+ "null",
1786
+ "true",
1787
+ "false",
1788
+ "between",
1789
+ "like",
1790
+ "exists",
1791
+ "all",
1792
+ "any",
1793
+ "asc",
1794
+ "desc",
1795
+ "with",
1796
+ "using",
1797
+ "as",
1798
+ "on",
1799
+ "infinity",
1800
+ "extract",
1801
+ "current_date",
1802
+ "current_timestamp"
1803
+ ]);
1804
+ static LITERAL_PLACEHOLDER = "\uE000";
1805
+ normalizeSqlIdentifiers(s) {
1806
+ const literals = [];
1807
+ let result = s.replace(/'([^']|'')*'/g, (lit) => {
1808
+ literals.push(lit);
1809
+ return `${_MigrationGenerator.LITERAL_PLACEHOLDER}${literals.length - 1}${_MigrationGenerator.LITERAL_PLACEHOLDER}`;
1810
+ });
1811
+ result = result.replace(/"([^"]*)"/g, (_, ident) => `"${ident.toLowerCase()}"`);
1812
+ result = result.replace(
1813
+ /\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g,
1814
+ (match) => _MigrationGenerator.SQL_KEYWORDS.has(match.toLowerCase()) ? match : `"${match.toLowerCase()}"`
1815
+ );
1816
+ for (let i = 0; i < literals.length; i++) {
1817
+ result = result.replace(
1818
+ new RegExp(`${_MigrationGenerator.LITERAL_PLACEHOLDER}${i}${_MigrationGenerator.LITERAL_PLACEHOLDER}`, "g"),
1819
+ literals[i]
1820
+ );
1821
+ }
1822
+ return result;
1823
+ }
1824
+ normalizeExcludeDef(def) {
1825
+ const s = def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").trim();
1826
+ return this.normalizeSqlIdentifiers(s);
1827
+ }
1828
+ normalizeTriggerDef(def) {
1829
+ 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();
1830
+ return this.normalizeSqlIdentifiers(s);
1831
+ }
1644
1832
  normalizeCheckExpression(expr) {
1645
1833
  let normalized = expr.replace(/\s+/g, " ").trim();
1646
1834
  while (this.isWrappedByOuterParentheses(normalized)) {
@@ -1776,10 +1964,11 @@ var MigrationGenerator = class {
1776
1964
  escapeExpressionForRaw(expr) {
1777
1965
  return expr.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
1778
1966
  }
1779
- addCheckConstraint(table, constraintName, expression) {
1967
+ addCheckConstraint(table, constraintName, expression, deferrable) {
1780
1968
  const escaped = this.escapeExpressionForRaw(expression);
1969
+ const deferrableClause = deferrable ? ` DEFERRABLE ${deferrable}` : "";
1781
1970
  this.writer.writeLine(
1782
- `await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`
1971
+ `await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})${deferrableClause}\`);`
1783
1972
  );
1784
1973
  this.writer.blankLine();
1785
1974
  }
@@ -1787,6 +1976,43 @@ var MigrationGenerator = class {
1787
1976
  this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
1788
1977
  this.writer.blankLine();
1789
1978
  }
1979
+ buildExcludeDef(entry) {
1980
+ const elementsStr = entry.elements.map((el) => "column" in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`).join(", ");
1981
+ const whereClause = entry.where ? ` WHERE (${entry.where})` : "";
1982
+ const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
1983
+ return `EXCLUDE USING ${entry.using} (${elementsStr})${whereClause}${deferrableClause}`;
1984
+ }
1985
+ addExcludeConstraint(table, constraintName, entry) {
1986
+ const def = this.buildExcludeDef(entry);
1987
+ const escaped = this.escapeExpressionForRaw(def);
1988
+ this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
1989
+ this.writer.blankLine();
1990
+ }
1991
+ dropExcludeConstraint(table, constraintName) {
1992
+ this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
1993
+ this.writer.blankLine();
1994
+ }
1995
+ buildConstraintTriggerDef(table, constraintName, entry) {
1996
+ const eventsStr = entry.events.join(" OR ");
1997
+ const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
1998
+ const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
1999
+ const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
2000
+ return `CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}`;
2001
+ }
2002
+ addConstraintTrigger(table, constraintName, entry) {
2003
+ const eventsStr = entry.events.join(" OR ");
2004
+ const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
2005
+ const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
2006
+ const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
2007
+ this.writer.writeLine(
2008
+ `await knex.raw(\`CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}\`);`
2009
+ );
2010
+ this.writer.blankLine();
2011
+ }
2012
+ dropConstraintTrigger(table, constraintName) {
2013
+ this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
2014
+ this.writer.blankLine();
2015
+ }
1790
2016
  value(value2) {
1791
2017
  if (typeof value2 === "string") {
1792
2018
  return `'${value2}'`;