@malloydata/malloy-tests 0.0.280 → 0.0.282

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.280",
25
- "@malloydata/db-duckdb": "0.0.280",
26
- "@malloydata/db-postgres": "0.0.280",
27
- "@malloydata/db-snowflake": "0.0.280",
28
- "@malloydata/db-trino": "0.0.280",
29
- "@malloydata/malloy": "0.0.280",
30
- "@malloydata/malloy-tag": "0.0.280",
31
- "@malloydata/render": "0.0.280",
24
+ "@malloydata/db-bigquery": "0.0.282",
25
+ "@malloydata/db-duckdb": "0.0.282",
26
+ "@malloydata/db-postgres": "0.0.282",
27
+ "@malloydata/db-snowflake": "0.0.282",
28
+ "@malloydata/db-trino": "0.0.282",
29
+ "@malloydata/malloy": "0.0.282",
30
+ "@malloydata/malloy-tag": "0.0.282",
31
+ "@malloydata/render": "0.0.282",
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.280"
41
+ "version": "0.0.282"
42
42
  }
@@ -192,6 +192,32 @@ describe('parameters', () => {
192
192
  `
193
193
  ).malloyResultMatches(runtime, {param_value: 11});
194
194
  });
195
+ it('default value modified through extension propagates', async () => {
196
+ await expect(
197
+ `
198
+ ##! experimental.parameters
199
+ source: ab_new(param::number is 10) is duckdb.table('malloytest.state_facts') extend {
200
+ dimension: param_value is param
201
+ }
202
+ source: ab_new_new(param::number is 11) is ab_new(param is param + 1) extend {}
203
+ run: ab_new_new -> { group_by: param_value }
204
+ `
205
+ ).malloyResultMatches(runtime, {param_value: 12});
206
+ });
207
+ // Fix this with namespaces!
208
+ it.skip('default value modified through extension twice propagates', async () => {
209
+ await expect(
210
+ `
211
+ ##! experimental.parameters
212
+ source: ab_plus_0(param::number is 0) is duckdb.table('malloytest.state_facts') extend {
213
+ dimension: param_value is param
214
+ }
215
+ source: ab_plus_one(param::number is 0) is ab_plus_0(param is param + 1) extend {}
216
+ source: ab_plus_two(param::number is 0) is ab_plus_one(param is param + 1) extend {}
217
+ run: ab_plus_two -> { group_by: param_value }
218
+ `
219
+ ).malloyResultMatches(runtime, {param_value: 2});
220
+ });
195
221
  it('use parameter in nested view', async () => {
196
222
  await expect(
197
223
  `
@@ -321,8 +321,7 @@ describe.each(runtimes.runtimeList)('filter expressions %s', (dbName, db) => {
321
321
  });
322
322
  });
323
323
 
324
- // mysql doesn't have true booleans ...
325
- const testBoolean = !db.dialect.booleanAsNumbers;
324
+ const testBoolean = db.dialect.booleanType === 'supported';
326
325
  describe('boolean filter expressions', () => {
327
326
  const facts = db.loadModel(`
328
327
  source: facts is ${dbName}.sql("""
@@ -24,11 +24,7 @@
24
24
 
25
25
  import {RuntimeList, allDatabases} from '../../runtimes';
26
26
  import '../../util/db-jest-matchers';
27
- import {
28
- booleanResult,
29
- databasesFromEnvironmentOr,
30
- mkSqlEqWith,
31
- } from '../../util';
27
+ import {databasesFromEnvironmentOr, mkSqlEqWith} from '../../util';
32
28
 
33
29
  const runtimes = new RuntimeList(databasesFromEnvironmentOr(allDatabases));
34
30
 
@@ -534,8 +530,8 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
534
530
  group_by: boolean_2 is sql_boolean("\${engines} = 2")
535
531
  }
536
532
  `).malloyResultMatches(expressionModel, {
537
- boolean_1: booleanResult(true, databaseName),
538
- boolean_2: booleanResult(false, databaseName),
533
+ boolean_1: runtime.dialect.resultBoolean(true),
534
+ boolean_2: runtime.dialect.resultBoolean(false),
539
535
  });
540
536
  });
541
537
 
@@ -591,54 +587,60 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
591
587
  });
592
588
  });
593
589
 
