@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.
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`; run the shared suite on one with `MALLOY_DATABASE=<name>` (`ci-<name>`) and expect failures. What CI runs for an experimental dialect is decided per dialect; see [adding-a-new-database.md](../packages/malloy/src/doc/adding-a-new-database.md). 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`; connects as the test runtime does (`sa`, localhost:1433). Waits for the server itself. Same table set as MySQL. Not in CI |
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. In CI, `db-sqlserver.yaml` runs the same script on SQL Server 2017, the oldest the dialect supports |
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: the container
8
- * mssql_start.sh runs, with the password it sets.
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
- return `Server=localhost;Port=1433;Database=${database};User Id=sa;Password=Malloy_Test_123;TrustServerCertificate=true`;
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
  }
@@ -6,9 +6,17 @@
6
6
  set -e
7
7
 
8
8
  SCRIPTDIR=$(cd $(dirname $0); pwd)
9
- CONTAINER_NAME="mssql-malloy"
10
- # Must match test/mssql/connection_string.ts
11
- SA_PASSWORD="Malloy_Test_123"
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 1433"
31
+ echo "MSSQL running on port $MSSQL_PORT"
24
32
  exit 0
25
33
  fi
26
34
 
27
- # Detect architecture — Azure SQL Edge for ARM64, SQL Server for x64
28
- ARCH=$(uname -m)
29
- if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then
30
- IMAGE="mcr.microsoft.com/azure-sql-edge:latest"
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 1433:1433 \
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 1433, database: malloytest"
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"
@@ -1,4 +1,4 @@
1
1
  #!/bin/bash
2
2
 
3
- # stop and remove container
4
- docker rm -f mssql-malloy
3
+ # stop and remove the container mssql_start.sh started
4
+ docker rm -f "${MSSQL_CONTAINER:-mssql-malloy}"
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": "jest --config=../jest.config.js",
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.434",
24
- "@malloydata/db-duckdb": "0.0.434",
25
- "@malloydata/db-postgres": "0.0.434",
26
- "@malloydata/db-snowflake": "0.0.434",
27
- "@malloydata/db-trino": "0.0.434",
28
- "@malloydata/malloy": "0.0.434",
29
- "@malloydata/malloy-tag": "0.0.434",
30
- "@malloydata/render": "0.0.434",
31
- "@malloydata/render-validator": "0.0.434",
23
+ "@malloydata/db-bigquery": "0.0.436",
24
+ "@malloydata/db-duckdb": "0.0.436",
25
+ "@malloydata/db-postgres": "0.0.436",
26
+ "@malloydata/db-snowflake": "0.0.436",
27
+ "@malloydata/db-sqlserver": "0.0.436",
28
+ "@malloydata/db-trino": "0.0.436",
29
+ "@malloydata/malloy": "0.0.436",
30
+ "@malloydata/malloy-tag": "0.0.436",
31
+ "@malloydata/render": "0.0.436",
32
+ "@malloydata/render-validator": "0.0.436",
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.434"
46
+ "version": "0.0.436"
46
47
  }
@@ -3,13 +3,14 @@
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
5
 
