@malloydata/malloy-tests 0.0.433 → 0.0.435
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/CONTEXT.md +18 -6
- package/data/parquet_loader.ts +106 -0
- package/duckdb/load_test_data.ts +15 -48
- package/mssql/connection_string.ts +28 -0
- package/mssql/load_test_data.sh +12 -0
- package/mssql/load_test_data.ts +63 -0
- package/mssql/mssql_start.sh +30 -143
- package/mssql/mssql_stop.sh +2 -2
- package/mysql/load_test_data.sh +12 -0
- package/mysql/load_test_data.ts +39 -0
- package/mysql/mysql_start.sh +36 -30
- package/package.json +12 -11
- package/postgres/load_test_data.sh +12 -0
- package/postgres/load_test_data.ts +34 -0
- package/postgres/postgres_start.sh +2 -6
- package/presto/Dockerfile.slim +12 -6
- package/presto/hive.properties +8 -0
- package/presto/presto_start.sh +58 -36
- package/src/core/experimental-dialects.spec.ts +125 -41
- package/src/core/restricted-mode.spec.ts +27 -0
- package/src/core/tags.spec.ts +22 -0
- package/src/core/virtual-sql.spec.ts +253 -0
- package/src/databases/all/db_index.spec.ts +4 -2
- package/src/databases/all/expr.spec.ts +150 -18
- package/src/databases/all/nest.spec.ts +237 -0
- package/src/databases/all/nomodel.spec.ts +0 -75
- package/src/databases/all/problems.spec.ts +32 -0
- package/src/databases/sqlserver/sqlserver.spec.ts +395 -0
- package/src/runtimes.ts +49 -9
- package/src/test-select.ts +10 -14
- package/trino/hive.properties +11 -0
- package/trino/hive_ddl.ts +148 -0
- package/trino/trino_start.sh +54 -38
- package/tsconfig.json +15 -1
- package/bin/dump_malloytest.sh +0 -1
- package/bin/postgres_init.sh +0 -10
- package/bin/postgres_start.sh +0 -2
- package/bin/postgres_stop.sh +0 -2
- package/data/mysql/malloytest.mysql.gz +0 -0
- package/data/postgres/malloytest-postgres.sql.gz +0 -0
- package/data/postgres/state_facts.sql +0 -366
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Contributors to the Malloy project
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {DuckDBConnection} from '@malloydata/db-duckdb';
|
|
7
|
+
import {
|
|
8
|
+
FixedConnectionMap,
|
|
9
|
+
InMemoryURLReader,
|
|
10
|
+
Malloy,
|
|
11
|
+
MalloyConfig,
|
|
12
|
+
Runtime,
|
|
13
|
+
} from '@malloydata/malloy';
|
|
14
|
+
import type {VirtualMap} from '@malloydata/malloy';
|
|
15
|
+
|
|
16
|
+
const virtualSource = `
|
|
17
|
+
##! experimental.virtual_source
|
|
18
|
+
type: row_fields is { x :: number }
|
|
19
|
+
source: data is duckdb.virtual('data')::row_fields
|
|
20
|
+
`;
|
|
21
|
+
const sqlSource = `
|
|
22
|
+
source: wrapped is duckdb.sql("""
|
|
23
|
+
SELECT x + 10 AS y FROM %{ data -> { select: x } }
|
|
24
|
+
""")
|
|
25
|
+
run: wrapped -> { select: y }
|
|
26
|
+
`;
|
|
27
|
+
const source = virtualSource + sqlSource;
|
|
28
|
+
|
|
29
|
+
function virtualMap(table: string): VirtualMap {
|
|
30
|
+
return new Map([['duckdb', new Map([['data', table]])]]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Language cases live in packages/malloy/src/lang/test/virtual-sql.spec.ts.
|
|
34
|
+
// These tests cover API option forwarding and real SQL schemas.
|
|
35
|
+
describe('virtual sources in SQL blocks through the Foundation API', () => {
|
|
36
|
+
const connection = new DuckDBConnection({
|
|
37
|
+
name: 'duckdb',
|
|
38
|
+
databasePath: ':memory:',
|
|
39
|
+
});
|
|
40
|
+
const connections = FixedConnectionMap.fromArray([connection]);
|
|
41
|
+
const compileMap = virtualMap('translation_data');
|
|
42
|
+
const executionMap = virtualMap('execution_data');
|
|
43
|
+
const urlReader = new InMemoryURLReader(new Map());
|
|
44
|
+
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
await connection.runSQL(
|
|
47
|
+
'CREATE TEMP TABLE translation_data AS SELECT 1::INTEGER AS x'
|
|
48
|
+
);
|
|
49
|
+
await connection.runSQL(
|
|
50
|
+
'CREATE TEMP TABLE execution_data AS SELECT 2::INTEGER AS x'
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterAll(async () => {
|
|
55
|
+
await connection.close();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
jest.restoreAllMocks();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('Malloy.compile uses a translation map without binding execution', async () => {
|
|
63
|
+
const model = await Malloy.compile({
|
|
64
|
+
source,
|
|
65
|
+
connections,
|
|
66
|
+
urlReader,
|
|
67
|
+
virtualMap: compileMap,
|
|
68
|
+
});
|
|
69
|
+
const result = model.preparedQuery.getPreparedResult({
|
|
70
|
+
virtualMap: executionMap,
|
|
71
|
+
});
|
|
72
|
+
expect(result.sql).toContain('execution_data');
|
|
73
|
+
expect(result.sql).not.toContain('translation_data');
|
|
74
|
+
expect((await connection.runSQL(result.sql)).rows).toEqual([{y: 12}]);
|
|
75
|
+
expect(() => model.preparedQuery.getPreparedResult()).toThrow(
|
|
76
|
+
/No virtual-map entry/
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('loadModel forwards its map and execution can override it', async () => {
|
|
81
|
+
const runtime = new Runtime({connections});
|
|
82
|
+
runtime.virtualMap = virtualMap('missing_runtime_table');
|
|
83
|
+
const result = await runtime
|
|
84
|
+
.loadModel(source, {virtualMap: compileMap})
|
|
85
|
+
.loadFinalQuery()
|
|
86
|
+
.run({virtualMap: executionMap});
|
|
87
|
+
expect(result.data.value).toEqual([{y: 12}]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('runtime config supplies the translation map', async () => {
|
|
91
|
+
const config = new MalloyConfig({
|
|
92
|
+
virtualMap: {duckdb: {data: 'translation_data'}},
|
|
93
|
+
});
|
|
94
|
+
const runtime = new Runtime({connections, config});
|
|
95
|
+
const result = await runtime.loadQuery(source).run();
|
|
96
|
+
expect(result.data.value).toEqual([{y: 11}]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('model.loadQuery uses an explicit translation map', async () => {
|
|
100
|
+
const runtime = new Runtime({connections});
|
|
101
|
+
runtime.virtualMap = virtualMap('missing_runtime_table');
|
|
102
|
+
const result = await runtime
|
|
103
|
+
.loadModel(virtualSource)
|
|
104
|
+
.loadQuery(sqlSource, {virtualMap: compileMap})
|
|
105
|
+
.run({virtualMap: executionMap});
|
|
106
|
+
expect(result.data.value).toEqual([{y: 12}]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('model.loadQuery falls back to the runtime translation map', async () => {
|
|
110
|
+
const runtime = new Runtime({connections});
|
|
111
|
+
runtime.virtualMap = compileMap;
|
|
112
|
+
const result = await runtime
|
|
113
|
+
.loadModel(virtualSource)
|
|
114
|
+
.loadQuery(sqlSource)
|
|
115
|
+
.run({virtualMap: executionMap});
|
|
116
|
+
expect(result.data.value).toEqual([{y: 12}]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('model.loadQuery inherits the model translation map', async () => {
|
|
120
|
+
const runtime = new Runtime({connections});
|
|
121
|
+
runtime.virtualMap = virtualMap('missing_runtime_table');
|
|
122
|
+
const result = await runtime
|
|
123
|
+
.loadModel(virtualSource, {virtualMap: compileMap})
|
|
124
|
+
.loadQuery(sqlSource)
|
|
125
|
+
.run({virtualMap: executionMap});
|
|
126
|
+
expect(result.data.value).toEqual([{y: 12}]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('an empty translation map overrides the runtime map', async () => {
|
|
130
|
+
const runtime = new Runtime({connections});
|
|
131
|
+
runtime.virtualMap = compileMap;
|
|
132
|
+
await expect(
|
|
133
|
+
runtime.loadModel(source, {virtualMap: new Map()}).getModel()
|
|
134
|
+
).rejects.toThrow(/No virtual-map entry/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('an invalid translation binding fails before SQL schema discovery', async () => {
|
|
138
|
+
const fetchSchema = jest
|
|
139
|
+
.spyOn(connection, 'fetchSchemaForSQLStruct')
|
|
140
|
+
.mockRejectedValue(new Error('Unexpected SQL schema request'));
|
|
141
|
+
await expect(
|
|
142
|
+
Malloy.compile({
|
|
143
|
+
source,
|
|
144
|
+
connections,
|
|
145
|
+
urlReader,
|
|
146
|
+
virtualMap: virtualMap('translation_data; SELECT 1'),
|
|
147
|
+
})
|
|
148
|
+
).rejects.toThrow(/virtualMap entry 'duckdb.data'.*duckdb/);
|
|
149
|
+
expect(fetchSchema).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('an invalid execution binding fails before query execution', async () => {
|
|
153
|
+
const runtime = new Runtime({connections});
|
|
154
|
+
const query = runtime.loadQuery(source, {virtualMap: compileMap});
|
|
155
|
+
await query.getPreparedQuery();
|
|
156
|
+
const runSQL = jest
|
|
157
|
+
.spyOn(connection, 'runSQL')
|
|
158
|
+
.mockRejectedValue(new Error('Unexpected query execution'));
|
|
159
|
+
await expect(
|
|
160
|
+
query.run({virtualMap: virtualMap('execution_data; SELECT 1')})
|
|
161
|
+
).rejects.toThrow(/virtualMap entry 'duckdb.data'.*duckdb/);
|
|
162
|
+
expect(runSQL).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test.each(['explicit', 'inherited', 'runtime'])(
|
|
166
|
+
'extendModel uses its %s translation map',
|
|
167
|
+
async binding => {
|
|
168
|
+
const runtime = new Runtime({connections});
|
|
169
|
+
runtime.virtualMap =
|
|
170
|
+
binding === 'runtime' ? compileMap : virtualMap('missing_table');
|
|
171
|
+
const model = runtime.loadModel(virtualSource, {
|
|
172
|
+
virtualMap:
|
|
173
|
+
binding === 'inherited'
|
|
174
|
+
? compileMap
|
|
175
|
+
: binding === 'explicit'
|
|
176
|
+
? virtualMap('missing_inherited_table')
|
|
177
|
+
: undefined,
|
|
178
|
+
});
|
|
179
|
+
const extended = model.extendModel(
|
|
180
|
+
sqlSource,
|
|
181
|
+
binding === 'explicit' ? {virtualMap: compileMap} : undefined
|
|
182
|
+
);
|
|
183
|
+
const result = await extended.loadFinalQuery().run();
|
|
184
|
+
expect(result.data.value).toEqual([{y: 11}]);
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
test('Malloy.compile forwards its map to a pre-parsed translator', async () => {
|
|
189
|
+
const parse = Malloy.parse({source});
|
|
190
|
+
const model = await Malloy.compile({
|
|
191
|
+
parse,
|
|
192
|
+
connections,
|
|
193
|
+
urlReader,
|
|
194
|
+
virtualMap: compileMap,
|
|
195
|
+
});
|
|
196
|
+
const result = model.preparedQuery.getPreparedResult({
|
|
197
|
+
virtualMap: executionMap,
|
|
198
|
+
});
|
|
199
|
+
expect((await connection.runSQL(result.sql)).rows).toEqual([{y: 12}]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test.each(['compile', 'run'])(
|
|
203
|
+
'%s ignores an invalid override binding when it is unused',
|
|
204
|
+
async operation => {
|
|
205
|
+
const badMap = virtualMap('translation_data');
|
|
206
|
+
badMap.get('duckdb')!.set('unused', 'translation_data; SELECT 1');
|
|
207
|
+
const runtime = new Runtime({connections});
|
|
208
|
+
const result =
|
|
209
|
+
operation === 'compile'
|
|
210
|
+
? Malloy.compile({source, connections, urlReader, virtualMap: badMap})
|
|
211
|
+
: runtime
|
|
212
|
+
.loadQuery(source, {virtualMap: compileMap})
|
|
213
|
+
.run({virtualMap: badMap});
|
|
214
|
+
await expect(result).resolves.toBeDefined();
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
test.each(['config', 'assignment'])(
|
|
219
|
+
'bindings from %s are preserved and checked only when used',
|
|
220
|
+
async kind => {
|
|
221
|
+
const badTable = 'translation_data; SELECT 1';
|
|
222
|
+
const config =
|
|
223
|
+
kind === 'config'
|
|
224
|
+
? new MalloyConfig({
|
|
225
|
+
virtualMap: {
|
|
226
|
+
duckdb: {data: 'translation_data', unused: badTable},
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
: undefined;
|
|
230
|
+
const runtime = new Runtime({connections, config});
|
|
231
|
+
if (kind === 'assignment') {
|
|
232
|
+
const map = virtualMap('translation_data');
|
|
233
|
+
map.get('duckdb')!.set('unused', badTable);
|
|
234
|
+
runtime.virtualMap = map;
|
|
235
|
+
}
|
|
236
|
+
expect(runtime.virtualMap?.get('duckdb')?.get('unused')).toBe(badTable);
|
|
237
|
+
expect((await runtime.loadQuery(source).run()).data.value).toEqual([
|
|
238
|
+
{y: 11},
|
|
239
|
+
]);
|
|
240
|
+
await expect(
|
|
241
|
+
runtime
|
|
242
|
+
.loadQuery(
|
|
243
|
+
`
|
|
244
|
+
${virtualSource}
|
|
245
|
+
source: bad is duckdb.virtual('unused')::row_fields
|
|
246
|
+
run: bad -> { select: x }
|
|
247
|
+
`
|
|
248
|
+
)
|
|
249
|
+
.getSQL()
|
|
250
|
+
).rejects.toThrow(/virtualMap entry 'duckdb.unused'.*duckdb/);
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
});
|
|
@@ -79,11 +79,13 @@ runtimes.runtimeMap.forEach((runtime, databaseName) => {
|
|
|
79
79
|
`).toMatchResult(testModel, {fieldName: 'one', weight: 51});
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
// bigquery doesn't support row count based sampling
|
|
82
|
+
// bigquery doesn't support row count based sampling; sqlserver's TABLESAMPLE
|
|
83
|
+
// returns whole pages, not the count asked for.
|
|
83
84
|
test.when(
|
|
84
85
|
databaseName !== 'bigquery' &&
|
|
85
86
|
databaseName !== 'trino' &&
|
|
86
|
-
databaseName !== 'presto'
|
|
87
|
+
databaseName !== 'presto' &&
|
|
88
|
+
databaseName !== 'sqlserver'
|
|
87
89
|
)(`index rows count - ${databaseName}`, async () => {
|
|
88
90
|
await expect(`
|
|
89
91
|
run: ${databaseName}.table('malloytest.state_facts') extend {
|
|
@@ -337,11 +337,8 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
|
|
|
337
337
|
});
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
'model: dates named',
|
|
343
|
-
async () => {
|
|
344
|
-
await expect(`
|
|
340
|
+
test('model: dates named', async () => {
|
|
341
|
+
await expect(`
|
|
345
342
|
run: ${databaseName}.table('malloytest.alltypes')->{
|
|
346
343
|
group_by:
|
|
347
344
|
t_date,
|
|
@@ -356,19 +353,18 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
|
|
|
356
353
|
t_timestamp_year is t_timestamp.year,
|
|
357
354
|
}
|
|
358
355
|
`).toMatchResult(testModel, {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
);
|
|
356
|
+
t_date: new Date('2020-03-02'),
|
|
357
|
+
t_date_month: new Date('2020-03-01'),
|
|
358
|
+
t_date_year: new Date('2020-01-01'),
|
|
359
|
+
t_timestamp: new Date('2020-03-02T12:35:56.000Z'),
|
|
360
|
+
t_timestamp_second: new Date('2020-03-02T12:35:56.000Z'),
|
|
361
|
+
t_timestamp_minute: new Date('2020-03-02T12:35:00.000Z'),
|
|
362
|
+
t_timestamp_hour: new Date('2020-03-02T12:00:00.000Z'),
|
|
363
|
+
t_timestamp_date: new Date('2020-03-02'),
|
|
364
|
+
t_timestamp_month: new Date('2020-03-01'),
|
|
365
|
+
t_timestamp_year: new Date('2020-01-01'),
|
|
366
|
+
});
|
|
367
|
+
});
|
|
372
368
|
|
|
373
369
|
it('named query metadata undefined', async () => {
|
|
374
370
|
const result = await expressionModel
|
|
@@ -736,6 +732,142 @@ describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
|
|
|
736
732
|
}
|
|
737
733
|
);
|
|
738
734
|
|
|
735
|
+
// A filtered join is emitted as a subquery, so every join below it lives
|
|
736
|
+
// inside that subquery and has to be projected back out for the outer query
|
|
737
|
+
// to name it -- at every depth, not just the direct children.
|
|
738
|
+
describe('joins below a filtered join', () => {
|
|
739
|
+
const chain = (filterTwo: string) => `
|
|
740
|
+
source: sf is ${databaseName}.table('malloytest.state_facts')
|
|
741
|
+
|
|
742
|
+
source: three is sf
|
|
743
|
+
source: two is sf extend {
|
|
744
|
+
${filterTwo}
|
|
745
|
+
join_one: three on three.state = state
|
|
746
|
+
}
|
|
747
|
+
source: one is sf extend {
|
|
748
|
+
where: state ~ 'A%'
|
|
749
|
+
join_one: two on two.state = state
|
|
750
|
+
}
|
|
751
|
+
source: zero is sf extend { join_one: one on one.state = state }
|
|
752
|
+
`;
|
|
753
|
+
|
|
754
|
+
test.when(runtime.dialect.supportsComplexFilteredSources)(
|
|
755
|
+
'two levels below',
|
|
756
|
+
async () => {
|
|
757
|
+
await expect(`
|
|
758
|
+
${chain('')}
|
|
759
|
+
run: zero -> {
|
|
760
|
+
aggregate:
|
|
761
|
+
child is one.two.count()
|
|
762
|
+
grandchild is one.two.three.count()
|
|
763
|
+
}
|
|
764
|
+
`).toMatchResult(testModel, {child: 4, grandchild: 4});
|
|
765
|
+
|
|
766
|
+
await expect(`
|
|
767
|
+
${chain('')}
|
|
768
|
+
run: zero -> {
|
|
769
|
+
group_by: grandchild_state is one.two.three.state
|
|
770
|
+
order_by: grandchild_state
|
|
771
|
+
limit: 1
|
|
772
|
+
}
|
|
773
|
+
`).toMatchResult(testModel, {grandchild_state: 'AK'});
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// `having:` and `calculate:` name output fields, so their field usage is
|
|
778
|
+
// rooted in the query's output and does not resolve against its source.
|
|
779
|
+
test.when(runtime.dialect.supportsComplexFilteredSources)(
|
|
780
|
+
'having on an output field, with a filtered join',
|
|
781
|
+
async () => {
|
|
782
|
+
await expect(`
|
|
783
|
+
${chain('')}
|
|
784
|
+
run: zero -> {
|
|
785
|
+
group_by: grandchild_state is one.two.three.state
|
|
786
|
+
aggregate: rows_here is count()
|
|
787
|
+
having: rows_here > 0
|
|
788
|
+
order_by: grandchild_state
|
|
789
|
+
limit: 1
|
|
790
|
+
}
|
|
791
|
+
`).toMatchResult(testModel, {grandchild_state: 'AK', rows_here: 1});
|
|
792
|
+
}
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
// The packed columns are the source's own column names, so the struct has
|
|
796
|
+
// to quote them on the value side and in any type it declares.
|
|
797
|
+
test.when(runtime.dialect.supportsComplexFilteredSources)(
|
|
798
|
+
'a packed column name which needs quoting',
|
|
799
|
+
async () => {
|
|
800
|
+
const oddName = runtime.dialect.sqlQuoteIdentifier('order');
|
|
801
|
+
await expect(`
|
|
802
|
+
source: two is ${databaseName}.sql("""SELECT 1 as ${oddName}, 1 as k""")
|
|
803
|
+
source: one is ${databaseName}.sql("""SELECT 1 as k, 2 as j""") extend {
|
|
804
|
+
where: j > 0
|
|
805
|
+
join_one: two on two.k = k
|
|
806
|
+
}
|
|
807
|
+
source: zero is ${databaseName}.sql("""SELECT 1 as k""") extend {
|
|
808
|
+
join_one: one on one.k = k
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
run: zero -> { group_by: g is one.two.\`order\` }
|
|
812
|
+
`).toMatchResult(testModel, {g: 1});
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// A distinct key is the one thing the packed struct must carry which no
|
|
817
|
+
// expression in the query names, and when the key is computed what the
|
|
818
|
+
// query reads is the columns its expression reads, not the key's name.
|
|
819
|
+
test.when(runtime.dialect.supportsComplexFilteredSources)(
|
|
820
|
+
'two levels below, reached by a computed primary key',
|
|
821
|
+
async () => {
|
|
822
|
+
await expect(`
|
|
823
|
+
source: sf is ${databaseName}.table('malloytest.state_facts')
|
|
824
|
+
|
|
825
|
+
source: three is sf extend {
|
|
826
|
+
dimension: pk is births
|
|
827
|
+
primary_key: pk
|
|
828
|
+
}
|
|
829
|
+
source: two is sf extend { join_one: three on three.state = state }
|
|
830
|
+
source: one is sf extend {
|
|
831
|
+
where: state ~ 'A%'
|
|
832
|
+
join_one: two on two.state = state
|
|
833
|
+
}
|
|
834
|
+
source: zero is sf extend { join_one: one on one.state = state }
|
|
835
|
+
|
|
836
|
+
run: zero -> {
|
|
837
|
+
aggregate:
|
|
838
|
+
child is one.two.count()
|
|
839
|
+
grandchild is one.two.three.count()
|
|
840
|
+
}
|
|
841
|
+
`).toMatchResult(testModel, {child: 4, grandchild: 4});
|
|
842
|
+
}
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
// `two` is filtered as well, so it becomes a subquery of its own inside
|
|
846
|
+
// the one for `one`, and `three` reaches the outer query through it.
|
|
847
|
+
test.when(runtime.dialect.supportsComplexFilteredSources)(
|
|
848
|
+
'two levels below, both filtered',
|
|
849
|
+
async () => {
|
|
850
|
+
await expect(`
|
|
851
|
+
${chain("where: state != 'AK'")}
|
|
852
|
+
run: zero -> {
|
|
853
|
+
aggregate:
|
|
854
|
+
child is one.two.count()
|
|
855
|
+
grandchild is one.two.three.count()
|
|
856
|
+
}
|
|
857
|
+
`).toMatchResult(testModel, {child: 3, grandchild: 3});
|
|
858
|
+
|
|
859
|
+
await expect(`
|
|
860
|
+
${chain("where: state != 'AK'")}
|
|
861
|
+
run: zero -> {
|
|
862
|
+
group_by: grandchild_state is one.two.three.state
|
|
863
|
+
order_by: grandchild_state
|
|
864
|
+
limit: 1
|
|
865
|
+
}
|
|
866
|
+
`).toMatchResult(testModel, {grandchild_state: 'AL'});
|
|
867
|
+
}
|
|
868
|
+
);
|
|
869
|
+
});
|
|
870
|
+
|
|
739
871
|
test('joined filtered explores with NO dependencies', async () => {
|
|
740
872
|
await expect(`
|
|
741
873
|
source: sf is ${databaseName}.table('malloytest.state_facts') extend {
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Contributors to the Malloy project
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {RuntimeList, allDatabases} from '../../runtimes';
|
|
7
|
+
import {databasesFromEnvironmentOr} from '../../util';
|
|
8
|
+
import '@malloydata/malloy/test/matchers';
|
|
9
|
+
import {wrapTestModel} from '@malloydata/malloy/test';
|
|
10
|
+
|
|
11
|
+
const runtimes = new RuntimeList(databasesFromEnvironmentOr(allDatabases));
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await runtimes.closeAll();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// A nest is a column whose value is the result of a query. What that column
|
|
18
|
+
// contains, and in what order, is the subject here.
|
|
19
|
+
//
|
|
20
|
+
// The order is the order of the stage which produced it -- the order the user
|
|
21
|
+
// asked for, or the one Malloy picked. Reading the array back is the only way to
|
|
22
|
+
// see that, and the ordering is attached to the SQL which packs the array, which
|
|
23
|
+
// is a different expression for a one-stage nest than for a pipelined one.
|
|
24
|
+
//
|
|
25
|
+
// The ordering tests nest over the four 'N' states whose popular_name is
|
|
26
|
+
// Isabella, because their four candidate orderings are all distinct:
|
|
27
|
+
// state desc NY NV NM NJ
|
|
28
|
+
// state asc NJ NM NV NY
|
|
29
|
+
// births desc NY NJ NM NV
|
|
30
|
+
// births asc NV NM NJ NY
|
|
31
|
+
// A pipelined test orders its first stage the opposite way from its last, so an
|
|
32
|
+
// array carrying the wrong stage's order fails instead of coincidentally passing.
|
|
33
|
+
|
|
34
|
+
describe.each(runtimes.runtimeList)('%s', (databaseName, runtime) => {
|
|
35
|
+
const model = wrapTestModel(
|
|
36
|
+
runtime,
|
|
37
|
+
`
|
|
38
|
+
query: isabellaN is ${databaseName}.table('malloytest.state_facts') -> {
|
|
39
|
+
where: popular_name = 'Isabella' and state ~ 'N%'
|
|
40
|
+
group_by: popular_name
|
|
41
|
+
}`
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const stateDesc = [{st: 'NY'}, {st: 'NV'}, {st: 'NM'}, {st: 'NJ'}];
|
|
45
|
+
const birthsDesc = [{st: 'NY'}, {st: 'NJ'}, {st: 'NM'}, {st: 'NV'}];
|
|
46
|
+
const keywordStateDesc = [
|
|
47
|
+
{'select': 'NY'},
|
|
48
|
+
{'select': 'NV'},
|
|
49
|
+
{'select': 'NM'},
|
|
50
|
+
{'select': 'NJ'},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
test.when(runtime.supportsNesting)(
|
|
54
|
+
`one-stage nest is in the order it asked for - ${databaseName}`,
|
|
55
|
+
async () => {
|
|
56
|
+
await expect(`
|
|
57
|
+
run: isabellaN + {
|
|
58
|
+
nest: n is {
|
|
59
|
+
group_by: st is state
|
|
60
|
+
aggregate: b is births.sum()
|
|
61
|
+
order_by: st desc
|
|
62
|
+
}
|
|
63
|
+
}`).toMatchResult(model, {n: stateDesc});
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
test.when(runtime.supportsNesting)(
|
|
68
|
+
`one-stage nest orders by a name which needs quoting - ${databaseName}`,
|
|
69
|
+
async () => {
|
|
70
|
+
await expect(`
|
|
71
|
+
run: isabellaN + {
|
|
72
|
+
nest: n is {
|
|
73
|
+
group_by: \`select\` is state
|
|
74
|
+
aggregate: b is births.sum()
|
|
75
|
+
order_by: \`select\` desc
|
|
76
|
+
}
|
|
77
|
+
}`).toMatchResult(model, {n: keywordStateDesc});
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
test.when(runtime.supportsNesting)(
|
|
82
|
+
`one-stage nest with no order_by is largest first - ${databaseName}`,
|
|
83
|
+
async () => {
|
|
84
|
+
await expect(`
|
|
85
|
+
run: isabellaN + {
|
|
86
|
+
nest: n is {
|
|
87
|
+
group_by: st is state
|
|
88
|
+
aggregate: b is births.sum()
|
|
89
|
+
}
|
|
90
|
+
}`).toMatchResult(model, {n: birthsDesc});
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
test.when(runtime.dialect.supportsPipelinesInViews)(
|
|
95
|
+
`pipelined nest is in the order its last stage asked for - ${databaseName}`,
|
|
96
|
+
async () => {
|
|
97
|
+
await expect(`
|
|
98
|
+
run: isabellaN + {
|
|
99
|
+
nest: n is {
|
|
100
|
+
group_by: st is state
|
|
101
|
+
aggregate: b is births.sum()
|
|
102
|
+
order_by: st asc
|
|
103
|
+
} -> {
|
|
104
|
+
group_by: st, b
|
|
105
|
+
order_by: st desc
|
|
106
|
+
}
|
|
107
|
+
}`).toMatchResult(model, {n: stateDesc});
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
test.when(runtime.dialect.supportsPipelinesInViews)(
|
|
112
|
+
`pipelined nest orders by a name which needs quoting - ${databaseName}`,
|
|
113
|
+
async () => {
|
|
114
|
+
await expect(`
|
|
115
|
+
run: isabellaN + {
|
|
116
|
+
nest: n is {
|
|
117
|
+
group_by: \`select\` is state
|
|
118
|
+
aggregate: b is births.sum()
|
|
119
|
+
order_by: \`select\` asc
|
|
120
|
+
} -> {
|
|
121
|
+
group_by: \`select\`, b
|
|
122
|
+
order_by: \`select\` desc
|
|
123
|
+
}
|
|
124
|
+
}`).toMatchResult(model, {n: keywordStateDesc});
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// An ordinal order_by names an output field by number, counting from one.
|
|
129
|
+
test.when(runtime.supportsNesting)(
|
|
130
|
+
`one-stage nest orders by an output field number - ${databaseName}`,
|
|
131
|
+
async () => {
|
|
132
|
+
await expect(`
|
|
133
|
+
run: isabellaN + {
|
|
134
|
+
nest: n is {
|
|
135
|
+
group_by: st is state
|
|
136
|
+
aggregate: b is births.sum()
|
|
137
|
+
order_by: 1 desc
|
|
138
|
+
}
|
|
139
|
+
}`).toMatchResult(model, {n: stateDesc});
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
test.when(runtime.dialect.supportsPipelinesInViews)(
|
|
144
|
+
`pipelined nest orders by an output field number - ${databaseName}`,
|
|
145
|
+
async () => {
|
|
146
|
+
await expect(`
|
|
147
|
+
run: isabellaN + {
|
|
148
|
+
nest: n is {
|
|
149
|
+
group_by: st is state
|
|
150
|
+
aggregate: b is births.sum()
|
|
151
|
+
order_by: st asc
|
|
152
|
+
} -> {
|
|
153
|
+
group_by: st, b
|
|
154
|
+
order_by: 1 desc
|
|
155
|
+
}
|
|
156
|
+
}`).toMatchResult(model, {n: stateDesc});
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
test.when(runtime.dialect.supportsPipelinesInViews)(
|
|
161
|
+
`pipelined nest ending in select orders by an output field number - ${databaseName}`,
|
|
162
|
+
async () => {
|
|
163
|
+
await expect(`
|
|
164
|
+
run: isabellaN + {
|
|
165
|
+
nest: n is {
|
|
166
|
+
group_by: st is state
|
|
167
|
+
aggregate: b is births.sum()
|
|
168
|
+
order_by: st asc
|
|
169
|
+
} -> {
|
|
170
|
+
select: st, b
|
|
171
|
+
order_by: 2 desc
|
|
172
|
+
}
|
|
173
|
+
}`).toMatchResult(model, {n: birthsDesc});
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
test.when(runtime.dialect.supportsPipelinesInViews)(
|
|
178
|
+
`pipelined nest with no order_by is largest first - ${databaseName}`,
|
|
179
|
+
async () => {
|
|
180
|
+
await expect(`
|
|
181
|
+
run: isabellaN + {
|
|
182
|
+
nest: n is {
|
|
183
|
+
group_by: st is state
|
|
184
|
+
aggregate: b is births.sum()
|
|
185
|
+
} -> {
|
|
186
|
+
group_by: st
|
|
187
|
+
aggregate: tot is b.sum()
|
|
188
|
+
}
|
|
189
|
+
}`).toMatchResult(model, {n: birthsDesc});
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
test.when(runtime.supportsNesting)(
|
|
194
|
+
`an aggregate which is null in a nest reads back as null - ${databaseName}`,
|
|
195
|
+
async () => {
|
|
196
|
+
await expect(`
|
|
197
|
+
run: ${databaseName}.table('malloytest.state_facts') -> {
|
|
198
|
+
group_by: state
|
|
199
|
+
nest: ugly is {
|
|
200
|
+
group_by: popular_name
|
|
201
|
+
aggregate: foo is NULLIF(sum(airport_count)*0,0)+1
|
|
202
|
+
}
|
|
203
|
+
}`).toMatchPaths(model, {'ugly.foo': null});
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// A null value in the grouping above a nest must not null out the nest.
|
|
208
|
+
test.when(runtime.supportsNesting)(
|
|
209
|
+
`a nest under a null group is still a nest - ${databaseName}`,
|
|
210
|
+
async () => {
|
|
211
|
+
const result = await runtime
|
|
212
|
+
.loadQuery(
|
|
213
|
+
`
|
|
214
|
+
run: ${databaseName}.table('malloytest.airports') -> {
|
|
215
|
+
where: faa_region is null
|
|
216
|
+
group_by: faa_region
|
|
217
|
+
aggregate: airport_count is count()
|
|
218
|
+
nest: by_state is {
|
|
219
|
+
where: state is not null
|
|
220
|
+
group_by: state
|
|
221
|
+
aggregate: airport_count is count()
|
|
222
|
+
}
|
|
223
|
+
nest: capped is {
|
|
224
|
+
where: state is not null
|
|
225
|
+
group_by: state
|
|
226
|
+
aggregate: airport_count is count()
|
|
227
|
+
limit: 1
|
|
228
|
+
}
|
|
229
|
+
}`
|
|
230
|
+
)
|
|
231
|
+
.run();
|
|
232
|
+
const row = result.data.toObject()[0] as Record<string, unknown>;
|
|
233
|
+
expect(row['by_state']).not.toBe(null);
|
|
234
|
+
expect(row['capped']).not.toBe(null);
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
});
|