@malloydata/malloy-tests 0.0.248-dev250325232143 → 0.0.248-dev250325235002

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 CHANGED
@@ -21,14 +21,14 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@jest/globals": "^29.4.3",
24
- "@malloydata/db-bigquery": "^0.0.248-dev250325232143",
25
- "@malloydata/db-duckdb": "^0.0.248-dev250325232143",
26
- "@malloydata/db-postgres": "^0.0.248-dev250325232143",
27
- "@malloydata/db-snowflake": "^0.0.248-dev250325232143",
28
- "@malloydata/db-trino": "^0.0.248-dev250325232143",
29
- "@malloydata/malloy": "^0.0.248-dev250325232143",
30
- "@malloydata/malloy-tag": "^0.0.248-dev250325232143",
31
- "@malloydata/render": "^0.0.248-dev250325232143",
24
+ "@malloydata/db-bigquery": "^0.0.248-dev250325235002",
25
+ "@malloydata/db-duckdb": "^0.0.248-dev250325235002",
26
+ "@malloydata/db-postgres": "^0.0.248-dev250325235002",
27
+ "@malloydata/db-snowflake": "^0.0.248-dev250325235002",
28
+ "@malloydata/db-trino": "^0.0.248-dev250325235002",
29
+ "@malloydata/malloy": "^0.0.248-dev250325235002",
30
+ "@malloydata/malloy-tag": "^0.0.248-dev250325235002",
31
+ "@malloydata/render": "^0.0.248-dev250325235002",
32
32
  "events": "^3.3.0",
33
33
  "jsdom": "^22.1.0",
34
34
  "luxon": "^2.4.0",
@@ -38,5 +38,5 @@
38
38
  "@types/jsdom": "^21.1.1",
39
39
  "@types/luxon": "^2.4.0"
40
40
  },
41
- "version": "0.0.248-dev250325232143"
41
+ "version": "0.0.248-dev250325235002"
42
42
  }
@@ -16,98 +16,94 @@ afterAll(async () => {
16
16
  await runtimes.closeAll();
17
17
  });
18
18
 
19
- // mtoy todo sit down with each parser and compiler and make sure there is a test for every case
20
-
21
19
  describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
22
20
  const q = db.getQuoter();
