@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.
- package/CHANGELOG.md +3 -3
- package/dist/bin/gqm.cjs +155 -256
- package/dist/cjs/index.cjs +155 -258
- 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 -15
- package/dist/esm/migrations/generate.js +156 -253
- 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 +191 -310
- 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
|
@@ -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.
|
|
238
|
-
|
|
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.
|
|
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.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
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
|
|
882
|
-
'
|
|
883
|
-
|
|
884
|
-
|
|
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
|
|
774
|
+
return normalized;
|
|
923
775
|
}
|
|
924
776
|
|
|
925
|
-
private
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
942
|
-
|
|
796
|
+
|
|
797
|
+
if (inSingleQuote) {
|
|
798
|
+
continue;
|
|
943
799
|
}
|
|
944
|
-
s = s.slice(1, -1).trim();
|
|
945
|
-
}
|
|
946
800
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
|
814
|
+
return depth === 0;
|
|
955
815
|
}
|
|
956
816
|
|
|
957
|
-
private
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
965
|
-
|
|
827
|
+
const preferredExpression = existingMap.get(preferredConstraintName);
|
|
828
|
+
if (preferredExpression !== undefined) {
|
|
829
|
+
return {
|
|
830
|
+
constraintName: preferredConstraintName,
|
|
831
|
+
expression: preferredExpression,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
966
834
|
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
|
852
|
+
private async equalExpressions(
|
|
981
853
|
table: string,
|
|
982
854
|
constraintName: string,
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
|
995
|
-
|
|
996
|
-
|
|
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
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
|
1015
|
-
|
|
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
|
|
1031
|
-
|
|
1032
|
-
|
|
940
|
+
private quoteQualifiedIdentifier(identifier: string): string {
|
|
941
|
+
return identifier
|
|
942
|
+
.split('.')
|
|
943
|
+
.map((part) => this.quoteIdentifier(part))
|
|
944
|
+
.join('.');
|
|
1033
945
|
}
|
|
1034
946
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
|
1057
|
-
|
|
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(\`
|
|
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
|
|
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;
|