@malloydata/malloy-tests 0.0.433 → 0.0.434

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,34 @@
1
+ /*
2
+ * Copyright Contributors to the Malloy project
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ /* eslint-disable no-console */
7
+
8
+ /*
9
+ * Load the malloytest tables into a running postgres from the parquet files.
10
+ * Connects the way psql and the postgres test connection do, through libpq's
11
+ * PG* variables, which the extension reads when the attach string is empty.
12
+ */
13
+
14
+ import {
15
+ loadExtension,
16
+ loadMalloytest,
17
+ openDuckDB,
18
+ } from '../data/parquet_loader';
19
+
20
+ (async () => {
21
+ const db = await openDuckDB();
22
+ await loadExtension(db, 'postgres');
23
+ await db.run("ATTACH '' AS pg (TYPE postgres)");
24
+ // The sampling tests use TABLESAMPLE SYSTEM_ROWS
25
+ await db.run(
26
+ "CALL postgres_execute('pg', 'CREATE EXTENSION IF NOT EXISTS tsm_system_rows')"
27
+ );
28
+ console.log('Loading malloytest into postgres');
29
+ await loadMalloytest(db, 'pg.malloytest', {
30
+ // Postgres has no anonymous record type for the nested columns
31
+ skip: ['ga_sample'],
32
+ });
33
+ await db.close();
34
+ })();
@@ -7,9 +7,7 @@ set -e
7
7
  rm -rf .tmp
8
8
  mkdir .tmp
9
9
 
10
- # run docker
11
10
  SCRIPTDIR=$(cd $(dirname $0); pwd)
12
- DATADIR=$(dirname $SCRIPTDIR)/data/postgres
13
11
 
14
12
  # Verify required environment variables and show export commands for any that are wrong
15
13
  EXPORTS=""
@@ -55,7 +53,7 @@ if docker container inspect "$CONTAINER_NAME" > /dev/null 2>&1; then
55
53
  exit 0
56
54
  fi
57
55
 
58
- docker run -p 5432:5432 -d -v $DATADIR:/init_data \
56
+ docker run -p 5432:5432 -d \
59
57
  --name "$CONTAINER_NAME" \
60
58
  -e POSTGRES_USER=root -e POSTGRES_PASSWORD=postgres \
61
59
  -e TZ=UTC \
@@ -74,8 +72,6 @@ done
74
72
  echo "Container $CONTAINER_NAME is now healthy!"
75
73
 
76
74
  echo "Loading data ..."
77
- # configure
78
- echo CREATE EXTENSION tsm_system_rows\; | psql
79
- gunzip -c ${DATADIR}/malloytest-postgres.sql.gz | psql
75
+ sh "$SCRIPTDIR/load_test_data.sh"
80
76
 
81
77
  echo "Ready"
@@ -1,11 +1,13 @@
1
1
  # Slim Presto image for Malloy CI testing
2
2
  #
3
- # This image is ~850MB vs ~9GB for the official prestodb/presto image.
4
- # It includes only what's needed to run Presto with the BigQuery connector.
3
+ # This image is ~1.4GB vs ~9GB for the official prestodb/presto image.
4
+ # It includes only what's needed to run Presto with the hive connector, which
5
+ # serves the test parquet (see hive.properties), and the CLI that creates the
6
+ # tables.
5
7
  #
6
8
  # How this was created:
7
9
  # 1. The official prestodb/presto:0.287 image contains 42 connector plugins (~2GB)
8
- # but we only need presto-bigquery (~36MB)
10
+ # but we only need hive-hadoop2 (~306MB)
9
11
  # 2. We use eclipse-temurin as the base because:
10
12
  # - Presto requires Java 11 (not newer - tested, Java 17 fails with classloading errors)
11
13
  # - Presto requires full JDK (not JRE - needs com.sun.tools.attach classes)
@@ -18,7 +20,7 @@
18
20
  #
19
21
  # Size comparison (as of 0.287):
20
22
  # Official image: 9.11 GB
21
- # This slim image: ~850 MB
23
+ # This slim image: ~1.4 GB
22
24
 
23
25
  ARG PRESTO_VERSION=0.287
24
26
 