23
21
  describe('string filter expressions', () => {
24
- const bq = db.dialect.sqlLiteralString('x\\');
22
+ function got(s: string) {
23
+ const zipMe = s.split(',');
24
+ return zipMe.map(s => ({nm: s}));
25
+ }
26
+ const xbq = db.dialect.sqlLiteralString('x\\');
25
27
  const abc = db.loadModel(`
26
28
  source: abc is ${dbName}.sql("""
27
- SELECT 'abc' as ${q`s`}, '0 - abc' as ${q`nm`}
28
- UNION ALL SELECT 'def', '1 - def'
29
- UNION ALL SELECT null, '2 - null'
30
- UNION ALL SELECT '', '3 - empty'
31
- UNION ALL SELECT ${bq}, '4 - xback'
29
+ SELECT 'abc' as ${q`s`}, 'abc' as ${q`nm`}
30
+ UNION ALL SELECT 'def', 'def'
31
+ UNION ALL SELECT ${xbq}, 'xback'
32
+ UNION ALL SELECT '', 'z-empty'
33
+ UNION ALL SELECT null, 'z-null'
32
34
  """)
33
35
  `);
34
36
 
35
- test('abc', async () => {
37
+ test('is abc', async () => {
36
38
  await expect(`
37
39
  run: abc -> {
38
40
  where: s ~ f'abc';
39
41
  select: s
40
42
  }`).malloyResultMatches(abc, [{s: 'abc'}]);
41
43
  });
44
+ test.skip('empty string filter expression', async () => {
45
+ /*
46
+ since the sql generated works when pasted into mysql
47
+ my next suggestion is that there is some funky re-ordering
48
+ happening in the result processing which will test tomorrow
49
+ */
50
+ await expect(`
51
+ # test.verbose
52
+ run: abc -> {
53
+ where: s ~ f'';
54
+ select: *; order_by: nm asc
55
+ }`).malloyResultMatches(abc, got('abc,def,xback,z-empty,z-null'));
56
+ });
42
57
  test('abc,def', async () => {
43
58
  await expect(`
44
59
  run: abc -> {
45
60
  where: s ~ f'abc,def';
46
- select: nm; order_by: nm
47
- }`).malloyResultMatches(abc, [{nm: '0 - abc'}, {nm: '1 - def'}]);
61
+ select: nm; order_by: nm asc
62
+ }`).malloyResultMatches(abc, got('abc,def'));
48
63
  });
49
64
  test('-abc', async () => {
50
65
  await expect(`
66
+ # test.verbose
51
67
  run: abc -> {
52
68
  where: s ~ f'-abc',
53
69
  select: nm; order_by: nm asc
54
- }`).malloyResultMatches(abc, [
55
- {nm: '1 - def'},
56
- {nm: '2 - null'},
57
- {nm: '3 - empty'},
58
- {nm: '4 - xback'},
59
- ]);
70
+ }`).malloyResultMatches(abc, got('def,xback,z-empty,z-null'));
60
71
  });
61
72
  test('-starts', async () => {
62
73
  await expect(`
74
+ # test.verbose
63
75
  run: abc -> {
64
76
  where: s ~ f'-a%',
65
77
  select: nm; order_by: nm asc
66
- }`).malloyResultMatches(abc, [
67
- {nm: '1 - def'},
68
- {nm: '2 - null'},
69
- {nm: '3 - empty'},
70
- {nm: '4 - xback'},
71
- ]);
78
+ }`).malloyResultMatches(abc, got('def,xback,z-empty,z-null'));
72
79
  });
73
80
  test('-contains', async () => {
74
81
  await expect(`
82
+ # test.verbose
75
83
  run: abc -> {
76
84
  where: s ~ f'-%b%',
77
85
  select: nm; order_by: nm asc
78
- }`).malloyResultMatches(abc, [
79
- {nm: '1 - def'},
80
- {nm: '2 - null'},
81
- {nm: '3 - empty'},
82
- {nm: '4 - xback'},
83
- ]);
86
+ }`).malloyResultMatches(abc, got('def,xback,z-empty,z-null'));
84
87
  });
85
88
  test('-end', async () => {
86
89
  await expect(`
90
+ # test.verbose
87
91
  run: abc -> {
88
92
  where: s ~ f'-%c',
89
93
  select: nm; order_by: nm asc
90
- }`).malloyResultMatches(abc, [
91
- {nm: '1 - def'},
92
- {nm: '2 - null'},
93
- {nm: '3 - empty'},
94
- {nm: '4 - xback'},
95
- ]);
94
+ }`).malloyResultMatches(abc, got('def,xback,z-empty,z-null'));
96
95
  });
97
96
  test('unlike', async () => {
98
97
  await expect(`
98
+ # test.verbose
99
99
  run: abc -> {
100
100
  where: s ~ f'-a%c',
101
101
  select: nm; order_by: nm asc
102
- }`).malloyResultMatches(abc, [
103
- {nm: '1 - def'},
104
- {nm: '2 - null'},
105
- {nm: '3 - empty'},
106
- {nm: '4 - xback'},
107
- ]);
102
+ }`).malloyResultMatches(abc, got('def,xback,z-empty,z-null'));
108
103
  });
109
104
  test('simple but not ___,-abc', async () => {
110
105
  await expect(`
106
+ # test.verbose
111
107
  run: abc -> {
112
108
  where: s ~ f'___,-abc';
113
109
  select: s
@@ -115,43 +111,39 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
115
111
  });
116
112
  test('empty', async () => {
117
113
  await expect(`
114
+ # test.verbose
118
115
  run: abc -> {
119
116
  where: s ~ f'empty'
120
117
  select: nm; order_by: nm asc
121
- }`).malloyResultMatches(abc, [{nm: '2 - null'}, {nm: '3 - empty'}]);
118
+ }`).malloyResultMatches(abc, got('z-empty,z-null'));
122
119
  });
