Skip to content

Commit 3c03e59

Browse files
authored
Optimize boolean operations between all, any, one, none functions (#555)
1 parent 83b8df3 commit 3c03e59

File tree

3 files changed

+174
-0
lines changed

3 files changed

+174
-0
lines changed

optimizer/optimizer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ func Optimize(node *Node, config *conf.Config) error {
3636
Walk(node, &filterLen{})
3737
Walk(node, &filterLast{})
3838
Walk(node, &filterFirst{})
39+
Walk(node, &predicateCombination{})
3940
return nil
4041
}

optimizer/optimizer_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package optimizer_test
22

33
import (
4+
"fmt"
45
"reflect"
56
"strings"
67
"testing"
@@ -339,3 +340,124 @@ func TestOptimize_filter_map_first(t *testing.T) {
339340

340341
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
341342
}
343+
344+
func TestOptimize_predicate_combination(t *testing.T) {
345+
tests := []struct {
346+
op string
347+
fn string
348+
wantOp string
349+
}{
350+
{"and", "all", "and"},
351+
{"&&", "all", "&&"},
352+
{"or", "all", "or"},
353+
{"||", "all", "||"},
354+
{"and", "any", "and"},
355+
{"&&", "any", "&&"},
356+
{"or", "any", "or"},
357+
{"||", "any", "||"},
358+
{"and", "none", "or"},
359+
{"&&", "none", "||"},
360+
{"and", "one", "or"},
361+
{"&&", "one", "||"},
362+
}
363+
364+
for _, tt := range tests {
365+
rule := fmt.Sprintf(`%s(users, .Age > 18 and .Name != "Bob") %s %s(users, .Age < 30)`, tt.fn, tt.op, tt.fn)
366+
t.Run(rule, func(t *testing.T) {
367+
tree, err := parser.Parse(rule)
368+
require.NoError(t, err)
369+
370+
err = optimizer.Optimize(&tree.Node, nil)
371+
require.NoError(t, err)
372+
373+
expected := &ast.BuiltinNode{
374+
Name: tt.fn,
375+
Arguments: []ast.Node{
376+
&ast.IdentifierNode{Value: "users"},
377+
&ast.ClosureNode{
378+
Node: &ast.BinaryNode{
379+
Operator: tt.wantOp,
380+
Left: &ast.BinaryNode{
381+
Operator: "and",
382+
Left: &ast.BinaryNode{
383+
Operator: ">",
384+
Left: &ast.MemberNode{
385+
Node: &ast.PointerNode{},
386+
Property: &ast.StringNode{Value: "Age"},
387+
},
388+
Right: &ast.IntegerNode{Value: 18},
389+
},
390+
Right: &ast.BinaryNode{
391+
Operator: "!=",
392+
Left: &ast.MemberNode{
393+
Node: &ast.PointerNode{},
394+
Property: &ast.StringNode{Value: "Name"},
395+
},
396+
Right: &ast.StringNode{Value: "Bob"},
397+
},
398+
},
399+
Right: &ast.BinaryNode{
400+
Operator: "<",
401+
Left: &ast.MemberNode{
402+
Node: &ast.PointerNode{},
403+
Property: &ast.StringNode{Value: "Age"},
404+
},
405+
Right: &ast.IntegerNode{Value: 30},
406+
},
407+
},
408+
},
409+
},
410+
}
411+
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
412+
})
413+
}
414+
}
415+
416+
func TestOptimize_predicate_combination_nested(t *testing.T) {
417+
tree, err := parser.Parse(`any(users, {all(.Friends, {.Age == 18 })}) && any(users, {all(.Friends, {.Name != "Bob" })})`)
418+
require.NoError(t, err)
419+
420+
err = optimizer.Optimize(&tree.Node, nil)
421+
require.NoError(t, err)
422+
423+
expected := &ast.BuiltinNode{
424+
Name: "any",
425+
Arguments: []ast.Node{
426+
&ast.IdentifierNode{Value: "users"},
427+
&ast.ClosureNode{
428+
Node: &ast.BuiltinNode{
429+
Name: "all",
430+
Arguments: []ast.Node{
431+
&ast.MemberNode{
432+
Node: &ast.PointerNode{},
433+
Property: &ast.StringNode{Value: "Friends"},
434+
},
435+
&ast.ClosureNode{
436+
Node: &ast.BinaryNode{
437+
Operator: "&&",
438+
Left: &ast.BinaryNode{
439+
Operator: "==",
440+
Left: &ast.MemberNode{
441+
Node: &ast.PointerNode{},
442+
Property: &ast.StringNode{Value: "Age"},
443+
},
444+
Right: &ast.IntegerNode{Value: 18},
445+
},
446+
Right: &ast.BinaryNode{
447+
Operator: "!=",
448+
Left: &ast.MemberNode{
449+
Node: &ast.PointerNode{},
450+
Property: &ast.StringNode{Value: "Name"},
451+
},
452+
Right: &ast.StringNode{Value: "Bob"},
453+
},
454+
},
455+
},
456+
},
457+
},
458+
},
459+
},
460+
}
461+
462+
assert.Equal(t, ast.Dump(expected), ast.Dump(tree.Node))
463+
}

optimizer/predicate_combination.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package optimizer
2+
3+
import (
4+
. "github.com/expr-lang/expr/ast"
5+
"github.com/expr-lang/expr/parser/operator"
6+
)
7+
8+
type predicateCombination struct{}
9+
10+
func (v *predicateCombination) Visit(node *Node) {
11+
if op, ok := (*node).(*BinaryNode); ok && operator.IsBoolean(op.Operator) {
12+
if left, ok := op.Left.(*BuiltinNode); ok {
13+
if combinedOp, ok := combinedOperator(left.Name, op.Operator); ok {
14+
if right, ok := op.Right.(*BuiltinNode); ok && right.Name == left.Name {
15+
if left.Arguments[0].Type() == right.Arguments[0].Type() && left.Arguments[0].String() == right.Arguments[0].String() {
16+
closure := &ClosureNode{
17+
Node: &BinaryNode{
18+
Operator: combinedOp,
19+
Left: left.Arguments[1].(*ClosureNode).Node,
20+
Right: right.Arguments[1].(*ClosureNode).Node,
21+
},
22+
}
23+
v.Visit(&closure.Node)
24+
Patch(node, &BuiltinNode{
25+
Name: left.Name,
26+
Arguments: []Node{
27+
left.Arguments[0],
28+
closure,
29+
},
30+
})
31+
}
32+
}
33+
}
34+
}
35+
}
36+
}
37+
38+
func combinedOperator(fn, op string) (string, bool) {
39+
switch fn {
40+
case "all", "any":
41+
return op, true
42+
case "one", "none":
43+
switch op {
44+
case "and":
45+
return "or", true
46+
case "&&":
47+
return "||", true
48+
}
49+
}
50+
return "", false
51+
}

0 commit comments

Comments
 (0)