@malloydata/malloy-tests 0.0.434 → 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 +5 -2
- package/mssql/connection_string.ts +19 -3
- package/mssql/mssql_start.sh +19 -13
- package/mssql/mssql_stop.sh +2 -2
- package/package.json +12 -11
- package/src/core/experimental-dialects.spec.ts +125 -41
- package/src/databases/all/db_index.spec.ts +4 -2
- package/src/databases/sqlserver/sqlserver.spec.ts +395 -0
- package/src/runtimes.ts +49 -10
- package/src/test-select.ts +10 -14
- package/tsconfig.json +3 -0
package/CONTEXT.md
CHANGED
|
@@ -40,6 +40,8 @@ Tests that run against **all** supported databases to ensure consistent behavior
|
|
|
40
40
|
|
|
41
41
|
These tests are particularly important for verifying that Malloy's abstraction works correctly across all supported SQL dialects.
|
|
42
42
|
|
|
43
|
+
An experimental dialect is not in `allDatabases`. CI runs only its smoke job, `ci-<name>-smoketest`, which selects the dialect's own jest project and must stay green; the shared suite is run by hand with `MALLOY_DATABASE=<name>` (`ci-<name>`) and is allowed to be red until the dialect leaves experimental. A follow-up that implements a feature moves its shared-suite tests into the green set by flipping the capability flag they are gated on. The files carry no `##! experimental.dialect.<name>` lines: `runtimeFor` in `runtimes.ts` calls `acceptExperimental()` on the dialect under test once per process, which is the only place that acknowledgment is waived.
|
|
44
|
+
|
|
43
45
|
### Consumer-contract canary (`test/consumer-canary/`)
|
|
44
46
|
Not a normal test — it consumes the *built* `@malloydata/*` packages the way a downstream app does (esbuild bundle + plain ts-jest, no babel) to catch native/ESM leaks that malloy's own CI is blind to. Run locally with `npm run test-consumer-canary` (it builds first). See [`test/consumer-canary/CONTEXT.md`](consumer-canary/CONTEXT.md).
|
|
45
47
|
|
|
@@ -99,7 +101,7 @@ npm run build-duckdb-db # Creates test/data/duckdb/duckdb_test.db
|
|
|
99
101
|
|
|
100
102
|
This creates a local DuckDB database file populated with test data.
|
|
101
103
|
|
|
102
|
-
### PostgreSQL, MySQL, Trino, Presto
|
|
104
|
+
### PostgreSQL, MySQL, Trino, Presto, SQL Server
|
|
103
105
|
These databases require Docker containers to be running.
|
|
104
106
|
|
|
105
107
|
**Starting database containers:**
|
|
@@ -108,6 +110,7 @@ Each database has a startup script in the test directory:
|
|
|
108
110
|
- `test/mysql/mysql_start.sh`
|
|
109
111
|
- `test/trino/trino_start.sh`
|
|
110
112
|
- `test/presto/presto_start.sh`
|
|
113
|
+
- `test/mssql/mssql_start.sh`
|
|
111
114
|
|
|
112
115
|
These scripts start Docker containers with appropriate test configurations and data.
|
|
113
116
|
|
|
@@ -174,7 +177,7 @@ All cross-database tests (`test/src/databases/all/`) reference tables as `malloy
|
|
|
174
177
|
| DuckDB | DuckDB `CREATE TABLE AS SELECT FROM read_parquet()` | `test/duckdb/load_test_data.sh` (wraps `load_test_data.ts`) | Run via `sh test/duckdb/load_test_data.sh` (or `npm run build-duckdb-db`). Creates `test/data/duckdb/duckdb_test.db` |
|
|
175
178
|
| PostgreSQL | DuckDB's `postgres` extension: `ATTACH` the server, `CREATE TABLE AS SELECT FROM read_parquet()` | `test/postgres/load_test_data.sh` (wraps `load_test_data.ts`) | Run by `test/postgres/postgres_start.sh` and by CI against a running server; connects with the `PG*` variables. No `ga_sample` (no anonymous record type) |
|
|
176
179
|
| MySQL | DuckDB's `mysql` extension, likewise | `test/mysql/load_test_data.sh` (wraps `load_test_data.ts`) | Run by `test/mysql/mysql_start.sh`; connects with the `MYSQL_*` variables. No `ga_sample`, and `alltypes` without its array columns |
|
|
177
|
-
| SQL Server | DuckDB's community `mssql` extension, likewise | `test/mssql/load_test_data.sh` (wraps `load_test_data.ts`) | Run by `test/mssql/mssql_start.sh
|
|
180
|
+
| SQL Server | DuckDB's community `mssql` extension, likewise | `test/mssql/load_test_data.sh` (wraps `load_test_data.ts`) | Run by `test/mssql/mssql_start.sh`, which sets the `MSSQL_*` variables for the loader; the tests and both SQL Server test runtimes (`sqlserver`, `mssql_via_duckdb`) read the same variables from their own environment. Waits for the server itself. Same table set as MySQL. `db-sqlserver.yaml` runs on dispatch only while the dialect is experimental |
|
|
178
181
|
| Trino/Presto | The hive connector reads the parquet in place: each file is mounted into the container and declared as an external table | `test/trino/hive_ddl.ts`, `test/trino/hive.properties`, `test/presto/hive.properties` | `trino_start.sh` / `presto_start.sh` derive the DDL from the parquet schemas at container start. The hive metastore lowercases names, so a table with nested columns (`ga_sample`) is a `_hive` table plus a view that casts the nested field names back; top-level column names stay lowercase, since Trino lowercases those in every connector |
|
|
179
182
|
| BigQuery | Pre-loaded manually in `malloydata-org` project | (none) | No loader script in repo. Data assumed to exist. |
|
|
180
183
|
| Snowflake | SQL: `PUT` local parquet → stage, `COPY INTO` table | `test/snowflake/load_test_data.sh` (wraps `load_test_data.sql`) | Run via `sh test/snowflake/load_test_data.sh` (needs the `snowsql` CLI) |
|
|
@@ -4,9 +4,25 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* The SQL Server the mssql tests and loader connect to
|
|
8
|
-
* mssql_start.sh
|
|
7
|
+
* The SQL Server the mssql tests and loader connect to, from the MSSQL_*
|
|
8
|
+
* variables mssql_start.sh exports. The test runtimes read the same
|
|
9
|
+
* variables through SQLServerExecutor.getConnectionOptionsFromEnv.
|
|
9
10
|
*/
|
|
11
|
+
function required(name: string): string {
|
|
12
|
+
const value = process.env[name];
|
|
13
|
+
if (!value) {
|
|
14
|
+
throw new Error(`${name} is not set; see test/mssql/mssql_start.sh`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The ADO-style string DuckDB's mssql extension takes in ATTACH */
|
|
10
20
|
export function mssqlConnectionString(database: string): string {
|
|
11
|
-
|
|
21
|
+
const host = required('MSSQL_HOST');
|
|
22
|
+
const port = required('MSSQL_PORT');
|
|
23
|
+
const user = required('MSSQL_USER');
|
|
24
|
+
const password = required('MSSQL_PASSWORD');
|
|
25
|
+
const trust = process.env['MSSQL_TRUST_SERVER_CERTIFICATE'] === 'true';
|
|
26
|
+
// `Server=host,port`: the extension reads no separate Port key
|
|
27
|
+
return `Server=${host},${port};Database=${database};User Id=${user};Password=${password};TrustServerCertificate=${trust}`;
|
|
12
28
|
}
|
package/mssql/mssql_start.sh
CHANGED
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
set -e
|
|
7
7
|
|
|
8
8
|
SCRIPTDIR=$(cd $(dirname $0); pwd)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# MSSQL_IMAGE and MSSQL_CONTAINER select a server version to run beside the default
|
|
10
|
+
CONTAINER_NAME="${MSSQL_CONTAINER:-mssql-malloy}"
|
|
11
|
+
# The loader and the tests read MSSQL_*; this script is the only place a
|
|
12
|
+
# default is written. Export the same values to run the tests by hand.
|
|
13
|
+
export MSSQL_HOST="${MSSQL_HOST:-localhost}"
|
|
14
|
+
export MSSQL_PORT="${MSSQL_PORT:-1433}"
|
|
15
|
+
export MSSQL_TRUST_SERVER_CERTIFICATE="${MSSQL_TRUST_SERVER_CERTIFICATE:-true}"
|
|
16
|
+
export MSSQL_USER="${MSSQL_USER:-sa}"
|
|
17
|
+
export MSSQL_PASSWORD="${MSSQL_PASSWORD:-Malloy_Test_123}"
|
|
18
|
+
export MSSQL_DATABASE="${MSSQL_DATABASE:-malloytest}"
|
|
19
|
+
SA_PASSWORD="$MSSQL_PASSWORD"
|
|
12
20
|
|
|
13
21
|
# Check for existing container
|
|
14
22
|
if docker container inspect "$CONTAINER_NAME" > /dev/null 2>&1; then
|
|
@@ -20,24 +28,21 @@ if docker container inspect "$CONTAINER_NAME" > /dev/null 2>&1; then
|
|
|
20
28
|
docker start "$CONTAINER_NAME"
|
|
21
29
|
# The loader waits for the server; reloading is cheap
|
|
22
30
|
sh "$SCRIPTDIR/load_test_data.sh"
|
|
23
|
-
echo "MSSQL running on port
|
|
31
|
+
echo "MSSQL running on port $MSSQL_PORT"
|
|
24
32
|
exit 0
|
|
25
33
|
fi
|
|
26
34
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
else
|
|
32
|
-
IMAGE="mcr.microsoft.com/mssql/server:2022-latest"
|
|
33
|
-
fi
|
|
35
|
+
# SQL Server 2022 by default; MSSQL_IMAGE picks another version, 2017 being
|
|
36
|
+
# the oldest the sqlserver dialect runs on. The x64 image runs under emulation
|
|
37
|
+
# on ARM64 Macs.
|
|
38
|
+
IMAGE="${MSSQL_IMAGE:-mcr.microsoft.com/mssql/server:2022-latest}"
|
|
34
39
|
|
|
35
40
|
echo "Starting $CONTAINER_NAME ($IMAGE)..."
|
|
36
41
|
docker run -d \
|
|
37
42
|
--name "$CONTAINER_NAME" \
|
|
38
43
|
-e "ACCEPT_EULA=Y" \
|
|
39
44
|
-e "MSSQL_SA_PASSWORD=$SA_PASSWORD" \
|
|
40
|
-
-p
|
|
45
|
+
-p "$MSSQL_PORT:1433" \
|
|
41
46
|
"$IMAGE"
|
|
42
47
|
|
|
43
48
|
# The loader waits for the server. A container without its tables must not
|
|
@@ -47,4 +52,5 @@ echo "Loading test data..."
|
|
|
47
52
|
sh "$SCRIPTDIR/load_test_data.sh"
|
|
48
53
|
trap - ERR
|
|
49
54
|
|
|
50
|
-
echo "MSSQL running on port
|
|
55
|
+
echo "MSSQL running on port $MSSQL_PORT, database: $MSSQL_DATABASE"
|
|
56
|
+
echo "Tests need the same MSSQL_HOST/MSSQL_PORT/MSSQL_USER/MSSQL_PASSWORD/MSSQL_DATABASE in their environment"
|
package/mssql/mssql_stop.sh
CHANGED
package/package.json
CHANGED
|
@@ -14,21 +14,22 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"lint": "eslint '**/*.ts{,x}'",
|
|
16
16
|
"lint-fix": "eslint '**/*.ts{,x}' --fix",
|
|
17
|
-
"test": "
|
|
17
|
+
"test": "echo 'Read the Testing section of the repo-root CONTEXT.md for information on testing in this monorepo'; exit 1",
|
|
18
18
|
"build": "node ../node_modules/typescript/bin/tsc --build",
|
|
19
19
|
"clean": "node ../node_modules/typescript/bin/tsc --build --clean && rm -f tsconfig.tsbuildinfo",
|
|
20
20
|
"malloyc": "ts-node ../scripts/malloy-to-json"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@malloydata/db-bigquery": "0.0.
|
|
24
|
-
"@malloydata/db-duckdb": "0.0.
|
|
25
|
-
"@malloydata/db-postgres": "0.0.
|
|
26
|
-
"@malloydata/db-snowflake": "0.0.
|
|
27
|
-
"@malloydata/db-
|
|
28
|
-
"@malloydata/
|
|
29
|
-
"@malloydata/malloy
|
|
30
|
-
"@malloydata/
|
|
31
|
-
"@malloydata/render
|
|
23
|
+
"@malloydata/db-bigquery": "0.0.435",
|
|
24
|
+
"@malloydata/db-duckdb": "0.0.435",
|
|
25
|
+
"@malloydata/db-postgres": "0.0.435",
|
|
26
|
+
"@malloydata/db-snowflake": "0.0.435",
|
|
27
|
+
"@malloydata/db-sqlserver": "0.0.435",
|
|
28
|
+
"@malloydata/db-trino": "0.0.435",
|
|
29
|
+
"@malloydata/malloy": "0.0.435",
|
|
30
|
+
"@malloydata/malloy-tag": "0.0.435",
|
|
31
|
+
"@malloydata/render": "0.0.435",
|
|
32
|
+
"@malloydata/render-validator": "0.0.435",
|
|
32
33
|
"events": "^3.3.0",
|
|
33
34
|
"jsdom": "^22.1.0",
|
|
34
35
|
"luxon": "^3.7.2",
|
|
@@ -42,5 +43,5 @@
|
|
|
42
43
|
"overrides": {
|
|
43
44
|
"typescript": "^6.0.3"
|
|
44
45
|
},
|
|
45
|
-
"version": "0.0.
|
|
46
|
+
"version": "0.0.435"
|
|
46
47
|
}
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* SPDX-License-Identifier: MIT
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import type {MalloyError} from '@malloydata/malloy';
|
|
7
|
+
import {
|
|
8
|
+
DuckDBDialect,
|
|
9
|
+
getDialect,
|
|
10
|
+
InMemoryURLReader,
|
|
11
|
+
registerDialect,
|
|
12
|
+
SingleConnectionRuntime,
|
|
10
13
|
} from '@malloydata/malloy';
|
|
11
|
-
import {DuckDBDialect, registerDialect} from '@malloydata/malloy';
|
|
12
|
-
import {testRuntimeFor} from '../runtimes';
|
|
13
14
|
import {DuckDBConnection} from '@malloydata/db-duckdb';
|
|
14
15
|
|
|
15
16
|
const envDatabases = (
|
|
@@ -33,30 +34,24 @@ async function getError<T>(promise: Promise<T>): Promise<Error | undefined> {
|
|
|
33
34
|
return undefined;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A file acknowledges an experimental dialect with
|
|
39
|
+
* `##! experimental.dialect.NAME` wherever it takes one on: by naming a
|
|
40
|
+
* connection on it, or by importing a source or query that uses it. The
|
|
41
|
+
* acknowledgment is a pragma, so an importer gets no credit for the line in
|
|
42
|
+
* the file it imports.
|
|
43
|
+
*/
|
|
36
44
|
describe('experimental dialects', () => {
|
|
37
45
|
const duckdbX = 'duckdb_experimental';
|
|
46
|
+
const flag = `##! experimental.dialect.${duckdbX}`;
|
|
47
|
+
|
|
38
48
|
class DuckdbXConnection extends DuckDBConnection {
|
|
39
49
|
name = duckdbX;
|
|
40
|
-
public async festchSchemaFOrSQLSource(
|
|
41
|
-
sqlRef: SQLSourceDef,
|
|
42
|
-
options: FetchSchemaOptions
|
|
43
|
-
): Promise<
|
|
44
|
-
| {structDef: SQLSourceDef; error?: undefined}
|
|
45
|
-
| {error: string; structDef?: undefined}
|
|
46
|
-
> {
|
|
47
|
-
const result = await super.fetchSchemaForSQLStruct(sqlRef, options);
|
|
48
|
-
if (result.error === undefined) {
|
|
49
|
-
return {structDef: {...result.structDef, dialect: duckdbX}};
|
|
50
|
-
}
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
50
|
get dialectName(): string {
|
|
54
51
|
return duckdbX;
|
|
55
52
|
}
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
const connection = new DuckdbXConnection(duckdbX, ':memory:');
|
|
59
|
-
|
|
60
55
|
class DuckdbXDialect extends DuckDBDialect {
|
|
61
56
|
experimental = true;
|
|
62
57
|
name = duckdbX;
|
|
@@ -65,35 +60,124 @@ describe('experimental dialects', () => {
|
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
62
|
|
|
63
|
+
const connection = new DuckdbXConnection(duckdbX, ':memory:');
|
|
68
64
|
registerDialect(new DuckdbXDialect());
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
|
|
66
|
+
const libURL = 'file:///lib.malloy';
|
|
67
|
+
const lib = [
|
|
68
|
+
flag,
|
|
69
|
+
`source: s is ${duckdbX}.sql('SELECT 1 as one')`,
|
|
70
|
+
'query: q is s -> { select: one }',
|
|
71
|
+
].join('\n');
|
|
72
|
+
|
|
73
|
+
function runtime(): SingleConnectionRuntime {
|
|
74
|
+
return new SingleConnectionRuntime({
|
|
75
|
+
connection,
|
|
76
|
+
urlReader: new InMemoryURLReader(new Map([[libURL, lib]])),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The one problem a refused compile reports, with the line it points at. */
|
|
81
|
+
async function refusal(
|
|
82
|
+
src: string
|
|
83
|
+
): Promise<{message: string; line: number | undefined}> {
|
|
84
|
+
const error = await getError(runtime().getModel(src));
|
|
85
|
+
expect(error).toBeDefined();
|
|
86
|
+
const problems = (error as MalloyError).problems;
|
|
87
|
+
expect(problems).toHaveLength(1);
|
|
88
|
+
return {
|
|
89
|
+
message: problems[0].message,
|
|
90
|
+
line: problems[0].at?.range.start.line,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test('naming a connection on the dialect needs the flag', async () => {
|
|
95
|
+
const got = await refusal(
|
|
96
|
+
['-- line zero', `source: s is ${duckdbX}.sql('SELECT 1 as one')`].join(
|
|
97
|
+
'\n'
|
|
98
|
+
)
|
|
77
99
|
);
|
|
78
|
-
expect(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
expect(got.message).toContain(flag);
|
|
101
|
+
expect(got.line).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test.each([['constructor'], ['__proto__'], ['toString']])(
|
|
105
|
+
'a connection named %s is checked like any other',
|
|
106
|
+
async connName => {
|
|
107
|
+
const named = new DuckdbXConnection(duckdbX, ':memory:');
|
|
108
|
+
named.name = connName;
|
|
109
|
+
const error = await getError(
|
|
110
|
+
new SingleConnectionRuntime({connection: named}).getModel(
|
|
111
|
+
`source: s is ${connName}.sql('SELECT 1 as one')`
|
|
112
|
+
)
|
|
84
113
|
);
|
|
114
|
+
expect(error).toBeDefined();
|
|
115
|
+
expect((error as MalloyError).problems[0].message).toContain(flag);
|
|
116
|
+
await named.close();
|
|
85
117
|
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
test('the flag admits a connection on the dialect', async () => {
|
|
121
|
+
await runtime().getModel(
|
|
122
|
+
[flag, `source: s is ${duckdbX}.sql('SELECT 1 as one')`].join('\n')
|
|
123
|
+
);
|
|
86
124
|
});
|
|
87
125
|
|
|
88
|
-
test('
|
|
89
|
-
await
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
test('importing a source on the dialect needs the flag, reported at the import', async () => {
|
|
127
|
+
const got = await refusal(
|
|
128
|
+
['-- line zero', `import "${libURL}"`, 'run: s -> { select: one }'].join(
|
|
129
|
+
'\n'
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
expect(got.message).toContain(flag);
|
|
133
|
+
expect(got.line).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('importing only a query on the dialect needs the flag', async () => {
|
|
137
|
+
const got = await refusal(`import {q} from "${libURL}"`);
|
|
138
|
+
expect(got.message).toContain(flag);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('the flag admits an import and use of the dialect', async () => {
|
|
142
|
+
const sql = await runtime()
|
|
143
|
+
.loadModel([flag, `import "${libURL}"`].join('\n'))
|
|
144
|
+
.loadQuery('run: s -> { select: one }')
|
|
145
|
+
.getSQL();
|
|
146
|
+
expect(sql).toContain('SELECT');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('a query over a model needs nothing of its own', async () => {
|
|
150
|
+
const sql = await runtime()
|
|
151
|
+
.loadModel(
|
|
152
|
+
[flag, `source: s is ${duckdbX}.sql('SELECT 1 as one')`].join('\n')
|
|
153
|
+
)
|
|
154
|
+
.loadQuery('run: s -> { select: one }')
|
|
155
|
+
.getSQL();
|
|
156
|
+
expect(sql).toContain('SELECT');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('an accepted dialect needs no flag anywhere', async () => {
|
|
160
|
+
// What the shared-suite harness does once per process for the dialect
|
|
161
|
+
// under test. Undone below by registering a fresh dialect object.
|
|
162
|
+
getDialect(duckdbX).acceptExperimental();
|
|
163
|
+
try {
|
|
164
|
+
const sql = await runtime()
|
|
165
|
+
.loadModel(
|
|
166
|
+
[
|
|
167
|
+
`import "${libURL}"`,
|
|
168
|
+
`source: t is ${duckdbX}.sql('SELECT 2 as two')`,
|
|
169
|
+
].join('\n')
|
|
170
|
+
)
|
|
171
|
+
.loadQuery('run: s -> { select: one }')
|
|
172
|
+
.getSQL();
|
|
173
|
+
expect(sql).toContain('SELECT');
|
|
174
|
+
} finally {
|
|
175
|
+
registerDialect(new DuckdbXDialect());
|
|
176
|
+
}
|
|
93
177
|
});
|
|
94
178
|
|
|
95
179
|
afterAll(async () => {
|
|
96
|
-
await
|
|
180
|
+
await connection.close();
|
|
97
181
|
registerDialect(new DuckDBDialect());
|
|
98
182
|
});
|
|
99
183
|
});
|
|
@@ -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 {
|
|
@@ -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,15 +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
|
-
|
|
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
|
}
|
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
|
}
|