@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
package/src/models/models.ts
CHANGED
|
@@ -40,7 +40,6 @@ import {
|
|
|
40
40
|
summonByName,
|
|
41
41
|
typeToField,
|
|
42
42
|
validateCheckConstraint,
|
|
43
|
-
validateExcludeConstraint,
|
|
44
43
|
} from './utils';
|
|
45
44
|
|
|
46
45
|
// These might one day become classes
|
|
@@ -358,7 +357,7 @@ export class EntityModel extends Model {
|
|
|
358
357
|
defaultOrderBy?: OrderBy[];
|
|
359
358
|
fields: EntityField[];
|
|
360
359
|
|
|
361
|
-
constraints?:
|
|
360
|
+
constraints?: { kind: 'check'; name: string; expression: string }[];
|
|
362
361
|
|
|
363
362
|
// temporary fields for the generation of migrations
|
|
364
363
|
deleted?: true;
|
|
@@ -393,8 +392,6 @@ export class EntityModel extends Model {
|
|
|
393
392
|
for (const constraint of this.constraints) {
|
|
394
393
|
if (constraint.kind === 'check') {
|
|
395
394
|
validateCheckConstraint(this, constraint);
|
|
396
|
-
} else if (constraint.kind === 'exclude') {
|
|
397
|
-
validateExcludeConstraint(this, constraint);
|
|
398
395
|
}
|
|
399
396
|
}
|
|
400
397
|
}
|
package/src/models/utils.ts
CHANGED
|
@@ -259,23 +259,3 @@ export const validateCheckConstraint = (model: EntityModel, constraint: { name:
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
};
|
|
262
|
-
|
|
263
|
-
export const validateExcludeConstraint = (
|
|
264
|
-
model: EntityModel,
|
|
265
|
-
constraint: {
|
|
266
|
-
name: string;
|
|
267
|
-
elements: ({ column: string; operator: string } | { expression: string; operator: string })[];
|
|
268
|
-
},
|
|
269
|
-
): void => {
|
|
270
|
-
const validColumnNames = new Set(model.fields.map((f) => getColumnName(f)));
|
|
271
|
-
for (const el of constraint.elements) {
|
|
272
|
-
if ('column' in el) {
|
|
273
|
-
if (!validColumnNames.has(el.column)) {
|
|
274
|
-
const validList = [...validColumnNames].sort().join(', ');
|
|
275
|
-
throw new Error(
|
|
276
|
-
`Exclude constraint "${constraint.name}" references column "${el.column}" which does not exist on model ${model.name}. Valid columns: ${validList}`,
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
};
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { ModelDefinitions, Models } from '../../src/models';
|
|
2
|
-
import {
|
|
3
|
-
extractColumnReferencesFromCheckExpression,
|
|
4
|
-
validateCheckConstraint,
|
|
5
|
-
validateExcludeConstraint,
|
|
6
|
-
} from '../../src/models/utils';
|
|
2
|
+
import { extractColumnReferencesFromCheckExpression, validateCheckConstraint } from '../../src/models/utils';
|
|
7
3
|
|
|
8
|
-
describe('constraints', () => {
|
|
4
|
+
describe('check constraints', () => {
|
|
9
5
|
const modelDefinitions: ModelDefinitions = [
|
|
10
6
|
{
|
|
11
7
|
kind: 'entity',
|
|
@@ -84,96 +80,4 @@ describe('constraints', () => {
|
|
|
84
80
|
).toThrow(/Valid columns:.*\bscore\b.*\bstatus\b/);
|
|
85
81
|
});
|
|
86
82
|
});
|
|
87
|
-
|
|
88
|
-
describe('validateExcludeConstraint', () => {
|
|
89
|
-
it('does not throw when column elements reference valid columns', () => {
|
|
90
|
-
expect(() =>
|
|
91
|
-
validateExcludeConstraint(productModel, {
|
|
92
|
-
name: 'valid',
|
|
93
|
-
elements: [
|
|
94
|
-
{ column: 'score', operator: '=' },
|
|
95
|
-
{ expression: 'tsrange("startDate", "endDate")', operator: '&&' },
|
|
96
|
-
],
|
|
97
|
-
}),
|
|
98
|
-
).not.toThrow();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('does not throw when only expression elements', () => {
|
|
102
|
-
expect(() =>
|
|
103
|
-
validateExcludeConstraint(productModel, {
|
|
104
|
-
name: 'valid',
|
|
105
|
-
elements: [{ expression: 'tsrange(now(), now())', operator: '&&' }],
|
|
106
|
-
}),
|
|
107
|
-
).not.toThrow();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('uses relation column name when validating', () => {
|
|
111
|
-
expect(() =>
|
|
112
|
-
validateExcludeConstraint(productModel, {
|
|
113
|
-
name: 'valid',
|
|
114
|
-
elements: [{ column: 'parentId', operator: '=' }],
|
|
115
|
-
}),
|
|
116
|
-
).not.toThrow();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('throws when column element references missing column', () => {
|
|
120
|
-
expect(() =>
|
|
121
|
-
validateExcludeConstraint(productModel, {
|
|
122
|
-
name: 'bad',
|
|
123
|
-
elements: [{ column: 'unknown_column', operator: '=' }],
|
|
124
|
-
}),
|
|
125
|
-
).toThrow(
|
|
126
|
-
/Exclude constraint "bad" references column "unknown_column" which does not exist on model Product/,
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('exclude and constraint_trigger constraints', () => {
|
|
132
|
-
const allocationDefinitions: ModelDefinitions = [
|
|
133
|
-
{
|
|
134
|
-
kind: 'entity',
|
|
135
|
-
name: 'PortfolioAllocation',
|
|
136
|
-
fields: [
|
|
137
|
-
{ name: 'portfolio', kind: 'relation', type: 'Portfolio', reverse: 'allocations' },
|
|
138
|
-
{ name: 'startDate', type: 'DateTime' },
|
|
139
|
-
{ name: 'endDate', type: 'DateTime' },
|
|
140
|
-
{ name: 'deleted', type: 'Boolean', nonNull: true, defaultValue: false },
|
|
141
|
-
],
|
|
142
|
-
constraints: [
|
|
143
|
-
{
|
|
144
|
-
kind: 'exclude',
|
|
145
|
-
name: 'no_overlap_per_portfolio',
|
|
146
|
-
using: 'gist',
|
|
147
|
-
elements: [
|
|
148
|
-
{ column: 'portfolioId', operator: '=' },
|
|
149
|
-
{
|
|
150
|
-
expression: 'tsrange("startDate", COALESCE("endDate", \'infinity\'::timestamptz))',
|
|
151
|
-
operator: '&&',
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
where: '"deleted" = false',
|
|
155
|
-
deferrable: 'INITIALLY DEFERRED',
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
kind: 'constraint_trigger',
|
|
159
|
-
name: 'contiguous_periods',
|
|
160
|
-
when: 'AFTER',
|
|
161
|
-
events: ['INSERT', 'UPDATE'],
|
|
162
|
-
forEach: 'ROW',
|
|
163
|
-
deferrable: 'INITIALLY DEFERRED',
|
|
164
|
-
function: { name: 'contiguous_periods_check' },
|
|
165
|
-
},
|
|
166
|
-
],
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
kind: 'entity',
|
|
170
|
-
name: 'Portfolio',
|
|
171
|
-
fields: [{ name: 'name', type: 'String' }],
|
|
172
|
-
},
|
|
173
|
-
];
|
|
174
|
-
|
|
175
|
-
it('constructs models with exclude and constraint_trigger without throwing', () => {
|
|
176
|
-
expect(() => new Models(allocationDefinitions)).not.toThrow();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
83
|
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { MigrationGenerator } from '../../src/migrations/generate';
|
|
2
|
+
import { ModelDefinitions, Models } from '../../src/models';
|
|
3
|
+
|
|
4
|
+
jest.mock('knex-schema-inspector', () => ({
|
|
5
|
+
SchemaInspector: jest.fn(() => ({})),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
jest.mock('code-block-writer', () => {
|
|
9
|
+
const Writer = class {
|
|
10
|
+
private output = '';
|
|
11
|
+
|
|
12
|
+
write(value: string) {
|
|
13
|
+
this.output += value;
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
writeLine(value: string) {
|
|
18
|
+
this.output += `${value}\n`;
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
blankLine() {
|
|
23
|
+
this.output += '\n';
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
newLine() {
|
|
28
|
+
this.output += '\n';
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
inlineBlock(fn: () => void) {
|
|
33
|
+
this.output += ' {\n';
|
|
34
|
+
fn();
|
|
35
|
+
this.output += '}';
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
block(fn: () => void) {
|
|
40
|
+
this.output += ' {\n';
|
|
41
|
+
fn();
|
|
42
|
+
this.output += '}\n';
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toString() {
|
|
47
|
+
return this.output;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return { __esModule: true, default: { default: Writer } };
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
type MockColumn = {
|
|
55
|
+
name: string;
|
|
56
|
+
data_type: string;
|
|
57
|
+
is_nullable: boolean;
|
|
58
|
+
generation_expression?: string | null;
|
|
59
|
+
numeric_precision?: number | null;
|
|
60
|
+
numeric_scale?: number | null;
|
|
61
|
+
max_length?: number | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type CheckRow = {
|
|
65
|
+
table_name: string;
|
|
66
|
+
constraint_name: string;
|
|
67
|
+
check_clause: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const normalizeForCanonicalizationMock = (expr: string) => {
|
|
71
|
+
let normalized = expr.replace(/\s+/g, ' ').trim();
|
|
72
|
+
while (normalized.startsWith('(') && normalized.endsWith(')')) {
|
|
73
|
+
normalized = normalized.slice(1, -1).trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return normalized.replace(/"([a-z_][a-z0-9_]*)"/g, '$1');
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const baseColumnsByTable: Record<string, MockColumn[]> = {
|
|
80
|
+
Product: [
|
|
81
|
+
{ name: 'id', data_type: 'uuid', is_nullable: false },
|
|
82
|
+
{ name: 'deleted', data_type: 'boolean', is_nullable: true },
|
|
83
|
+
{ name: 'startDate', data_type: 'timestamp without time zone', is_nullable: true },
|
|
84
|
+
{ name: 'endDate', data_type: 'timestamp without time zone', is_nullable: true },
|
|
85
|
+
{ name: 'score', data_type: 'integer', is_nullable: true },
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const createProductModels = (constraints: { kind: 'check'; name: string; expression: string }[]) => {
|
|
90
|
+
const definitions: ModelDefinitions = [
|
|
91
|
+
{
|
|
92
|
+
kind: 'entity',
|
|
93
|
+
name: 'Product',
|
|
94
|
+
fields: [{ name: 'score', type: 'Int' }],
|
|
95
|
+
constraints,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
return new Models(definitions);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const createGenerator = (rows: CheckRow[], models: Models) => {
|
|
103
|
+
const raw = jest.fn().mockResolvedValue({ rows });
|
|
104
|
+
const knexLike = Object.assign(
|
|
105
|
+
jest.fn().mockReturnValue({
|
|
106
|
+
where: jest.fn().mockReturnValue({
|
|
107
|
+
select: jest.fn().mockResolvedValue([]),
|
|
108
|
+
}),
|
|
109
|
+
}),
|
|
110
|
+
{ raw },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const schema = {
|
|
114
|
+
knex: knexLike,
|
|
115
|
+
tables: jest.fn().mockResolvedValue(['Product']),
|
|
116
|
+
columnInfo: jest.fn(async (table: string) => baseColumnsByTable[table] ?? []),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const generator = new MigrationGenerator({} as never, models);
|
|
120
|
+
(generator as unknown as { schema: unknown }).schema = schema;
|
|
121
|
+
|
|
122
|
+
return generator;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
describe('MigrationGenerator check constraints', () => {
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
jest
|
|
128
|
+
.spyOn(MigrationGenerator.prototype as any, 'canonicalizeCheckExpressionWithPostgres')
|
|
129
|
+
.mockImplementation(async (...args: unknown[]) => normalizeForCanonicalizationMock(args[1] as string));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
jest.restoreAllMocks();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does not detect changes for equivalent expressions with extra parentheses', async () => {
|
|
137
|
+
const models = createProductModels([{ kind: 'check', name: 'score_non_negative', expression: '"score" >= 0' }]);
|
|
138
|
+
const generator = createGenerator(
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
table_name: 'Product',
|
|
142
|
+
constraint_name: 'Product_score_non_negative_check_0',
|
|
143
|
+
check_clause: ' (( "score" >= 0 )) ',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
models,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
await generator.generate();
|
|
150
|
+
|
|
151
|
+
expect(generator.needsMigration).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('does not detect changes for equivalent expressions with quoted vs unquoted lowercase identifiers', async () => {
|
|
155
|
+
jest
|
|
156
|
+
.spyOn(MigrationGenerator.prototype as any, 'canonicalizeCheckExpressionWithPostgres')
|
|
157
|
+
.mockImplementation(
|
|
158
|
+
async () => '(deleted = true) OR ("endDate" IS NULL) OR ("startDate" <= "endDate")',
|
|
159
|
+
);
|
|
160
|
+
const models = new Models([
|
|
161
|
+
{
|
|
162
|
+
kind: 'entity',
|
|
163
|
+
name: 'Product',
|
|
164
|
+
fields: [
|
|
165
|
+
{ name: 'deleted', type: 'Boolean' },
|
|
166
|
+
{ name: 'startDate', type: 'DateTime' },
|
|
167
|
+
{ name: 'endDate', type: 'DateTime' },
|
|
168
|
+
],
|
|
169
|
+
constraints: [
|
|
170
|
+
{
|
|
171
|
+
kind: 'check',
|
|
172
|
+
name: 'period_start_before_end',
|
|
173
|
+
expression: '"deleted" = true OR "endDate" IS NULL OR "startDate" <= "endDate"',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
const generator = createGenerator(
|
|
179
|
+
[
|
|
180
|
+
{
|
|
181
|
+
table_name: 'Product',
|
|
182
|
+
constraint_name: 'Product_period_start_before_end_check_0',
|
|
183
|
+
check_clause: '((deleted = true) OR ("endDate" IS NULL) OR ("startDate" <= "endDate"))',
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
models,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
await generator.generate();
|
|
190
|
+
|
|
191
|
+
expect(generator.needsMigration).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('does not detect changes if only generated constraint index changed but expression is identical', async () => {
|
|
195
|
+
const models = createProductModels([
|
|
196
|
+
{ kind: 'check', name: 'first', expression: '"score" >= 0' },
|
|
197
|
+
{ kind: 'check', name: 'second', expression: '"score" <= 100' },
|
|
198
|
+
]);
|
|
199
|
+
const generator = createGenerator(
|
|
200
|
+
[
|
|
201
|
+
{
|
|
202
|
+
table_name: 'Product',
|
|
203
|
+
constraint_name: 'Product_first_check_1',
|
|
204
|
+
check_clause: '(("score" >= 0))',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
table_name: 'Product',
|
|
208
|
+
constraint_name: 'Product_second_check_0',
|
|
209
|
+
check_clause: '(("score" <= 100))',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
models,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
await generator.generate();
|
|
216
|
+
|
|
217
|
+
expect(generator.needsMigration).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('still detects actual expression changes', async () => {
|
|
221
|
+
const models = createProductModels([{ kind: 'check', name: 'score_non_negative', expression: '"score" >= 0' }]);
|
|
222
|
+
const generator = createGenerator(
|
|
223
|
+
[
|
|
224
|
+
{
|
|
225
|
+
table_name: 'Product',
|
|
226
|
+
constraint_name: 'Product_score_non_negative_check_0',
|
|
227
|
+
check_clause: '"score" > 0',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
models,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const migration = await generator.generate();
|
|
234
|
+
|
|
235
|
+
expect(generator.needsMigration).toBe(true);
|
|
236
|
+
expect(migration).toContain('DROP CONSTRAINT "Product_score_non_negative_check_0"');
|
|
237
|
+
expect(migration).toContain('ADD CONSTRAINT "Product_score_non_negative_check_0" CHECK ("score" >= 0)');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('warns and treats constraint as changed when canonicalization fails', async () => {
|
|
241
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
242
|
+
jest
|
|
243
|
+
.spyOn(MigrationGenerator.prototype as any, 'canonicalizeCheckExpressionWithPostgres')
|
|
244
|
+
.mockRejectedValue(new Error('canonicalization failed'));
|
|
245
|
+
const models = createProductModels([{ kind: 'check', name: 'score_non_negative', expression: '"score" >= 0' }]);
|
|
246
|
+
const generator = createGenerator(
|
|
247
|
+
[
|
|
248
|
+
{
|
|
249
|
+
table_name: 'Product',
|
|
250
|
+
constraint_name: 'Product_score_non_negative_check_0',
|
|
251
|
+
check_clause: '"score" >= 0',
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
models,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await generator.generate();
|
|
258
|
+
|
|
259
|
+
expect(generator.needsMigration).toBe(true);
|
|
260
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('MigrationGenerator canonicalizeCheckExpressionWithPostgres', () => {
|
|
265
|
+
afterEach(() => {
|
|
266
|
+
jest.restoreAllMocks();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('creates temp table using LIKE source table shape', async () => {
|
|
270
|
+
const raw = jest
|
|
271
|
+
.fn()
|
|
272
|
+
.mockResolvedValueOnce(undefined)
|
|
273
|
+
.mockResolvedValueOnce(undefined)
|
|
274
|
+
.mockResolvedValueOnce({
|
|
275
|
+
rows: [
|
|
276
|
+
{
|
|
277
|
+
constraint_definition: 'CHECK (((deleted = true) OR ("endDate" IS NULL) OR ("startDate" <= "endDate")))',
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
const rollback = jest.fn().mockResolvedValue(undefined);
|
|
282
|
+
const trx = { raw, rollback };
|
|
283
|
+
const knexLike = {
|
|
284
|
+
transaction: jest.fn().mockResolvedValue(trx),
|
|
285
|
+
};
|
|
286
|
+
const generator = new MigrationGenerator(knexLike as never, createProductModels([]));
|
|
287
|
+
|
|
288
|
+
const canonical = await (generator as any).canonicalizeCheckExpressionWithPostgres(
|
|
289
|
+
'RevenueSplit',
|
|
290
|
+
'((deleted = true) OR ("endDate" IS NULL) OR ("startDate" <= "endDate"))',
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const createTempSql = raw.mock.calls[0]?.[0] as string;
|
|
294
|
+
expect(createTempSql).toContain('CREATE TEMP TABLE');
|
|
295
|
+
expect(createTempSql).toContain('(LIKE "RevenueSplit")');
|
|
296
|
+
expect(createTempSql).not.toContain('gqm_check_value');
|
|
297
|
+
expect(canonical).toBe('(deleted = true) OR ("endDate" IS NULL) OR ("startDate" <= "endDate")');
|
|
298
|
+
expect(rollback).toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
});
|