123
120
  test('-empty', async () => {
124
121
  await expect(`
122
+ # test.verbose
125
123
  run: abc -> {
126
124
  where: s ~ f'-empty'
127
125
  select: nm; order_by: nm asc
128
- }`).malloyResultMatches(abc, [
129
- {nm: '0 - abc'},
130
- {nm: '1 - def'},
131
- {nm: '4 - xback'},
132
- ]);
126
+ }`).malloyResultMatches(abc, got('abc,def,xback'));
133
127
  });
134
128
  test('null', async () => {
135
129
  await expect(`
130
+ # test.verbose
136
131
  run: abc -> {
137
132
  where: s ~ f'null'
138
133
  select: nm
139
- }`).malloyResultMatches(abc, [{nm: '2 - null'}]);
134
+ }`).malloyResultMatches(abc, got('z-null'));
140
135
  });
141
136
  test('-null', async () => {
142
137
  await expect(`
138
+ # test.verbose
143
139
  run: abc -> {
144
140
  where: s ~ f'-null'
145
141
  select: nm; order_by: nm asc
146
- }`).malloyResultMatches(abc, [
147
- {nm: '0 - abc'},
148
- {nm: '1 - def'},
149
- {nm: '3 - empty'},
150
- {nm: '4 - xback'},
151
- ]);
142
+ }`).malloyResultMatches(abc, got('abc,def,xback,z-empty'));
152
143
  });
153
144
  test('starts', async () => {
154
145
  await expect(`
146
+ # test.verbose
155
147
  run: abc -> {
156
148
  where: s ~ f'a%';
157
149
  select: s
@@ -159,13 +151,15 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
159
151
  });
160
152
  test('contains', async () => {
161
153
  await expect(`
154
+ # test.verbose
162
155
  run: abc -> {
163
156
  where: s ~ f'%b%,%e%';
164
- select: s; order_by: s asc
157
+ select: *; order_by: nm asc
165
158
  }`).malloyResultMatches(abc, [{s: 'abc'}, {s: 'def'}]);
166
159
  });
167
160
  test('simple ends', async () => {
168
161
  await expect(`
162
+ # test.verbose
169
163
  run: abc -> {
170
164
  where: s ~ f'%c';
171
165
  select: s
@@ -173,17 +167,19 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
173
167
  });
174
168
  test('ends in backslash', async () => {
175
169
  await expect(`
170
+ # test.verbose
176
171
  run: abc -> {
177
172
  where: s ~ f'%\\\\'
178
173
  select: nm
179
- }`).malloyResultMatches(abc, [{nm: '4 - xback'}]);
174
+ }`).malloyResultMatches(abc, got('xback'));
180
175
  });
181
176
  test('= x backslash', async () => {
182
177
  await expect(`
178
+ # test.verbose
183
179
  run: abc -> {
184
180
  where: s ~ f'x\\\\'
185
181
  select: nm
186
- }`).malloyResultMatches(abc, [{nm: '4 - xback'}]);
182
+ }`).malloyResultMatches(abc, got('xback'));
187
183
  });
188
184
  });
189
185
 
@@ -198,6 +194,20 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
198
194
  UNION ALL SELECT NULL, 'null'
199
195
  """)
200
196
  `);