594
- describe('[not yet supported]', () => {
595
- // See ${...} documentation for lookml here for guidance on remaining work:
596
- // https://cloud.google.com/looker/docs/reference/param-field-sql#sql_for_dimensions
597
- it('${view_name.dimension_name} - one path', async () => {
598
- const query = await expressionModel.loadQuery(
599
- `
600
- ##! experimental { sql_functions }
601
- source: a is ${databaseName}.table('malloytest.aircraft_models') extend { where: aircraft_model_code ? '0270202' }
590
+ it('${view_name.dimension_name} - one path', async () => {
591
+ await expect(`
592
+ ##! experimental { sql_functions }
593
+ source: a0 is ${databaseName}.table('malloytest.aircraft_models')
594
+ source: a is ${databaseName}.table('malloytest.aircraft_models') extend {
595
+ where: aircraft_model_code ? '0270202'
596
+ join_one: a0 on aircraft_model_code = a0.aircraft_model_code
597
+ }
602
598
 
603
- run: a -> {
604
- group_by: string_1 is sql_string("UPPER(\${a.manufacturer})")
605
- }
606
- `
607
- );
608
- await expect(query.run()).rejects.toThrow(
609
- "'.' paths are not yet supported in sql interpolations, found ${a.manufacturer}"
610
- );
599
+ run: a -> {
600
+ group_by: string_1 is sql_string("UPPER(\${a0.manufacturer})")
601
+ }
602
+ `).malloyResultMatches(expressionModel, {
603
+ string_1: 'AHRENS AIRCRAFT CORP.',
611
604
  });
605
+ });
612
606
 
613
- it('${view_name.dimension_name} - multiple paths', async () => {
614
- const query = await expressionModel.loadQuery(
615
- `
616
- ##! experimental { sql_functions }
617
- source: a is ${databaseName}.table('malloytest.aircraft_models') extend { where: aircraft_model_code ? '0270202' }
607
+ it('${view_name.dimension_name} - multiple paths', async () => {
608
+ await expect(`
609
+ ##! experimental { sql_functions }
610
+ source: a0 is ${databaseName}.table('malloytest.aircraft_models')
611
+ source: a is ${databaseName}.table('malloytest.aircraft_models') extend {
612
+ where: aircraft_model_code ? '0270202'
613
+ join_one: a0 on aircraft_model_code = a0.aircraft_model_code
614
+ }
618
615
 
619
- run: a -> {
620
- group_by: number_1 is sql_number("\${a.seats} * \${a.seats} + \${a.total_seats}")
621
- }
622
- `
623
- );
624
- await expect(query.run()).rejects.toThrow(
625
- "'.' paths are not yet supported in sql interpolations, found (${a.seats}, ${a.seats}, ${a.total_seats})"
626
- );
616
+ run: a -> {
617
+ group_by: number_1 is sql_number("\${seats} + \${a0.seats}")
618
+ }
619
+ `).malloyResultMatches(expressionModel, {
620
+ number_1: 58,
627
621
  });
622
+ });
628
623
 
629
- it('${view_name.SQL_TABLE_NAME}', async () => {
624
+ describe('[not yet supported]', () => {
625
+ // See ${...} documentation for lookml here for guidance on remaining work:
626
+ // https://cloud.google.com/looker/docs/reference/param-field-sql#sql_for_dimensions
627
+ it('${SQL_TABLE_NAME}', async () => {
630
628
  const query = await expressionModel.loadQuery(
631
629
  `
632
630
  ##! experimental { sql_functions }
633
- source: a is ${databaseName}.table('malloytest.aircraft_models') extend { where: aircraft_model_code ? '0270202' }
631
+ source: a0 is ${databaseName}.table('malloytest.aircraft_models')
632
+ source: a is ${databaseName}.table('malloytest.aircraft_models') extend {
633
+ where: aircraft_model_code ? '0270202'
634
+ join_one: a0 on aircraft_model_code = a0.aircraft_model_code
635
+ }
634
636
 
635
637
  run: a -> {
636
- group_by: number_1 is sql_number("\${a.SQL_TABLE_NAME}.seats")
637
- }
638
+ group_by: number_1 is sql_number("\${a0.SQL_TABLE_NAME}.seats")
639
+ }
638
640
  `
639
641
  );
640
642
  await expect(query.run()).rejects.toThrow(
641
- "'.' paths are not yet supported in sql interpolations, found ${a.SQL_TABLE_NAME}"
643
+ "Invalid interpolation: 'SQL_TABLE_NAME' is not defined"
642
644
  );
643
645
  });
644
646
  });
@@ -861,57 +863,59 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
861
863
  });
862
864
  });
863
865
 
