Skip to content
Merged
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
11 changes: 10 additions & 1 deletion cypher/models/pgsql/test/translation_cases/nodes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -285,4 +285,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 ? 'prop' and not (n0.properties ->> 'prop') = any (array ['null', '[]']::text[])))) select s0.n0 as s from s0;

-- case: match (s) where [] <> s.prop return s
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ? 'prop' and not (n0.properties ->> 'prop') = any (array ['null', '[]']::text[])))) select s0.n0 as s from s0;
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0 where ((n0.properties ? 'prop' and not (n0.properties ->> 'prop') = any (array ['null', '[]']::text[])))) 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 ->> 'distinguishedname') = ((s0.n0).properties -> 'unknown') + (n1.properties -> '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;

-- 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 ->> 'distinguishedname') = ((s0.n0).properties -> 'unknown') + (n1.properties -> '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 -> 'distinguishedname') <> ((s2.n0).properties -> '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;
134 changes: 132 additions & 2 deletions cypher/models/pgsql/translate/match.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package translate

func (s *Translator) translateMatch() error {
import (
"fmt"

"github.com/specterops/dawgs/cypher/models/cypher"
"github.com/specterops/dawgs/cypher/models/pgsql"
)

func (s *Translator) translateMatch(match *cypher.Match) error {
currentQueryPart := s.query.CurrentPart()

for _, part := range currentQueryPart.ConsumeCurrentPattern().Parts {
Expand All @@ -25,5 +32,128 @@ func (s *Translator) translateMatch() error {
}
}

return s.buildPatternPredicates()
if err := s.buildPatternPredicates(); err != nil {
return err
}

// If there is no previous frame, then skip translating an `OPTIONAL MATCH`/treat as plain `MATCH`
if match.Optional && s.scope.CurrentFrame().Previous != nil {
return s.translateOptionalMatch()
}

return nil
}

func (s *Translator) translateOptionalMatch() 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)
if err != nil {
return err
}

// Attach the aggregation step to the current CTE chain
s.query.CurrentPart().Model.AddCTE(pgsql.CommonTableExpression{
Alias: pgsql.TableAlias{
Name: aggrFrame.Binding.Identifier,
},
Query: query,
})

// For each identifier that is exported by our new frame, update which frame
// last materialized the identifier, so that references in future and final projections
// are corrected
for _, exported := range aggrFrame.Exported.Slice() {
if boundIdent, exists := s.scope.Lookup(exported); exists {
boundIdent.MaterializedBy(aggrFrame)
}
}

return nil
}

// 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) {
// 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.
optMatchFrame := aggregationFrame.Previous
originFrame := optMatchFrame.Previous
// originFrame could be nil if no previous frame is defined (for ex., leading OPTIONAL MATCH, which is
// valid but effectively a plain MATCH)
if originFrame == nil {
return pgsql.Query{}, fmt.Errorf("could not get origin frame prior to OPTIONAL MATCH")
}

// Construct the join condition based on exports from the "origin" frame
// We expect the OPTIONAL MATCH frame to also export the same, so that becomes
// our join anchor between the two CTEs
var joinConstraints pgsql.Expression
for _, exported := range originFrame.Exported.Slice() {
joinConstraints = pgsql.OptionalAnd(
pgsql.NewParenthetical(
pgsql.NewBinaryExpression(
pgsql.CompoundIdentifier{originFrame.Binding.Identifier, exported},
pgsql.OperatorEquals,
pgsql.CompoundIdentifier{optMatchFrame.Binding.Identifier, exported},
),
),
joinConstraints,
)
}

// Construct the projection for this frame. Just take all of the exports for the "origin" frame
// and optional match frame and re-export them
// TODO: Does there need to be additional logic for visible/defined bindings, instead of only exports?
originIDExclusions := map[string]struct{}{}
projection := pgsql.Projection{}
for _, exported := range originFrame.Exported.Slice() {
projection = append(projection, &pgsql.AliasedExpression{
Expression: pgsql.CompoundIdentifier{originFrame.Binding.Identifier, exported},
Alias: pgsql.AsOptionalIdentifier(exported),
})
originIDExclusions[exported.String()] = struct{}{}
aggregationFrame.Export(exported)
}
for _, exported := range optMatchFrame.Exported.Slice() {
// Optional match frame would shadow the origin frame's export with a filtered
// view of the origin's exports, so make sure not to shadow them
if _, ok := originIDExclusions[exported.String()]; ok {
continue
}

projection = append(projection, &pgsql.AliasedExpression{
Expression: pgsql.CompoundIdentifier{optMatchFrame.Binding.Identifier, exported},
Alias: pgsql.AsOptionalIdentifier(exported),
})
aggregationFrame.Export(exported)
}

query := pgsql.Query{
Body: pgsql.Select{
// The primary source for the aggregation after an OPTIONAL MATCH should be the "origin" frame
From: []pgsql.FromClause{{
Source: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{originFrame.Binding.Identifier},
},
Joins: []pgsql.Join{{
Table: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{optMatchFrame.Binding.Identifier},
},
JoinOperator: pgsql.JoinOperator{
JoinType: pgsql.JoinTypeLeftOuter,
Constraint: joinConstraints,
},
}},
}},
Projection: projection,
},
}

return query, nil
}
12 changes: 7 additions & 5 deletions cypher/models/pgsql/translate/predicate.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ func (s *Translator) buildOptimizedRelationshipExistPredicate(part *PatternPart,
Projection: []pgsql.SelectItem{
pgsql.NewLiteral(1, pgsql.Int),
},
From: []pgsql.FromClause{{
Source: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{pgsql.TableEdge},
Binding: models.OptionalValue(traversalStep.Edge.Identifier),
}},
From: []pgsql.FromClause{
{
Source: pgsql.TableReference{
Name: pgsql.CompoundIdentifier{pgsql.TableEdge},
Binding: models.OptionalValue(traversalStep.Edge.Identifier),
},
},
},
Where: whereClause,
},
Expand Down
2 changes: 1 addition & 1 deletion cypher/models/pgsql/translate/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (s *Translator) buildMultiPartQuery(singlePartQuery *cypher.SinglePartQuery
part.Model.CommonTableExpressions = nil
}

// Autor the part as a nested CTE
// Author the part as a nested CTE
nextCTE := pgsql.CommonTableExpression{
Query: *part.Model,
}
Expand Down
2 changes: 1 addition & 1 deletion cypher/models/pgsql/translate/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ func (s *Translator) Exit(expression cypher.SyntaxNode) {
}

case *cypher.Match:
if err := s.translateMatch(); err != nil {
if err := s.translateMatch(typedExpression); err != nil {
s.SetError(err)
}

Expand Down