From 04565e3b720315b66088cde1cd6b831a5590e1c7 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Tue, 24 Mar 2026 10:45:25 -0700 Subject: [PATCH 1/2] chore: expansion in shortest path ref to prev part BP-2257 --- cypher/Cypher Syntax Support.md | 14 -------------- .../pgsql/test/translation_cases/multipart.sql | 2 ++ .../test/translation_cases/pattern_binding.sql | 8 ++++++++ .../test/translation_cases/shortest_paths.sql | 7 +++++++ 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/cypher/Cypher Syntax Support.md b/cypher/Cypher Syntax Support.md index 2255d5b..c78c2ad 100644 --- a/cypher/Cypher Syntax Support.md +++ b/cypher/Cypher Syntax Support.md @@ -354,20 +354,6 @@ return p limit 1 Queries that contain similar constructs will result in the following translation error: `ERROR: column notation .id applied to type nodecomposite[], which is not a composite type (SQLSTATE 42809)`. -### Right-Hand Bound Node Lookups - -Patterns that utilize a bound reference in the right-hand node pattern will not correctly author the required SQL joins: - -``` -match (e) -match p = ()-[]->(e) -return p -limit 1 -``` - -Queries that contain similar constructs will result in the following translation error: -`ERROR: invalid reference to FROM-clause entry for table "s0" (SQLSTATE 42P01)`. - ### Untyped Array References and Literals Untyped array references, including empty arrays, fail to pass type inference checks in CySQL. Support for additional diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index 5b7c05a..bf9d69e 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -87,3 +87,5 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e -- case: MATCH (m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2)-[:EdgeKind2]->(c3:NodeKind1) WHERE m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' AND NOT m.samaccountname contains "DEX" AND NOT g.name IN ["D"] AND NOT m.samaccountname =~ "^.*$" WITH COLLECT(g.name) AS admingroups MATCH p=(m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2) WHERE m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' AND g.name IN admingroups AND NOT m.samaccountname =~ "^.*$" RETURN p with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s2.path), s2.path || e0.id from s2 join edge e0 on e0.start_id = s2.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and s2.depth < 15 and not s2.is_cycle) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s2.path)) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join node n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s0, edge e2 join node n4 on n4.id = e2.end_id join node n3 on n3.id = e2.start_id where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0)) and e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.id = any (s5.path), s5.path || e2.id from s5 join edge e2 on e2.end_id = s5.next_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) and s5.depth < 15 and not s5.is_cycle) select (select array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite) from edge e2 where e2.id = any (s5.path)) as e2, s5.path as ep1, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join node n4 on n4.id = s5.root_id join node n3 on n3.id = s5.next_id where s5.satisfied) select edges_to_path(variadic ep1)::pathcomposite as p from s4; +-- case: match (a:NodeKind2)-[:EdgeKind1]->(g:NodeKind1)-[:EdgeKind2]->(s:NodeKind2) with count(a) as uc where uc > 5 match p = (a)-[:EdgeKind1]->(g)-[:EdgeKind2]->(s) return p +with s0 as (with s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select count(s2.n0)::int8 as i0 from s2), s3 as (select (e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite as e2, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, edge e2 join node n3 on n3.id = e2.start_id join node n4 on n4.id = e2.end_id where e2.kind_id = any (array [3]::int2[]) and (s0.i0 > 5)), s4 as (select s3.e2 as e2, (e3.id, e3.start_id, e3.end_id, e3.kind_id, e3.properties)::edgecomposite as e3, s3.i0 as i0, s3.n3 as n3, s3.n4 as n4, (n5.id, n5.kind_ids, n5.properties)::nodecomposite as n5 from s3 join edge e3 on (s3.n4).id = e3.start_id join node n5 on n5.id = e3.end_id where e3.kind_id = any (array [4]::int2[])) select edges_to_path(variadic array [(s4.e2).id, (s4.e3).id]::int8[])::pathcomposite as p from s4; diff --git a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql index 2df21cd..9d131ce 100644 --- a/cypher/models/pgsql/test/translation_cases/pattern_binding.sql +++ b/cypher/models/pgsql/test/translation_cases/pattern_binding.sql @@ -68,3 +68,11 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from -- case: match (x:NodeKind1{name:'foo'}) match p=(x)-[:EdgeKind1]->(y:NodeKind2{name:'bar'}) return p with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and (n0.properties ->> 'name') = 'foo'), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and (n1.properties ->> 'name') = 'bar' and n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[])) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p from s1; +-- case: match (e) match p = ()-[]->(e) return p limit 1 +with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on n1.id = e0.start_id) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p from s1 limit 1; + +-- case: match p = (a)-[]->() match q = ()-[]->(a) return p, q +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id), s1 as (select s0.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s0.n0 as n0, s0.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0 join edge e1 on (s0.n0).id = e1.end_id join node n2 on n2.id = e1.start_id) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p, edges_to_path(variadic array [(s1.e1).id]::int8[])::pathcomposite as q from s1; + +-- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p +with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] union select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s2.path), s2.path || e0.id from s2 join edge e0 on e0.start_id = s2.next_id join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s2.path)) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join node n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s0, edge e2 join node n4 on n4.id = e2.end_id join node n3 on n3.id = e2.start_id where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0)) union select s5.root_id, e2.start_id, s5.depth + 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.id = any (s5.path), s5.path || e2.id from s5 join edge e2 on e2.end_id = s5.next_id join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select (select array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite) from edge e2 where e2.id = any (s5.path)) as e2, s5.path as ep1, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join node n4 on n4.id = s5.root_id join node n3 on n3.id = s5.next_id where s5.satisfied) select edges_to_path(variadic ep1)::pathcomposite as p from s4; diff --git a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql index a31c1e7..1a91ec8 100644 --- a/cypher/models/pgsql/test/translation_cases/shortest_paths.sql +++ b/cypher/models/pgsql/test/translation_cases/shortest_paths.sql @@ -58,3 +58,10 @@ with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (sele -- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n1 on n1.id = e0.start_id join node n0 on n0.id = e0.end_id where ((n1.properties -\u003e\u003e 'name') = '123') and e0.kind_id = any (array [3]::int2[]);","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id join node n0 on n0.id = e0.end_id where e0.kind_id = any (array [3]::int2[]);","pi2":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.end_id, e0.start_id, 1, ((n1.properties -\u003e\u003e 'name') = '123'), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n1 on n1.id = e0.start_id join node n0 on n0.id = e0.end_id where (coalesce((n0.properties -\u003e\u003e 'system_tags'), '')::text like '%admin_tier_0%') and n0.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]);","pi3":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.start_id, s1.depth + 1, ((n1.properties -\u003e\u003e 'name') = '123'), e0.id = any (s1.path), e0.id || s1.path from backward_front s1 join edge e0 on e0.end_id = s1.next_id join node n1 on n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]);"} with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from bidirectional_asp_harness(@pi0::text, @pi1::text, @pi2::text, @pi3::text, 15)) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n1 on n1.id = s1.root_id join node n0 on n0.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 where ((s0.n1).id <> (s0.n0).id); +-- case: match p=shortestPath((a)-[:EdgeKind1*]->(b:NodeKind1)) where a <> b return p +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]);", "pi1": "insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [1]::int2[], e0.id = any (s1.path), s1.path || e0.id from forward_front s1 join edge e0 on e0.start_id = s1.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]);"} +with s0 as (with s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s1.path)) as e0, s1.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join node n0 on n0.id = s1.root_id join node n1 on n1.id = s1.next_id) select edges_to_path(variadic ep0)::pathcomposite as p from s0 where ((s0.n0).id <> (s0.n1).id); + +-- case: MATCH p=(c:NodeKind1)-[]->(u:NodeKind2) MATCH p2=shortestPath((u:NodeKind2)-[*1..]->(d:NodeKind1)) RETURN p,p2 LIMIT 500 +-- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e1.start_id, e1.end_id, 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.start_id = e1.end_id, array [e1.id] from edge e1 join node n1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where (s0.n1).kind_ids operator (pg_catalog.@>) array [2]::int2[];", "pi1": "insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s2.root_id, e1.end_id, s2.depth + 1, n2.kind_ids operator (pg_catalog.@>) array [1]::int2[], e1.id = any (s2.path), s2.path || e1.id from forward_front s2 join edge e1 on e1.start_id = s2.next_id join node n2 on n2.id = e1.end_id;"} +with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id), s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select s0.e0 as e0, (select array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite) from edge e1 where e1.id = any (s2.path)) as e1, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join node n1 on n1.id = s2.root_id join node n2 on n2.id = s2.next_id where (s0.n1).id = s2.root_id) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p, edges_to_path(variadic ep0)::pathcomposite as p2 from s1 limit 500; From 15ff20c01aab84fb6472cd9aa0a1a89cc3950e20 Mon Sep 17 00:00:00 2001 From: Ulises Rangel Date: Tue, 24 Mar 2026 13:54:58 -0500 Subject: [PATCH 2/2] fix: shortest path expansion references --- .../test/translation_cases/multipart.sql | 4 -- cypher/models/pgsql/translate/expansion.go | 49 +++++++++++++++++-- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index bf9d69e..6ed1ec5 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -80,10 +80,6 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e -- case: match (m:NodeKind1)-[*1..]->(g:NodeKind2)-[]->(c3:NodeKind1) where not g.name in ["foo"] with collect(g.name) as bar match p=(m:NodeKind1)-[*1..]->(g:NodeKind2) where g.name in bar return p with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] union select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['foo']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s2.path), s2.path || e0.id from s2 join edge e0 on e0.start_id = s2.next_id join node n1 on n1.id = e0.end_id where s2.depth < 15 and not s2.is_cycle) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s2.path)) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join node n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s0, edge e2 join node n4 on n4.id = e2.end_id join node n3 on n3.id = e2.start_id where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0)) union select s5.root_id, e2.start_id, s5.depth + 1, n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.id = any (s5.path), s5.path || e2.id from s5 join edge e2 on e2.end_id = s5.next_id join node n3 on n3.id = e2.start_id where s5.depth < 15 and not s5.is_cycle) select (select array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite) from edge e2 where e2.id = any (s5.path)) as e2, s5.path as ep1, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join node n4 on n4.id = s5.root_id join node n3 on n3.id = s5.next_id where s5.satisfied) select edges_to_path(variadic ep1)::pathcomposite as p from s4; --- case: MATCH p=(c:NodeKind1)-[]->(u:NodeKind2) MATCH p2=shortestPath((u:NodeKind2)-[*1..]->(d:NodeKind1)) RETURN p, p2 LIMIT 500 --- pgsql_params:{"pi0":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select e1.start_id, e1.end_id, 1, n2.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e1.start_id = e1.end_id, array [e1.id] from edge e1 join node n1 on (s0.n1).id = e1.start_id join node n2 on n2.id = e1.end_id where (s0.n1).kind_ids operator (pg_catalog.@\u003e) array [2]::int2[];","pi1":"insert into next_front (root_id, next_id, depth, satisfied, is_cycle, path) select s2.root_id, e1.end_id, s2.depth + 1, n2.kind_ids operator (pg_catalog.@\u003e) array [1]::int2[], e1.id = any (s2.path), s2.path || e1.id from forward_front s2 join edge e1 on e1.start_id = s2.next_id join node n2 on n2.id = e1.end_id;"} -with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.end_id), s1 as (with s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select * from unidirectional_sp_harness(@pi0::text, @pi1::text, 15)) select s0.e0 as e0, (select array_agg((e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite) from edge e1 where e1.id = any (s2.path)) as e1, s2.path as ep0, s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s0, s2 join node n1 on n1.id = s2.root_id join node n2 on n2.id = s2.next_id where (s0.n1).id = s2.root_id) select edges_to_path(variadic array [(s1.e0).id]::int8[])::pathcomposite as p, edges_to_path(variadic ep0)::pathcomposite as p2 from s1 limit 500; - -- case: MATCH (m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2)-[:EdgeKind2]->(c3:NodeKind1) WHERE m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' AND NOT m.samaccountname contains "DEX" AND NOT g.name IN ["D"] AND NOT m.samaccountname =~ "^.*$" WITH COLLECT(g.name) AS admingroups MATCH p=(m:NodeKind1)-[:EdgeKind1*1..]->(g:NodeKind2) WHERE m.samaccountname =~ '^[A-Z]{1,3}[0-9]{1,3}$' AND g.name IN admingroups AND NOT m.samaccountname =~ "^.*$" RETURN p with s0 as (with s1 as (with recursive s2(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((n0.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not coalesce((n0.properties ->> 'samaccountname'), '')::text like '%DEX%' and not (n0.properties ->> 'samaccountname') ~ '^.*$') and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[] and e0.kind_id = any (array [3]::int2[]) union select s2.root_id, e0.end_id, s2.depth + 1, (not (n1.properties ->> 'name') = any (array ['D']::text[])) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[], e0.id = any (s2.path), s2.path || e0.id from s2 join edge e0 on e0.start_id = s2.next_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [3]::int2[]) and s2.depth < 15 and not s2.is_cycle) select (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite) from edge e0 where e0.id = any (s2.path)) as e0, s2.path as ep0, (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s2 join node n0 on n0.id = s2.root_id join node n1 on n1.id = s2.next_id where s2.satisfied), s3 as (select s1.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s1.ep0 as ep0, s1.n0 as n0, s1.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[])) select array_remove(coalesce(array_agg(((s3.n1).properties ->> 'name'))::anyarray, array []::text[])::anyarray, null)::anyarray as i0 from s3), s4 as (with recursive s5(root_id, next_id, depth, satisfied, is_cycle, path) as (select e2.end_id, e2.start_id, 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.end_id = e2.start_id, array [e2.id] from s0, edge e2 join node n4 on n4.id = e2.end_id join node n3 on n3.id = e2.start_id where n4.kind_ids operator (pg_catalog.@>) array [2]::int2[] and ((n4.properties ->> 'name') = any (s0.i0)) and e2.kind_id = any (array [3]::int2[]) union select s5.root_id, e2.start_id, s5.depth + 1, ((n3.properties ->> 'samaccountname') ~ '^[A-Z]{1,3}[0-9]{1,3}$' and not (n3.properties ->> 'samaccountname') ~ '^.*$') and n3.kind_ids operator (pg_catalog.@>) array [1]::int2[], e2.id = any (s5.path), s5.path || e2.id from s5 join edge e2 on e2.end_id = s5.next_id join node n3 on n3.id = e2.start_id where e2.kind_id = any (array [3]::int2[]) and s5.depth < 15 and not s5.is_cycle) select (select array_agg((e2.id, e2.start_id, e2.end_id, e2.kind_id, e2.properties)::edgecomposite) from edge e2 where e2.id = any (s5.path)) as e2, s5.path as ep1, s0.i0 as i0, (n3.id, n3.kind_ids, n3.properties)::nodecomposite as n3, (n4.id, n4.kind_ids, n4.properties)::nodecomposite as n4 from s0, s5 join node n4 on n4.id = s5.root_id join node n3 on n3.id = s5.next_id where s5.satisfied) select edges_to_path(variadic ep1)::pathcomposite as p from s4; diff --git a/cypher/models/pgsql/translate/expansion.go b/cypher/models/pgsql/translate/expansion.go index ba2a821..540d4ee 100644 --- a/cypher/models/pgsql/translate/expansion.go +++ b/cypher/models/pgsql/translate/expansion.go @@ -70,9 +70,50 @@ func nextFrontInsert(body pgsql.SetExpression) pgsql.Insert { } } +func deframeExpression(expression pgsql.Expression) pgsql.Expression { + if expression == nil { + return nil + } + + switch typedExpression := expression.(type) { + case pgsql.RowColumnReference: + if compound, ok := typedExpression.Identifier.(pgsql.CompoundIdentifier); ok && len(compound) >= 2 { + // Drop the frame prefix and keep only the leaf identifier + column. + return pgsql.CompoundIdentifier{compound[len(compound)-1], typedExpression.Column} + } + + return expression + + case *pgsql.BinaryExpression: + return &pgsql.BinaryExpression{ + Operator: typedExpression.Operator, + LOperand: deframeExpression(typedExpression.LOperand), + ROperand: deframeExpression(typedExpression.ROperand), + } + + case *pgsql.Parenthetical: + return &pgsql.Parenthetical{ + Expression: deframeExpression(typedExpression.Expression), + } + + default: + return expression + } +} + func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expansion) pgsql.Select { + var ( + primerNodeConstraints = expansionModel.PrimerNodeConstraints + primerNodeJoinCondition = expansionModel.PrimerNodeJoinCondition + ) + + if s.traversalStep.LeftNodeBound { + primerNodeConstraints = deframeExpression(primerNodeConstraints) + primerNodeJoinCondition = pgd.Equals(pgd.Column(s.traversalStep.LeftNode.Identifier, pgsql.ColumnID), pgd.StartID(s.traversalStep.Edge.Identifier)) + } + nextQuery := pgsql.Select{ - Where: pgsql.OptionalAnd(expansionModel.PrimerNodeConstraints, expansionModel.EdgeConstraints), + Where: pgsql.OptionalAnd(primerNodeConstraints, expansionModel.EdgeConstraints), } nextQuery.Projection = []pgsql.SelectItem{ @@ -124,7 +165,7 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans }, } - if expansionModel.PrimerNodeConstraints != nil { + if primerNodeJoinCondition != nil { nextQueryFrom.Joins = append(nextQueryFrom.Joins, pgsql.Join{ Table: pgsql.TableReference{ Name: pgsql.CompoundIdentifier{pgsql.TableNode}, @@ -132,7 +173,7 @@ func (s *ExpansionBuilder) prepareForwardFrontPrimerQuery(expansionModel *Expans }, JoinOperator: pgsql.JoinOperator{ JoinType: pgsql.JoinTypeInner, - Constraint: s.traversalStep.Expansion.PrimerNodeJoinCondition, + Constraint: primerNodeJoinCondition, }, }) } @@ -296,7 +337,7 @@ func (s *ExpansionBuilder) prepareBackwardFrontPrimerQuery(expansionModel *Expan }, } - if expansionModel.PrimerNodeConstraints != nil { + if expansionModel.PrimerNodeJoinCondition != nil { nextQueryFrom.Joins = append(nextQueryFrom.Joins, pgsql.Join{ Table: pgsql.TableReference{ Name: pgsql.CompoundIdentifier{pgsql.TableNode},