diff --git a/expr.go b/expr.go index 1f74c7f..f1e1c19 100644 --- a/expr.go +++ b/expr.go @@ -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 @@ -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: // @@ -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 + } +} diff --git a/parser.go b/parser.go index 1bf202b..d9c2207 100644 --- a/parser.go +++ b/parser.go @@ -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 "&&". diff --git a/parser_test.go b/parser_test.go index 3f35daf..f55f941 100644 --- a/parser_test.go +++ b/parser_test.go @@ -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()), ) } } @@ -756,15 +757,22 @@ 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, }, }, @@ -772,15 +780,15 @@ func TestParse(t *testing.T) { 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, }, }, @@ -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) { diff --git a/tree.go b/tree.go index 375d96c..424db9a 100644 --- a/tree.go +++ b/tree.go @@ -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'" @@ -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 }