@malloydata/malloy-tests 0.0.434 → 0.0.436

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.
@@ -0,0 +1,509 @@
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 in a result column is refused, naming the value to write', async () => {
98
+ await expect(
99
+ runtime
100
+ .loadQuery(
101
+ `run: sqlserver.table('malloytest.state_facts') extend {
102
+ dimension: big is airport_count > 500
103
+ } -> { group_by: big; aggregate: n is count() }`
104
+ )
105
+ .run()
106
+ ).rejects.toThrow(
107
+ "'sqlserver' does not support a boolean as a result column; write the value, pick 'yes' when big else 'no'"
108
+ );
109
+ });
110
+
111
+ test('a declared boolean is a condition in where: and having:', async () => {
112
+ await expect(`
113
+ run: sqlserver.table('malloytest.state_facts') extend {
114
+ dimension: big is airport_count > 500
115
+ measure: n is count()
116
+ } -> {
117
+ where: big
118
+ aggregate: n
119
+ having: n > 1
120
+ }
121
+ `).toEqualResult(tm, [{n: 12}]);
122
+ });
123
+
124
+ test('not, and a comparison true on NULL, follow Malloy rather than SQL', async () => {
125
+ await expect(`
126
+ run: sqlserver.sql('SELECT CAST(NULL AS INT) AS n, 1 AS one') -> {
127
+ select:
128
+ not_null is pick 'yes' when not (n = 1) else 'no'
129
+ ne_null is pick 'yes' when n != 1 else 'no'
130
+ not_one is pick 'yes' when not (one = 1) else 'no'
131
+ ne_one is pick 'yes' when one != 1 else 'no'
132
+ }
133
+ `).toEqualResult(tm, [
134
+ {not_null: 'yes', ne_null: 'yes', not_one: 'no', ne_one: 'no'},
135
+ ]);
136
+ });
137
+
138
+ test('a comparison picks', async () => {
139
+ await expect(`
140
+ run: sqlserver.table('malloytest.state_facts') -> {
141
+ group_by: size is pick 'big' when airport_count > 500 else 'small'
142
+ aggregate: n is count()
143
+ order_by: size
144
+ }
145
+ `).toEqualResult(tm, [
146
+ {size: 'big', n: 12},
147
+ {size: 'small', n: 39},
148
+ ]);
149
+ });
150
+
151
+ test('a boolean function is a condition', async () => {
152
+ await expect(`
153
+ run: sqlserver.table('malloytest.state_facts') -> {
154
+ where: starts_with(state, 'C') and not ends_with(state, 'A')
155
+ select: state
156
+ order_by: state
157
+ }
158
+ `).toEqualResult(tm, [{state: 'CO'}, {state: 'CT'}]);
159
+ });
160
+
161
+ test('sql_boolean is a condition', async () => {
162
+ await expect(`
163
+ ##! experimental { sql_functions }
164
+ run: sqlserver.table('malloytest.state_facts') extend {
165
+ dimension: big is sql_boolean("\${airport_count} > 500")
166
+ } -> {
167
+ where: big
168
+ aggregate: n is count()
169
+ }
170
+ `).toEqualResult(tm, [{n: 12}]);
171
+ });
172
+
173
+ test('a cast to boolean is refused', async () => {
174
+ await expect(
175
+ runtime
176
+ .loadQuery(
177
+ `run: sqlserver.table('malloytest.state_facts') -> {
178
+ select: b is airport_count::boolean
179
+ }`
180
+ )
181
+ .run()
182
+ ).rejects.toThrow(/boolean/);
183
+ });
184
+
185
+ test('starts_with, ends_with and is_nan filter an aggregate', async () => {
186
+ await expect(`
187
+ run: sqlserver.table('malloytest.state_facts') -> {
188
+ aggregate:
189
+ c_states is count() { where: starts_with(state, 'C') }
190
+ a_states is count() { where: ends_with(state, 'A') }
191
+ nan_counts is count() { where: is_nan(airport_count) }
192
+ }
193
+ `).toEqualResult(tm, [{c_states: 3, a_states: 8, nan_counts: 0}]);
194
+ });
195
+
196
+ test('reads a column', async () => {
197
+ await expect(`
198
+ run: sqlserver.table('malloytest.state_facts') -> {
199
+ select: state
200
+ where: state = 'CA'
201
+ }
202
+ `).toEqualResult(tm, [{state: 'CA'}]);
203
+ });
204
+
205
+ test('reads a bracket-quoted path', async () => {
206
+ await expect(`
207
+ run: sqlserver.table('[malloytest].[state_facts]') -> {
208
+ select: state
209
+ where: state = 'CA'
210
+ }
211
+ `).toEqualResult(tm, [{state: 'CA'}]);
212
+ });
213
+
214
+ test('reads a database-qualified path', async () => {
215
+ await expect(`
216
+ run: sqlserver.table('malloytest.malloytest.state_facts') -> {
217
+ aggregate: n is count()
218
+ }
219
+ `).toMatchResult(tm, {n: 51});
220
+ });
221
+
222
+ test('orders and limits', async () => {
223
+ await expect(`
224
+ run: sqlserver.table('malloytest.state_facts') -> {
225
+ select: state
226
+ order_by: state desc
227
+ limit: 2
228
+ }
229
+ `).toEqualResult(tm, [{state: 'WY'}, {state: 'WV'}]);
230
+ });
231
+
232
+ test('limits a stage that feeds another', async () => {
233
+ await expect(`
234
+ run: sqlserver.table('malloytest.state_facts') -> {
235
+ select: state
236
+ order_by: state desc
237
+ limit: 3
238
+ } -> {
239
+ select: state
240
+ order_by: state asc
241
+ }
242
+ `).toEqualResult(tm, [{state: 'WI'}, {state: 'WV'}, {state: 'WY'}]);
243
+ });
244
+
245
+ test('filters on a comparison', async () => {
246
+ await expect(`
247
+ run: sqlserver.table('malloytest.state_facts') -> {
248
+ aggregate: n is count()
249
+ where: airport_count > 500
250
+ }
251
+ `).toMatchResult(tm, {n: 12});
252
+ });
253
+
254
+ test('escapes a bracket in a LIKE pattern', async () => {
255
+ await expect(`
256
+ run: sqlserver.sql("""
257
+ SELECT s FROM (VALUES (N'a[b'), (N'ab'), (N'axb')) v(s)
258
+ """) -> { select: s; where: s ~ 'a[%' }
259
+ `).toEqualResult(tm, [{s: 'a[b'}]);
260
+ });
261
+ });
262
+
263
+ describe('aggregation', () => {
264
+ test('a group of only constants is one row', async () => {
265
+ await expect(`
266
+ run: sqlserver.table('malloytest.state_facts') -> { group_by: x is 1, y is 'a' }
267
+ `).toEqualResult(tm, [{x: 1, y: 'a'}]);
268
+ });
269
+
270
+ test('a constant dimension groups with the others', async () => {
271
+ await expect(`
272
+ run: sqlserver.table('malloytest.state_facts') -> {
273
+ group_by: tag is 'facts', size is pick 'big' when airport_count > 500 else 'small'
274
+ aggregate: n is count()
275
+ order_by: size
276
+ }
277
+ `).toEqualResult(tm, [
278
+ {tag: 'facts', size: 'big', n: 12},
279
+ {tag: 'facts', size: 'small', n: 39},
280
+ ]);
281
+ });
282
+
283
+ test('counts without a group', async () => {
284
+ await expect(`
285
+ run: sqlserver.table('malloytest.state_facts') -> {
286
+ aggregate: state_count is count()
287
+ }
288
+ `).toMatchResult(tm, {state_count: 51});
289
+ });
290
+
291
+ test('groups by a column', async () => {
292
+ await expect(`
293
+ run: sqlserver.table('malloytest.state_facts') -> {
294
+ group_by: popular_name
295
+ aggregate: state_count is count()
296
+ order_by: state_count desc
297
+ }
298
+ `).toMatchResult(tm, {popular_name: 'Isabella'});
299
+ });
300
+
301
+ test('groups by an expression', async () => {
302
+ await expect(`
303
+ run: sqlserver.table('malloytest.state_facts') -> {
304
+ group_by: first is substr(state, 1, 1)
305
+ aggregate: state_count is count()
306
+ order_by: state_count desc, first
307
+ }
308
+ `).toMatchResult(tm, {first: 'M', state_count: 8});
309
+ });
310
+
311
+ test('orders and limits a grouped result', async () => {
312
+ await expect(`
313
+ run: sqlserver.table('malloytest.state_facts') -> {
314
+ group_by: popular_name
315
+ aggregate: state_count is count()
316
+ order_by: state_count desc, popular_name
317
+ limit: 2
318
+ }
319
+ `).toEqualResult(tm, [
320
+ {popular_name: 'Isabella', state_count: 24},
321
+ {popular_name: 'Sophia', state_count: 11},
322
+ ]);
323
+ });
324
+
325
+ test('orders a query used as a source', async () => {
326
+ await expect(`
327
+ source: by_name is sqlserver.table('malloytest.state_facts') -> {
328
+ group_by: popular_name
329
+ aggregate: state_count is count()
330
+ order_by: state_count desc
331
+ }
332
+ run: by_name -> {
333
+ select: popular_name
334
+ where: state_count > 10
335
+ order_by: popular_name
336
+ }
337
+ `).toEqualResult(tm, [
338
+ {popular_name: 'Isabella'},
339
+ {popular_name: 'Sophia'},
340
+ ]);
341
+ });
342
+
343
+ test('orders the last stage of a pipeline', async () => {
344
+ await expect(`
345
+ run: sqlserver.table('malloytest.state_facts') -> {
346
+ group_by: popular_name
347
+ aggregate: state_count is count()
348
+ } -> {
349
+ select: popular_name, state_count
350
+ order_by: state_count asc, popular_name desc
351
+ limit: 2
352
+ }
353
+ `).toEqualResult(tm, [
354
+ {popular_name: 'Madison', state_count: 3},
355
+ {popular_name: 'Ava', state_count: 3},
356
+ ]);
357
+ });
358
+
359
+ test('divides as a float', async () => {
360
+ await expect(`
361
+ run: sqlserver.sql("SELECT 7 AS a, 2 AS b") -> {
362
+ select: q is a / b
363
+ }
364
+ `).toMatchResult(tm, {q: 3.5});
365
+ });
366
+ });
367
+
368
+ describe('functions', () => {
369
+ test('pow computes in floating point', async () => {
370
+ await expect(`
371
+ run: sqlserver.sql("SELECT 1 AS n") -> {
372
+ select: root is round(pow(2, 0.5), 6), hundredth is pow(10, -2)
373
+ }
374
+ `).toEqualResult(tm, [{root: 1.414214, hundredth: 0.01}]);
375
+ });
376
+
377
+ test('length counts trailing spaces', async () => {
378
+ await expect(`
379
+ run: sqlserver.sql("SELECT 1 AS n") -> {
380
+ select:
381
+ padded is length('abc ')
382
+ back is substr('abc ', -2)
383
+ }
384
+ `).toEqualResult(tm, [{padded: 5, back: 'c '}]);
385
+ });
386
+ });
387
+
388
+ describe('time zones', () => {
389
+ test('converts a query time zone through its Windows name', async () => {
390
+ await expect(`
391
+ run: sqlserver.sql("SELECT CAST('2020-02-20 00:00:00' AS DATETIME2) AS t") -> {
392
+ timezone: 'America/Mexico_City'
393
+ select: mex_day is t.day, mex_hour is hour(t)
394
+ }
395
+ `).toMatchResult(tm, {
396
+ mex_day: new Date('2020-02-19T06:00:00Z'),
397
+ mex_hour: 18,
398
+ });
399
+ });
400
+
401
+ test('a zone is known by its current IANA name as well as its old one', async () => {
402
+ await expect(`
403
+ run: sqlserver.sql("SELECT CAST('2021-02-24 03:05:06' AS DATETIME2) AS t") -> {
404
+ timezone: 'Asia/Kolkata'
405
+ select: h is hour(t)
406
+ }
407
+ `).toEqualResult(tm, [{h: 8}]);
408
+ await expect(`
409
+ run: sqlserver.sql("SELECT CAST('2021-02-24 03:05:06' AS DATETIME2) AS t") -> {
410
+ timezone: 'Europe/Kyiv'
411
+ select: h is hour(t)
412
+ }
413
+ `).toEqualResult(tm, [{h: 5}]);
414
+ });
415
+
416
+ test('a literal written in a zone is that instant', async () => {
417
+ await expect(`
418
+ run: sqlserver.sql("SELECT 1 AS n") -> {
419
+ select: t is @2020-02-20 00:00:00[America/Mexico_City]
420
+ }
421
+ `).toMatchResult(tm, {t: new Date('2020-02-20T06:00:00Z')});
422
+ });
423
+ });
424
+
425
+ describe('a datetimeoffset', () => {
426
+ // 12:34:56 at -05:00 is 17:34:56 UTC; 22:34:56 at -05:00 is 03:34:56 UTC the next day
427
+ const noon = "CAST('2024-03-20 12:34:56 -05:00' AS DATETIMEOFFSET)";
428
+ const night = "CAST('2024-03-20 22:34:56 -05:00' AS DATETIMEOFFSET)";
429
+
430
+ test('is an instant', async () => {
431
+ await expect(`
432
+ run: sqlserver.sql("SELECT ${noon} AS t") -> {
433
+ select: t, utc is t::timestamp
434
+ }
435
+ `).toMatchResult(tm, {
436
+ t: new Date('2024-03-20T17:34:56Z'),
437
+ utc: new Date('2024-03-20T17:34:56Z'),
438
+ });
439
+ });
440
+
441
+ test('reads its clock at UTC without a query time zone', async () => {
442
+ await expect(`
443
+ run: sqlserver.sql("SELECT ${night} AS t") -> {
444
+ select: h is hour(t), d is t.day, dt is t::date
445
+ }
446
+ `).toMatchResult(tm, {
447
+ h: 3,
448
+ d: new Date('2024-03-21T00:00:00Z'),
449
+ dt: '2024-03-21',
450
+ });
451
+ });
452
+
453
+ test('compares as an instant', async () => {
454
+ await expect(`
455
+ run: sqlserver.sql("SELECT ${noon} AS t") -> {
456
+ where: t > @2024-03-20 15:00:00
457
+ aggregate: n is count()
458
+ }
459
+ `).toMatchResult(tm, {n: 1});
460
+ });
461
+
462
+ test('offsets by a calendar unit at UTC', async () => {
463
+ await expect(`
464
+ run: sqlserver.sql("SELECT CAST('2024-01-30 22:34:56 -05:00' AS DATETIMEOFFSET) AS t") -> {
465
+ select: next is t + 1 month
466
+ }
467
+ `).toMatchResult(tm, {next: new Date('2024-02-29T03:34:56Z')});
468
+ });
469
+
470
+ test('measures between two offsets as instants', async () => {
471
+ await expect(`
472
+ run: sqlserver.sql("""
473
+ SELECT CAST('2024-03-20 12:00:00 +00:00' AS DATETIMEOFFSET) AS a,
474
+ CAST('2024-03-20 12:00:00 -05:00' AS DATETIMEOFFSET) AS b
475
+ """) -> { select: h is hours(a to b) }
476
+ `).toMatchResult(tm, {h: 5});
477
+ });
478
+ });
479
+
480
+ describe('joins', () => {
481
+ test('sums each side of a fan-out once', async () => {
482
+ await expect(`
483
+ source: facts is sqlserver.table('malloytest.state_facts')
484
+ run: facts extend { join_cross: other is facts on other.state = 'CA' | 'NY' } -> {
485
+ aggregate:
486
+ row_count is count(concat(state, other.state))
487
+ left_sum is airport_count.sum()
488
+ right_sum is other.airport_count.sum()
489
+ }
490
+ `).toMatchResult(tm, {row_count: 102, left_sum: 19701, right_sum: 1560});
491
+ });
492
+
493
+ test('joins and aggregates symmetrically', async () => {
494
+ await expect(`
495
+ source: carriers is sqlserver.table('malloytest.carriers') extend {
496
+ primary_key: code
497
+ }
498
+ source: flights is sqlserver.table('malloytest.flights') extend {
499
+ join_one: carriers with carrier
500
+ }
501
+ run: flights -> {
502
+ group_by: carriers.nickname
503
+ aggregate: flight_count is count(), carrier_count is carriers.count()
504
+ order_by: flight_count desc
505
+ }
506
+ `).toMatchResult(tm, {nickname: 'Southwest', carrier_count: 1});
507
+ });
508
+ });
509
+ });
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,15 +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
- // Must match test/mssql/connection_string.ts
211
- "ATTACH 'Server=localhost;Port=1433;Database=malloytest;User Id=sa;Password=Malloy_Test_123;TrustServerCertificate=true' AS msdb (TYPE mssql)",
212
- 'USE msdb.malloytest',
213
- ].join(';\n'),
214
- });
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
+ }
215
246
  break;
