Skip to content

Commit c8e782a

Browse files
committed
Unfold OR expression into unions of AND ones.
Using SQL Union queries is way more efficient than OR queries. Note: aggregation functions are not yet supported when using union queries.
1 parent 3c0c786 commit c8e782a

File tree

8 files changed

+384
-66
lines changed

8 files changed

+384
-66
lines changed

internal/database/mariadb.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -647,15 +647,14 @@ func (mc *MariaDBCursor) Read(ctx context.Context, doc interface{}) error {
647647
}
648648
output[i] = a
649649
case knowledge.EdgeExprType:
650-
items, err := q.Get(5)
650+
items, err := q.Get(3)
651651
if err != nil {
652652
return nil
653653
}
654654
r := knowledge.RelationWithID{
655-
ID: fmt.Sprintf("%v", reflect.ValueOf(items[0])),
656-
From: fmt.Sprintf("%v", reflect.ValueOf(items[1])),
657-
To: fmt.Sprintf("%v", reflect.ValueOf(items[2])),
658-
Type: schema.RelationKeyType(fmt.Sprintf("%v", reflect.ValueOf(items[3]))),
655+
From: fmt.Sprintf("%v", reflect.ValueOf(items[0])),
656+
To: fmt.Sprintf("%v", reflect.ValueOf(items[1])),
657+
Type: schema.RelationKeyType(fmt.Sprintf("%v", reflect.ValueOf(items[2]))),
659658
}
660659
output[i] = r
661660
case knowledge.PropertyExprType:

