@malloydata/malloy-tests 0.0.195-dev241001233244 → 0.0.195-dev241003204819
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/package.json
CHANGED
|
@@ -21,13 +21,14 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@jest/globals": "^29.4.3",
|
|
24
|
-
"@malloydata/db-bigquery": "^0.0.195-
|
|
25
|
-
"@malloydata/db-duckdb": "^0.0.195-
|
|
26
|
-
"@malloydata/db-postgres": "^0.0.195-
|
|
27
|
-
"@malloydata/db-snowflake": "^0.0.195-
|
|
28
|
-
"@malloydata/db-trino": "^0.0.195-
|
|
29
|
-
"@malloydata/malloy": "^0.0.195-
|
|
30
|
-
"@malloydata/render": "^0.0.195-
|
|
24
|
+
"@malloydata/db-bigquery": "^0.0.195-dev241003204819",
|
|
25
|
+
"@malloydata/db-duckdb": "^0.0.195-dev241003204819",
|
|
26
|
+
"@malloydata/db-postgres": "^0.0.195-dev241003204819",
|
|
27
|
+
"@malloydata/db-snowflake": "^0.0.195-dev241003204819",
|
|
28
|
+
"@malloydata/db-trino": "^0.0.195-dev241003204819",
|
|
29
|
+
"@malloydata/malloy": "^0.0.195-dev241003204819",
|
|
30
|
+
"@malloydata/render": "^0.0.195-dev241003204819",
|
|
31
|
+
"events": "^3.3.0",
|
|
31
32
|
"jsdom": "^22.1.0",
|
|
32
33
|
"luxon": "^2.4.0",
|
|
33
34
|
"madge": "^6.0.0"
|
|
@@ -36,5 +37,5 @@
|
|
|
36
37
|
"@types/jsdom": "^21.1.1",
|
|
37
38
|
"@types/luxon": "^2.4.0"
|
|
38
39
|
},
|
|
39
|
-
"version": "0.0.195-
|
|
40
|
+
"version": "0.0.195-dev241003204819"
|
|
40
41
|
}
|
|
@@ -52,12 +52,7 @@ async function runQuery(model: malloy.ModelMaterializer, query: string) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async function bqCompile(sql: string): Promise<boolean> {
|
|
55
|
-
|
|
56
|
-
await bq.executeSQLRaw(`WITH test AS(\n${sql}) SELECT 1 as one`);
|
|
57
|
-
} catch (e) {
|
|
58
|
-
malloy.Malloy.log.error(`SQL: didn't compile\n=============\n${sql}`);
|
|
59
|
-
throw e;
|
|
60
|
-
}
|
|
55
|
+
await bq.executeSQLRaw(`WITH test AS(\n${sql}) SELECT 1 as one`);
|
|
61
56
|
return true;
|
|
62
57
|
}
|
|
63
58
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {runtimeFor} from './runtimes';
|
|
9
|
+
import './util/db-jest-matchers';
|
|
10
|
+
|
|
11
|
+
const runtime = runtimeFor('duckdb');
|
|
12
|
+
|
|
13
|
+
const envDatabases = (
|
|
14
|
+
process.env['MALLOY_DATABASES'] ||
|
|
15
|
+
process.env['MALLOY_DATABASE'] ||
|
|
16
|
+
'duckdb'
|
|
17
|
+
).split(',');
|
|
18
|
+
|
|
19
|
+
let describe = globalThis.describe;
|
|
20
|
+
if (!envDatabases.includes('duckdb')) {
|
|
21
|
+
describe = describe.skip;
|
|
22
|
+
describe.skip = describe;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('emits events', () => {
|
|
26
|
+
describe('for parameters', () => {
|
|
27
|
+
test('argument compiled event is emitted', async () => {
|
|
28
|
+
await expect(`
|
|
29
|
+
##! experimental.parameters
|
|
30
|
+
source: s(x::string) is duckdb.table('malloytest.state_facts') extend {
|
|
31
|
+
where: x = 'CA'
|
|
32
|
+
}
|
|
33
|
+
run: s(x is "foo") -> { select: * }
|
|
34
|
+
`).toEmitDuringCompile(runtime, {
|
|
35
|
+
id: 'source-argument-compiled',
|
|
36
|
+
data: {name: 'x'},
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
test('parameterized source compiled event is emitted when source is used', async () => {
|
|
40
|
+
await expect(`
|
|
41
|
+
##! experimental.parameters
|
|
42
|
+
source: s(x::string) is duckdb.table('malloytest.state_facts') extend {
|
|
43
|
+
where: x = 'CA'
|
|
44
|
+
}
|
|
45
|
+
run: s(x is "foo") -> { select: * }
|
|
46
|
+
`).toEmitDuringCompile(runtime, {
|
|
47
|
+
id: 'parameterized-source-compiled',
|
|
48
|
+
data: {parameters: {x: {type: 'string'}}},
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
test('parameterized source compiled event is not emitted when source is not used', async () => {
|
|
52
|
+
await expect(`
|
|
53
|
+
##! experimental.parameters
|
|
54
|
+
source: a(used::string) is duckdb.table('malloytest.state_facts') extend {
|
|
55
|
+
where: used = 'CA'
|
|
56
|
+
}
|
|
57
|
+
source: b(unused::string) is duckdb.table('malloytest.state_facts') extend {
|
|
58
|
+
where: unused = 'CA'
|
|
59
|
+
}
|
|
60
|
+
run: a(used is "foo") -> { select: * }
|
|
61
|
+
`).toEmitDuringCompile(runtime, {
|
|
62
|
+
id: 'parameterized-source-compiled',
|
|
63
|
+
data: {parameters: {used: {type: 'string'}}},
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
test('parameterized source compiled event is emitted when join is used', async () => {
|
|
67
|
+
await expect(`
|
|
68
|
+
##! experimental.parameters
|
|
69
|
+
source: s0(x::string) is duckdb.table('malloytest.state_facts') extend {
|
|
70
|
+
where: x = 'CA'
|
|
71
|
+
}
|
|
72
|
+
source: s1 is duckdb.table('malloytest.state_facts') extend {
|
|
73
|
+
join_one: s0(x is "foo") on 1 = 1
|
|
74
|
+
}
|
|
75
|
+
run: s1 -> { select: s0.state }
|
|
76
|
+
`).toEmitDuringCompile(runtime, {
|
|
77
|
+
id: 'parameterized-source-compiled',
|
|
78
|
+
data: {parameters: {x: {type: 'string'}}},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('for joins', () => {
|
|
83
|
+
test('join usage is emitted', async () => {
|
|
84
|
+
await expect(`
|
|
85
|
+
##! experimental.parameters
|
|
86
|
+
source: s0(x::string) is duckdb.table('malloytest.state_facts') extend {
|
|
87
|
+
where: x = 'CA'
|
|
88
|
+
}
|
|
89
|
+
source: s1 is duckdb.table('malloytest.state_facts') extend {
|
|
90
|
+
join_one: s0(x is "foo") on 1 = 1
|
|
91
|
+
join_one: s0_copy is s0(x is "bar") on 1 = 1
|
|
92
|
+
}
|
|
93
|
+
run: s1 -> {
|
|
94
|
+
select: s0_copy.state
|
|
95
|
+
select: foo is s0_copy.state // only should be emitted once
|
|
96
|
+
}
|
|
97
|
+
`).toEmitDuringCompile(runtime, {
|
|
98
|
+
id: 'join-used',
|
|
99
|
+
data: {name: 's0_copy'},
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('for errors', () => {
|
|
104
|
+
test('translator errors are emitted', async () => {
|
|
105
|
+
await expect(`
|
|
106
|
+
source: s1 is duckdb.table('malloytest.state_facts') extend {
|
|
107
|
+
dimension: foo is pick "foo" when 1 = 1 else 2
|
|
108
|
+
}
|
|
109
|
+
run: s1 -> { select: foo }
|
|
110
|
+
`).toEmitDuringTranslation(runtime, {
|
|
111
|
+
id: 'translation-error',
|
|
112
|
+
data: {
|
|
113
|
+
code: 'pick-else-type-does-not-match',
|
|
114
|
+
data: {
|
|
115
|
+
elseType: 'number',
|
|
116
|
+
returnType: 'string',
|
|
117
|
+
},
|
|
118
|
+
message: 'else type `number` does not match return type `string`',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
package/src/runtimes.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {PooledPostgresConnection} from '@malloydata/db-postgres';
|
|
|
38
38
|
import {TrinoConnection, TrinoExecutor} from '@malloydata/db-trino';
|
|
39
39
|
import {SnowflakeExecutor} from '@malloydata/db-snowflake/src/snowflake_executor';
|
|
40
40
|
import {PrestoConnection} from '@malloydata/db-trino/src/trino_connection';
|
|
41
|
+
import {EventEmitter} from 'events';
|
|
41
42
|
|
|
42
43
|
export class SnowflakeTestConnection extends SnowflakeConnection {
|
|
43
44
|
public async runSQL(
|
|
@@ -193,7 +194,7 @@ export function runtimeFor(dbName: string): SingleConnectionRuntime {
|
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
export function testRuntimeFor(connection: Connection) {
|
|
196
|
-
return new SingleConnectionRuntime(files, connection);
|
|
197
|
+
return new SingleConnectionRuntime(files, connection, new EventEmitter());
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
/**
|
|
@@ -31,12 +31,19 @@ import {
|
|
|
31
31
|
LogMessage,
|
|
32
32
|
SingleConnectionRuntime,
|
|
33
33
|
} from '@malloydata/malloy';
|
|
34
|
+
import EventEmitter from 'events';
|
|
34
35
|
import {inspect} from 'util';
|
|
35
36
|
|
|
36
37
|
type ExpectedResultRow = Record<string, unknown>;
|
|
37
38
|
type ExpectedResult = ExpectedResultRow | ExpectedResultRow[];
|
|
38
39
|
type Runner = Runtime | ModelMaterializer;
|
|
39
40
|
|
|
41
|
+
interface ExpectedEvent {
|
|
42
|
+
id: string;
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
data: any;
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
declare global {
|
|
41
48
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
42
49
|
namespace jest {
|
|
@@ -72,6 +79,14 @@ declare global {
|
|
|
72
79
|
runtime: Runner,
|
|
73
80
|
matchVals: ExpectedResult
|
|
74
81
|
): Promise<R>;
|
|
82
|
+
toEmitDuringCompile(
|
|
83
|
+
runtime: Runtime,
|
|
84
|
+
...events: ExpectedEvent[]
|
|
85
|
+
): Promise<R>;
|
|
86
|
+
toEmitDuringTranslation(
|
|
87
|
+
runtime: Runtime,
|
|
88
|
+
...events: ExpectedEvent[]
|
|
89
|
+
): Promise<R>;
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
}
|
|
@@ -130,6 +145,7 @@ expect.extend({
|
|
|
130
145
|
|
|
131
146
|
const queryTags = (await query.getPreparedQuery()).tagParse().tag;
|
|
132
147
|
const queryTestTag = queryTags.tag('test');
|
|
148
|
+
|
|
133
149
|
let result: Result;
|
|
134
150
|
try {
|
|
135
151
|
result = await query.run();
|
|
@@ -217,8 +233,82 @@ expect.extend({
|
|
|
217
233
|
message: () => 'All rows matched expected results',
|
|
218
234
|
};
|
|
219
235
|
},
|
|
236
|
+
async toEmitDuringCompile(
|
|
237
|
+
querySrc: string,
|
|
238
|
+
runtime: Runtime,
|
|
239
|
+
...expectedEvents: ExpectedEvent[]
|
|
240
|
+
) {
|
|
241
|
+
return toEmit(this, querySrc, 'compile', runtime, ...expectedEvents);
|
|
242
|
+
},
|
|
243
|
+
async toEmitDuringTranslation(
|
|
244
|
+
querySrc: string,
|
|
245
|
+
runtime: Runtime,
|
|
246
|
+
...expectedEvents: ExpectedEvent[]
|
|
247
|
+
) {
|
|
248
|
+
return toEmit(this, querySrc, 'translate', runtime, ...expectedEvents);
|
|
249
|
+
},
|
|
220
250
|
});
|
|
221
251
|
|
|
252
|
+
async function toEmit(
|
|
253
|
+
context: jest.MatcherContext,
|
|
254
|
+
querySrc: string,
|
|
255
|
+
when: 'compile' | 'translate',
|
|
256
|
+
runtime: Runtime,
|
|
257
|
+
...expectedEvents: ExpectedEvent[]
|
|
258
|
+
) {
|
|
259
|
+
const eventStream = runtime.eventStream;
|
|
260
|
+
if (eventStream === undefined) {
|
|
261
|
+
return {
|
|
262
|
+
pass: false,
|
|
263
|
+
message: () => 'No event stream found',
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (!(eventStream instanceof EventEmitter)) {
|
|
267
|
+
return {
|
|
268
|
+
pass: false,
|
|
269
|
+
message: () => 'Event stream is not an EventEmitter',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const gotEvents: ExpectedEvent[] = [];
|
|
273
|
+
const eventIdsWeCareAbout = new Set(expectedEvents.map(e => e.id));
|
|
274
|
+
for (const id of eventIdsWeCareAbout) {
|
|
275
|
+
eventStream.on(id, data => {
|
|
276
|
+
gotEvents.push({id, data});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const model = runtime.loadModel(querySrc, {
|
|
280
|
+
noThrowOnError: when === 'translate',
|
|
281
|
+
});
|
|
282
|
+
if (when === 'compile') {
|
|
283
|
+
const query = model.loadFinalQuery();
|
|
284
|
+
await query.getPreparedResult();
|
|
285
|
+
} else {
|
|
286
|
+
await model.getModel();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let matching = gotEvents.length === expectedEvents.length;
|
|
290
|
+
if (matching) {
|
|
291
|
+
for (let i = 0; i < expectedEvents.length; i++) {
|
|
292
|
+
const got = gotEvents[i];
|
|
293
|
+
const want = expectedEvents[i];
|
|
294
|
+
matching &&= objectsMatch(got, want);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!matching) {
|
|
299
|
+
return {
|
|
300
|
+
pass: false,
|
|
301
|
+
message: () =>
|
|
302
|
+
`Expected events ${context.utils.diff(expectedEvents, gotEvents)}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
pass: true,
|
|
308
|
+
message: () => 'All rows matched expected results',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
222
312
|
function errorLogToString(src: string, msgs: LogMessage[]) {
|
|
223
313
|
let lovely = '';
|
|
224
314
|
let lineNo = 0;
|
|
@@ -240,3 +330,44 @@ function errorLogToString(src: string, msgs: LogMessage[]) {
|
|
|
240
330
|
function humanReadable(thing: unknown): string {
|
|
241
331
|
return inspect(thing, {breakLength: 72, depth: Infinity});
|
|
242
332
|
}
|
|
333
|
+
|
|
334
|
+
// b is "expected"
|
|
335
|
+
// a is "actual"
|
|
336
|
+
// If expected is an object, all of the keys should also
|
|
337
|
+
// match, buy the expected is allowed to have other keys that are not matched
|
|
338
|
+
function objectsMatch(a: unknown, b: unknown): boolean {
|
|
339
|
+
if (
|
|
340
|
+
typeof b === 'string' ||
|
|
341
|
+
typeof b === 'number' ||
|
|
342
|
+
typeof b === 'boolean' ||
|
|
343
|
+
typeof b === 'bigint' ||
|
|
344
|
+
b === undefined ||
|
|
345
|
+
b === null
|
|
346
|
+
) {
|
|
347
|
+
return b === a;
|
|
348
|
+
} else if (Array.isArray(b)) {
|
|
349
|
+
if (Array.isArray(a)) {
|
|
350
|
+
return a.length === b.length && a.every((v, i) => objectsMatch(v, b[i]));
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
} else {
|
|
354
|
+
if (
|
|
355
|
+
typeof a === 'string' ||
|
|
356
|
+
typeof a === 'number' ||
|
|
357
|
+
typeof a === 'boolean' ||
|
|
358
|
+
typeof a === 'bigint' ||
|
|
359
|
+
a === undefined ||
|
|
360
|
+
a === null
|
|
361
|
+
) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
if (Array.isArray(a)) return false;
|
|
365
|
+
const keys = Object.keys(b);
|
|
366
|
+
for (const key of keys) {
|
|
367
|
+
if (!objectsMatch(a[key], b[key])) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
}
|