Skip to content

Commit bd83abc

Browse files
fixing json conversion and adding tests (#168)
1 parent ddf051a commit bd83abc

File tree

5 files changed

+130
-74
lines changed

5 files changed

+130
-74
lines changed

meerkat-browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devrev/meerkat-browser",
3-
"version": "0.0.106",
3+
"version": "0.0.107",
44
"dependencies": {
55
"tslib": "^2.3.0",
66
"@devrev/meerkat-core": "*",

meerkat-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devrev/meerkat-core",
3-
"version": "0.0.106",
3+
"version": "0.0.107",
44
"dependencies": {
55
"tslib": "^2.3.0"
66
},

meerkat-core/src/resolution/steps/aggregation-step.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ export const getAggregatedSql = async ({
8585

8686
// Use ARRAY_AGG for resolved array columns, MAX for others
8787
// Filter out null values for ARRAY_AGG using FILTER clause
88+
// Wrap with to_json() to ensure proper JSON format in CSV exports
8889
const aggregationFn = isArrayColumn
89-
? `COALESCE(ARRAY_AGG(DISTINCT ${columnRef}) FILTER (WHERE ${columnRef} IS NOT NULL), [])`
90+
? `to_json(COALESCE(ARRAY_AGG(DISTINCT ${columnRef}) FILTER (WHERE ${columnRef} IS NOT NULL), []))`
9091
: `MAX(${columnRef})`;
9192

9293
aggregationMeasures.push({

meerkat-node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devrev/meerkat-node",
3-
"version": "0.0.106",
3+
"version": "0.0.107",
44
"dependencies": {
55
"@swc/helpers": "~0.5.0",
66
"@devrev/meerkat-core": "*",

meerkat-node/src/__tests__/cube-to-sql-with-resolution.spec.ts

Lines changed: 125 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
import { Query, ResolutionConfig, TableSchema } from '@devrev/meerkat-core';
22
import { cubeQueryToSQLWithResolution } from '../cube-to-sql-with-resolution/cube-to-sql-with-resolution';
33
import { duckdbExec } from '../duckdb-exec';
4+
5+
/**
6+
* Helper function to parse JSON string arrays returned by to_json(ARRAY_AGG(...))
7+
* DuckDB returns arrays as JSON strings when using to_json()
8+
*/
9+
const parseJsonArray = (value: any): any => {
10+
if (typeof value === 'string') {
11+
try {
12+
return JSON.parse(value);
13+
} catch {
14+
return value;
15+
}
16+
}
17+
return value;
18+
};
419
const CREATE_TEST_TABLE = `CREATE TABLE tickets (
520
id INTEGER,
621
owners VARCHAR[],
@@ -190,17 +205,24 @@ describe('cubeQueryToSQLWithResolution - Array field resolution', () => {
190205

191206
console.log('SQL with resolution:', sql);
192207

193-
// Execute the SQL to verify it works
194-
const result = (await duckdbExec(sql)) as any[];
195-
console.log('Result:', result);
208+
// Export to CSV using COPY command
209+
const csvPath = '/tmp/test_array_resolution.csv';
210+
await duckdbExec(`COPY (${sql}) TO '${csvPath}' (HEADER, DELIMITER ',')`);
211+
212+
// Read the CSV back
213+
const result = (await duckdbExec(
214+
`SELECT * FROM read_csv_auto('${csvPath}')`
215+
)) as any[];
216+
console.log('Result from CSV:', result);
196217

197218
// Without array unnesting, should have 3 rows (original count)
198219
expect(result.length).toBe(3);
199220

200221
// Verify ordering is maintained (ORDER BY tickets.id ASC)
201-
expect(result[0].ID).toBe(1);
202-
expect(result[1].ID).toBe(2);
203-
expect(result[2].ID).toBe(3);
222+
// Note: CSV reads integers as BigInt
223+
expect(Number(result[0].ID)).toBe(1);
224+
expect(Number(result[1].ID)).toBe(2);
225+
expect(Number(result[2].ID)).toBe(3);
204226

205227
// Each row should have the expected properties
206228
expect(result[0]).toHaveProperty('tickets__count');
@@ -210,21 +232,27 @@ describe('cubeQueryToSQLWithResolution - Array field resolution', () => {
210232
expect(result[0]).toHaveProperty('Owners - Display Name');
211233
expect(result[0]).toHaveProperty('Owners - Email');
212234

235+
// Parse JSON arrays from CSV (to_json ensures proper JSON format in CSV)
213236
const id1Record = result[0];
237+
const owners1 = parseJsonArray(id1Record['Owners - Display Name']);
238+
const emails1 = parseJsonArray(id1Record['Owners - Email']);
239+
214240
// Note: Array order may not be preserved without index tracking in UNNEST/ARRAY_AGG
215-
expect(id1Record['Owners - Display Name']).toEqual(
241+
expect(owners1).toEqual(
216242
expect.arrayContaining(['Alice Smith', 'Bob Jones'])
217243
);
218-
expect(id1Record['Owners - Email']).toEqual(
244+
expect(emails1).toEqual(
219245
expect.arrayContaining(['alice@example.com', 'bob@example.com'])
220246
);
221247

222248
const id2Record = result[1];
223-
expect(id2Record.ID).toBe(2);
224-
expect(id2Record['Owners - Display Name']).toEqual(
249+
expect(Number(id2Record.ID)).toBe(2);
250+
const owners2 = parseJsonArray(id2Record['Owners - Display Name']);
251+
const emails2 = parseJsonArray(id2Record['Owners - Email']);
252+
expect(owners2).toEqual(
225253
expect.arrayContaining(['Bob Jones', 'Charlie Brown'])
226254
);
227-
expect(id2Record['Owners - Email']).toEqual(
255+
expect(emails2).toEqual(
228256
expect.arrayContaining(['bob@example.com', 'charlie@example.com'])
229257
);
230258
});
@@ -288,17 +316,24 @@ describe('cubeQueryToSQLWithResolution - Array field resolution', () => {
288316

289317
console.log('SQL (multiple arrays):', sql);
290318

291-
// Execute the SQL to verify it works
292-
const result = (await duckdbExec(sql)) as any[];
293-
console.log('Result:', result);
319+
// Export to CSV using COPY command
320+
const csvPath = '/tmp/test_multiple_arrays.csv';
321+
await duckdbExec(`COPY (${sql}) TO '${csvPath}' (HEADER, DELIMITER ',')`);
322+
323+
// Read the CSV back
324+
const result = (await duckdbExec(
325+
`SELECT * FROM read_csv_auto('${csvPath}')`
326+
)) as any[];
327+
console.log('Result from CSV:', result);
294328

295329
// Should have 3 rows (original ticket count)
296330
expect(result.length).toBe(3);
297331

298332
// Verify ordering is maintained (ORDER BY tickets.id ASC)
299-
expect(result[0].ID).toBe(1);
300-
expect(result[1].ID).toBe(2);
301-
expect(result[2].ID).toBe(3);
333+
// Note: CSV reads integers as BigInt
334+
expect(Number(result[0].ID)).toBe(1);
335+
expect(Number(result[1].ID)).toBe(2);
336+
expect(Number(result[2].ID)).toBe(3);
302337

303338
// Each row should have the expected properties
304339
expect(result[0]).toHaveProperty('tickets__count');
@@ -307,42 +342,42 @@ describe('cubeQueryToSQLWithResolution - Array field resolution', () => {
307342
expect(result[0]).toHaveProperty('Tags - Tag Name');
308343
expect(result[0]).toHaveProperty('Created By - Name');
309344

310-
// Verify ticket 1: 2 owners, 1 tag
345+
// Verify ticket 1: 2 owners, 1 tag (parse JSON from CSV)
311346
const ticket1 = result[0];
312-
expect(ticket1['Owners - Display Name']).toEqual(
347+
const ticket1Owners = parseJsonArray(ticket1['Owners - Display Name']);
348+
const ticket1Tags = parseJsonArray(ticket1['Tags - Tag Name']);
349+
expect(ticket1Owners).toEqual(
313350
expect.arrayContaining(['Alice Smith', 'Bob Jones'])
314351
);
315-
expect(ticket1['Owners - Display Name'].length).toBe(2);
316-
expect(ticket1['Tags - Tag Name']).toEqual(
317-
expect.arrayContaining(['Tag 1'])
318-
);
319-
expect(ticket1['Tags - Tag Name'].length).toBe(1);
352+
expect(ticket1Owners.length).toBe(2);
353+
expect(ticket1Tags).toEqual(expect.arrayContaining(['Tag 1']));
354+
expect(ticket1Tags.length).toBe(1);
320355
expect(ticket1['Created By - Name']).toBe('User 1');
321356

322357
// Verify ticket 2: 2 owners, 2 tags
323358
const ticket2 = result[1];
324-
expect(ticket2.ID).toBe(2);
325-
expect(ticket2['Owners - Display Name']).toEqual(
359+
expect(Number(ticket2.ID)).toBe(2);
360+
const ticket2Owners = parseJsonArray(ticket2['Owners - Display Name']);
361+
const ticket2Tags = parseJsonArray(ticket2['Tags - Tag Name']);
362+
expect(ticket2Owners).toEqual(
326363
expect.arrayContaining(['Bob Jones', 'Charlie Brown'])
327364
);
328-
expect(ticket2['Owners - Display Name'].length).toBe(2);
329-
expect(ticket2['Tags - Tag Name']).toEqual(
330-
expect.arrayContaining(['Tag 2', 'Tag 3'])
331-
);
332-
expect(ticket2['Tags - Tag Name'].length).toBe(2);
365+
expect(ticket2Owners.length).toBe(2);
366+
expect(ticket2Tags).toEqual(expect.arrayContaining(['Tag 2', 'Tag 3']));
367+
expect(ticket2Tags.length).toBe(2);
333368
expect(ticket2['Created By - Name']).toBe('User 2');
334369

335370
// Verify ticket 3: 1 owner, 3 tags
336371
const ticket3 = result[2];
337-
expect(ticket3.ID).toBe(3);
338-
expect(ticket3['Owners - Display Name']).toEqual(
339-
expect.arrayContaining(['Diana Prince'])
340-
);
341-
expect(ticket3['Owners - Display Name'].length).toBe(1);
342-
expect(ticket3['Tags - Tag Name']).toEqual(
372+
expect(Number(ticket3.ID)).toBe(3);
373+
const ticket3Owners = parseJsonArray(ticket3['Owners - Display Name']);
374+
const ticket3Tags = parseJsonArray(ticket3['Tags - Tag Name']);
375+
expect(ticket3Owners).toEqual(expect.arrayContaining(['Diana Prince']));
376+
expect(ticket3Owners.length).toBe(1);
377+
expect(ticket3Tags).toEqual(
343378
expect.arrayContaining(['Tag 1', 'Tag 3', 'Tag 4'])
344379
);
345-
expect(ticket3['Tags - Tag Name'].length).toBe(3);
380+
expect(ticket3Tags.length).toBe(3);
346381
expect(ticket3['Created By - Name']).toBe('User 3');
347382
});
348383

@@ -388,17 +423,24 @@ describe('cubeQueryToSQLWithResolution - Array field resolution', () => {
388423

389424
console.log('SQL (scalar resolution only):', sql);
390425

391-
// Execute the SQL to verify it works
392-
const result = (await duckdbExec(sql)) as any[];
393-
console.log('Result:', result);
426+
// Export to CSV using COPY command
427+
const csvPath = '/tmp/test_scalar_resolution.csv';
428+
await duckdbExec(`COPY (${sql}) TO '${csvPath}' (HEADER, DELIMITER ',')`);
429+
430+
// Read the CSV back
431+
const result = (await duckdbExec(
432+
`SELECT * FROM read_csv_auto('${csvPath}')`
433+
)) as any[];
434+
console.log('Result from CSV:', result);
394435

395436
// Should have 3 rows (no array unnesting, only scalar resolution)
396437
expect(result.length).toBe(3);
397438

398439
// Verify ordering is maintained (ORDER BY tickets.id ASC)
399-
expect(result[0].ID).toBe(1);
400-
expect(result[1].ID).toBe(2);
401-
expect(result[2].ID).toBe(3);
440+
// Note: CSV reads integers as BigInt
441+
expect(Number(result[0].ID)).toBe(1);
442+
expect(Number(result[1].ID)).toBe(2);
443+
expect(Number(result[2].ID)).toBe(3);
402444

403445
// Each row should have the expected properties
404446
expect(result[0]).toHaveProperty('tickets__count');
@@ -408,25 +450,25 @@ describe('cubeQueryToSQLWithResolution - Array field resolution', () => {
408450
expect(result[0]).toHaveProperty('Created By - Name'); // Resolved scalar field
409451

410452
// Verify scalar resolution worked correctly
453+
// Note: Arrays in CSV are read back as strings, not arrays
411454
const ticket1 = result[0];
412-
expect(ticket1.ID).toBe(1);
455+
expect(Number(ticket1.ID)).toBe(1);
413456
expect(ticket1['Created By - Name']).toBe('User 1');
414-
expect(Array.isArray(ticket1['Owners'])).toBe(true);
415-
expect(ticket1['Owners']).toEqual(['owner1', 'owner2']);
416-
expect(Array.isArray(ticket1['Tags'])).toBe(true);
417-
expect(ticket1['Tags']).toEqual(['tag1']);
457+
// Arrays from CSV come back as strings like "[owner1, owner2]"
458+
expect(typeof ticket1['Owners']).toBe('string');
459+
expect(ticket1['Owners']).toContain('owner1');
460+
expect(ticket1['Owners']).toContain('owner2');
418461

419462
const ticket2 = result[1];
420-
expect(ticket2.ID).toBe(2);
463+
expect(Number(ticket2.ID)).toBe(2);
421464
expect(ticket2['Created By - Name']).toBe('User 2');
422-
expect(ticket2['Owners']).toEqual(['owner2', 'owner3']);
423-
expect(ticket2['Tags']).toEqual(['tag2', 'tag3']);
465+
expect(ticket2['Owners']).toContain('owner2');
466+
expect(ticket2['Owners']).toContain('owner3');
424467

425468
const ticket3 = result[2];
426-
expect(ticket3.ID).toBe(3);
469+
expect(Number(ticket3.ID)).toBe(3);
427470
expect(ticket3['Created By - Name']).toBe('User 3');
428-
expect(ticket3['Owners']).toEqual(['owner4']);
429-
expect(ticket3['Tags']).toEqual(['tag1', 'tag4', 'tag3']);
471+
expect(ticket3['Owners']).toContain('owner4');
430472
});
431473

432474
it('Should return aggregated SQL even when no resolution is configured', async () => {
@@ -525,9 +567,15 @@ describe('cubeQueryToSQLWithResolution - Array field resolution', () => {
525567
// Should still order by row_id at the end
526568
expect(sql).toContain('order by __row_id');
527569

528-
// Execute the SQL to verify it works
529-
const result = (await duckdbExec(sql)) as any[];
530-
console.log('Result (no ORDER BY):', result);
570+
// Export to CSV using COPY command
571+
const csvPath = '/tmp/test_no_order_by.csv';
572+
await duckdbExec(`COPY (${sql}) TO '${csvPath}' (HEADER, DELIMITER ',')`);
573+
574+
// Read the CSV back
575+
const result = (await duckdbExec(
576+
`SELECT * FROM read_csv_auto('${csvPath}')`
577+
)) as any[];
578+
console.log('Result from CSV (no ORDER BY):', result);
531579

532580
// Should have 3 rows (no array unnesting, only scalar resolution)
533581
expect(result.length).toBe(3);
@@ -541,17 +589,24 @@ describe('cubeQueryToSQLWithResolution - Array field resolution', () => {
541589

542590
// Verify scalar resolution worked correctly
543591
// Order might vary without ORDER BY, so we find by ID
544-
const ticket1 = result.find((r: any) => r.ID === 1);
545-
expect(ticket1['Created By - Name']).toBe('User 1');
546-
expect(Array.isArray(ticket1['Owners'])).toBe(true);
547-
expect(ticket1['Owners']).toEqual(['owner1', 'owner2']);
548-
549-
const ticket2 = result.find((r: any) => r.ID === 2);
550-
expect(ticket2['Created By - Name']).toBe('User 2');
551-
expect(ticket2['Owners']).toEqual(['owner2', 'owner3']);
552-
553-
const ticket3 = result.find((r: any) => r.ID === 3);
554-
expect(ticket3['Created By - Name']).toBe('User 3');
555-
expect(ticket3['Owners']).toEqual(['owner4']);
592+
// Note: CSV reads integers as BigInt, so we need to convert
593+
const ticket1 = result.find((r: any) => Number(r.ID) === 1);
594+
expect(ticket1).toBeDefined();
595+
expect(ticket1!['Created By - Name']).toBe('User 1');
596+
// Arrays from CSV come back as strings
597+
expect(typeof ticket1!['Owners']).toBe('string');
598+
expect(ticket1!['Owners']).toContain('owner1');
599+
expect(ticket1!['Owners']).toContain('owner2');
600+
601+
const ticket2 = result.find((r: any) => Number(r.ID) === 2);
602+
expect(ticket2).toBeDefined();
603+
expect(ticket2!['Created By - Name']).toBe('User 2');
604+
expect(ticket2!['Owners']).toContain('owner2');
605+
expect(ticket2!['Owners']).toContain('owner3');
606+
607+
const ticket3 = result.find((r: any) => Number(r.ID) === 3);
608+
expect(ticket3).toBeDefined();
609+
expect(ticket3!['Created By - Name']).toBe('User 3');
610+
expect(ticket3!['Owners']).toContain('owner4');
556611
});
557612
});

0 commit comments

Comments
 (0)