Skip to content

Commit

Permalink
Pre-work for ART searches
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyhb committed Dec 22, 2023
1 parent 0ea8ecf commit 598acf5
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 25 deletions.
92 changes: 78 additions & 14 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package expr
import (
"context"
"fmt"

"github.com/google/cel-go/common/operators"
)

// AggregateEvaluator represents a group of expressions that must be evaluated for a single
// event received.
type AggregateEvaluator interface {
// Add adds an expression to the tree evaluator
Add(ctx context.Context, eval Evaluable) error
Add(ctx context.Context, eval Evaluable) (bool, error)
// Remove removes an expression from the aggregate evaluator
Remove(ctx context.Context, eval Evaluable) error

Expand Down Expand Up @@ -39,21 +41,22 @@ func (a *aggregator) Evaluate(ctx context.Context, data map[string]any) ([]Evalu
return nil, nil
}

func (a *aggregator) Add(ctx context.Context, eval Evaluable) error {
func (a *aggregator) Add(ctx context.Context, eval Evaluable) (bool, error) {
parsed, err := a.parser.Parse(ctx, eval.Expression())
if err != nil {
return err
return false, err
}

_ = parsed

// TODO: Iterate through each group and add the expression to tree
// types specified.

// TODO: Add each group to a tree. The leaf node should point to the
// expressions that match this leaf node (pause?)

// TODO: Pointer of checksums -> groups
exhaustive := true
for _, g := range parsed.RootGroups() {
ok, err := a.addGroup(ctx, g)
if err != nil {
return false, err
}
if !ok {
exhaustive = false
}
}

// on event entered:
//
Expand All @@ -62,10 +65,71 @@ func (a *aggregator) Add(ctx context.Context, eval Evaluable) error {
// 3. load nodes for pause, if none, run expression
// 4. evaluate tree nodes for pause against data, if ok, run expression

fmt.Printf("%#v\n", parsed)
return fmt.Errorf("not implemented")
return exhaustive, nil
}

func (a *aggregator) addGroup(ctx context.Context, node *Node) (exhaustive bool, err error) {
if len(node.Ors) > 0 {
// If there are additional branches, don't bother to add this to the aggregate tree.
// Mark this as a non-exhaustive addition and skip immediately.
return false, nil
}

// Merge all of the nodes together and check whether each node is aggregateable.
all := append(node.Ands, node)

// TODO: Create a new GroupID for the nodes.

for _, n := range all {
if !n.HasPredicate() || len(n.Ors) > 0 {
// Don't handle sub-branching for now.
return false, nil
}
if !isAggregateable(n) {
return false, nil
}
}

// TODO: Pointer of checksums -> groups

for _, n := range all {
// Each node is aggregateable, so add this to the map for fast filtering.
// TODO: Add to tree
switch n.Predicate.TreeType() {
case TreeTypeART:
case TreeTypeBTree:
return false, nil
}
_ = n
}

return true, err
}

func (a *aggregator) Remove(ctx context.Context, eval Evaluable) error {
return fmt.Errorf("not implemented")
}

func isAggregateable(n *Node) bool {
if n.Predicate == nil {
return true
}
switch n.Predicate.Literal.(type) {
case string:
if n.Predicate.Operator == operators.NotEquals {
// NOTE: NotEquals is _not_ supported. This requires selecting all leaf nodes _except_
// a given leaf, iterating over a tree. We may as well execute every expressiona s the difference
// is negligible.
return false
}
// Right now, we only support equality checking.
//
// TODO: Add GT(e)/LT(e) matching with tree iteration.
return n.Predicate.Operator == operators.Equals
case int64, float64:
// TODO: Add binary tree matching for ints/floats
return false
default:
return false
}
}
9 changes: 9 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ type ParsedExpression struct {
// Exhaustive bool
}

// RootGroups returns the top-level matching groups within an expression. This is a small
// utility to check the number of matching groups easily.
func (p ParsedExpression) RootGroups() []*Node {
if len(p.Root.Ands) == 0 && len(p.Root.Ors) > 1 {
return p.Root.Ors
}
return []*Node{&p.Root}
}

// PredicateGroup represents a group of predicates that must all pass in order to execute the
// given expression. For example, this might contain two predicates representing an expression
// with two operators combined with "&&".
Expand Down
66 changes: 57 additions & 9 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ func TestParse(t *testing.T) {
t,
test.expected,
*actual,
"Invalid strucutre:\n%s\nExpected: %s\n\nGot: %s",
"Invalid strucutre:\n%s\nExpected: %s\n\nGot: %s\nGroups: %d",
test.input,
string(a),
string(b),
len(actual.RootGroups()),
)
}
}
Expand Down Expand Up @@ -756,31 +757,38 @@ func TestParse(t *testing.T) {
},
{
// Swapping the order of the expression
input: `c == 3 && (a == 1 || b == 2)`,
output: `c == 3 && (a == 1 || b == 2)`,
input: `a == 1 && b == 2 && (c == 3 || d == 4)`,
output: `a == 1 && b == 2 && (c == 3 || d == 4)`,
expected: ParsedExpression{
Root: Node{
Ands: []*Node{
{
Predicate: &Predicate{
Literal: int64(3),
Ident: "c",
Literal: int64(1),
Ident: "a",
Operator: operators.Equals,
},
},
{
Predicate: &Predicate{
Literal: int64(2),
Ident: "b",
Operator: operators.Equals,
},
},
},
Ors: []*Node{
{
Predicate: &Predicate{
Literal: int64(1),
Ident: "a",
Literal: int64(3),
Ident: "c",
Operator: operators.Equals,
},
},
{
Predicate: &Predicate{
Literal: int64(2),
Ident: "b",
Literal: int64(4),
Ident: "d",
Operator: operators.Equals,
},
},
Expand Down Expand Up @@ -907,6 +915,46 @@ func TestParse(t *testing.T) {

}

func TestRootGroups(t *testing.T) {
r := require.New(t)
ctx := context.Background()
parser, err := newParser()

r.NoError(err)

t.Run("With single groups", func(t *testing.T) {
actual, err := parser.Parse(ctx, "a == 1")
r.NoError(err)
r.Equal(1, len(actual.RootGroups()))
r.Equal(&actual.Root, actual.RootGroups()[0])

actual, err = parser.Parse(ctx, "a == 1 && b == 2")
r.NoError(err)
r.Equal(1, len(actual.RootGroups()))
r.Equal(&actual.Root, actual.RootGroups()[0])

actual, err = parser.Parse(ctx, "root == 'yes' && (a == 1 || b == 2)")
r.NoError(err)
r.Equal(1, len(actual.RootGroups()))
r.Equal(&actual.Root, actual.RootGroups()[0])
})

t.Run("With an or", func(t *testing.T) {
actual, err := parser.Parse(ctx, "a == 1 || b == 2")
r.NoError(err)
r.Equal(2, len(actual.RootGroups()))

actual, err = parser.Parse(ctx, "a == 1 || b == 2 || c == 3")
r.NoError(err)
r.Equal(3, len(actual.RootGroups()))

actual, err = parser.Parse(ctx, "a == 1 && b == 2 || c == 3")
r.NoError(err)
r.Equal(2, len(actual.RootGroups()))
})

}

/*
func TestParseGroupIDs(t *testing.T) {
t.Run("It creates new group IDs when parsing the same expression", func(t *testing.T) {
Expand Down
3 changes: 1 addition & 2 deletions tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type PredicateTree interface {
Add(ctx context.Context, p Predicate) error
}

// leaf represents the leaf within a tree. This stores all expressions
// Leaf represents the leaf within a tree. This stores all expressions
// which match the given expression.
//
// For example, adding two expressions each matching "event.data == 'foo'"
Expand All @@ -29,7 +29,6 @@ type PredicateTree interface {
//
// Note that there are many sub-clauses which need to be matched. Each
// leaf is a subset of a full expression. Therefore,

type Leaf struct {
Evals []ExpressionPart
}
Expand Down

0 comments on commit 598acf5

Please sign in to comment.