864
- describe('null safe booleans', () => {
865
- const nulls = `${databaseName}.sql("""
866
- SELECT
867
- 0 as ${q`n`},
868
- 1 as ${q`x`}, 2 as ${q`y`},
869
- 'a' as ${q`a`}, 'b' as ${q`b`},
870
- (1 = 1) as ${q`tf`}
871
- UNION ALL SELECT
872
- 5,
873
- null, null, null, null, null
874
- """) extend { where: n > 0 }`;
875
- const is_true = databaseName === 'mysql' ? 1 : true;
866
+ if (runtime.dialect.booleanType !== 'none') {
867
+ describe('null safe booleans', () => {
868
+ const nulls = `${databaseName}.sql("""
869
+ SELECT
870
+ 0 as ${q`n`},
871
+ 1 as ${q`x`}, 2 as ${q`y`},
872
+ 'a' as ${q`a`}, 'b' as ${q`b`},
873
+ (1=1) as ${q`tf`}
874
+ UNION ALL SELECT
875
+ 5,
876
+ null, null, null, null, null
877
+ """) extend { where: n > 0 }`;
878
+ const is_true = runtime.dialect.resultBoolean(true);
876
879
 
877
- it('select boolean', async () => {
878
- await expect(`run: ${nulls} -> {
879
- select:
880
- null_boolean is tf
881
- }`).malloyResultMatches(runtime, {null_boolean: null});
882
- });
883
- it('not boolean', async () => {
884
- await expect(`run: ${nulls} -> {
885
- select:
886
- not_null_boolean is not tf
887
- }`).malloyResultMatches(runtime, {not_null_boolean: is_true});
888
- });
889
- it('numeric != non-null to null', async () => {
890
- await expect(
891
- `run: ${nulls} -> { select: val_ne_null is x != 9 }`
892
- ).malloyResultMatches(runtime, {val_ne_null: is_true});
893
- });
894
- it('string !~ non-null to null', async () => {
895
- await expect(
896
- `run: ${nulls} -> { select: val_ne_null is a !~ 'z' }`
897
- ).malloyResultMatches(runtime, {val_ne_null: is_true});
898
- });
899
- it('regex !~ non-null to null', async () => {
900
- await expect(
901
- `run: ${nulls} -> { select: val_ne_null is a !~ r'z' }`
902
- ).malloyResultMatches(runtime, {val_ne_null: is_true});
903
- });
904
- it('numeric != null-to-null', async () => {
905
- await expect(
906
- `run: ${nulls} -> { select: null_ne_null is x != y }`
907
- ).malloyResultMatches(runtime, {null_ne_null: is_true});
908
- });
909
- it('string !~ null-to-null', async () => {
910
- await expect(
911
- `run: ${nulls} -> { select: null_ne_null is a !~ b }`
912
- ).malloyResultMatches(runtime, {null_ne_null: is_true});
880
+ it('select boolean', async () => {
881
+ await expect(`run: ${nulls} -> {
882
+ select:
883
+ null_boolean is tf
884
+ }`).malloyResultMatches(runtime, {null_boolean: null});
885
+ });
886
+ it('not boolean', async () => {
887
+ await expect(`run: ${nulls} -> {
888
+ select:
889
+ not_null_boolean is not tf
890
+ }`).malloyResultMatches(runtime, {not_null_boolean: is_true});
891
+ });
892
+ it('numeric != non-null to null', async () => {
893
+ await expect(
894
+ `run: ${nulls} -> { select: val_ne_null is x != 9 }`
895
+ ).malloyResultMatches(runtime, {val_ne_null: is_true});
896
+ });
897
+ it('string !~ non-null to null', async () => {
898
+ await expect(
899
+ `run: ${nulls} -> { select: val_ne_null is a !~ 'z' }`
900
+ ).malloyResultMatches(runtime, {val_ne_null: is_true});
901
+ });
902
+ it('regex !~ non-null to null', async () => {
903
+ await expect(
904
+ `run: ${nulls} -> { select: val_ne_null is a !~ r'z' }`
905
+ ).malloyResultMatches(runtime, {val_ne_null: is_true});
906
+ });
907
+ it('numeric != null-to-null', async () => {
908
+ await expect(
909
+ `run: ${nulls} -> { select: null_ne_null is x != y }`
910
+ ).malloyResultMatches(runtime, {null_ne_null: is_true});
911
+ });
912
+ it('string !~ null-to-null', async () => {
913
+ await expect(
914
+ `run: ${nulls} -> { select: null_ne_null is a !~ b }`
915
+ ).malloyResultMatches(runtime, {null_ne_null: is_true});
916
+ });
913
917
  });
914
- });
918
+ }
915
919
 
916
920
  test('dimension expressions expanded with parens properly', async () => {
917
921
  await expect(
@@ -923,8 +927,8 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
923
927
  paren is false and (fot)
924
928
  }`
925
929
  ).malloyResultMatches(runtime, {
926
- paren: booleanResult(false, databaseName),
927
- no_paren: booleanResult(false, databaseName),
930
+ paren: runtime.dialect.resultBoolean(false),
931
+ no_paren: runtime.dialect.resultBoolean(false),
928
932
  });
929
933
  });
930
934
  });
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import {RuntimeList, allDatabases} from '../../runtimes';
26
- import {booleanResult, brokenIn, databasesFromEnvironmentOr} from '../../util';
26
+ import {brokenIn, databasesFromEnvironmentOr} from '../../util';
27
27
  import '../../util/db-jest-matchers';
28
28
  import type * as malloy from '@malloydata/malloy';
29
29
 
@@ -68,6 +68,8 @@ runtimes.runtimeMap.forEach((runtime, databaseName) =>
68
68
  expressionModels.forEach((x, databaseName) => {
69
69
  const expressionModel = x.expressionModel;
70
70
  const runtime = x.runtime;
71
+ const dbTrue = runtime.dialect.resultBoolean(true);
72
+ const dbFalse = runtime.dialect.resultBoolean(false);
71
73
  const funcTestGeneral = async (
72
74
  expr: string,
73
75
  type: 'group_by' | 'aggregate',
@@ -85,8 +87,12 @@ expressionModels.forEach((x, databaseName) => {
85
87
  };
86
88
 
87
89
  if (expected.success !== undefined) {
90
+ const expectedSuccess =
91
+ typeof expected.success === 'boolean'
92
+ ? runtime.dialect.resultBoolean(expected.success)
93
+ : expected.success;
88
94
  const result = await run();
89
- expect(result.data.path(0, 'f').value).toBe(expected.success);
95
+ expect(result.data.path(0, 'f').value).toBe(expectedSuccess);
90
96
  } else {
91
97
  expect(run).rejects.toThrowError(expected.error);
92
98
  }
@@ -623,6 +629,7 @@ expressionModels.forEach((x, databaseName) => {
623
629
  const result = await expressionModel
624
630
  .loadQuery(
625
631
  `
632
+ # test.debug
626
633
  run: state_facts -> {
627
634
  group_by: state
628
635
  calculate: lag_val is lag(@2011-11-11 11:11:11, 1, now).year = now.year
@@ -630,10 +637,10 @@ expressionModels.forEach((x, databaseName) => {
630
637
  )
631
638
  .run();
632
639
  expect(result.data.path(0, 'lag_val').value).toBe(
633
- booleanResult(true, databaseName)
640
+ runtime.dialect.resultBoolean(true)
634
641
  );
635
642
  expect(result.data.path(1, 'lag_val').value).toBe(
636
- booleanResult(false, databaseName)
643
+ runtime.dialect.resultBoolean(false)
637
644
  );
638
645
  });
639
646
  });
@@ -856,18 +863,18 @@ expressionModels.forEach((x, databaseName) => {
856
863
  : "'+inf'::number";
857
864
  it.when(databaseName !== 'mysql')(`works - ${databaseName}`, async () => {
858
865
  await funcTestMultiple(
859
- [`is_inf(${inf})`, true],
860
- ['is_inf(100)', false],
861
- ['is_inf(null)', false]
866
+ [`is_inf(${inf})`, dbTrue],
867
+ ['is_inf(100)', dbFalse],
868
+ ['is_inf(null)', dbFalse]
862
869
  );
863
870
  });
864
871
  });
865
872
  describe('is_nan', () => {
866
873
  it.when(databaseName !== 'mysql')(`works - ${databaseName}`, async () => {
867
874
  await funcTestMultiple(
868
- ["is_nan('NaN'::number)", true],
869
- ['is_nan(100)', false],
870
- ['is_nan(null)', false]
875
+ ["is_nan('NaN'::number)", dbTrue],
876
+ ['is_nan(100)', dbFalse],
877
+ ['is_nan(null)', dbFalse]
871
878
  );
872
879
  });
873
880
  });
@@ -877,11 +884,11 @@ expressionModels.forEach((x, databaseName) => {
877
884
  ['greatest(1, 10, -100)', 10],
878
885
  [
879
886
  'greatest(@2003, @2004, @1994) = @2004',
880
- booleanResult(true, databaseName),
887
+ runtime.dialect.resultBoolean(true),
881
888
  ],
882
889
  [
883
890
  'greatest(@2023-05-26 11:58:00, @2023-05-26 11:59:00) = @2023-05-26 11:59:00',
884
- booleanResult(true, databaseName),
891
+ runtime.dialect.resultBoolean(true),
885
892
  ],
886
893
  ["greatest('a', 'b')", 'b'],
887
894
  ['greatest(1, null, 0)', null],
@@ -895,11 +902,11 @@ expressionModels.forEach((x, databaseName) => {
895
902
  ['least(1, 10, -100)', -100],
896
903
  [
897
904
  'least(@2003, @2004, @1994) = @1994',
898
- booleanResult(true, databaseName),
905
+ runtime.dialect.resultBoolean(true),
899
906
  ],
900
907
  [
901
908
  'least(@2023-05-26 11:58:00, @2023-05-26 11:59:00) = @2023-05-26 11:58:00',
902
- booleanResult(true, databaseName),
909
+ runtime.dialect.resultBoolean(true),
903
910
  ],
904
911
  ["least('a', 'b')", 'a'],
905
912
  ['least(1, null, 0)', null],
@@ -931,14 +938,17 @@ expressionModels.forEach((x, databaseName) => {
931
938
  await funcTestMultiple(
932
939
  [
933
940
  "starts_with('hello world', 'hello')",
934
- booleanResult(true, databaseName),
941
+ runtime.dialect.resultBoolean(true),
935
942
  ],
936
943
  [
937
944
  "starts_with('hello world', 'world')",
938
- booleanResult(false, databaseName),
945
+ runtime.dialect.resultBoolean(false),
939
946
  ],
940
- ["starts_with(null, 'world')", booleanResult(false, databaseName)],
941
- ["starts_with('hello world', null)", booleanResult(false, databaseName)]
947
+ ["starts_with(null, 'world')", runtime.dialect.resultBoolean(false)],
948
+ [
949
+ "starts_with('hello world', null)",
950
+ runtime.dialect.resultBoolean(false),
951
+ ]
942
952
  );
943
953
  });
944
954
  });
@@ -947,14 +957,14 @@ expressionModels.forEach((x, databaseName) => {
947
957
  await funcTestMultiple(
948
958
  [
949
959
  "ends_with('hello world', 'world')",
950
- booleanResult(true, databaseName),
960
+ runtime.dialect.resultBoolean(true),
951
961
  ],
952
962
  [
953
963
  "ends_with('hello world', 'hello')",
954
- booleanResult(false, databaseName),
964
+ runtime.dialect.resultBoolean(false),
955
965
  ],
956
- ["ends_with(null, 'world')", booleanResult(false, databaseName)],
957
- ["ends_with('hello world', null)", booleanResult(false, databaseName)]
966
+ ["ends_with(null, 'world')", runtime.dialect.resultBoolean(false)],
967
+ ["ends_with('hello world', null)", runtime.dialect.resultBoolean(false)]
958
968
  );
959
969
  });
960
970
  });
@@ -999,14 +1009,14 @@ expressionModels.forEach((x, databaseName) => {
999
1009
  // There are around a billion values that rand() can be, so if this
1000
1010
  // test fails, most likely something is broken. Otherwise, you're the lucky
1001
1011
  // one in a billion!
1002
- await funcTest('rand() = rand()', booleanResult(false, databaseName));
1012
+ await funcTest('rand() = rand()', runtime.dialect.resultBoolean(false));
1003
1013
  });
1004
1014
  });
