Skip to content

Commit

Permalink
Merge pull request #6 from inngest/chore/enable-array-indexing
Browse files Browse the repository at this point in the history
Enable support for array indexing
  • Loading branch information
tonyhb authored Jan 5, 2024
2 parents 8736ddc + 98d6146 commit 83de348
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 22 deletions.
1 change: 0 additions & 1 deletion expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
85 changes: 83 additions & 2 deletions expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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{
Expand All @@ -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) {
Expand Down
63 changes: 45 additions & 18 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -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:
Expand Down
34 changes: 33 additions & 1 deletion parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
{
Expand Down

0 comments on commit 83de348

Please sign in to comment.