@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.
@@ -8,7 +8,6 @@ export const generateFunctionsFromDatabase = async (knex: Knex): Promise<string>
8
8
  JOIN pg_namespace n ON p.pronamespace = n.oid
9
9
  WHERE n.nspname = 'public'
10
10
  AND NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid)
11
- AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
12
11
  ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
13
12
  `);
14
13
 
@@ -24,7 +23,6 @@ export const generateFunctionsFromDatabase = async (knex: Knex): Promise<string>
24
23
  JOIN pg_aggregate a ON p.oid = a.aggfnoid
25
24
  JOIN pg_namespace n ON p.pronamespace = n.oid
26
25
  WHERE n.nspname = 'public'
27
- AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
28
26
  ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
29
27
  `);
30
28
 
@@ -18,7 +18,6 @@ import {
18
18
  summonByName,
19
19
  typeToField,
20
20
  validateCheckConstraint,
21
- validateExcludeConstraint,
22
21
  } from '../models/utils';
23
22
  import { getColumnName } from '../resolvers';
24
23
  import { Value } from '../values';
@@ -42,10 +41,6 @@ export class MigrationGenerator {
42
41
  private columns: Record<string, Column[]> = {};
43
42
  /** table name -> constraint name -> check clause expression */
44
43
  private existingCheckConstraints: Record<string, Map<string, string>> = {};
45
- /** table name -> constraint name -> exclude definition (normalized) */
46
- private existingExcludeConstraints: Record<string, Map<string, string>> = {};
47
- /** table name -> constraint name -> trigger definition (normalized) */
48
- private existingConstraintTriggers: Record<string, Map<string, string>> = {};
49
44
  private uuidUsed?: boolean;
50
45
  private nowUsed?: boolean;
51
46
  public needsMigration = false;
@@ -88,60 +83,9 @@ export class MigrationGenerator {
88
83
  this.existingCheckConstraints[tableName].set(row.constraint_name, row.check_clause);
89
84
  }
90
85
 
91
- const excludeResult = await schema.knex.raw(
92
- `SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_constraintdef(c.oid) as constraint_def
93
- FROM pg_constraint c
94
- JOIN pg_namespace n ON c.connamespace = n.oid
95
- WHERE n.nspname = 'public' AND c.contype = 'x'`,
96
- );
97
- const excludeRows: { table_name: string; constraint_name: string; constraint_def: string }[] =
98
- 'rows' in excludeResult && Array.isArray((excludeResult as { rows: unknown }).rows)
99
- ? (excludeResult as { rows: { table_name: string; constraint_name: string; constraint_def: string }[] }).rows
100
- : [];
101
- for (const row of excludeRows) {
102
- const tableName = row.table_name.split('.').pop()?.replace(/^"|"$/g, '') ?? row.table_name;
103
- if (!this.existingExcludeConstraints[tableName]) {
104
- this.existingExcludeConstraints[tableName] = new Map();
105
- }
106
- this.existingExcludeConstraints[tableName].set(row.constraint_name, this.normalizeExcludeDef(row.constraint_def));
107
- }
108
-
109
- const triggerResult = await schema.knex.raw(
110
- `SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_triggerdef(t.oid) as trigger_def
111
- FROM pg_constraint c
112
- JOIN pg_trigger t ON t.tgconstraint = c.oid
113
- JOIN pg_namespace n ON c.connamespace = n.oid
114
- WHERE n.nspname = 'public' AND c.contype = 't'`,
115
- );
116
- const triggerRows: { table_name: string; constraint_name: string; trigger_def: string }[] =
117
- 'rows' in triggerResult && Array.isArray((triggerResult as { rows: unknown }).rows)
118
- ? (triggerResult as { rows: { table_name: string; constraint_name: string; trigger_def: string }[] }).rows
119
- : [];
120
- for (const row of triggerRows) {
121
- const tableName = row.table_name.split('.').pop()?.replace(/^"|"$/g, '') ?? row.table_name;
122
- if (!this.existingConstraintTriggers[tableName]) {
123
- this.existingConstraintTriggers[tableName] = new Map();
124
- }
125
- this.existingConstraintTriggers[tableName].set(row.constraint_name, this.normalizeTriggerDef(row.trigger_def));
126
- }
127
-
128
86
  const up: Callbacks = [];
129
87
  const down: Callbacks = [];
130
88
 
131
- const wantsBtreeGist = models.entities.some((model) =>
132
- model.constraints?.some((c) => c.kind === 'exclude' && c.elements.some((el) => 'column' in el && el.operator === '=')),
133
- );
134
- if (wantsBtreeGist) {
135
- const extResult = await schema.knex('pg_extension').where('extname', 'btree_gist').select('oid').first();
136
- const btreeGistInstalled = !!extResult;
137
- if (!btreeGistInstalled) {
138
- up.unshift(() => {
139
- this.writer.writeLine(`await knex.raw('CREATE EXTENSION IF NOT EXISTS btree_gist');`);
140
- this.writer.blankLine();
141
- });
142
- }
143
- }
144
-
145
89
  this.createEnums(
146
90
  this.models.enums.filter((enm) => !enums.includes(lowerFirst(enm.name))),
147
91
  up,
@@ -234,22 +178,10 @@ export class MigrationGenerator {
234
178
  if (entry.kind === 'check') {
235
179
  validateCheckConstraint(model, entry);
236
180
  const table = model.name;
237
- const constraintName = this.getConstraintName(model, entry, i);
238
- up.push(() => {
239
- this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
240
- });
241
- } else if (entry.kind === 'exclude') {
242
- validateExcludeConstraint(model, entry);
243
- const table = model.name;
244
- const constraintName = this.getConstraintName(model, entry, i);
245
- up.push(() => {
246
- this.addExcludeConstraint(table, constraintName, entry);
247
- });
248
- } else if (entry.kind === 'constraint_trigger') {
249
- const table = model.name;
250
- const constraintName = this.getConstraintName(model, entry, i);
181
+ const constraintName = this.getCheckConstraintName(model, entry, i);
182
+ const expression = entry.expression;
251
183
  up.push(() => {
252
- this.addConstraintTrigger(table, constraintName, entry);
184
+ this.addCheckConstraint(table, constraintName, expression);
253
185
  });
254
186
  }
255
187
  }
@@ -305,83 +237,38 @@ export class MigrationGenerator {
305
237
  this.updateFields(model, existingFields, up, down);
306
238
 
307
239
  if (model.constraints?.length) {
308
- const existingCheckMap = this.existingCheckConstraints[model.name];
309
- const existingExcludeMap = this.existingExcludeConstraints[model.name];
310
- const existingTriggerMap = this.existingConstraintTriggers[model.name];
311
240
  for (let i = 0; i < model.constraints.length; i++) {
312
241
  const entry = model.constraints[i];
242
+ if (entry.kind !== 'check') {
243
+ continue;
244
+ }
245
+ validateCheckConstraint(model, entry);
313
246
  const table = model.name;
314
- const constraintName = this.getConstraintName(model, entry, i);
315
- if (entry.kind === 'check') {
316
- validateCheckConstraint(model, entry);
317
- const newExpression = entry.expression;
318
- const existingExpression = existingCheckMap?.get(constraintName);
319
- if (existingExpression === undefined) {
320
- up.push(() => {
321
- this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
322
- });
323
- down.push(() => {
324
- this.dropCheckConstraint(table, constraintName);
325
- });
326
- } else if (
327
- this.normalizeCheckExpression(existingExpression) !== this.normalizeCheckExpression(newExpression)
328
- ) {
329
- up.push(() => {
330
- this.dropCheckConstraint(table, constraintName);
331
- this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
332
- });
333
- down.push(() => {
334
- this.dropCheckConstraint(table, constraintName);
335
- this.addCheckConstraint(table, constraintName, existingExpression);
336
- });
337
- }
338
- } else if (entry.kind === 'exclude') {
339
- validateExcludeConstraint(model, entry);
340
- const newDef = this.normalizeExcludeDef(this.buildExcludeDef(entry));
341
- const existingDef = existingExcludeMap?.get(constraintName);
342
- if (existingDef === undefined) {
343
- up.push(() => {
344
- this.addExcludeConstraint(table, constraintName, entry);
345
- });
346
- down.push(() => {
347
- this.dropExcludeConstraint(table, constraintName);
348
- });
349
- } else if (existingDef !== newDef) {
350
- up.push(() => {
351
- this.dropExcludeConstraint(table, constraintName);
352
- this.addExcludeConstraint(table, constraintName, entry);
353
- });
354
- down.push(() => {
355
- this.dropExcludeConstraint(table, constraintName);
356
- const escaped = this.escapeExpressionForRaw(existingDef);
357
- this.writer.writeLine(
358
- `await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`,
359
- );
360
- this.writer.blankLine();
361
- });
362
- }
363
- } else if (entry.kind === 'constraint_trigger') {
364
- const newDef = this.normalizeTriggerDef(this.buildConstraintTriggerDef(table, constraintName, entry));
365
- const existingDef = existingTriggerMap?.get(constraintName);
366
- if (existingDef === undefined) {
367
- up.push(() => {
368
- this.addConstraintTrigger(table, constraintName, entry);
369
- });
370
- down.push(() => {
371
- this.dropConstraintTrigger(table, constraintName);
372
- });
373
- } else if (existingDef !== newDef) {
374
- up.push(() => {
375
- this.dropConstraintTrigger(table, constraintName);
376
- this.addConstraintTrigger(table, constraintName, entry);
377
- });
378
- down.push(() => {
379
- this.dropConstraintTrigger(table, constraintName);
380
- const escaped = existingDef.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
381
- this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
382
- this.writer.blankLine();
383
- });
384
- }
247
+ const constraintName = this.getCheckConstraintName(model, entry, i);
248
+ const existingConstraint = this.findExistingConstraint(table, entry, constraintName);
249
+ if (!existingConstraint) {
250
+ up.push(() => {
251
+ this.addCheckConstraint(table, constraintName, entry.expression);
252
+ });
253
+ down.push(() => {
254
+ this.dropCheckConstraint(table, constraintName);
255
+ });
256
+ } else if (
257
+ !(await this.equalExpressions(
258
+ table,
259
+ existingConstraint.constraintName,
260
+ existingConstraint.expression,
261
+ entry.expression,
262
+ ))
263
+ ) {
264
+ up.push(() => {
265
+ this.dropCheckConstraint(table, existingConstraint.constraintName);
266
+ this.addCheckConstraint(table, constraintName, entry.expression);
267
+ });
268
+ down.push(() => {
269
+ this.dropCheckConstraint(table, constraintName);
270
+ this.addCheckConstraint(table, existingConstraint.constraintName, existingConstraint.expression);
271
+ });
385
272
  }
386
273
  }
387
274
  }
@@ -874,209 +761,203 @@ export class MigrationGenerator {
874
761
  this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
875
762
  }
876
763
 
877
- private getConstraintName(model: EntityModel, entry: { kind: string; name: string }, index: number): string {
764
+ private getCheckConstraintName(model: EntityModel, entry: { kind: string; name: string }, index: number): string {
878
765
  return `${model.name}_${entry.name}_${entry.kind}_${index}`;
879
766
  }
880
767
 
881
- private static readonly SQL_KEYWORDS = new Set([
882
- 'and',
883
- 'or',
884
- 'not',
885
- 'in',
886
- 'is',
887
- 'null',
888
- 'true',
889
- 'false',
890
- 'between',
891
- 'like',
892
- 'exists',
893
- 'all',
894
- 'any',
895
- 'asc',
896
- 'desc',
897
- 'with',
898
- 'using',
899
- 'as',
900
- 'on',
901
- 'infinity',
902
- 'extract',
903
- 'current_date',
904
- 'current_timestamp',
905
- ]);
906
-
907
- private normalizeSqlIdentifiers(s: string): string {
908
- const literals: string[] = [];
909
- let result = s.replace(/'([^']|'')*'/g, (lit) => {
910
- literals.push(lit);
911
-
912
- return `\x00L${literals.length - 1}\x00`;
913
- });
914
- result = result.replace(/"([^"]*)"/g, (_, ident) => `"${ident.toLowerCase()}"`);
915
- result = result.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match) =>
916
- MigrationGenerator.SQL_KEYWORDS.has(match.toLowerCase()) ? match : `"${match.toLowerCase()}"`,
917
- );
918
- for (let i = 0; i < literals.length; i++) {
919
- result = result.replace(new RegExp(`\x00L${i}\x00`, 'g'), literals[i]);
768
+ private normalizeCheckExpression(expr: string): string {
769
+ let normalized = expr.replace(/\s+/g, ' ').trim();
770
+ while (this.isWrappedByOuterParentheses(normalized)) {
771
+ normalized = normalized.slice(1, -1).trim();
920
772
  }
921
773
 
922
- return result;
774
+ return normalized;
923
775
  }
924
776
 
925
- private normalizeCheckExpression(expr: string): string {
926
- let s = expr.replace(/\s+/g, ' ').trim();
927
- while (s.length >= 2 && s.startsWith('(') && s.endsWith(')')) {
928
- let depth = 0;
929
- let match = true;
930
- for (let i = 0; i < s.length; i++) {
931
- if (s[i] === '(') {
932
- depth++;
933
- } else if (s[i] === ')') {
934
- depth--;
935
- }
936
- if (depth === 0 && i < s.length - 1) {
937
- match = false;
938
- break;
777
+ private isWrappedByOuterParentheses(expr: string): boolean {
778
+ if (!expr.startsWith('(') || !expr.endsWith(')')) {
779
+ return false;
780
+ }
781
+
782
+ let depth = 0;
783
+ let inSingleQuote = false;
784
+ for (let i = 0; i < expr.length; i++) {
785
+ const char = expr[i];
786
+ const next = expr[i + 1];
787
+
788
+ if (char === "'") {
789
+ if (inSingleQuote && next === "'") {
790
+ i++;
791
+ continue;
939
792
  }
793
+ inSingleQuote = !inSingleQuote;
794
+ continue;
940
795
  }
941
- if (!match || depth !== 0) {
942
- break;
796
+
797
+ if (inSingleQuote) {
798
+ continue;
943
799
  }
944
- s = s.slice(1, -1).trim();
945
- }
946
800
 
947
- s = s
948
- .replace(/\s*\(\s*/g, '(')
949
- .replace(/\s*\)\s*/g, ')')
950
- .replace(/\s+AND\s+/gi, ' AND ')
951
- .replace(/\s+OR\s+/gi, ' OR ')
952
- .trim();
801
+ if (char === '(') {
802
+ depth++;
803
+ } else if (char === ')') {
804
+ depth--;
805
+ if (depth === 0 && i !== expr.length - 1) {
806
+ return false;
807
+ }
808
+ if (depth < 0) {
809
+ return false;
810
+ }
811
+ }
812
+ }
953
813
 
954
- return this.normalizeSqlIdentifiers(s);
814
+ return depth === 0;
955
815
  }
956
816
 
957
- private normalizeExcludeDef(def: string): string {
958
- const s = def
959
- .replace(/\s+/g, ' ')
960
- .replace(/\s*\(\s*/g, '(')
961
- .replace(/\s*\)\s*/g, ')')
962
- .trim();
817
+ private findExistingConstraint(
818
+ table: string,
819
+ entry: { kind: 'check'; name: string; expression: string },
820
+ preferredConstraintName: string,
821
+ ): { constraintName: string; expression: string } | null {
822
+ const existingMap = this.existingCheckConstraints[table];
823
+ if (!existingMap) {
824
+ return null;
825
+ }
963
826
 
964
- return this.normalizeSqlIdentifiers(s);
965
- }
827
+ const preferredExpression = existingMap.get(preferredConstraintName);
828
+ if (preferredExpression !== undefined) {
829
+ return {
830
+ constraintName: preferredConstraintName,
831
+ expression: preferredExpression,
832
+ };
833
+ }
966
834
 
967
- private normalizeTriggerDef(def: string): string {
968
- return def
969
- .replace(/\s+/g, ' ')
970
- .replace(/\s*\(\s*/g, '(')
971
- .replace(/\s*\)\s*/g, ')')
972
- .trim();
973
- }
835
+ const normalizedNewExpression = this.normalizeCheckExpression(entry.expression);
836
+ const constraintPrefix = `${table}_${entry.name}_${entry.kind}_`;
974
837
 
975
- /** Escape expression for embedding inside a template literal in generated code */
976
- private escapeExpressionForRaw(expr: string): string {
977
- return expr.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
838
+ for (const [constraintName, expression] of existingMap.entries()) {
839
+ if (!constraintName.startsWith(constraintPrefix)) {
840
+ continue;
841
+ }
842
+ if (this.normalizeCheckExpression(expression) !== normalizedNewExpression) {
843
+ continue;
844
+ }
845
+
846
+ return { constraintName, expression };
847
+ }
848
+
849
+ return null;
978
850
  }
979
851
 
980
- private addCheckConstraint(
852
+ private async equalExpressions(
981
853
  table: string,
982
854
  constraintName: string,
983
- expression: string,
984
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE',
985
- ) {
986
- const escaped = this.escapeExpressionForRaw(expression);
987
- const deferrableClause = deferrable ? ` DEFERRABLE ${deferrable}` : '';
988
- this.writer.writeLine(
989
- `await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})${deferrableClause}\`);`,
990
- );
991
- this.writer.blankLine();
855
+ existingExpression: string,
856
+ newExpression: string,
857
+ ): Promise<boolean> {
858
+ try {
859
+ const [canonicalExisting, canonicalNew] = await Promise.all([
860
+ this.canonicalizeCheckExpressionWithPostgres(table, existingExpression),
861
+ this.canonicalizeCheckExpressionWithPostgres(table, newExpression),
862
+ ]);
863
+
864
+ return canonicalExisting === canonicalNew;
865
+ } catch (error) {
866
+ console.warn(
867
+ `Failed to canonicalize check constraint "${constraintName}" on table "${table}". Treating it as changed.`,
868
+ error,
869
+ );
870
+
871
+ return false;
872
+ }
992
873
  }
993
874
 
994
- private dropCheckConstraint(table: string, constraintName: string) {
995
- this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
996
- this.writer.blankLine();
875
+ private async canonicalizeCheckExpressionWithPostgres(table: string, expression: string): Promise<string> {
876
+ const sourceTableIdentifier = table
877
+ .split('.')
878
+ .map((part) => this.quoteIdentifier(part))
879
+ .join('.');
880
+
881
+ const uniqueSuffix = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
882
+ const tableSlug = table.toLowerCase().replace(/[^a-z0-9_]/g, '_');
883
+
884
+ const tempTableName = `gqm_tmp_check_${tableSlug}_${uniqueSuffix}`;
885
+ const tempTableIdentifier = this.quoteIdentifier(tempTableName);
886
+
887
+ const constraintName = `gqm_tmp_check_${uniqueSuffix}`;
888
+ const constraintIdentifier = this.quoteIdentifier(constraintName);
889
+
890
+ const trx = await this.knex.transaction();
891
+
892
+ try {
893
+ await trx.raw(`CREATE TEMP TABLE ${tempTableIdentifier} (LIKE ${sourceTableIdentifier}) ON COMMIT DROP`);
894
+ await trx.raw(`ALTER TABLE ${tempTableIdentifier} ADD CONSTRAINT ${constraintIdentifier} CHECK (${expression})`);
895
+ const result = await trx.raw(
896
+ `SELECT pg_get_constraintdef(c.oid, true) AS constraint_definition
897
+ FROM pg_constraint c
898
+ JOIN pg_class t
899
+ ON t.oid = c.conrelid
900
+ WHERE t.relname = ?
901
+ AND c.conname = ?
902
+ ORDER BY c.oid DESC
903
+ LIMIT 1`,
904
+ [tempTableName, constraintName],
905
+ );
906
+
907
+ const rows: { constraint_definition: string }[] =
908
+ 'rows' in result && Array.isArray((result as { rows: unknown }).rows)
909
+ ? (result as { rows: { constraint_definition: string }[] }).rows
910
+ : [];
911
+ const definition = rows[0]?.constraint_definition;
912
+ if (!definition) {
913
+ throw new Error(`Could not read canonical check definition for expression: ${expression}`);
914
+ }
915
+
916
+ return this.normalizeCheckExpression(this.extractCheckExpressionFromDefinition(definition));
917
+ } finally {
918
+ try {
919
+ await trx.rollback();
920
+ } catch {
921
+ // no-op: transaction may already be closed by driver after failure
922
+ }
923
+ }
997
924
  }
998
925
 
999
- private buildExcludeDef(entry: {
1000
- using: string;
1001
- elements: ({ column: string; operator: string } | { expression: string; operator: string })[];
1002
- where?: string;
1003
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
1004
- }): string {
1005
- const elementsStr = entry.elements
1006
- .map((el) => ('column' in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`))
1007
- .join(', ');
1008
- const whereClause = entry.where ? ` WHERE (${entry.where})` : '';
1009
- const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
1010
-
1011
- return `EXCLUDE USING ${entry.using} (${elementsStr})${whereClause}${deferrableClause}`;
926
+ private extractCheckExpressionFromDefinition(definition: string): string {
927
+ const trimmed = definition.trim();
928
+ const match = trimmed.match(/^CHECK\s*\(([\s\S]*)\)$/i);
929
+ if (!match) {
930
+ return trimmed;
931
+ }
932
+
933
+ return match[1];
1012
934
  }
1013
935
 
1014
- private addExcludeConstraint(
1015
- table: string,
1016
- constraintName: string,
1017
- entry: {
1018
- using: string;
1019
- elements: ({ column: string; operator: string } | { expression: string; operator: string })[];
1020
- where?: string;
1021
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
1022
- },
1023
- ) {
1024
- const def = this.buildExcludeDef(entry);
1025
- const escaped = this.escapeExpressionForRaw(def);
1026
- this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
1027
- this.writer.blankLine();
936
+ private quoteIdentifier(identifier: string): string {
937
+ return `"${identifier.replace(/"/g, '""')}"`;
1028
938
  }
1029
939
 
1030
- private dropExcludeConstraint(table: string, constraintName: string) {
1031
- this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
1032
- this.writer.blankLine();
940
+ private quoteQualifiedIdentifier(identifier: string): string {
941
+ return identifier
942
+ .split('.')
943
+ .map((part) => this.quoteIdentifier(part))
944
+ .join('.');
1033
945
  }
1034
946
 
1035
- private buildConstraintTriggerDef(
1036
- table: string,
1037
- constraintName: string,
1038
- entry: {
1039
- when: 'AFTER' | 'BEFORE';
1040
- events: ('INSERT' | 'UPDATE')[];
1041
- forEach: 'ROW' | 'STATEMENT';
1042
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
1043
- function: { name: string; args?: string[] };
1044
- },
1045
- ): string {
1046
- const eventsStr = entry.events.join(' OR ');
1047
- const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
1048
- const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(', ') : '';
1049
- const executeClause = argsStr
1050
- ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})`
1051
- : `EXECUTE FUNCTION ${entry.function.name}()`;
1052
-
1053
- return `CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}`;
947
+ /** Escape expression for embedding inside a template literal in generated code */
948
+ private escapeExpressionForRaw(expr: string): string {
949
+ return expr.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
1054
950
  }
1055
951
 
1056
- private addConstraintTrigger(
1057
- table: string,
1058
- constraintName: string,
1059
- entry: {
1060
- when: 'AFTER' | 'BEFORE';
1061
- events: ('INSERT' | 'UPDATE')[];
1062
- forEach: 'ROW' | 'STATEMENT';
1063
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
1064
- function: { name: string; args?: string[] };
1065
- },
1066
- ) {
1067
- const eventsStr = entry.events.join(' OR ');
1068
- const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
1069
- const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(', ') : '';
1070
- const executeClause = argsStr
1071
- ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})`
1072
- : `EXECUTE FUNCTION ${entry.function.name}()`;
952
+ private addCheckConstraint(table: string, constraintName: string, expression: string) {
953
+ const escaped = this.escapeExpressionForRaw(expression);
1073
954
  this.writer.writeLine(
1074
- `await knex.raw(\`CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}\`);`,
955
+ `await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`,
1075
956
  );
1076
957
  this.writer.blankLine();
1077
958
  }
1078
959
 
1079
- private dropConstraintTrigger(table: string, constraintName: string) {
960
+ private dropCheckConstraint(table: string, constraintName: string) {
1080
961
  this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
1081
962
  this.writer.blankLine();
1082
963
  }
@@ -68,7 +68,6 @@ export const getDatabaseFunctions = async (knex: Knex): Promise<DatabaseFunction
68
68
  JOIN pg_namespace n ON p.pronamespace = n.oid
69
69
  WHERE n.nspname = 'public'
70
70
  AND NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid)
71
- AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
72
71
  ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
73
72
  `);
74
73
 
@@ -85,7 +84,6 @@ export const getDatabaseFunctions = async (knex: Knex): Promise<DatabaseFunction
85
84
  JOIN pg_aggregate a ON p.oid = a.aggfnoid
86
85
  JOIN pg_namespace n ON p.pronamespace = n.oid
87
86
  WHERE n.nspname = 'public'
88
- AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
89
87
  ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
90
88
  `);
91
89
 
@@ -165,26 +165,7 @@ export type ModelDefinition = {
165
165
  */
166
166
  manyToManyRelation?: boolean;
167
167
 
168
- constraints?: (
169
- | { kind: 'check'; name: string; expression: string; deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE' }
170
- | {
171
- kind: 'exclude';
172
- name: string;
173
- using: 'gist';
174
- elements: ({ column: string; operator: '=' } | { expression: string; operator: '&&' })[];
175
- where?: string;
176
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
177
- }
178
- | {
179
- kind: 'constraint_trigger';
180
- name: string;
181
- when: 'AFTER' | 'BEFORE';
182
- events: ('INSERT' | 'UPDATE')[];
183
- forEach: 'ROW' | 'STATEMENT';
184
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
185
- function: { name: string; args?: string[] };
186
- }
187
- )[];
168
+ constraints?: { kind: 'check'; name: string; expression: string }[];
188
169
 
189
170
  // temporary fields for the generation of migrations
190
171
  deleted?: true;