@balena/abstract-sql-compiler 11.0.0-build-11-x-45529f014aa1c181f338c0f7348767f2990a9084-1 → 11.0.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.
Files changed (53) hide show
  1. package/package.json +5 -2
  2. package/.github/workflows/flowzone.yml +0 -21
  3. package/.husky/pre-commit +0 -2
  4. package/.versionbot/CHANGELOG.yml +0 -10729
  5. package/CHANGELOG.md +0 -3515
  6. package/repo.yml +0 -12
  7. package/src/abstract-sql-compiler.ts +0 -1138
  8. package/src/abstract-sql-optimizer.ts +0 -1632
  9. package/src/abstract-sql-rules-to-sql.ts +0 -1730
  10. package/src/abstract-sql-schema-optimizer.ts +0 -172
  11. package/src/referenced-fields.ts +0 -600
  12. package/test/abstract-sql/aggregate-json.ts +0 -49
  13. package/test/abstract-sql/aggregate.ts +0 -161
  14. package/test/abstract-sql/and-or-boolean-optimisations.ts +0 -115
  15. package/test/abstract-sql/case-when-else.ts +0 -48
  16. package/test/abstract-sql/cast.ts +0 -25
  17. package/test/abstract-sql/coalesce.ts +0 -24
  18. package/test/abstract-sql/comparisons.ts +0 -360
  19. package/test/abstract-sql/dates.ts +0 -512
  20. package/test/abstract-sql/duration.ts +0 -56
  21. package/test/abstract-sql/empty-query-optimisations.ts +0 -54
  22. package/test/abstract-sql/functions-wrapper.ts +0 -70
  23. package/test/abstract-sql/get-referenced-fields.ts +0 -674
  24. package/test/abstract-sql/get-rule-referenced-fields.ts +0 -345
  25. package/test/abstract-sql/insert-query.ts +0 -22
  26. package/test/abstract-sql/is-distinct.ts +0 -102
  27. package/test/abstract-sql/joins.ts +0 -84
  28. package/test/abstract-sql/json.ts +0 -58
  29. package/test/abstract-sql/math.ts +0 -467
  30. package/test/abstract-sql/nested-in-optimisations.ts +0 -200
  31. package/test/abstract-sql/not-not-optimisations.ts +0 -15
  32. package/test/abstract-sql/schema-checks.ts +0 -168
  33. package/test/abstract-sql/schema-informative-reference.ts +0 -420
  34. package/test/abstract-sql/schema-rule-optimization.ts +0 -120
  35. package/test/abstract-sql/schema-rule-to-check.ts +0 -393
  36. package/test/abstract-sql/schema-views.ts +0 -73
  37. package/test/abstract-sql/test.ts +0 -192
  38. package/test/abstract-sql/text.ts +0 -168
  39. package/test/model.sbvr +0 -60
  40. package/test/odata/expand.ts +0 -674
  41. package/test/odata/fields.ts +0 -59
  42. package/test/odata/filterby.ts +0 -1517
  43. package/test/odata/orderby.ts +0 -96
  44. package/test/odata/paging.ts +0 -48
  45. package/test/odata/resource-parsing.ts +0 -568
  46. package/test/odata/select.ts +0 -119
  47. package/test/odata/stress.ts +0 -93
  48. package/test/odata/test.ts +0 -297
  49. package/test/sbvr/pilots.ts +0 -1097
  50. package/test/sbvr/reference-type.ts +0 -211
  51. package/test/sbvr/test.ts +0 -101
  52. package/tsconfig.build.json +0 -6
  53. package/tsconfig.json +0 -25
@@ -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
- });