1005
1015
  describe('pi', () => {
1006
1016
  it(`is pi - ${databaseName}`, async () => {
1007
1017
  await funcTest(
1008
1018
  'abs(pi() - 3.141592653589793) < 0.0000000000001',
1009
- booleanResult(true, databaseName)
1019
+ runtime.dialect.resultBoolean(true)
1010
1020
  );
1011
1021
  });
1012
1022
  });
@@ -1169,8 +1179,8 @@ expressionModels.forEach((x, databaseName) => {
1169
1179
  aggregate: also_passes is abs(count_approx(airport_count)-count(airport_count))/count(airport_count) < 0.3
1170
1180
  }
1171
1181
  `).malloyResultMatches(runtime, {
1172
- 'passes': booleanResult(true, databaseName),
1173
- 'also_passes': booleanResult(true, databaseName),
1182
+ 'passes': runtime.dialect.resultBoolean(true),
1183
+ 'also_passes': runtime.dialect.resultBoolean(true),
1174
1184
  });
1175
1185
  });
1176
1186
  test.when(supported)('works with fanout', async () => {
@@ -1182,7 +1192,7 @@ expressionModels.forEach((x, databaseName) => {
1182
1192
  run: state_facts_fanout -> {
1183
1193
  aggregate: x is state_facts.state.count_approx() > 0
1184
1194
  }
1185
- `).malloyResultMatches(runtime, {x: booleanResult(true, databaseName)});
1195
+ `).malloyResultMatches(runtime, {x: runtime.dialect.resultBoolean(true)});
1186
1196
  });
1187
1197
  });
1188
1198
  describe('last_value', () => {
@@ -1368,7 +1378,7 @@ expressionModels.forEach((x, databaseName) => {
1368
1378
  it.when(isDuckdb)('to_timestamp', async () => {
1369
1379
  await funcTest(
1370
1380
  'to_timestamp(1725555835) = @2024-09-05 17:03:55',
1371
- booleanResult(true, databaseName)
1381
+ runtime.dialect.resultBoolean(true)
1372
1382
  );
1373
1383
  });
1374
1384
  it.when(isDuckdb)('list_extract', async () => {
@@ -1384,7 +1394,7 @@ expressionModels.forEach((x, databaseName) => {
1384
1394
  trino('from_unixtime', async () => {
1385
1395
  await funcTest(
1386
1396
  'from_unixtime(1725555835) = @2024-09-05 17:03:55',
1387
- booleanResult(true, databaseName)
1397
+ runtime.dialect.resultBoolean(true)
1388
1398
  );
1389
1399
  });
1390
1400
  });
@@ -1796,6 +1806,42 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
1796
1806
  },
1797
1807
  ]);
1798
1808
  });
1809
+ // TODO remove the need for the `##! unsafe_complex_select_query` compiler flag
1810
+ it('can be used in a select in a composite source', async () => {
1811
+ await expect(`
1812
+ ##! experimental { function_order_by partition_by composite_sources }
1813
+ ##! unsafe_complex_select_query
1814
+ source: state_facts_composite is compose(state_facts, state_facts)
1815
+ run: state_facts_composite -> {
1816
+ select: state, births, popular_name
1817
+ calculate: prev_births_by_name is lag(births) {
1818
+ partition_by: popular_name
1819
+ order_by: births desc
1820
+ }
1821
+ order_by: births desc
1822
+ limit: 3
1823
+ }
1824
+ `).malloyResultMatches(expressionModel, [
1825
+ {
1826
+ state: 'CA',
1827
+ births: 28810563,
1828
+ popular_name: 'Isabella',
1829
+ prev_births_by_name: null,
1830
+ },
1831
+ {
1832
+ state: 'NY',
1833
+ births: 23694136,
1834
+ popular_name: 'Isabella',
1835
+ prev_births_by_name: 28810563,
1836
+ },
1837
+ {
1838
+ state: 'TX',
1839
+ births: 21467359,
1840
+ popular_name: 'Isabella',
1841
+ prev_births_by_name: 23694136,
1842
+ },
1843
+ ]);
1844
+ });
1799
1845
  });
