@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.
Files changed (41) hide show
  1. package/CONTEXT.md +18 -6
  2. package/data/parquet_loader.ts +106 -0
  3. package/duckdb/load_test_data.ts +15 -48
  4. package/mssql/connection_string.ts +28 -0
  5. package/mssql/load_test_data.sh +12 -0
  6. package/mssql/load_test_data.ts +63 -0
  7. package/mssql/mssql_start.sh +30 -143
  8. package/mssql/mssql_stop.sh +2 -2
  9. package/mysql/load_test_data.sh +12 -0
  10. package/mysql/load_test_data.ts +39 -0
  11. package/mysql/mysql_start.sh +36 -30
  12. package/package.json +12 -11
  13. package/postgres/load_test_data.sh +12 -0
  14. package/postgres/load_test_data.ts +34 -0
  15. package/postgres/postgres_start.sh +2 -6
  16. package/presto/Dockerfile.slim +12 -6
  17. package/presto/hive.properties +8 -0
  18. package/presto/presto_start.sh +58 -36
  19. package/src/core/experimental-dialects.spec.ts +125 -41
  20. package/src/core/restricted-mode.spec.ts +27 -0
  21. package/src/core/tags.spec.ts +22 -0
  22. package/src/core/virtual-sql.spec.ts +253 -0
  23. package/src/databases/all/db_index.spec.ts +4 -2
  24. package/src/databases/all/expr.spec.ts +150 -18
  25. package/src/databases/all/nest.spec.ts +237 -0
  26. package/src/databases/all/nomodel.spec.ts +0 -75
  27. package/src/databases/all/problems.spec.ts +32 -0
  28. package/src/databases/sqlserver/sqlserver.spec.ts +395 -0
  29. package/src/runtimes.ts +49 -9
  30. package/src/test-select.ts +10 -14
  31. package/trino/hive.properties +11 -0
  32. package/trino/hive_ddl.ts +148 -0
  33. package/trino/trino_start.sh +54 -38
  34. package/tsconfig.json +15 -1
  35. package/bin/dump_malloytest.sh +0 -1
  36. package/bin/postgres_init.sh +0 -10
  37. package/bin/postgres_start.sh +0 -2
  38. package/bin/postgres_stop.sh +0 -2
  39. package/data/mysql/malloytest.mysql.gz +0 -0
  40. package/data/postgres/malloytest-postgres.sql.gz +0 -0
  41. 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
- connection = new DuckDBTestConnection({
207
- name: dbName,
208
- additionalExtensions: ['mssql'],
209
- setupSQL: [
210
- "ATTACH 'Server=localhost;Port=1433;Database=malloytest;User Id=sa;Password=Malloy_Test_123;TrustServerCertificate=true' AS msdb (TYPE mssql)",
211
- 'USE msdb.malloytest',
212
- ].join(';\n'),
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
  }
@@ -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
- let orderByClause: string;
525
- if (this.dialect.orderByClause === 'ordinal') {
526
- // ORDER BY position (column count + 1 since row_id is last)
527
- orderByClause = `ORDER BY ${columnList.length + 1}`;
528
- } else if (this.dialect.orderByClause === 'output_name') {
529
- // ORDER BY column name
530
- orderByClause = `ORDER BY ${rowIdColumn}`;
531
- } else {
532
- // ORDER BY expression - just use column name (qualified would be t_sorted.__ts_row_id__)
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}\n) AS t_result\n`;
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