@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
|
@@ -652,26 +652,6 @@ runtimes.runtimeMap.forEach((runtime, databaseName) => {
|
|
|
652
652
|
});
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
-
test.when(runtime.supportsNesting)(
|
|
656
|
-
`number as null 2 - ${databaseName}`,
|
|
657
|
-
async () => {
|
|
658
|
-
// a cross join produces a Many to Many result.
|
|
659
|
-
// symmetric aggregate are needed on both sides of the join
|
|
660
|
-
// Check the row count and that sums on each side work properly.
|
|
661
|
-
const testModel = wrapTestModel(runtime, '');
|
|
662
|
-
await expect(`
|
|
663
|
-
# test.verbose
|
|
664
|
-
run: ${databaseName}.table('malloytest.state_facts') -> {
|
|
665
|
-
group_by: state
|
|
666
|
-
nest: ugly is {
|
|
667
|
-
group_by: popular_name
|
|
668
|
-
aggregate: foo is NULLIF(sum(airport_count)*0,0)+1
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
`).toMatchPaths(testModel, {'ugly.foo': null});
|
|
672
|
-
}
|
|
673
|
-
);
|
|
674
|
-
|
|
675
655
|
it(`sql block- ${databaseName}`, async () => {
|
|
676
656
|
const testModel = wrapTestModel(runtime, '');
|
|
677
657
|
await expect(`
|
|
@@ -1279,39 +1259,6 @@ SELECT row_to_json(finalStage) as row FROM __stage0 AS finalStage`);
|
|
|
1279
1259
|
`).toMatchResult(testModel, {p: 23001594});
|
|
1280
1260
|
});
|
|
1281
1261
|
|
|
1282
|
-
test.when(runtime.supportsNesting)(
|
|
1283
|
-
`nest null - ${databaseName}`,
|
|
1284
|
-
async () => {
|
|
1285
|
-
const result = await runtime
|
|
1286
|
-
.loadQuery(
|
|
1287
|
-
`
|
|
1288
|
-
run: ${databaseName}.table('malloytest.airports') -> {
|
|
1289
|
-
where: faa_region is null
|
|
1290
|
-
group_by: faa_region
|
|
1291
|
-
aggregate: airport_count is count()
|
|
1292
|
-
nest: by_state is {
|
|
1293
|
-
where: state is not null
|
|
1294
|
-
group_by: state
|
|
1295
|
-
aggregate: airport_count is count()
|
|
1296
|
-
}
|
|
1297
|
-
nest: by_state1 is {
|
|
1298
|
-
where: state is not null
|
|
1299
|
-
group_by: state
|
|
1300
|
-
aggregate: airport_count is count()
|
|
1301
|
-
limit: 1
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
`
|
|
1305
|
-
)
|
|
1306
|
-
.run();
|
|
1307
|
-
|
|
1308
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1309
|
-
const d: any = result.data.toObject();
|
|
1310
|
-
expect(d[0]['by_state']).not.toBe(null);
|
|
1311
|
-
expect(d[0]['by_state1']).not.toBe(null);
|
|
1312
|
-
}
|
|
1313
|
-
);
|
|
1314
|
-
|
|
1315
1262
|
test.when(
|
|
1316
1263
|
runtime.supportsNesting && runtime.dialect.supportsPipelinesInViews
|
|
1317
1264
|
)(`Nested pipelines sort properly - ${databaseName}`, async () => {
|
|
@@ -1391,28 +1338,6 @@ SELECT row_to_json(finalStage) as row FROM __stage0 AS finalStage`);
|
|
|
1391
1338
|
}
|
|
1392
1339
|
});
|
|
1393
1340
|
|
|
1394
|
-
test.when(runtime.supportsNesting)(
|
|
1395
|
-
`number as null- ${databaseName}`,
|
|
1396
|
-
async () => {
|
|
1397
|
-
const result = await runtime
|
|
1398
|
-
.loadQuery(
|
|
1399
|
-
`
|
|
1400
|
-
source: s is ${databaseName}.table('malloytest.state_facts') extend {
|
|
1401
|
-
}
|
|
1402
|
-
run: s-> {
|
|
1403
|
-
group_by: state
|
|
1404
|
-
nest: ugly is {
|
|
1405
|
-
group_by: popular_name
|
|
1406
|
-
aggregate: foo is NULLIF(sum(airport_count)*0,0)+1
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
`
|
|
1410
|
-
)
|
|
1411
|
-
.run();
|
|
1412
|
-
expect(result.data.path(0, 'ugly', 0, 'foo').value).toBe(null);
|
|
1413
|
-
}
|
|
1414
|
-
);
|
|
1415
|
-
|
|
1416
1341
|
it(`removes surpuflous order_by - solo aggregates - ${databaseName}`, async () => {
|
|
1417
1342
|
const result = await runtime
|
|
1418
1343
|
.loadQuery(
|
|
@@ -36,11 +36,43 @@ runtimes.runtimeMap.forEach((runtime, databaseName) => {
|
|
|
36
36
|
aggregate: \`#\` is count(one)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
// A nest of nothing but aggregates is a single record rather than a
|
|
40
|
+
// list, and is built by a different dialect method.
|
|
41
|
+
nest: measures_only is {
|
|
42
|
+
aggregate: \`#\` is count(one)
|
|
43
|
+
aggregate: \`order\` is one.sum()
|
|
44
|
+
}
|
|
39
45
|
}`).toMatchResult(testModel, {
|
|
40
46
|
foo: [{'one': 1, '#': 1, 'deepfoo': [{'one': 1, '#': 1}]}],
|
|
47
|
+
measures_only: {'#': 1, 'order': 1},
|
|
41
48
|
});
|
|
42
49
|
});
|
|
43
50
|
|
|
51
|
+
// A nest's output field names are the user's, so every place a stage
|
|
52
|
+
// declares or reads them has to quote them -- including the row type a
|
|
53
|
+
// multi-stage nest builds to carry its first stage into its second.
|
|
54
|
+
test.when(
|
|
55
|
+
runtime.supportsNesting && runtime.dialect.supportsPipelinesInViews
|
|
56
|
+
)(
|
|
57
|
+
`properly quotes nested field names through a second stage in ${databaseName}`,
|
|
58
|
+
async () => {
|
|
59
|
+
await expect(`
|
|
60
|
+
run: ${databaseName}.table('malloytest.state_facts') -> {
|
|
61
|
+
where: state = 'CA'
|
|
62
|
+
group_by: popular_name
|
|
63
|
+
nest: counted is {
|
|
64
|
+
group_by: \`order\` is state
|
|
65
|
+
aggregate: n is count()
|
|
66
|
+
} -> {
|
|
67
|
+
group_by: \`order\`
|
|
68
|
+
aggregate: t is n.sum()
|
|
69
|
+
}
|
|
70
|
+
}`).toMatchResult(testModel, {
|
|
71
|
+
counted: [{'order': 'CA', 't': 1}],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
44
76
|
describe('warnings', () => {
|
|
45
77
|
// NOTE: This test generates SQL errors on the console because of
|
|
46
78
|
// a hard-coded console.log() in the duckdb-wasm worker
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Contributors to the Malloy project
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {RuntimeList} from '../../runtimes';
|
|
7
|
+
import {describeIfDatabaseAvailable} from '../../util';
|
|
8
|
+
import '@malloydata/malloy/test/matchers';
|
|
9
|
+
import {wrapTestModel} from '@malloydata/malloy/test';
|
|
10
|
+
|
|
11
|
+
const [describe] = describeIfDatabaseAvailable(['sqlserver']);
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* The dialect is built up here in the order a query grows: a literal, a
|
|
15
|
+
* table, a filter, an ordering, a grouping, a join. Each block is what the
|
|
16
|
+
* next one stands on, and what the cross-dialect suite in ../all assumes.
|
|
17
|
+
*/
|
|
18
|
+
describe('SQL Server', () => {
|
|
19
|
+
const runtimes = new RuntimeList(['sqlserver']);
|
|
20
|
+
const runtime = runtimes.runtimeMap.get('sqlserver');
|
|
21
|
+
if (runtime === undefined) {
|
|
22
|
+
throw new Error("Couldn't build runtime");
|
|
23
|
+
}
|
|
24
|
+
// A test runtime enables experimental dialects; no compiler flag is needed
|
|
25
|
+
const tm = wrapTestModel(runtime, '');
|
|
26
|
+
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
await runtimes.closeAll();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('a SQL block', () => {
|
|
32
|
+
test('selects a literal', async () => {
|
|
33
|
+
await expect(
|
|
34
|
+
'run: sqlserver.sql("SELECT 1 AS n") -> { select: n }'
|
|
35
|
+
).toMatchResult(tm, {n: 1});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('carries string, date and timestamp values', async () => {
|
|
39
|
+
await expect(`
|
|
40
|
+
run: sqlserver.sql("""
|
|
41
|
+
SELECT N'héllo' AS s,
|
|
42
|
+
CAST('2021-02-24' AS DATE) AS d,
|
|
43
|
+
CAST('2021-02-24 03:05:06' AS DATETIME2) AS t
|
|
44
|
+
""") -> { select: s, d, t }
|
|
45
|
+
`).toMatchResult(tm, {
|
|
46
|
+
s: 'héllo',
|
|
47
|
+
d: new Date('2021-02-24T00:00:00Z'),
|
|
48
|
+
t: new Date('2021-02-24T03:05:06Z'),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('carries a newline in a string literal', async () => {
|
|
53
|
+
await expect(`
|
|
54
|
+
run: sqlserver.sql("SELECT 1 AS n") -> { select: s is 'a\\nb' }
|
|
55
|
+
`).toMatchResult(tm, {s: 'a\nb'});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('quotes a field named like a keyword', async () => {
|
|
59
|
+
await expect(`
|
|
60
|
+
run: sqlserver.sql('SELECT 1 AS "select"') -> {
|
|
61
|
+
select: \`select\`, \`create\` is \`select\` + 1
|
|
62
|
+
}
|
|
63
|
+
`).toMatchResult(tm, {select: 1, create: 2});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('a table', () => {
|
|
68
|
+
test('a boolean literal is a condition', async () => {
|
|
69
|
+
await expect(`
|
|
70
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
71
|
+
where: true
|
|
72
|
+
aggregate: n is count(), none is count() { where: false }
|
|
73
|
+
}
|
|
74
|
+
`).toEqualResult(tm, [{n: 51, none: 0}]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('a bit column is an integer', async () => {
|
|
78
|
+
await expect(`
|
|
79
|
+
run: sqlserver.table('malloytest.alltypes') -> {
|
|
80
|
+
select: t_bool_true, t_bool_false, t_bool_null
|
|
81
|
+
}
|
|
82
|
+
`).toEqualResult(tm, [
|
|
83
|
+
{t_bool_true: 1, t_bool_false: 0, t_bool_null: null},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a limit of zero is no limit', async () => {
|
|
88
|
+
// FETCH NEXT 0 ROWS is a parse error; the compiler writes no limit at all
|
|
89
|
+
await expect(`
|
|
90
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
91
|
+
select: state
|
|
92
|
+
limit: 0
|
|
93
|
+
} -> { aggregate: n is count() }
|
|
94
|
+
`).toEqualResult(tm, [{n: 51}]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('a declared boolean is a 0/1 value', async () => {
|
|
98
|
+
await expect(`
|
|
99
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
100
|
+
group_by:
|
|
101
|
+
big is airport_count > 500
|
|
102
|
+
c_state is starts_with(state, 'C')
|
|
103
|
+
aggregate: n is count()
|
|
104
|
+
order_by: big desc, c_state desc
|
|
105
|
+
}
|
|
106
|
+
`).toEqualResult(tm, [
|
|
107
|
+
{big: true, c_state: true, n: 1},
|
|
108
|
+
{big: true, c_state: false, n: 11},
|
|
109
|
+
{big: false, c_state: true, n: 2},
|
|
110
|
+
{big: false, c_state: false, n: 37},
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a cast to boolean is refused', async () => {
|
|
115
|
+
await expect(
|
|
116
|
+
runtime
|
|
117
|
+
.loadQuery(
|
|
118
|
+
`run: sqlserver.table('malloytest.state_facts') -> {
|
|
119
|
+
select: b is airport_count::boolean
|
|
120
|
+
}`
|
|
121
|
+
)
|
|
122
|
+
.run()
|
|
123
|
+
).rejects.toThrow(/boolean/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('starts_with and ends_with are conditions', async () => {
|
|
127
|
+
await expect(`
|
|
128
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
129
|
+
aggregate:
|
|
130
|
+
c_states is count() { where: starts_with(state, 'C') }
|
|
131
|
+
a_states is count() { where: ends_with(state, 'A') }
|
|
132
|
+
nan_counts is count() { where: is_nan(airport_count) }
|
|
133
|
+
}
|
|
134
|
+
`).toEqualResult(tm, [{c_states: 3, a_states: 8, nan_counts: 0}]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('reads a column', async () => {
|
|
138
|
+
await expect(`
|
|
139
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
140
|
+
select: state
|
|
141
|
+
where: state = 'CA'
|
|
142
|
+
}
|
|
143
|
+
`).toEqualResult(tm, [{state: 'CA'}]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('reads a bracket-quoted path', async () => {
|
|
147
|
+
await expect(`
|
|
148
|
+
run: sqlserver.table('[malloytest].[state_facts]') -> {
|
|
149
|
+
select: state
|
|
150
|
+
where: state = 'CA'
|
|
151
|
+
}
|
|
152
|
+
`).toEqualResult(tm, [{state: 'CA'}]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('reads a database-qualified path', async () => {
|
|
156
|
+
await expect(`
|
|
157
|
+
run: sqlserver.table('malloytest.malloytest.state_facts') -> {
|
|
158
|
+
aggregate: n is count()
|
|
159
|
+
}
|
|
160
|
+
`).toMatchResult(tm, {n: 51});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('orders and limits', async () => {
|
|
164
|
+
await expect(`
|
|
165
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
166
|
+
select: state
|
|
167
|
+
order_by: state desc
|
|
168
|
+
limit: 2
|
|
169
|
+
}
|
|
170
|
+
`).toEqualResult(tm, [{state: 'WY'}, {state: 'WV'}]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('limits a stage that feeds another', async () => {
|
|
174
|
+
await expect(`
|
|
175
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
176
|
+
select: state
|
|
177
|
+
order_by: state desc
|
|
178
|
+
limit: 3
|
|
179
|
+
} -> {
|
|
180
|
+
select: state
|
|
181
|
+
order_by: state asc
|
|
182
|
+
}
|
|
183
|
+
`).toEqualResult(tm, [{state: 'WI'}, {state: 'WV'}, {state: 'WY'}]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('filters on a comparison', async () => {
|
|
187
|
+
await expect(`
|
|
188
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
189
|
+
aggregate: n is count()
|
|
190
|
+
where: airport_count > 500
|
|
191
|
+
}
|
|
192
|
+
`).toMatchResult(tm, {n: 12});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('escapes a bracket in a LIKE pattern', async () => {
|
|
196
|
+
await expect(`
|
|
197
|
+
run: sqlserver.sql("""
|
|
198
|
+
SELECT s FROM (VALUES (N'a[b'), (N'ab'), (N'axb')) v(s)
|
|
199
|
+
""") -> { select: s; where: s ~ 'a[%' }
|
|
200
|
+
`).toEqualResult(tm, [{s: 'a[b'}]);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('aggregation', () => {
|
|
205
|
+
test('a group of only constants is one row', async () => {
|
|
206
|
+
await expect(`
|
|
207
|
+
run: sqlserver.table('malloytest.state_facts') -> { group_by: x is 1, y is 'a' }
|
|
208
|
+
`).toEqualResult(tm, [{x: 1, y: 'a'}]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('a constant dimension groups with the others', async () => {
|
|
212
|
+
await expect(`
|
|
213
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
214
|
+
group_by: tag is 'facts', big is airport_count > 500
|
|
215
|
+
aggregate: n is count()
|
|
216
|
+
order_by: big desc
|
|
217
|
+
}
|
|
218
|
+
`).toEqualResult(tm, [
|
|
219
|
+
{tag: 'facts', big: true, n: 12},
|
|
220
|
+
{tag: 'facts', big: false, n: 39},
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('counts without a group', async () => {
|
|
225
|
+
await expect(`
|
|
226
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
227
|
+
aggregate: state_count is count()
|
|
228
|
+
}
|
|
229
|
+
`).toMatchResult(tm, {state_count: 51});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('groups by a column', async () => {
|
|
233
|
+
await expect(`
|
|
234
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
235
|
+
group_by: popular_name
|
|
236
|
+
aggregate: state_count is count()
|
|
237
|
+
order_by: state_count desc
|
|
238
|
+
}
|
|
239
|
+
`).toMatchResult(tm, {popular_name: 'Isabella'});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('groups by an expression', async () => {
|
|
243
|
+
await expect(`
|
|
244
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
245
|
+
group_by: first is substr(state, 1, 1)
|
|
246
|
+
aggregate: state_count is count()
|
|
247
|
+
order_by: state_count desc, first
|
|
248
|
+
}
|
|
249
|
+
`).toMatchResult(tm, {first: 'M', state_count: 8});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('orders and limits a grouped result', async () => {
|
|
253
|
+
await expect(`
|
|
254
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
255
|
+
group_by: popular_name
|
|
256
|
+
aggregate: state_count is count()
|
|
257
|
+
order_by: state_count desc, popular_name
|
|
258
|
+
limit: 2
|
|
259
|
+
}
|
|
260
|
+
`).toEqualResult(tm, [
|
|
261
|
+
{popular_name: 'Isabella', state_count: 24},
|
|
262
|
+
{popular_name: 'Sophia', state_count: 11},
|
|
263
|
+
]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('orders a query used as a source', async () => {
|
|
267
|
+
await expect(`
|
|
268
|
+
source: by_name is sqlserver.table('malloytest.state_facts') -> {
|
|
269
|
+
group_by: popular_name
|
|
270
|
+
aggregate: state_count is count()
|
|
271
|
+
order_by: state_count desc
|
|
272
|
+
}
|
|
273
|
+
run: by_name -> {
|
|
274
|
+
select: popular_name
|
|
275
|
+
where: state_count > 10
|
|
276
|
+
order_by: popular_name
|
|
277
|
+
}
|
|
278
|
+
`).toEqualResult(tm, [
|
|
279
|
+
{popular_name: 'Isabella'},
|
|
280
|
+
{popular_name: 'Sophia'},
|
|
281
|
+
]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('orders the last stage of a pipeline', async () => {
|
|
285
|
+
await expect(`
|
|
286
|
+
run: sqlserver.table('malloytest.state_facts') -> {
|
|
287
|
+
group_by: popular_name
|
|
288
|
+
aggregate: state_count is count()
|
|
289
|
+
} -> {
|
|
290
|
+
select: popular_name, state_count
|
|
291
|
+
order_by: state_count asc, popular_name desc
|
|
292
|
+
limit: 2
|
|
293
|
+
}
|
|
294
|
+
`).toEqualResult(tm, [
|
|
295
|
+
{popular_name: 'Madison', state_count: 3},
|
|
296
|
+
{popular_name: 'Ava', state_count: 3},
|
|
297
|
+
]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('divides as a float', async () => {
|
|
301
|
+
await expect(`
|
|
302
|
+
run: sqlserver.sql("SELECT 7 AS a, 2 AS b") -> {
|
|
303
|
+
select: q is a / b
|
|
304
|
+
}
|
|
305
|
+
`).toMatchResult(tm, {q: 3.5});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('functions', () => {
|
|
310
|
+
test('pow computes in floating point', async () => {
|
|
311
|
+
await expect(`
|
|
312
|
+
run: sqlserver.sql("SELECT 1 AS n") -> {
|
|
313
|
+
select: root is round(pow(2, 0.5), 6), hundredth is pow(10, -2)
|
|
314
|
+
}
|
|
315
|
+
`).toEqualResult(tm, [{root: 1.414214, hundredth: 0.01}]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('length counts trailing spaces', async () => {
|
|
319
|
+
await expect(`
|
|
320
|
+
run: sqlserver.sql("SELECT 1 AS n") -> {
|
|
321
|
+
select:
|
|
322
|
+
padded is length('abc ')
|
|
323
|
+
back is substr('abc ', -2)
|
|
324
|
+
}
|
|
325
|
+
`).toEqualResult(tm, [{padded: 5, back: 'c '}]);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('time zones', () => {
|
|
330
|
+
test('converts a query time zone through its Windows name', async () => {
|
|
331
|
+
await expect(`
|
|
332
|
+
run: sqlserver.sql("SELECT CAST('2020-02-20 00:00:00' AS DATETIME2) AS t") -> {
|
|
333
|
+
timezone: 'America/Mexico_City'
|
|
334
|
+
select: mex_day is t.day, mex_hour is hour(t)
|
|
335
|
+
}
|
|
336
|
+
`).toMatchResult(tm, {
|
|
337
|
+
mex_day: new Date('2020-02-19T06:00:00Z'),
|
|
338
|
+
mex_hour: 18,
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('a zone is known by its current IANA name as well as its old one', async () => {
|
|
343
|
+
await expect(`
|
|
344
|
+
run: sqlserver.sql("SELECT CAST('2021-02-24 03:05:06' AS DATETIME2) AS t") -> {
|
|
345
|
+
timezone: 'Asia/Kolkata'
|
|
346
|
+
select: h is hour(t)
|
|
347
|
+
}
|
|
348
|
+
`).toEqualResult(tm, [{h: 8}]);
|
|
349
|
+
await expect(`
|
|
350
|
+
run: sqlserver.sql("SELECT CAST('2021-02-24 03:05:06' AS DATETIME2) AS t") -> {
|
|
351
|
+
timezone: 'Europe/Kyiv'
|
|
352
|
+
select: h is hour(t)
|
|
353
|
+
}
|
|
354
|
+
`).toEqualResult(tm, [{h: 5}]);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('a literal written in a zone is that instant', async () => {
|
|
358
|
+
await expect(`
|
|
359
|
+
run: sqlserver.sql("SELECT 1 AS n") -> {
|
|
360
|
+
select: t is @2020-02-20 00:00:00[America/Mexico_City]
|
|
361
|
+
}
|
|
362
|
+
`).toMatchResult(tm, {t: new Date('2020-02-20T06:00:00Z')});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('joins', () => {
|
|
367
|
+
test('sums each side of a fan-out once', async () => {
|
|
368
|
+
await expect(`
|
|
369
|
+
source: facts is sqlserver.table('malloytest.state_facts')
|
|
370
|
+
run: facts extend { join_cross: other is facts on other.state = 'CA' | 'NY' } -> {
|
|
371
|
+
aggregate:
|
|
372
|
+
row_count is count(concat(state, other.state))
|
|
373
|
+
left_sum is airport_count.sum()
|
|
374
|
+
right_sum is other.airport_count.sum()
|
|
375
|
+
}
|
|
376
|
+
`).toMatchResult(tm, {row_count: 102, left_sum: 19701, right_sum: 1560});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('joins and aggregates symmetrically', async () => {
|
|
380
|
+
await expect(`
|
|
381
|
+
source: carriers is sqlserver.table('malloytest.carriers') extend {
|
|
382
|
+
primary_key: code
|
|
383
|
+
}
|
|
384
|
+
source: flights is sqlserver.table('malloytest.flights') extend {
|
|
385
|
+
join_one: carriers with carrier
|
|
386
|
+
}
|
|
387
|
+
run: flights -> {
|
|
388
|
+
group_by: carriers.nickname
|
|
389
|
+
aggregate: flight_count is count(), carrier_count is carriers.count()
|
|
390
|
+
order_by: flight_count desc
|
|
391
|
+
}
|
|
392
|
+
`).toMatchResult(tm, {nickname: 'Southwest', carrier_count: 1});
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
package/src/runtimes.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
InMemoryURLReader,
|
|
17
17
|
InMemoryModelCache,
|
|
18
18
|
CacheManager,
|
|
19
|
+
getDialect,
|
|
19
20
|
} from '@malloydata/malloy';
|
|
20
21
|
import {createTestRuntime} from '@malloydata/malloy/test';
|
|
21
22
|
import {BigQueryConnection} from '@malloydata/db-bigquery';
|
|
@@ -31,6 +32,10 @@ import {
|
|
|
31
32
|
MySQLExecutor,
|
|
32
33
|
} from '@malloydata/db-mysql/src/mysql_connection';
|
|
33
34
|
import {DatabricksConnection} from '@malloydata/db-databricks/src/databricks_connection';
|
|
35
|
+
import {
|
|
36
|
+
SQLServerConnection,
|
|
37
|
+
SQLServerExecutor,
|
|
38
|
+
} from '@malloydata/db-sqlserver/src/sqlserver_connection';
|
|
34
39
|
import {EventEmitter} from 'events';
|
|
35
40
|
|
|
36
41
|
export class SnowflakeTestConnection extends SnowflakeConnection {
|
|
@@ -144,6 +149,21 @@ export class DuckDBWASMTestConnection extends DuckDBWASMConnection {
|
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
151
|
|
|
152
|
+
export class SQLServerTestConnection extends SQLServerConnection {
|
|
153
|
+
public async runSQL(
|
|
154
|
+
sqlCommand: string,
|
|
155
|
+
options?: RunSQLOptions
|
|
156
|
+
): Promise<MalloyQueryData> {
|
|
157
|
+
try {
|
|
158
|
+
return await super.runSQL(sqlCommand, options);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.log(`Error in SQL:\n ${sqlCommand}`);
|
|
162
|
+
throw e;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
export class TestCacheManager extends CacheManager {
|
|
148
168
|
constructor(readonly _modelCache: ModelCache) {
|
|
149
169
|
super(_modelCache);
|
|
@@ -203,14 +223,26 @@ export function runtimeFor(dbName: string): SingleConnectionRuntime {
|
|
|
203
223
|
);
|
|
204
224
|
break;
|
|
205
225
|
case 'mssql_via_duckdb':
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
{
|
|
227
|
+
const s = SQLServerExecutor.getConnectionOptionsFromEnv();
|
|
228
|
+
if (s.server === undefined) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
'MSSQL_HOST is not set; see test/mssql/mssql_start.sh'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
connection = new DuckDBTestConnection({
|
|
234
|
+
name: dbName,
|
|
235
|
+
additionalExtensions: ['mssql'],
|
|
236
|
+
setupSQL: [
|
|
237
|
+
// The same string test/mssql/connection_string.ts builds for the
|
|
238
|
+
// loader, written here because packages/malloy-render type-checks
|
|
239
|
+
// this file alone and admits no import beside it. `Server=host,port`:
|
|
240
|
+
// the extension reads no separate Port key.
|
|
241
|
+
`ATTACH 'Server=${s.server},${s.port ?? 1433};Database=${s.database};User Id=${s.user};Password=${s.password};TrustServerCertificate=${s.trustServerCertificate ?? false}' AS msdb (TYPE mssql)`,
|
|
242
|
+
`USE msdb.${s.database}`,
|
|
243
|
+
].join(';\n'),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
214
246
|
break;
|
|
215
247
|
case 'motherduck':
|
|
216
248
|
connection = new DuckDBTestConnection({
|
|
@@ -246,6 +278,12 @@ export function runtimeFor(dbName: string): SingleConnectionRuntime {
|
|
|
246
278
|
TrinoExecutor.getConnectionOptionsFromEnv(dbName) // they share configs.
|
|
247
279
|
);
|
|
248
280
|
break;
|
|
281
|
+
case 'sqlserver':
|
|
282
|
+
connection = new SQLServerTestConnection(
|
|
283
|
+
dbName,
|
|
284
|
+
SQLServerExecutor.getConnectionOptionsFromEnv()
|
|
285
|
+
);
|
|
286
|
+
break;
|
|
249
287
|
case 'databricks':
|
|
250
288
|
{
|
|
251
289
|
const warehouseId = process.env['DATABRICKS_WAREHOUSE_ID'];
|
|
@@ -264,6 +302,9 @@ export function runtimeFor(dbName: string): SingleConnectionRuntime {
|
|
|
264
302
|
default:
|
|
265
303
|
throw new Error(`Unknown runtime "${dbName}`);
|
|
266
304
|
}
|
|
305
|
+
// The shared suites carry no `##! experimental.dialect.NAME` lines. A
|
|
306
|
+
// dialect under test is accepted once, for the whole process, instead.
|
|
307
|
+
getDialect(connection.dialectName).acceptExperimental();
|
|
267
308
|
return testRuntimeFor(connection);
|
|
268
309
|
} catch (error) {
|
|
269
310
|
throw new Error(
|
|
@@ -313,7 +354,6 @@ export class RuntimeList {
|
|
|
313
354
|
database instanceof SingleConnectionRuntime
|
|
314
355
|
? database
|
|
315
356
|
: runtimeFor(database);
|
|
316
|
-
rt.isTestRuntime = true;
|
|
317
357
|
this.runtimeMap.set(rt.connection.name, rt);
|
|
318
358
|
this.runtimeList.push([rt.connection.name, rt]);
|
|
319
359
|
}
|
package/src/test-select.ts
CHANGED
|
@@ -521,21 +521,17 @@ export class TestSelect {
|
|
|
521
521
|
const innerQuery = selects.join('\nUNION ALL ');
|
|
522
522
|
|
|
523
523
|
// Generate ORDER BY based on dialect preference
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
orderByClause = `ORDER BY ${rowIdColumn}`;
|
|
534
|
-
}
|
|
535
|
-
// Presto/Trino ignores ORDER BY on a subquery without LIMIT
|
|
536
|
-
orderByClause += ` LIMIT ${rows.length}`;
|
|
524
|
+
const orderTerm =
|
|
525
|
+
this.dialect.orderByClause === 'ordinal'
|
|
526
|
+
? `${columnList.length + 1}`
|
|
527
|
+
: rowIdColumn;
|
|
528
|
+
// Presto/Trino ignores ORDER BY on a subquery without a row limit
|
|
529
|
+
const orderByClause = this.dialect.sqlOrderByLimit(
|
|
530
|
+
[orderTerm],
|
|
531
|
+
rows.length
|
|
532
|
+
);
|
|
537
533
|
|
|
538
|
-
const sql = `SELECT ${quotedColumns}\nFROM (\n SELECT *\n FROM (\n${innerQuery}\n ) AS t_sorted\n ${orderByClause}
|
|
534
|
+
const sql = `SELECT ${quotedColumns}\nFROM (\n SELECT *\n FROM (\n${innerQuery}\n ) AS t_sorted\n ${orderByClause}) AS t_result\n`;
|
|
539
535
|
|
|
540
536
|
return sql;
|
|
541
537
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# The hive catalog serves test/data/malloytest-parquet, mounted read-only
|
|
2
|
+
# under /data/malloytest, with the tables hive_ddl.ts declares. The file
|
|
3
|
+
# metastore lives inside the container's own data directory, which the trino
|
|
4
|
+
# user can write; nothing but the parquet crosses the container boundary.
|
|
5
|
+
connector.name=hive
|
|
6
|
+
hive.metastore=file
|
|
7
|
+
hive.metastore.catalog.dir=local:///trino/metastore
|
|
8
|
+
fs.native-local.enabled=true
|
|
9
|
+
local.location=/data
|
|
10
|
+
# The parquet timestamps are microseconds; the DDL declares TIMESTAMP(6)
|
|
11
|
+
hive.timestamp-precision=MICROSECONDS
|