1800
1846
  });
1801
1847
 
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import {RuntimeList, allDatabases} from '../../runtimes';
26
- import {booleanResult, databasesFromEnvironmentOr} from '../../util';
26
+ import {databasesFromEnvironmentOr} from '../../util';
27
27
  import '../../util/db-jest-matchers';
28
28
 
29
29
  const runtimes = new RuntimeList(databasesFromEnvironmentOr(allDatabases));
@@ -45,7 +45,7 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
45
45
  aggregate: model_count is count()
46
46
  }
47
47
  `).malloyResultMatches(orderByModel, {
48
- big: booleanResult(false, databaseName),
48
+ big: runtime.dialect.resultBoolean(false),
49
49
  model_count: 58451,
50
50
  });
51
51
  });
@@ -62,7 +62,7 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
62
62
  aggregate: model_count is model_count.sum()
63
63
  }
64
64
  `).malloyResultMatches(orderByModel, {
65
- big: booleanResult(false, databaseName),
65
+ big: runtime.dialect.resultBoolean(false),
66
66
  model_count: 58500,
67
67
  });
68
68
  });
@@ -0,0 +1,79 @@
1
+ /*
2
+ * Copyright 2023 Google LLC
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining
5
+ * a copy of this software and associated documentation files
6
+ * (the "Software"), to deal in the Software without restriction,
7
+ * including without limitation the rights to use, copy, modify, merge,
8
+ * publish, distribute, sublicense, and/or sell copies of the Software,
9
+ * and to permit persons to whom the Software is furnished to do so,
10
+ * subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be
13
+ * included in all copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ */
23
+
24
+ import {RuntimeList} from '../../runtimes';
25
+ import '../../util/db-jest-matchers';
26
+ import {describeIfDatabaseAvailable} from '../../util';
27
+
28
+ const [describe, databases] = describeIfDatabaseAvailable(['bigquery']);
29
+ const runtimes = new RuntimeList(databases);
30
+
31
+ afterAll(async () => {
32
+ await runtimes.closeAll();
33
+ });
34
+
35
+ describe('dialect specific function tests for standardsql', () => {
36
+ const runtime = runtimes.runtimeMap.get('bigquery');
37
+
38
+ it('runs the max_by function - bigquery', async () => {
39
+ await expect(`run: bigquery.sql("""
40
+ SELECT 1 as y, 55 as x
41
+ UNION ALL SELECT 50 as y, 22 as x
42
+ UNION ALL SELECT 100 as y, 1 as x
43
+ """) -> {
44
+ aggregate:
45
+ m1 is max_by(x, y)
46
+ m2 is max_by(y, x)
47
+ }`).malloyResultMatches(runtime!, {m1: 1, m2: 1});
48
+ });
49
+
50
+ it('runs the max_by function by grouping - bigquery', async () => {
51
+ await expect(`run: bigquery.sql("""
52
+ SELECT 1 as y, 55 as x, 10 as z
53
+ UNION ALL SELECT 50 as y, 22 as x, 10 as z
54
+ UNION ALL SELECT 1 as y, 10 as x, 20 as z
55
+ UNION ALL SELECT 100 as y, 15 as x, 20 as z
56
+ """) -> {
57
+ group_by:
58
+ z
59
+ aggregate:
60
+ m1 is max_by(x, y)
61
+ m2 is max_by(y, x)
62
+ }`).malloyResultMatches(runtime!, [
63
+ {z: 10, m1: 22, m2: 1},
64
+ {z: 20, m1: 15, m2: 100},
65
+ ]);
66
+ });
67
+
68
+ it('runs the min_by function - bigquery', async () => {
69
+ await expect(`run: bigquery.sql("""
70
+ SELECT 1 as y, 55 as x
71
+ UNION ALL SELECT 50 as y, 22 as x
72
+ UNION ALL SELECT 100 as y, 1 as x
73
+ """) -> {
74
+ aggregate:
75
+ m1 is min_by(x, y)
76
+ m2 is min_by(y, x)
77
+ }`).malloyResultMatches(runtime!, {m1: 55, m2: 100});
78
+ });
79
+ });
@@ -256,6 +256,24 @@ describe.each(runtimes.runtimeList)(
256
256
  }`).malloyResultMatches(runtime, {m1: 1, m2: 1});
257
257
  });
258
258
 
259
+ it(`runs the max_by function by grouping - ${databaseName}`, async () => {
260
+ await expect(`run: ${databaseName}.sql("""
261
+ SELECT 1 as y, 55 as x, 10 as z
262
+ UNION ALL SELECT 50 as y, 22 as x, 10 as z
263
+ UNION ALL SELECT 1 as y, 10 as x, 20 as z
264
+ UNION ALL SELECT 100 as y, 15 as x, 20 as z
265
+ """) -> {
266
+ group_by:
267
+ z
268
+ aggregate:
269
+ m1 is max_by(x, y)
270
+ m2 is max_by(y, x)
271
+ }`).malloyResultMatches(runtime, [
272
+ {z: 10, m1: 22, m2: 1},
273
+ {z: 20, m1: 15, m2: 100},
274
+ ]);
275
+ });
276
+
259
277
  it(`runs the min_by function - ${databaseName}`, async () => {
260
278
  await expect(`run: ${databaseName}.sql("""
261
279
  SELECT 1 as y, 55 as x
@@ -13,51 +13,103 @@ const duckdb = runtimeFor('duckdb');
13
13
 
14
14
  describe('drill query', () => {
15
15
  const model = `
16
- source: carriers is duckdb.table('test/data/duckdb/carriers.parquet') extend {
17
- primary_key: code
18
- measure: carrier_count is count()
16
+ ##! experimental { drill parameters }
17
+ source: carriers is duckdb.table('test/data/duckdb/carriers.parquet') extend {
18
+ primary_key: code
19
+ measure: carrier_count is count()
20
+ }
21
+ source: flights is duckdb.table('test/data/duckdb/flights/part.*.parquet') extend {
22
+ primary_key: id2
23
+ // rename some fields as from their physical names
24
+ rename: \`Origin Code\` is origin
25
+ measure: flight_count is count()
26
+ join_one: carriers with carrier
27
+
28
+ view: top_carriers is {
29
+ group_by: carriers.nickname
30
+ aggregate:
31
+ flight_count
32
+ limit: 1
33
+ }
34
+
35
+ view: over_time is {
36
+ group_by: dep_month is month(dep_time)
37
+ aggregate: flight_count
38
+ limit: 1
19
39
  }
20
- source: flights is duckdb.table('test/data/duckdb/flights/part.*.parquet') extend {
21
- primary_key: id2
22
- // rename some fields as from their physical names
23
- rename: \`Origin Code\` is origin
24
- measure: flight_count is count()
25
- join_one: carriers with carrier
26
-
27
- view: top_carriers is {
28
- group_by: carriers.nickname
29
- aggregate:
30
- flight_count
31
- limit: 1
32
- }
33
-
34
- view: over_time is {
35
- group_by: dep_month is month(dep_time)
36
- aggregate: flight_count
37
- limit: 1
38
- }
39
-
40
- view: by_origin is {
41
- group_by: \`Origin Code\`
42
- aggregate: flight_count
43
- limit: 1
44
- }
45
-
46
- view: no_filter is {
47
- aggregate: flight_count
48
- }
49
-
50
- view: cool_carriers is {
51
- where: carrier = 'AA' or carrier = 'WN'
52
- group_by: \`Origin Code\`
53
- }
40
+
41
+ view: by_origin is {
42
+ group_by: \`Origin Code\`
43
+ aggregate: flight_count
44
+ limit: 1
45
+ }
46
+
47
+ view: no_filter is {
48
+ aggregate: flight_count
54
49
  }
55
- query: top_carriers is flights -> top_carriers
56
- query: over_time is flights -> over_time
57
- query: by_origin is flights -> by_origin
58
- query: no_filter is flights -> no_filter
59
- query: cool_carriers is flights -> cool_carriers
60
- `;
50
+
51
+ view: cool_carriers is {
52
+ where: carrier = 'AA' or carrier = 'WN'
53
+ group_by: \`Origin Code\`
54
+ }
55
+
56
+ view: negative_value is {
57
+ group_by: negative_one is -1
58
+ }
59
+ }
60
+ source: flights_with_parameters(
61
+ number_param is 1,
62
+ string_param is 'foo',
63
+ boolean_param is true,
64
+ date_param is @2000,
65
+ timestamp_param is @2004-01-01 10:00,
66
+ filter_expression_param::filter<number> is f'> 3'
67
+ ) is flights
68
+ source: flights_with_timestamp_param(
69
+ timestamp_param is @2004-01-01 10:00
70
+ ) is flights
71
+ query: top_carriers is flights -> top_carriers
72
+ query: over_time is flights -> over_time
73
+ query: by_origin is flights -> by_origin
74
+ query: no_filter is flights -> no_filter
75
+ query: cool_carriers is flights -> cool_carriers
76
+ query: negative_value is flights -> negative_value
77
+ query: literal_with_nested_view_stable is flights -> {
78
+ where:
79
+ \`Origin Code\` ~ f'SFO, ORD',
80
+ destination = 'SJC'
81
+ group_by: carrier
82
+ nest: cool_carriers
83
+ }
84
+ query: literal_with_nested_view_unstable is flights -> {
85
+ where:
86
+ carriers.nickname ~ '%A%',
87
+ distance > 100,
88
+ month(dep_time) = 7
89
+ group_by: carrier
90
+ nest: cool_carriers
91
+ having: flight_count > 100
92
+ }
93
+ query: already_has_some_drills is flights -> {
94
+ drill:
95
+ \`Origin Code\` ~ f\`SFO, ORD\`,
96
+ destination = "SJC",
97
+ carrier = "AA",
98
+ cool_carriers.\`Origin Code\` = "ORD"
99
+ } + over_time
100
+ query: source_has_parameters is flights_with_parameters(
101
+ number_param is 1,
102
+ string_param is 'foo',
103
+ boolean_param is true,
104
+ date_param is @2000,
105
+ timestamp_param is @2004-01-01 10:00,
106
+ filter_expression_param is f'> 3'
107
+ ) -> top_carriers
108
+ query: source_has_timezone_param is flights_with_timestamp_param(
109
+ timestamp_param is @2004-01-01 10:00:00[America/Los_Angeles]
110
+ ) -> top_carriers
111
+ query: only_default_params is flights_with_parameters -> top_carriers
112
+ `;
61
113
 
62
114
  beforeEach(() => {
63
115
  jest.spyOn(console, 'warn').mockImplementation(() => {});
@@ -70,12 +122,9 @@ describe('drill query', () => {
70
122
  .run();
71
123
  const table = getDataTree(API.util.wrapResult(result));
72
124
  const expDrillQuery =
73
- 'run: flights -> {\n' +
74
- ' where:\n' +
75
- " carriers.nickname = 'Southwest'\n" +
76
- '} + { select: * }';
125
+ 'run: flights -> { drill: top_carriers.nickname = "Southwest" } + { select: * }';
77
126
  const row = table.rows[0];
78
- expect(row.getDrillQuery()).toEqual(expDrillQuery);
127
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
79
128
  });
80
129
 
81
130
  test('can handle expression fields', async () => {
@@ -85,10 +134,9 @@ describe('drill query', () => {
85
134
  .run();
86
135
  const table = getDataTree(API.util.wrapResult(result));
87
136
  const expDrillQuery =
88
- 'run: flights -> {\n where:\n ' +
89
- 'month(dep_time) = 8\n} + { select: * }';
137
+ 'run: flights -> { drill: over_time.dep_month = 8 } + { select: * }';
90
138
  const row = table.rows[0];
91
- expect(row.getDrillQuery()).toEqual(expDrillQuery);
139
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
92
140
  });
93
141
 
94
142
  test('can handle renamed and multi-word field names', async () => {
@@ -98,10 +146,9 @@ describe('drill query', () => {
98
146
  .run();
99
147
  const table = getDataTree(API.util.wrapResult(result));
100
148
  const expDrillQuery =
101
- 'run: flights -> {\n where:\n ' +
102
- "`Origin Code` = 'ATL'\n} + { select: * }";
149
+ 'run: flights -> { drill: by_origin.`Origin Code` = "ATL" } + { select: * }';
103
150
  const row = table.rows[0];
104
- expect(row.getDrillQuery()).toEqual(expDrillQuery);
151
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
105
152
  });
106
153
 
107
154
  test('can handle queries with no filter', async () => {
@@ -112,7 +159,7 @@ describe('drill query', () => {
112
159
  const table = getDataTree(API.util.wrapResult(result));
113
160
  const expDrillQuery = 'run: flights -> { select: * }';
114
161
  const row = table.rows[0];
115
- expect(row.getDrillQuery()).toEqual(expDrillQuery);
162
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
116
163
  });
117
164
 
118
165
  test('can handle view filters', async () => {
@@ -122,12 +169,170 @@ describe('drill query', () => {
122
169
  .run();
123
170
  const table = getDataTree(API.util.wrapResult(result));
124
171
  const expDrillQuery =
125
- 'run: flights -> {\n' +
126
- ' where:\n' +
127
- " carrier = 'AA' or carrier = 'WN',\n" +
128
- " `Origin Code` = 'ABQ'\n" +
129
- '} + { select: * }';
172
+ 'run: flights -> { drill: cool_carriers.`Origin Code` = "ABQ" } + { select: * }';
130
173
  const row = table.rows[0];
131
- expect(row.getDrillQuery()).toEqual(expDrillQuery);
174
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
175
+ });
176
+
177
+ test('can handle filters that are not in a view (not stable compatible)', async () => {
178
+ const result = await duckdb
179
+ .loadModel(model)
180
+ .loadQueryByName('literal_with_nested_view_unstable')
181
+ .run();
182
+ const table = getDataTree(API.util.wrapResult(result));
183
+ const expDrillQuery = `run: flights -> {
184
+ drill:
185
+ carriers.nickname ~ '%A%',
186
+ distance > 100,
187
+ month(dep_time) = 7,
188
+ carrier = "AA"
189
+ } + { select: * }`;
190
+ const row1 = table.rows[0];
191
+ expect(row1.getDrillQueryMalloy()).toEqual(expDrillQuery);
192
+ const nest = row1.column('cool_carriers');
193
+ expect(nest.isRepeatedRecord()).toBe(true);
194
+ if (nest.isRepeatedRecord()) {
195
+ const expDrillQuery = `run: flights -> {
196
+ drill:
197
+ carriers.nickname ~ '%A%',
198
+ distance > 100,
199
+ month(dep_time) = 7,
200
+ carrier = "AA",
201
+ cool_carriers.\`Origin Code\` = "ABQ"
202
+ } + { select: * }`;
203
+ const row = nest.rows[0];
204
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
205
+ }
206
+ });
207
+
208
+ test('can handle filters that are not in a view (stable compatible)', async () => {
209
+ const result = await duckdb
210
+ .loadModel(model)
211
+ .loadQueryByName('literal_with_nested_view_stable')
212
+ .run();
213
+ const table = getDataTree(API.util.wrapResult(result));
214
+ const expDrillQuery = `run: flights -> {
215
+ drill:
216
+ \`Origin Code\` ~ f\`SFO, ORD\`,
217
+ destination = "SJC",
218
+ carrier = "AA"
219
+ } + { select: * }`;
220
+ const row1 = table.rows[0];
221
+ expect(row1.getDrillQueryMalloy()).toEqual(expDrillQuery);
222
+ const nest = row1.column('cool_carriers');
223
+ expect(nest.isRepeatedRecord()).toBe(true);
224
+ if (nest.isRepeatedRecord()) {
225
+ const expDrillQuery = `run: flights -> {
226
+ drill:
227
+ \`Origin Code\` ~ f\`SFO, ORD\`,
228
+ destination = "SJC",
229
+ carrier = "AA",
230
+ cool_carriers.\`Origin Code\` = "ORD"
231
+ } + { select: * }`;
232
+ const row = nest.rows[0];
233
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
234
+ }
235
+ });
236
+
237
+ test('can handle drills that are already there', async () => {
238
+ const result = await duckdb
239
+ .loadModel(model)
240
+ .loadQueryByName('already_has_some_drills')
241
+ .run();
242
+ const table = getDataTree(API.util.wrapResult(result));
243
+ const expDrillQuery = `run: flights -> {
244
+ drill:
245
+ \`Origin Code\` ~ f\`SFO, ORD\`,
246
+ destination = "SJC",
247
+ carrier = "AA",
248
+ cool_carriers.\`Origin Code\` = "ORD",
249
+ over_time.dep_month = 5
250
+ } + { select: * }`;
251
+ const row = table.rows[0];
252
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
253
+ });
254
+
255
+ test('negative number can be used in stable query filter', async () => {
256
+ const result = await duckdb
257
+ .loadModel(model)
258
+ .loadQueryByName('negative_value')
259
+ .run();
260
+ const table = getDataTree(API.util.wrapResult(result));
261
+ const expDrillQuery =
262
+ 'run: flights -> { drill: negative_value.negative_one = -1 } + { select: * }';
263
+ const row = table.rows[0];
264
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
265
+ expect(row.getStableDrillQuery()).toMatchObject({
266
+ definition: {
267
+ kind: 'arrow',
268
+ source: {
269
+ kind: 'source_reference',
270
+ name: 'flights',
271
+ },
272
+ view: {
273
+ kind: 'segment',
274
+ operations: [
275
+ {
276
+ filter: {
277
+ field_reference: {
278
+ name: 'negative_one',
279
+ path: ['negative_value'],
280
+ },
281
+ kind: 'literal_equality',
282
+ value: {
283
+ kind: 'number_literal',
284
+ number_value: -1,
285
+ },
286
+ },
287
+ kind: 'drill',
288
+ },
289
+ ],
290
+ },
291
+ },
292
+ });
293
+ });
294
+
295
+ describe('source parameters', () => {
296
+ test('can handle source parameters', async () => {
297
+ const result = await duckdb
298
+ .loadModel(model)
299
+ .loadQueryByName('source_has_parameters')
300
+ .run();
301
+ const table = getDataTree(API.util.wrapResult(result));
302
+ const expDrillQuery = `run: flights_with_parameters(
303
+ number_param is 1,
304
+ string_param is "foo",
305
+ boolean_param is true,
306
+ date_param is @2000,
307
+ timestamp_param is @2004-01-01 10:00,
308
+ filter_expression_param is f\`> 3\`
309
+ ) -> { drill: top_carriers.nickname = "Southwest" } + { select: * }`;
310
+ const row = table.rows[0];
311
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
312
+ });
313
+
314
+ test('can handle timezone in source parameter', async () => {
315
+ const result = await duckdb
316
+ .loadModel(model)
317
+ .loadQueryByName('source_has_timezone_param')
318
+ .run();
319
+ const table = getDataTree(API.util.wrapResult(result));
320
+ const expDrillQuery =
321
+ 'run: flights_with_timestamp_param(timestamp_param is @2004-01-01 10:00:00[America/Los_Angeles]) -> { drill: top_carriers.nickname = "Southwest" } + { select: * }';
322
+ const row = table.rows[0];
323
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
324
+ });
325
+
326
+ test('default_params_are_not_included', async () => {
327
+ const result = await duckdb
328
+ .loadModel(model)
329
+ .loadQueryByName('only_default_params')
330
+ .run();
331
+ const table = getDataTree(API.util.wrapResult(result));
332
+ const expDrillQuery =
333
+ 'run: flights_with_parameters -> { drill: top_carriers.nickname = "Southwest" } + { select: * }';
334
+ const row = table.rows[0];
335
+ expect(row.getDrillQueryMalloy()).toEqual(expDrillQuery);
336
+ });
132
337
  });
133
338
  });
package/src/util/index.ts CHANGED
@@ -29,6 +29,7 @@ import type {
29
29
  Result,
30
30
  Runtime,
31
31
  Expr,
32
+ SingleConnectionRuntime,
32
33
  } from '@malloydata/malloy';
33
34
  import {composeSQLExpr} from '@malloydata/malloy';
34
35
  export * from '@malloydata/malloy/test';
@@ -102,7 +103,7 @@ function sqlSafe(str: string): string {
102
103
  }
103
104
 
104
105
  export function mkSqlEqWith(
105
- runtime: Runtime,
106
+ runtime: SingleConnectionRuntime,
106
107
  cName: string,
107
108
  initV?: InitValues
108
109
  ) {
@@ -198,19 +199,3 @@ export async function runQuery(runtime: Runtime, querySrc: string) {
198
199
 
199
200
  return result;
200
201
  }
201
-
202
- export function booleanResult(value: boolean, dbName: string) {
203
- if (dbName === 'mysql') {
204
- return value ? 1 : 0;
205
- } else {
206
- return value;
207
- }
208
- }
209
-
210
- export function booleanCode(value: boolean, dbName: string) {
211
- if (dbName === 'mysql') {
212
- return value ? '1' : '0';
213
- } else {
214
- return value ? 'true' : 'false';
215
- }
216
- }