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