@malloydata/malloy-tests 0.0.261-dev250410215229 → 0.0.261-dev250410224049
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,14 +21,14 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@jest/globals": "^29.4.3",
|
|
24
|
-
"@malloydata/db-bigquery": "^0.0.261-
|
|
25
|
-
"@malloydata/db-duckdb": "^0.0.261-
|
|
26
|
-
"@malloydata/db-postgres": "^0.0.261-
|
|
27
|
-
"@malloydata/db-snowflake": "^0.0.261-
|
|
28
|
-
"@malloydata/db-trino": "^0.0.261-
|
|
29
|
-
"@malloydata/malloy": "^0.0.261-
|
|
30
|
-
"@malloydata/malloy-tag": "^0.0.261-
|
|
31
|
-
"@malloydata/render": "^0.0.261-
|
|
24
|
+
"@malloydata/db-bigquery": "^0.0.261-dev250410224049",
|
|
25
|
+
"@malloydata/db-duckdb": "^0.0.261-dev250410224049",
|
|
26
|
+
"@malloydata/db-postgres": "^0.0.261-dev250410224049",
|
|
27
|
+
"@malloydata/db-snowflake": "^0.0.261-dev250410224049",
|
|
28
|
+
"@malloydata/db-trino": "^0.0.261-dev250410224049",
|
|
29
|
+
"@malloydata/malloy": "^0.0.261-dev250410224049",
|
|
30
|
+
"@malloydata/malloy-tag": "^0.0.261-dev250410224049",
|
|
31
|
+
"@malloydata/render": "^0.0.261-dev250410224049",
|
|
32
32
|
"events": "^3.3.0",
|
|
33
33
|
"jsdom": "^22.1.0",
|
|
34
34
|
"luxon": "^2.4.0",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"@types/jsdom": "^21.1.1",
|
|
39
39
|
"@types/luxon": "^2.4.0"
|
|
40
40
|
},
|
|
41
|
-
"version": "0.0.261-
|
|
41
|
+
"version": "0.0.261-dev250410224049"
|
|
42
42
|
}
|
|
@@ -41,6 +41,23 @@ async function getError<T>(fn: () => Promise<T>) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
runtimes.runtimeMap.forEach((runtime, databaseName) => {
|
|
44
|
+
it(`properly quotes nested field names in ${databaseName}`, async () => {
|
|
45
|
+
const one = runtime.dialect.sqlMaybeQuoteIdentifier('one');
|
|
46
|
+
await expect(`
|
|
47
|
+
run: ${databaseName}.sql(""" SELECT 1 as ${one} """) -> {
|
|
48
|
+
nest: foo is {
|
|
49
|
+
group_by: one
|
|
50
|
+
aggregate: \`#\` is count(one)
|
|
51
|
+
nest: deepfoo is {
|
|
52
|
+
group_by: one
|
|
53
|
+
aggregate: \`#\` is count(one)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}`).matchesRows(runtime, {
|
|
57
|
+
foo: [{'one': 1, '#': 1, 'deepfoo': [{'one': 1, '#': 1}]}],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
44
61
|
describe('warnings', () => {
|
|
45
62
|
// NOTE: This test generates SQL errors on the console because of
|
|
46
63
|
// a hard-coded console.log() in the duckdb-wasm worker
|
|
@@ -22,7 +22,6 @@ describe.each(runtimes.runtimeList)(
|
|
|
22
22
|
throw new Error("Couldn't build runtime");
|
|
23
23
|
}
|
|
24
24
|
const presto = databaseName === 'presto';
|
|
25
|
-
|
|
26
25
|
it(`runs an sql query - ${databaseName}`, async () => {
|
|
27
26
|
await expect(
|
|
28
27
|
`run: ${databaseName}.sql("SELECT 1 as n") -> { select: n }`
|
|
@@ -22,21 +22,16 @@
|
|
|
22
22
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import type {
|
|
26
|
-
|
|
27
|
-
QueryMaterializer,
|
|
28
|
-
Result,
|
|
29
|
-
Runtime,
|
|
30
|
-
LogMessage,
|
|
31
|
-
} from '@malloydata/malloy';
|
|
32
|
-
import {MalloyError, SingleConnectionRuntime, API} from '@malloydata/malloy';
|
|
33
|
-
import type {Tag} from '@malloydata/malloy-tag';
|
|
25
|
+
import type {Result, Runtime} from '@malloydata/malloy';
|
|
26
|
+
import {SingleConnectionRuntime} from '@malloydata/malloy';
|
|
34
27
|
import EventEmitter from 'events';
|
|
35
28
|
import {inspect} from 'util';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
import type {
|
|
30
|
+
ExpectedResult,
|
|
31
|
+
ExpectedResultRow,
|
|
32
|
+
TestRunner,
|
|
33
|
+
} from './db-matcher-support';
|
|
34
|
+
import {runQuery} from './db-matcher-support';
|
|
40
35
|
|
|
41
36
|
interface ExpectedEvent {
|
|
42
37
|
id: string;
|
|
@@ -74,9 +69,45 @@ declare global {
|
|
|
74
69
|
* @param expected Key value pairs or array of key value pairs
|
|
75
70
|
*/
|
|
76
71
|
malloyResultMatches(
|
|
77
|
-
runtime:
|
|
72
|
+
runtime: TestRunner,
|
|
78
73
|
matchVals: ExpectedResult
|
|
79
74
|
): Promise<R>;
|
|
75
|
+
/**
|
|
76
|
+
* Jest matcher for running a Malloy query, checks that each row
|
|
77
|
+
* contains the values matching the template. The argument list
|
|
78
|
+
* at the end will be matched for each row of the query.
|
|
79
|
+
*
|
|
80
|
+
* To see if the first row of a query contains a field called num with a value of 7
|
|
81
|
+
* ( a "runtime" can be a Runtime, or a Model from load/extend of a Model)
|
|
82
|
+
*
|
|
83
|
+
* await expect('run: ...').matchesRows(runtime, {num: 7});
|
|
84
|
+
*
|
|
85
|
+
* To see if the first two rows of a query contains a field called num with a values 7 and 8
|
|
86
|
+
*
|
|
87
|
+
* await expect('run: ...').matchesRows(runtime {num: 7}, {num:8});
|
|
88
|
+
*
|
|
89
|
+
* Every symbol in the expect match must be in the row, however there can be columns in the row
|
|
90
|
+
* which are not in the match.
|
|
91
|
+
*
|
|
92
|
+
* mtoy todo maybe this should be "debug_query()" instead of a tag ... ?
|
|
93
|
+
* In addition, the query is checked for the tags, preceed your run statement with ...
|
|
94
|
+
*
|
|
95
|
+
* * test.debug -- Force test failure, and the result data will be printed
|
|
96
|
+
*
|
|
97
|
+
* @param matchVals ... list of row objects containing key-value pairs
|
|
98
|
+
*/
|
|
99
|
+
matchesRows(
|
|
100
|
+
runtime: TestRunner,
|
|
101
|
+
...matchVals: ExpectedResultRow[]
|
|
102
|
+
): Promise<R>;
|
|
103
|
+
/**
|
|
104
|
+
* Similar to matchesRows, argument is an array of rows, not a list of rows
|
|
105
|
+
* Output on a mismatch is a jest-diff
|
|
106
|
+
*/
|
|
107
|
+
matchesResult(
|
|
108
|
+
runtime: TestRunner,
|
|
109
|
+
matchVals: ExpectedResultRow[]
|
|
110
|
+
): Promise<R>;
|
|
80
111
|
toEmitDuringCompile(
|
|
81
112
|
runtime: Runtime,
|
|
82
113
|
...events: ExpectedEvent[]
|
|
@@ -114,7 +145,7 @@ expect.extend({
|
|
|
114
145
|
|
|
115
146
|
async malloyResultMatches(
|
|
116
147
|
querySrc: string,
|
|
117
|
-
runtime:
|
|
148
|
+
runtime: TestRunner,
|
|
118
149
|
shouldEqual: ExpectedResult
|
|
119
150
|
) {
|
|
120
151
|
// TODO -- THIS IS NOT OK BUT I AM NOT FIXING IT NOW
|
|
@@ -131,48 +162,15 @@ expect.extend({
|
|
|
131
162
|
}
|
|
132
163
|
}
|
|
133
164
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} catch (e) {
|
|
141
|
-
return {
|
|
142
|
-
pass: false,
|
|
143
|
-
message: () =>
|
|
144
|
-
`Could not prepare query to run: ${e.message}\nQuery:\n${querySrc}`,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
let result: Result;
|
|
149
|
-
try {
|
|
150
|
-
result = await query.run();
|
|
151
|
-
} catch (e) {
|
|
152
|
-
let failMsg = `query.run failed: ${e.message}\n`;
|
|
153
|
-
if (e instanceof MalloyError) {
|
|
154
|
-
failMsg = `Error in query compilation\n${errorLogToString(
|
|
155
|
-
querySrc,
|
|
156
|
-
e.problems
|
|
157
|
-
)}`;
|
|
158
|
-
} else {
|
|
159
|
-
try {
|
|
160
|
-
failMsg += `SQL: ${await query.getSQL()}\n`;
|
|
161
|
-
} catch (e2) {
|
|
162
|
-
// we could not show the SQL for unknown reasons
|
|
163
|
-
}
|
|
164
|
-
failMsg += e.stack;
|
|
165
|
-
}
|
|
166
|
-
return {pass: false, message: () => failMsg};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
API.util.wrapResult(result);
|
|
171
|
-
} catch (error) {
|
|
165
|
+
const {fail, result, queryTestTag, query} = await runQuery({
|
|
166
|
+
runner: runtime,
|
|
167
|
+
src: querySrc,
|
|
168
|
+
});
|
|
169
|
+
if (fail) return fail;
|
|
170
|
+
if (!result) {
|
|
172
171
|
return {
|
|
173
172
|
pass: false,
|
|
174
|
-
message: () =>
|
|
175
|
-
`Result could not be wrapped into new style result: ${error}\n${error.stack}`,
|
|
173
|
+
message: () => 'runQuery returned no results and no errors',
|
|
176
174
|
};
|
|
177
175
|
}
|
|
178
176
|
|
|
@@ -229,11 +227,13 @@ expect.extend({
|
|
|
229
227
|
}
|
|
230
228
|
|
|
231
229
|
if (fails.length > 0) {
|
|
232
|
-
const fromSQL = ' ' + (await query.getSQL()).split('\n').join('\n ');
|
|
233
230
|
if (debugFail && !failedTest) {
|
|
234
231
|
fails.push('\nTest forced failure (# test.debug)');
|
|
235
232
|
}
|
|
236
|
-
const
|
|
233
|
+
const fromSQL = query
|
|
234
|
+
? 'SQL Generated:\n ' + (await query.getSQL()).split('\n').join('\n ')
|
|
235
|
+
: 'SQL Missing';
|
|
236
|
+
const failMsg = `${fromSQL}\n${fails.join('\n')}`;
|
|
237
237
|
return {pass: false, message: () => failMsg};
|
|
238
238
|
}
|
|
239
239
|
|
|
@@ -242,6 +242,126 @@ expect.extend({
|
|
|
242
242
|
message: () => 'All rows matched expected results',
|
|
243
243
|
};
|
|
244
244
|
},
|
|
245
|
+
|
|
246
|
+
async matchesRows(
|
|
247
|
+
querySrc: string,
|
|
248
|
+
runtime: TestRunner,
|
|
249
|
+
...expected: ExpectedResultRow[]
|
|
250
|
+
) {
|
|
251
|
+
querySrc = querySrc.trimEnd().replace(/^\n*/, '');
|
|
252
|
+
const {fail, result, queryTestTag, query} = await runQuery({
|
|
253
|
+
runner: runtime,
|
|
254
|
+
src: querySrc,
|
|
255
|
+
});
|
|
256
|
+
if (fail) return fail;
|
|
257
|
+
if (!result) {
|
|
258
|
+
return {
|
|
259
|
+
pass: false,
|
|
260
|
+
message: () => 'runQuery returned no results and no errors',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const fails: string[] = [];
|
|
265
|
+
const got = result.data.toObject();
|
|
266
|
+
const expectStr = this.utils.EXPECTED_COLOR(humanReadable(expected));
|
|
267
|
+
|
|
268
|
+
if (!Array.isArray(got)) {
|
|
269
|
+
fails.push(`!!! Expected: ${expectStr}`);
|
|
270
|
+
fails.push(
|
|
271
|
+
`??? NonArray: ${this.utils.RECEIVED_COLOR(humanReadable(got))}`
|
|
272
|
+
);
|
|
273
|
+
} else {
|
|
274
|
+
// compare each row in the result to each row in the expectation
|
|
275
|
+
// This is more useful than a straight diff
|
|
276
|
+
const diffs: string[] = [];
|
|
277
|
+
let unMatched = false;
|
|
278
|
+
for (let expectNum = 0; expectNum < expected.length; expectNum += 1) {
|
|
279
|
+
const eStr = humanReadable(expected[expectNum]);
|
|
280
|
+
if (objectsMatch(got[expectNum], expected[expectNum])) {
|
|
281
|
+
diffs.push(` ${eStr}`);
|
|
282
|
+
} else {
|
|
283
|
+
diffs.push(this.utils.EXPECTED_COLOR(`<<< Expected: ${eStr}`));
|
|
284
|
+
diffs.push(
|
|
285
|
+
this.utils.RECEIVED_COLOR(
|
|
286
|
+
`>>> Received: ${humanReadable(got[expectNum])}`
|
|
287
|
+
)
|
|
288
|
+
);
|
|
289
|
+
unMatched = true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (unMatched) {
|
|
293
|
+
fails.push('ROWS:', ...diffs);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (queryTestTag?.has('debug') && fails.length === 0) {
|
|
298
|
+
fails.push(
|
|
299
|
+
`\n${this.utils.RECEIVED_COLOR('Test forced failure (# test.debug)')}`
|
|
300
|
+
);
|
|
301
|
+
fails.push(`Received: ${this.utils.EXPECTED_COLOR(humanReadable(got))}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (fails.length > 0) {
|
|
305
|
+
const fromSQL = query
|
|
306
|
+
? 'SQL Generated:\n ' + (await query.getSQL()).split('\n').join('\n ')
|
|
307
|
+
: 'SQL Missing';
|
|
308
|
+
const failMsg = `QUERY:\n${querySrc}\n\n${fromSQL}\n${fails.join('\n')}`;
|
|
309
|
+
return {pass: false, message: () => failMsg};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {pass: true, message: () => `Matched: ${expectStr}`};
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
async matchesResult(
|
|
316
|
+
querySrc: string,
|
|
317
|
+
runtime: TestRunner,
|
|
318
|
+
expected: ExpectedResultRow[]
|
|
319
|
+
) {
|
|
320
|
+
querySrc = querySrc.trimEnd();
|
|
321
|
+
const {fail, result, queryTestTag, query} = await runQuery({
|
|
322
|
+
runner: runtime,
|
|
323
|
+
src: querySrc,
|
|
324
|
+
});
|
|
325
|
+
if (fail) return fail;
|
|
326
|
+
if (!result) {
|
|
327
|
+
return {
|
|
328
|
+
pass: false,
|
|
329
|
+
message: () => 'runQuery returned no results and no errors',
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const fails: string[] = [];
|
|
334
|
+
const got = result.data.toObject();
|
|
335
|
+
const expectStr = humanReadable(expected);
|
|
336
|
+
|
|
337
|
+
if (!objectsMatch(got, expected)) {
|
|
338
|
+
fails.push(
|
|
339
|
+
'RESULT:',
|
|
340
|
+
this.utils.diff(expectStr, humanReadable(got)) || ''
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (queryTestTag?.has('debug') && fails.length === 0) {
|
|
345
|
+
fails.push(
|
|
346
|
+
`\n${this.utils.RECEIVED_COLOR('Test forced failure (# test.debug)')}`
|
|
347
|
+
);
|
|
348
|
+
fails.push(`Received: ${this.utils.EXPECTED_COLOR(humanReadable(got))}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (fails.length > 0) {
|
|
352
|
+
const fromSQL = query
|
|
353
|
+
? 'SQL Generated:\n ' + (await query.getSQL()).split('\n').join('\n ')
|
|
354
|
+
: 'SQL Missing';
|
|
355
|
+
const failMsg = `QUERY:\n${querySrc}\n${fromSQL}\n${fails.join('\n')}`;
|
|
356
|
+
return {pass: false, message: () => failMsg};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
pass: true,
|
|
361
|
+
message: () => `Matched: ${this.utils.EXPECTED_COLOR(expectStr)}`,
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
|
|
245
365
|
async toEmitDuringCompile(
|
|
246
366
|
querySrc: string,
|
|
247
367
|
runtime: Runtime,
|
|
@@ -318,45 +438,28 @@ async function toEmit(
|
|
|
318
438
|
};
|
|
319
439
|
}
|
|
320
440
|
|
|
321
|
-
function errorLogToString(src: string, msgs: LogMessage[]) {
|
|
322
|
-
let lovely = '';
|
|
323
|
-
let lineNo = 0;
|
|
324
|
-
for (const line of src.split('\n')) {
|
|
325
|
-
lovely += ` | ${line}\n`;
|
|
326
|
-
for (const entry of msgs) {
|
|
327
|
-
if (entry.at) {
|
|
328
|
-
if (entry.at.range.start.line === lineNo) {
|
|
329
|
-
const charFrom = entry.at.range.start.character;
|
|
330
|
-
lovely += `!!!!! ${' '.repeat(charFrom)}^ ${entry.message}\n`;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
lineNo += 1;
|
|
335
|
-
}
|
|
336
|
-
return lovely;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
441
|
function humanReadable(thing: unknown): string {
|
|
340
442
|
return inspect(thing, {breakLength: 72, depth: Infinity});
|
|
341
443
|
}
|
|
342
444
|
|
|
343
|
-
// b is "expected"
|
|
344
|
-
// a is "actual"
|
|
345
445
|
// If expected is an object, all of the keys should also match,
|
|
346
446
|
// but the expected is allowed to have other keys that are not matched
|
|
347
|
-
function objectsMatch(a: unknown,
|
|
447
|
+
function objectsMatch(a: unknown, mustHave: unknown): boolean {
|
|
348
448
|
if (
|
|
349
|
-
typeof
|
|
350
|
-
typeof
|
|
351
|
-
typeof
|
|
352
|
-
typeof
|
|
353
|
-
|
|
354
|
-
|
|
449
|
+
typeof mustHave === 'string' ||
|
|
450
|
+
typeof mustHave === 'number' ||
|
|
451
|
+
typeof mustHave === 'boolean' ||
|
|
452
|
+
typeof mustHave === 'bigint' ||
|
|
453
|
+
mustHave === undefined ||
|
|
454
|
+
mustHave === null
|
|
355
455
|
) {
|
|
356
|
-
return
|
|
357
|
-
} else if (Array.isArray(
|
|
456
|
+
return mustHave === a;
|
|
457
|
+
} else if (Array.isArray(mustHave)) {
|
|
358
458
|
if (Array.isArray(a)) {
|
|
359
|
-
return
|
|
459
|
+
return (
|
|
460
|
+
a.length === mustHave.length &&
|
|
461
|
+
a.every((v, i) => objectsMatch(v, mustHave[i]))
|
|
462
|
+
);
|
|
360
463
|
}
|
|
361
464
|
return false;
|
|
362
465
|
} else {
|
|
@@ -371,9 +474,9 @@ function objectsMatch(a: unknown, b: unknown): boolean {
|
|
|
371
474
|
return false;
|
|
372
475
|
}
|
|
373
476
|
if (Array.isArray(a)) return false;
|
|
374
|
-
const keys = Object.keys(
|
|
477
|
+
const keys = Object.keys(mustHave);
|
|
375
478
|
for (const key of keys) {
|
|
376
|
-
if (!objectsMatch(a[key],
|
|
479
|
+
if (!objectsMatch(a[key], mustHave[key])) {
|
|
377
480
|
return false;
|
|
378
481
|
}
|
|
379
482
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
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 type {
|
|
9
|
+
Result,
|
|
10
|
+
Runtime,
|
|
11
|
+
ModelMaterializer,
|
|
12
|
+
QueryMaterializer,
|
|
13
|
+
LogMessage,
|
|
14
|
+
Dialect,
|
|
15
|
+
} from '@malloydata/malloy';
|
|
16
|
+
import {API, MalloyError} from '@malloydata/malloy';
|
|
17
|
+
import type {Tag} from '@malloydata/malloy-tag';
|
|
18
|
+
|
|
19
|
+
type JestMatcherResult = {
|
|
20
|
+
pass: boolean;
|
|
21
|
+
message: () => string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ExpectedResultRow = Record<string, unknown>;
|
|
25
|
+
export type ExpectedResult = ExpectedResultRow | ExpectedResultRow[];
|
|
26
|
+
export type TestRunner = Runtime | ModelMaterializer;
|
|
27
|
+
|
|
28
|
+
export interface TestQuery {
|
|
29
|
+
runner: TestRunner;
|
|
30
|
+
src: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function query(runner: TestRunner, querySource: string): TestQuery {
|
|
34
|
+
return {runner, src: querySource};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface QueryRunResult {
|
|
38
|
+
fail: JestMatcherResult;
|
|
39
|
+
result: Result;
|
|
40
|
+
query: QueryMaterializer;
|
|
41
|
+
queryTestTag: Tag;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runQuery(
|
|
45
|
+
tq: TestQuery
|
|
46
|
+
): Promise<Partial<QueryRunResult>> {
|
|
47
|
+
let query: QueryMaterializer;
|
|
48
|
+
let queryTestTag: Tag | undefined = undefined;
|
|
49
|
+
try {
|
|
50
|
+
query = tq.runner.loadQuery(tq.src);
|
|
51
|
+
const queryTags = (await query.getPreparedQuery()).tagParse().tag;
|
|
52
|
+
queryTestTag = queryTags.tag('test');
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// Add line numbers, helpful if failure is a compiler error
|
|
55
|
+
const queryText = tq.src
|
|
56
|
+
.split('\n')
|
|
57
|
+
.map((line, index) => `${(index + 1).toString().padStart(4)}: ${line}`)
|
|
58
|
+
.join('\n');
|
|
59
|
+
return {
|
|
60
|
+
fail: {
|
|
61
|
+
pass: false,
|
|
62
|
+
message: () =>
|
|
63
|
+
`Could not prepare query to run: ${e.message}\n\nQUERY:\n${queryText}`,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let result: Result;
|
|
69
|
+
try {
|
|
70
|
+
result = await query.run();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
let failMsg = `QUERY RUN FAILED: ${tq.src}\nMESSAGE: ${e.message}\n`;
|
|
73
|
+
if (e instanceof MalloyError) {
|
|
74
|
+
failMsg = `Error in query compilation\n${errorLogToString(
|
|
75
|
+
tq.src,
|
|
76
|
+
e.problems
|
|
77
|
+
)}`;
|
|
78
|
+
} else {
|
|
79
|
+
try {
|
|
80
|
+
failMsg += `SQL: ${await query.getSQL()}\n`;
|
|
81
|
+
} catch (e2) {
|
|
82
|
+
failMsg += `SQL FOR FAILING QUERY COULD NOT BE COMPUTED: ${e2.message}\n`;
|
|
83
|
+
}
|
|
84
|
+
failMsg += e.stack;
|
|
85
|
+
}
|
|
86
|
+
return {fail: {pass: false, message: () => failMsg}, query};
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
API.util.wrapResult(result);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
fail: {
|
|
93
|
+
pass: false,
|
|
94
|
+
message: () =>
|
|
95
|
+
`Result could not be wrapped into new style result: ${error}\n${error.stack}`,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {result, queryTestTag, query};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function errorLogToString(src: string, msgs: LogMessage[]) {
|
|
103
|
+
let lovely = '';
|
|
104
|
+
let lineNo = 0;
|
|
105
|
+
for (const line of src.split('\n')) {
|
|
106
|
+
lovely += ` | ${line}\n`;
|
|
107
|
+
for (const entry of msgs) {
|
|
108
|
+
if (entry.at) {
|
|
109
|
+
if (entry.at.range.start.line === lineNo) {
|
|
110
|
+
const charFrom = entry.at.range.start.character;
|
|
111
|
+
lovely += `!!!!! ${' '.repeat(charFrom)}^ ${entry.message}\n`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
lineNo += 1;
|
|
116
|
+
}
|
|
117
|
+
return lovely;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type TL = 'timeLiteral';
|
|
121
|
+
|
|
122
|
+
function lit(d: Dialect, t: string, type: 'timestamp' | 'date'): string {
|
|
123
|
+
const typeDef: {type: 'timestamp' | 'date'} = {type};
|
|
124
|
+
const timeLiteral: TL = 'timeLiteral';
|
|
125
|
+
const n = {
|
|
126
|
+
node: timeLiteral,
|
|
127
|
+
typeDef,
|
|
128
|
+
literal: t,
|
|
129
|
+
};
|
|
130
|
+
return d.sqlLiteralTime({}, n);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type SQLDataType = 'string' | 'number' | 'timestamp' | 'date' | 'boolean';
|
|
134
|
+
type SQLRow = unknown[];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create source built from the SQL for a series of
|
|
138
|
+
* SELECT ... UNION ALL SELECT statements which returns
|
|
139
|
+
* the passed data. This uses the dialect object to do
|
|
140
|
+
* all the quoting and type translation.
|
|
141
|
+
* @returns '${connectionName}.sql("""SELECT ..."""")'
|
|
142
|
+
*/
|
|
143
|
+
export function mkSQLSource(
|
|
144
|
+
dialect: Dialect,
|
|
145
|
+
connectioName: string,
|
|
146
|
+
schema: Record<string, SQLDataType>,
|
|
147
|
+
...dataRows: SQLRow[]
|
|
148
|
+
): string {
|
|
149
|
+
const stmts: string[] = [];
|
|
150
|
+
for (const oneRow of dataRows) {
|
|
151
|
+
const outRow: string[] = [];
|
|
152
|
+
let colNum = 0;
|
|
153
|
+
for (const colName of Object.keys(schema)) {
|
|
154
|
+
const val = oneRow[colNum];
|
|
155
|
+
colNum += 1;
|
|
156
|
+
let valStr = `ERROR BAD TYPE FOR ${schema[colName]}: ${typeof val}`;
|
|
157
|
+
if (schema[colName] === 'string' && typeof val === 'string') {
|
|
158
|
+
valStr = dialect.sqlLiteralString(val);
|
|
159
|
+
} else if (schema[colName] === 'number' && typeof val === 'number') {
|
|
160
|
+
valStr = val.toString();
|
|
161
|
+
} else if (schema[colName] === 'boolean' && typeof val === 'boolean') {
|
|
162
|
+
valStr = val.toString();
|
|
163
|
+
} else if (val === null) {
|
|
164
|
+
valStr = 'NULL';
|
|
165
|
+
} else if (schema[colName] === 'timestamp' && typeof val === 'string') {
|
|
166
|
+
valStr = lit(dialect, val, 'timestamp');
|
|
167
|
+
} else if (schema[colName] === 'date' && typeof val === 'string') {
|
|
168
|
+
valStr = lit(dialect, val, 'date');
|
|
169
|
+
}
|
|
170
|
+
outRow.push(
|
|
171
|
+
stmts.length === 0
|
|
172
|
+
? `${valStr} AS ${dialect.sqlMaybeQuoteIdentifier(colName)}`
|
|
173
|
+
: valStr
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
stmts.push(outRow.join(','));
|
|
177
|
+
}
|
|
178
|
+
return `${connectioName}.sql("""SELECT ${stmts.join(
|
|
179
|
+
'\nUNION ALL SELECT '
|
|
180
|
+
)}\n""")`;
|
|
181
|
+
}
|