Skip to content
9 changes: 6 additions & 3 deletions cypher/models/pgsql/test/translation_cases/nodes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,13 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id));

-- case: match (s) where not (s)-[]->()-[]->() return s
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select 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.id = e0.end_id), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select count(*) > 0 from s2));
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from edge e0 join node n1 on n1.id = e0.end_id where (s0.n0).id = e0.start_id), s2 as (select s1.n0 as n0, s1.n1 as n1 from s1 join edge e1 on (s1.n1).id = e1.start_id join node n2 on n2.id = e1.end_id) select count(*) > 0 from s2));

-- case: match (s) where not (s)-[{prop: 'a'}]-({name: 'n3'}) return s
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from s0 join edge e0 on ((s0.n0).id = e0.end_id or (s0.n0).id = e0.start_id) join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb and (n1.id = e0.end_id or n1.id = e0.start_id) where ((s0.n0).id <> n1.id) and ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb) select count(*) > 0 from s1));

-- case: match (s) where not (s)<-[{prop: 'a'}]-({name: 'n3'}) return s
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from s0 join edge e0 on (s0.n0).id = e0.end_id join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb and n1.id = e0.start_id where ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb) select count(*) > 0 from s1));
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from edge e0 join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb and n1.id = e0.start_id where ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb and (s0.n0).id = e0.end_id) select count(*) > 0 from s1));

-- case: match (n:NodeKind1) where n.distinguishedname = toUpper('admin') return n
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'distinguishedname'))::jsonb = to_jsonb((upper('admin')::text)::text)::jsonb) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0;
Expand All @@ -229,7 +229,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (cypher_ends_with((n0.properties ->> 'distinguishedname'), (upper('admin')::text)::text)::bool) and n0.kind_ids operator (pg_catalog.@>) array [1]::int2[]) select s0.n0 as n from s0;

-- case: match (s) where not (s)-[{prop: 'a'}]->({name: 'n3'}) return s
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from s0 join edge e0 on (s0.n0).id = e0.start_id join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb and n1.id = e0.end_id where ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb) select count(*) > 0 from s1));
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as s from s0 where (not (with s1 as (select s0.n0 as n0 from edge e0 join node n1 on ((n1.properties -> 'name'))::jsonb = to_jsonb(('n3')::text)::jsonb and n1.id = e0.end_id where ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb and (s0.n0).id = e0.start_id) select count(*) > 0 from s1));

-- case: match (s) where not (s)-[]-() return id(s)
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select (s0.n0).id from s0 where (not exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id));
Expand Down Expand Up @@ -338,3 +338,6 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from

-- case: match (n) where n.name = "alpha' || (SELECT inet_server_addr()::text::int) || '" return n
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where (((n0.properties -> 'name'))::jsonb = to_jsonb(('alpha'' || (SELECT inet_server_addr()::text::int) || ''')::text)::jsonb)) select s0.n0 as n from s0;

-- case: match (g:NodeKind2) where not ((g)<-[:EdgeKind1]-(:NodeKind1)) return g
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 [2]::int2[]) select s0.n0 as g from s0 where (not ((with s1 as (select s0.n0 as n0 from edge e0 join node n1 on n1.kind_ids operator (pg_catalog.@>) array [1]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[]) and (s0.n0).id = e0.end_id) select count(*) > 0 from s1)));
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,12 @@ with s0 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::e