@@ -39,8 +41,12 @@ COPY --from=presto-official /opt/presto-server/lib /opt/presto-server/lib
39
41
  COPY --from=presto-official /opt/presto-server/bin /opt/presto-server/bin
40
42
  COPY --from=presto-official /opt/presto-server/etc /opt/presto-server/etc
41
43
 
42
- # Copy only BigQuery plugin (the only connector we use for testing)
43
- COPY --from=presto-official /opt/presto-server/plugin/presto-bigquery /opt/presto-server/plugin/presto-bigquery
44
+ # Copy only the hive plugin (the only connector we use for testing)
45
+ COPY --from=presto-official /opt/presto-server/plugin/hive-hadoop2 /opt/presto-server/plugin/hive-hadoop2
46
+
47
+ # The CLI, which presto_start.sh uses to create the tables
48
+ COPY --from=presto-official /opt/presto-cli /opt/presto-cli
49
+ RUN ln -s /opt/presto-cli /usr/local/bin/presto-cli
44
50
 
45
51
  EXPOSE 8080
46
52
 
@@ -0,0 +1,8 @@
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.
4
+ connector.name=hive-hadoop2
5
+ hive.metastore=file
6
+ hive.metastore.catalog.dir=file:///tmp/hive-metastore
7
+ # Match parquet columns by name; presto's default is by position
8
+ hive.parquet.use-column-names=true
@@ -1,10 +1,19 @@
1
1
  #! /bin/bash
2
-
2
+ #
3
+ # Start a presto container whose hive catalog serves test/data/malloytest-parquet
4
+ # as the malloytest schema. The parquet is read in place; nothing is loaded.
5
+ #
6
+ # presto_start.sh [--slim] use the slim image from Dockerfile.slim
7
+ #
3
8
  set -e
4
9
 
5
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ SCRIPTDIR=$(cd $(dirname $0); pwd)
11
+ ROOTDIR=$(cd "$SCRIPTDIR/../.."; pwd)
12
+ DATADIR=$ROOTDIR/test/data/malloytest-parquet
6
13
  PRESTO_VERSION=0.287
7
14
  USE_SLIM=false
15
+ CONTAINER_NAME="presto-malloy"
16
+ PORT=${PRESTO_PORT:-8080}
8
17
 
9
18
  # Parse arguments
10
19
  for arg in "$@"; do
@@ -16,32 +25,40 @@ for arg in "$@"; do
16
25
  esac
17
26
  done
18
27
 
28
+ cd "$ROOTDIR"
19
29
  rm -rf .tmp
20
30
  mkdir .tmp
21
31
 
22
- # generate config file
23
- > ./.tmp/bigquery-pesto.properties
24
- cat << EOF > ./.tmp/bigquery-pesto.properties
25
- connector.name=bigquery
26
- bigquery.project-id=advance-lacing-417917
27
- bigquery.credentials-key=$BQ_CREDENTIALS_KEY
28
- EOF
29
-
30
32
  # Select image
31
33
  if [ "$USE_SLIM" = true ]; then
32
34
  PRESTO_IMAGE="presto-slim:${PRESTO_VERSION}"
33
35
  # Build slim image if it doesn't exist
34
36
  if ! docker image inspect "$PRESTO_IMAGE" > /dev/null 2>&1; then
35
37
  echo "Building slim Presto image..."
36
- docker build -f "$SCRIPT_DIR/Dockerfile.slim" \
38
+ docker build -f "$SCRIPTDIR/Dockerfile.slim" \
37
39
  --build-arg PRESTO_VERSION="$PRESTO_VERSION" \
38
- -t "$PRESTO_IMAGE" "$SCRIPT_DIR"
40
+ -t "$PRESTO_IMAGE" "$SCRIPTDIR"
39
41
  fi
40
42
  else
41
43
  PRESTO_IMAGE="prestodb/presto:${PRESTO_VERSION}"
42
44
  fi
43
45
 
44
- CONTAINER_NAME="presto-malloy"
46
+ # Only the log since this script started: a restarted container's old log
47
+ # already says SERVER STARTED.
48
+ SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
49
+
50
+ wait_for_presto() {
51
+ local counter=0
52
+ while ! docker logs --since "$SINCE" "$CONTAINER_NAME" 2>&1 | grep -q "SERVER STARTED"; do
53
+ counter=$((counter+1))
54
+ # give up after 2 minutes
55
+ if [ $counter -eq 120 ]; then
56
+ docker logs "$CONTAINER_NAME" >& ./.tmp/presto-malloy.logs
57
+ return 1
58
+ fi
59
+ sleep 1
60
+ done
61
+ }
45
62
 
