@@ -322,6 +322,147 @@ describe('searchableJson postgres integration', () => {
322322
323323 expect ( decrypted . data . metadata ) . toEqual ( plaintextWithPath )
324324 } , 30000 )
325+
326+ it ( 'finds row by simple top-level path (Simple)' , async ( ) => {
327+ const plaintext = { role : 'path-tl-simple' , extra : 'data' }
328+
329+ const encrypted = await protectClient . encryptModel ( { metadata : plaintext } , table )
330+ if ( encrypted . failure ) throw new Error ( encrypted . failure . message )
331+
332+ const [ inserted ] = await sql `
333+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
334+ VALUES (${ sql . json ( encrypted . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
335+ RETURNING id
336+ `
337+
338+ const queryResult = await protectClient . encryptQuery ( '$.role' , {
339+ column : table . metadata ,
340+ table : table ,
341+ queryType : 'steVecSelector' ,
342+ returnType : 'composite-literal' ,
343+ } )
344+ if ( queryResult . failure ) throw new Error ( queryResult . failure . message )
345+ const selectorTerm = queryResult . data
346+
347+ const rows = await sql . unsafe (
348+ `SELECT id, (metadata).data as metadata
349+ FROM "protect-ci-jsonb" t,
350+ eql_v2.jsonb_path_query(t.metadata, '${ selectorTerm } '::eql_v2_encrypted) as result
351+ WHERE t.test_run_id = '${ TEST_RUN_ID } '`
352+ )
353+
354+ expect ( rows . length ) . toBeGreaterThanOrEqual ( 1 )
355+ const matchingRow = rows . find ( ( r : any ) => r . id === inserted . id )
356+ expect ( matchingRow ) . toBeDefined ( )
357+
358+ const decrypted = await protectClient . decryptModel ( { metadata : matchingRow ! . metadata } )
359+ if ( decrypted . failure ) throw new Error ( decrypted . failure . message )
360+ expect ( decrypted . data . metadata ) . toEqual ( plaintext )
361+ } , 30000 )
362+
363+ it ( 'finds row by nested path (Simple)' , async ( ) => {
364+ const plaintext = { user : { email : 'nested-simple@test.com' } , type : 'nested-path-simple' }
365+
366+ const encrypted = await protectClient . encryptModel ( { metadata : plaintext } , table )
367+ if ( encrypted . failure ) throw new Error ( encrypted . failure . message )
368+
369+ const [ inserted ] = await sql `
370+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
371+ VALUES (${ sql . json ( encrypted . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
372+ RETURNING id
373+ `
374+
375+ const queryResult = await protectClient . encryptQuery ( '$.user.email' , {
376+ column : table . metadata ,
377+ table : table ,
378+ queryType : 'steVecSelector' ,
379+ returnType : 'composite-literal' ,
380+ } )
381+ if ( queryResult . failure ) throw new Error ( queryResult . failure . message )
382+ const selectorTerm = queryResult . data
383+
384+ const rows = await sql . unsafe (
385+ `SELECT id, (metadata).data as metadata
386+ FROM "protect-ci-jsonb" t,
387+ eql_v2.jsonb_path_query(t.metadata, '${ selectorTerm } '::eql_v2_encrypted) as result
388+ WHERE t.test_run_id = '${ TEST_RUN_ID } '`
389+ )
390+
391+ expect ( rows . length ) . toBeGreaterThanOrEqual ( 1 )
392+ const matchingRow = rows . find ( ( r : any ) => r . id === inserted . id )
393+ expect ( matchingRow ) . toBeDefined ( )
394+
395+ const decrypted = await protectClient . decryptModel ( { metadata : matchingRow ! . metadata } )
396+ if ( decrypted . failure ) throw new Error ( decrypted . failure . message )
397+ expect ( decrypted . data . metadata ) . toEqual ( plaintext )
398+ } , 30000 )
399+
400+ it ( 'finds with deep nested path (Simple)' , async ( ) => {
401+ const plaintext = { target : { nested : { value : 'deep-simple' } } , marker : 'jpq-deep-simple' }
402+
403+ const encrypted = await protectClient . encryptModel ( { metadata : plaintext } , table )
404+ if ( encrypted . failure ) throw new Error ( encrypted . failure . message )
405+
406+ const [ inserted ] = await sql `
407+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
408+ VALUES (${ sql . json ( encrypted . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
409+ RETURNING id
410+ `
411+
412+ const queryResult = await protectClient . encryptQuery ( '$.target.nested.value' , {
413+ column : table . metadata ,
414+ table : table ,
415+ queryType : 'steVecSelector' ,
416+ returnType : 'composite-literal' ,
417+ } )
418+ if ( queryResult . failure ) throw new Error ( queryResult . failure . message )
419+ const selectorTerm = queryResult . data
420+
421+ const rows = await sql . unsafe (
422+ `SELECT id, (metadata).data as metadata
423+ FROM "protect-ci-jsonb" t,
424+ eql_v2.jsonb_path_query(t.metadata, '${ selectorTerm } '::eql_v2_encrypted) as result
425+ WHERE t.test_run_id = '${ TEST_RUN_ID } '`
426+ )
427+
428+ expect ( rows . length ) . toBeGreaterThanOrEqual ( 1 )
429+ const matchingRow = rows . find ( ( r : any ) => r . id === inserted . id )
430+ expect ( matchingRow ) . toBeDefined ( )
431+
432+ const decrypted = await protectClient . decryptModel ( { metadata : matchingRow ! . metadata } )
433+ if ( decrypted . failure ) throw new Error ( decrypted . failure . message )
434+ expect ( decrypted . data . metadata ) . toEqual ( plaintext )
435+ } , 30000 )
436+
437+ it ( 'non-matching path returns zero rows (Simple)' , async ( ) => {
438+ const plaintext = { data : true , marker : 'jpq-nomatch-simple' }
439+
440+ const encrypted = await protectClient . encryptModel ( { metadata : plaintext } , table )
441+ if ( encrypted . failure ) throw new Error ( encrypted . failure . message )
442+
443+ await sql `
444+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
445+ VALUES (${ sql . json ( encrypted . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
446+ `
447+
448+ const queryResult = await protectClient . encryptQuery ( '$.missing.path' , {
449+ column : table . metadata ,
450+ table : table ,
451+ queryType : 'steVecSelector' ,
452+ returnType : 'composite-literal' ,
453+ } )
454+ if ( queryResult . failure ) throw new Error ( queryResult . failure . message )
455+ const selectorTerm = queryResult . data
456+
457+ const rows = await sql . unsafe (
458+ `SELECT id, (metadata).data as metadata
459+ FROM "protect-ci-jsonb" t,
460+ eql_v2.jsonb_path_query(t.metadata, '${ selectorTerm } '::eql_v2_encrypted) as result
461+ WHERE t.test_run_id = '${ TEST_RUN_ID } '`
462+ )
463+
464+ expect ( rows . length ) . toBe ( 0 )
465+ } , 30000 )
325466 } )
326467
327468 // ─── Containment: @> term queries ──────────────────────────────────
@@ -1792,6 +1933,43 @@ describe('searchableJson postgres integration', () => {
17921933 expect ( decrypted . data . metadata ) . toEqual ( plaintext )
17931934 } , 30000 )
17941935
1936+ it ( 'returns true for nested path (Simple)' , async ( ) => {
1937+ const plaintext = { user : { email : 'pe-nested-simple@test.com' } , type : 'pe-nested-simple' }
1938+
1939+ const encrypted = await protectClient . encryptModel ( { metadata : plaintext } , table )
1940+ if ( encrypted . failure ) throw new Error ( encrypted . failure . message )
1941+
1942+ const [ inserted ] = await sql `
1943+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
1944+ VALUES (${ sql . json ( encrypted . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
1945+ RETURNING id
1946+ `
1947+
1948+ const queryResult = await protectClient . encryptQuery ( '$.user.email' , {
1949+ column : table . metadata ,
1950+ table : table ,
1951+ queryType : 'steVecSelector' ,
1952+ returnType : 'composite-literal' ,
1953+ } )
1954+ if ( queryResult . failure ) throw new Error ( queryResult . failure . message )
1955+ const selectorTerm = queryResult . data
1956+
1957+ const rows = await sql . unsafe (
1958+ `SELECT id, (metadata).data as metadata
1959+ FROM "protect-ci-jsonb" t
1960+ WHERE eql_v2.jsonb_path_exists(t.metadata, '${ selectorTerm } '::eql_v2_encrypted)
1961+ AND test_run_id = '${ TEST_RUN_ID } '`
1962+ )
1963+
1964+ expect ( rows . length ) . toBeGreaterThanOrEqual ( 1 )
1965+ const matchingRow = rows . find ( ( r : any ) => r . id === inserted . id )
1966+ expect ( matchingRow ) . toBeDefined ( )
1967+
1968+ const decrypted = await protectClient . decryptModel ( { metadata : matchingRow ! . metadata } )
1969+ if ( decrypted . failure ) throw new Error ( decrypted . failure . message )
1970+ expect ( decrypted . data . metadata ) . toEqual ( plaintext )
1971+ } , 30000 )
1972+
17951973 it ( 'returns false for unknown path (Simple)' , async ( ) => {
17961974 const plaintext = { exists : true , marker : 'pe-nomatch-simple' }
17971975
@@ -2631,6 +2809,29 @@ describe('searchableJson postgres integration', () => {
26312809 expect ( rows [ 0 ] . extracted ) . not . toBeNull ( )
26322810 } , 30000 )
26332811
2812+ it ( 'extracts nested field by chained text keys (Simple)' , async ( ) => {
2813+ const plaintext = { user : { email : 'fa-nested-simple@test.com' } , marker : 'fa-nested-simple' }
2814+
2815+ const encrypted = await protectClient . encryptModel ( { metadata : plaintext } , table )
2816+ if ( encrypted . failure ) throw new Error ( encrypted . failure . message )
2817+
2818+ const [ inserted ] = await sql `
2819+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
2820+ VALUES (${ sql . json ( encrypted . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
2821+ RETURNING id
2822+ `
2823+
2824+ const rows = await sql . unsafe (
2825+ `SELECT (t.metadata -> 'user') -> 'email' as extracted
2826+ FROM "protect-ci-jsonb" t
2827+ WHERE t.id = $1` ,
2828+ [ inserted . id ]
2829+ )
2830+
2831+ expect ( rows ) . toHaveLength ( 1 )
2832+ expect ( rows [ 0 ] . extracted ) . not . toBeNull ( )
2833+ } , 30000 )
2834+
26342835 it ( 'returns null for missing field (Simple)' , async ( ) => {
26352836 const plaintext = { exists : true , marker : 'fa-s-miss' }
26362837
@@ -2779,5 +2980,131 @@ describe('searchableJson postgres integration', () => {
27792980
27802981 expect ( rows ) . toHaveLength ( 0 )
27812982 } , 30000 )
2983+
2984+ it ( '-> extraction = self-comparison (Simple)' , async ( ) => {
2985+ const plaintext = { role : 'eq-self-s' , marker : 'eq-self-s-marker' }
2986+
2987+ const encrypted = await protectClient . encryptModel ( { metadata : plaintext } , table )
2988+ if ( encrypted . failure ) throw new Error ( encrypted . failure . message )
2989+
2990+ const [ inserted ] = await sql `
2991+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
2992+ VALUES (${ sql . json ( encrypted . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
2993+ RETURNING id
2994+ `
2995+
2996+ const rows = await sql . unsafe (
2997+ `SELECT id
2998+ FROM "protect-ci-jsonb" t
2999+ WHERE t.metadata -> 'role' = t.metadata -> 'role'
3000+ AND t.id = $1` ,
3001+ [ inserted . id ]
3002+ )
3003+
3004+ expect ( rows ) . toHaveLength ( 1 )
3005+ } , 30000 )
3006+
3007+ it ( 'jsonb_path_query_first = self-comparison (Simple)' , async ( ) => {
3008+ const plaintext = { role : 'eq-jpqf-s' , marker : 'eq-jpqf-s-marker' }
3009+
3010+ const encrypted = await protectClient . encryptModel ( { metadata : plaintext } , table )
3011+ if ( encrypted . failure ) throw new Error ( encrypted . failure . message )
3012+
3013+ const [ inserted ] = await sql `
3014+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
3015+ VALUES (${ sql . json ( encrypted . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
3016+ RETURNING id
3017+ `
3018+
3019+ const queryResult = await protectClient . encryptQuery ( '$.role' , {
3020+ column : table . metadata ,
3021+ table : table ,
3022+ queryType : 'steVecSelector' ,
3023+ returnType : 'composite-literal' ,
3024+ } )
3025+ if ( queryResult . failure ) throw new Error ( queryResult . failure . message )
3026+ const selectorTerm = queryResult . data
3027+
3028+ const rows = await sql . unsafe (
3029+ `SELECT id
3030+ FROM "protect-ci-jsonb" t
3031+ WHERE eql_v2.jsonb_path_query_first(t.metadata, $1::eql_v2_encrypted)
3032+ = eql_v2.jsonb_path_query_first(t.metadata, $1::eql_v2_encrypted)
3033+ AND t.id = $2` ,
3034+ [ selectorTerm , inserted . id ]
3035+ )
3036+
3037+ expect ( rows ) . toHaveLength ( 1 )
3038+ } , 30000 )
3039+
3040+ it ( '-> extraction = cross-row match (Simple)' , async ( ) => {
3041+ const plaintext1 = { role : 'eq-cross-s' , marker : 'eq-cross-s-1' }
3042+ const plaintext2 = { role : 'eq-cross-s' , marker : 'eq-cross-s-2' }
3043+
3044+ const enc1 = await protectClient . encryptModel ( { metadata : plaintext1 } , table )
3045+ if ( enc1 . failure ) throw new Error ( enc1 . failure . message )
3046+ const enc2 = await protectClient . encryptModel ( { metadata : plaintext2 } , table )
3047+ if ( enc2 . failure ) throw new Error ( enc2 . failure . message )
3048+
3049+ const [ row1 ] = await sql `
3050+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
3051+ VALUES (${ sql . json ( enc1 . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
3052+ RETURNING id
3053+ `
3054+ const [ row2 ] = await sql `
3055+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
3056+ VALUES (${ sql . json ( enc2 . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
3057+ RETURNING id
3058+ `
3059+
3060+ const rows = await sql . unsafe (
3061+ `SELECT t.id
3062+ FROM "protect-ci-jsonb" t
3063+ WHERE t.metadata -> 'role' = (
3064+ SELECT s.metadata -> 'role'
3065+ FROM "protect-ci-jsonb" s
3066+ WHERE s.id = $2
3067+ )
3068+ AND t.id = $1` ,
3069+ [ row1 . id , row2 . id ]
3070+ )
3071+
3072+ expect ( rows ) . toHaveLength ( 1 )
3073+ } , 30000 )
3074+
3075+ it ( '-> extraction != different value (Simple)' , async ( ) => {
3076+ const plaintext1 = { role : 'eq-diff-s-a' , marker : 'eq-diff-s-1' }
3077+ const plaintext2 = { role : 'eq-diff-s-b' , marker : 'eq-diff-s-2' }
3078+
3079+ const enc1 = await protectClient . encryptModel ( { metadata : plaintext1 } , table )
3080+ if ( enc1 . failure ) throw new Error ( enc1 . failure . message )
3081+ const enc2 = await protectClient . encryptModel ( { metadata : plaintext2 } , table )
3082+ if ( enc2 . failure ) throw new Error ( enc2 . failure . message )
3083+
3084+ const [ row1 ] = await sql `
3085+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
3086+ VALUES (${ sql . json ( enc1 . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
3087+ RETURNING id
3088+ `
3089+ const [ row2 ] = await sql `
3090+ INSERT INTO "protect-ci-jsonb" (metadata, test_run_id)
3091+ VALUES (${ sql . json ( enc2 . data . metadata ) } ::eql_v2_encrypted, ${ TEST_RUN_ID } )
3092+ RETURNING id
3093+ `
3094+
3095+ const rows = await sql . unsafe (
3096+ `SELECT t.id
3097+ FROM "protect-ci-jsonb" t
3098+ WHERE t.metadata -> 'role' = (
3099+ SELECT s.metadata -> 'role'
3100+ FROM "protect-ci-jsonb" s
3101+ WHERE s.id = $2
3102+ )
3103+ AND t.id = $1` ,
3104+ [ row1 . id , row2 . id ]
3105+ )
3106+
3107+ expect ( rows ) . toHaveLength ( 0 )
3108+ } , 30000 )
27823109 } )
27833110} )
0 commit comments