@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.
- package/CHANGELOG.md +3 -3
- package/dist/bin/gqm.cjs +152 -291
- package/dist/cjs/index.cjs +153 -294
- package/dist/esm/migrations/generate-functions.js +0 -2
- package/dist/esm/migrations/generate-functions.js.map +1 -1
- package/dist/esm/migrations/generate.d.ts +8 -18
- package/dist/esm/migrations/generate.js +150 -286
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/migrations/update-functions.js +0 -2
- package/dist/esm/migrations/update-functions.js.map +1 -1
- package/dist/esm/models/model-definitions.d.ts +2 -27
- package/dist/esm/models/models.d.ts +5 -1
- package/dist/esm/models/models.js +1 -4
- package/dist/esm/models/models.js.map +1 -1
- package/dist/esm/models/utils.d.ts +0 -10
- package/dist/esm/models/utils.js +0 -11
- package/dist/esm/models/utils.js.map +1 -1
- package/docs/docs/2-models.md +4 -18
- package/docs/docs/5-migrations.md +7 -9
- package/package.json +1 -1
- package/src/bin/gqm/parse-knexfile.ts +0 -1
- package/src/bin/gqm/settings.ts +0 -4
- package/src/migrations/generate-functions.ts +0 -2
- package/src/migrations/generate.ts +186 -351
- package/src/migrations/update-functions.ts +0 -2
- package/src/models/model-definitions.ts +1 -20
- package/src/models/models.ts +1 -4
- package/src/models/utils.ts +0 -20
- package/tests/unit/constraints.spec.ts +2 -98
- package/tests/unit/migration-constraints.spec.ts +300 -0
|
@@ -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.
|
|
244
|
-
|
|
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.
|
|
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.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
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
|
|
888
|
-
'
|
|
889
|
-
|
|
890
|
-
|
|
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
|
|
774
|
+
return normalized;
|
|
934
775
|
}
|
|
935
776
|
|
|
936
|
-
private
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
|
|
958
|
-
|
|
797
|
+
if (inSingleQuote) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
959
800
|
|
|
960
|
-
|
|
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 (
|
|
803
|
+
} else if (char === ')') {
|
|
969
804
|
depth--;
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
|
814
|
+
return depth === 0;
|
|
980
815
|
}
|
|
981
816
|
|
|
982
|
-
private
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
827
|
+
const preferredExpression = existingMap.get(preferredConstraintName);
|
|
828
|
+
if (preferredExpression !== undefined) {
|
|
829
|
+
return {
|
|
830
|
+
constraintName: preferredConstraintName,
|
|
831
|
+
expression: preferredExpression,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
1008
834
|
|
|
1009
|
-
|
|
1010
|
-
|
|
835
|
+
const normalizedNewExpression = this.normalizeCheckExpression(entry.expression);
|
|
836
|
+
const constraintPrefix = `${table}_${entry.name}_${entry.kind}_`;
|
|
1011
837
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
.
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
846
|
+
return { constraintName, expression };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return null;
|
|
1024
850
|
}
|
|
1025
851
|
|
|
1026
|
-
private
|
|
852
|
+
private async equalExpressions(
|
|
1027
853
|
table: string,
|
|
1028
854
|
constraintName: string,
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
) {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
|
1061
|
-
|
|
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
|
|
1077
|
-
|
|
1078
|
-
|
|
940
|
+
private quoteQualifiedIdentifier(identifier: string): string {
|
|
941
|
+
return identifier
|
|
942
|
+
.split('.')
|
|
943
|
+
.map((part) => this.quoteIdentifier(part))
|
|
944
|
+
.join('.');
|
|
1079
945
|
}
|
|
1080
946
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
|
1103
|
-
|
|
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(\`
|
|
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
|
|
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
|
|