@malloydata/malloy 0.0.326 → 0.0.327

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.
Files changed (51) hide show
  1. package/dist/api/core.js +2 -0
  2. package/dist/api/index.d.ts +1 -0
  3. package/dist/api/index.js +1 -0
  4. package/dist/api/row_data_utils.d.ts +30 -0
  5. package/dist/api/row_data_utils.js +87 -0
  6. package/dist/api/util.js +20 -38
  7. package/dist/dialect/dialect.d.ts +36 -0
  8. package/dist/dialect/dialect.js +28 -1
  9. package/dist/dialect/duckdb/duckdb.d.ts +2 -1
  10. package/dist/dialect/duckdb/duckdb.js +12 -3
  11. package/dist/dialect/mysql/mysql.js +18 -4
  12. package/dist/dialect/pg_impl.d.ts +1 -0
  13. package/dist/dialect/pg_impl.js +2 -0
  14. package/dist/dialect/postgres/postgres.js +4 -1
  15. package/dist/dialect/snowflake/snowflake.d.ts +2 -1
  16. package/dist/dialect/snowflake/snowflake.js +37 -15
  17. package/dist/dialect/standardsql/standardsql.d.ts +2 -1
  18. package/dist/dialect/standardsql/standardsql.js +9 -3
  19. package/dist/dialect/trino/trino.js +10 -2
  20. package/dist/lang/ast/expressions/expr-avg.d.ts +5 -0
  21. package/dist/lang/ast/expressions/expr-avg.js +9 -0
  22. package/dist/lang/ast/expressions/expr-coalesce.js +6 -0
  23. package/dist/lang/ast/expressions/expr-number.d.ts +14 -1
  24. package/dist/lang/ast/expressions/expr-number.js +71 -6
  25. package/dist/lang/ast/expressions/expr-props.d.ts +1 -1
  26. package/dist/lang/ast/expressions/pick-when.js +10 -3
  27. package/dist/lang/ast/types/expression-def.js +36 -2
  28. package/dist/lang/parse-log.d.ts +1 -0
  29. package/dist/lang/test/test-translator.js +2 -0
  30. package/dist/malloy.d.ts +23 -2
  31. package/dist/malloy.js +204 -41
  32. package/dist/model/malloy_types.d.ts +2 -2
  33. package/dist/model/query_model_impl.js +5 -1
  34. package/dist/test/cellsToObject.d.ts +6 -0
  35. package/dist/test/cellsToObject.js +111 -0
  36. package/dist/test/index.d.ts +7 -0
  37. package/dist/test/index.js +16 -20
  38. package/dist/test/matchers.d.ts +10 -0
  39. package/dist/test/matchers.js +17 -0
  40. package/dist/test/resultMatchers.d.ts +42 -0
  41. package/dist/test/resultMatchers.js +722 -0
  42. package/dist/test/runQuery.d.ts +31 -0
  43. package/dist/test/runQuery.js +67 -0
  44. package/dist/test/test-models.d.ts +77 -0
  45. package/dist/test/test-models.js +319 -0
  46. package/dist/test/test-values.d.ts +66 -0
  47. package/dist/test/test-values.js +208 -0
  48. package/dist/to_stable.js +3 -1
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +9 -5
@@ -0,0 +1,722 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright Contributors to the Malloy project
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const __1 = require("..");
8
+ const util_1 = require("util");
9
+ const cellsToObject_1 = require("./cellsToObject");
10
+ function errInfo(e) {
11
+ var _a;
12
+ let err = '';
13
+ const trace = (_a = e.stack) !== null && _a !== void 0 ? _a : '';
14
+ if (e.message && !trace.includes(e.message)) {
15
+ err = `ERROR: ${e.message}\n`;
16
+ }
17
+ if (e.stack) {
18
+ err += `STACK: ${e.stack}\n`;
19
+ }
20
+ return err;
21
+ }
22
+ function errorLogToString(src, msgs) {
23
+ let lovely = '';
24
+ let lineNo = 0;
25
+ for (const line of src.split('\n')) {
26
+ lovely += ` | ${line}\n`;
27
+ for (const entry of msgs) {
28
+ if (entry.at) {
29
+ if (entry.at.range.start.line === lineNo) {
30
+ const charFrom = entry.at.range.start.character;
31
+ lovely += `!!!!! ${' '.repeat(charFrom)}^ ${entry.message}\n`;
32
+ }
33
+ }
34
+ }
35
+ lineNo += 1;
36
+ }
37
+ return lovely;
38
+ }
39
+ function looseEqual(a, b) {
40
+ if (a === b) {
41
+ return true;
42
+ }
43
+ if (typeof a === 'number' && typeof b === 'bigint') {
44
+ try {
45
+ return BigInt(a) === b;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ if (typeof a === 'bigint' && typeof b === 'number') {
52
+ try {
53
+ return a === BigInt(b);
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ // MySQL infers booleans as BIGINT in UNION contexts
60
+ if (typeof a === 'boolean' && typeof b === 'bigint') {
61
+ return a === (b !== BigInt(0));
62
+ }
63
+ if (typeof a === 'bigint' && typeof b === 'boolean') {
64
+ return (a !== BigInt(0)) === b;
65
+ }
66
+ return false;
67
+ }
68
+ async function runQueryInternal(tm, src) {
69
+ let query;
70
+ let queryTestTag = undefined;
71
+ try {
72
+ query = tm.model.loadQuery(src);
73
+ const queryTags = (await query.getPreparedQuery()).tagParse().tag;
74
+ queryTestTag = queryTags.tag('test');
75
+ }
76
+ catch (e) {
77
+ // Add line numbers, helpful if failure is a compiler error
78
+ const queryText = src
79
+ .split('\n')
80
+ .map((line, index) => `${(index + 1).toString().padStart(4)}: ${line}`)
81
+ .join('\n');
82
+ return {
83
+ fail: {
84
+ pass: false,
85
+ message: () => `Could not prepare query to run:\n${queryText}\n\n${errInfo(e)}`,
86
+ },
87
+ };
88
+ }
89
+ try {
90
+ const result = await query.run();
91
+ // Use wrapResult to normalize data across all databases
92
+ // This handles BigQuery timestamp wrappers, MySQL boolean 0/1, etc.
93
+ const malloyResult = __1.API.util.wrapResult(result);
94
+ if (!malloyResult.data) {
95
+ return {
96
+ fail: { pass: false, message: () => 'Query returned no data' },
97
+ query,
98
+ };
99
+ }
100
+ // Return both raw cells+schema (for schema-aware matching) and converted objects (for debug output)
101
+ const dataObjects = (0, cellsToObject_1.cellsToObjects)(malloyResult.data, malloyResult.schema);
102
+ return {
103
+ data: malloyResult.data,
104
+ schema: malloyResult.schema,
105
+ dataObjects,
106
+ queryTestTag,
107
+ query,
108
+ };
109
+ }
110
+ catch (e) {
111
+ const cleanSrc = src.replace(/^\n+/m, '').trimEnd();
112
+ let failMsg = `QUERY RUN FAILED:\n${cleanSrc}`;
113
+ if (e instanceof __1.MalloyError) {
114
+ failMsg = `Error in query compilation\n${errorLogToString(src, e.problems)}`;
115
+ }
116
+ else {
117
+ try {
118
+ failMsg += `\nSQL: ${await query.getSQL()}\n`;
119
+ }
120
+ catch {
121
+ failMsg += '\nSQL FOR QUERY COULD NOT BE COMPUTED\n';
122
+ }
123
+ failMsg += errInfo(e);
124
+ }
125
+ return { fail: { pass: false, message: () => failMsg }, query };
126
+ }
127
+ }
128
+ function humanReadable(thing) {
129
+ return (0, util_1.inspect)(thing, { breakLength: 72, depth: Infinity });
130
+ }
131
+ function matchFail(path, expected, actual) {
132
+ return {
133
+ pass: false,
134
+ path,
135
+ expected: humanReadable(expected),
136
+ actual: humanReadable(actual),
137
+ };
138
+ }
139
+ function matchFailStr(path, expected, actual) {
140
+ return { pass: false, path, expected, actual };
141
+ }
142
+ /**
143
+ * Get the type kind from a FieldInfo.
144
+ * Returns undefined for joins and views (which have schemas, not types).
145
+ */
146
+ function getTypeKind(fieldInfo) {
147
+ if (fieldInfo.kind === 'join' || fieldInfo.kind === 'view') {
148
+ return undefined;
149
+ }
150
+ return fieldInfo.type.kind;
151
+ }
152
+ /**
153
+ * Convert a cell to a plain JS value for error messages.
154
+ */
155
+ function cellToValue(cell) {
156
+ switch (cell.kind) {
157
+ case 'null_cell':
158
+ return null;
159
+ case 'string_cell':
160
+ return cell.string_value;
161
+ case 'number_cell':
162
+ return cell.number_value;
163
+ case 'big_number_cell':
164
+ return BigInt(cell.number_value);
165
+ case 'boolean_cell':
166
+ return cell.boolean_value;
167
+ case 'date_cell':
168
+ return cell.date_value;
169
+ case 'timestamp_cell':
170
+ return cell.timestamp_value;
171
+ case 'json_cell':
172
+ return JSON.parse(cell.json_value);
173
+ case 'sql_native_cell':
174
+ return JSON.parse(cell.sql_native_value);
175
+ case 'array_cell':
176
+ return cell.array_value.map(cellToValue);
177
+ case 'record_cell':
178
+ return cell.record_value.map(cellToValue);
179
+ default:
180
+ return `<unknown cell: ${cell.kind}>`;
181
+ }
182
+ }
183
+ /**
184
+ * Compare a cell against an expected value.
185
+ * Uses schema type info for intelligent date/timestamp comparison.
186
+ * @param mode - 'partial' allows extra fields in records, 'exact' requires exact match
187
+ */
188
+ function matchCell(cell, fieldInfo, expected, path, dialect, mode) {
189
+ // Handle null
190
+ if (cell.kind === 'null_cell') {
191
+ if (expected === null) {
192
+ return { pass: true };
193
+ }
194
+ return matchFail(path, expected, null);
195
+ }
196
+ // If expected is null but cell is not null
197
+ if (expected === null) {
198
+ return matchFail(path, null, cellToValue(cell));
199
+ }
200
+ const typeKind = getTypeKind(fieldInfo);
201
+ // Handle date fields with schema-aware comparison
202
+ if (cell.kind === 'date_cell' && typeKind === 'date_type') {
203
+ const actualDateStr = cell.date_value;
204
+ // Extract just the date portion (YYYY-MM-DD) from ISO string
205
+ const actualDate = actualDateStr.split('T')[0];
206
+ if (typeof expected === 'string') {
207
+ // If expected is a date string like 'YYYY-MM-DD', compare directly
208
+ if (/^\d{4}-\d{2}-\d{2}$/.test(expected)) {
209
+ if (actualDate === expected) {
210
+ return { pass: true };
211
+ }
212
+ return matchFailStr(path, expected, actualDate);
213
+ }
214
+ // If expected is a full ISO string, compare as-is
215
+ if (actualDateStr === expected) {
216
+ return { pass: true };
217
+ }
218
+ return matchFailStr(path, expected, actualDateStr);
219
+ }
220
+ if (expected instanceof Date) {
221
+ const actualDateObj = new Date(actualDateStr);
222
+ if (expected.getTime() === actualDateObj.getTime()) {
223
+ return { pass: true };
224
+ }
225
+ return matchFailStr(path, expected.toISOString(), actualDateObj.toISOString());
226
+ }
227
+ return matchFail(path, expected, actualDate);
228
+ }
229
+ // Handle timestamp fields with schema-aware comparison
230
+ if (cell.kind === 'timestamp_cell' &&
231
+ (typeKind === 'timestamp_type' || typeKind === 'timestamptz_type')) {
232
+ const actualTsStr = cell.timestamp_value;
233
+ if (typeof expected === 'string') {
234
+ // Compare as timestamps (parse both and compare times)
235
+ const actualDate = new Date(actualTsStr);
236
+ const expectedDate = new Date(expected);
237
+ if (!isNaN(expectedDate.getTime())) {
238
+ if (actualDate.getTime() === expectedDate.getTime()) {
239
+ return { pass: true };
240
+ }
241
+ return matchFailStr(path, expectedDate.toISOString(), actualDate.toISOString());
242
+ }
243
+ // If expected is not a valid date string, compare as strings
244
+ if (actualTsStr === expected) {
245
+ return { pass: true };
246
+ }
247
+ return matchFailStr(path, expected, actualTsStr);
248
+ }
249
+ if (expected instanceof Date) {
250
+ const actualDate = new Date(actualTsStr);
251
+ if (expected.getTime() === actualDate.getTime()) {
252
+ return { pass: true };
253
+ }
254
+ return matchFailStr(path, expected.toISOString(), actualDate.toISOString());
255
+ }
256
+ return matchFail(path, expected, actualTsStr);
257
+ }
258
+ // Handle boolean cells
259
+ if (cell.kind === 'boolean_cell') {
260
+ const actual = cell.boolean_value;
261
+ if (typeof expected === 'boolean') {
262
+ if (actual === expected) {
263
+ return { pass: true };
264
+ }
265
+ return matchFail(path, expected, actual);
266
+ }
267
+ return matchFail(path, expected, actual);
268
+ }
269
+ // Handle string cells
270
+ if (cell.kind === 'string_cell') {
271
+ const actual = cell.string_value;
272
+ if (actual === expected) {
273
+ return { pass: true };
274
+ }
275
+ return matchFail(path, expected, actual);
276
+ }
277
+ // Handle number cells
278
+ if (cell.kind === 'number_cell') {
279
+ let actual = cell.number_value;
280
+ // Handle simulated booleans (MySQL returns 1/0 for booleans)
281
+ if (typeof expected === 'boolean' &&
282
+ dialect.booleanType === 'simulated' &&
283
+ typeof actual === 'number') {
284
+ actual = actual !== 0;
285
+ }
286
+ if (looseEqual(actual, expected)) {
287
+ return { pass: true };
288
+ }
289
+ return matchFail(path, expected, actual);
290
+ }
291
+ // Handle big number cells
292
+ if (cell.kind === 'big_number_cell') {
293
+ const actual = BigInt(cell.number_value);
294
+ if (looseEqual(actual, expected)) {
295
+ return { pass: true };
296
+ }
297
+ // Also allow matching against string representation of the number
298
+ if (typeof expected === 'string' && expected === cell.number_value) {
299
+ return { pass: true };
300
+ }
301
+ return matchFail(path, expected, actual);
302
+ }
303
+ // Handle JSON cells
304
+ if (cell.kind === 'json_cell') {
305
+ const actual = JSON.parse(cell.json_value);
306
+ return matchValue(actual, expected, path, dialect, mode);
307
+ }
308
+ // Handle SQL native cells
309
+ if (cell.kind === 'sql_native_cell') {
310
+ const actual = JSON.parse(cell.sql_native_value);
311
+ return matchValue(actual, expected, path, dialect, mode);
312
+ }
313
+ // Handle array cells
314
+ if (cell.kind === 'array_cell') {
315
+ if (!Array.isArray(expected)) {
316
+ return matchFailStr(path, 'array', `expected non-array: ${humanReadable(expected)}`);
317
+ }
318
+ const actualArray = cell.array_value;
319
+ if (actualArray.length !== expected.length) {
320
+ return matchFailStr(path, `${expected.length} elements`, `${actualArray.length} elements`);
321
+ }
322
+ // Get element type info
323
+ let elementFieldInfo;
324
+ if (fieldInfo.kind === 'join') {
325
+ // For joins, the schema describes the element type
326
+ elementFieldInfo = fieldInfo;
327
+ }
328
+ else if (fieldInfo.kind === 'view') {
329
+ // Views have schema but no type
330
+ return matchFailStr(path, 'array type', 'view fieldInfo');
331
+ }
332
+ else if (fieldInfo.type.kind === 'array_type') {
333
+ elementFieldInfo = {
334
+ kind: 'dimension',
335
+ name: 'element',
336
+ type: fieldInfo.type.element_type,
337
+ };
338
+ }
339
+ else {
340
+ return matchFailStr(path, 'array type', `unexpected type: ${fieldInfo.type.kind}`);
341
+ }
342
+ for (let i = 0; i < expected.length; i++) {
343
+ const result = matchCell(actualArray[i], elementFieldInfo, expected[i], `${path}[${i}]`, dialect, mode);
344
+ if (!result.pass) {
345
+ return result;
346
+ }
347
+ }
348
+ return { pass: true };
349
+ }
350
+ // Handle record cells (from joins or nested queries)
351
+ if (cell.kind === 'record_cell') {
352
+ if (typeof expected !== 'object' || expected === null) {
353
+ return matchFailStr(path, 'object', `expected non-object: ${humanReadable(expected)}`);
354
+ }
355
+ // Get field schema based on fieldInfo type
356
+ let fields;
357
+ let getChildFieldInfo;
358
+ if (fieldInfo.kind === 'join') {
359
+ fields = fieldInfo.schema.fields;
360
+ getChildFieldInfo = i => fieldInfo.schema.fields[i];
361
+ }
362
+ else if (fieldInfo.kind === 'dimension' &&
363
+ fieldInfo.type.kind === 'record_type') {
364
+ const recordType = fieldInfo.type;
365
+ fields = recordType.fields;
366
+ getChildFieldInfo = i => ({
367
+ kind: 'dimension',
368
+ name: recordType.fields[i].name,
369
+ type: recordType.fields[i].type,
370
+ });
371
+ }
372
+ else {
373
+ return matchFailStr(path, 'record type', `unexpected fieldInfo: ${fieldInfo.kind}`);
374
+ }
375
+ // Build a map of field names to indices
376
+ const fieldIndexMap = new Map();
377
+ for (let i = 0; i < fields.length; i++) {
378
+ fieldIndexMap.set(fields[i].name, i);
379
+ }
380
+ const expectedObj = expected;
381
+ // For exact mode, check that keys match exactly
382
+ if (mode === 'exact') {
383
+ const expectedKeys = Object.keys(expectedObj).sort();
384
+ const actualKeys = fields.map(f => f.name).sort();
385
+ for (const key of actualKeys) {
386
+ if (!expectedKeys.includes(key)) {
387
+ return matchFailStr(path, `no field '${key}'`, `unexpected field '${key}'`);
388
+ }
389
+ }
390
+ for (const key of expectedKeys) {
391
+ if (!actualKeys.includes(key)) {
392
+ return matchFailStr(path, `field '${key}'`, `missing field '${key}'`);
393
+ }
394
+ }
395
+ }
396
+ // Check expected keys
397
+ for (const [key, expectedValue] of Object.entries(expectedObj)) {
398
+ const fieldIndex = fieldIndexMap.get(key);
399
+ if (fieldIndex === undefined) {
400
+ return matchFailStr(path, `field '${key}'`, 'field not found in schema');
401
+ }
402
+ const childCell = cell.record_value[fieldIndex];
403
+ const childFieldInfo = getChildFieldInfo(fieldIndex);
404
+ const result = matchCell(childCell, childFieldInfo, expectedValue, `${path}.${key}`, dialect, mode);
405
+ if (!result.pass) {
406
+ return result;
407
+ }
408
+ }
409
+ return { pass: true };
410
+ }
411
+ // Fallback for unknown cell types
412
+ return matchFailStr(path, humanReadable(expected), `unknown cell kind: ${cell.kind}`);
413
+ }
414
+ /**
415
+ * Compare two plain values.
416
+ * Used for JSON/SQL native values that have already been parsed.
417
+ * @param mode - 'partial' allows extra fields in objects, 'exact' requires exact match
418
+ */
419
+ function matchValue(actual, expected, path, dialect, mode) {
420
+ // Handle null
421
+ if (expected === null) {
422
+ if (actual === null) {
423
+ return { pass: true };
424
+ }
425
+ return matchFail(path, null, actual);
426
+ }
427
+ // Handle primitives
428
+ if (typeof expected === 'string' ||
429
+ typeof expected === 'number' ||
430
+ typeof expected === 'boolean' ||
431
+ typeof expected === 'bigint') {
432
+ if (looseEqual(actual, expected)) {
433
+ return { pass: true };
434
+ }
435
+ return matchFail(path, expected, actual);
436
+ }
437
+ // Handle Date objects
438
+ if (expected instanceof Date) {
439
+ if (actual === null) {
440
+ return matchFailStr(path, expected.toISOString(), 'null');
441
+ }
442
+ let actualDate;
443
+ if (actual instanceof Date) {
444
+ actualDate = actual;
445
+ }
446
+ else if (typeof actual === 'string') {
447
+ actualDate = new Date(actual);
448
+ if (isNaN(actualDate.getTime())) {
449
+ return matchFailStr(path, expected.toISOString(), `'${actual}' (not a valid date)`);
450
+ }
451
+ }
452
+ else {
453
+ return matchFailStr(path, expected.toISOString(), humanReadable(actual));
454
+ }
455
+ if (expected.getTime() === actualDate.getTime()) {
456
+ return { pass: true };
457
+ }
458
+ return matchFailStr(path, expected.toISOString(), actualDate.toISOString());
459
+ }
460
+ // Handle arrays
461
+ if (Array.isArray(expected)) {
462
+ if (!Array.isArray(actual)) {
463
+ return matchFailStr(path, `array with ${expected.length} elements`, `${typeof actual}`);
464
+ }
465
+ if (actual.length !== expected.length) {
466
+ return matchFailStr(path, `${expected.length} elements`, `${actual.length} elements`);
467
+ }
468
+ for (let i = 0; i < expected.length; i++) {
469
+ const result = matchValue(actual[i], expected[i], `${path}[${i}]`, dialect, mode);
470
+ if (!result.pass) {
471
+ return result;
472
+ }
473
+ }
474
+ return { pass: true };
475
+ }
476
+ // Handle objects
477
+ if (typeof expected === 'object') {
478
+ if (typeof actual !== 'object' ||
479
+ actual === null ||
480
+ Array.isArray(actual)) {
481
+ return matchFailStr(path, 'object', humanReadable(actual));
482
+ }
483
+ const expectedKeys = Object.keys(expected).sort();
484
+ const actualKeys = Object.keys(actual).sort();
485
+ // For exact mode, check that keys match exactly
486
+ if (mode === 'exact') {
487
+ for (const key of actualKeys) {
488
+ if (!expectedKeys.includes(key)) {
489
+ return matchFailStr(path, `no field '${key}'`, `unexpected field '${key}'`);
490
+ }
491
+ }
492
+ for (const key of expectedKeys) {
493
+ if (!actualKeys.includes(key)) {
494
+ return matchFailStr(path, `field '${key}'`, `missing field '${key}'`);
495
+ }
496
+ }
497
+ }
498
+ for (const [key, value] of Object.entries(expected)) {
499
+ const actualValue = actual[key];
500
+ const result = matchValue(actualValue, value, `${path}.${key}`, dialect, mode);
501
+ if (!result.pass) {
502
+ return result;
503
+ }
504
+ }
505
+ return { pass: true };
506
+ }
507
+ return matchFailStr(path, 'supported type', `unsupported: ${typeof expected}`);
508
+ }
509
+ /**
510
+ * Check if last argument is an options object.
511
+ */
512
+ function isOptions(arg) {
513
+ if (typeof arg !== 'object' || arg === null) {
514
+ return false;
515
+ }
516
+ const keys = Object.keys(arg);
517
+ // Options object only has 'debug' key
518
+ return keys.length === 1 && keys[0] === 'debug';
519
+ }
520
+ /**
521
+ * Shared implementation for all result matching.
522
+ * @param mode - 'partial' for toMatchResult/toMatchRows, 'exact' for toEqualResult
523
+ * @param strictRowCount - If true, requires exact row count match
524
+ */
525
+ async function matchImpl(querySrc, tm, expectedRows, options, mode, strictRowCount, jestUtils) {
526
+ var _a;
527
+ querySrc = querySrc.trimEnd().replace(/^\n*/, '');
528
+ const { fail, data, schema, dataObjects, queryTestTag, query } = await runQueryInternal(tm, querySrc);
529
+ if (fail)
530
+ return fail;
531
+ if (!data || !schema || !dataObjects) {
532
+ return {
533
+ pass: false,
534
+ message: () => 'runQuery returned no data and no errors',
535
+ };
536
+ }
537
+ const fails = [];
538
+ const debug = options.debug || (queryTestTag === null || queryTestTag === void 0 ? void 0 : queryTestTag.has('debug'));
539
+ // Data is an array_cell containing record_cells
540
+ if (data.kind !== 'array_cell') {
541
+ return {
542
+ pass: false,
543
+ message: () => `Expected array_cell at root, got ${data.kind}`,
544
+ };
545
+ }
546
+ const rows = data.array_value;
547
+ // Create root fieldInfo for schema navigation
548
+ const rootFieldInfo = {
549
+ kind: 'join',
550
+ name: 'root',
551
+ relationship: 'one',
552
+ schema,
553
+ };
554
+ // Collect row-level issues for unified output
555
+ const rowCountMismatch = strictRowCount && rows.length !== expectedRows.length
556
+ ? `Expected ${expectedRows.length} rows, got ${rows.length}`
557
+ : !strictRowCount &&
558
+ expectedRows.length > 0 &&
559
+ rows.length < expectedRows.length
560
+ ? `Expected at least ${expectedRows.length} rows, got ${rows.length}`
561
+ : null;
562
+ // Track per-row mismatches: row index -> list of field issues
563
+ const rowIssues = new Map();
564
+ // Check empty match {} means "at least one row"
565
+ if (mode === 'partial' &&
566
+ expectedRows.length === 1 &&
567
+ Object.keys(expectedRows[0]).length === 0) {
568
+ if (rows.length === 0) {
569
+ fails.push('Expected at least one row, got 0');
570
+ }
571
+ }
572
+ else {
573
+ // Compare each expected row using schema-aware cell matching
574
+ const rowsToCheck = Math.min(rows.length, expectedRows.length);
575
+ for (let i = 0; i < rowsToCheck; i++) {
576
+ const matchResult = matchCell(rows[i], rootFieldInfo, expectedRows[i], `Row ${i}`, tm.dialect, mode);
577
+ if (!matchResult.pass) {
578
+ // Extract field name from path like "Row 0.fieldname" or "Row 0.nested.field"
579
+ const pathMatch = (_a = matchResult.path) === null || _a === void 0 ? void 0 : _a.match(/^Row \d+\.(.+)$/);
580
+ const fieldPath = pathMatch ? pathMatch[1] : matchResult.path;
581
+ const issue = jestUtils.EXPECTED_COLOR(` Expected ${fieldPath}: ${matchResult.expected}`);
582
+ if (!rowIssues.has(i)) {
583
+ rowIssues.set(i, []);
584
+ }
585
+ rowIssues.get(i).push(issue);
586
+ }
587
+ }
588
+ }
589
+ // Build DATA DIFFERENCES section if there are any issues
590
+ if (rowCountMismatch || rowIssues.size > 0) {
591
+ const diffLines = ['DATA DIFFERENCES'];
592
+ if (rowCountMismatch) {
593
+ diffLines.push(` ${rowCountMismatch}`);
594
+ }
595
+ // Show each row with its issues
596
+ const maxRowToShow = Math.max(rows.length, expectedRows.length);
597
+ for (let i = 0; i < maxRowToShow; i++) {
598
+ if (i < rows.length) {
599
+ const rowData = humanReadable(dataObjects[i]);
600
+ const issues = rowIssues.get(i);
601
+ const isExtra = i >= expectedRows.length;
602
+ if (issues || isExtra) {
603
+ // Red if there are issues or it's an extra row (use ! for non-colored output)
604
+ diffLines.push(jestUtils.RECEIVED_COLOR(` ${i}! ${rowData}`));
605
+ if (issues) {
606
+ for (const issue of issues) {
607
+ diffLines.push(issue);
608
+ }
609
+ }
610
+ }
611
+ else {
612
+ // Row matched - green
613
+ diffLines.push(jestUtils.EXPECTED_COLOR(` ${i}: ${rowData}`));
614
+ }
615
+ }
616
+ else {
617
+ // Missing row - show what was expected (use ! for non-colored output)
618
+ diffLines.push(jestUtils.RECEIVED_COLOR(` ${i}! (missing)`));
619
+ if (i < expectedRows.length) {
620
+ diffLines.push(jestUtils.EXPECTED_COLOR(` Expected: ${humanReadable(expectedRows[i])}`));
621
+ }
622
+ }
623
+ }
624
+ fails.push(diffLines.join('\n'));
625
+ }
626
+ if (debug && fails.length === 0) {
627
+ fails.push('Test forced failure (# test.debug)');
628
+ fails.push(jestUtils.RECEIVED_COLOR(`Result: ${humanReadable(dataObjects)}`));
629
+ }
630
+ if (fails.length > 0) {
631
+ if (debug) {
632
+ fails.unshift(`Result Data: ${humanReadable(dataObjects)}`);
633
+ }
634
+ const fromSQL = query
635
+ ? 'SQL Generated:\n ' + (await query.getSQL()).split('\n').join('\n ')
636
+ : 'SQL Missing';
637
+ const failMsg = `QUERY:\n${querySrc}\n\n${fromSQL}\n\n${fails.join('\n')}`;
638
+ return { pass: false, message: () => failMsg };
639
+ }
640
+ return {
641
+ pass: true,
642
+ message: () => mode === 'exact'
643
+ ? 'All rows matched expected results exactly'
644
+ : 'All rows matched expected results',
645
+ };
646
+ }
647
+ expect.extend({
648
+ async toMatchResult(querySrc, tm, ...rowsOrOptions) {
649
+ // Parse args - last might be options
650
+ let options = {};
651
+ let expectedRows;
652
+ if (rowsOrOptions.length > 0 &&
653
+ isOptions(rowsOrOptions[rowsOrOptions.length - 1])) {
654
+ options = rowsOrOptions[rowsOrOptions.length - 1];
655
+ expectedRows = rowsOrOptions.slice(0, -1);
656
+ }
657
+ else {
658
+ expectedRows = rowsOrOptions;
659
+ }
660
+ return matchImpl(querySrc, tm, expectedRows, options, 'partial', false, this.utils);
661
+ },
662
+ async toMatchRows(querySrc, tm, expectedRows, options = {}) {
663
+ return matchImpl(querySrc, tm, expectedRows, options, 'partial', true, this.utils);
664
+ },
665
+ async toEqualResult(querySrc, tm, expectedRows, options = {}) {
666
+ return matchImpl(querySrc, tm, expectedRows, options, 'exact', true, this.utils);
667
+ },
668
+ /**
669
+ * Navigate a dotted path through an object, taking the first element of arrays.
670
+ * For path 'a.b.c', navigates: obj -> obj.a (or obj.a[0] if array) -> .b -> .c
671
+ */
672
+ toHavePath(received, paths) {
673
+ const fails = [];
674
+ for (const [path, expected] of Object.entries(paths)) {
675
+ const segments = path.split('.');
676
+ let current = received;
677
+ for (const segment of segments) {
678
+ if (current === null || current === undefined) {
679
+ fails.push(`Path '${path}': cannot navigate through null/undefined`);
680
+ break;
681
+ }
682
+ // If current is an array, take first element then access property
683
+ if (Array.isArray(current)) {
684
+ if (current.length === 0) {
685
+ fails.push(`Path '${path}': empty array at '${segment}'`);
686
+ current = undefined;
687
+ break;
688
+ }
689
+ current = current[0];
690
+ }
691
+ if (typeof current === 'object' && current !== null) {
692
+ current = current[segment];
693
+ }
694
+ else {
695
+ fails.push(`Path '${path}': cannot access '${segment}' on ${typeof current}`);
696
+ current = undefined;
697
+ break;
698
+ }
699
+ }
700
+ // Final value might be in an array too
701
+ if (Array.isArray(current) && current.length > 0) {
702
+ current = current[0];
703
+ }
704
+ if (!looseEqual(current, expected)) {
705
+ fails.push(`Path '${path}':`);
706
+ fails.push(this.utils.EXPECTED_COLOR(` Expected: ${humanReadable(expected)}`));
707
+ fails.push(this.utils.RECEIVED_COLOR(` Received: ${humanReadable(current)}`));
708
+ }
709
+ }
710
+ if (fails.length > 0) {
711
+ return {
712
+ pass: false,
713
+ message: () => fails.join('\n'),
714
+ };
715
+ }
716
+ return {
717
+ pass: true,
718
+ message: () => 'All paths matched expected values',
719
+ };
720
+ },
721
+ });
722
+ //# sourceMappingURL=resultMatchers.js.map