diff --git a/expr.go b/expr.go index c3b4265..d1c2ced 100644 --- a/expr.go +++ b/expr.go @@ -115,7 +115,6 @@ func (a *aggregator) Evaluate(ctx context.Context, data map[string]any) ([]Evalu // TODO: Concurrently match constant expressions using a semaphore for capacity. for _, expr := range a.constants { atomic.AddInt32(&matched, 1) - // NOTE: We don't need to add lifted expression variables, // because match.Parsed.Evaluable() returns the original expression // string. diff --git a/expr_test.go b/expr_test.go index a5baa79..93a0594 100644 --- a/expr_test.go +++ b/expr_test.go @@ -76,7 +76,6 @@ func TestEvaluate(t *testing.T) { e := NewAggregateEvaluator(parser, testBoolEvaluator) expected := tex(`event.data.account_id == "yes" && event.data.match == "true"`) - _, err = e.Add(ctx, expected) require.NoError(t, err) @@ -121,7 +120,6 @@ func TestEvaluate(t *testing.T) { }) t.Run("It handles non-matching data", func(t *testing.T) { - fmt.Println("evaluating") pre := time.Now() evals, matched, err := e.Evaluate(ctx, map[string]any{ "event": map[string]any{ @@ -139,6 +137,89 @@ func TestEvaluate(t *testing.T) { require.EqualValues(t, 0, len(evals)) require.EqualValues(t, 1, matched) // We still ran one expression }) + + t.Run("It handles matching on arrays of data", func(t *testing.T) { + pre := time.Now() + evals, matched, err := e.Evaluate(ctx, map[string]any{ + "event": map[string]any{ + "data": map[string]any{ + "ids": []string{"a", "b", "c"}, + }, + }, + }) + total := time.Since(pre) + fmt.Printf("Matched in %v ns\n", total.Nanoseconds()) + fmt.Printf("Matched in %v ms\n", total.Milliseconds()) + + require.NoError(t, err) + require.EqualValues(t, 0, len(evals)) + require.EqualValues(t, 1, matched) // We still ran one expression + }) +} + +func TestEvaluate_ArrayIndexes(t *testing.T) { + ctx := context.Background() + parser, err := NewTreeParser(NewCachingParser(newEnv())) + require.NoError(t, err) + e := NewAggregateEvaluator(parser, testBoolEvaluator) + + expected := tex(`event.data.ids[2] == "id-b"`) + _, err = e.Add(ctx, expected) + require.NoError(t, err) + + n := 100_000 + wg := sync.WaitGroup{} + for i := 0; i < n; i++ { + wg.Add(1) + //nolint:all + go func() { + defer wg.Done() + byt := make([]byte, 8) + _, err := rand.Read(byt) + require.NoError(t, err) + str := hex.EncodeToString(byt) + + _, err = e.Add(ctx, tex(fmt.Sprintf(`event.data.account_id == "%s"`, str))) + require.NoError(t, err) + }() + } + wg.Wait() + + t.Run("It doesn't return if arrays contain non-matching data", func(t *testing.T) { + pre := time.Now() + evals, matched, err := e.Evaluate(ctx, map[string]any{ + "event": map[string]any{ + "data": map[string]any{ + "ids": []string{"none-match", "nope"}, + }, + }, + }) + total := time.Since(pre) + fmt.Printf("Matched in %v ns\n", total.Nanoseconds()) + fmt.Printf("Matched in %v ms\n", total.Milliseconds()) + + require.NoError(t, err) + require.EqualValues(t, 0, len(evals)) + require.EqualValues(t, 0, matched) + }) + + t.Run("It matches arrays", func(t *testing.T) { + pre := time.Now() + evals, matched, err := e.Evaluate(ctx, map[string]any{ + "event": map[string]any{ + "data": map[string]any{ + "ids": []string{"a", "yes", "id-b"}, + }, + }, + }) + total := time.Since(pre) + fmt.Printf("Matched in %v ns\n", total.Nanoseconds()) + fmt.Printf("Matched in %v ms\n", total.Milliseconds()) + + require.NoError(t, err) + require.EqualValues(t, 1, len(evals)) + require.EqualValues(t, 1, matched) + }) } func TestAggregateMatch(t *testing.T) { diff --git a/parser.go b/parser.go index 1793a21..537b969 100644 --- a/parser.go +++ b/parser.go @@ -486,34 +486,32 @@ func callToPredicate(item celast.Expr, negated bool, vars LiftedArgs) *Predicate ) for _, item := range args { + var ident string + switch item.Kind() { - case celast.IdentKind: - if identA == "" { - identA = item.AsIdent() - } else { - identB = item.AsIdent() + case celast.CallKind: + ident = parseArrayAccess(item) + if ident == "" { + // TODO: Panic or mark as non-exhaustive parse. + return nil } + case celast.IdentKind: + ident = item.AsIdent() case celast.LiteralKind: literal = item.AsLiteral().Value() case celast.SelectKind: // This is an expression, ie. "event.data.foo" Iterate from the root field upwards // to get the full ident. - walked := "" - for item.Kind() == celast.SelectKind { - sel := item.AsSelect() - if walked == "" { - walked = sel.FieldName() - } else { - walked = sel.FieldName() + "." + walked - } - item = sel.Operand() - } - walked = item.AsIdent() + "." + walked + ident = walkSelect(item) + } + if ident != "" { if identA == "" { - identA = walked + // Set the first ident + identA = ident } else { - identB = walked + // Set the second. + identB = ident } } } @@ -621,6 +619,35 @@ func callToPredicate(item celast.Expr, negated bool, vars LiftedArgs) *Predicate } } +func walkSelect(item celast.Expr) string { + // This is an expression, ie. "event.data.foo" Iterate from the root field upwards + // to get the full ident. + walked := "" + for item.Kind() == celast.SelectKind { + sel := item.AsSelect() + if walked == "" { + walked = sel.FieldName() + } else { + walked = sel.FieldName() + "." + walked + } + item = sel.Operand() + if item.Kind() == celast.CallKind { + arrayPrefix := parseArrayAccess(item) + walked = arrayPrefix + "." + walked + } + } + return strings.TrimPrefix(item.AsIdent()+"."+walked, ".") +} + +func parseArrayAccess(item celast.Expr) string { + // The only supported accessor here is _[_], which is an array index accessor. + if item.AsCall().FunctionName() != operators.Index && item.AsCall().FunctionName() != operators.OptIndex { + return "" + } + args := item.AsCall().Args() + return fmt.Sprintf("%s[%v]", walkSelect(args[0]), args[1].AsLiteral().Value()) +} + func invert(op string) string { switch op { case operators.Equals: diff --git a/parser_test.go b/parser_test.go index 15d9db1..e8be265 100644 --- a/parser_test.go +++ b/parser_test.go @@ -69,9 +69,41 @@ func TestParse(t *testing.T) { } } + t.Run("It handles array indexing", func(t *testing.T) { + tests := []parseTestInput{ + { + input: `event.data.ids[2] == "a"`, + output: `event.data.ids[2] == "a"`, + expected: ParsedExpression{ + Root: Node{ + Predicate: &Predicate{ + Ident: "event.data.ids[2]", + Literal: "a", + Operator: operators.Equals, + }, + }, + }, + }, + { + input: `event.data.ids[2].id == "a"`, + output: `event.data.ids[2].id == "a"`, + expected: ParsedExpression{ + Root: Node{ + Predicate: &Predicate{ + Ident: "event.data.ids[2].id", + Literal: "a", + Operator: operators.Equals, + }, + }, + }, + }, + } + + assert(t, tests) + }) + t.Run("It handles ident matching", func(t *testing.T) { ident := "vars.a" - _ = ident tests := []parseTestInput{ {