46
63
  # Check for existing container
47
64
  if docker container inspect "$CONTAINER_NAME" > /dev/null 2>&1; then
@@ -51,31 +68,36 @@ if docker container inspect "$CONTAINER_NAME" > /dev/null 2>&1; then
51
68
  fi
52
69
  echo "Restarting existing $CONTAINER_NAME container..."
53
70
  docker start "$CONTAINER_NAME"
54
- while ! docker logs "$CONTAINER_NAME" 2>&1 | grep -q "SERVER STARTED"; do
55
- sleep 1
56
- done
57
- echo "Presto running on port 8080"
71
+ if ! wait_for_presto; then
72
+ echo "Presto did not restart successfully, check .tmp/presto-malloy.logs"
73
+ exit 1
74
+ fi
75
+ echo "Presto running on port $PORT"
58
76
  exit 0
59
77
  fi
60
78
 
61
- # run docker
62
- docker run -p ${PRESTO_PORT:-8080}:8080 -d -v ./.tmp/bigquery-pesto.properties:/opt/presto-server/etc/catalog/bigquery.properties --name "$CONTAINER_NAME" "$PRESTO_IMAGE"
63
-
64
- # wait for server to start
65
- counter=0
66
- while ! docker logs "$CONTAINER_NAME" 2>&1 | grep -q "SERVER STARTED"
67
- do
68
- sleep 1
69
- counter=$((counter+1))
70
- # if doesn't start after 2 minutes, output logs and kill process
71
- if [ $counter -eq 120 ]
72
- then
73
- docker logs "$CONTAINER_NAME" >& ./.tmp/presto-malloy.logs
74
- docker rm -f "$CONTAINER_NAME"
75
- echo "Presto did not start successfully, check .tmp/presto-malloy.logs"
76
- exit 1
77
- break
78
- fi
79
+ # A hive external_location is a directory, so each parquet file gets one.
80
+ MOUNTS=()
81
+ for parquet in "$DATADIR"/*.parquet; do
82
+ table=$(basename "$parquet" .parquet)
83
+ MOUNTS+=(-v "$parquet:/data/malloytest/$table/$table.parquet:ro")
79
84
  done
80
85
 
81
- echo "Presto running on port 8080"
86
+ docker run -p $PORT:8080 -d "${MOUNTS[@]}" \
87
+ -v "$SCRIPTDIR/hive.properties:/opt/presto-server/etc/catalog/hive.properties:ro" \
88
+ --name "$CONTAINER_NAME" "$PRESTO_IMAGE"
89
+
90
+ # A container without its tables must not survive to be "already running"
91
+ trap 'docker rm -f "$CONTAINER_NAME" > /dev/null; echo "Presto setup failed, check .tmp/presto-malloy.logs"' ERR
92
+
93
+ if ! wait_for_presto; then
94
+ false
95
+ fi
96
+
97
+ echo "Creating the malloytest tables"
98
+ npx ts-node test/trino/hive_ddl.ts hive file:///data/malloytest --presto > .tmp/hive_ddl.sql
99
+ docker cp .tmp/hive_ddl.sql "$CONTAINER_NAME":/tmp/hive_ddl.sql
100
+ docker exec "$CONTAINER_NAME" presto-cli -f /tmp/hive_ddl.sql
101
+ trap - ERR
102
+
103
+ echo "Presto running on port $PORT"
@@ -35,6 +35,33 @@ describe('loadRestrictedQuery — end-to-end through the public API', () => {
35
35
  expect(result.data.path(0, 'ct').value).toBeGreaterThan(0);
36
36
  });
37
37
 
38
+ test('timezone injection is rejected before SQL generation or execution', async () => {
39
+ const model = runtime.loadModel(`
40
+ source: states is duckdb.table('malloytest.state_facts')
41
+ `);
42
+ const queryText = `run: states -> {
43
+ timezone: "UTC' || (SELECT 'UTC') || '"
44
+ select: d is now.day
45
+ limit: 1
46
+ }`;
47
+ const expectedError = {
48
+ problems: expect.arrayContaining([
49
+ expect.objectContaining({
50
+ code: 'invalid-timezone',
51
+ severity: 'error',
52
+ }),
53
+ ]),
54
+ };
55
+ // Separate query instances exercise each entry point without reusing
56
+ // the other entry point's cached compilation failure.
57
+ await expect(
58
+ model.loadRestrictedQuery(queryText).getSQL()
59
+ ).rejects.toMatchObject(expectedError);
60
+ await expect(
61
+ model.loadRestrictedQuery(queryText).run()
62
+ ).rejects.toMatchObject(expectedError);
63
+ });
64
+
38
65
  test('restricted query with `import` throws MalloyError tagged restricted-mode', async () => {
39
66
  const model = runtime.loadModel(`
40
67
  source: states is duckdb.table('malloytest.state_facts')
@@ -340,6 +340,28 @@ describe('tags in results', () => {
340
340
  });
341
341
  });
342
342
 
343
+ describe('property names inherited from Object.prototype', () => {
344
+ // A tag property name comes from source text, so `__proto__`, `constructor`
345
+ // and `toString` are ordinary names with no special meaning.
346
+ test('an annotation carries an inherited name through to the result', async () => {
347
+ const loaded = runtime.loadQuery(
348
+ `run: duckdb.sql("select 1 as num") -> {
349
+ select:
350
+ # __proto__=1 toString.nested=2 constructor
351
+ one is num
352
+ }`
353
+ );
354
+ const result = await loaded.run();
355
+ const one = result.resultExplore.getFieldByName('one');
356
+ const {tag, log} = one.tagParse();
357
+ expect(log).toEqual([]);
358
+ expect(tag.numeric('__proto__')).toEqual(1);
359
+ expect(tag.numeric('toString', 'nested')).toEqual(2);
360
+ expect(tag.has('constructor')).toBe(true);
361
+ expect(tag.has('valueOf')).toBe(false);
362
+ });
363
+ });
364
+
343
365
  afterAll(async () => {
344
366
  await runtime.connection.close();
345
367
  });
@@ -0,0 +1,253 @@
1
+ /*
2
+ * Copyright Contributors to the Malloy project
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import {DuckDBConnection} from '@malloydata/db-duckdb';
7
+ import {
8
+ FixedConnectionMap,
9
+ InMemoryURLReader,
10
+ Malloy,
11
+ MalloyConfig,
12
+ Runtime,
13
+ } from '@malloydata/malloy';
14
+ import type {VirtualMap} from '@malloydata/malloy';
15
+
16
+ const virtualSource = `
17
+ ##! experimental.virtual_source
18
+ type: row_fields is { x :: number }
19
+ source: data is duckdb.virtual('data')::row_fields
20
+ `;
21
+ const sqlSource = `
22
+ source: wrapped is duckdb.sql("""
23
+ SELECT x + 10 AS y FROM %{ data -> { select: x } }
24
+ """)
25
+ run: wrapped -> { select: y }
26
+ `;
27
+ const source = virtualSource + sqlSource;
28
+
29
+ function virtualMap(table: string): VirtualMap {
30
+ return new Map([['duckdb', new Map([['data', table]])]]);
31
+ }
32
+
33
+ // Language cases live in packages/malloy/src/lang/test/virtual-sql.spec.ts.
34
+ // These tests cover API option forwarding and real SQL schemas.
35
+ describe('virtual sources in SQL blocks through the Foundation API', () => {
36
+ const connection = new DuckDBConnection({
37
+ name: 'duckdb',
38
+ databasePath: ':memory:',
39
+ });
40
+ const connections = FixedConnectionMap.fromArray([connection]);
41
+ const compileMap = virtualMap('translation_data');
42
+ const executionMap = virtualMap('execution_data');
43
+ const urlReader = new InMemoryURLReader(new Map());
44
+
45
+ beforeAll(async () => {
46
+ await connection.runSQL(
47
+ 'CREATE TEMP TABLE translation_data AS SELECT 1::INTEGER AS x'
48
+ );
49
+ await connection.runSQL(
50
+ 'CREATE TEMP TABLE execution_data AS SELECT 2::INTEGER AS x'
51
+ );
52
+ });
53
+
54
+ afterAll(async () => {
55
+ await connection.close();
56
+ });
57
+
58
+ afterEach(() => {
59
+ jest.restoreAllMocks();
60
+ });
61
+
62
+ test('Malloy.compile uses a translation map without binding execution', async () => {
63
+ const model = await Malloy.compile({
64
+ source,
65
+ connections,
66
+ urlReader,
67
+ virtualMap: compileMap,
68
+ });
69
+ const result = model.preparedQuery.getPreparedResult({
70
+ virtualMap: executionMap,
71
+ });
72
+ expect(result.sql).toContain('execution_data');
73
+ expect(result.sql).not.toContain('translation_data');
74
+ expect((await connection.runSQL(result.sql)).rows).toEqual([{y: 12}]);
75
+ expect(() => model.preparedQuery.getPreparedResult()).toThrow(
76
+ /No virtual-map entry/
77
+ );
78
+ });
79
+
80
+ test('loadModel forwards its map and execution can override it', async () => {
81
+ const runtime = new Runtime({connections});
82
+ runtime.virtualMap = virtualMap('missing_runtime_table');
83
+ const result = await runtime
84
+ .loadModel(source, {virtualMap: compileMap})
85
+ .loadFinalQuery()
86
+ .run({virtualMap: executionMap});
87
+ expect(result.data.value).toEqual([{y: 12}]);
88
+ });
89
+
90
+ test('runtime config supplies the translation map', async () => {
91
+ const config = new MalloyConfig({
92
+ virtualMap: {duckdb: {data: 'translation_data'}},
93
+ });
94
+ const runtime = new Runtime({connections, config});
95
+ const result = await runtime.loadQuery(source).run();
96
+ expect(result.data.value).toEqual([{y: 11}]);
97
+ });
98
+
99
+ test('model.loadQuery uses an explicit translation map', async () => {
100
+ const runtime = new Runtime({connections});
101
+ runtime.virtualMap = virtualMap('missing_runtime_table');
102
+ const result = await runtime
103
+ .loadModel(virtualSource)
104
+ .loadQuery(sqlSource, {virtualMap: compileMap})
105
+ .run({virtualMap: executionMap});
106
+ expect(result.data.value).toEqual([{y: 12}]);
107
+ });
108
+
109
+ test('model.loadQuery falls back to the runtime translation map', async () => {
110
+ const runtime = new Runtime({connections});
111
+ runtime.virtualMap = compileMap;
112
+ const result = await runtime
113
+ .loadModel(virtualSource)
114
+ .loadQuery(sqlSource)
115
+ .run({virtualMap: executionMap});
116
+ expect(result.data.value).toEqual([{y: 12}]);
117
+ });
118
+
119
+ test('model.loadQuery inherits the model translation map', async () => {
120
+ const runtime = new Runtime({connections});
121
+ runtime.virtualMap = virtualMap('missing_runtime_table');
122
+ const result = await runtime
123
+ .loadModel(virtualSource, {virtualMap: compileMap})
124
+ .loadQuery(sqlSource)
125
+ .run({virtualMap: executionMap});
126
+ expect(result.data.value).toEqual([{y: 12}]);
127
+ });
128
+
129
+ test('an empty translation map overrides the runtime map', async () => {
130
+ const runtime = new Runtime({connections});
131
+ runtime.virtualMap = compileMap;
132
+ await expect(
133
+ runtime.loadModel(source, {virtualMap: new Map()}).getModel()
134
+ ).rejects.toThrow(/No virtual-map entry/);
135
+ });
136
+
137
+ test('an invalid translation binding fails before SQL schema discovery', async () => {
138
+ const fetchSchema = jest
139
+ .spyOn(connection, 'fetchSchemaForSQLStruct')
140
+ .mockRejectedValue(new Error('Unexpected SQL schema request'));
141
+ await expect(
142
+ Malloy.compile({
143
+ source,
144
+ connections,
145
+ urlReader,
146
+ virtualMap: virtualMap('translation_data; SELECT 1'),
147
+ })
148
+ ).rejects.toThrow(/virtualMap entry 'duckdb.data'.*duckdb/);
149
+ expect(fetchSchema).not.toHaveBeenCalled();
150
+ });
151
+
152
+ test('an invalid execution binding fails before query execution', async () => {
153
+ const runtime = new Runtime({connections});
154
+ const query = runtime.loadQuery(source, {virtualMap: compileMap});
155
+ await query.getPreparedQuery();
156
+ const runSQL = jest
157
+ .spyOn(connection, 'runSQL')
158
+ .mockRejectedValue(new Error('Unexpected query execution'));
159
+ await expect(
160
+ query.run({virtualMap: virtualMap('execution_data; SELECT 1')})
161
+ ).rejects.toThrow(/virtualMap entry 'duckdb.data'.*duckdb/);
162
+ expect(runSQL).not.toHaveBeenCalled();
163
+ });
164
+
165
+ test.each(['explicit', 'inherited', 'runtime'])(
166
+ 'extendModel uses its %s translation map',
167
+ async binding => {
168
+ const runtime = new Runtime({connections});
169
+ runtime.virtualMap =
170
+ binding === 'runtime' ? compileMap : virtualMap('missing_table');
171
+ const model = runtime.loadModel(virtualSource, {
172
+ virtualMap:
173
+ binding === 'inherited'
174
+ ? compileMap
175
+ : binding === 'explicit'
176
+ ? virtualMap('missing_inherited_table')
177
+ : undefined,
178
+ });
179
+ const extended = model.extendModel(
180
+ sqlSource,
181
+ binding === 'explicit' ? {virtualMap: compileMap} : undefined
182
+ );
183
+ const result = await extended.loadFinalQuery().run();
184
+ expect(result.data.value).toEqual([{y: 11}]);
185
+ }
186
+ );
187
+
188
+ test('Malloy.compile forwards its map to a pre-parsed translator', async () => {
189
+ const parse = Malloy.parse({source});
190
+ const model = await Malloy.compile({
191
+ parse,
192
+ connections,
193
+ urlReader,
194
+ virtualMap: compileMap,
195
+ });
196
+ const result = model.preparedQuery.getPreparedResult({
197
+ virtualMap: executionMap,
198
+ });
199
+ expect((await connection.runSQL(result.sql)).rows).toEqual([{y: 12}]);
200
+ });
201
+
202
+ test.each(['compile', 'run'])(
203
+ '%s ignores an invalid override binding when it is unused',
204
+ async operation => {
205
+ const badMap = virtualMap('translation_data');
206
+ badMap.get('duckdb')!.set('unused', 'translation_data; SELECT 1');
207
+ const runtime = new Runtime({connections});
208
+ const result =
209
+ operation === 'compile'
210
+ ? Malloy.compile({source, connections, urlReader, virtualMap: badMap})
211
+ : runtime
212
+ .loadQuery(source, {virtualMap: compileMap})
213
+ .run({virtualMap: badMap});
214
+ await expect(result).resolves.toBeDefined();
215
+ }
216
+ );
217
+
218
+ test.each(['config', 'assignment'])(
219
+ 'bindings from %s are preserved and checked only when used',
220
+ async kind => {
221
+ const badTable = 'translation_data; SELECT 1';
222
+ const config =
223
+ kind === 'config'
224
+ ? new MalloyConfig({
225
+ virtualMap: {
226
+ duckdb: {data: 'translation_data', unused: badTable},
227
+ },
228
+ })
229
+ : undefined;
230
+ const runtime = new Runtime({connections, config});
231
+ if (kind === 'assignment') {
232
+ const map = virtualMap('translation_data');
233
+ map.get('duckdb')!.set('unused', badTable);
234
+ runtime.virtualMap = map;
235
+ }
236
+ expect(runtime.virtualMap?.get('duckdb')?.get('unused')).toBe(badTable);
237
+ expect((await runtime.loadQuery(source).run()).data.value).toEqual([
238
+ {y: 11},
239
+ ]);
240
+ await expect(
241
+ runtime
242
+ .loadQuery(
243
+ `
244
+ ${virtualSource}
245
+ source: bad is duckdb.virtual('unused')::row_fields
246
+ run: bad -> { select: x }
247
+ `
248
+ )
249
+ .getSQL()
250
+ ).rejects.toThrow(/virtualMap entry 'duckdb.unused'.*duckdb/);
251
+ }
252
+ );
253
+ });