@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.
@@ -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 -> { normalized, raw } */
46
- private existingExcludeConstraints: Record<string, Map<string, { normalized: string; raw: string }>> = {};
47
- /** table name -> constraint name -> { normalized, raw } */
48
- private existingConstraintTriggers: Record<string, Map<string, { normalized: string; raw: string }>> = {};
49
44
  private uuidUsed?: boolean;
50
45
  private nowUsed?: boolean;
51
46
  public needsMigration = false;
@@ -88,66 +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, {
107
- normalized: this.normalizeExcludeDef(row.constraint_def),
108
- raw: row.constraint_def,
109
- });
110
- }
111
-
112
- const triggerResult = await schema.knex.raw(
113
- `SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_triggerdef(t.oid) as trigger_def
114
- FROM pg_constraint c
115
- JOIN pg_trigger t ON t.tgconstraint = c.oid
116
- JOIN pg_namespace n ON c.connamespace = n.oid
117
- WHERE n.nspname = 'public' AND c.contype = 't'`,
118
- );
119
- const triggerRows: { table_name: string; constraint_name: string; trigger_def: string }[] =
120
- 'rows' in triggerResult && Array.isArray((triggerResult as { rows: unknown }).rows)
121
- ? (triggerResult as { rows: { table_name: string; constraint_name: string; trigger_def: string }[] }).rows
122
- : [];
123
- for (const row of triggerRows) {
124
- const tableName = row.table_name.split('.').pop()?.replace(/^"|"$/g, '') ?? row.table_name;
125
- if (!this.existingConstraintTriggers[tableName]) {
126
- this.existingConstraintTriggers[tableName] = new Map();
127
- }
128
- this.existingConstraintTriggers[tableName].set(row.constraint_name, {
129
- normalized: this.normalizeTriggerDef(row.trigger_def),
130
- raw: row.trigger_def,
131
- });
132
- }
133
-
134
86
  const up: Callbacks = [];
135
87
  const down: Callbacks = [];
136
88
 
137
- const wantsBtreeGist = models.entities.some((model) =>
138
- model.constraints?.some((c) => c.kind === 'exclude' && c.elements.some((el) => 'column' in el && el.operator === '=')),
139
- );
140
- if (wantsBtreeGist) {
141
- const extResult = await schema.knex('pg_extension').where('extname', 'btree_gist').select('oid').first();
142
- const btreeGistInstalled = !!extResult;
143
- if (!btreeGistInstalled) {
144
- up.unshift(() => {
145
- this.writer.writeLine(`await knex.raw('CREATE EXTENSION IF NOT EXISTS btree_gist');`);
146
- this.writer.blankLine();
147
- });
148
- }
149
- }
150
-
151
89
  this.createEnums(
152
90
  this.models.enums.filter((enm) => !enums.includes(lowerFirst(enm.name))),
153
91
  up,
@@ -240,22 +178,10 @@ export class MigrationGenerator {
240
178
  if (entry.kind === 'check') {
241
179
  validateCheckConstraint(model, entry);
242
180
  const table = model.name;
243
- const constraintName = this.getConstraintName(model, entry, i);
244
- up.push(() => {
245
- this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
246
- });
247
- } else if (entry.kind === 'exclude') {
248
- validateExcludeConstraint(model, entry);
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.addExcludeConstraint(table, constraintName, entry);
253
- });
254
- } else if (entry.kind === 'constraint_trigger') {
255
- const table = model.name;
256
- const constraintName = this.getConstraintName(model, entry, i);
257
- up.push(() => {
258
- this.addConstraintTrigger(table, constraintName, entry);
184
+ this.addCheckConstraint(table, constraintName, expression);
259
185
  });
260
186
  }
261
187
  }
