@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.
- package/CONTEXT.md +14 -5
- package/data/parquet_loader.ts +106 -0
- package/duckdb/load_test_data.ts +15 -48
- package/mssql/connection_string.ts +12 -0
- package/mssql/load_test_data.sh +12 -0
- package/mssql/load_test_data.ts +63 -0
- package/mssql/mssql_start.sh +13 -132
- package/mysql/load_test_data.sh +12 -0
- package/mysql/load_test_data.ts +39 -0
- package/mysql/mysql_start.sh +36 -30
- package/package.json +10 -10
- package/postgres/load_test_data.sh +12 -0
- package/postgres/load_test_data.ts +34 -0
- package/postgres/postgres_start.sh +2 -6
- package/presto/Dockerfile.slim +12 -6
- package/presto/hive.properties +8 -0
- package/presto/presto_start.sh +58 -36
- package/src/core/restricted-mode.spec.ts +27 -0
- package/src/core/tags.spec.ts +22 -0
- package/src/core/virtual-sql.spec.ts +253 -0
- package/src/databases/all/expr.spec.ts +150 -18
- package/src/databases/all/nest.spec.ts +237 -0
- package/src/databases/all/nomodel.spec.ts +0 -75
- package/src/databases/all/problems.spec.ts +32 -0
- package/src/runtimes.ts +1 -0
- package/trino/hive.properties +11 -0
- package/trino/hive_ddl.ts +148 -0
- package/trino/trino_start.sh +54 -38
- package/tsconfig.json +12 -1
- package/bin/dump_malloytest.sh +0 -1
- package/bin/postgres_init.sh +0 -10
- package/bin/postgres_start.sh +0 -2
- package/bin/postgres_stop.sh +0 -2
- package/data/mysql/malloytest.mysql.gz +0 -0
- package/data/postgres/malloytest-postgres.sql.gz +0 -0
- package/data/postgres/state_facts.sql +0 -366
|
@@ -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
|
|
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
|
-
|
|
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"
|
package/presto/Dockerfile.slim
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Slim Presto image for Malloy CI testing
|
|
2
2
|
#
|
|
3
|
-
# This image is ~
|
|
4
|
-
# It includes only what's needed to run Presto with the
|
|
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
|
|
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: ~
|
|
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
|
|
43
|
-
COPY --from=presto-official /opt/presto-server/plugin/
|
|
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
|
package/presto/presto_start.sh
CHANGED
|
@@ -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
|
-
|
|
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 "$
|
|
38
|
+
docker build -f "$SCRIPTDIR/Dockerfile.slim" \
|
|
37
39
|
--build-arg PRESTO_VERSION="$PRESTO_VERSION" \
|
|
38
|
-
-t "$PRESTO_IMAGE" "$
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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')
|
package/src/core/tags.spec.ts
CHANGED
|
@@ -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
|
+
});
|