11import { Query , ResolutionConfig , TableSchema } from '@devrev/meerkat-core' ;
22import { cubeQueryToSQLWithResolution } from '../cube-to-sql-with-resolution/cube-to-sql-with-resolution' ;
33import { 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+ } ;
419const 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