216
247
  case 'motherduck':
217
248
  connection = new DuckDBTestConnection({
@@ -247,6 +278,12 @@ export function runtimeFor(dbName: string): SingleConnectionRuntime {
247
278
  TrinoExecutor.getConnectionOptionsFromEnv(dbName) // they share configs.
248
279
  );
249
280
  break;
281
+ case 'sqlserver':
282
+ connection = new SQLServerTestConnection(
283
+ dbName,
284
+ SQLServerExecutor.getConnectionOptionsFromEnv()
285
+ );
286
+ break;
250
287
  case 'databricks':
251
288
  {
252
289
  const warehouseId = process.env['DATABRICKS_WAREHOUSE_ID'];
@@ -265,6 +302,9 @@ export function runtimeFor(dbName: string): SingleConnectionRuntime {
265
302
  default:
266
303
  throw new Error(`Unknown runtime "${dbName}`);
267
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();
268
308
  return testRuntimeFor(connection);
269
309
  } catch (error) {
270
310
  throw new Error(
@@ -314,7 +354,6 @@ export class RuntimeList {
314
354
  database instanceof SingleConnectionRuntime
315
355
  ? database
316
356
  : runtimeFor(database);
317
- rt.isTestRuntime = true;
318
357
  this.runtimeMap.set(rt.connection.name, rt);
319
358
  this.runtimeList.push([rt.connection.name, rt]);
320
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
  }
package/tsconfig.json CHANGED
@@ -34,6 +34,9 @@
34
34
  {
35
35
  "path": "../packages/malloy-db-snowflake"
36
36
  },
37
+ {
38
+ "path": "../packages/malloy-db-sqlserver"
39
+ },
37
40
  {
38
41
  "path": "../packages/malloy-db-trino"
39
42
  },