@malloydata/malloy 0.0.80-dev230830211713 → 0.0.80-dev230903192621

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.
@@ -0,0 +1,998 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright 2023 Google LLC
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining
6
+ * a copy of this software and associated documentation files
7
+ * (the "Software"), to deal in the Software without restriction,
8
+ * including without limitation the rights to use, copy, modify, merge,
9
+ * publish, distribute, sublicense, and/or sell copies of the Software,
10
+ * and to permit persons to whom the Software is furnished to do so,
11
+ * subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be
14
+ * included in all copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ const test_translator_1 = require("./test-translator");
26
+ require("./parse-expects");
27
+ const model_1 = require("../../model");
28
+ describe('query:', () => {
29
+ test('anonymous query', () => {
30
+ expect((0, test_translator_1.markSource) `query: ${"table('aTable') -> { group_by: astr }"}`).toTranslate();
31
+ });
32
+ test('anonymous query', () => {
33
+ expect((0, test_translator_1.markSource) `##! m4warnings
34
+ query: ${"conn.table('aTable') -> { group_by: astr }"}`).toTranslateWithWarnings('Anonymous `query:` statements are deprecated, use `run:` instead');
35
+ });
36
+ test('run query', () => expect("run: table('aTable') -> { group_by: astr }").toTranslate());
37
+ test('run query ref', () => expect(`
38
+ query: foo is table('aTable') -> { group_by: astr }
39
+ run: foo
40
+ `).toTranslate());
41
+ test('query', () => expect("query: name is table('aTable') -> { group_by: astr }").toTranslate());
42
+ test('query', () => {
43
+ expect("query: name is table('aTable') -> { group_by: astr }").toTranslate();
44
+ });
45
+ test('query from query', () => {
46
+ expect(`
47
+ query: q1 is ab->{ group_by: astr limit: 10 }
48
+ query: q2 is ->q1
49
+ `).toTranslate();
50
+ });
51
+ test('query with refinements from query', () => {
52
+ expect(`
53
+ query: q1 is ab->{ group_by: astr limit: 10 }
54
+ query: q2 is ->q1 { aggregate: acount }
55
+ `).toTranslate();
56
+ });
57
+ test('chained query operations', () => {
58
+ expect(`
59
+ query: ab
60
+ -> { group_by: astr; aggregate: acount }
61
+ -> { top: 5; where: astr ~ 'a%' group_by: astr }
62
+ `).toTranslate();
63
+ });
64
+ test('from(query) refined into query', () => {
65
+ expect('query: from(ab -> {group_by: astr}) { dimension: bigstr is upper(astr) } -> { group_by: bigstr }').toTranslate();
66
+ });
67
+ test('query with shortcut filtered turtle', () => {
68
+ expect("query: allA is ab->aturtle {? astr ~ 'a%' }").toTranslate();
69
+ });
70
+ test('query with shortcut filtered turtle m4warning', () => {
71
+ expect(`
72
+ ##! m4warnings
73
+ query: allA is ab->aturtle refine {? astr ~ 'a%' }
74
+ `).toTranslateWithWarnings('Filter shortcut `{? condition }` is deprecated; use `{ where: condition } instead');
75
+ });
76
+ test('query with filtered turtle', () => {
77
+ expect("query: allA is ab->aturtle { where: astr ~ 'a%' }").toTranslate();
78
+ });
79
+ test('nest: in group_by:', () => {
80
+ expect(`
81
+ query: ab -> {
82
+ group_by: astr;
83
+ nest: nested_count is {
84
+ aggregate: acount
85
+ }
86
+ }
87
+ `).toTranslate();
88
+ });
89
+ test('reduce pipe project', () => {
90
+ expect(`
91
+ query: a -> { aggregate: f is count() } -> { project: f2 is f + 1 }
92
+ `).toTranslate();
93
+ });
94
+ test('refine and extend query', () => {
95
+ expect(`
96
+ query: a_by_str is a -> { group_by: astr }
97
+ query: -> a_by_str { aggregate: str_count is count() }
98
+ `).toTranslate();
99
+ });
100
+ test('query refinement preserves original', () => {
101
+ const x = new test_translator_1.TestTranslator(`
102
+ query: q is a -> { aggregate: acount is count() }
103
+ query: nq is -> q + { group_by: astr }
104
+ `);
105
+ expect(x).toTranslate();
106
+ const q = x.getQuery('q');
107
+ expect(q).toBeDefined();
108
+ if (q) {
109
+ const qFields = q.pipeline[0].fields;
110
+ expect(qFields.length).toBe(1);
111
+ }
112
+ });
113
+ test('query composition preserves original', () => {
114
+ const x = new test_translator_1.TestTranslator(`
115
+ query: q is ab -> { aggregate: acount }
116
+ query: nq is -> q -> { project: * }
117
+ `);
118
+ expect(x).toTranslate();
119
+ const q = x.getQuery('q');
120
+ expect(q).toBeDefined();
121
+ if (q) {
122
+ expect(q.pipeline.length).toBe(1);
123
+ }
124
+ });
125
+ test('all ungroup with args', () => {
126
+ expect(`
127
+ query: a -> {
128
+ group_by: astr
129
+ nest: by_int is {
130
+ group_by: ai
131
+ aggregate: bi_count is all(count(), ai)
132
+ }
133
+ }
134
+ `).toTranslate();
135
+ });
136
+ test('all ungroup checks args', () => {
137
+ expect(`
138
+ query: a -> {
139
+ group_by: astr
140
+ nest: by_int is {
141
+ group_by: ai
142
+ aggregate: bi_count is all(count(), afloat)
143
+ }
144
+ }
145
+ `).translationToFailWith("all() 'afloat' is missing from query output");
146
+ });
147
+ test('exclude ungroup with args', () => {
148
+ expect(`
149
+ query: a -> {
150
+ group_by: aa is 'a'
151
+ nest: by_b is {
152
+ group_by: bb is 'b'
153
+ nest: by_c is {
154
+ group_by: cc is 'c'
155
+ aggregate: bb_count is exclude(count(), aa, cc)
156
+ }
157
+ }
158
+ }
159
+ `).toTranslate();
160
+ });
161
+ test('exclude ungroup checks args', () => {
162
+ expect(`
163
+ query: a -> {
164
+ group_by: aa is 'a'
165
+ nest: by_b is {
166
+ group_by: bb is 'b'
167
+ nest: by_c is {
168
+ group_by: cc is 'c'
169
+ aggregate: bb_count is exclude(count(), aaa, cc)
170
+ }
171
+ }
172
+ }
173
+ `).translationToFailWith("exclude() 'aaa' is missing from query output");
174
+ });
175
+ test('exclude problem revealed by production models', () => {
176
+ expect(`
177
+ source: carriers is table('malloytest.carriers') {
178
+ primary_key: code
179
+ }
180
+ source: flights is table('malloytest.flights') {
181
+ primary_key: id2
182
+ join_one: carriers with carrier
183
+
184
+ query: carrier_overview is {
185
+ group_by: carrier_name is carriers.nickname
186
+ nest: top_destinations is {
187
+ group_by: destination
188
+ aggregate:
189
+ flights_to_dest is exclude(count(), carrier_name)*100
190
+ }
191
+ }
192
+ }
193
+ `).toTranslate();
194
+ });
195
+ describe('query operation typechecking', () => {
196
+ describe('field declarations', () => {
197
+ test('cannot use aggregate in group_by', () => {
198
+ expect('query: a -> { group_by: s is count()}').translationToFailWith('Cannot use an aggregate field in a group_by operation, did you mean to use an aggregate operation instead?');
199
+ });
200
+ test('cannot use ungrouped_aggregate in group_by', () => {
201
+ expect('query: a -> { group_by: s is all(count())}').translationToFailWith('Cannot use an aggregate field in a group_by operation, did you mean to use an aggregate operation instead?');
202
+ });
203
+ test('cannot use analytic in group_by', () => {
204
+ expect('query: a -> { group_by: s is row_number()}').translationToFailWith('Cannot use an analytic field in a group_by operation, did you mean to use a calculate operation instead?');
205
+ });
206
+ test('cannot use aggregate in dimension', () => {
207
+ expect('source: a1 is a { dimension: s is count()}').translationToFailWith('Cannot use an aggregate field in a dimension declaration, did you mean to use a measure declaration instead?');
208
+ });
209
+ test('cannot use ungrouped_aggregate in dimension', () => {
210
+ expect('source: a1 is a { dimension: s is all(count())}').translationToFailWith('Cannot use an aggregate field in a dimension declaration, did you mean to use a measure declaration instead?');
211
+ });
212
+ test('cannot use analytic in dimension', () => {
213
+ expect('source: a1 is a { dimension: s is row_number()}').translationToFailWith('Cannot use an analytic field in a dimension declaration');
214
+ });
215
+ test('cannot use scalar in measure', () => {
216
+ expect('source: a1 is a { measure: s is 1}').translationToFailWith('Cannot use a scalar field in a measure declaration, did you mean to use a dimension declaration instead?');
217
+ });
218
+ test('cannot use analytic in measure', () => {
219
+ expect('source: a1 is a { measure: s is lag(count())}').translationToFailWith('Cannot use an analytic field in a measure declaration');
220
+ });
221
+ test('cannot use scalar in aggregate', () => {
222
+ expect('query: a -> { aggregate: s is 1}').translationToFailWith('Cannot use a scalar field in an aggregate operation, did you mean to use a group_by or project operation instead?');
223
+ });
224
+ test('cannot use analytic in aggregate', () => {
225
+ expect('query: a -> { aggregate: s is lag(count())}').translationToFailWith('Cannot use an analytic field in an aggregate operation, did you mean to use a calculate operation instead?');
226
+ });
227
+ test('cannot use scalar in calculate', () => {
228
+ expect('query: a -> { group_by: a is 1; calculate: s is 1 }').translationToFailWith('Cannot use a scalar field in a calculate operation, did you mean to use a group_by or project operation instead?');
229
+ });
230
+ test('cannot use aggregate in calculate', () => {
231
+ expect('query: a -> { group_by: a is 1; calculate: s is count() }').translationToFailWith('Cannot use an aggregate field in a calculate operation, did you mean to use an aggregate operation instead?');
232
+ });
233
+ test('cannot use aggregate in project', () => {
234
+ expect('query: a -> { project: s is count() }').translationToFailWith('Cannot use an aggregate field in a project operation, did you mean to use an aggregate operation instead?');
235
+ });
236
+ test('cannot use analytic in project', () => {
237
+ expect('query: a -> { project: s is row_number() }').translationToFailWith('Cannot use an analytic field in a project operation, did you mean to use a calculate operation instead?');
238
+ });
239
+ test('cannot use analytic in declare', () => {
240
+ expect('query: a -> { group_by: a is 1; declare: s is row_number() }').translationToFailWith('Analytic expressions can not be used in a declare block');
241
+ });
242
+ test('cannot use aggregate in index', () => {
243
+ expect('query: a { measure: acount is count() } -> { index: acount }').translationToFailWith('Cannot use an aggregate field in an index operation');
244
+ });
245
+ test('can use aggregate in except', () => {
246
+ expect(`
247
+ source: b1 is a { measure: acount is count() }
248
+ source: c1 is b1 { except: acount }
249
+ `).toTranslate();
250
+ });
251
+ });
252
+ describe('field references', () => {
253
+ test('cannot use aggregate in group_by', () => {
254
+ expect('query: a -> { declare: acount is count(); group_by: acount }').translationToFailWith('Cannot use an aggregate field in a group_by operation, did you mean to use an aggregate operation instead?');
255
+ });
256
+ test('cannot use query in group_by', () => {
257
+ expect('query: a { query: q is { group_by: x is 1 } } -> { group_by: q }').translationToFailWith('Cannot use a query field in a group_by operation, did you mean to use a nest operation instead?');
258
+ });
259
+ test('cannot use scalar in aggregate', () => {
260
+ expect('query: a -> { declare: aconst is 1; aggregate: aconst }').translationToFailWith('Cannot use a scalar field in an aggregate operation, did you mean to use a group_by or project operation instead?');
261
+ });
262
+ test('cannot use scalar in calculate', () => {
263
+ expect('query: a -> { declare: aconst is 1; group_by: x is 1; calculate: aconst }').translationToFailWith('Cannot use a scalar field in a calculate operation, did you mean to use a group_by or project operation instead?');
264
+ });
265
+ test('cannot use aggregate in calculate', () => {
266
+ expect('query: a -> { declare: acount is count(); group_by: x is 1; calculate: acount }').translationToFailWith('Cannot use an aggregate field in a calculate operation, did you mean to use an aggregate operation instead?');
267
+ });
268
+ test('cannot use query in project', () => {
269
+ expect('query: a { query: q is { group_by: x is 1 } } -> { project: q }').translationToFailWith('Cannot use a query field in a project operation, did you mean to use a nest operation instead?');
270
+ });
271
+ test('cannot use query in index', () => {
272
+ expect('query: a { query: q is { group_by: x is 1 } } -> { index: q }').translationToFailWith('Cannot use a query field in an index operation');
273
+ });
274
+ test('cannot use query in calculate', () => {
275
+ expect('query: a { query: q is { group_by: x is 1 } } -> { group_by: x is 1; calculate: q }').translationToFailWith('Cannot use a query field in a calculate operation, did you mean to use a nest operation instead?');
276
+ });
277
+ test('cannot use query in aggregate', () => {
278
+ expect('query: a { query: q is { group_by: x is 1 } } -> { aggregate: q }').translationToFailWith('Cannot use a query field in an aggregate operation, did you mean to use a nest operation instead?');
279
+ });
280
+ test('cannot use aggregate in calculate, preserved over refinement', () => {
281
+ expect(`query: a1 is a -> {
282
+ aggregate: c is count()
283
+ }
284
+ query: -> a1 {
285
+ calculate: b is c
286
+ }`).translationToFailWith('Cannot use an aggregate field in a calculate operation, did you mean to use an aggregate operation instead?');
287
+ });
288
+ test('cannot use scalar in calculate, preserved over refinement', () => {
289
+ expect(`query: a1 is a -> {
290
+ group_by: c is 1
291
+ }
292
+ query: -> a1 {
293
+ calculate: b is c
294
+ }`).translationToFailWith('Cannot use a scalar field in a calculate operation, did you mean to use a group_by or project operation instead?');
295
+ });
296
+ test('cannot use analytic in group_by, preserved over refinement', () => {
297
+ expect(`query: a1 is a -> {
298
+ group_by: c is 1
299
+ calculate: c2 is lag(c)
300
+ }
301
+ query: -> a1 {
302
+ group_by: b is c2
303
+ }`).translationToFailWith(
304
+ // c2 is not defined because group_by doesn't know to look in the output space
305
+ "'c2' is not defined");
306
+ });
307
+ test('cannot use analytic in order_by, preserved over refinement', () => {
308
+ expect(`query: a1 is a -> {
309
+ group_by: c is 1
310
+ calculate: c2 is lag(c)
311
+ }
312
+ query: -> a1 {
313
+ order_by: c2
314
+ }`).translationToFailWith('Illegal order by of analytic field c2');
315
+ });
316
+ test('cannot ungroup an ungrouped', () => {
317
+ expect(`query: a1 is a -> {
318
+ group_by: c is 1
319
+ aggregate: c2 is all(all(sum(ai)))
320
+ }`).translationToFailWith('all() expression must not already be ungrouped');
321
+ });
322
+ test('cannot aggregate an ungrouped', () => {
323
+ expect(`query: a1 is a -> {
324
+ group_by: c is 1
325
+ aggregate: c2 is sum(all(sum(ai)))
326
+ }`).translationToFailWith('Aggregate expression cannot be aggregate');
327
+ });
328
+ test('cannot aggregate an aggregate', () => {
329
+ expect(`query: a1 is a -> {
330
+ group_by: c is 1
331
+ aggregate: c2 is sum(sum(ai))
332
+ }`).translationToFailWith('Aggregate expression cannot be aggregate');
333
+ });
334
+ test('can use field def in group_by, preserved over refinement', () => {
335
+ expect(`query: a1 is a -> {
336
+ group_by: c is 1
337
+ }
338
+ query: -> a1 {
339
+ order_by: c
340
+ }`).toTranslate();
341
+ });
342
+ test('can use field ref in group_by, preserved over refinement', () => {
343
+ expect(`query: a1 is a -> {
344
+ group_by: c is astr
345
+ }
346
+ query: -> a1 {
347
+ order_by: c
348
+ }`).toTranslate();
349
+ });
350
+ });
351
+ });
352
+ describe('function typechecking', () => {
353
+ test('use function correctly', () => {
354
+ expect(`query: a -> {
355
+ group_by: s is concat('a', 'b')
356
+ }`).toTranslate();
357
+ });
358
+ test('function incorrect case', () => {
359
+ expect(`query: a -> {
360
+ group_by: s is CONCAT('a', 'b')
361
+ }`).toTranslateWithWarnings("Case insensitivity for function names is deprecated, use 'concat' instead");
362
+ });
363
+ test('function no matching overload', () => {
364
+ expect(`query: a -> {
365
+ group_by: s is floor('a', 'b')
366
+ }`).translationToFailWith('No matching overload for function floor(string, string)');
367
+ });
368
+ test('unknown function', () => {
369
+ expect(`query: a -> {
370
+ group_by: s is asdfasdf()
371
+ }`).translationToFailWith("Unknown function 'asdfasdf'. Use 'asdfasdf!(...)' to call a SQL function directly.");
372
+ });
373
+ test('can select different overload', () => {
374
+ expect('query: a -> { group_by: s is concat() }').toTranslate();
375
+ });
376
+ test('can pass different expression types', () => {
377
+ expect(`query: a -> {
378
+ group_by: f1 is sqrt(1)
379
+ aggregate: f2 is sqrt(count())
380
+ aggregate: f3 is sqrt(all(count()))
381
+ calculate: f4 is sqrt(lag(f1))
382
+ calculate: f5 is sqrt(lag(count()))
383
+ }`).toTranslate();
384
+ });
385
+ test('function return type correct', () => {
386
+ expect(`query: a -> {
387
+ group_by: s is floor(1.2) + 1
388
+ }`).toTranslate();
389
+ });
390
+ test('function return type incorrect', () => {
391
+ expect(`query: a -> {
392
+ group_by: s is floor(1.2) + 'a'
393
+ }`).translationToFailWith("Non numeric('number,string') value with '+'");
394
+ });
395
+ test('can use output value in calculate', () => {
396
+ expect(`query: a -> {
397
+ group_by: x is 1
398
+ calculate: s is lag(x)
399
+ }`).toTranslate();
400
+ });
401
+ test('cannot use output value in group_by', () => {
402
+ expect(`query: a -> {
403
+ group_by: x is 1
404
+ group_by: y is x
405
+ }`).translationToFailWith("'x' is not defined");
406
+ });
407
+ test('lag can check that other args are constant', () => {
408
+ expect(`query: a -> {
409
+ group_by: x is 1
410
+ calculate: s is lag(x, 1, x)
411
+ }`).translationToFailWith(
412
+ // TODO improve this error message
413
+ "Parameter 3 ('default') of lag must be literal or constant, but received output");
414
+ });
415
+ test('lag can check that other args are literal', () => {
416
+ expect(`query: a -> {
417
+ group_by: x is 1
418
+ calculate: s is lag(x, 1 + 1)
419
+ }`).translationToFailWith(
420
+ // TODO improve this error message
421
+ "Parameter 2 ('offset') of lag must be literal, but received constant");
422
+ });
423
+ test('lag can check that other args are nonnull', () => {
424
+ expect(`query: a -> {
425
+ group_by: x is 1
426
+ calculate: s is lag(x, null)
427
+ }`).translationToFailWith("Parameter 2 ('offset') of lag must not be a literal null");
428
+ });
429
+ test('lag can use constant values for other args', () => {
430
+ expect(`query: a -> {
431
+ group_by: x is 1
432
+ calculate: s is lag(x, 2)
433
+ }`).toTranslate();
434
+ });
435
+ test('cannot name top level objects same as functions', () => {
436
+ expect((0, test_translator_1.markSource) `query: ${'concat is a -> { group_by: x is 1 }'}`).translationToFailWith(
437
+ // TODO improve this error message
438
+ "'concat' is already defined, cannot redefine");
439
+ });
440
+ test('`now` is considered constant`', () => {
441
+ expect(`query: a -> {
442
+ group_by: n is now
443
+ calculate: l is lag(n, 1, now)
444
+ }`).toTranslate();
445
+ });
446
+ // TODO it might be nice to reference a field which is a constant, and be able to
447
+ // use that as a constant param. Same with a literal.
448
+ test('cannot use a field which is a constant in a constant param', () => {
449
+ expect(`query: a -> {
450
+ group_by: ai, pi is pi()
451
+ calculate: l is lag(ai, 1, pi)
452
+ }`).translationToFailWith("Parameter 3 ('default') of lag must be literal or constant, but received output");
453
+ });
454
+ // TODO we don't handle referencing a join as a field correctly in all cases today.
455
+ // For now, it at least is considered type `struct` and therefore fails to parse
456
+ // as a function argument.
457
+ // We add <join_name>_id to the query, but it's not included in the output space
458
+ test('cannot use struct in function arg', () => {
459
+ expect(`query: a {join_one: b with astr } -> {
460
+ group_by: b
461
+ calculate: foo is lag(b)
462
+ }`).translationToFailWith('No matching overload for function lag(struct)');
463
+ });
464
+ // TODO this doesn't work today, we're not rigorous enough with integer
465
+ // subtypes. But we should probably make this typecheck properly.
466
+ test.skip('cannot use float in round precision', () => {
467
+ expect(`query: a -> {
468
+ group_by: x is round(1.5, 1.6)
469
+ }`).translationToFailWith(
470
+ // TODO improve this error message
471
+ "Parameter 2 ('precision') for round must be integer, received float");
472
+ });
473
+ test('cannot use stddev with no arguments', () => {
474
+ expect(`query: a -> {
475
+ aggregate: x is stddev()
476
+ }`).translationToFailWith('No matching overload for function stddev()');
477
+ });
478
+ test('can use stddev with postfix syntax', () => {
479
+ expect(`query: a -> {
480
+ declare: y is 1
481
+ aggregate: x is y.stddev()
482
+ }`).toTranslate();
483
+ });
484
+ test('can use stddev with postfix syntax on join', () => {
485
+ expect(`query: a -> {
486
+ join_one: b with astr
487
+ aggregate: x is b.ai.stddev()
488
+ }`).toTranslate();
489
+ });
490
+ test('can use calculate with a measure', () => {
491
+ expect(`query: a { measure: c is count() } -> {
492
+ group_by: y is 1
493
+ calculate: x is lag(c)
494
+ }`).toTranslate();
495
+ });
496
+ test('cannot use calculate with input fields', () => {
497
+ expect(`query: a -> {
498
+ group_by: y is 1
499
+ calculate: x is lag(ai)
500
+ }`).translationToFailWith(
501
+ // TODO improve this error message:
502
+ // Parameter 1 ('value') of 'lag' must be a constant, an aggregate, or an expression using
503
+ // only fields that appear in the query output. Received an expression which uses a field
504
+ // that is not in the query output.
505
+ "Parameter 1 ('value') of lag must be literal, constant or output, but received input");
506
+ });
507
+ test('can use calculate with aggregate field which is not in query', () => {
508
+ expect(`query: a { measure: acount is count() } -> {
509
+ group_by: astr
510
+ calculate: pc is lag(acount)
511
+ }`).toTranslate();
512
+ });
513
+ test('cannot use agregate as argument to agg function', () => {
514
+ expect(`query: a -> {
515
+ aggregate: x is stddev(count())
516
+ }`).translationToFailWith("Parameter 1 ('value') of stddev must be scalar, but received aggregate");
517
+ });
518
+ test('cannot use calculate with no other fields', () => {
519
+ expect(`query: a -> {
520
+ calculate: x is row_number()
521
+ }`).translationToFailWith("Can't determine query type (group_by/aggregate/nest,project,index)");
522
+ });
523
+ // TODO someday make it so we can order by an analytic function
524
+ test('today: cannot order by analytic function', () => {
525
+ expect(`query: a -> {
526
+ group_by: astr
527
+ calculate: row_num is row_number()
528
+ order_by: row_num desc
529
+ }`).translationToFailWith('Illegal order by of analytic field row_num');
530
+ });
531
+ test('cannot use analytic in calculate -- and preserved over refinement', () => {
532
+ expect(`query: a1 is a -> {
533
+ group_by: astr
534
+ calculate: p is lag(astr)
535
+ }
536
+ query: -> a1 {
537
+ calculate: p1 is lag(p)
538
+ }`).translationToFailWith("Parameter 1 ('value') of lag must be scalar or aggregate, but received scalar_analytic");
539
+ });
540
+ test('cannot use aggregate analytic in project', () => {
541
+ expect(`query: a -> {
542
+ project: astr
543
+ calculate: p is lag(count())
544
+ }`).translationToFailWith('Cannot add aggregate analyics to project');
545
+ });
546
+ test('reference field in join', () => {
547
+ expect(`query: a -> {
548
+ join_one: b with astr
549
+ group_by: b.ai
550
+ }`).toTranslate();
551
+ });
552
+ // TODO decide whether this syntax should be legal, really...
553
+ test('reference join name in group_by', () => {
554
+ expect(`query: a -> {
555
+ join_one: b with astr
556
+ group_by: b
557
+ }`).toTranslate();
558
+ });
559
+ test('can reference project: inline join.* field in calculate', () => {
560
+ expect(`query: a -> {
561
+ join_one: b with astr
562
+ project: b.*
563
+ calculate: s is lag(ai)
564
+ }`).toTranslate();
565
+ });
566
+ test('can reference project: join.* field in calculate', () => {
567
+ expect(`query: a { join_one: b with astr } -> {
568
+ project: b.*
569
+ calculate: s is lag(ai)
570
+ }`).toTranslate();
571
+ });
572
+ test('can reference implied output entries in calculate', () => {
573
+ expect(`source: aaa is a extend { dimension: big_i is ai+10 }
574
+ query: a extend { join_one: aaa on astr=aaa.astr } -> {
575
+ group_by: aaa.big_i
576
+ aggregate: s is aaa.big_i.sum()
577
+ calculate: a is lag(big_i.sum())
578
+ }`).toTranslate();
579
+ });
580
+ });
581
+ describe('qops', () => {
582
+ test('group by single', () => {
583
+ expect('query: a->{ group_by: astr }').toTranslate();
584
+ });
585
+ test("group_by x is x'", () => {
586
+ expect('query: a->{ group_by: ai is ai/2 }').toTranslate();
587
+ });
588
+ test('group by multiple', () => {
589
+ expect('query: a->{ group_by: astr,ai }').toTranslate();
590
+ });
591
+ test('aggregate single', () => {
592
+ expect('query: a->{ aggregate: num is count() }').toTranslate();
593
+ });
594
+ test('calculate in reduce', () => {
595
+ expect('query: a->{ group_by: astr, ai calculate: num is lag(ai) }').toTranslate();
596
+ });
597
+ test('calculate in project', () => {
598
+ expect('query: a->{ project: astr, ai calculate: num is lag(ai) }').toTranslate();
599
+ });
600
+ test('aggregate reference', () => {
601
+ var _a;
602
+ const doc = (0, test_translator_1.model) `query: a->{ aggregate: ai.sum() }`;
603
+ expect(doc).toTranslate();
604
+ const q = doc.translator.getQuery(0);
605
+ expect(q).toBeDefined();
606
+ const ai = (_a = q === null || q === void 0 ? void 0 : q.pipeline[0]) === null || _a === void 0 ? void 0 : _a.fields[0];
607
+ expect(ai).toMatchObject({
608
+ name: 'ai',
609
+ type: 'number',
610
+ expressionType: 'aggregate',
611
+ });
612
+ });
613
+ test('timeunit reference', () => {
614
+ var _a;
615
+ const doc = (0, test_translator_1.model) `query: a->{ group_by: ats.day }`;
616
+ expect(doc).toTranslate();
617
+ const q = doc.translator.getQuery(0);
618
+ expect(q).toBeDefined();
619
+ const ats = (_a = q === null || q === void 0 ? void 0 : q.pipeline[0]) === null || _a === void 0 ? void 0 : _a.fields[0];
620
+ expect(ats).toMatchObject({ name: 'ats', type: 'timestamp' });
621
+ });
622
+ test('aggregate multiple', () => {
623
+ expect(`
624
+ query: a->{
625
+ aggregate: num is count(), total is sum(ai)
626
+ }
627
+ `).toTranslate();
628
+ });
629
+ test('project ref', () => {
630
+ expect('query:ab->{ project: b.astr }').toTranslate();
631
+ });
632
+ test('project *', () => {
633
+ expect('query:ab->{ project: * }').toTranslate();
634
+ });
635
+ test('project def', () => {
636
+ expect('query:ab->{ project: one is 1 }').toTranslate();
637
+ });
638
+ test('project multiple', () => {
639
+ expect(`
640
+ query: a->{
641
+ project: one is 1, astr
642
+ }
643
+ `).toTranslate();
644
+ });
645
+ test('index single', () => {
646
+ expect('query:a->{index: astr}').toTranslate();
647
+ });
648
+ test('index path', () => {
649
+ expect('query:ab->{index: ab.astr}').toTranslate();
650
+ });
651
+ test('index unique on path', () => {
652
+ expect('query:ab->{index: b.astr, ab.astr}').toTranslate();
653
+ });
654
+ test('index join.*', () => {
655
+ expect('query:ab->{index: ab.*}').toTranslate();
656
+ });
657
+ test('index multiple', () => {
658
+ const model = new test_translator_1.TestTranslator('query:a->{index: af, astr}');
659
+ expect(model).toTranslate();
660
+ const q = model.getQuery(0);
661
+ expect(q).toBeDefined();
662
+ if (q) {
663
+ const index = q.pipeline[0];
664
+ expect(index.type).toBe('index');
665
+ expect(index.fields).toEqual(['af', 'astr']);
666
+ }
667
+ });
668
+ test('index star', () => {
669
+ const model = new test_translator_1.TestTranslator('query:a->{index: *, astr}');
670
+ expect(model).toTranslate();
671
+ const q = model.getQuery(0);
672
+ expect(q).toBeDefined();
673
+ if (q) {
674
+ const index = q.pipeline[0];
675
+ expect(index.type).toBe('index');
676
+ expect(index.fields).toEqual(['*', 'astr']);
677
+ }
678
+ });
679
+ test('index by', () => {
680
+ expect('query:a->{index: * by ai}').toTranslate();
681
+ });
682
+ test('index sampled', () => {
683
+ expect('query:a->{index: *; sample: true}').toTranslate();
684
+ });
685
+ test('index unsampled', () => {
686
+ expect('query:a->{index: *; sample: false}').toTranslate();
687
+ });
688
+ test('index sample-percent', () => {
689
+ const model = new test_translator_1.TestTranslator('query:a->{index: *; sample: 42%}');
690
+ expect(model).toTranslate();
691
+ const q = model.getQuery(0);
692
+ expect(q).toBeDefined();
693
+ if (q) {
694
+ const index = q.pipeline[0];
695
+ expect(index.type).toBe('index');
696
+ if (index.type === 'index') {
697
+ expect(index.sample).toEqual({ percent: 42 });
698
+ }
699
+ }
700
+ });
701
+ test('index sample-rows', () => {
702
+ expect('query:a->{index: *; sample: 100000}').toTranslate();
703
+ });
704
+ test('top N', () => {
705
+ expect('query: a->{ top: 5; group_by: astr }').toTranslate();
706
+ });
707
+ test('top N by field', () => {
708
+ expect('query: a->{top: 5 by astr; group_by: astr}').toTranslate();
709
+ });
710
+ test('top N by expression', () => {
711
+ expect('query: ab->{top: 5 by ai + 1; group_by: ai}').toTranslate();
712
+ });
713
+ test('top N by field must be in the output space', () => expect('query: a->{top: 5 by af; group_by: astr}').translationToFailWith('Unknown field af in output space'));
714
+ test('limit N', () => {
715
+ expect('query: a->{ limit: 5; group_by: astr }').toTranslate();
716
+ });
717
+ test('order by', () => {
718
+ expect('query: a->{ order_by: astr; group_by: astr }').toTranslate();
719
+ });
720
+ test('order by preserved over refinement', () => {
721
+ expect(`
722
+ query: a1 is a -> { group_by: astr }
723
+ query: -> a1 { order_by: astr }
724
+ `).toTranslate();
725
+ });
726
+ test('order by must be in the output space', () => expect('query: a -> { order_by: af; group_by: astr }').translationToFailWith('Unknown field af in output space'));
727
+ test('order by asc', () => {
728
+ expect('query: a->{ order_by: astr asc; group_by: astr }').toTranslate();
729
+ });
730
+ test('order by desc', () => {
731
+ expect('query: a->{ order_by: astr desc; group_by: astr }').toTranslate();
732
+ });
733
+ test('order by N', () => {
734
+ expect('query: a->{ order_by: 1 asc; group_by: astr }').toTranslate();
735
+ });
736
+ test('order by multiple', () => {
737
+ expect(`
738
+ query: a->{
739
+ order_by: 1 asc, af desc
740
+ group_by: astr, af
741
+ }
742
+ `).toTranslate();
743
+ });
744
+ test('where single', () => {
745
+ expect('query:a->{ group_by: astr; where: af > 10 }').toTranslate();
746
+ });
747
+ test('having single', () => {
748
+ expect('query:ab->{ aggregate: acount; group_by: astr; having: acount > 10 }').toTranslate();
749
+ });
750
+ test('compound having still works', () => {
751
+ expect('query:ab->{ aggregate: acount; having: acount > 10 and acount < 100 }').toTranslate();
752
+ });
753
+ test('compound aggregate still works', () => {
754
+ expect('query:ab->{ aggregate: thing is acount > 10 and acount < 100 }').toTranslate();
755
+ });
756
+ test('where multiple', () => {
757
+ expect("query:a->{ group_by: astr; where: af > 10,astr~'a%' }").toTranslate();
758
+ });
759
+ test('filters preserve source formatting in code:', () => {
760
+ const model = new test_translator_1.TestTranslator("source: notb is a + { where: astr != 'b' }");
761
+ expect(model).toTranslate();
762
+ const notb = model.getSourceDef('notb');
763
+ expect(notb).toBeDefined();
764
+ if (notb) {
765
+ const f = notb.filterList;
766
+ expect(f).toBeDefined();
767
+ if (f) {
768
+ expect(f[0].code).toBe("astr != 'b'");
769
+ }
770
+ }
771
+ });
772
+ test('field expressions preserve source formatting in code:', () => {
773
+ const model = new test_translator_1.TestTranslator('source: notb is a + { dimension: d is 1 + 2 }');
774
+ expect(model).toTranslate();
775
+ const notb = model.getSourceDef('notb');
776
+ expect(notb).toBeDefined();
777
+ if (notb) {
778
+ const d = notb.fields.find(f => f.as || f.name === 'd');
779
+ expect(d).toBeDefined();
780
+ expect(d === null || d === void 0 ? void 0 : d.type).toBe('number');
781
+ if ((d === null || d === void 0 ? void 0 : d.type) === 'number') {
782
+ expect(d.code).toBe('1 + 2');
783
+ }
784
+ }
785
+ });
786
+ test('nest single', () => {
787
+ expect(`
788
+ query: a->{
789
+ group_by: ai
790
+ nest: nestbystr is { group_by: astr; aggregate: N is count() }
791
+ }
792
+ `).toTranslate();
793
+ });
794
+ test('nest multiple', () => expect(`
795
+ query: a->{
796
+ group_by: ai
797
+ nest:
798
+ nestbystr is { group_by: astr; aggregate: N is count() },
799
+ renest is { group_by: astr; aggregate: N is count() }
800
+ }
801
+ `).toTranslate());
802
+ test('nest ref', () => expect('query: ab->{group_by: ai; nest: aturtle}').toTranslate());
803
+ describe('extend block', () => {
804
+ test('works with dimension', () => {
805
+ expect('query: a -> { extend: { dimension: x is 1 }; group_by: x }').toTranslate();
806
+ });
807
+ test('works with measure', () => {
808
+ expect('query: a -> { extend: { measure: x is count() }; aggregate: x }').toTranslate();
809
+ });
810
+ test('works with join_one', () => {
811
+ expect('query: a -> { extend: { join_one: bb is b on bb.astr = astr }; group_by: bb.astr }').toTranslate();
812
+ });
813
+ test('works with join_many', () => {
814
+ expect('query: a -> { extend: { join_many: b on astr = b.astr }; group_by: b.astr }').toTranslate();
815
+ });
816
+ test('works with join_cross', () => {
817
+ expect('query: a -> { extend: { join_cross: b on true }; group_by: b.astr }').toTranslate();
818
+ });
819
+ test('works with multiple in one block', () => {
820
+ expect('query: a -> { extend: { dimension: x is 1, y is 2 }; group_by: x, y }').toTranslate();
821
+ });
822
+ test('works with multiple blocks', () => {
823
+ expect('query: a -> { extend: { dimension: x is 1; dimension: y is 2; measure: c is count() }; group_by: x, y; aggregate: c }').toTranslate();
824
+ });
825
+ });
826
+ describe('declare/query join warnings', () => {
827
+ test('declare warning in query', () => {
828
+ expect((0, test_translator_1.markSource) `##! m4warnings
829
+ run: a -> { ${'declare: x is 1'}; group_by: x }`).toTranslateWithWarnings('`declare:` is deprecated; use `dimension:` or `measure:` inside a source or `extend:` block');
830
+ });
831
+ test('declare warning in source', () => {
832
+ expect((0, test_translator_1.markSource) `##! m4warnings
833
+ source: a2 is a extend { ${'declare: x is 1'} }`).toTranslateWithWarnings('`declare:` is deprecated; use `dimension:` or `measure:` inside a source or `extend:` block');
834
+ });
835
+ test('joins in query', () => {
836
+ expect((0, test_translator_1.markSource) `##! m4warnings
837
+ run: a -> { ${'join_one: b on true'}; ${'join_many: c is b on true'}; ${'join_cross: d is b on true'}; group_by: b.astr }`).toTranslateWithWarnings('Joins in queries are deprecated, move into an `extend:` block.', 'Joins in queries are deprecated, move into an `extend:` block.', 'Joins in queries are deprecated, move into an `extend:` block.');
838
+ });
839
+ });
840
+ test('refine query with extended source', () => {
841
+ const m = new test_translator_1.TestTranslator(`
842
+ source: nab is ab {
843
+ query: xturtle is aturtle + {
844
+ declare: aratio is ai / acount
845
+ }
846
+ }
847
+ query: nab -> xturtle + { aggregate: aratio }
848
+ `);
849
+ expect(m).toTranslate();
850
+ const t = m.translate();
851
+ if (t.translated) {
852
+ const q = t.translated.queryList[0].pipeline[0];
853
+ expect(q).toBeDefined();
854
+ if (q.type === 'reduce' && q.extendSource) {
855
+ expect(q.extendSource.length).toBe(1);
856
+ const f = q.extendSource[0];
857
+ expect(f.type).toBe('number');
858
+ if (f.type === 'number') {
859
+ expect((0, model_1.expressionIsCalculation)(f.expressionType)).toBeTruthy();
860
+ }
861
+ }
862
+ else {
863
+ fail('Did not generate extendSource');
864
+ }
865
+ }
866
+ });
867
+ test('refine query source with field', () => {
868
+ const m = new test_translator_1.TestTranslator(`
869
+ query: ab -> aturtle + {
870
+ declare: aratio is ai / acount
871
+ aggregate: aratio
872
+ }
873
+ `);
874
+ expect(m).toTranslate();
875
+ const t = m.translate();
876
+ if (t.translated) {
877
+ const q = t.translated.queryList[0].pipeline[0];
878
+ if (q.type === 'reduce' && q.extendSource) {
879
+ expect(q.extendSource.length).toBe(1);
880
+ const f = q.extendSource[0];
881
+ expect(f.type).toBe('number');
882
+ if (f.type === 'number') {
883
+ expect((0, model_1.expressionIsCalculation)(f.expressionType)).toBeTruthy();
884
+ }
885
+ }
886
+ else {
887
+ fail('Did not generate extendSource');
888
+ }
889
+ }
890
+ });
891
+ test('refine query source with join', () => {
892
+ const m = new test_translator_1.TestTranslator(`
893
+ query: ab -> aturtle + {
894
+ join_one: bb is b on bb.astr = astr
895
+ group_by: foo is bb.astr
896
+ }
897
+ `);
898
+ expect(m).toTranslate();
899
+ const t = m.translate();
900
+ if (t.translated) {
901
+ const q = t.translated.queryList[0].pipeline[0];
902
+ if (q.type === 'reduce' && q.extendSource) {
903
+ expect(q.extendSource.length).toBe(1);
904
+ expect(q.extendSource[0].type).toBe('struct');
905
+ }
906
+ else {
907
+ fail('Did not generate extendSource');
908
+ }
909
+ }
910
+ });
911
+ });
912
+ describe('refinement location rules', () => {
913
+ test('where clauses go into the first segment', () => {
914
+ const doc = (0, test_translator_1.model) `
915
+ query: refineme is a -> { project: stage is "stage1" } -> { project: stage is "stage2" }
916
+ query: checkme is refineme refine { where: astr = 'a' }`;
917
+ expect(doc).toTranslate();
918
+ const checkme = doc.translator.getQuery('checkme');
919
+ expect(checkme).toBeDefined();
920
+ if (checkme) {
921
+ const whereClause = checkme.pipeline[0].filterList;
922
+ expect(whereClause).toBeDefined();
923
+ if (whereClause) {
924
+ expect(whereClause.length).toBe(1);
925
+ }
926
+ }
927
+ });
928
+ test('having clauses go into the last segment', () => {
929
+ const doc = (0, test_translator_1.model) `
930
+ query: refineme is a -> { group_by: ai,astr } -> { group_by: ai, aggregate: ac is count() }
931
+ query: checkme is refineme refine { having: ac > 0 }`;
932
+ expect(doc).toTranslate();
933
+ const checkme = doc.translator.getQuery('checkme');
934
+ expect(checkme).toBeDefined();
935
+ if (checkme) {
936
+ const havingClause = checkme.pipeline[1].filterList;
937
+ expect(havingClause).toBeDefined();
938
+ if (havingClause) {
939
+ expect(havingClause.length).toBe(1);
940
+ }
941
+ }
942
+ });
943
+ test('limit goes into the last segment', () => {
944
+ const doc = (0, test_translator_1.model) `
945
+ query: refineme is a -> { group_by: ai,astr } -> { group_by: ai, aggregate: ac is count() }
946
+ query: checkme is refineme refine { limit: 1 }`;
947
+ expect(doc).toTranslate();
948
+ const checkme = doc.translator.getQuery('checkme');
949
+ expect(checkme).toBeDefined();
950
+ if (checkme) {
951
+ expect(checkme.pipeline[1]).toMatchObject({ limit: 1 });
952
+ }
953
+ });
954
+ test('order_by goes into the last segment', () => {
955
+ const doc = (0, test_translator_1.model) `
956
+ query: refineme is a -> { group_by: ai,astr } -> { group_by: ai, aggregate: ac is count() }
957
+ query: checkme is refineme refine { order_by: 2 }`;
958
+ expect(doc).toTranslate();
959
+ const checkme = doc.translator.getQuery('checkme');
960
+ expect(checkme).toBeDefined();
961
+ if (checkme) {
962
+ expect(checkme.pipeline[1]).toHaveProperty('orderBy');
963
+ }
964
+ });
965
+ const stageErr = 'Illegal in refinement of a query with more than one stage';
966
+ test('group_by illegal in long pipes', () => {
967
+ expect((0, test_translator_1.markSource) `query: refineme is a -> { project: stage is "stage1" } -> { project: stage is "stage2" }
968
+ query: checkme is refineme refine { ${'group_by: stage'} }`).translationToFailWith(stageErr);
969
+ });
970
+ test('aggregate illegal in long pipes', () => {
971
+ expect((0, test_translator_1.markSource) `query: refineme is a -> { project: stage is "stage1" } -> { project: stage is "stage2" }
972
+ query: checkme is refineme refine { ${'aggregate: c is count()'} }`).translationToFailWith(stageErr);
973
+ });
974
+ test('calcluate illegal in long pipes', () => {
975
+ expect((0, test_translator_1.markSource) `query: refineme is a -> { project: stage is "stage1" } -> { project: stage is "stage2" }
976
+ query: checkme is refineme refine { ${'calculate: c is count()'} }`).translationToFailWith(stageErr);
977
+ });
978
+ test('extend illegal in long pipes', () => {
979
+ expect((0, test_translator_1.markSource) `query: refineme is a -> { project: stage is "stage1" } -> { project: stage is "stage2" }
980
+ query: checkme is refineme refine { ${'extend: {measure: c is count()}'} }`).translationToFailWith(stageErr);
981
+ });
982
+ test('nest illegal in long pipes', () => {
983
+ expect((0, test_translator_1.markSource) `query: refineme is a -> { project: stage is "stage1" } -> { project: stage is "stage2" }
984
+ query: checkme is refineme refine { ${'nest: b is {group_by: stage}'} }`).translationToFailWith(stageErr);
985
+ });
986
+ test('all single stage refinements are accepted', () => {
987
+ expect((0, test_translator_1.markSource) `query: refineme is a -> { group_by: stage is "stage1" }
988
+ query: checkme is refineme refine {
989
+ extend: { dimension: red is 'red' }
990
+ nest: nestRed is { group_by: red }
991
+ group_by: ai
992
+ aggregate: counted is count()
993
+ calculate: l is lag(ai)
994
+ }`).toTranslate();
995
+ });
996
+ });
997
+ });
998
+ //# sourceMappingURL=query.spec.js.map