@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-dev241001233244",
25
- "@malloydata/db-duckdb": "^0.0.195-dev241001233244",
26
- "@malloydata/db-postgres": "^0.0.195-dev241001233244",
27
- "@malloydata/db-snowflake": "^0.0.195-dev241001233244",
28
- "@malloydata/db-trino": "^0.0.195-dev241001233244",
29
- "@malloydata/malloy": "^0.0.195-dev241001233244",
30
- "@malloydata/render": "^0.0.195-dev241001233244",
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-dev241001233244"
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
- try {
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
+ }