197
+ test.skip('empty numeric filter', async () => {
198
+ await expect(`
199
+ run: nums -> {
200
+ where: n ~ f''
201
+ select: t; order_by: t asc
202
+ }`).malloyResultMatches(nums, [
203
+ {t: '0'},
204
+ {t: '1'},
205
+ {t: '2'},
206
+ {t: '3'},
207
+ {t: '4'},
208
+ {t: 'null'},
209
+ ]);
210
+ });
201
211
  test('2', async () => {
202
212
  await expect(`
203
213
  run: nums -> {
@@ -227,7 +237,6 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
227
237
  });
228
238
  test('not [1 to 3]', async () => {
229
239
  await expect(`
230
- # test.verbose
231
240
  run: nums -> {
232
241
  where: n ~ f'not [1 to 3]'
233
242
  select: t; order_by: t asc
@@ -299,6 +308,7 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
299
308
  });
300
309
 
301
310
  // mysql doesn't have true booleans ...
311
+ const testBoolean = !db.dialect.booleanAsNumbers;
302
312
  describe('boolean filter expressions', () => {
303
313
  const facts = db.loadModel(`
304
314
  source: facts is ${dbName}.sql("""
@@ -307,48 +317,59 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
307
317
  UNION ALL SELECT NULL, 'null'
308
318
  """)
309
319
  `);
310
- test.when(dbName !== 'mysql')('true', async () => {
320
+ test.when(testBoolean)('true', async () => {
311
321
  await expect(`
312
322
  run: facts -> {
313
323
  where: b ~ f'true'
314
324
  select: t; order_by: t asc
315
325
  }`).malloyResultMatches(facts, [{t: 'true'}]);
316
326
  });
317
- test.when(dbName !== 'mysql')('true', async () => {
327
+ test.when(testBoolean)('true', async () => {
318
328
  await expect(`
319
329
  run: facts -> {
320
330
  where: b ~ f'true'
321
331
  select: t; order_by: t asc
322
332
  }`).malloyResultMatches(facts, [{t: 'true'}]);
323
333
  });
324
- test.when(dbName !== 'mysql')('false', async () => {
334
+ test.when(testBoolean)('false', async () => {
325
335
  await expect(`
326
336
  run: facts -> {
327
337
  where: b ~ f'false'
328
338
  select: t; order_by: t asc
329
339
  }`).malloyResultMatches(facts, [{t: 'false'}, {t: 'null'}]);
330
340
  });
331
- test.when(dbName !== 'mysql')('=false', async () => {
341
+ test.when(testBoolean)('=false', async () => {
332
342
  await expect(`
333
343
  run: facts -> {
334
344
  where: b ~ f'=false'
335
345
  select: t; order_by: t asc
336
346
  }`).malloyResultMatches(facts, [{t: 'false'}]);
337
347
  });
338
- test.when(dbName !== 'mysql')('null', async () => {
348
+ test.when(testBoolean)('null', async () => {
339
349
  await expect(`
340
350
  run: facts -> {
341
351
  where: b ~ f'null'
342
352
  select: t; order_by: t asc
343
353
  }`).malloyResultMatches(facts, [{t: 'null'}]);
344
354
  });
345
- test.when(dbName !== 'mysql')('not null', async () => {
355
+ test.when(testBoolean)('not null', async () => {
346
356
  await expect(`
347
357
  run: facts -> {
348
358
  where: b ~ f'not null'
349
359
  select: t; order_by: t asc
350
360
  }`).malloyResultMatches(facts, [{t: 'false'}, {t: 'true'}]);
351
361
  });
362
+ // test.when(testBoolean)('empty boolean filter', async () => {
363
+ // await expect(`
364
+ // run: facts -> {
365
+ // where: b ~ f''
366
+ // select: t; order_by: t asc
367
+ // }`).malloyResultMatches(facts, [
368
+ // {t: 'false'},
369
+ // {t: 'null'},
370
+ // {t: 'true'},
371
+ // ]);
372
+ // });
352
373
  });
353
374
 
354
375
  type TL = 'timeLiteral';
@@ -373,12 +394,12 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
373
394
  * { t: 1 second before start, n: 'before' }
374
395
  * { t: start, n: 'first' }
375
396
  * { t: 1 second before end, n: 'last' }
376
- * { t: end, n: 'zend' }
377
- * { t: NULL n: ' null ' }
397
+ * { t: end, n: 'post-range' }
398
+ * { t: NULL n: 'z-null' }
378
399
  * Use malloyResultMatches(range, inRange) or (range, notInRange)
379
400
  */
380
401
  const inRange = [{n: 'first'}, {n: 'last'}];
381
- const notInRange = [{n: 'before'}, {n: 'zend'}];
402
+ const notInRange = [{n: 'before'}, {n: 'post-range'}];
382
403
  function mkRange(start: string, end: string) {
383
404
  const begin = LuxonDateTime.fromFormat(start, fTimestamp);
384
405
  const b4 = begin.minus({second: 1});
@@ -394,8 +415,8 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
394
415
  SELECT ${before} AS ${q`t`}, 'before' AS ${q`n`}
395
416
  UNION ALL SELECT ${lit(start, 'timestamp')}, 'first'
396
417
  UNION ALL SELECT ${last} , 'last'
397
- UNION ALL SELECT ${lit(end, 'timestamp')}, 'zend'
398
- UNION ALL SELECT NULL, ' null '
418
+ UNION ALL SELECT ${lit(end, 'timestamp')}, 'post-range'
419
+ UNION ALL SELECT NULL, 'z-null'
399
420
  """)
400
421
  -> {select: *; order_by: n}`;
401
422
  return db.loadModel(rangeModel);
@@ -412,10 +433,10 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
412
433
  )} AS ${q`t`}, 'before' AS ${q`n`}
413
434
  UNION ALL SELECT ${lit(start, 'date')}, 'first'
414
435
  UNION ALL SELECT ${lit(last.toFormat(fDate), 'date')} , 'last'
415
- UNION ALL SELECT ${lit(end, 'date')}, 'zend'
416
- UNION ALL SELECT NULL, ' null '
436
+ UNION ALL SELECT ${lit(end, 'date')}, 'post-range'
437
+ UNION ALL SELECT NULL, 'z-null'
417
438
  """)
418
- -> {select: *; order_by: n}`;
439
+ -> {select: t,n; order_by: n}`;
419
440
  return db.loadModel(rangeModel);
420
441
  }
421
442
 
@@ -432,7 +453,7 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
432
453
  const range = mkDateRange('2001-01-01', '2001-04-01');
433
454
  await expect(`
434
455
  run: range + { where: t ~ f'after 2001-Q1' }
435
- `).malloyResultMatches(range, {n: 'zend'});
456
+ `).malloyResultMatches(range, {n: 'post-range'});
436
457
  });
437
458
  test('date before month', async () => {
438
459
  const range = mkDateRange('2001-01-01', '2001-02-01');
@@ -497,7 +518,7 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
497
518
  const range = mkRange('2001-01-01 00:00:00', '2002-01-01 00:00:00');
498
519
  await expect(`
499
520
  run: range + { where: t ~ f'after 2001' }
500
- `).malloyResultMatches(range, [{n: 'zend'}]);
521
+ `).malloyResultMatches(range, [{n: 'post-range'}]);
501
522
  });
502
523
  test('y2k for 1 minute', async () => {
503
524
  const range = mkRange('2001-01-01 00:00:00', '2001-01-01 00:01:00');
@@ -545,17 +566,30 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
545
566
  const range = mkRange('2001-01-01 00:00:00', '2002-01-01 00:00:00');
546
567
  await expect(`
547
568
  run: range + { where: t ~ f'null' }
548
- `).malloyResultMatches(range, [{n: ' null '}]);
569
+ `).malloyResultMatches(range, [{n: 'z-null'}]);
549
570
  });
550
571
  test('not null', async () => {
551
572
  const range = mkRange('2001-01-01 00:00:00', '2002-01-01 00:00:00');
552
573
  await expect(`
553
- run: range + { where: t ~ f'not null' }
574
+ run: range + { where: t ~ f'not null'; order_by: n }
575
+ `).malloyResultMatches(range, [
576
+ {n: 'before'},
577
+ {n: 'first'},
578
+ {n: 'last'},
579
+ {n: 'post-range'},
580
+ ]);
581
+ });
582
+ test.skip('empty temporal filter', async () => {
583
+ const range = mkRange('2001-01-01 00:00:00', '2002-01-01 00:00:00');
584
+ await expect(`
585
+ # test.verbose
586
+ run: range + { where: t ~ f''; order_by: n }
554
587
  `).malloyResultMatches(range, [
555
588
  {n: 'before'},
556
589
  {n: 'first'},
557
590
  {n: 'last'},
558
- {n: 'zend'},
591
+ {n: 'post-range'},
592
+ {n: 'z-null'},
559
593
  ]);
560
594
  });
561
595
  test('year literal', async () => {
@@ -550,6 +550,7 @@ runtimes.runtimeMap.forEach((runtime, databaseName) => {
550
550
  // symmetric aggregate are needed on both sides of the join
551
551
  // Check the row count and that sums on each side work properly.
552
552
  await expect(`
553
+ # test.verbose
553
554
  run: ${databaseName}.table('malloytest.state_facts') -> {
554
555
  group_by: state
555
556
  nest: ugly is {
@@ -184,4 +184,166 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
184
184
  } -> foo
185
185
  `).malloyResultMatches(orderByModel, {});
186
186
  });
187
+
188
+ // There's a problem with null ordering in MySQL which we are ignoring for now
189
+ const testNullOrdering = databaseName !== 'mysql';
190
+ const testTimes =
191
+ testNullOrdering && !['presto', 'trino'].includes(databaseName);
192
+ describe('null ordering', () => {
193
+ const q = runtime.getQuoter();
194
+ const a = runtime.dialect.sqlLiteralString('a');
195
+ const b = runtime.dialect.sqlLiteralString('b');
196
+ const c = runtime.dialect.sqlLiteralString('c');
197
+ const abc = `${databaseName}.sql("""
198
+ SELECT ${a} as ${q`l`}, 'a' as ${q`ln`}
199
+ UNION ALL SELECT ${c}, 'c'
200
+ UNION ALL SELECT ${b}, 'b'
201
+ UNION ALL SELECT NULL, 'null'
202
+ """)`;
203
+ test.when(testNullOrdering)(
204
+ 'null last in select string with ascending order',
205
+ async () => {
206
+ await expect(
207
+ `run: ${abc} -> { select: *; order_by: l asc }`
208
+ ).malloyResultMatches(runtime, [
209
+ {ln: 'a'},
210
+ {ln: 'b'},
211
+ {ln: 'c'},
212
+ {ln: 'null'},
213
+ ]);
214
+ }
215
+ );
216
+ test.when(testNullOrdering)(
217
+ 'null last in select string with descending order',
218
+ async () => {
219
+ await expect(
220
+ `run: ${abc} -> { select: *; order_by: l desc }`
221
+ ).malloyResultMatches(runtime, [
222
+ {ln: 'c'},
223
+ {ln: 'b'},
224
+ {ln: 'a'},
225
+ {ln: 'null'},
226
+ ]);
227
+ }
228
+ );
229
+ test.when(testNullOrdering)(
230
+ 'null last in reduce string with default ascending order',
231
+ async () => {
232
+ await expect(`
233
+ #! test.verbose
234
+ run: ${abc} -> { group_by: l }`).malloyResultMatches(runtime, [
235
+ {l: 'a'},
236
+ {l: 'b'},
237
+ {l: 'c'},
238
+ {l: null},
239
+ ]);
240
+ }
241
+ );
242
+ const nums = `${databaseName}.sql("""
243
+ SELECT 1 as ${q`n`}
244
+ UNION ALL SELECT 9
245
+ UNION ALL SELECT 5
246
+ UNION ALL SELECT NULL
247
+ """)`;
248
+ test.when(testNullOrdering)(
249
+ 'null last in numbers with ascending order',
250
+ async () => {
251
+ await expect(
252
+ `run: ${nums} -> { select: n; order_by: n asc }`
253
+ ).malloyResultMatches(runtime, [{n: 1}, {n: 5}, {n: 9}, {n: null}]);
254
+ }
255
+ );
256
+ test.when(testNullOrdering)(
257
+ 'null last in numbers with descending order',
258
+ async () => {
259
+ await expect(
260
+ `run: ${nums} -> { select: n; order_by: n desc }`
261
+ ).malloyResultMatches(runtime, [{n: 9}, {n: 5}, {n: 1}, {n: null}]);
262
+ }
263
+ );
264
+ test.when(testNullOrdering)(
265
+ 'null last in reduce measure with default descending order',
266
+ async () => {
267
+ await expect(`
268
+ # test.verbose
269
+ run: ${nums} -> {
270
+ group_by: nstr is n ? pick 'null' when is null else concat('number', n::string)
271
+ aggregate: nTotal is n.sum()
272
+ }`).malloyResultMatches(runtime, [
273
+ {nstr: 'number9'},
274
+ {nstr: 'number5'},
275
+ {nstr: 'number1'},
276
+ {nstr: 'null'},
277
+ ]);
278
+ }
279
+ );
280
+ const y2020 = runtime.dialect.sqlLiteralTime(
281
+ {},
282
+ {
283
+ node: 'timeLiteral',
284
+ literal: '2020-01-01 00:00:00',
285
+ typeDef: {type: 'timestamp'},
286
+ }
287
+ );
288
+ const d2020 = new Date('2020-01-01 00:00:00Z');
289
+ const d2022 = new Date('2022-01-01 00:00:00Z');
290
+ const d2025 = new Date('2025-01-01 00:00:00Z');
291
+ const y2025 = runtime.dialect.sqlLiteralTime(
292
+ {},
293
+ {
294
+ node: 'timeLiteral',
295
+ literal: '2025-01-01 00:00:00',
296
+ typeDef: {type: 'timestamp'},
297
+ }
298
+ );
299
+ const y2022 = runtime.dialect.sqlLiteralTime(
300
+ {},
301
+ {
302
+ node: 'timeLiteral',
303
+ literal: '2022-01-01 00:00:00',
304
+ typeDef: {type: 'timestamp'},
305
+ }
306
+ );
307
+ const times = `${databaseName}.sql("""
308
+ SELECT ${y2020} as ${q`t`}
309
+ UNION ALL SELECT ${y2025}
310
+ UNION ALL SELECT ${y2022}
311
+ UNION ALL SELECT NULL
312
+ """)`;
313
+ test.when(testTimes)(
314
+ 'null last in timestamps with ascending order',
315
+ async () => {
316
+ await expect(
317
+ `run: ${times} -> { select: t; order_by: t asc }`
318
+ ).malloyResultMatches(runtime, [
319
+ {t: d2020},
320
+ {t: d2022},
321
+ {t: d2025},
322
+ {t: null},
323
+ ]);
324
+ }
325
+ );
326
+ test.when(testTimes)(
327
+ 'null last in timestamps with descending order',
328
+ async () => {
329
+ await expect(
330
+ `run: ${times} -> { select: t; order_by: t desc }`
331
+ ).malloyResultMatches(runtime, [
332
+ {t: d2025},
333
+ {t: d2022},
334
+ {t: d2020},
335
+ {t: null},
336
+ ]);
337
+ }
338
+ );
339
+ test.when(testTimes)(
340
+ 'null last in reduce timestamps with default descending order',
341
+ async () => {
342
+ await expect(`run: ${times} -> { group_by: t }`).malloyResultMatches(
343
+ runtime,
344
+ [{t: d2025}, {t: d2022}, {t: d2020}, {t: null}]
345
+ );
346
+ }
347
+ );
348
+ });
187
349
  });
@@ -100,7 +100,6 @@ describe('Wildcard BigQuery Tables', () => {
100
100
  )
101
101
  .run();
102
102
  expect(result.data.value).toStrictEqual([
103
- {state: null, aircraft_count: 43},
104
103
  {state: 'IA', aircraft_count: 1},
105
104
  {state: 'KS', aircraft_count: 1},
106
105
  {state: 'LA', aircraft_count: 1},
@@ -109,6 +108,7 @@ describe('Wildcard BigQuery Tables', () => {
109
108
  {state: 'OK', aircraft_count: 1},
110
109
  {state: 'OR', aircraft_count: 2},
111
110
  {state: 'TX', aircraft_count: 1},
111
+ {state: null, aircraft_count: 43},
112
112
  ]);
113
113
  }
114
114
  });
@@ -137,11 +137,11 @@ describe('Wildcard BigQuery Tables', () => {
137
137
  )
138
138
  .run();
139
139
  expect(result.data.value).toStrictEqual([
140
- {state: null, aircraft_count: 47},
141
140
  {state: 'KS', aircraft_count: 1},
142
141
  {state: 'LA', aircraft_count: 1},
143
142
  {state: 'OK', aircraft_count: 1},
144
143
  {state: 'OR', aircraft_count: 1},
144
+ {state: null, aircraft_count: 47},
145
145
  ]);
146
146
  }
147
147
  });
@@ -197,8 +197,8 @@ describe('Wildcard BigQuery Tables', () => {
197
197
  )
198
198
  .run();
199
199
  expect(result.data.value).toStrictEqual([
200
- {_TABLE_SUFFIX: null, aircraft_count: 47},
201
200
  {_TABLE_SUFFIX: '02', aircraft_count: 4},
201
+ {_TABLE_SUFFIX: null, aircraft_count: 47},
202
202
  ]);
203
203
  }
204
204
  });