@@ -311,83 +237,38 @@ export class MigrationGenerator {
311
237
  this.updateFields(model, existingFields, up, down);
312
238
 
313
239
  if (model.constraints?.length) {
314
- const existingCheckMap = this.existingCheckConstraints[model.name];
315
- const existingExcludeMap = this.existingExcludeConstraints[model.name];
316
- const existingTriggerMap = this.existingConstraintTriggers[model.name];
317
240
  for (let i = 0; i < model.constraints.length; i++) {
318
241
  const entry = model.constraints[i];
242
+ if (entry.kind !== 'check') {
243
+ continue;
244
+ }
245
+ validateCheckConstraint(model, entry);
319
246
  const table = model.name;
320
- const constraintName = this.getConstraintName(model, entry, i);
321
- if (entry.kind === 'check') {
322
- validateCheckConstraint(model, entry);
323
- const newExpression = entry.expression;
324
- const existingExpression = existingCheckMap?.get(constraintName);
325
- if (existingExpression === undefined) {
326
- up.push(() => {
327
- this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
328
- });
329
- down.push(() => {
330
- this.dropCheckConstraint(table, constraintName);
331
- });
332
- } else if (
333
- this.normalizeCheckExpression(existingExpression) !== this.normalizeCheckExpression(newExpression)
334
- ) {
335
- up.push(() => {
336
- this.dropCheckConstraint(table, constraintName);
337
- this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
338
- });
339
- down.push(() => {
340
- this.dropCheckConstraint(table, constraintName);
341
- this.addCheckConstraint(table, constraintName, existingExpression);
342
- });
343
- }
344
- } else if (entry.kind === 'exclude') {
345
- validateExcludeConstraint(model, entry);
346
- const newDef = this.normalizeExcludeDef(this.buildExcludeDef(entry));
347
- const existing = existingExcludeMap?.get(constraintName);
348
- if (existing === undefined) {
349
- up.push(() => {
350
- this.addExcludeConstraint(table, constraintName, entry);
351
- });
352
- down.push(() => {
353
- this.dropExcludeConstraint(table, constraintName);
354
- });
355
- } else if (existing.normalized !== newDef) {
356
- up.push(() => {
357
- this.dropExcludeConstraint(table, constraintName);
358
- this.addExcludeConstraint(table, constraintName, entry);
359
- });
360
- down.push(() => {
361
- this.dropExcludeConstraint(table, constraintName);
362
- const escaped = this.escapeExpressionForRaw(existing.raw);
363
- this.writer.writeLine(
364
- `await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`,
365
- );
366
- this.writer.blankLine();
367
- });
368
- }
369
- } else if (entry.kind === 'constraint_trigger') {
370
- const newDef = this.normalizeTriggerDef(this.buildConstraintTriggerDef(table, constraintName, entry));
371
- const existing = existingTriggerMap?.get(constraintName);
372
- if (existing === undefined) {
373
- up.push(() => {
374
- this.addConstraintTrigger(table, constraintName, entry);
375
- });
376
- down.push(() => {
377
- this.dropConstraintTrigger(table, constraintName);
378
- });
379
- } else if (existing.normalized !== newDef) {
380
- up.push(() => {
381
- this.dropConstraintTrigger(table, constraintName);
382
- this.addConstraintTrigger(table, constraintName, entry);
383
- });
384
- down.push(() => {
385
- this.dropConstraintTrigger(table, constraintName);
386
- const escaped = existing.raw.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
387
- this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
388
- this.writer.blankLine();
389
- });
390
- }
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
+ });
391
272
  }
392
273
  }
393
274
  }
@@ -880,249 +761,203 @@ export class MigrationGenerator {
880
761
  this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
881
762
  }
882
763
 
883
- 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 {
884
765
  return `${model.name}_${entry.name}_${entry.kind}_${index}`;
885
766
  }
886
767
 
887
- private static readonly SQL_KEYWORDS = new Set([
888
- 'and',
889
- 'or',
890
- 'not',
891
- 'in',
892
- 'is',
893
- 'null',
894
- 'true',
895
- 'false',
896
- 'between',
897
- 'like',
898
- 'exists',
899
- 'all',
900
- 'any',
901
- 'asc',
902
- 'desc',
903
- 'with',
904
- 'using',
905
- 'as',
906
- 'on',
907
- 'infinity',
908
- 'extract',
909
- 'current_date',
910
- 'current_timestamp',
911
- ]);
912
-
913
- private static readonly LITERAL_PLACEHOLDER = '\uE000';
914
-
915
- private normalizeSqlIdentifiers(s: string): string {
916
- const literals: string[] = [];
917
- let result = s.replace(/'([^']|'')*'/g, (lit) => {
918
- literals.push(lit);
919
-
920
- return `${MigrationGenerator.LITERAL_PLACEHOLDER}${literals.length - 1}${MigrationGenerator.LITERAL_PLACEHOLDER}`;
921
- });
922
- result = result.replace(/"([^"]*)"/g, (_, ident) => `"${ident.toLowerCase()}"`);
923
- result = result.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match) =>
924
- MigrationGenerator.SQL_KEYWORDS.has(match.toLowerCase()) ? match : `"${match.toLowerCase()}"`,
925
- );
926
- for (let i = 0; i < literals.length; i++) {
927
- result = result.replace(
928
- new RegExp(`${MigrationGenerator.LITERAL_PLACEHOLDER}${i}${MigrationGenerator.LITERAL_PLACEHOLDER}`, 'g'),
929
- literals[i],
930
- );
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();
931
772
  }
