dynamo-browse/internal/dynamo-browse/models/queryexpr/expr_test.go
Leon Mika 32ae488066
All checks were successful
ci / build (push) Successful in 3m17s
Moved package to lmika.dev/cmd/dynamo-browse
2025-05-26 22:04:23 +10:00

1081 lines
39 KiB
Go

package queryexpr_test
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
"github.com/stretchr/testify/assert"
)
func TestModExpr_Query(t *testing.T) {
tableInfo := &models.TableInfo{
Name: "test",
Keys: models.KeyAttribute{
PartitionKey: "pk",
SortKey: "sk",
},
GSIs: []models.TableGSI{
{
Name: "with-color",
Keys: models.KeyAttribute{
PartitionKey: "color",
SortKey: "shade",
},
},
{
Name: "with-apples",
Keys: models.KeyAttribute{
PartitionKey: "apples",
SortKey: "sk",
},
},
{
Name: "with-apples-and-oranges",
Keys: models.KeyAttribute{
PartitionKey: "apples",
SortKey: "oranges",
},
},
},
}
t.Run("as queries", func(t *testing.T) {
scenarios := []scanScenario{
scanCase("when request pk is fixed",
`pk="prefix"`,
`#0 = :0`,
exprNameIsString(0, 0, "pk", "prefix"),
),
//scanCase("when request pk is fixed (reverse)",
// `prefix="pk"`,
// `#0 = :0`,
// exprNameIsString(0, 0, "pk", "prefix"),
//),
scanCase("when request pk is fixed in parens #1",
`(pk="prefix")`,
`#0 = :0`,
exprNameIsString(0, 0, "pk", "prefix"),
),
scanCase("when request pk is fixed in parens #2",
`(pk)="prefix"`,
`#0 = :0`,
exprNameIsString(0, 0, "pk", "prefix"),
),
scanCase("when request pk is fixed in parens #3",
`pk=("prefix")`,
`#0 = :0`,
exprNameIsString(0, 0, "pk", "prefix"),
),
scanCase("when request pk is in with a single value",
`pk in ("prefix")`,
`#0 = :0`,
exprNameIsString(0, 0, "pk", "prefix"),
),
scanCase("when request pk and sk is fixed",
`pk="prefix" and sk="another"`,
`(#0 = :0) AND (#1 = :1)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsString(1, 1, "sk", "another"),
),
scanCase("when request pk and sk is fixed (using 'in')",
`pk in ("prefix") and sk in ("another")`,
`(#0 = :0) AND (#1 = :1)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsString(1, 1, "sk", "another"),
),
scanCase("when request pk is equals and sk is prefix #1",
`pk="prefix" and sk^="another"`,
`(#0 = :0) AND (begins_with (#1, :1))`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsString(1, 1, "sk", "another"),
),
scanCase("when request pk is equals and sk is prefix #2",
`sk^="another" and pk="prefix"`,
`(#0 = :0) AND (begins_with (#1, :1))`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsString(1, 1, "sk", "another"),
),
scanCase("when request pk is equals and sk is less than",
`pk="prefix" and sk < 100`,
`(#0 = :0) AND (#1 < :1)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "sk", "100"),
),
scanCase("when request pk is equals and sk is less or equal to",
`pk="prefix" and sk <= 100`,
`(#0 = :0) AND (#1 <= :1)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "sk", "100"),
),
scanCase("when request pk is equals and sk is greater than",
`pk="prefix" and sk > 100`,
`(#0 = :0) AND (#1 > :1)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "sk", "100"),
),
scanCase("when request pk is equals and sk is greater or equal to",
`pk="prefix" and sk >= 100`,
`(#0 = :0) AND (#1 >= :1)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "sk", "100"),
),
scanCase("when request pk is equals and sk is greater or equal to",
`pk="prefix" and sk between 100 and 200`,
`(#0 = :0) AND (#1 BETWEEN :1 AND :2)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "sk", "100"),
exprValueIsNumber(2, "200"),
),
scanCase("with placeholders",
`:partition=$valuePrefix and :sort=$valueAnother`,
`(#0 = :0) AND (#1 = :1)`,
placeholderNames(map[string]string{
"partition": "pk",
"sort": "sk",
}),
placeholderValues(map[string]types.AttributeValue{
"valuePrefix": &types.AttributeValueMemberS{Value: "prefix"},
"valueAnother": &types.AttributeValueMemberS{Value: "another"},
}),
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsString(1, 1, "sk", "another"),
),
// Querying the index
scanCase("querying the index with the index pk",
`color="blue"`,
`#0 = :0`,
indexName("with-color"),
exprNameIsString(0, 0, "color", "blue"),
),
scanCase("querying the index with the index pk and index sk",
`color="red" and shade="gray"`,
`(#0 = :0) AND (#1 = :1)`,
indexName("with-color"),
exprNameIsString(0, 0, "color", "red"),
exprNameIsString(1, 1, "shade", "gray"),
),
scanCase("querying the index with the index pk and begins with index sk",
`color="yellow" and shade ^= "dark"`,
`(#0 = :0) AND (begins_with (#1, :1))`,
indexName("with-color"),
exprNameIsString(0, 0, "color", "yellow"),
exprNameIsString(1, 1, "shade", "dark"),
),
// Function calls
scanCase("use the value of fn call in query",
`pk = _x_concat("Hello ", "world")`,
`#0 = :0`,
exprNameIsString(0, 0, "pk", "Hello world"),
),
}
for _, scenario := range scenarios {
t.Run(scenario.description, func(t *testing.T) {
modExpr, err := queryexpr.Parse(scenario.expression)
assert.NoError(t, err)
modExpr = modExpr.WithNameParams(scenario.placeholderNames).WithValueParams(scenario.placeholderValues)
plan, err := modExpr.Plan(tableInfo)
assert.NoError(t, err)
assert.True(t, plan.CanQuery)
assert.Equal(t, scenario.indexName, plan.IndexName)
assert.Equal(t, scenario.expectedFilter, aws.ToString(plan.Expression.KeyCondition()))
for k, v := range scenario.expectedNames {
assert.Equal(t, v, plan.Expression.Names()[k])
}
for k, v := range scenario.expectedValues {
assert.Equal(t, v, plan.Expression.Values()[k])
}
})
}
})
t.Run("as scans", func(t *testing.T) {
scenarios := []scanScenario{
scanCase("when request pk prefix", `pk^="prefix"`, `begins_with (#0, :0)`,
exprNameIsString(0, 0, "pk", "prefix"),
),
scanCase("when request sk equals something", `sk="something"`, `#0 = :0`,
exprNameIsString(0, 0, "sk", "something"),
),
scanCase("when request sk starts with something", `sk^="something"`, `begins_with (#0, :0)`,
exprNameIsString(0, 0, "sk", "something"),
),
scanCase("with not equal", `sk != "something"`, `#0 <> :0`,
exprNameIsString(0, 0, "sk", "something"),
),
scanCase("less than value", `num < 100`, `#0 < :0`,
exprNameIsNumber(0, 0, "num", "100"),
),
scanCase("less or equal to value", `num <= 100`, `#0 <= :0`,
exprNameIsNumber(0, 0, "num", "100"),
),
scanCase("greater than value", `num > 100`, `#0 > :0`,
exprNameIsNumber(0, 0, "num", "100"),
),
scanCase("greater or equal to value", `num >= 100`, `#0 >= :0`,
exprNameIsNumber(0, 0, "num", "100"),
),
scanCase("is true", `bool = true`, `#0 = :0`,
exprName(0, "bool"),
exprValueIsBool(0, true),
),
scanCase("is false", `bool = false`, `#0 = :0`,
exprName(0, "bool"),
exprValueIsBool(0, false),
),
scanCase("with disjunctions",
`pk="prefix" or sk="another"`,
`(#0 = :0) OR (#1 = :1)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsString(1, 1, "sk", "another"),
),
scanCase("with disjunctions with numbers",
`pk="prefix" or num=123 and negnum=-131`,
`(#0 = :0) OR ((#1 = :1) AND (#2 = :2))`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "num", "123"),
exprNameIsNumber(2, 2, "negnum", "-131"),
),
scanCase("with disjunctions with numbers (different priority)",
`(pk="prefix" or num=123) and negnum=-131`,
`((#0 = :0) OR (#1 = :1)) AND (#2 = :2)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "num", "123"),
exprNameIsNumber(2, 2, "negnum", "-131"),
),
scanCase("with disjunctions if pk is present twice in expression",
`pk="prefix" and pk="another"`,
`(#0 = :0) AND (#0 = :1)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsString(0, 1, "pk", "another"),
),
scanCase("with not", `not pk="prefix"`, `NOT (#0 = :0)`,
exprNameIsString(0, 0, "pk", "prefix"),
),
scanCase("with between", `pk between "a" and "z"`,
`#0 BETWEEN :0 AND :1`,
exprName(0, "pk"),
exprValueIsString(0, "a"),
exprValueIsString(1, "z"),
),
scanCase("with in", `pk in ("alpha", "bravo", "charlie")`,
`#0 IN (:0, :1, :2)`,
exprName(0, "pk"),
exprValueIsString(0, "alpha"),
exprValueIsString(1, "bravo"),
exprValueIsString(2, "charlie"),
),
scanCase("with not in", `pk not in ("alpha", "bravo", "charlie")`,
`NOT (#0 IN (:0, :1, :2))`,
exprName(0, "pk"),
exprValueIsString(0, "alpha"),
exprValueIsString(1, "bravo"),
exprValueIsString(2, "charlie"),
),
scanCase("with in with single operand returning a sequence", `pk in range(1, 5)`,
`#0 IN (:0, :1, :2, :3, :4)`,
exprName(0, "pk"),
exprValueIsNumber(0, "1"),
exprValueIsNumber(1, "2"),
exprValueIsNumber(2, "3"),
exprValueIsNumber(3, "4"),
exprValueIsNumber(4, "5"),
),
scanCase("with in with single operand not returning a literal", `"foobar" in pk`,
`contains (#0, :0)`,
exprNameIsString(0, 0, "pk", "foobar"),
),
// TODO: in > 100 items ==> items OR items
scanCase("with is S", `pk is "S"`,
`attribute_type (#0, :0)`,
exprNameIsString(0, 0, "pk", "S"),
),
scanCase("with is N", `pk is "N"`,
`attribute_type (#0, :0)`,
exprNameIsString(0, 0, "pk", "N"),
),
scanCase("with is not N", `pk is not "SS"`,
`NOT (attribute_type (#0, :0))`,
exprNameIsString(0, 0, "pk", "SS"),
),
scanCase("with is any", `pk is "any"`,
`attribute_exists (#0)`,
exprName(0, "pk"),
),
scanCase("with is not any", `pk is not "any"`,
`attribute_not_exists (#0)`,
exprName(0, "pk"),
),
scanCase("the size function as a right-side operand", `ln=size(pk)`,
`#0 = size (#1)`,
exprName(0, "ln"),
exprName(1, "pk"),
),
scanCase("the size function as a left-side operand #1", `size(pk) = 123`,
`size (#0) = :0`,
exprNameIsNumber(0, 0, "pk", "123"),
),
scanCase("the size function as a left-side operand #2", `size(pk) > 123`,
`size (#0) > :0`,
exprNameIsNumber(0, 0, "pk", "123"),
),
scanCase("the size function on both sizes",
`size(pk) != size(sk) and size(pk) > size(third) and size(pk) = 131`,
`(size (#0) <> size (#1)) AND (size (#0) > size (#2)) AND (size (#0) = :0)`,
exprName(0, "pk"),
exprName(1, "sk"),
exprName(2, "third"),
exprValueIsNumber(0, "131"),
),
// Sub refs
scanCase("with index", `this[2] = "something"`, `#0[2] = :0`,
exprName(0, "this"),
exprValueIsString(0, "something"),
),
scanCase("with the dot", `this.value = "something"`, `#0 = :0`,
exprName(0, "this.value"),
exprValueIsString(0, "something"),
),
/*
scanCase("with multiple indices", `this[2][3] = "something"`, `#0[2][3] = :0`,
exprName(0, "this"),
exprValueIsString(0, "something"),
),
scanCase("with multiple indices with paren", `((this[2])[3])[4] = "something"`, `#0[2][3][4] = :0`,
exprName(0, "this"),
exprValueIsString(0, "something"),
),
*/
scanCase("with multiple dots", `this.that.other.value = "else"`, `#0 = :0`,
exprName(0, "this.that.other.value"),
exprValueIsString(0, "else"),
),
// TODO: the contains function
// Placeholders
scanCase("with placeholders",
`:partition=$valuePrefix or :sort=$valueAnother`,
`(#0 = :0) OR (#1 = :1)`,
placeholderNames(map[string]string{
"partition": "pk",
"sort": "sk",
}),
placeholderValues(map[string]types.AttributeValue{
"valuePrefix": &types.AttributeValueMemberS{Value: "prefix"},
"valueAnother": &types.AttributeValueMemberS{Value: "another"},
}),
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsString(1, 1, "sk", "another"),
),
}
for _, scenario := range scenarios {
t.Run(scenario.description, func(t *testing.T) {
modExpr, err := queryexpr.Parse(scenario.expression)
assert.NoError(t, err)
modExpr = modExpr.WithNameParams(scenario.placeholderNames).WithValueParams(scenario.placeholderValues)
plan, err := modExpr.Plan(tableInfo)
assert.NoError(t, err)
assert.False(t, plan.CanQuery)
assert.Equal(t, scenario.expectedFilter, aws.ToString(plan.Expression.Filter()))
for k, v := range scenario.expectedNames {
assert.Equal(t, v, plan.Expression.Names()[k])
}
for k, v := range scenario.expectedValues {
assert.Equal(t, v, plan.Expression.Values()[k])
}
})
}
})
t.Run("with index clash", func(t *testing.T) {
t.Run("should return error if attempt to run query with two indices that can be chosen", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this"`)
assert.NoError(t, err)
_, err = modExpr.Plan(tableInfo)
assert.Error(t, err)
})
t.Run("should run as scan if explicitly forced to", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this" using scan`)
assert.NoError(t, err)
plan, err := modExpr.Plan(tableInfo)
assert.NoError(t, err)
assert.False(t, plan.CanQuery)
})
t.Run("should run as query with the 'with-apples' index", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this" using index("with-apples")`)
assert.NoError(t, err)
plan, err := modExpr.Plan(tableInfo)
assert.NoError(t, err)
assert.True(t, plan.CanQuery)
assert.Equal(t, "with-apples", plan.IndexName)
})
t.Run("should run as query with the 'with-apples-and-oranges' index", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this" using index("with-apples-and-oranges")`)
assert.NoError(t, err)
plan, err := modExpr.Plan(tableInfo)
assert.NoError(t, err)
assert.True(t, plan.CanQuery)
assert.Equal(t, "with-apples-and-oranges", plan.IndexName)
})
t.Run("should return error if the chosen index can't be used", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this" using index("with-missing")`)
assert.NoError(t, err)
_, err = modExpr.Plan(tableInfo)
assert.Error(t, err)
})
})
}
func TestQueryExpr_EvalItem(t *testing.T) {
var (
item = models.Item{
"alpha": &types.AttributeValueMemberS{Value: "alpha"},
"bravo": &types.AttributeValueMemberN{Value: "123"},
"charlie": &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"door": &types.AttributeValueMemberS{Value: "red"},
"tree": &types.AttributeValueMemberS{Value: "green"},
},
},
"prime": &types.AttributeValueMemberL{
Value: []types.AttributeValue{
&types.AttributeValueMemberN{Value: "2"},
&types.AttributeValueMemberN{Value: "3"},
&types.AttributeValueMemberN{Value: "5"},
&types.AttributeValueMemberN{Value: "7"},
},
},
"one": &types.AttributeValueMemberN{Value: "1"},
"three": &types.AttributeValueMemberN{Value: "3"},
"five": &types.AttributeValueMemberN{Value: "5"},
}
)
t.Run("simple values", func(t *testing.T) {
scenarios := []struct {
expr string
expected types.AttributeValue
}{
// Simple values
{expr: `alpha`, expected: &types.AttributeValueMemberS{Value: "alpha"}},
{expr: `bravo`, expected: &types.AttributeValueMemberN{Value: "123"}},
{expr: `charlie`, expected: item["charlie"]},
{expr: `missing`, expected: nil},
// Equality with literal
{expr: `alpha="alpha"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha!="not alpha"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `bravo=123`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `charlie.tree="green"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha^="al"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha="foobar"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `alpha^="need-something"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
// Comparison
{expr: "three > 4", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "three >= 4", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "three < 4", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three <= 4", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three > 3", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "three >= 3", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three < 3", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "three <= 3", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three > 2", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three >= 2", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three < 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "three <= 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
// Between
{expr: "3 between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between one and five", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 10 and 15", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "three between 1 and 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "8 between five and 10", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 1 and 3", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 3 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `"e" between "a" and "z"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `"eee" between "aaa" and "zzz"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `"e" between "between" and "beyond"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
// In
{expr: "three in (2, 3, 4, 5)", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three in (20, 30, 40)", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `alpha in ("alpha", "beta", "gamma", "delta")`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha in ("ey", "be", "see")`, expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `three in prime`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `1 in prime`, expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `"door" in charlie`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `"sky" in charlie`, expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `"al" in alpha`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `"cent" in "percentage"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
// Is
{expr: `alpha is "S"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha is not "N"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `three is "N"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `three is not "S"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `(three = 3) is "BOOL"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `prime is "L"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `charlie is "M"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha is "any"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `three is "any"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `(three = 3) is "any"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `charlie is "any"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `prime is "any"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `undef is not "any"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
// Size
{expr: `size(alpha)`, expected: &types.AttributeValueMemberN{Value: "5"}},
{expr: `size("This is a test")`, expected: &types.AttributeValueMemberN{Value: "14"}},
{expr: `size(charlie)`, expected: &types.AttributeValueMemberN{Value: "2"}},
{expr: `size(prime)`, expected: &types.AttributeValueMemberN{Value: "4"}},
// Dot values
{expr: `charlie.door`, expected: &types.AttributeValueMemberS{Value: "red"}},
{expr: `(charlie).door`, expected: &types.AttributeValueMemberS{Value: "red"}},
{expr: `charlie.tree`, expected: &types.AttributeValueMemberS{Value: "green"}},
// Conjunction
{expr: `alpha="alpha" and bravo=123`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha="alpha" and bravo=321`, expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `alpha="bravo" and bravo=123`, expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `alpha="bravo" and bravo=321`, expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `alpha="alpha" and bravo=123 and charlie.door="red"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha="alpha" and bravo=123 and charlie.door^="green"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
// Disjunction
{expr: `alpha="alpha" or bravo=123`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha="alpha" or bravo=321`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha="bravo" or bravo=123`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha="bravo" or bravo=321`, expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `alpha="alpha" or bravo=123 or charlie.tree="green"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha="bravo" or bravo=321 or charlie.tree^="red"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
// Bool negation
{expr: `not alpha="alpha"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: `not alpha!="alpha"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
// Order of operation
{expr: `alpha="alpha" and bravo=123 or charlie.door="green"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `alpha="bravo" or bravo=321 and charlie.door="green"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
}
for _, scenario := range scenarios {
t.Run(scenario.expr, func(t *testing.T) {
modExpr, err := queryexpr.Parse(scenario.expr)
assert.NoError(t, err)
res, err := modExpr.EvalItem(item)
assert.NoError(t, err)
assert.Equal(t, scenario.expected, res)
})
}
})
t.Run("functions", func(t *testing.T) {
timeNow := time.Now()
contextResultSet := models.ResultSet{}
contextResultSet.SetItems([]models.Item{
{"pk": &types.AttributeValueMemberS{Value: "1"}, "num": &types.AttributeValueMemberN{Value: "1"}},
{"pk": &types.AttributeValueMemberS{Value: "2"}, "num": &types.AttributeValueMemberN{Value: "2"}},
{"pk": &types.AttributeValueMemberS{Value: "3"}, "num": &types.AttributeValueMemberN{Value: "3"}},
{"pk": &types.AttributeValueMemberS{Value: "4"}, "num": &types.AttributeValueMemberN{Value: "4"}},
})
contextResultSet.SetMark(0, true)
contextResultSet.SetMark(1, true)
scenarios := []struct {
expr string
expected types.AttributeValue
}{
// _x_now() -- unreleased version of now
{expr: `_x_now()`, expected: &types.AttributeValueMemberN{Value: fmt.Sprint(timeNow.Unix())}},
// Marked
{expr: `marked("num")`, expected: &types.AttributeValueMemberL{Value: []types.AttributeValue{
&types.AttributeValueMemberN{Value: "1"},
&types.AttributeValueMemberN{Value: "2"},
}}},
{expr: `one in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `three in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: false}},
}
for _, scenario := range scenarios {
t.Run(scenario.expr, func(t *testing.T) {
modExpr, err := queryexpr.Parse(scenario.expr)
assert.NoError(t, err)
res, err := modExpr.
WithTestTimeSource(timeNow).
WithCurrentResultSet(&contextResultSet).
EvalItem(item)
assert.NoError(t, err)
assert.Equal(t, scenario.expected, res)
})
}
})
t.Run("unparsed expression", func(t *testing.T) {
scenarios := []struct {
expr string
expectedError error
}{
{expr: `bla ^ = "something"`},
}
for _, scenario := range scenarios {
t.Run(scenario.expr, func(t *testing.T) {
_, err := queryexpr.Parse(scenario.expr)
assert.Error(t, err)
})
}
})
t.Run("expression errors", func(t *testing.T) {
scenarios := []struct {
expr string
expectedError error
}{
{expr: `alpha.bravo`, expectedError: queryexpr.ValueNotAMapError([]string{"alpha", "bravo"})},
{expr: `charlie.tree.bla`, expectedError: queryexpr.ValueNotAMapError([]string{"charlie", "tree", "bla"})},
{expr: `missing="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}},
{expr: `missing!="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}},
{expr: `missing^="no"`, expectedError: queryexpr.ValueNotConvertableToString{nil}},
}
for _, scenario := range scenarios {
t.Run(scenario.expr, func(t *testing.T) {
modExpr, err := queryexpr.Parse(scenario.expr)
assert.NoError(t, err)
res, err := modExpr.EvalItem(item)
assert.Nil(t, res)
assert.Equal(t, scenario.expectedError, err)
})
}
})
t.Run("name and value placeholders", func(t *testing.T) {
scenarios := []struct {
expr string
expected types.AttributeValue
}{
{expr: `alpha = $a`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `:theBName = 123`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `:theCMap.door`, expected: &types.AttributeValueMemberS{Value: "red"}},
}
for _, scenario := range scenarios {
t.Run(scenario.expr, func(t *testing.T) {
modExpr, err := queryexpr.Parse(scenario.expr)
assert.NoError(t, err)
modExpr = modExpr.WithValueParams(map[string]types.AttributeValue{
"a": &types.AttributeValueMemberS{Value: "alpha"},
}).WithNameParams(map[string]string{
"theBName": "bravo",
"theCMap": "charlie",
})
res, err := modExpr.EvalItem(item)
assert.NoError(t, err)
assert.Equal(t, scenario.expected, res)
})
}
})
}
func TestQueryExpr_SetEvalItem(t *testing.T) {
var templateItem = func() models.Item {
return models.Item{
"alpha": &types.AttributeValueMemberS{Value: "alpha"},
"bravo": &types.AttributeValueMemberN{Value: "123"},
"charlie": &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"door": &types.AttributeValueMemberS{Value: "red"},
"tree": &types.AttributeValueMemberS{Value: "green"},
},
},
"prime": &types.AttributeValueMemberL{
Value: []types.AttributeValue{
&types.AttributeValueMemberN{Value: "2"},
&types.AttributeValueMemberN{Value: "3"},
&types.AttributeValueMemberN{Value: "5"},
&types.AttributeValueMemberN{Value: "7"},
},
},
"three": &types.AttributeValueMemberN{Value: "3"},
}
}
t.Run("simple values", func(t *testing.T) {
item := templateItem()
modExpr, err := queryexpr.Parse("alpha")
assert.NoError(t, err)
assert.True(t, modExpr.IsModifiablePath(item))
err = modExpr.SetEvalItem(item, &types.AttributeValueMemberS{Value: "not alpha"})
assert.NoError(t, err)
assert.Equal(t, "not alpha", item["alpha"].(*types.AttributeValueMemberS).Value)
})
t.Run("dot values", func(t *testing.T) {
item := templateItem()
modExpr, err := queryexpr.Parse("charlie.tree")
assert.NoError(t, err)
assert.True(t, modExpr.IsModifiablePath(item))
err = modExpr.SetEvalItem(item, &types.AttributeValueMemberS{Value: "Birch"})
assert.NoError(t, err)
assert.Equal(t, "Birch", item["charlie"].(*types.AttributeValueMemberM).Value["tree"].(*types.AttributeValueMemberS).Value)
})
}
func TestQueryExpr_DeleteAttribute(t *testing.T) {
var templateItem = func() models.Item {
return models.Item{
"alpha": &types.AttributeValueMemberS{Value: "alpha"},
"bravo": &types.AttributeValueMemberN{Value: "123"},
"charlie": &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"door": &types.AttributeValueMemberS{Value: "red"},
"tree": &types.AttributeValueMemberS{Value: "green"},
},
},
"prime": &types.AttributeValueMemberL{
Value: []types.AttributeValue{
&types.AttributeValueMemberN{Value: "2"},
&types.AttributeValueMemberN{Value: "3"},
&types.AttributeValueMemberN{Value: "5"},
&types.AttributeValueMemberN{Value: "7"},
},
},
"three": &types.AttributeValueMemberN{Value: "3"},
}
}
t.Run("simple values", func(t *testing.T) {
item := templateItem()
modExpr, err := queryexpr.Parse("alpha")
assert.NoError(t, err)
err = modExpr.DeleteAttribute(item)
assert.NoError(t, err)
_, hasKey := item["alpha"]
assert.False(t, hasKey)
})
t.Run("placeholder values", func(t *testing.T) {
item := templateItem()
modExpr, err := queryexpr.Parse(":a")
assert.NoError(t, err)
modExpr = modExpr.WithNameParams(map[string]string{"a": "alpha"})
err = modExpr.DeleteAttribute(item)
assert.NoError(t, err)
_, hasKey := item["alpha"]
assert.False(t, hasKey)
})
t.Run("dot values", func(t *testing.T) {
item := templateItem()
modExpr, err := queryexpr.Parse("charlie.tree")
assert.NoError(t, err)
err = modExpr.DeleteAttribute(item)
assert.NoError(t, err)
_, hasKey := item["charlie"].(*types.AttributeValueMemberM).Value["tree"]
assert.False(t, hasKey)
})
t.Run("dot values with placeholders", func(t *testing.T) {
item := templateItem()
modExpr, err := queryexpr.Parse(":c.tree")
assert.NoError(t, err)
modExpr = modExpr.WithNameParams(map[string]string{"c": "charlie"})
err = modExpr.DeleteAttribute(item)
assert.NoError(t, err)
_, hasKey := item["charlie"].(*types.AttributeValueMemberM).Value["tree"]
assert.False(t, hasKey)
})
//t.Run("dot values with multiple placeholders", func(t *testing.T) {
// item := templateItem()
//
// modExpr, err := queryexpr.Parse(":c.:t")
// assert.NoError(t, err)
//
// modExpr = modExpr.WithNameParams(map[string]string{
// "c": "charlie",
// "t": "tree",
// })
//
// err = modExpr.DeleteAttribute(item)
// assert.NoError(t, err)
//
// _, hasKey := item["charlie"].(*types.AttributeValueMemberM).Value["tree"]
// assert.False(t, hasKey)
//})
}
func TestQueryExpr_SerializeTo(t *testing.T) {
t.Run("should be able to serialized and deseralize the parsed expression", func(t *testing.T) {
exprStr := `something = $value and :placeholder = "something else" and thirdThing in (1,2,3)`
bts := new(bytes.Buffer)
modExpr, err := queryexpr.Parse(exprStr)
assert.NoError(t, err)
modExpr = modExpr.WithNameParams(map[string]string{
"placeholder": "some name",
}).WithValueParams(map[string]types.AttributeValue{
"value": &types.AttributeValueMemberS{Value: "some value"},
"num": &types.AttributeValueMemberN{Value: "12345"},
"veryLargeNumber": &types.AttributeValueMemberN{Value: "123456789012345678901234567890"},
"numberSet": &types.AttributeValueMemberNS{Value: []string{"123", "234", "345"}},
"bool": &types.AttributeValueMemberBOOL{Value: true},
"list": &types.AttributeValueMemberL{Value: []types.AttributeValue{
&types.AttributeValueMemberN{Value: "1"},
&types.AttributeValueMemberN{Value: "2"},
&types.AttributeValueMemberN{Value: "3"},
}},
"dict": &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"alpha": &types.AttributeValueMemberS{Value: "apple"},
"bravo": &types.AttributeValueMemberS{Value: "banana"},
"charlie": &types.AttributeValueMemberS{Value: "cherry"},
},
},
})
assert.NoError(t, modExpr.SerializeTo(bts))
newExpr, err := queryexpr.DeserializeFrom(bts)
assert.NoError(t, err)
assert.Equal(t, modExpr.String(), newExpr.String())
name, hasName := newExpr.NameParam("placeholder")
assert.Equal(t, "some name", name)
assert.True(t, hasName)
assert.Equal(t, "some value", newExpr.ValueParamOrNil("value").(*types.AttributeValueMemberS).Value)
assert.Equal(t, "12345", newExpr.ValueParamOrNil("num").(*types.AttributeValueMemberN).Value)
assert.Equal(t, "123456789012345678901234567890", newExpr.ValueParamOrNil("veryLargeNumber").(*types.AttributeValueMemberN).Value)
assert.Equal(t, []string{"123", "234", "345"}, newExpr.ValueParamOrNil("numberSet").(*types.AttributeValueMemberNS).Value)
assert.Equal(t, true, newExpr.ValueParamOrNil("bool").(*types.AttributeValueMemberBOOL).Value)
assert.Equal(t, "1", newExpr.ValueParamOrNil("list").(*types.AttributeValueMemberL).Value[0].(*types.AttributeValueMemberN).Value)
assert.Equal(t, "2", newExpr.ValueParamOrNil("list").(*types.AttributeValueMemberL).Value[1].(*types.AttributeValueMemberN).Value)
assert.Equal(t, "3", newExpr.ValueParamOrNil("list").(*types.AttributeValueMemberL).Value[2].(*types.AttributeValueMemberN).Value)
assert.Equal(t, "apple", newExpr.ValueParamOrNil("dict").(*types.AttributeValueMemberM).Value["alpha"].(*types.AttributeValueMemberS).Value)
assert.Equal(t, "banana", newExpr.ValueParamOrNil("dict").(*types.AttributeValueMemberM).Value["bravo"].(*types.AttributeValueMemberS).Value)
assert.Equal(t, "cherry", newExpr.ValueParamOrNil("dict").(*types.AttributeValueMemberM).Value["charlie"].(*types.AttributeValueMemberS).Value)
})
}
func TestQueryExpr_Equals(t *testing.T) {
t.Run("should perform equals correctly", func(t *testing.T) {
exprStr := `something = $value and :placeholder = "something else" and thirdThing in (1,2,3)`
modExpr, _ := queryexpr.Parse(exprStr)
modExpr = modExpr.WithNameParams(map[string]string{
"placeholder": "some name",
"another": "name",
"more": "names",
}).WithValueParams(map[string]types.AttributeValue{
"value": &types.AttributeValueMemberS{Value: "some value"},
"num": &types.AttributeValueMemberN{Value: "12345"},
"veryLargeNumber": &types.AttributeValueMemberN{Value: "123456789012345678901234567890"},
"numberSet": &types.AttributeValueMemberNS{Value: []string{"123", "234", "345"}},
"bool": &types.AttributeValueMemberBOOL{Value: true},
"list": &types.AttributeValueMemberL{Value: []types.AttributeValue{
&types.AttributeValueMemberN{Value: "1"},
&types.AttributeValueMemberN{Value: "2"},
&types.AttributeValueMemberN{Value: "3"},
}},
"dict": &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"alpha": &types.AttributeValueMemberS{Value: "apple"},
"bravo": &types.AttributeValueMemberS{Value: "banana"},
"charlie": &types.AttributeValueMemberS{Value: "cherry"},
},
},
})
differentExpr, _ := queryexpr.Parse(`abc = :bla`)
differentExpr = modExpr.WithNameParams(map[string]string{
"fla": "some name",
}).WithValueParams(map[string]types.AttributeValue{
"value": &types.AttributeValueMemberS{Value: "some value"},
})
bts1, err := modExpr.SerializeToBytes()
assert.NoError(t, err)
expr2, err := queryexpr.DeserializeFrom(bytes.NewReader(bts1))
assert.NoError(t, err)
bts2, err := expr2.SerializeToBytes()
assert.NoError(t, err)
expr3, err := queryexpr.DeserializeFrom(bytes.NewReader(bts2))
assert.NoError(t, err)
_, err = expr3.SerializeToBytes()
assert.NoError(t, err)
var nilQE *queryexpr.QueryExpr
assert.True(t, nilQE.Equal(nil))
assert.True(t, modExpr.Equal(expr2))
assert.True(t, expr2.Equal(expr3))
assert.True(t, expr3.Equal(modExpr))
assert.False(t, nilQE.Equal(differentExpr))
assert.False(t, modExpr.Equal(differentExpr))
assert.False(t, expr2.Equal(differentExpr))
assert.False(t, expr3.Equal(differentExpr))
assert.Equal(t, uint64(0), nilQE.HashCode())
assert.Equal(t, modExpr.HashCode(), expr2.HashCode())
assert.Equal(t, expr2.HashCode(), expr3.HashCode())
assert.Equal(t, expr3.HashCode(), modExpr.HashCode())
assert.NotEqual(t, differentExpr.HashCode(), nilQE.HashCode())
assert.NotEqual(t, differentExpr.HashCode(), expr2.HashCode())
assert.NotEqual(t, differentExpr.HashCode(), expr3.HashCode())
assert.NotEqual(t, differentExpr.HashCode(), modExpr.HashCode())
})
}
type scanScenario struct {
description string
expression string
expectedFilter string
indexName string
expectedNames map[string]string
expectedValues map[string]types.AttributeValue
placeholderNames map[string]string
placeholderValues map[string]types.AttributeValue
}
func scanCase(description, expression, expectedFilter string, options ...func(ss *scanScenario)) scanScenario {
ss := scanScenario{
description: description,
expression: expression,
expectedFilter: expectedFilter,
expectedNames: map[string]string{},
expectedValues: map[string]types.AttributeValue{},
}
for _, opt := range options {
opt(&ss)
}
return ss
}
func indexName(indexName string) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.indexName = indexName
}
}
func placeholderNames(placeholderNames map[string]string) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.placeholderNames = placeholderNames
}
}
func placeholderValues(placeholderValues map[string]types.AttributeValue) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.placeholderValues = placeholderValues
}
}
func exprName(idx int, name string) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.expectedNames[fmt.Sprintf("#%d", idx)] = name
}
}
func exprValueIsString(valIdx int, expected string) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.expectedValues[fmt.Sprintf(":%d", valIdx)] = &types.AttributeValueMemberS{Value: expected}
}
}
func exprValueIsNumber(valIdx int, expected string) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.expectedValues[fmt.Sprintf(":%d", valIdx)] = &types.AttributeValueMemberN{Value: expected}
}
}
func exprValueIsBool(valIdx int, expected bool) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.expectedValues[fmt.Sprintf(":%d", valIdx)] = &types.AttributeValueMemberBOOL{Value: expected}
}
}
func exprNameIsString(idx, valIdx int, name string, expected string) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.expectedNames[fmt.Sprintf("#%d", idx)] = name
ss.expectedValues[fmt.Sprintf(":%d", valIdx)] = &types.AttributeValueMemberS{Value: expected}
}
}
func exprNameIsNumber(idx, valIdx int, name string, expected string) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.expectedNames[fmt.Sprintf("#%d", idx)] = name
ss.expectedValues[fmt.Sprintf(":%d", valIdx)] = &types.AttributeValueMemberN{Value: expected}
}
}