@balena/abstract-sql-compiler 11.0.0-build-11-x-b2280608fff69d9959999c79db6245c4ad561bbc-1 → 11.0.0-build-11-x-7511b8ebe5a9461f20add0ed97d0670ed3b5a479-1
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/package.json +5 -2
- package/.github/workflows/flowzone.yml +0 -21
- package/.husky/pre-commit +0 -2
- package/.versionbot/CHANGELOG.yml +0 -10729
- package/CHANGELOG.md +0 -3515
- package/repo.yml +0 -12
- package/src/abstract-sql-compiler.ts +0 -1138
- package/src/abstract-sql-optimizer.ts +0 -1632
- package/src/abstract-sql-rules-to-sql.ts +0 -1730
- package/src/abstract-sql-schema-optimizer.ts +0 -172
- package/src/referenced-fields.ts +0 -600
- package/test/abstract-sql/aggregate-json.ts +0 -49
- package/test/abstract-sql/aggregate.ts +0 -161
- package/test/abstract-sql/and-or-boolean-optimisations.ts +0 -115
- package/test/abstract-sql/case-when-else.ts +0 -48
- package/test/abstract-sql/cast.ts +0 -25
- package/test/abstract-sql/coalesce.ts +0 -24
- package/test/abstract-sql/comparisons.ts +0 -360
- package/test/abstract-sql/dates.ts +0 -512
- package/test/abstract-sql/duration.ts +0 -56
- package/test/abstract-sql/empty-query-optimisations.ts +0 -54
- package/test/abstract-sql/functions-wrapper.ts +0 -70
- package/test/abstract-sql/get-referenced-fields.ts +0 -674
- package/test/abstract-sql/get-rule-referenced-fields.ts +0 -345
- package/test/abstract-sql/insert-query.ts +0 -22
- package/test/abstract-sql/is-distinct.ts +0 -102
- package/test/abstract-sql/joins.ts +0 -84
- package/test/abstract-sql/json.ts +0 -58
- package/test/abstract-sql/math.ts +0 -467
- package/test/abstract-sql/nested-in-optimisations.ts +0 -200
- package/test/abstract-sql/not-not-optimisations.ts +0 -15
- package/test/abstract-sql/schema-checks.ts +0 -168
- package/test/abstract-sql/schema-informative-reference.ts +0 -420
- package/test/abstract-sql/schema-rule-optimization.ts +0 -120
- package/test/abstract-sql/schema-rule-to-check.ts +0 -393
- package/test/abstract-sql/schema-views.ts +0 -73
- package/test/abstract-sql/test.ts +0 -192
- package/test/abstract-sql/text.ts +0 -168
- package/test/model.sbvr +0 -60
- package/test/odata/expand.ts +0 -674
- package/test/odata/fields.ts +0 -59
- package/test/odata/filterby.ts +0 -1517
- package/test/odata/orderby.ts +0 -96
- package/test/odata/paging.ts +0 -48
- package/test/odata/resource-parsing.ts +0 -568
- package/test/odata/select.ts +0 -119
- package/test/odata/stress.ts +0 -93
- package/test/odata/test.ts +0 -297
- package/test/sbvr/pilots.ts +0 -1097
- package/test/sbvr/reference-type.ts +0 -211
- package/test/sbvr/test.ts +0 -101
- package/tsconfig.build.json +0 -6
- package/tsconfig.json +0 -25
package/src/referenced-fields.ts
DELETED
@@ -1,600 +0,0 @@
|
|
1
|
-
import type {
|
2
|
-
AbstractSqlQuery,
|
3
|
-
AbstractSqlType,
|
4
|
-
AddDateDurationNode,
|
5
|
-
AddDateNumberNode,
|
6
|
-
AliasNode,
|
7
|
-
AndNode,
|
8
|
-
AnyNode,
|
9
|
-
AverageNode,
|
10
|
-
CastNode,
|
11
|
-
CharacterLengthNode,
|
12
|
-
CountNode,
|
13
|
-
CrossJoinNode,
|
14
|
-
DateTruncNode,
|
15
|
-
EngineInstance,
|
16
|
-
EqualsNode,
|
17
|
-
ExistsNode,
|
18
|
-
ExtractJSONPathAsTextNode,
|
19
|
-
FieldsNode,
|
20
|
-
FromNode,
|
21
|
-
FromTypeNodes,
|
22
|
-
FullJoinNode,
|
23
|
-
GreaterThanNode,
|
24
|
-
GreaterThanOrEqualNode,
|
25
|
-
HavingNode,
|
26
|
-
InnerJoinNode,
|
27
|
-
InNode,
|
28
|
-
IsDistinctFromNode,
|
29
|
-
IsNotDistinctFromNode,
|
30
|
-
LeftJoinNode,
|
31
|
-
LessThanNode,
|
32
|
-
LessThanOrEqualNode,
|
33
|
-
LfRuleInfo,
|
34
|
-
NotEqualsNode,
|
35
|
-
NotExistsNode,
|
36
|
-
NotInNode,
|
37
|
-
NotNode,
|
38
|
-
OrNode,
|
39
|
-
RightJoinNode,
|
40
|
-
SelectNode,
|
41
|
-
SelectQueryNode,
|
42
|
-
SubtractDateDateNode,
|
43
|
-
SubtractDateDurationNode,
|
44
|
-
SubtractDateNumberNode,
|
45
|
-
SumNode,
|
46
|
-
TableNode,
|
47
|
-
TextArrayNode,
|
48
|
-
ToJSONNode,
|
49
|
-
UnionQueryNode,
|
50
|
-
WhereNode,
|
51
|
-
} from './abstract-sql-compiler.js';
|
52
|
-
import {
|
53
|
-
isAliasNode,
|
54
|
-
isFromNode,
|
55
|
-
isSelectNode,
|
56
|
-
isSelectQueryNode,
|
57
|
-
isTableNode,
|
58
|
-
isWhereNode,
|
59
|
-
} from './abstract-sql-compiler.js';
|
60
|
-
import { AbstractSQLOptimizer } from './abstract-sql-optimizer.js';
|
61
|
-
import { isAbstractSqlQuery } from './abstract-sql-rules-to-sql.js';
|
62
|
-
|
63
|
-
export interface ReferencedFields {
|
64
|
-
[alias: string]: string[];
|
65
|
-
}
|
66
|
-
|
67
|
-
export interface ModifiedFields {
|
68
|
-
table: string;
|
69
|
-
action: keyof RuleReferencedFields[string];
|
70
|
-
fields?: string[];
|
71
|
-
}
|
72
|
-
|
73
|
-
export const getReferencedFields: EngineInstance['getReferencedFields'] = (
|
74
|
-
ruleBody,
|
75
|
-
) => {
|
76
|
-
const referencedFields = getRuleReferencedFields(ruleBody);
|
77
|
-
|
78
|
-
const result: { [key: string]: string[] } = {};
|
79
|
-
for (const key of Object.keys(referencedFields)) {
|
80
|
-
result[key] = [...new Set(referencedFields[key].update)];
|
81
|
-
}
|
82
|
-
return result;
|
83
|
-
};
|
84
|
-
|
85
|
-
export interface RuleReferencedFields {
|
86
|
-
[alias: string]: {
|
87
|
-
create: string[];
|
88
|
-
update: string[];
|
89
|
-
delete: string[];
|
90
|
-
};
|
91
|
-
}
|
92
|
-
enum IsSafe {
|
93
|
-
Insert = 'ins',
|
94
|
-
Delete = 'del',
|
95
|
-
Unknown = '',
|
96
|
-
}
|
97
|
-
type RuleReferencedScope = {
|
98
|
-
[aliasName: string]: {
|
99
|
-
tableName: string;
|
100
|
-
isSafe: IsSafe;
|
101
|
-
};
|
102
|
-
};
|
103
|
-
const getRuleReferencedScope = (
|
104
|
-
rulePart: AbstractSqlQuery,
|
105
|
-
scope: RuleReferencedScope,
|
106
|
-
isSafe: IsSafe,
|
107
|
-
): { scope: RuleReferencedScope; currentlyScopedAliases: string[] } => {
|
108
|
-
const currentlyScopedAliases: string[] = [];
|
109
|
-
scope = { ...scope };
|
110
|
-
const fromNodes = rulePart.filter(isFromNode);
|
111
|
-
fromNodes.forEach((node) => {
|
112
|
-
const nested = node[1];
|
113
|
-
if (nested[0] === 'Alias') {
|
114
|
-
const [, from, alias] = nested;
|
115
|
-
if (typeof alias !== 'string') {
|
116
|
-
throw new Error('Cannot handle non-string aliases');
|
117
|
-
}
|
118
|
-
currentlyScopedAliases.push(alias);
|
119
|
-
switch (from[0]) {
|
120
|
-
case 'Table':
|
121
|
-
scope[alias] = { tableName: from[1], isSafe };
|
122
|
-
break;
|
123
|
-
case 'SelectQuery':
|
124
|
-
// Ignore SelectQuery in the From as we'll handle any fields it selects
|
125
|
-
// when we recurse in. With true scope handling however we could prune
|
126
|
-
// fields that don't affect the end result and avoid false positives
|
127
|
-
scope[alias] = { tableName: '', isSafe };
|
128
|
-
break;
|
129
|
-
default:
|
130
|
-
throw new Error(`Cannot handle aliased ${from[0]} nodes`);
|
131
|
-
}
|
132
|
-
} else if (nested[0] === 'Table') {
|
133
|
-
currentlyScopedAliases.push(nested[1]);
|
134
|
-
scope[nested[1]] = { tableName: nested[1], isSafe };
|
135
|
-
} else {
|
136
|
-
throw Error(`Unsupported FromNode for scoping: ${nested[0]}`);
|
137
|
-
}
|
138
|
-
});
|
139
|
-
return { scope, currentlyScopedAliases };
|
140
|
-
};
|
141
|
-
const addReference = (
|
142
|
-
referencedFields: RuleReferencedFields,
|
143
|
-
scope: RuleReferencedScope,
|
144
|
-
aliasName: string,
|
145
|
-
fieldName: string,
|
146
|
-
) => {
|
147
|
-
const a = scope[aliasName];
|
148
|
-
// The scoped tableName is empty in the case of an aliased from query
|
149
|
-
// and those fields will be covered when we recurse into them
|
150
|
-
if (a.tableName !== '') {
|
151
|
-
referencedFields[a.tableName] ??= {
|
152
|
-
create: [],
|
153
|
-
update: [],
|
154
|
-
delete: [],
|
155
|
-
};
|
156
|
-
if (a.isSafe !== IsSafe.Insert) {
|
157
|
-
referencedFields[a.tableName].create.push(fieldName);
|
158
|
-
}
|
159
|
-
if (a.isSafe !== IsSafe.Delete) {
|
160
|
-
referencedFields[a.tableName].delete.push(fieldName);
|
161
|
-
}
|
162
|
-
referencedFields[a.tableName].update.push(fieldName);
|
163
|
-
}
|
164
|
-
};
|
165
|
-
const $getRuleReferencedFields = (
|
166
|
-
referencedFields: RuleReferencedFields,
|
167
|
-
rulePart: AbstractSqlQuery,
|
168
|
-
isSafe: IsSafe,
|
169
|
-
{
|
170
|
-
scope,
|
171
|
-
currentlyScopedAliases,
|
172
|
-
}: ReturnType<typeof getRuleReferencedScope> = {
|
173
|
-
scope: {},
|
174
|
-
currentlyScopedAliases: [],
|
175
|
-
},
|
176
|
-
) => {
|
177
|
-
if (!Array.isArray(rulePart)) {
|
178
|
-
return;
|
179
|
-
}
|
180
|
-
switch (rulePart[0]) {
|
181
|
-
case 'SelectQuery':
|
182
|
-
// Update the current scope before trying to resolve field references
|
183
|
-
({ scope, currentlyScopedAliases } = getRuleReferencedScope(
|
184
|
-
rulePart,
|
185
|
-
scope,
|
186
|
-
isSafe,
|
187
|
-
));
|
188
|
-
rulePart.forEach((node: AbstractSqlQuery) => {
|
189
|
-
$getRuleReferencedFields(referencedFields, node, isSafe, {
|
190
|
-
scope,
|
191
|
-
currentlyScopedAliases,
|
192
|
-
});
|
193
|
-
});
|
194
|
-
return;
|
195
|
-
case 'ReferencedField': {
|
196
|
-
const [, aliasName, fieldName] = rulePart;
|
197
|
-
if (typeof aliasName !== 'string' || typeof fieldName !== 'string') {
|
198
|
-
throw new Error(`Invalid ReferencedField: ${rulePart}`);
|
199
|
-
}
|
200
|
-
addReference(referencedFields, scope, aliasName, fieldName);
|
201
|
-
return;
|
202
|
-
}
|
203
|
-
case 'Field': {
|
204
|
-
const [, fieldName] = rulePart;
|
205
|
-
if (typeof fieldName !== 'string') {
|
206
|
-
throw new Error(`Invalid ReferencedField: ${rulePart}`);
|
207
|
-
}
|
208
|
-
for (const aliasName of Object.keys(scope)) {
|
209
|
-
// We assume any unreferenced field can come from any of the scoped tables
|
210
|
-
addReference(referencedFields, scope, aliasName, fieldName);
|
211
|
-
}
|
212
|
-
return;
|
213
|
-
}
|
214
|
-
case 'Not':
|
215
|
-
case 'NotExists':
|
216
|
-
// When hitting a `Not` we invert the safety rule
|
217
|
-
if (isSafe === IsSafe.Insert) {
|
218
|
-
isSafe = IsSafe.Delete;
|
219
|
-
} else if (isSafe === IsSafe.Delete) {
|
220
|
-
isSafe = IsSafe.Insert;
|
221
|
-
}
|
222
|
-
// eslint-disable-next-line no-fallthrough -- Fallthrough
|
223
|
-
case 'Where':
|
224
|
-
case 'And':
|
225
|
-
case 'Exists':
|
226
|
-
rulePart.forEach((node: AbstractSqlQuery) => {
|
227
|
-
$getRuleReferencedFields(referencedFields, node, isSafe, {
|
228
|
-
scope,
|
229
|
-
currentlyScopedAliases,
|
230
|
-
});
|
231
|
-
});
|
232
|
-
return;
|
233
|
-
case 'Having':
|
234
|
-
scope = { ...scope };
|
235
|
-
for (const key of Object.keys(scope)) {
|
236
|
-
// Treat all entries under a `HAVING` as unknown since it can include counts in such a way
|
237
|
-
// that our expectations of safety do not hold
|
238
|
-
scope[key] = { ...scope[key], isSafe: IsSafe.Unknown };
|
239
|
-
}
|
240
|
-
rulePart.forEach((node: AbstractSqlQuery) => {
|
241
|
-
$getRuleReferencedFields(referencedFields, node, isSafe, {
|
242
|
-
scope,
|
243
|
-
currentlyScopedAliases,
|
244
|
-
});
|
245
|
-
});
|
246
|
-
return;
|
247
|
-
case 'Count':
|
248
|
-
if (rulePart[1] !== '*') {
|
249
|
-
throw new Error(
|
250
|
-
'Only COUNT(*) is supported for rule referenced fields',
|
251
|
-
);
|
252
|
-
}
|
253
|
-
for (const aliasName of currentlyScopedAliases) {
|
254
|
-
// We use '' as it means that only operations that affect every field will match against it
|
255
|
-
addReference(referencedFields, scope, aliasName, '');
|
256
|
-
}
|
257
|
-
return;
|
258
|
-
default:
|
259
|
-
rulePart.forEach((node: AbstractSqlQuery) => {
|
260
|
-
$getRuleReferencedFields(referencedFields, node, IsSafe.Unknown, {
|
261
|
-
scope,
|
262
|
-
currentlyScopedAliases,
|
263
|
-
});
|
264
|
-
});
|
265
|
-
}
|
266
|
-
};
|
267
|
-
export const getRuleReferencedFields: EngineInstance['getRuleReferencedFields'] =
|
268
|
-
(ruleBody) => {
|
269
|
-
ruleBody = AbstractSQLOptimizer(ruleBody);
|
270
|
-
const referencedFields: RuleReferencedFields = {};
|
271
|
-
if (
|
272
|
-
ruleBody[0] === 'Equals' &&
|
273
|
-
ruleBody[2][0] === 'Number' &&
|
274
|
-
ruleBody[2][1] === 0 &&
|
275
|
-
isSelectQueryNode(ruleBody[1])
|
276
|
-
) {
|
277
|
-
const select = ruleBody[1].find(isSelectNode)!;
|
278
|
-
select[1] = [];
|
279
|
-
$getRuleReferencedFields(referencedFields, ruleBody[1], IsSafe.Delete);
|
280
|
-
} else {
|
281
|
-
$getRuleReferencedFields(referencedFields, ruleBody, IsSafe.Insert);
|
282
|
-
}
|
283
|
-
for (const tableName of Object.keys(referencedFields)) {
|
284
|
-
const tableRefs = referencedFields[tableName];
|
285
|
-
for (const method of Object.keys(tableRefs) as Array<
|
286
|
-
keyof typeof tableRefs
|
287
|
-
>) {
|
288
|
-
tableRefs[method] = [...new Set(tableRefs[method])];
|
289
|
-
}
|
290
|
-
}
|
291
|
-
|
292
|
-
return referencedFields;
|
293
|
-
};
|
294
|
-
|
295
|
-
const checkQuery = (query: AbstractSqlQuery): ModifiedFields | undefined => {
|
296
|
-
const queryType = query[0];
|
297
|
-
if (!['InsertQuery', 'UpdateQuery', 'DeleteQuery'].includes(queryType)) {
|
298
|
-
return;
|
299
|
-
}
|
300
|
-
|
301
|
-
const froms = query.filter(isFromNode);
|
302
|
-
if (froms.length !== 1) {
|
303
|
-
return;
|
304
|
-
}
|
305
|
-
|
306
|
-
let table = froms[0][1];
|
307
|
-
if (isAliasNode(table)) {
|
308
|
-
table = table[1];
|
309
|
-
}
|
310
|
-
|
311
|
-
let tableName: string;
|
312
|
-
if (isTableNode(table)) {
|
313
|
-
tableName = table[1];
|
314
|
-
} else if (typeof table === 'string') {
|
315
|
-
// Deprecated: Remove this when we drop implicit tables
|
316
|
-
tableName = table;
|
317
|
-
} else {
|
318
|
-
return;
|
319
|
-
}
|
320
|
-
|
321
|
-
if (queryType === 'InsertQuery') {
|
322
|
-
return { table: tableName, action: 'create' };
|
323
|
-
}
|
324
|
-
if (queryType === 'DeleteQuery') {
|
325
|
-
return { table: tableName, action: 'delete' };
|
326
|
-
}
|
327
|
-
|
328
|
-
const fields = query
|
329
|
-
.filter((v): v is FieldsNode => v != null && v[0] === 'Fields')
|
330
|
-
.flatMap((v) => v[1]);
|
331
|
-
return { table: tableName, action: 'update', fields };
|
332
|
-
};
|
333
|
-
export const getModifiedFields: EngineInstance['getModifiedFields'] = (
|
334
|
-
abstractSqlQuery: AbstractSqlQuery,
|
335
|
-
) => {
|
336
|
-
if (abstractSqlQuery[0] === 'UpsertQuery') {
|
337
|
-
return abstractSqlQuery.slice(1).map(checkQuery);
|
338
|
-
} else if (Array.isArray(abstractSqlQuery[0])) {
|
339
|
-
return abstractSqlQuery.map(checkQuery);
|
340
|
-
} else {
|
341
|
-
return checkQuery(abstractSqlQuery);
|
342
|
-
}
|
343
|
-
};
|
344
|
-
|
345
|
-
// TS requires this to be a funtion declaration
|
346
|
-
function assertAbstractSqlIsNotLegacy(
|
347
|
-
abstractSql: AbstractSqlType,
|
348
|
-
): asserts abstractSql is AbstractSqlQuery {
|
349
|
-
if (!isAbstractSqlQuery(abstractSql)) {
|
350
|
-
throw new Error(
|
351
|
-
'cannot introspect into the string form of AbstractSqlQuery',
|
352
|
-
);
|
353
|
-
}
|
354
|
-
}
|
355
|
-
|
356
|
-
// Find how many times an abstract sql fragment selects from the given table
|
357
|
-
// TODO:
|
358
|
-
// - Not all abstract sql nodes are supported here yet but hopefully nothing
|
359
|
-
// important is missing atm
|
360
|
-
// - Create missing node types
|
361
|
-
const countTableSelects = (
|
362
|
-
abstractSql: AbstractSqlQuery,
|
363
|
-
table: string,
|
364
|
-
): number => {
|
365
|
-
assertAbstractSqlIsNotLegacy(abstractSql);
|
366
|
-
let sum = 0;
|
367
|
-
switch (abstractSql[0]) {
|
368
|
-
// Unary nodes
|
369
|
-
case 'Alias':
|
370
|
-
case 'Any':
|
371
|
-
case 'Average':
|
372
|
-
case 'Cast':
|
373
|
-
case 'CharacterLength':
|
374
|
-
case 'CrossJoin':
|
375
|
-
case 'Exists':
|
376
|
-
case 'From':
|
377
|
-
case 'Having':
|
378
|
-
case 'Lower':
|
379
|
-
case 'Not':
|
380
|
-
case 'NotExists':
|
381
|
-
case 'Sum':
|
382
|
-
case 'ToJSON':
|
383
|
-
case 'Where': {
|
384
|
-
const unaryOperation = abstractSql as
|
385
|
-
| AliasNode<FromTypeNodes>
|
386
|
-
| AnyNode
|
387
|
-
| AverageNode
|
388
|
-
| CastNode
|
389
|
-
| CharacterLengthNode
|
390
|
-
| CrossJoinNode
|
391
|
-
| ExistsNode
|
392
|
-
| FromNode
|
393
|
-
| HavingNode
|
394
|
-
| NotExistsNode
|
395
|
-
| NotNode
|
396
|
-
| SumNode
|
397
|
-
| ToJSONNode
|
398
|
-
| WhereNode;
|
399
|
-
assertAbstractSqlIsNotLegacy(unaryOperation[1]);
|
400
|
-
|
401
|
-
return countTableSelects(unaryOperation[1], table);
|
402
|
-
}
|
403
|
-
// `COUNT` is an unary function but we only support the `COUNT(*)` form
|
404
|
-
case 'Count': {
|
405
|
-
const countNode = abstractSql as CountNode;
|
406
|
-
if (countNode[1] !== '*') {
|
407
|
-
throw new Error('Only COUNT(*) is supported');
|
408
|
-
}
|
409
|
-
|
410
|
-
return 0;
|
411
|
-
}
|
412
|
-
// Binary nodes
|
413
|
-
case 'AddDateDuration':
|
414
|
-
case 'AddDateNumber':
|
415
|
-
case 'DateTrunc':
|
416
|
-
case 'Equals':
|
417
|
-
case 'ExtractJSONPathAsText':
|
418
|
-
case 'GreaterThan':
|
419
|
-
case 'GreaterThanOrEqual':
|
420
|
-
case 'IsDistinctFrom':
|
421
|
-
case 'IsNotDistinctFrom':
|
422
|
-
case 'LessThan':
|
423
|
-
case 'LessThanOrEqual':
|
424
|
-
case 'NotEquals':
|
425
|
-
case 'SubtractDateDate':
|
426
|
-
case 'SubtractDateDuration':
|
427
|
-
case 'SubtractDateNumber': {
|
428
|
-
const binaryOperation = abstractSql as
|
429
|
-
| AddDateDurationNode
|
430
|
-
| AddDateNumberNode
|
431
|
-
| DateTruncNode
|
432
|
-
| EqualsNode
|
433
|
-
| ExtractJSONPathAsTextNode
|
434
|
-
| GreaterThanNode
|
435
|
-
| GreaterThanOrEqualNode
|
436
|
-
| IsDistinctFromNode
|
437
|
-
| IsNotDistinctFromNode
|
438
|
-
| LessThanNode
|
439
|
-
| LessThanOrEqualNode
|
440
|
-
| NotEqualsNode
|
441
|
-
| SubtractDateDateNode
|
442
|
-
| SubtractDateDurationNode
|
443
|
-
| SubtractDateNumberNode;
|
444
|
-
const leftOperand = binaryOperation[1];
|
445
|
-
assertAbstractSqlIsNotLegacy(leftOperand);
|
446
|
-
const rightOperand = binaryOperation[2];
|
447
|
-
assertAbstractSqlIsNotLegacy(rightOperand);
|
448
|
-
|
449
|
-
return (
|
450
|
-
countTableSelects(leftOperand, table) +
|
451
|
-
countTableSelects(rightOperand, table)
|
452
|
-
);
|
453
|
-
}
|
454
|
-
// Binary nodes with optional `ON` second argument
|
455
|
-
case 'FullJoin':
|
456
|
-
case 'Join':
|
457
|
-
case 'LeftJoin':
|
458
|
-
case 'RightJoin': {
|
459
|
-
const joinNode = abstractSql as
|
460
|
-
| FullJoinNode
|
461
|
-
| InnerJoinNode
|
462
|
-
| LeftJoinNode
|
463
|
-
| RightJoinNode;
|
464
|
-
assertAbstractSqlIsNotLegacy(joinNode[1]);
|
465
|
-
if (joinNode[2] !== undefined) {
|
466
|
-
assertAbstractSqlIsNotLegacy(joinNode[2][1]);
|
467
|
-
sum = countTableSelects(joinNode[2][1], table);
|
468
|
-
}
|
469
|
-
|
470
|
-
return sum + countTableSelects(joinNode[1], table);
|
471
|
-
}
|
472
|
-
// n-ary nodes
|
473
|
-
case 'And':
|
474
|
-
case 'Or':
|
475
|
-
case 'SelectQuery':
|
476
|
-
case 'TextArray':
|
477
|
-
case 'UnionQuery': {
|
478
|
-
const selectQueryNode = abstractSql as
|
479
|
-
| AndNode
|
480
|
-
| OrNode
|
481
|
-
| SelectQueryNode
|
482
|
-
| TextArrayNode
|
483
|
-
| UnionQueryNode;
|
484
|
-
for (const arg of selectQueryNode.slice(1)) {
|
485
|
-
assertAbstractSqlIsNotLegacy(arg);
|
486
|
-
sum += countTableSelects(arg, table);
|
487
|
-
}
|
488
|
-
|
489
|
-
return sum;
|
490
|
-
}
|
491
|
-
// n-ary nodes but the slice starts at the third argument
|
492
|
-
case 'In':
|
493
|
-
case 'NotIn': {
|
494
|
-
const inNode = abstractSql as InNode | NotInNode;
|
495
|
-
for (const arg of inNode.slice(2)) {
|
496
|
-
assertAbstractSqlIsNotLegacy(arg);
|
497
|
-
sum += countTableSelects(arg, table);
|
498
|
-
}
|
499
|
-
|
500
|
-
return sum;
|
501
|
-
}
|
502
|
-
// n-ary-like node
|
503
|
-
case 'Select': {
|
504
|
-
const selectNode = abstractSql as SelectNode;
|
505
|
-
for (const arg of selectNode[1]) {
|
506
|
-
assertAbstractSqlIsNotLegacy(arg);
|
507
|
-
sum += countTableSelects(arg, table);
|
508
|
-
}
|
509
|
-
|
510
|
-
return sum;
|
511
|
-
}
|
512
|
-
// Uninteresting atomic nodes
|
513
|
-
case 'Boolean':
|
514
|
-
case 'Date':
|
515
|
-
case 'Duration':
|
516
|
-
case 'EmbeddedText':
|
517
|
-
case 'GroupBy':
|
518
|
-
case 'Integer':
|
519
|
-
case 'Null':
|
520
|
-
case 'Number':
|
521
|
-
case 'ReferencedField':
|
522
|
-
case 'Text':
|
523
|
-
return 0;
|
524
|
-
|
525
|
-
// The atomic node we're looking for: a table selection
|
526
|
-
case 'Table': {
|
527
|
-
const tableNode = abstractSql as TableNode;
|
528
|
-
|
529
|
-
if (tableNode[1] === table) {
|
530
|
-
return 1;
|
531
|
-
} else {
|
532
|
-
return 0;
|
533
|
-
}
|
534
|
-
}
|
535
|
-
default:
|
536
|
-
throw new Error(`unknown abstract sql type: ${abstractSql[0]}`);
|
537
|
-
}
|
538
|
-
};
|
539
|
-
|
540
|
-
// TODO:
|
541
|
-
// - This function only narrows the root table of the rule. This is always
|
542
|
-
// safe when the root table isn't selected from more than once and it is not
|
543
|
-
// negated in the LF. Right now we conservatively check for the former but
|
544
|
-
// not the second. The negative forms (e.g. it is forbidden that ...) are
|
545
|
-
// not fully supported anyway.
|
546
|
-
// - Removing multiple candidates selecting from the same database table to
|
547
|
-
// avoid visibility issues is too conservative. The correct criteria is to
|
548
|
-
// just remove any that are present in at least 2 disjoint subqueries.
|
549
|
-
// Because in this case the problem is that in those cases there is not a
|
550
|
-
// single place in the query that has visibility inside both disjoint
|
551
|
-
// subqueries and that is a requirement for adding the corrent binds for
|
552
|
-
// narrowing.
|
553
|
-
// - We assume the ID column is named "id".
|
554
|
-
// - This is a very restricted implementation of narrowing which could be
|
555
|
-
// expanded to cover more situations.
|
556
|
-
//
|
557
|
-
// This function modifies `abstractSql` in place.
|
558
|
-
export const insertAffectedIdsBinds = (
|
559
|
-
abstractSql: AbstractSqlQuery,
|
560
|
-
lfRuleInfo: LfRuleInfo,
|
561
|
-
) => {
|
562
|
-
const rootTableSelectCount = countTableSelects(
|
563
|
-
abstractSql,
|
564
|
-
lfRuleInfo.root.table,
|
565
|
-
);
|
566
|
-
if (rootTableSelectCount !== 1) {
|
567
|
-
return;
|
568
|
-
}
|
569
|
-
|
570
|
-
const narrowing: OrNode = [
|
571
|
-
'Or',
|
572
|
-
['Equals', ['Bind', lfRuleInfo.root.table], ['EmbeddedText', '{}']],
|
573
|
-
[
|
574
|
-
'Equals',
|
575
|
-
['ReferencedField', lfRuleInfo.root.alias, 'id'],
|
576
|
-
['Any', ['Bind', lfRuleInfo.root.table], 'Integer'],
|
577
|
-
],
|
578
|
-
];
|
579
|
-
|
580
|
-
// Assume (but check) that the query is of the form:
|
581
|
-
//
|
582
|
-
// SELECT (SELECT COUNT(*) ...) = 0
|
583
|
-
if (
|
584
|
-
abstractSql[0] !== 'Equals' ||
|
585
|
-
abstractSql[1][0] !== 'SelectQuery' ||
|
586
|
-
abstractSql[2][0] !== 'Number'
|
587
|
-
) {
|
588
|
-
throw new Error(
|
589
|
-
'Query is not of the form: SELECT (SELECT COUNT(*) ...) = 0',
|
590
|
-
);
|
591
|
-
}
|
592
|
-
|
593
|
-
const selectQueryNode = abstractSql[1] as SelectQueryNode;
|
594
|
-
const whereNode = selectQueryNode.slice(1).find(isWhereNode);
|
595
|
-
if (whereNode === undefined) {
|
596
|
-
selectQueryNode.push(['Where', narrowing]);
|
597
|
-
} else {
|
598
|
-
whereNode[1] = ['And', whereNode[1], narrowing];
|
599
|
-
}
|
600
|
-
};
|
@@ -1,49 +0,0 @@
|
|
1
|
-
import test from './test.js';
|
2
|
-
|
3
|
-
describe('AggregateJSON', () => {
|
4
|
-
test(
|
5
|
-
[
|
6
|
-
'SelectQuery',
|
7
|
-
[
|
8
|
-
'Select',
|
9
|
-
[
|
10
|
-
[
|
11
|
-
'Alias',
|
12
|
-
[
|
13
|
-
'SelectQuery',
|
14
|
-
[
|
15
|
-
'Select',
|
16
|
-
[
|
17
|
-
[
|
18
|
-
'Alias',
|
19
|
-
[
|
20
|
-
'AggregateJSON',
|
21
|
-
['ReferencedField', 'pilot.licence', '*'],
|
22
|
-
],
|
23
|
-
'licence',
|
24
|
-
],
|
25
|
-
],
|
26
|
-
],
|
27
|
-
['From', ['Alias', ['Table', 'licence'], 'pilot.licence']],
|
28
|
-
],
|
29
|
-
'licence',
|
30
|
-
],
|
31
|
-
],
|
32
|
-
],
|
33
|
-
['From', ['Table', 'pilot']],
|
34
|
-
],
|
35
|
-
(result, sqlEquals) => {
|
36
|
-
it('should produce a valid aggregate JSON statement', () => {
|
37
|
-
sqlEquals(
|
38
|
-
result,
|
39
|
-
`\
|
40
|
-
SELECT (
|
41
|
-
SELECT COALESCE(JSON_AGG("pilot.licence".*), '[]') AS "licence"
|
42
|
-
FROM "licence" AS "pilot.licence"
|
43
|
-
) AS "licence"
|
44
|
-
FROM "pilot"`,
|
45
|
-
);
|
46
|
-
});
|
47
|
-
},
|
48
|
-
);
|
49
|
-
});
|