932
773
 
933
- return result;
774
+ return normalized;
934
775
  }
935
776
 
936
- private stripOuterParens(s: string): string {
937
- while (s.length >= 2 && s.startsWith('(') && s.endsWith(')')) {
938
- let depth = 0;
939
- let match = true;
940
- for (let i = 0; i < s.length; i++) {
941
- if (s[i] === '(') {
942
- depth++;
943
- } else if (s[i] === ')') {
944
- depth--;
945
- }
946
- if (depth === 0 && i < s.length - 1) {
947
- match = false;
948
- 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;
949
792
  }
793
+ inSingleQuote = !inSingleQuote;
794
+ continue;
950
795
  }
951
- if (!match || depth !== 0) {
952
- break;
953
- }
954
- s = s.slice(1, -1).trim();
955
- }
956
796
 
957
- return s;
958
- }
797
+ if (inSingleQuote) {
798
+ continue;
799
+ }
959
800
 
960
- private splitAtTopLevel(s: string, sep: string): string[] {
961
- const parts: string[] = [];
962
- let depth = 0;
963
- let start = 0;
964
- const sepLen = sep.length;
965
- for (let i = 0; i <= s.length - sepLen; i++) {
966
- if (s[i] === '(') {
801
+ if (char === '(') {
967
802
  depth++;
968
- } else if (s[i] === ')') {
803
+ } else if (char === ')') {
969
804
  depth--;
970
- }
971
- if (depth === 0 && s.slice(i, i + sepLen).toLowerCase() === sep.toLowerCase()) {
972
- parts.push(s.slice(start, i).trim());
973
- start = i + sepLen;
974
- i += sepLen - 1;
805
+ if (depth === 0 && i !== expr.length - 1) {
806
+ return false;
807
+ }
808
+ if (depth < 0) {
809
+ return false;
810
+ }
975
811
  }
976
812
  }
977
- parts.push(s.slice(start).trim());
978
813
 
979
- return parts.filter(Boolean);
814
+ return depth === 0;
980
815
  }
981
816
 
982
- private normalizeCheckExpression(expr: string): string {
983
- let s = expr.replace(/\s+/g, ' ').trim();
984
- const normalizeParts = (str: string, separator: string): string =>
985
- this.splitAtTopLevel(str, separator)
986
- .map((p) => this.stripOuterParens(p))
987
- .join(separator);
988
- s = this.stripOuterParens(s);
989
- s = normalizeParts(s, ' OR ');
990
- s = this.stripOuterParens(s);
991
- s = normalizeParts(s, ' AND ');
992
- s = s
993
- .replace(/\s*\(\s*/g, '(')
994
- .replace(/\s*\)\s*/g, ')')
995
- .replace(/\s+AND\s+/gi, ' AND ')
996
- .replace(/\s+OR\s+/gi, ' OR ')
997
- .trim();
998
-
999
- return this.normalizeSqlIdentifiers(s);
1000
- }
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
+ }
1001
826
 
1002
- private normalizeExcludeDef(def: string): string {
1003
- const s = def
1004
- .replace(/\s+/g, ' ')
1005
- .replace(/\s*\(\s*/g, '(')
1006
- .replace(/\s*\)\s*/g, ')')
1007
- .trim();
827
+ const preferredExpression = existingMap.get(preferredConstraintName);
828
+ if (preferredExpression !== undefined) {
829
+ return {
830
+ constraintName: preferredConstraintName,
831
+ expression: preferredExpression,
832
+ };
833
+ }
1008
834
 
1009
- return this.normalizeSqlIdentifiers(s);
1010
- }
835
+ const normalizedNewExpression = this.normalizeCheckExpression(entry.expression);
836
+ const constraintPrefix = `${table}_${entry.name}_${entry.kind}_`;
1011
837
 
1012
- private normalizeTriggerDef(def: string): string {
1013
- return def
1014
- .replace(/\s+/g, ' ')
1015
- .replace(/\s*\(\s*/g, '(')
1016
- .replace(/\s*\)\s*/g, ')')
1017
- .replace(/\bON\s+[a-zA-Z_][a-zA-Z0-9_]*\./gi, 'ON ')
1018
- .trim();
1019
- }
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
+ }
1020
845
 
