Skip to content

Commit 06d06f7

Browse files
committed
Add support for patterns in where clause.
1 parent 8b2d486 commit 06d06f7

12 files changed

+406
-137
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package knowledge
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// AndOrExpression represent a AND or OR expression
9+
type AndOrExpression struct {
10+
And bool // true for And and false for Or
11+
Children []AndOrExpression
12+
Expression string
13+
}
14+
15+
func (aoe AndOrExpression) String() string {
16+
if aoe.Expression != "" {
17+
return aoe.Expression
18+
}
19+
20+
if len(aoe.Children) > 0 {
21+
op := " AND "
22+
if !aoe.And {
23+
op = " OR "
24+
}
25+
exprs := make([]string, 0)
26+
for _, e := range aoe.Children {
27+
exprs = append(exprs, e.String())
28+
}
29+
return strings.Join(exprs, op)
30+
}
31+
return "(empty)"
32+
}
33+
34+
func BuildAndOrExpression(tree AndOrExpression) (string, error) {
35+
if tree.Expression != "" {
36+
return tree.Expression, nil
37+
} else if tree.And {
38+
exprs := make([]string, 0)
39+
for i := range tree.Children {
40+
expr, err := BuildAndOrExpression(tree.Children[i])
41+
if err != nil {
42+
return "", err
43+
}
44+
if expr != "" {
45+
exprs = append(exprs, expr)
46+
}
47+
}
48+
if len(exprs) > 1 {
49+
return fmt.Sprintf("(%s)", strings.Join(exprs, " AND ")), nil
50+
}
51+
return strings.Join(exprs, " AND "), nil
52+
} else if !tree.And {
53+
exprs := make([]string, 0)
54+
for i := range tree.Children {
55+
expr, err := BuildAndOrExpression(tree.Children[i])
56+
if err != nil {
57+
return "", err
58+
}
59+
60+
if expr != "" {
61+
exprs = append(exprs, expr)
62+
}
63+
}
64+
if len(exprs) > 1 {
65+
return fmt.Sprintf("(%s)", strings.Join(exprs, " OR ")), nil
66+
}
67+
return strings.Join(exprs, " OR "), nil
68+
}
69+
return "", nil
70+
}
71+
72+
func CrossProductExpressions(and1 []AndOrExpression, and2 []AndOrExpression) []AndOrExpression {
73+
outExpr := []AndOrExpression{}
74+
for i := range and1 {
75+
for j := range and2 {
76+
children := AndOrExpression{
77+
And: true,
78+
Children: []AndOrExpression{and1[i], and2[j]},
79+
}
80+
outExpr = append(outExpr, children)
81+
}
82+
}
83+
return outExpr
84+
}
85+
86+
// UnwindOrExpressions in order to transform query with or relations into a union
87+
// query which is more performant, an AndOrExpression is transformed into a list of AndExpressions
88+
func UnwindOrExpressions(tree AndOrExpression) ([]AndOrExpression, error) {
89+
if tree.Expression != "" {
90+
child := AndOrExpression{Children: []AndOrExpression{tree}, And: true}
91+
return []AndOrExpression{child}, nil
92+
} else if !tree.And {
93+
exprs := []AndOrExpression{}
94+
for i := range tree.Children {
95+
nestedExpressions, err := UnwindOrExpressions(tree.Children[i])
96+
if err != nil {
97+
return nil, err
98+
}
99+
exprs = append(exprs, nestedExpressions...)
100+
}
101+
return exprs, nil
102+
} else if tree.And {
103+
exprs := []AndOrExpression{}
104+
for i := range tree.Children {
105+
expr, err := UnwindOrExpressions(tree.Children[i])
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
if len(exprs) == 0 {
111+
exprs = append(exprs, expr...)
112+
} else {
113+
exprs = CrossProductExpressions(exprs, expr)
114+
}
115+
}
116+
return exprs, nil
117+
}
118+
return nil, fmt.Errorf("Unable to detect kind of node")
119+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package knowledge
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestAndOrExpression(t *testing.T) {
10+
exprs := AndOrExpression{
11+
And: true,
12+
Children: []AndOrExpression{
13+
AndOrExpression{
14+
And: false,
15+
Children: []AndOrExpression{
16+
AndOrExpression{
17+
And: false,
18+
Expression: "a0.type = 'ip'",
19+
},
20+
},
21+
},
22+
AndOrExpression{
23+
And: false,
24+
Children: []AndOrExpression{
25+
AndOrExpression{
26+
And: false,
27+
Expression: "a0.type = 'device'",
28+
},
29+
},
30+
},
31+
AndOrExpression{
32+
And: false,
33+
Children: []AndOrExpression{
34+
AndOrExpression{
35+
And: false,
36+
Expression: "a0.type = 'metascan_task_id'",
37+
},
38+
},
39+
},
40+
AndOrExpression{
41+
And: false,
42+
Children: []AndOrExpression{
43+
AndOrExpression{
44+
And: false,
45+
Expression: "r0.type = 'observed'",
46+
},
47+
},
48+
},
49+
AndOrExpression{
50+
And: true,
51+
Children: []AndOrExpression{
52+
AndOrExpression{
53+
And: false,
54+
Expression: "r0.from_id = a1.id",
55+
},
56+
AndOrExpression{
57+
And: false,
58+
Expression: "r0.to_id = a0.id",
59+
},
60+
},
61+
},
62+
AndOrExpression{
63+
And: false,
64+
Children: []AndOrExpression{
65+
AndOrExpression{
66+
And: false,
67+
Expression: "r1.type = 'scanned'",
68+
},
69+
},
70+
},
71+
AndOrExpression{
72+
And: true,
73+
Children: []AndOrExpression{
74+
AndOrExpression{
75+
And: false,
76+
Expression: "r1.from_id = a2.id",
77+
},
78+
AndOrExpression{
79+
And: false,
80+
Expression: "r1.to_id = a0.id",
81+
},
82+
},
83+
},
84+
AndOrExpression{
85+
And: true,
86+
Children: []AndOrExpression{
87+
AndOrExpression{
88+
And: false,
89+
Expression: "",
90+
},
91+
},
92+
},
93+
},
94+
}
95+
96+
x, err := UnwindOrExpressions(exprs)
97+
assert.NoError(t, err)
98+
assert.Len(t, x, 4)
99+
}

internal/knowledge/query_expression_builder.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func NewExpressionBuilder(queryGraph *QueryGraph) *ExpressionBuilder {
1919
visitor.queryGraph = queryGraph
2020
return &ExpressionBuilder{
2121
QueryGraph: queryGraph,
22-
parser: NewExpressionParser(&visitor),
22+
parser: NewExpressionParser(&visitor, queryGraph),
2323
visitor: &visitor,
2424
}
2525
}
@@ -63,6 +63,11 @@ type SQLExpressionVisitor struct {
6363
expression string
6464
}
6565

66+
func (sev *SQLExpressionVisitor) OnRelationshipsPattern(e query.QueryRelationshipsPattern) error {
67+
fmt.Println("relationships pattern")
68+
return fmt.Errorf("relationships")
69+
}
70+
6671
func (sev *SQLExpressionVisitor) OnVariable(name string) error {
6772
sev.variableName = new(string)
6873
*sev.variableName = name

internal/knowledge/query_expression_parser.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const (
1515
PropertyExprType ExpressionType = iota
1616
)
1717

18+
// ExpressionVisitor a visitor of expression
1819
type ExpressionVisitor interface {
1920
OnEnterPropertyOrLabelsExpression(e query.QueryPropertyOrLabelsExpression) error
2021
OnExitPropertyOrLabelsExpression(e query.QueryPropertyOrLabelsExpression) error
@@ -72,13 +73,17 @@ type ExpressionVisitor interface {
7273
OnExitExpression() error
7374
}
7475

76+
// ExpressionParser is a parser of expression
7577
type ExpressionParser struct {
76-
visitor ExpressionVisitor
78+
visitor ExpressionVisitor
79+
queryGraph *QueryGraph
7780
}
7881

79-
func NewExpressionParser(visitor ExpressionVisitor) *ExpressionParser {
82+
// NewExpressionParser create a new instance of expression parser.
83+
func NewExpressionParser(visitor ExpressionVisitor, queryGraph *QueryGraph) *ExpressionParser {
8084
return &ExpressionParser{
81-
visitor: visitor,
85+
visitor: visitor,
86+
queryGraph: queryGraph,
8287
}
8388
}
8489

@@ -152,6 +157,12 @@ func (ep *ExpressionParser) ParsePropertyOrLabelsExpression(q *query.QueryProper
152157
if err != nil {
153158
return err
154159
}
160+
} else if q.Atom.RelationshipsPattern != nil {
161+
parser := NewPatternParser(ep.queryGraph)
162+
err := parser.ParseRelationshipsPattern(q.Atom.RelationshipsPattern)
163+
if err != nil {
164+
return err
165+
}
155166
} else {
156167
return fmt.Errorf("Unable to parse property or labels expression")
157168
}
@@ -408,6 +419,19 @@ func (ep *ExpressionParser) ParseExpression(q *query.QueryExpression) error {
408419

409420
type ExpressionVisitorBase struct{}
410421

422+
func (evb *ExpressionVisitorBase) OnEnterRelationshipsPattern() error {
423+
return nil
424+
}
425+
func (evb *ExpressionVisitorBase) OnExitRelationshipsPattern() error {
426+
return nil
427+
}
428+
429+
func (evb *ExpressionVisitorBase) OnEnterNodePattern() error {
430+
return nil
431+
}
432+
func (evb *ExpressionVisitorBase) OnExitNodePattern() error {
433+
return nil
434+
}
411435
func (evb *ExpressionVisitorBase) OnEnterPropertyOrLabelsExpression(e query.QueryPropertyOrLabelsExpression) error {
412436
return nil
413437
}

internal/knowledge/query_limit_visitor.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@ type QueryLimitVisitor struct {
66
ExpressionVisitorBase
77

88
Limit int64
9+
10+
queryGraph *QueryGraph
11+
}
12+
13+
// NewQueryLimitVisitor create an instance of query limit visitor
14+
func NewQueryLimitVisitor(queryGraph *QueryGraph) *QueryLimitVisitor {
15+
return &QueryLimitVisitor{
16+
queryGraph: queryGraph,
17+
}
918
}
1019

1120
// ParseExpression return whether the expression require aggregation
1221
func (qlv *QueryLimitVisitor) ParseExpression(q *query.QueryExpression) error {
13-
err := NewExpressionParser(qlv).ParseExpression(q)
22+
err := NewExpressionParser(qlv, qlv.queryGraph).ParseExpression(q)
1423
if err != nil {
1524
return err
1625
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package knowledge
2+
3+
import "github.com/clems4ever/go-graphkb/internal/query"
4+
5+
// PatternParser is a parser of patterns
6+
type PatternParser struct {
7+
queryGraph *QueryGraph
8+
}
9+
10+
// NewPatternParser create an instance of pattern parser
11+
func NewPatternParser(queryGraph *QueryGraph) *PatternParser {
12+
return &PatternParser{
13+
queryGraph: queryGraph,
14+
}
15+
}
16+
17+
// ParseRelationshipsPattern parse a relationships pattern
18+
func (ep *PatternParser) ParseRelationshipsPattern(q *query.QueryRelationshipsPattern) error {
19+
_, i1, err := ep.queryGraph.PushNode(q.QueryNodePattern)
20+
if err != nil {
21+
return err
22+
}
23+
24+
for _, z := range q.QueryPatternElementChains {
25+
_, i2, err := ep.queryGraph.PushNode(z.QueryNodePattern)
26+
if err != nil {
27+
return err
28+
}
29+
30+
_, _, err = ep.queryGraph.PushRelation(z.RelationshipPattern, i1, i2)
31+
if err != nil {
32+
return err
33+
}
34+
i1 = i2
35+
}
36+
return nil
37+
}
38+
39+
// ParsePatternElement parse a pattern element
40+
func (ep *PatternParser) ParsePatternElement(q *query.QueryPatternElement) error {
41+
_, i1, err := ep.queryGraph.PushNode(q.QueryNodePattern)
42+
if err != nil {
43+
return err
44+
}
45+
46+
for _, z := range q.QueryPatternElementChains {
47+
_, i2, err := ep.queryGraph.PushNode(z.QueryNodePattern)
48+
if err != nil {
49+
return err
50+
}
51+
52+
_, _, err = ep.queryGraph.PushRelation(z.RelationshipPattern, i1, i2)
53+
if err != nil {
54+
return err
55+
}
56+
i1 = i2
57+
}
58+
return nil
59+
}

0 commit comments

Comments
 (0)