@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
|
@@ -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,
|
|
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 -> exclude definition (normalized) */
|
|
20
|
-
existingExcludeConstraints = {};
|
|
21
|
-
/** table name -> constraint name -> trigger definition (normalized) */
|
|
22
|
-
existingConstraintTriggers = {};
|
|
23
19
|
uuidUsed;
|
|
24
20
|
nowUsed;
|
|
25
21
|
needsMigration = false;
|
|
@@ -52,48 +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, this.normalizeExcludeDef(row.constraint_def));
|
|
68
|
-
}
|
|
69
|
-
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
|
|
70
|
-
FROM pg_constraint c
|
|
71
|
-
JOIN pg_trigger t ON t.tgconstraint = c.oid
|
|
72
|
-
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
73
|
-
WHERE n.nspname = 'public' AND c.contype = 't'`);
|
|
74
|
-
const triggerRows = 'rows' in triggerResult && Array.isArray(triggerResult.rows)
|
|
75
|
-
? triggerResult.rows
|
|
76
|
-
: [];
|
|
77
|
-
for (const row of triggerRows) {
|
|
78
|
-
const tableName = row.table_name.split('.').pop()?.replace(/^"|"$/g, '') ?? row.table_name;
|
|
79
|
-
if (!this.existingConstraintTriggers[tableName]) {
|
|
80
|
-
this.existingConstraintTriggers[tableName] = new Map();
|
|
81
|
-
}
|
|
82
|
-
this.existingConstraintTriggers[tableName].set(row.constraint_name, this.normalizeTriggerDef(row.trigger_def));
|
|
83
|
-
}
|
|
84
51
|
const up = [];
|
|
85
52
|
const down = [];
|
|
86
|
-
const wantsBtreeGist = models.entities.some((model) => model.constraints?.some((c) => c.kind === 'exclude' && c.elements.some((el) => 'column' in el && el.operator === '=')));
|
|
87
|
-
if (wantsBtreeGist) {
|
|
88
|
-
const extResult = await schema.knex('pg_extension').where('extname', 'btree_gist').select('oid').first();
|
|
89
|
-
const btreeGistInstalled = !!extResult;
|
|
90
|
-
if (!btreeGistInstalled) {
|
|
91
|
-
up.unshift(() => {
|
|
92
|
-
this.writer.writeLine(`await knex.raw('CREATE EXTENSION IF NOT EXISTS btree_gist');`);
|
|
93
|
-
this.writer.blankLine();
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
53
|
this.createEnums(this.models.enums.filter((enm) => !enums.includes(lowerFirst(enm.name))), up, down);
|
|
98
54
|
await this.handleFunctions(up, down);
|
|
99
55
|
for (const model of models.entities) {
|
|
@@ -173,24 +129,10 @@ export class MigrationGenerator {
|
|
|
173
129
|
if (entry.kind === 'check') {
|
|
174
130
|
validateCheckConstraint(model, entry);
|
|
175
131
|
const table = model.name;
|
|
176
|
-
const constraintName = this.
|
|
177
|
-
|
|
178
|
-
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
else if (entry.kind === 'exclude') {
|
|
182
|
-
validateExcludeConstraint(model, entry);
|
|
183
|
-
const table = model.name;
|
|
184
|
-
const constraintName = this.getConstraintName(model, entry, i);
|
|
185
|
-
up.push(() => {
|
|
186
|
-
this.addExcludeConstraint(table, constraintName, entry);
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
else if (entry.kind === 'constraint_trigger') {
|
|
190
|
-
const table = model.name;
|
|
191
|
-
const constraintName = this.getConstraintName(model, entry, i);
|
|
132
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
133
|
+
const expression = entry.expression;
|
|
192
134
|
up.push(() => {
|
|
193
|
-
this.
|
|
135
|
+
this.addCheckConstraint(table, constraintName, expression);
|
|
194
136
|
});
|
|
195
137
|
}
|
|
196
138
|
}
|
|
@@ -229,84 +171,32 @@ export class MigrationGenerator {
|
|
|
229
171
|
const existingFields = model.fields.filter((field) => (!field.generateAs || field.generateAs.type === 'expression') && this.hasChanged(model, field));
|
|
230
172
|
this.updateFields(model, existingFields, up, down);
|
|
231
173
|
if (model.constraints?.length) {
|
|
232
|
-
const existingCheckMap = this.existingCheckConstraints[model.name];
|
|
233
|
-
const existingExcludeMap = this.existingExcludeConstraints[model.name];
|
|
234
|
-
const existingTriggerMap = this.existingConstraintTriggers[model.name];
|
|
235
174
|
for (let i = 0; i < model.constraints.length; i++) {
|
|
236
175
|
const entry = model.constraints[i];
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (entry.kind === 'check') {
|
|
240
|
-
validateCheckConstraint(model, entry);
|
|
241
|
-
const newExpression = entry.expression;
|
|
242
|
-
const existingExpression = existingCheckMap?.get(constraintName);
|
|
243
|
-
if (existingExpression === undefined) {
|
|
244
|
-
up.push(() => {
|
|
245
|
-
this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
|
|
246
|
-
});
|
|
247
|
-
down.push(() => {
|
|
248
|
-
this.dropCheckConstraint(table, constraintName);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
else if (this.normalizeCheckExpression(existingExpression) !== this.normalizeCheckExpression(newExpression)) {
|
|
252
|
-
up.push(() => {
|
|
253
|
-
this.dropCheckConstraint(table, constraintName);
|
|
254
|
-
this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
|
|
255
|
-
});
|
|
256
|
-
down.push(() => {
|
|
257
|
-
this.dropCheckConstraint(table, constraintName);
|
|
258
|
-
this.addCheckConstraint(table, constraintName, existingExpression);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
176
|
+
if (entry.kind !== 'check') {
|
|
177
|
+
continue;
|
|
261
178
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
else if (existingDef !== newDef) {
|
|
275
|
-
up.push(() => {
|
|
276
|
-
this.dropExcludeConstraint(table, constraintName);
|
|
277
|
-
this.addExcludeConstraint(table, constraintName, entry);
|
|
278
|
-
});
|
|
279
|
-
down.push(() => {
|
|
280
|
-
this.dropExcludeConstraint(table, constraintName);
|
|
281
|
-
const escaped = this.escapeExpressionForRaw(existingDef);
|
|
282
|
-
this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
|
|
283
|
-
this.writer.blankLine();
|
|
284
|
-
});
|
|
285
|
-
}
|
|
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
|
+
});
|
|
286
190
|
}
|
|
287
|
-
else if (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
else if (existingDef !== newDef) {
|
|
299
|
-
up.push(() => {
|
|
300
|
-
this.dropConstraintTrigger(table, constraintName);
|
|
301
|
-
this.addConstraintTrigger(table, constraintName, entry);
|
|
302
|
-
});
|
|
303
|
-
down.push(() => {
|
|
304
|
-
this.dropConstraintTrigger(table, constraintName);
|
|
305
|
-
const escaped = existingDef.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
306
|
-
this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
|
|
307
|
-
this.writer.blankLine();
|
|
308
|
-
});
|
|
309
|
-
}
|
|
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
|
+
});
|
|
310
200
|
}
|
|
311
201
|
}
|
|
312
202
|
}
|
|
@@ -689,147 +579,160 @@ export class MigrationGenerator {
|
|
|
689
579
|
renameColumn(from, to) {
|
|
690
580
|
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
691
581
|
}
|
|
692
|
-
|
|
582
|
+
getCheckConstraintName(model, entry, index) {
|
|
693
583
|
return `${model.name}_${entry.name}_${entry.kind}_${index}`;
|
|
694
584
|
}
|
|
695
|
-
|
|
696
|
-
'
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
'in',
|
|
700
|
-
'is',
|
|
701
|
-
'null',
|
|
702
|
-
'true',
|
|
703
|
-
'false',
|
|
704
|
-
'between',
|
|
705
|
-
'like',
|
|
706
|
-
'exists',
|
|
707
|
-
'all',
|
|
708
|
-
'any',
|
|
709
|
-
'asc',
|
|
710
|
-
'desc',
|
|
711
|
-
'with',
|
|
712
|
-
'using',
|
|
713
|
-
'as',
|
|
714
|
-
'on',
|
|
715
|
-
'infinity',
|
|
716
|
-
'extract',
|
|
717
|
-
'current_date',
|
|
718
|
-
'current_timestamp',
|
|
719
|
-
]);
|
|
720
|
-
normalizeSqlIdentifiers(s) {
|
|
721
|
-
const literals = [];
|
|
722
|
-
let result = s.replace(/'([^']|'')*'/g, (lit) => {
|
|
723
|
-
literals.push(lit);
|
|
724
|
-
return `\x00L${literals.length - 1}\x00`;
|
|
725
|
-
});
|
|
726
|
-
result = result.replace(/"([^"]*)"/g, (_, ident) => `"${ident.toLowerCase()}"`);
|
|
727
|
-
result = result.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match) => MigrationGenerator.SQL_KEYWORDS.has(match.toLowerCase()) ? match : `"${match.toLowerCase()}"`);
|
|
728
|
-
for (let i = 0; i < literals.length; i++) {
|
|
729
|
-
result = result.replace(new RegExp(`\x00L${i}\x00`, '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();
|
|
730
589
|
}
|
|
731
|
-
return
|
|
590
|
+
return normalized;
|
|
732
591
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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;
|
|
741
605
|
}
|
|
742
|
-
|
|
743
|
-
|
|
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;
|
|
744
619
|
}
|
|
745
|
-
if (depth
|
|
746
|
-
|
|
747
|
-
break;
|
|
620
|
+
if (depth < 0) {
|
|
621
|
+
return false;
|
|
748
622
|
}
|
|
749
623
|
}
|
|
750
|
-
|
|
751
|
-
|
|
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;
|
|
752
647
|
}
|
|
753
|
-
|
|
648
|
+
return { constraintName, expression };
|
|
754
649
|
}
|
|
755
|
-
|
|
756
|
-
.replace(/\s*\(\s*/g, '(')
|
|
757
|
-
.replace(/\s*\)\s*/g, ')')
|
|
758
|
-
.replace(/\s+AND\s+/gi, ' AND ')
|
|
759
|
-
.replace(/\s+OR\s+/gi, ' OR ')
|
|
760
|
-
.trim();
|
|
761
|
-
return this.normalizeSqlIdentifiers(s);
|
|
650
|
+
return null;
|
|
762
651
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
+
}
|
|
770
664
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
.
|
|
774
|
-
.
|
|
775
|
-
.
|
|
776
|
-
|
|
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}`);
|
|
694
|
+
}
|
|
695
|
+
return this.normalizeCheckExpression(this.extractCheckExpressionFromDefinition(definition));
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
try {
|
|
699
|
+
await trx.rollback();
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
// no-op: transaction may already be closed by driver after failure
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
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];
|
|
713
|
+
}
|
|
714
|
+
quoteIdentifier(identifier) {
|
|
715
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
716
|
+
}
|
|
717
|
+
quoteQualifiedIdentifier(identifier) {
|
|
718
|
+
return identifier
|
|
719
|
+
.split('.')
|
|
720
|
+
.map((part) => this.quoteIdentifier(part))
|
|
721
|
+
.join('.');
|
|
777
722
|
}
|
|
778
723
|
/** Escape expression for embedding inside a template literal in generated code */
|
|
779
724
|
escapeExpressionForRaw(expr) {
|
|
780
725
|
return expr.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
781
726
|
}
|
|
782
|
-
addCheckConstraint(table, constraintName, expression
|
|
727
|
+
addCheckConstraint(table, constraintName, expression) {
|
|
783
728
|
const escaped = this.escapeExpressionForRaw(expression);
|
|
784
|
-
|
|
785
|
-
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})\`);`);
|
|
786
730
|
this.writer.blankLine();
|
|
787
731
|
}
|
|
788
732
|
dropCheckConstraint(table, constraintName) {
|
|
789
733
|
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
790
734
|
this.writer.blankLine();
|
|
791
735
|
}
|
|
792
|
-
buildExcludeDef(entry) {
|
|
793
|
-
const elementsStr = entry.elements
|
|
794
|
-
.map((el) => ('column' in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`))
|
|
795
|
-
.join(', ');
|
|
796
|
-
const whereClause = entry.where ? ` WHERE (${entry.where})` : '';
|
|
797
|
-
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
|
|
798
|
-
return `EXCLUDE USING ${entry.using} (${elementsStr})${whereClause}${deferrableClause}`;
|
|
799
|
-
}
|
|
800
|
-
addExcludeConstraint(table, constraintName, entry) {
|
|
801
|
-
const def = this.buildExcludeDef(entry);
|
|
802
|
-
const escaped = this.escapeExpressionForRaw(def);
|
|
803
|
-
this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
|
|
804
|
-
this.writer.blankLine();
|
|
805
|
-
}
|
|
806
|
-
dropExcludeConstraint(table, constraintName) {
|
|
807
|
-
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
808
|
-
this.writer.blankLine();
|
|
809
|
-
}
|
|
810
|
-
buildConstraintTriggerDef(table, constraintName, entry) {
|
|
811
|
-
const eventsStr = entry.events.join(' OR ');
|
|
812
|
-
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
|
|
813
|
-
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(', ') : '';
|
|
814
|
-
const executeClause = argsStr
|
|
815
|
-
? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})`
|
|
816
|
-
: `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
817
|
-
return `CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}`;
|
|
818
|
-
}
|
|
819
|
-
addConstraintTrigger(table, constraintName, entry) {
|
|
820
|
-
const eventsStr = entry.events.join(' OR ');
|
|
821
|
-
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : '';
|
|
822
|
-
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(', ') : '';
|
|
823
|
-
const executeClause = argsStr
|
|
824
|
-
? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})`
|
|
825
|
-
: `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
826
|
-
this.writer.writeLine(`await knex.raw(\`CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}\`);`);
|
|
827
|
-
this.writer.blankLine();
|
|
828
|
-
}
|
|
829
|
-
dropConstraintTrigger(table, constraintName) {
|
|
830
|
-
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
831
|
-
this.writer.blankLine();
|
|
832
|
-
}
|
|
833
736
|
value(value) {
|
|
834
737
|
if (typeof value === 'string') {
|
|
835
738
|
return `'${value}'`;
|