1021
- /** Escape expression for embedding inside a template literal in generated code */
1022
- private escapeExpressionForRaw(expr: string): string {
1023
- return expr.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
846
+ return { constraintName, expression };
847
+ }
848
+
849
+ return null;
1024
850
  }
1025
851
 
1026
- private addCheckConstraint(
852
+ private async equalExpressions(
1027
853
  table: string,
1028
854
  constraintName: string,
1029
- expression: string,
1030
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE',
1031
- ) {
1032
- const escaped = this.escapeExpressionForRaw(expression);
1033
- const deferrableClause = deferrable ? ` DEFERRABLE ${deferrable}` : '';
1034
- this.writer.writeLine(
1035
- `await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})${deferrableClause}\`);`,
1036
- );
1037
- 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
+ }
1038
873
  }
1039
874
 
1040
- private dropCheckConstraint(table: string, constraintName: string) {
1041
- this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
1042
- 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
+ }
1043
924
  }
1044
925
 
1045
- private buildExcludeDef(entry: {
1046
- using: string;
1047
- elements: ({ column: string; operator: string } | { expression: string; operator: string })[];
1048
- where?: string;
1049
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
1050
- }): string {
1051
- const elementsStr = entry.elements
1052
- .map((el) => ('column' in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`))
1053
- .join(', ');
1054
- const whereClause = entry.where ? ` WHERE (${entry.where})` : '';
1055
- const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
1056
-
1057
- 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];
1058
934
  }
1059
935
 
1060
- private addExcludeConstraint(
1061
- table: string,
1062
- constraintName: string,
1063
- entry: {
1064
- using: string;
1065
- elements: ({ column: string; operator: string } | { expression: string; operator: string })[];
1066
- where?: string;
1067
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
1068
- },
1069
- ) {
1070
- const def = this.buildExcludeDef(entry);
1071
- const escaped = this.escapeExpressionForRaw(def);
1072
- this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
1073
- this.writer.blankLine();
936
+ private quoteIdentifier(identifier: string): string {
937
+ return `"${identifier.replace(/"/g, '""')}"`;
1074
938
  }
1075
939
 
1076
- private dropExcludeConstraint(table: string, constraintName: string) {
1077
- this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
1078
- this.writer.blankLine();
940
+ private quoteQualifiedIdentifier(identifier: string): string {
941
+ return identifier
942
+ .split('.')
943
+ .map((part) => this.quoteIdentifier(part))
944
+ .join('.');
1079
945
  }
1080
946
 
1081
- private buildConstraintTriggerDef(
1082
- table: string,
1083
- constraintName: string,
1084
- entry: {
1085
- when: 'AFTER' | 'BEFORE';
1086
- events: ('INSERT' | 'UPDATE')[];
1087
- forEach: 'ROW' | 'STATEMENT';
1088
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
1089
- function: { name: string; args?: string[] };
1090
- },
1091
- ): string {
1092
- const eventsStr = entry.events.join(' OR ');
1093
- const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
1094
- const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(', ') : '';
1095
- const executeClause = argsStr
1096
- ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})`
1097
- : `EXECUTE FUNCTION ${entry.function.name}()`;
1098
-
1099
- 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, '\\$');
1100
950
  }
1101
951
 
1102
- private addConstraintTrigger(
1103
- table: string,
1104
- constraintName: string,
1105
- entry: {
1106
- when: 'AFTER' | 'BEFORE';
1107
- events: ('INSERT' | 'UPDATE')[];
1108
- forEach: 'ROW' | 'STATEMENT';
1109
- deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
1110
- function: { name: string; args?: string[] };
1111
- },
1112
- ) {
1113
- const eventsStr = entry.events.join(' OR ');
1114
- const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
1115
- const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(', ') : '';
1116
- const executeClause = argsStr
1117
- ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})`
1118
- : `EXECUTE FUNCTION ${entry.function.name}()`;
952
+ private addCheckConstraint(table: string, constraintName: string, expression: string) {
953
+ const escaped = this.escapeExpressionForRaw(expression);
1119
954
  this.writer.writeLine(
1120
- `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})\`);`,
1121
956
  );
1122
957
  this.writer.blankLine();
1123
958
  }
1124
959
 
1125
- private dropConstraintTrigger(table: string, constraintName: string) {
960
+ private dropCheckConstraint(table: string, constraintName: string) {
1126
961
  this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
1127
962
  this.writer.blankLine();
1128
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