diff --git a/cypher/models/pgsql/operators.go b/cypher/models/pgsql/operators.go index 14970398..9af83f70 100644 --- a/cypher/models/pgsql/operators.go +++ b/cypher/models/pgsql/operators.go @@ -90,6 +90,8 @@ const ( OperatorRegexMatch Operator = "~" OperatorAssignment Operator = "=" OperatorAdditionAssignment Operator = "+=" + OperatorIsDistinctFrom Operator = "is distinct from" + OperatorIsNotDistinctFrom Operator = "is not distinct from" OperatorCypherRegexMatch Operator = "=~" OperatorCypherStartsWith Operator = "starts with" diff --git a/cypher/models/pgsql/test/translation_cases/multipart.sql b/cypher/models/pgsql/test/translation_cases/multipart.sql index 919e83e5..d8117b65 100644 --- a/cypher/models/pgsql/test/translation_cases/multipart.sql +++ b/cypher/models/pgsql/test/translation_cases/multipart.sql @@ -108,4 +108,16 @@ with s0 as (with s1 as (with recursive s2_seed(root_id) as not materialized (sel with s0 as (with s1 as (select e0.id 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, 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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n2.id = e1.end_id where e1.kind_id = any (array [4]::int2[]) and e1.id != s1.e0) select count(s2.n0)::int8 as i0 from s2), s3 as (select e2.id 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 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[]) and e3.id != s3.e2) select case when (s4.n3).id is null or s4.e2 is null or (s4.n4).id is null or s4.e3 is null or (s4.n5).id is null then null else ordered_edges_to_path(s4.n3, (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e2]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id) || (select coalesce(array_agg((_edge.id, _edge.start_id, _edge.end_id, _edge.kind_id, _edge.properties)::edgecomposite order by _path.ordinality), array []::edgecomposite[]) from unnest(array [s4.e3]::int8[]) with ordinality as _path(id, ordinality) join edge _edge on _edge.id = _path.id), array [s4.n3, s4.n4, s4.n5]::nodecomposite[])::pathcomposite end as p from s4; -- case: match (g:NodeKind1) optional match (g)<-[r:EdgeKind1]-(m:NodeKind2) with g, count(r) as memberCount where memberCount = 0 return g -with s0 as (with s1 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[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join edge e0 on (s1.n0).id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])), s3 as (select s1.n0 as n0, s2.e0 as e0, s2.n1 as n1 from s1 left outer join s2 on (s1.n0 = s2.n0)) select s3.n0 as n0, count(s3.e0)::int8 as i0 from s3 group by n0) select s0.n0 as g from s0 where (s0.i0 = 0); +with s0 as (with s1 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[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join edge e0 on (s1.n0).id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])), s3 as (select s1.n0 as n0, s2.e0 as e0, s2.n1 as n1 from s1 left outer join s2 on (s1.n0 is not distinct from s2.n0)) select s3.n0 as n0, count(s3.e0)::int8 as i0 from s3 group by n0) select s0.n0 as g from s0 where (s0.i0 = 0); + +-- case: match (g:NodeKind1) optional match (g)<-[r:EdgeKind1]-(m:NodeKind2) with g, count(r) as memberCount where memberCount = 0 return g +with s0 as (with s1 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[]), s2 as (select (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0, s1.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s1 join edge e0 on (s1.n0).id = e0.end_id join node n1 on n1.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])), s3 as (select s1.n0 as n0, s2.e0 as e0, s2.n1 as n1 from s1 left outer join s2 on (s1.n0 is not distinct from s2.n0)) select s3.n0 as n0, count(s3.e0)::int8 as i0 from s3 group by n0) select s0.n0 as g from s0 where (s0.i0 = 0); + +-- case: match (g:NodeKind1) optional match (m:NodeKind2)-[:EdgeKind1]->(g)-[:EdgeKind1]->(m2:NodeKind2) 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 [1]::int2[]), 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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_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.n0).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 [3]::int2[]) and e1.id != (s1.e0).id), s3 as (select s0.n0 as n0, s2.e0 as e0, s2.e1 as e1, s2.n1 as n1, s2.n2 as n2 from s0 left outer join s2 on (s0.n0 is not distinct from s2.n0)) select s3.n0 as g from s3; + +-- case: match (g:NodeKind1) optional match (m:NodeKind2)-[:EdgeKind1]->(g) optional match (g)-[:EdgeKind1]->(m2:NodeKind2) 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 [1]::int2[]), 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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_id where e0.kind_id = any (array [3]::int2[])), s2 as (select s0.n0 as n0, s1.e0 as e0, s1.n1 as n1 from s0 left outer join s1 on (s0.n0 is not distinct from s1.n0)), s3 as (select s2.e0 as e0, (e1.id, e1.start_id, e1.end_id, e1.kind_id, e1.properties)::edgecomposite as e1, s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2 join edge e1 on (s2.n0).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 [3]::int2[])), s4 as (select s2.e0 as e0, s2.n0 as n0, s2.n1 as n1, s3.e1 as e1, s3.n2 as n2 from s2 left outer join s3 on (s2.n1 is not distinct from s3.n1) and (s2.n0 is not distinct from s3.n0) and (s2.e0 is not distinct from s3.e0)) select s4.n0 as g from s4; + +-- case: optional match (g:NodeKind1) optional match (m:NodeKind2)-[:EdgeKind1]->(g)-[:EdgeKind1]->(m2:NodeKind2) 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 [1]::int2[]), 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.kind_ids operator (pg_catalog.@>) array [2]::int2[] and n1.id = e0.start_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.n0).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 [3]::int2[]) and e1.id != (s1.e0).id), s3 as (select s0.n0 as n0, s2.e0 as e0, s2.e1 as e1, s2.n1 as n1, s2.n2 as n2 from s0 left outer join s2 on (s0.n0 is not distinct from s2.n0)) select s3.n0 as g from s3; diff --git a/cypher/models/pgsql/test/translation_cases/nodes.sql b/cypher/models/pgsql/test/translation_cases/nodes.sql index 96a1b431..6cf3c52f 100644 --- a/cypher/models/pgsql/test/translation_cases/nodes.sql +++ b/cypher/models/pgsql/test/translation_cases/nodes.sql @@ -467,13 +467,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 where (((n0.properties -> E'prop') != (@__strlit0::text)::jsonb and (n0.properties -> E'prop') != (@__strlit1::text)::jsonb or (n0.properties -> E'prop') = (@__strlit1::text)::jsonb and null))) select s0.n0 as s from s0; -- case: match (n:NodeKind1) optional match (m:NodeKind2) where m.distinguishedname = n.unknown + m.unknown return n, m -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[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> E'distinguishedname') = ((s0.n0).properties ->> E'unknown') || (n1.properties ->> E'unknown')) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select s0.n0 as n0, s1.n1 as n1 from s0 left outer join s1 on (s0.n0 = s1.n0)) select s2.n0 as n, s2.n1 as m from s2; +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[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> E'distinguishedname') = ((s0.n0).properties ->> E'unknown') || (n1.properties ->> E'unknown')) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select s0.n0 as n0, s1.n1 as n1 from s0 left outer join s1 on (s0.n0 is not distinct from s1.n0)) select s2.n0 as n, s2.n1 as m from s2; -- case: optional match (n:NodeKind1) return n 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[]) select s0.n0 as n from s0; -- case: match (n:NodeKind1) optional match (m:NodeKind2) where m.distinguishedname = n.unknown + m.unknown optional match (o:NodeKind2) where o.distinguishedname <> n.otherunknown return n, m, o -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[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> E'distinguishedname') = ((s0.n0).properties ->> E'unknown') || (n1.properties ->> E'unknown')) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select s0.n0 as n0, s1.n1 as n1 from s0 left outer join s1 on (s0.n0 = s1.n0)), s3 as (select s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where ((n2.properties -> E'distinguishedname') <> ((s2.n0).properties -> E'otherunknown')) and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s4 as (select s2.n0 as n0, s2.n1 as n1, s3.n2 as n2 from s2 left outer join s3 on (s2.n1 = s3.n1) and (s2.n0 = s3.n0)) select s4.n0 as n, s4.n1 as m, s4.n2 as o from s4; +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[]), s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1 from s0, node n1 where ((n1.properties ->> E'distinguishedname') = ((s0.n0).properties ->> E'unknown') || (n1.properties ->> E'unknown')) and n1.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s2 as (select s0.n0 as n0, s1.n1 as n1 from s0 left outer join s1 on (s0.n0 is not distinct from s1.n0)), s3 as (select s2.n0 as n0, s2.n1 as n1, (n2.id, n2.kind_ids, n2.properties)::nodecomposite as n2 from s2, node n2 where ((n2.properties -> E'distinguishedname') <> ((s2.n0).properties -> E'otherunknown')) and n2.kind_ids operator (pg_catalog.@>) array [2]::int2[]), s4 as (select s2.n0 as n0, s2.n1 as n1, s3.n2 as n2 from s2 left outer join s3 on (s2.n1 is not distinct from s3.n1) and (s2.n0 is not distinct from s3.n0)) select s4.n0 as n, s4.n1 as m, s4.n2 as o from s4; -- case: match (n) where n.name = "alpha' || (SELECT inet_server_addr()::text::int) || '" return n -- pgsql_params:{"__strlit0":"string","__strlit1":"alpha' || (SELECT inet_server_addr()::text::int) || '"} diff --git a/cypher/models/pgsql/translate/match.go b/cypher/models/pgsql/translate/match.go index 55c955e0..946a2eb5 100644 --- a/cypher/models/pgsql/translate/match.go +++ b/cypher/models/pgsql/translate/match.go @@ -10,6 +10,10 @@ import ( func (s *Translator) translateMatch(match *cypher.Match) error { currentQueryPart := s.query.CurrentPart() + // Note the current frame before adding frames from the match + // so we can use this point for optional match outer joins + joinFrame := s.scope.CurrentFrame() + for _, part := range currentQueryPart.ConsumeCurrentPattern().Parts { if !part.IsTraversal { if err := s.translateNonTraversalPatternPart(part); err != nil { @@ -41,22 +45,22 @@ func (s *Translator) translateMatch(match *cypher.Match) error { // If there is no valid previous frame, skip translating an `OPTIONAL MATCH`/treat as plain `MATCH` if match.Optional { - if _, hasValidPrevious := s.previousValidFrame(s.scope.CurrentFrame()); hasValidPrevious { - return s.translateOptionalMatch() + if joinFrame != nil { + return s.translateOptionalMatch(joinFrame) } } return nil } -func (s *Translator) translateOptionalMatch() error { +func (s *Translator) translateOptionalMatch(joinFrame *Frame) error { // Building this aggregation step requires pushing another frame onto the scope aggrFrame, err := s.scope.PushFrame() if err != nil { return err } - query, err := s.buildOptionalMatchAggregationStep(aggrFrame) + query, err := s.buildOptionalMatchAggregationStep(aggrFrame, joinFrame) if err != nil { return err } @@ -83,13 +87,12 @@ func (s *Translator) translateOptionalMatch() error { // buildOptionalMatchAggregationStep constructs a "merge" frame to insert after an `OPTIONAL MATCH`, // which requires a subsequent "aggregation" step to collate the optional match to the initial result set. -func (s *Translator) buildOptionalMatchAggregationStep(aggregationFrame *Frame) (pgsql.Query, error) { +func (s *Translator) buildOptionalMatchAggregationStep(aggregationFrame *Frame, originFrame *Frame) (pgsql.Query, error) { // An "aggregation" frame like this will only be triggered after an OPTIONAL MATCH, which should only // take place AFTER `n>=1` previous MATCH expressions. To properly base the aggregation, we need to // join to the origin frame (prior to the OPTIONAL MATCH) based on the OPTIONAL MATCH's frame. var ( optMatchFrame = aggregationFrame.Previous - originFrame = optMatchFrame.Previous ) // originFrame could be nil if no previous frame is defined (for ex., leading OPTIONAL MATCH, which is @@ -103,11 +106,13 @@ func (s *Translator) buildOptionalMatchAggregationStep(aggregationFrame *Frame) // our join anchor between the two CTEs var joinConstraints pgsql.Expression for _, exported := range originFrame.Exported.Slice() { + // Note using this operator will match Null=Null. This is needed to handle OPTIONAL MATCH + // correctly. It assumes exports will always have appropriate non-null join keys. joinConstraints = pgsql.OptionalAnd( pgsql.NewParenthetical( pgsql.NewBinaryExpression( pgsql.CompoundIdentifier{originFrame.Binding.Identifier, exported}, - pgsql.OperatorEquals, + pgsql.OperatorIsNotDistinctFrom, pgsql.CompoundIdentifier{optMatchFrame.Binding.Identifier, exported}, ), ), diff --git a/integration/testdata/templates/optional_shapes.json b/integration/testdata/templates/optional_shapes.json index a0fe134a..386489e2 100644 --- a/integration/testdata/templates/optional_shapes.json +++ b/integration/testdata/templates/optional_shapes.json @@ -40,13 +40,15 @@ {"id": "unlinked", "kinds": ["TemplateNodeKind1"], "properties": {"name": "unlinked", "group": "optional-filter"}}, {"id": "target", "kinds": ["TemplateNodeKind2"], "properties": {"name": "target"}}, {"id": "wrong", "kinds": ["TemplateNodeKind2"], "properties": {"name": "wrong"}}, - {"id": "tail", "kinds": ["TemplateNodeKind1"], "properties": {"name": "tail"}} + {"id": "tail", "kinds": ["TemplateNodeKind1"], "properties": {"name": "tail"}}, + {"id": "prefiltered", "kinds": ["TemplateNodeKind2"], "properties": {"name": "prefiltered"}} ], "edges": [ {"start_id": "linked", "end_id": "target", "kind": "TemplateEdgeKind1", "properties": {"label": "accepted"}}, {"start_id": "filtered", "end_id": "wrong", "kind": "TemplateEdgeKind1", "properties": {"label": "rejected"}}, {"start_id": "target", "end_id": "tail", "kind": "TemplateEdgeKind2", "properties": {"label": "tail"}}, - {"start_id": "wrong", "end_id": "tail", "kind": "TemplateEdgeKind2", "properties": {"label": "tail"}} + {"start_id": "wrong", "end_id": "tail", "kind": "TemplateEdgeKind2", "properties": {"label": "tail"}}, + {"start_id": "prefiltered", "end_id": "filtered", "kind": "TemplateEdgeKind2", "properties": {"label": "prefiltered"}} ] }, "variants": [ @@ -93,6 +95,20 @@ "query": "match (a:TemplateNodeKind1) where a.group = 'optional-filter' optional match (a)-[:TemplateEdgeKind1]->(b:TemplateNodeKind2) where b.name = 'target' return a.name, count(b) order by a.name" }, "assert": {"ordered_row_values": [["filtered", 0], ["linked", 1], ["unlinked", 0]]} + }, + { + "name": "chained pattern optional", + "vars": { + "query": "match (a:TemplateNodeKind1) where a.group = 'optional-filter' optional match (a)-[:TemplateEdgeKind1]->(b:TemplateNodeKind2)-[:TemplateEdgeKind2]->(:TemplateNodeKind1) where b.name = 'target' return a.name, count(b) order by a.name" + }, + "assert": {"ordered_row_values": [["filtered", 0], ["linked", 1], ["unlinked", 0]]} + }, + { + "name": "second chained optional can find values when first optional did not", + "vars": { + "query": "match (a:TemplateNodeKind1) where a.group = 'optional-filter' optional match (a)-[:TemplateEdgeKind1]->(b:TemplateNodeKind2) where b.name = 'target' optional match (c)-[:TemplateEdgeKind2]->(a) return c.name, a.name, b.name order by a.name" + }, + "assert": {"ordered_row_values": [["prefiltered", "filtered", null], [null, "linked", "target"], [null, "unlinked", null]]} } ] }