6
- import type {
7
- FetchSchemaOptions,
8
- MalloyError,
9
- SQLSourceDef,
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
- const runtime = testRuntimeFor(connection);
70
- runtime.isTestRuntime = false; // Enables checking experimental dialects
71
-
72
- test('generate an error when used without experiment enabled', async () => {
73
- const error = await getError(
74
- runtime.getModel(`
75
- source: s is ${duckdbX}.sql('SELECT 1 as one')
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(error).not.toBeUndefined();
79
- if (error !== undefined) {
80
- const problems = (error as MalloyError).problems;
81
- expect(problems.length).toBe(1);
82
- expect(problems[0].message).toContain(
83
- `##! experimental.dialect.${duckdbX}`
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('does not generate an error when used with experiment enabled', async () => {
89
- await runtime.getModel(`
90
- ##! experimental.dialect.${duckdbX}
91
- source: s is ${duckdbX}.sql('SELECT 1 as one')
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 runtime.connection.close();
180
+ await connection.close();
97
181
  registerDialect(new DuckDBDialect());
98
182
  });
99
183
  });
@@ -14,6 +14,9 @@ import type {
14
14
  } from '@malloydata/malloy';
15
15
  import {
16
16
  ConnectionRuntime,
17
+ MalloyConfig,
18
+ Manifest,
19
+ Runtime,
17
20
  SingleConnectionRuntime,
18
21
  EMPTY_BUILD_MANIFEST,
19
22
  } from '@malloydata/malloy';
@@ -2656,9 +2659,9 @@ describe('source persistence', () => {
2656
2659
  });
2657
2660
 
2658
2661
  it('a given with an inline default keeps both sides on one key', async () => {
2659
- // The other half of the rule: `resolvedGivens` also changes the SQL, but
2660
- // only the compiler can produce one, so neither side resolves givens.
2661
- // Resolve on one side only and this throws — the builder emits
2662
+ // The other half of the rule: `resolvedGivens` also changes the SQL, so
2663
+ // both sides bind givens through the one function, inline folding
2664
+ // included. Fold on one side only and this throws — the builder emits
2662
2665
  // `'admin'='admin'` where the compiler folds the same expression.
2663
2666
  const model = `${PERSIST_ANNOTATION}
2664
2667
  ##! experimental.givens
@@ -2802,4 +2805,273 @@ describe('source persistence', () => {
2802
2805
  expect(sql).toContain('LIMIT 7');
2803
2806
  });
2804
2807
  });
2808
+
2809
+ describe('a persisted source that reads a given', () => {
2810
+ // The value is in the SQL that is hashed, and both sides compile that SQL
2811
+ // under the same values, so a query running under other values computes
2812
+ // another key and does not find the table.
2813
+ const GIVEN_MODEL = `
2814
+ ##! experimental { persistence givens }
2815
+ given: CARRIER :: string is "WN"
2816
+ ${FLIGHTS_SOURCE}
2817
+
2818
+ #@ persist
2819
+ source: by_carrier is flights -> {
2820
+ where: carrier = $CARRIER
2821
+ group_by: carrier
2822
+ aggregate: flight_count is count()
2823
+ }
2824
+
2825
+ query: q is by_carrier -> { select: * }
2826
+ `;
2827
+
2828
+ /** The no-default form, with a second source that reads no given. */
2829
+ const SUPPLIED_MODEL = `
2830
+ ##! experimental { persistence givens }
2831
+ given: CARRIER :: string
2832
+ ${FLIGHTS_SOURCE}
2833
+
2834
+ #@ persist
2835
+ source: carrier_stats is flights -> {
2836
+ group_by: carrier
2837
+ aggregate: flight_count is count()
2838
+ }
2839
+
2840
+ #@ persist
2841
+ source: by_carrier is flights -> {
2842
+ where: carrier = $CARRIER
2843
+ group_by: carrier
2844
+ aggregate: flight_count is count()
2845
+ }
2846
+
2847
+ query: q is by_carrier -> { select: * }
2848
+ `;
2849
+
2850
+ async function onlyTarget(
2851
+ rt: Runtime,
2852
+ modelText: string,
2853
+ givens?: Record<string, string>
2854
+ ): Promise<BuildTarget> {
2855
+ const model = await rt.loadModel(modelText).getModel();
2856
+ const {connections, unbuildable} = await rt.getBuildTargets(
2857
+ model,
2858
+ givens ? {givens} : undefined
2859
+ );
2860
+ expect(unbuildable).toEqual([]);
2861
+ const targets = connections.flatMap(c => c.targets);
2862
+ expect(targets).toHaveLength(1);
2863
+ return targets[0];
2864
+ }
2865
+
2866
+ it('is built for the default, and a query under the default finds it', async () => {
2867
+ const manifest = createManifest();
2868
+ const rt = runtimeWithManifest(manifest);
2869
+ const target = await onlyTarget(rt, GIVEN_MODEL);
2870
+ expect(target.readsGivens).toBe(true);
2871
+ expect(target.sql).toContain("'WN'");
2872
+ addManifestEntry(manifest, target.buildId, 'wn_table');
2873
+
2874
+ const query = rt.loadModel(GIVEN_MODEL).loadQueryByName('q');
2875
+ expect(await query.getSQL()).toContain('wn_table');
2876
+ expect(await query.getSQL({givens: {CARRIER: 'WN'}})).toContain(
2877
+ 'wn_table'
2878
+ );
2879
+ });
2880
+
2881
+ it('a query under another value does not find it, and runs live', async () => {
2882
+ const manifest = createManifest();
2883
+ const rt = runtimeWithManifest(manifest);
2884
+ const target = await onlyTarget(rt, GIVEN_MODEL);
2885
+ addManifestEntry(manifest, target.buildId, 'wn_table');
2886
+
2887
+ const sql = await rt
2888
+ .loadModel(GIVEN_MODEL)
2889
+ .loadQueryByName('q')
2890
+ .getSQL({givens: {CARRIER: 'AA'}});
2891
+ expect(sql).not.toContain('wn_table');
2892
+ expect(sql).toContain("'AA'");
2893
+ });
2894
+
2895
+ it('under a strict manifest the miss says the source reads givens', async () => {
2896
+ const manifest = createManifest();
2897
+ manifest.strict = true;
2898
+ const rt = runtimeWithManifest(manifest);
2899
+ const target = await onlyTarget(rt, GIVEN_MODEL);
2900
+ addManifestEntry(manifest, target.buildId, 'wn_table');
2901
+
2902
+ await expect(
2903
+ rt
2904
+ .loadModel(GIVEN_MODEL)
2905
+ .loadQueryByName('q')
2906
+ .getSQL({givens: {CARRIER: 'AA'}})
2907
+ ).rejects.toThrow(/strict manifest.*reads givens/s);
2908
+ });
2909
+
2910
+ it('a build under another value is another table, and each query finds its own', async () => {
2911
+ const manifest = createManifest();
2912
+ const rt = runtimeWithManifest(manifest);
2913
+ const wn = await onlyTarget(rt, GIVEN_MODEL);
2914
+ const aa = await onlyTarget(rt, GIVEN_MODEL, {CARRIER: 'AA'});
2915
+ expect(aa.buildId).not.toBe(wn.buildId);
2916
+ expect(aa.sql).toContain("'AA'");
2917
+ addManifestEntry(manifest, wn.buildId, 'wn_table');
2918
+ addManifestEntry(manifest, aa.buildId, 'aa_table');
2919
+
2920
+ const query = rt.loadModel(GIVEN_MODEL).loadQueryByName('q');
2921
+ expect(await query.getSQL()).toContain('wn_table');
2922
+ expect(await query.getSQL({givens: {CARRIER: 'AA'}})).toContain(
2923
+ 'aa_table'
2924
+ );
2925
+ });
2926
+
2927
+ it('the rows a query reads are the rows for its own value', async () => {
2928
+ const rwConn = new DuckDBConnection({
2929
+ name: tstDB,
2930
+ databasePath: ':memory:',
2931
+ workingDirectory: 'test/data/duckdb',
2932
+ });
2933
+ try {
2934
+ const flightsPath = 'test/data/malloytest-parquet/flights.parquet';
2935
+ const modelText = GIVEN_MODEL.replace(
2936
+ FLIGHTS_SOURCE,
2937
+ `source: flights is ${tstDB}.table('${flightsPath}')`
2938
+ );
2939
+ const rt = new SingleConnectionRuntime({
2940
+ connection: rwConn,
2941
+ urlReader: testFileSpace,
2942
+ });
2943
+ const manifest = new Manifest();
2944
+ const build = async (name: string, givens?: Record<string, string>) => {
2945
+ const target = await onlyTarget(rt, modelText, givens);
2946
+ await rwConn.runSQL(
2947
+ `CREATE TABLE ${name} AS ${target.buildSQL(manifest.buildManifest)}`
2948
+ );
2949
+ manifest.update(target.buildId, {tableName: name});
2950
+ };
2951
+ await build('wn_table');
2952
+ await build('aa_table', {CARRIER: 'AA'});
2953
+ rt.buildManifest = manifest.buildManifest;
2954
+
2955
+ const query = rt.loadModel(modelText).loadQueryByName('q');
2956
+ const carriers = async (givens?: Record<string, string>) => {
2957
+ const result = await query.run(givens ? {givens} : undefined);
2958
+ return result.data.toObject().map(row => row['carrier']);
2959
+ };
2960
+ expect(await carriers()).toEqual(['WN']);
2961
+ expect(await carriers({CARRIER: 'AA'})).toEqual(['AA']);
2962
+ expect(await query.getSQL({givens: {CARRIER: 'AA'}})).toContain(
2963
+ 'aa_table'
2964
+ );
2965
+ } finally {
2966
+ await rwConn.close();
2967
+ }
2968
+ });
2969
+
2970
+ it('with no default and no value for the build it is unbuildable, and the rest is planned', async () => {
2971
+ const model = await tstRuntime.loadModel(SUPPLIED_MODEL).getModel();
2972
+ const {connections, unbuildable} =
2973
+ await tstRuntime.getBuildTargets(model);
2974
+
2975
+ expect(
2976
+ connections.flatMap(c => c.targets).map(t => t.sources[0].name)
2977
+ ).toEqual(['carrier_stats']);
2978
+ expect(unbuildable.map(u => u.source.name)).toEqual(['by_carrier']);
2979
+ const [{problem}] = unbuildable;
2980
+ expect(problem.code).toBe('runtime-persist-given-no-value');
2981
+ expect(problem.message).toMatch(/by_carrier@.*reads given 'CARRIER'/);
2982
+ });
2983
+
2984
+ it('a query over the unbuildable source runs live under a manifest of other tables', async () => {
2985
+ const manifest = createManifest();
2986
+ const rt = runtimeWithManifest(manifest);
2987
+ const model = await rt.loadModel(SUPPLIED_MODEL).getModel();
2988
+ const {connections} = await rt.getBuildTargets(model);
2989
+ const [stats] = connections.flatMap(c => c.targets);
2990
+ addManifestEntry(manifest, stats.buildId, 'carrier_stats_table');
2991
+
2992
+ const sql = await rt
2993
+ .loadModel(SUPPLIED_MODEL)
2994
+ .loadQueryByName('q')
2995
+ .getSQL({givens: {CARRIER: 'WN'}});
2996
+ expect(sql).toContain("'WN'");
2997
+ expect(sql).not.toContain('carrier_stats_table');
2998
+ });
2999
+
3000
+ it('a value supplied to the build is a value', async () => {
3001
+ const target = await onlyTarget(
3002
+ tstRuntime,
3003
+ SUPPLIED_MODEL.replace(/#@ persist\s+source: carrier_stats[^}]*}/, ''),
3004
+ {CARRIER: 'WN'}
3005
+ );
3006
+ expect(target.sql).toContain("'WN'");
3007
+ });
3008
+
3009
+ it("the Runtime's givens are the build's givens", async () => {
3010
+ const rt = new Runtime({
3011
+ connections: tstRuntime.connections,
3012
+ urlReader: testFileSpace,
3013
+ givens: {CARRIER: 'UA'},
3014
+ });
3015
+ const target = await onlyTarget(
3016
+ rt,
3017
+ SUPPLIED_MODEL.replace(/#@ persist\s+source: carrier_stats[^}]*}/, '')
3018
+ );
3019
+ expect(target.sql).toContain("'UA'");
3020
+ });
3021
+
3022
+ it('a finalized given cannot be supplied to a build', async () => {
3023
+ const config = new MalloyConfig(
3024
+ {finalizeGivens: ['CARRIER']},
3025
+ {configURL: 'file:///finalized/malloy-config.json'}
3026
+ );
3027
+ const rt = new Runtime({
3028
+ config,
3029
+ connections: tstRuntime.connections,
3030
+ urlReader: testFileSpace,
3031
+ givens: {CARRIER: 'WN'},
3032
+ });
3033
+ const model = await rt.loadModel(GIVEN_MODEL).getModel();
3034
+ await expect(
3035
+ rt.getBuildTargets(model, {givens: {CARRIER: 'AA'}})
3036
+ ).rejects.toThrow(/'CARRIER'.*finalized/);
3037
+ });
3038
+ });
3039
+
3040
+ describe('a build and its virtualMap', () => {
3041
+ const VIRTUAL_MODEL = `${PERSIST_ANNOTATION}
3042
+ ##! experimental.virtual_source
3043
+ type: ff is { carrier :: string }
3044
+ source: vf is ${tstDB}.virtual('vflights')::ff
3045
+
3046
+ #@ persist
3047
+ source: by_carrier is vf -> { group_by: carrier }
3048
+
3049
+ run: by_carrier -> { select: * }
3050
+ `;
3051
+ const virtualMap: VirtualMap = new Map([
3052
+ [tstDB, new Map([['vflights', 'malloytest.flights']])],
3053
+ ]);
3054
+
3055
+ it('a virtual source with no entry is unbuildable, not a failed plan', async () => {
3056
+ const model = await tstRuntime.loadModel(VIRTUAL_MODEL).getModel();
3057
+ const {connections, unbuildable} =
3058
+ await tstRuntime.getBuildTargets(model);
3059
+ expect(connections).toEqual([]);
3060
+ expect(unbuildable.map(u => u.problem.code)).toEqual([
3061
+ 'runtime-virtual-map-missing',
3062
+ ]);
3063
+ });
3064
+
3065
+ it('a virtualMap given to the build keys and builds the table', async () => {
3066
+ const model = await tstRuntime.loadModel(VIRTUAL_MODEL).getModel();
3067
+ const {connections, unbuildable} = await tstRuntime.getBuildTargets(
3068
+ model,
3069
+ {virtualMap}
3070
+ );
3071
+ expect(unbuildable).toEqual([]);
3072
+ const [target] = connections[0].targets;
3073
+ expect(target.sql).toContain('malloytest.flights');
3074
+ expect(target.buildSQL(createManifest())).toContain('malloytest.flights');
3075
+ });
3076
+ });
2805
3077
  });