@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.
@@ -1,7 +1,7 @@
1
1
  import CodeBlockWriter from 'code-block-writer';
2
2
  import { SchemaInspector } from 'knex-schema-inspector';
3
3
  import lowerFirst from 'lodash/lowerFirst';
4
- import { and, get, isCreatableModel, isInherited, isStoredInDatabase, isUpdatableField, isUpdatableModel, modelNeedsTable, not, summonByName, typeToField, validateCheckConstraint, validateExcludeConstraint, } from '../models/utils';
4
+ import { and, get, isCreatableModel, isInherited, isStoredInDatabase, isUpdatableField, isUpdatableModel, modelNeedsTable, not, summonByName, typeToField, validateCheckConstraint, } from '../models/utils';
5
5
  import { getColumnName } from '../resolvers';
6
6
  import { getDatabaseFunctions, normalizeAggregateDefinition, normalizeFunctionBody, } from './update-functions';
7
7
  export class MigrationGenerator {
@@ -16,10 +16,6 @@ export class MigrationGenerator {
16
16
  columns = {};
17
17
  /** table name -> constraint name -> check clause expression */
18
18
  existingCheckConstraints = {};
19
- /** table name -> constraint name -> { normalized, raw } */
20
- existingExcludeConstraints = {};
21
- /** table name -> constraint name -> { normalized, raw } */
22
- existingConstraintTriggers = {};
23
19
  uuidUsed;
24
20
  nowUsed;
25
21
  needsMigration = false;
@@ -52,54 +48,8 @@ export class MigrationGenerator {
52
48
  }
53
49
  this.existingCheckConstraints[tableName].set(row.constraint_name, row.check_clause);
54
50
  }
55
- const excludeResult = await schema.knex.raw(`SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_constraintdef(c.oid) as constraint_def
56
- FROM pg_constraint c
57
- JOIN pg_namespace n ON c.connamespace = n.oid
58
- WHERE n.nspname = 'public' AND c.contype = 'x'`);
59
- const excludeRows = 'rows' in excludeResult && Array.isArray(excludeResult.rows)
60
- ? excludeResult.rows
61
- : [];
62
- for (const row of excludeRows) {
63
- const tableName = row.table_name.split('.').pop()?.replace(/^"|"$/g, '') ?? row.table_name;
64
- if (!this.existingExcludeConstraints[tableName]) {
65
- this.existingExcludeConstraints[tableName] = new Map();
66
- }
67
- this.existingExcludeConstraints[tableName].set(row.constraint_name, {
68
- normalized: this.normalizeExcludeDef(row.constraint_def),
69
- raw: row.constraint_def,
70
- });
71
- }
72
- const triggerResult = await schema.knex.raw(`SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_triggerdef(t.oid) as trigger_def
73
- FROM pg_constraint c
74
- JOIN pg_trigger t ON t.tgconstraint = c.oid
75
- JOIN pg_namespace n ON c.connamespace = n.oid
76
- WHERE n.nspname = 'public' AND c.contype = 't'`);
77
- const triggerRows = 'rows' in triggerResult && Array.isArray(triggerResult.rows)
78
- ? triggerResult.rows
79
- : [];
80
- for (const row of triggerRows) {
81
- const tableName = row.table_name.split('.').pop()?.replace(/^"|"$/g, '') ?? row.table_name;
82
- if (!this.existingConstraintTriggers[tableName]) {
83
- this.existingConstraintTriggers[tableName] = new Map();
84
- }
85
- this.existingConstraintTriggers[tableName].set(row.constraint_name, {
86
- normalized: this.normalizeTriggerDef(row.trigger_def),
87
- raw: row.trigger_def,
88
- });
89
- }
90
51
  const up = [];
91
52
  const down = [];
92
- const wantsBtreeGist = models.entities.some((model) => model.constraints?.some((c) => c.kind === 'exclude' && c.elements.some((el) => 'column' in el && el.operator === '=')));
93
- if (wantsBtreeGist) {
94
- const extResult = await schema.knex('pg_extension').where('extname', 'btree_gist').select('oid').first();
95
- const btreeGistInstalled = !!extResult;
96
- if (!btreeGistInstalled) {
97
- up.unshift(() => {
98
- this.writer.writeLine(`await knex.raw('CREATE EXTENSION IF NOT EXISTS btree_gist');`);
99
- this.writer.blankLine();
100
- });
101
- }
102
- }
103
53
  this.createEnums(this.models.enums.filter((enm) => !enums.includes(lowerFirst(enm.name))), up, down);
104
54
  await this.handleFunctions(up, down);
105
55
  for (const model of models.entities) {
@@ -179,24 +129,10 @@ export class MigrationGenerator {
179
129
  if (entry.kind === 'check') {
180
130
  validateCheckConstraint(model, entry);
181
131
  const table = model.name;
182
- const constraintName = this.getConstraintName(model, entry, i);
132
+ const constraintName = this.getCheckConstraintName(model, entry, i);
133
+ const expression = entry.expression;
183
134
  up.push(() => {
184
- this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
185
- });
186
- }
187
- else if (entry.kind === 'exclude') {
188
- validateExcludeConstraint(model, entry);
189
- const table = model.name;
190
- const constraintName = this.getConstraintName(model, entry, i);
191
- up.push(() => {
192
- this.addExcludeConstraint(table, constraintName, entry);
193
- });
194
- }
195
- else if (entry.kind === 'constraint_trigger') {
196
- const table = model.name;
197
- const constraintName = this.getConstraintName(model, entry, i);
198
- up.push(() => {
199
- this.addConstraintTrigger(table, constraintName, entry);
135
+ this.addCheckConstraint(table, constraintName, expression);
200
136
  });
201
137
  }
202
138
  }
@@ -235,84 +171,32 @@ export class MigrationGenerator {
235
171
  const existingFields = model.fields.filter((field) => (!field.generateAs || field.generateAs.type === 'expression') && this.hasChanged(model, field));
236
172
  this.updateFields(model, existingFields, up, down);
237
173
  if (model.constraints?.length) {
238
- const existingCheckMap = this.existingCheckConstraints[model.name];
239
- const existingExcludeMap = this.existingExcludeConstraints[model.name];
240
- const existingTriggerMap = this.existingConstraintTriggers[model.name];
241
174
  for (let i = 0; i < model.constraints.length; i++) {
242
175
  const entry = model.constraints[i];
243
- const table = model.name;
244
- const constraintName = this.getConstraintName(model, entry, i);
245
- if (entry.kind === 'check') {
246
- validateCheckConstraint(model, entry);
247
- const newExpression = entry.expression;
248
- const existingExpression = existingCheckMap?.get(constraintName);
249
- if (existingExpression === undefined) {
250
- up.push(() => {
251
- this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
252
- });
253
- down.push(() => {
254
- this.dropCheckConstraint(table, constraintName);
255
- });
256
- }
257
- else if (this.normalizeCheckExpression(existingExpression) !== this.normalizeCheckExpression(newExpression)) {
258
- up.push(() => {
259
- this.dropCheckConstraint(table, constraintName);
260
- this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
261
- });
262
- down.push(() => {
263
- this.dropCheckConstraint(table, constraintName);
264
- this.addCheckConstraint(table, constraintName, existingExpression);
265
- });
266
- }
176
+ if (entry.kind !== 'check') {
177
+ continue;
267
178
  }
268
- else if (entry.kind === 'exclude') {
269
- validateExcludeConstraint(model, entry);
270
- const newDef = this.normalizeExcludeDef(this.buildExcludeDef(entry));
271
- const existing = existingExcludeMap?.get(constraintName);
272
- if (existing === undefined) {
273
- up.push(() => {
274
- this.addExcludeConstraint(table, constraintName, entry);
275
- });
276
- down.push(() => {
277
- this.dropExcludeConstraint(table, constraintName);
278
- });
279
- }
280
- else if (existing.normalized !== newDef) {
281
- up.push(() => {
282
- this.dropExcludeConstraint(table, constraintName);
283
- this.addExcludeConstraint(table, constraintName, entry);
284
- });
285
- down.push(() => {
286
- this.dropExcludeConstraint(table, constraintName);
287
- const escaped = this.escapeExpressionForRaw(existing.raw);
288
- this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
289
- this.writer.blankLine();
290
- });
291
- }
179
+ validateCheckConstraint(model, entry);
180
+ const table = model.name;
181
+ const constraintName = this.getCheckConstraintName(model, entry, i);
182
+ const existingConstraint = this.findExistingConstraint(table, entry, constraintName);
183
+ if (!existingConstraint) {
184
+ up.push(() => {
185
+ this.addCheckConstraint(table, constraintName, entry.expression);
186
+ });
187
+ down.push(() => {
188
+ this.dropCheckConstraint(table, constraintName);
189
+ });
292
190
  }
293
- else if (entry.kind === 'constraint_trigger') {
294
- const newDef = this.normalizeTriggerDef(this.buildConstraintTriggerDef(table, constraintName, entry));
295
- const existing = existingTriggerMap?.get(constraintName);
296
- if (existing === undefined) {
297
- up.push(() => {
298
- this.addConstraintTrigger(table, constraintName, entry);
299
- });
300
- down.push(() => {
301
- this.dropConstraintTrigger(table, constraintName);
302
- });
303
- }
304
- else if (existing.normalized !== newDef) {
305
- up.push(() => {
306
- this.dropConstraintTrigger(table, constraintName);
307
- this.addConstraintTrigger(table, constraintName, entry);
308
- });
309
- down.push(() => {
310
- this.dropConstraintTrigger(table, constraintName);
311
- const escaped = existing.raw.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
312
- this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
313
- this.writer.blankLine();
314
- });
315
- }
191
+ else if (!(await this.equalExpressions(table, existingConstraint.constraintName, existingConstraint.expression, entry.expression))) {
192
+ up.push(() => {
193
+ this.dropCheckConstraint(table, existingConstraint.constraintName);
194
+ this.addCheckConstraint(table, constraintName, entry.expression);
195
+ });
196
+ down.push(() => {
197
+ this.dropCheckConstraint(table, constraintName);
198
+ this.addCheckConstraint(table, existingConstraint.constraintName, existingConstraint.expression);
199
+ });
316
200
  }
317
201
  }
318
202
  }
@@ -695,180 +579,160 @@ export class MigrationGenerator {
695
579
  renameColumn(from, to) {
696
580
  this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
697
581
  }
698
- getConstraintName(model, entry, index) {
582
+ getCheckConstraintName(model, entry, index) {
699
583
  return `${model.name}_${entry.name}_${entry.kind}_${index}`;
700
584
  }
701
- static SQL_KEYWORDS = new Set([
702
- 'and',
703
- 'or',
704
- 'not',
705
- 'in',
706
- 'is',
707
- 'null',
708
- 'true',
709
- 'false',
710
- 'between',
711
- 'like',
712
- 'exists',
713
- 'all',
714
- 'any',
715
- 'asc',
716
- 'desc',
717
- 'with',
718
- 'using',
719
- 'as',
720
- 'on',
721
- 'infinity',
722
- 'extract',
723
- 'current_date',
724
- 'current_timestamp',
725
- ]);
726
- static LITERAL_PLACEHOLDER = '\uE000';
727
- normalizeSqlIdentifiers(s) {
728
- const literals = [];
729
- let result = s.replace(/'([^']|'')*'/g, (lit) => {
730
- literals.push(lit);
731
- return `${MigrationGenerator.LITERAL_PLACEHOLDER}${literals.length - 1}${MigrationGenerator.LITERAL_PLACEHOLDER}`;
732
- });
733
- result = result.replace(/"([^"]*)"/g, (_, ident) => `"${ident.toLowerCase()}"`);
734
- result = result.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match) => MigrationGenerator.SQL_KEYWORDS.has(match.toLowerCase()) ? match : `"${match.toLowerCase()}"`);
735
- for (let i = 0; i < literals.length; i++) {
736
- result = result.replace(new RegExp(`${MigrationGenerator.LITERAL_PLACEHOLDER}${i}${MigrationGenerator.LITERAL_PLACEHOLDER}`, 'g'), literals[i]);
585
+ normalizeCheckExpression(expr) {
586
+ let normalized = expr.replace(/\s+/g, ' ').trim();
587
+ while (this.isWrappedByOuterParentheses(normalized)) {
588
+ normalized = normalized.slice(1, -1).trim();
737
589
  }
738
- return result;
590
+ return normalized;
739
591
  }
740
- stripOuterParens(s) {
741
- while (s.length >= 2 && s.startsWith('(') && s.endsWith(')')) {
742
- let depth = 0;
743
- let match = true;
744
- for (let i = 0; i < s.length; i++) {
745
- if (s[i] === '(') {
746
- depth++;
592
+ isWrappedByOuterParentheses(expr) {
593
+ if (!expr.startsWith('(') || !expr.endsWith(')')) {
594
+ return false;
595
+ }
596
+ let depth = 0;
597
+ let inSingleQuote = false;
598
+ for (let i = 0; i < expr.length; i++) {
599
+ const char = expr[i];
600
+ const next = expr[i + 1];
601
+ if (char === "'") {
602
+ if (inSingleQuote && next === "'") {
603
+ i++;
604
+ continue;
747
605
  }
748
- else if (s[i] === ')') {
749
- depth--;
606
+ inSingleQuote = !inSingleQuote;
607
+ continue;
608
+ }
609
+ if (inSingleQuote) {
610
+ continue;
611
+ }
612
+ if (char === '(') {
613
+ depth++;
614
+ }
615
+ else if (char === ')') {
616
+ depth--;
617
+ if (depth === 0 && i !== expr.length - 1) {
618
+ return false;
750
619
  }
751
- if (depth === 0 && i < s.length - 1) {
752
- match = false;
753
- break;
620
+ if (depth < 0) {
621
+ return false;
754
622
  }
755
623
  }
756
- if (!match || depth !== 0) {
757
- break;
624
+ }
625
+ return depth === 0;
626
+ }
627
+ findExistingConstraint(table, entry, preferredConstraintName) {
628
+ const existingMap = this.existingCheckConstraints[table];
629
+ if (!existingMap) {
630
+ return null;
631
+ }
632
+ const preferredExpression = existingMap.get(preferredConstraintName);
633
+ if (preferredExpression !== undefined) {
634
+ return {
635
+ constraintName: preferredConstraintName,
636
+ expression: preferredExpression,
637
+ };
638
+ }
639
+ const normalizedNewExpression = this.normalizeCheckExpression(entry.expression);
640
+ const constraintPrefix = `${table}_${entry.name}_${entry.kind}_`;
641
+ for (const [constraintName, expression] of existingMap.entries()) {
642
+ if (!constraintName.startsWith(constraintPrefix)) {
643
+ continue;
644
+ }
645
+ if (this.normalizeCheckExpression(expression) !== normalizedNewExpression) {
646
+ continue;
758
647
  }
759
- s = s.slice(1, -1).trim();
648
+ return { constraintName, expression };
760
649
  }
761
- return s;
650
+ return null;
762
651
  }
763
- splitAtTopLevel(s, sep) {
764
- const parts = [];
765
- let depth = 0;
766
- let start = 0;
767
- const sepLen = sep.length;
768
- for (let i = 0; i <= s.length - sepLen; i++) {
769
- if (s[i] === '(') {
770
- depth++;
652
+ async equalExpressions(table, constraintName, existingExpression, newExpression) {
653
+ try {
654
+ const [canonicalExisting, canonicalNew] = await Promise.all([
655
+ this.canonicalizeCheckExpressionWithPostgres(table, existingExpression),
656
+ this.canonicalizeCheckExpressionWithPostgres(table, newExpression),
657
+ ]);
658
+ return canonicalExisting === canonicalNew;
659
+ }
660
+ catch (error) {
661
+ console.warn(`Failed to canonicalize check constraint "${constraintName}" on table "${table}". Treating it as changed.`, error);
662
+ return false;
663
+ }
664
+ }
665
+ async canonicalizeCheckExpressionWithPostgres(table, expression) {
666
+ const sourceTableIdentifier = table
667
+ .split('.')
668
+ .map((part) => this.quoteIdentifier(part))
669
+ .join('.');
670
+ const uniqueSuffix = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
671
+ const tableSlug = table.toLowerCase().replace(/[^a-z0-9_]/g, '_');
672
+ const tempTableName = `gqm_tmp_check_${tableSlug}_${uniqueSuffix}`;
673
+ const tempTableIdentifier = this.quoteIdentifier(tempTableName);
674
+ const constraintName = `gqm_tmp_check_${uniqueSuffix}`;
675
+ const constraintIdentifier = this.quoteIdentifier(constraintName);
676
+ const trx = await this.knex.transaction();
677
+ try {
678
+ await trx.raw(`CREATE TEMP TABLE ${tempTableIdentifier} (LIKE ${sourceTableIdentifier}) ON COMMIT DROP`);
679
+ await trx.raw(`ALTER TABLE ${tempTableIdentifier} ADD CONSTRAINT ${constraintIdentifier} CHECK (${expression})`);
680
+ const result = await trx.raw(`SELECT pg_get_constraintdef(c.oid, true) AS constraint_definition
681
+ FROM pg_constraint c
682
+ JOIN pg_class t
683
+ ON t.oid = c.conrelid
684
+ WHERE t.relname = ?
685
+ AND c.conname = ?
686
+ ORDER BY c.oid DESC
687
+ LIMIT 1`, [tempTableName, constraintName]);
688
+ const rows = 'rows' in result && Array.isArray(result.rows)
689
+ ? result.rows
690
+ : [];
691
+ const definition = rows[0]?.constraint_definition;
692
+ if (!definition) {
693
+ throw new Error(`Could not read canonical check definition for expression: ${expression}`);
771
694
  }
772
- else if (s[i] === ')') {
773
- depth--;
695
+ return this.normalizeCheckExpression(this.extractCheckExpressionFromDefinition(definition));
696
+ }
697
+ finally {
698
+ try {
699
+ await trx.rollback();
774
700
  }
775
- if (depth === 0 && s.slice(i, i + sepLen).toLowerCase() === sep.toLowerCase()) {
776
- parts.push(s.slice(start, i).trim());
777
- start = i + sepLen;
778
- i += sepLen - 1;
701
+ catch {
702
+ // no-op: transaction may already be closed by driver after failure
779
703
  }
780
704
  }
781
- parts.push(s.slice(start).trim());
782
- return parts.filter(Boolean);
783
705
  }
784
- normalizeCheckExpression(expr) {
785
- let s = expr.replace(/\s+/g, ' ').trim();
786
- const normalizeParts = (str, separator) => this.splitAtTopLevel(str, separator)
787
- .map((p) => this.stripOuterParens(p))
788
- .join(separator);
789
- s = this.stripOuterParens(s);
790
- s = normalizeParts(s, ' OR ');
791
- s = this.stripOuterParens(s);
792
- s = normalizeParts(s, ' AND ');
793
- s = s
794
- .replace(/\s*\(\s*/g, '(')
795
- .replace(/\s*\)\s*/g, ')')
796
- .replace(/\s+AND\s+/gi, ' AND ')
797
- .replace(/\s+OR\s+/gi, ' OR ')
798
- .trim();
799
- return this.normalizeSqlIdentifiers(s);
706
+ extractCheckExpressionFromDefinition(definition) {
707
+ const trimmed = definition.trim();
708
+ const match = trimmed.match(/^CHECK\s*\(([\s\S]*)\)$/i);
709
+ if (!match) {
710
+ return trimmed;
711
+ }
712
+ return match[1];
800
713
  }
801
- normalizeExcludeDef(def) {
802
- const s = def
803
- .replace(/\s+/g, ' ')
804
- .replace(/\s*\(\s*/g, '(')
805
- .replace(/\s*\)\s*/g, ')')
806
- .trim();
807
- return this.normalizeSqlIdentifiers(s);
714
+ quoteIdentifier(identifier) {
715
+ return `"${identifier.replace(/"/g, '""')}"`;
808
716
  }
809
- normalizeTriggerDef(def) {
810
- return def
811
- .replace(/\s+/g, ' ')
812
- .replace(/\s*\(\s*/g, '(')
813
- .replace(/\s*\)\s*/g, ')')
814
- .replace(/\bON\s+[a-zA-Z_][a-zA-Z0-9_]*\./gi, 'ON ')
815
- .trim();
717
+ quoteQualifiedIdentifier(identifier) {
718
+ return identifier
719
+ .split('.')
720
+ .map((part) => this.quoteIdentifier(part))
721
+ .join('.');
816
722
  }
817
723
  /** Escape expression for embedding inside a template literal in generated code */
818
724
  escapeExpressionForRaw(expr) {
819
725
  return expr.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
820
726
  }
821
- addCheckConstraint(table, constraintName, expression, deferrable) {
727
+ addCheckConstraint(table, constraintName, expression) {
822
728
  const escaped = this.escapeExpressionForRaw(expression);
823
- const deferrableClause = deferrable ? ` DEFERRABLE ${deferrable}` : '';
824
- this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})${deferrableClause}\`);`);
729
+ this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`);
825
730
  this.writer.blankLine();
826
731
  }
827
732
  dropCheckConstraint(table, constraintName) {
828
733
  this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
829
734
  this.writer.blankLine();
830
735
  }
831
- buildExcludeDef(entry) {
832
- const elementsStr = entry.elements
833
- .map((el) => ('column' in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`))
834
- .join(', ');
835
- const whereClause = entry.where ? ` WHERE (${entry.where})` : '';
836
- const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
837
- return `EXCLUDE USING ${entry.using} (${elementsStr})${whereClause}${deferrableClause}`;
838
- }
839
- addExcludeConstraint(table, constraintName, entry) {
840
- const def = this.buildExcludeDef(entry);
841
- const escaped = this.escapeExpressionForRaw(def);
842
- this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
843
- this.writer.blankLine();
844
- }
845
- dropExcludeConstraint(table, constraintName) {
846
- this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
847
- this.writer.blankLine();
848
- }
849
- buildConstraintTriggerDef(table, constraintName, entry) {
850
- const eventsStr = entry.events.join(' OR ');
851
- const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
852
- const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(', ') : '';
853
- const executeClause = argsStr
854
- ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})`
855
- : `EXECUTE FUNCTION ${entry.function.name}()`;
856
- return `CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}`;
857
- }
858
- addConstraintTrigger(table, constraintName, entry) {
859
- const eventsStr = entry.events.join(' OR ');
860
- const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
861
- const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(', ') : '';
862
- const executeClause = argsStr
863
- ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})`
864
- : `EXECUTE FUNCTION ${entry.function.name}()`;
865
- this.writer.writeLine(`await knex.raw(\`CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}\`);`);
866
- this.writer.blankLine();
867
- }
868
- dropConstraintTrigger(table, constraintName) {
869
- this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
870
- this.writer.blankLine();
871
- }
872
736
  value(value) {
873
737
  if (typeof value === 'string') {
874
738
  return `'${value}'`;