internal/knowledge/query_expression_builder.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,7 @@ func (sev *SQLExpressionVisitor) OnExitParenthesizedExpression() error {
105105

106106
func (sev *SQLExpressionVisitor) OnExitPropertyOrLabelsExpression(e query.QueryPropertyOrLabelsExpression) error {
107107
if sev.variableName != nil {
108-
path := "*"
109-
if len(sev.propertiesPath) > 0 {
110-
path = strings.Join(sev.propertiesPath, ".")
111-
}
108+
var properties []string
112109
typeAndIndex, err := sev.queryGraph.FindVariable(*sev.variableName)
113110
if err != nil {
114111
return err
@@ -118,12 +115,22 @@ func (sev *SQLExpressionVisitor) OnExitPropertyOrLabelsExpression(e query.QueryP
118115
switch typeAndIndex.Type {
119116
case NodeType:
120117
alias = "a"
118+
properties = []string{"id", "value", "type"}
121119
case RelationType:
122120
alias = "r"
121+
properties = []string{"from_id", "to_id", "type"}
123122
}
124123
alias += fmt.Sprintf("%d", typeAndIndex.Index)
124+
if len(sev.propertiesPath) > 0 {
125+
properties = []string{strings.Join(sev.propertiesPath, ".")}
126+
}
127+
128+
projection := []string{}
129+
for _, p := range properties {
130+
projection = append(projection, fmt.Sprintf("%s.%s", alias, p))
131+
}
125132

126-
sev.propertyLabelsExpression = fmt.Sprintf("%s.%s", alias, path)
133+
sev.propertyLabelsExpression = strings.Join(projection, ", ")
127134
sev.variableName = nil
128135
sev.propertiesPath = nil
129136
} else if sev.stringLiteral != nil {

internal/knowledge/query_expression_builder_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ func TestShouldBuildExpression(t *testing.T) {
5252
})
5353
require.NoError(t, err)
5454

55+
require.NoError(t, err)
56+
_, _, err = qg.PushRelation(query.QueryRelationshipPattern{
57+
RelationshipDetail: &query.QueryRelationshipDetail{
58+
Variable: "r",
59+
},
60+
}, 0, 0)
61+
require.NoError(t, err)
62+
5563
expr := CypherToExpr(tc.Cypher)
5664

5765
sql, err := ep.Build(&expr)
@@ -116,7 +124,11 @@ var testCases = []ExpressionTestCase{
116124
},
117125
ExpressionTestCase{
118126
Cypher: "a",
119-
SQL: "a0.*",
127+
SQL: "a0.id, a0.value, a0.type",
128+
},
129+
ExpressionTestCase{
130+
Cypher: "r",
131+
SQL: "r0.from_id, r0.to_id, r0.type",
120132
},
121133
ExpressionTestCase{
122134
Cypher: "a.value",

internal/knowledge/query_projection_visitor.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ type ProjectionVisitor struct {
1010
QueryGraph *QueryGraph
1111

1212
Aggregation bool
13-
ExpressionType ExpressionType
1413
TypeAndIndex TypeAndIndex
14+
ExpressionType ExpressionType
15+
16+
funcInvoc bool
17+
etype ExpressionType
18+
properties []string
1519
}
1620

1721
// ParseExpression return whether the expression require aggregation
@@ -26,6 +30,7 @@ func (pv *ProjectionVisitor) ParseExpression(q *query.QueryExpression) error {
2630
func (pv *ProjectionVisitor) OnEnterFunctionInvocation(name string) error {
2731
if name == "COUNT" {
2832
pv.Aggregation = true
33+
pv.funcInvoc = true
2934
} else {
3035
return fmt.Errorf("Function %s is not supported", name)
3136
}
@@ -40,12 +45,28 @@ func (pv *ProjectionVisitor) OnVariable(name string) error {
4045

4146
switch typeAndIndex.Type {
4247
case NodeType:
43-
pv.ExpressionType = NodeExprType
48+
pv.etype = NodeExprType
4449
case RelationType:
45-
pv.ExpressionType = EdgeExprType
50+
pv.etype = EdgeExprType
4651
default:
47-
pv.ExpressionType = PropertyExprType
52+
pv.etype = PropertyExprType
4853
}
4954
pv.TypeAndIndex = typeAndIndex
5055
return nil
5156
}
57+
58+
func (pv *ProjectionVisitor) OnVariablePropertiesPath(properties []string) error {
59+
pv.properties = properties
60+
return nil
61+
}
62+
63+
func (pv *ProjectionVisitor) OnExitPropertyOrLabelsExpression(e query.QueryPropertyOrLabelsExpression) error {
64+
if len(pv.properties) > 0 || pv.funcInvoc {
65+
pv.ExpressionType = PropertyExprType
66+
} else {
67+
pv.ExpressionType = pv.etype
68+
}
69+
pv.properties = nil
70+
pv.funcInvoc = false
71+
return nil
72+
}

internal/knowledge/query_sql.go

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,113 @@ func BuildAndOrExpression(tree AndOrExpression) (string, error) {
6969
return "", nil
7070
}
7171

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+
}
120+
72121
func (sqt *SQLQueryTranslator) buildSQLSelect(
122+
distinct bool, projections []string, projectionTypes []Projection, fromTables []string,
123+
whereExpressions AndOrExpression, groupBy []int, limit int, offset int) (string, error) {
124+
var sqlQuery string
125+
126+
andExpressions, err := UnwindOrExpressions(whereExpressions)
127+
if err != nil {
128+
return "", err
129+
}
130+
131+
if len(andExpressions) > 1 {
132+
singleQueries := []string{}
133+
for _, where := range andExpressions {
134+
singleQuery, err := sqt.buildSingleSQLSelect(false, projections, fromTables, where, nil, 0, 0)
135+
if err != nil {
136+
return "", err
137+
}
138+
singleQueries = append(singleQueries, fmt.Sprintf("(%s)", singleQuery))
139+
}
140+
if distinct {
141+
sqlQuery = strings.Join(singleQueries, "\nUNION\n")
142+
} else {
143+
sqlQuery = strings.Join(singleQueries, "\nUNION ALL\n")
144+
}
145+
146+
if len(groupBy) > 0 {
147+
groupByProjections := []string{}
148+
for i := range groupBy {
149+
groupByProjections = append(groupByProjections, projections[groupBy[i]])
150+
}
151+
152+
sqlQuery = fmt.Sprintf("SELECT %s FROM\n(%s)\nGROUP BY %s",
153+
strings.Join(projections, ", "), sqlQuery, strings.Join(groupByProjections, ","))
154+
}
155+
156+
if limit > 0 {
157+
sqlQuery += fmt.Sprintf("\nLIMIT %d", limit)
158+
}
159+
160+
if offset > 0 {
161+
sqlQuery += fmt.Sprintf("\nOFFSET %d", offset)
162+
}
163+
164+
} else {
165+
and := AndOrExpression{And: true, Children: andExpressions}
166+
singleQuery, err := sqt.buildSingleSQLSelect(distinct, projections, fromTables, and, groupBy, limit, offset)
167+
if err != nil {
168+
return "", err
169+
}
170+
sqlQuery = singleQuery
171+
}
172+
173+
return sqlQuery, nil
174+
}
175+
176+
func (sqt *SQLQueryTranslator) buildSingleSQLSelect(
73177
distinct bool, projections []string, fromTables []string,
74-
whereExpressions AndOrExpression, groupBy []string, limit int, offset int) (string, error) {
178+
whereExpressions AndOrExpression, groupBy []int, limit int, offset int) (string, error) {
75179

76180
projectionsStr := ""
77181
if distinct {
@@ -92,7 +196,11 @@ func (sqt *SQLQueryTranslator) buildSQLSelect(
92196
}
93197

94198
if len(groupBy) > 0 {
95-
sqlQuery += fmt.Sprintf("\nGROUP BY %s", strings.Join(groupBy, ", "))
199+
groupByProjection := make([]string, len(groupBy))
200+
for i := range groupBy {
201+
groupByProjection[i] = projections[groupBy[i]]
202+
}
203+
sqlQuery += fmt.Sprintf("\nGROUP BY %s", strings.Join(groupByProjection, ", "))
96204
}
97205

98206
if limit > 0 {
@@ -154,10 +262,10 @@ func (sqt *SQLQueryTranslator) Translate(query *query.QueryCypher) (*SQLTranslat
154262
projectionTypes := make([]Projection, 0)
155263
from := make([]string, 0)
156264

157-
unaggregatedProjectionItems := make([]string, 0)
265+
unaggregatedProjectionItems := []int{}
158266
aggregationRequired := false
159267

160-
for _, p := range query.QuerySinglePartQuery.ProjectionBody.ProjectionItems {
268+
for i, p := range query.QuerySinglePartQuery.ProjectionBody.ProjectionItems {
161269
projectionVisitor := ProjectionVisitor{QueryGraph: &sqt.QueryGraph}
162270
err := projectionVisitor.ParseExpression(&p.Expression)
163271
if err != nil {
@@ -170,7 +278,7 @@ func (sqt *SQLQueryTranslator) Translate(query *query.QueryCypher) (*SQLTranslat
170278
}
171279

172280
if !projectionVisitor.Aggregation {
173-
unaggregatedProjectionItems = append(unaggregatedProjectionItems, projection)
281+
unaggregatedProjectionItems = append(unaggregatedProjectionItems, i)
174282
} else {
175283
aggregationRequired = true
176284
}
@@ -197,18 +305,24 @@ func (sqt *SQLQueryTranslator) Translate(query *query.QueryCypher) (*SQLTranslat
197305
})
198306
}
199307

200-
// Append assets constraints
201-
andExpressions.Children = append(andExpressions.Children, typesConstraints)
308+
if len(typesConstraints.Children) > 0 {
309+
// Append assets constraints
310+
andExpressions.Children = append(andExpressions.Children, typesConstraints)
311+
}
202312
}
203313
for i, r := range sqt.QueryGraph.Relations {
204314
alias := fmt.Sprintf("r%d", i)
205315
from = append(from, fmt.Sprintf("relations %s", alias))
206316

317+
typesConstraints := AndOrExpression{And: false}
207318
for _, label := range r.Labels {
208-
andExpressions.Children = append(andExpressions.Children, AndOrExpression{
319+
typesConstraints.Children = append(typesConstraints.Children, AndOrExpression{
209320
Expression: fmt.Sprintf("%s.type = '%s'", alias, label),
210321
})
211322
}
323+
if len(typesConstraints.Children) > 0 {
324+
andExpressions.Children = append(andExpressions.Children, typesConstraints)
325+
}
212326

213327
out := AndOrExpression{
214328
And: true,
@@ -280,7 +394,6 @@ func (sqt *SQLQueryTranslator) Translate(query *query.QueryCypher) (*SQLTranslat
280394
}
281395
andExpressions.Children = append(andExpressions.Children, orExpression)
282396
}
283-
284397
}
285398
}
286399

@@ -309,11 +422,14 @@ func (sqt *SQLQueryTranslator) Translate(query *query.QueryCypher) (*SQLTranslat
309422
offset = int(skipVisitor.Skip)
310423
}
311424

312-
andExpressions.Children = append(andExpressions.Children, filterExpressions)
425+
if len(filterExpressions.Children) > 0 {
426+
andExpressions.Children = append(andExpressions.Children, filterExpressions)
427+
}
313428

314429
sqlQuery, err := sqt.buildSQLSelect(
315430
query.QuerySinglePartQuery.ProjectionBody.Distinct,
316431
projections,
432+
projectionTypes,
317433
from,
318434
andExpressions,
319435
unaggregatedProjectionItems,

0 commit comments

Comments
 (0)