-- case: MATCH p=(:GPO)-[r:GPLink|Contains*1..]->(:Base) WHERE HEAD(r).enforced OR NONE(n in TAIL(TAIL(NODES(p))) WHERE (n:OU AND n.blocksinheritance)) RETURN p
with s0 as (with recursive s1_seed(root_id) as not materialized (select n0.id as root_id from node n0 where n0.kind_ids operator (pg_catalog.@>) array [8]::int2[]), s1(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id, e0.end_id, 1, n1.kind_ids operator (pg_catalog.@>) array [10]::int2[], e0.start_id = e0.end_id, array [e0.id] from s1_seed join edge e0 on e0.start_id = s1_seed.root_id join node n1 on n1.id = e0.end_id where e0.kind_id = any (array [11, 12]::int2[]) union all select s1.root_id, e0.end_id, s1.depth + 1, n1.kind_ids operator (pg_catalog.@>) array [10]::int2[], false, s1.path || e0.id from s1 join lateral (select e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties from edge e0 where e0.start_id = s1.next_id and e0.id != all (s1.path) and e0.kind_id = any (array [11, 12]::int2[]) offset 0) e0 on true join node n1 on n1.id = e0.end_id where s1.depth < 15 and not s1.is_cycle) select (select coalesce(array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(s1.path) with ordinality as _path(id, ordinality) join edge e0 on e0.id = _path.id) 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 lateral (select n0.id, n0.kind_ids, n0.properties from node n0 where n0.id = s1.root_id offset 0) n0 on true join lateral (select n1.id, n1.kind_ids, n1.properties from node n1 where n1.id = s1.next_id offset 0) n1 on true where s1.satisfied) select ordered_edges_to_path(s0.n0, s0.e0, array [s0.n0, s0.n1]::nodecomposite[])::pathcomposite as p from s0 where (((((s0.e0)[1]).properties ->> 'enforced'))::bool or ((select count(*)::int from unnest(coalesce((coalesce((((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])[2:cardinality(((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])[2:cardinality(coalesce((((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])[2:cardinality(((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[]) as i0 where ((i0.kind_ids operator (pg_catalog.@>) array [9]::int2[] and ((i0.properties ->> 'blocksinheritance'))::bool))) = 0 and coalesce((coalesce((((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])[2:cardinality(((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])[2:cardinality(coalesce((((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])[2:cardinality(((ordered_edges_to_path(((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, s0.e0, array [((s0.n0).id, (s0.n0).kind_ids, (s0.n0).properties)::nodecomposite, ((s0.n1).id, (s0.n1).kind_ids, (s0.n1).properties)::nodecomposite]::nodecomposite[])::pathcomposite).nodes)::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[])::int], array []::nodecomposite[])::nodecomposite[] is not null)::bool);

-- case: match (a), (b) where (a)--(b) return a, b
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1) select s1.n0 as a, s1.n1 as b from s1 where (exists (select 1 from edge e0 where (e0.start_id = (s1.n0).id and e0.end_id = (s1.n1).id) or (e0.start_id = (s1.n1).id and e0.end_id = (s1.n0).id)));

-- case: match (a) where ()--(a) return a
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as a from s0 where (exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id));

-- case: match (a) where (a)--() return a
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0) select s0.n0 as a from s0 where (exists (select 1 from edge e0 where e0.start_id = (s0.n0).id or e0.end_id = (s0.n0).id));
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0, (n1
with s0 as (select (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 where e0.kind_id = any (array [3, 4]::int2[])) select ((s0.n0).properties -> 'name'), ((s0.n1).properties -> 'name') from s0;

-- case: match (s)-[r:EdgeKind1]->() where (s)-[r {prop: 'a'}]->() return s
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 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb and e0.kind_id = any (array [3]::int2[])) select s0.n0 as s from s0 where ((with s1 as (select s0.e0 as e0, s0.n0 as n0 from s0 join edge e0 on (s0.n0).id = (s0.e0).start_id join node n2 on n2.id = (s0.e0).end_id) select count(*) > 0 from s1));
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 from edge e0 join node n0 on n0.id = e0.start_id join node n1 on n1.id = e0.end_id where ((e0.properties -> 'prop'))::jsonb = to_jsonb(('a')::text)::jsonb and e0.kind_id = any (array [3]::int2[])) select s0.n0 as s from s0 where ((with s1 as (select s0.e0 as e0, s0.n0 as n0 from edge e0 join node n2 on n2.id = (s0.e0).end_id where (s0.n0).id = (s0.e0).start_id) select count(*) > 0 from s1));

-- case: match (s)-[r:EdgeKind1]->(e) where not (s.system_tags contains 'admin_tier_0') and id(e) = 1 return id(s), labels(s), id(r), type(r)
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 n1 on (n1.id = 1) and n1.id = e0.end_id join node n0 on (not (coalesce((n0.properties ->> 'system_tags'), '')::text like '%admin\_tier\_0%')) and n0.id = e0.start_id where e0.kind_id = any (array [3]::int2[])) select (s0.n0).id, (array(select _kind.name from generate_subscripts((s0.n0).kind_ids, 1) as _kind_idx, kind _kind where _kind.id = ((s0.n0).kind_ids)[_kind_idx] order by _kind_idx))::text[], (s0.e0).id, kind_name((s0.e0).kind_id)::text from s0;
Expand Down
95 changes: 83 additions & 12 deletions cypher/models/pgsql/translate/predicate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,72 @@ func (s *Translator) preparePatternPredicate() error {
}

func (s *Translator) buildOptimizedRelationshipExistPredicate(part *PatternPart, traversalStep *TraversalStep) (pgsql.Expression, error) {
whereClause := pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}),
pgsql.OperatorOr,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID}),
)
var whereClause pgsql.Expression
if traversalStep.LeftNodeBound && traversalStep.RightNodeBound {
// Pair-wise bounds on the directionless relationship
whereClause = pgsql.NewBinaryExpression(
pgsql.NewParenthetical(
pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
pgsql.OperatorAnd,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID},
),
),
),
pgsql.OperatorOr,
pgsql.NewParenthetical(
pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID},
),
pgsql.OperatorAnd,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
),
),
)
} else if traversalStep.RightNodeBound {
whereClause = pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID},
),
pgsql.OperatorOr,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.RightNode.Identifier, pgsql.ColumnID},
),
)
} else {
// Left-side node is bound OR neither is bound
whereClause = pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
pgsql.OperatorOr,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
)
}
Comment on lines +75 to +90
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle the fully-unbound optimization separately.

This fallback also covers !LeftNodeBound && !RightNodeBound, but it still compares against traversalStep.LeftNode.Identifier.id. That identifier is never introduced in the subquery, so a legal predicate like WHERE ()--() generates invalid SQL instead of a plain edge-existence check.

Suggested fix
-	} else {
-		// Left-side node is bound OR neither is bound
+	} else if traversalStep.LeftNodeBound {
+		// Left-side node is bound
 		whereClause = pgsql.NewBinaryExpression(
 			pgsql.NewBinaryExpression(
 				pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
 				pgsql.OperatorEquals,
 				pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
@@
 			),
 		)
+	} else {
+		// Neither side is bound; any edge satisfies the predicate.
+		whereClause = pgsql.NewLiteral(true, pgsql.Boolean)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
// Left-side node is bound OR neither is bound
whereClause = pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
pgsql.OperatorOr,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
)
}
} else if traversalStep.LeftNodeBound {
// Left-side node is bound
whereClause = pgsql.NewBinaryExpression(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnStartID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
pgsql.OperatorOr,
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{traversalStep.Edge.Identifier, pgsql.ColumnEndID},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{traversalStep.LeftNode.Identifier, pgsql.ColumnID},
),
)
} else {
// Neither side is bound; any edge satisfies the predicate.
whereClause = pgsql.NewLiteral(true, pgsql.Boolean)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cypher/models/pgsql/translate/predicate.go` around lines 75 - 90, The current
fallback branch still references traversalStep.LeftNode.Identifier even when
both LeftNodeBound and RightNodeBound are false, producing invalid SQL for a
fully-unbound pattern; add an explicit branch that detects
(!traversalStep.LeftNodeBound && !traversalStep.RightNodeBound) and for that
case set whereClause to a simple edge-existence predicate (e.g., an IS NOT NULL
or existence check on traversalStep.Edge.Identifier/its primary id) or omit the
predicate entirely instead of comparing to traversalStep.LeftNode.Identifier.id;
update the logic around whereClause construction (the block using
pgsql.NewBinaryExpression with ColumnStartID/ColumnEndID and ColumnID) to only
run when at least one side is bound.


if err := RewriteFrameBindings(s.scope, whereClause); err != nil {
return nil, err
Expand Down Expand Up @@ -83,6 +138,8 @@ func (s *Translator) translatePatternPredicate() error {
return nil
}

// buildPatternPredicates is used by translateMatch to resolve deferred pattern predicate
// futures collected for the current MATCH/OPTIONAL MATCH query part's WHERE expressions
func (s *Translator) buildPatternPredicates() error {
for _, predicateFuture := range s.query.CurrentPart().patternPredicates {
var (
Expand All @@ -94,6 +151,8 @@ func (s *Translator) buildPatternPredicates() error {
}
)

// Single-step traversals can use an optimized relationship-exists step instead of
// following the "long" traversal step building process.
if len(patternPart.TraversalSteps) == 1 {
var (
traversalStep = patternPart.TraversalSteps[0]
Expand Down Expand Up @@ -142,7 +201,19 @@ func (s *Translator) buildPatternPredicates() error {
})
}
} else {
if traversalStepQuery, err := s.buildTraversalPatternRoot(traversalStep.Frame, traversalStep); err != nil {
var (
traversalStepQuery pgsql.Query
err error
)
// We also want to be able to build correlated root steps for undirected traversals
// if traversalStep.Direction != graph.DirectionBoth && (traversalStep.LeftNodeBound || traversalStep.RightNodeBound) {
if traversalStep.LeftNodeBound || traversalStep.RightNodeBound {
traversalStepQuery, err = s.buildTraversalPatternRootWithOuterCorrelation(traversalStep.Frame, traversalStep)
} else {
traversalStepQuery, err = s.buildTraversalPatternRoot(traversalStep.Frame, traversalStep)
}

if err != nil {
return err
} else {
subQuery.AddCTE(pgsql.CommonTableExpression{
Expand Down
Loading
Loading