Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
14 changes: 13 additions & 1 deletion cypher/models/pgsql/translate/predicate.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,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 Down Expand Up @@ -142,7 +144,17 @@ func (s *Translator) buildPatternPredicates() error {
})
}
} else {
if traversalStepQuery, err := s.buildTraversalPatternRoot(traversalStep.Frame, traversalStep); err != nil {
var (
traversalStepQuery pgsql.Query
err error
)
if traversalStep.Direction != graph.DirectionBoth && (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
113 changes: 112 additions & 1 deletion cypher/models/pgsql/translate/traversal.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ func (s *Translator) buildDirectionlessTraversalPatternRoot(traversalStep *Trave
},
JoinOperator: pgsql.JoinOperator{
JoinType: pgsql.JoinTypeInner,
Constraint: pgsql.OptionalAnd(rightJoinLocal, traversalStep.RightNodeJoinCondition)},
Constraint: pgsql.OptionalAnd(rightJoinLocal, traversalStep.RightNodeJoinCondition),
},
}},
})

Expand Down Expand Up @@ -138,6 +139,116 @@ func (s *Translator) buildDirectionlessTraversalPatternRoot(traversalStep *Trave
}, nil
}

// buildTraversalPatternRootWithOuterCorrelation constructs a traversal pattern root, preserving the correlation to
// the outer query part's context
func (s *Translator) buildTraversalPatternRootWithOuterCorrelation(partFrame *Frame, traversalStep *TraversalStep) (pgsql.Query, error) {
if traversalStep.Direction == graph.DirectionBoth {
return s.buildDirectionlessTraversalPatternRoot(traversalStep)
}

var (
// Partition right-node constraints: only locally-scoped terms go into JOIN ON.
// Constraints that reference comma-connected CTEs (e.g. s0.i0 from a prior WITH)
// must remain in WHERE — they are out of scope inside an explicit JOIN chain.
rightJoinLocal, rightJoinExternal = partitionConstraintByLocality(
traversalStep.RightNodeConstraints,
pgsql.AsIdentifierSet(traversalStep.RightNode.Identifier, traversalStep.Edge.Identifier),
)

nextSelect = pgsql.Select{
Projection: traversalStep.Projection,
}
)

if traversalStep.LeftNodeBound && traversalStep.RightNodeBound {
nextSelect.From = append(nextSelect.From, pgsql.FromClause{
Source: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{pgsql.TableEdge},
Binding: models.OptionalValue(traversalStep.Edge.Identifier),
},
})

// Both nodes of the traversal are fully bound by the outer query and the frame bindings
// will have been rewritten to reference the outer CTEs here, so we don't need any JOINs
// and can use those conditions inside of the inner WHERE to correlate the result set.
nextSelect.Where = pgsql.OptionalAnd(traversalStep.LeftNodeConstraints, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(traversalStep.RightNodeConstraints, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(traversalStep.LeftNodeJoinCondition, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(traversalStep.RightNodeJoinCondition, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(traversalStep.EdgeConstraints.Expression, nextSelect.Where)

return pgsql.Query{
Body: nextSelect,
}, nil
} else if traversalStep.LeftNodeBound {
nextSelect.From = append(nextSelect.From, pgsql.FromClause{
Source: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{pgsql.TableEdge},
Binding: models.OptionalValue(traversalStep.Edge.Identifier),
},
Joins: []pgsql.Join{{
Table: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{pgsql.TableNode},
Binding: models.OptionalValue(traversalStep.RightNode.Identifier),
},
JoinOperator: pgsql.JoinOperator{
JoinType: pgsql.JoinTypeInner,
Constraint: pgsql.OptionalAnd(rightJoinLocal, traversalStep.RightNodeJoinCondition),
},
}},
})

nextSelect.Where = pgsql.OptionalAnd(traversalStep.LeftNodeConstraints, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(traversalStep.LeftNodeJoinCondition, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(traversalStep.EdgeConstraints.Expression, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(rightJoinExternal, nextSelect.Where)

return pgsql.Query{
Body: nextSelect,
}, nil
} else if traversalStep.RightNodeBound {
// Right node was already materialized in a previous frame.
//
// We have to promote that frame to the explicit JOIN root so that RightNodeJoinCondition can reference
// it in the ON clause. PostgreSQL forbids referencing a comma-joined table inside a subsequent
// explicit JOIN's ON clause.
leftJoinLocal, leftJoinExternal := partitionConstraintByLocality(
traversalStep.LeftNodeConstraints,
pgsql.AsIdentifierSet(traversalStep.LeftNode.Identifier, traversalStep.Edge.Identifier),
)

nextSelect.From = append(nextSelect.From, pgsql.FromClause{
Source: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{pgsql.TableEdge},
Binding: models.OptionalValue(traversalStep.Edge.Identifier),
},
Joins: []pgsql.Join{{
Table: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{pgsql.TableNode},
Binding: models.OptionalValue(traversalStep.LeftNode.Identifier),
},
JoinOperator: pgsql.JoinOperator{
JoinType: pgsql.JoinTypeInner,
Constraint: pgsql.OptionalAnd(leftJoinLocal, traversalStep.LeftNodeJoinCondition),
},
}},
})

nextSelect.Where = pgsql.OptionalAnd(rightJoinLocal, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(traversalStep.RightNodeJoinCondition, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(leftJoinExternal, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(traversalStep.EdgeConstraints.Expression, nextSelect.Where)
nextSelect.Where = pgsql.OptionalAnd(rightJoinExternal, nextSelect.Where)

return pgsql.Query{
Body: nextSelect,
}, nil
} else {
// There is nothing to do to preserve outer bounds correlation - do the unbound traversal step
return s.buildTraversalPatternRoot(partFrame, traversalStep)
}
}

func (s *Translator) buildTraversalPatternRoot(partFrame *Frame, traversalStep *TraversalStep) (pgsql.Query, error) {
if traversalStep.Direction == graph.DirectionBoth {
return s.buildDirectionlessTraversalPatternRoot(traversalStep)
Expand Down
114 changes: 114 additions & 0 deletions integration/testdata/bed6695.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"graph": {
"nodes": [
{
"id": "key_admins_empty",
"kinds": [
"NodeKind1"
],
"properties": {
"name": "BED-6695 KEY ADMINS EMPTY"
}
},
{
"id": "key_admins_membered",
"kinds": [
"NodeKind1"
],
"properties": {
"name": "BED-6695 KEY ADMINS MEMBERED"
}
},
{
"id": "member_user",
"kinds": [
"NodeKind2"
],
"properties": {
"name": "BED-6695 MEMBER USER"
}
},
{
"id": "u1",
"kinds": [
"NodeKind1"
],
"properties": {
"name": "User A"
}
},
{
"id": "u2",
"kinds": [
"NodeKind1"
],
"properties": {
"name": "User B"
}
},
{
"id": "g1",
"kinds": [
"NodeKind2"
],
"properties": {
"name": "KEY ADMINS ALPHA"
}
},
{
"id": "g2",
"kinds": [
"NodeKind2"
],
"properties": {
"name": "KEY ADMINS BETA"
}
},
{
"id": "g3",
"kinds": [
"NodeKind2"
],
"properties": {
"name": "OPERATORS"
}
},
{
"id": "g4",
"kinds": [
"NodeKind2"
],
"properties": {
"name": "KEY ADMINS GAMMA"
}
}
],
"edges": [
{
"start_id": "member_user",
"end_id": "key_admins_membered",
"kind": "EdgeKind1"
},
{
"start_id": "u1",
"end_id": "g2",
"kind": "EdgeKind1"
},
{
"start_id": "u2",
"end_id": "g3",
"kind": "EdgeKind1"
},
{
"start_id": "g3",
"end_id": "g1",
"kind": "EdgeKind1"
},
{
"start_id": "u1",
"end_id": "g1",
"kind": "EdgeKind2"
}
]
}
}
Loading
Loading