Added a few changes to query expressions (#51)

- Added the between operator to query expressions.
- Added the using query expression suffix to specify which index to query (or force a scan). This is required if query planning has found multiple indices that can potentially be used.
- Rewrote the types of the query expressions to allow for functions to be defined once, and be useful in queries that result in DynamoDB queries, and evaluation.
- Added some test functions around time and summing numbers.
- Fixed a bug in the del-attr which was not honouring marked rows in a similar way to set-attr: it was only deleting attributes from the first row.
- Added the -to type flag to set-attr which will set the attribute to the value of a query expression.
This commit is contained in:
Leon Mika 2023-04-14 15:35:43 +10:00 committed by GitHub
parent 835ddd5630
commit 4b4d515ade
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1284 additions and 339 deletions

View file

@ -8,6 +8,16 @@ func Values[K comparable, T any](ts map[K]T) []T {
return values return values
} }
func MapValues[K comparable, T, U any](ts map[K]T, fn func(t T) U) map[K]U {
us := make(map[K]U)
for k, t := range ts {
us[k] = fn(t)
}
return us
}
func MapValuesWithError[K comparable, T, U any](ts map[K]T, fn func(t T) (U, error)) (map[K]U, error) { func MapValuesWithError[K comparable, T, U any](ts map[K]T, fn func(t T) (U, error)) (map[K]U, error) {
us := make(map[K]U) us := make(map[K]U)

View file

@ -39,3 +39,22 @@ func Filter[T any](ts []T, fn func(t T) bool) []T {
} }
return us return us
} }
func FindFirst[T any](ts []T, fn func(t T) bool) (returnedT T, found bool) {
for _, t := range ts {
if fn(t) {
return t, true
}
}
return returnedT, false
}
func FindLast[T any](ts []T, fn func(t T) bool) (returnedT T, found bool) {
for i := len(ts) - 1; i >= 0; i-- {
t := ts[i]
if fn(t) {
return t, true
}
}
return returnedT, false
}

View file

@ -182,6 +182,9 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
if opts.ValuePlaceholders != nil { if opts.ValuePlaceholders != nil {
expr = expr.WithValueParams(opts.ValuePlaceholders) expr = expr.WithValueParams(opts.ValuePlaceholders)
} }
if opts.IndexName != "" {
expr = expr.WithIndex(opts.IndexName)
}
// Get the table info // Get the table info
var tableInfo *models.TableInfo var tableInfo *models.TableInfo

View file

@ -122,6 +122,8 @@ func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.Item
return twc.setBoolValue(idx, path) return twc.setBoolValue(idx, path)
case models.NullItemType: case models.NullItemType:
return twc.setNullValue(idx, path) return twc.setNullValue(idx, path)
case models.ExprValueItemType:
return twc.setToExpressionValue(idx, path)
default: default:
return events.Error(errors.New("unsupported attribute type")) return events.Error(errors.New("unsupported attribute type"))
} }
@ -151,6 +153,39 @@ func (twc *TableWriteController) setStringValue(idx int, attr *queryexpr.QueryEx
} }
} }
func (twc *TableWriteController) setToExpressionValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
return events.PromptForInputMsg{
Prompt: "expr value: ",
OnDone: func(value string) tea.Msg {
valueExpr, err := queryexpr.Parse(value)
if err != nil {
return events.Error(err)
}
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
newValue, err := valueExpr.EvalItem(item)
if err != nil {
return err
}
if err := attr.SetEvalItem(item, newValue); err != nil {
return err
}
set.SetDirty(idx, true)
return nil
}); err != nil {
return err
}
set.RefreshColumns()
return nil
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
},
}
}
func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryExpr) tea.Msg { func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
return events.PromptForInputMsg{ return events.PromptForInputMsg{
Prompt: "number value: ", Prompt: "number value: ",
@ -239,12 +274,17 @@ func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg {
} }
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
err := path.DeleteAttribute(set.Items()[idx]) if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
if err != nil { if err := path.DeleteAttribute(set.Items()[idx]); err != nil {
return err
}
set.SetDirty(idx, true)
return nil
}); err != nil {
return err return err
} }
set.SetDirty(idx, true)
set.RefreshColumns() set.RefreshColumns()
return nil return nil
}); err != nil { }); err != nil {

View file

@ -8,4 +8,6 @@ const (
NumberItemType ItemType = "N" NumberItemType ItemType = "N"
BoolItemType ItemType = "BOOL" BoolItemType ItemType = "BOOL"
NullItemType ItemType = "NULL" NullItemType ItemType = "NULL"
ExprValueItemType ItemType = "exprvalue"
) )

View file

@ -4,17 +4,23 @@ import (
"github.com/alecthomas/participle/v2" "github.com/alecthomas/participle/v2"
"github.com/alecthomas/participle/v2/lexer" "github.com/alecthomas/participle/v2/lexer"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/pkg/errors" "github.com/pkg/errors"
"strconv"
) )
// Modelled on the expression language here // Modelled on the expression language here
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
type astExpr struct { type astExpr struct {
Root *astDisjunction `parser:"@@"` Root *astDisjunction `parser:"@@"`
Options *astOptions `parser:"( 'using' @@ )?"`
}
type astOptions struct {
Scan bool `parser:"@'scan'"`
Index string `parser:" | 'index' '(' @String ')'"`
} }
type astDisjunction struct { type astDisjunction struct {
@ -38,9 +44,15 @@ type astIn struct {
} }
type astComparisonOp struct { type astComparisonOp struct {
Ref *astEqualityOp `parser:"@@"` Ref *astBetweenOp `parser:"@@"`
Op string `parser:"( @('<' | '<=' | '>' | '>=')"` Op string `parser:"( @('<' | '<=' | '>' | '>=')"`
Value *astEqualityOp `parser:"@@ )?"` Value *astBetweenOp `parser:"@@ )?"`
}
type astBetweenOp struct {
Ref *astEqualityOp `parser:"@@"`
From *astEqualityOp `parser:"( 'between' @@ "`
To *astEqualityOp `parser:" 'and' @@ )?"`
} }
type astEqualityOp struct { type astEqualityOp struct {
@ -58,7 +70,6 @@ type astIsOp struct {
type astSubRef struct { type astSubRef struct {
Ref *astFunctionCall `parser:"@@"` Ref *astFunctionCall `parser:"@@"`
SubRefs []*astSubRefType `parser:"@@*"` SubRefs []*astSubRefType `parser:"@@*"`
//Quals []string `parser:"('.' @Ident)*"`
} }
type astSubRefType struct { type astSubRefType struct {
@ -121,7 +132,58 @@ func Parse(expr string) (*QueryExpr, error) {
return &QueryExpr{ast: ast}, nil return &QueryExpr{ast: ast}, nil
} }
func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.QueryExecutionPlan, error) { func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo, preferredIndex string) (*models.QueryExecutionPlan, error) {
plans, err := a.determinePlausibleExecutionPlans(ctx, info)
if err != nil {
return nil, err
}
scanPlan, _ := sliceutils.FindLast(plans, func(p *models.QueryExecutionPlan) bool {
return !p.CanQuery
})
queryPlans := sliceutils.Filter(plans, func(p *models.QueryExecutionPlan) bool {
if !p.CanQuery {
return false
}
return true
})
if len(queryPlans) == 0 || (a.Options != nil && a.Options.Scan) {
if preferredIndex != "" {
return nil, NoPlausiblePlanWithIndexError{
PreferredIndex: preferredIndex,
PossibleIndices: sliceutils.Map(queryPlans, func(p *models.QueryExecutionPlan) string { return p.IndexName }),
}
}
return scanPlan, nil
}
if preferredIndex == "" && (a.Options != nil && a.Options.Index != "") {
preferredIndex, err = strconv.Unquote(a.Options.Index)
if err != nil {
return nil, err
}
}
if preferredIndex != "" {
queryPlans = sliceutils.Filter(queryPlans, func(p *models.QueryExecutionPlan) bool { return p.IndexName == preferredIndex })
}
if len(queryPlans) == 1 {
return queryPlans[0], nil
} else if len(queryPlans) == 0 {
return nil, NoPlausiblePlanWithIndexError{
PreferredIndex: preferredIndex,
}
}
return nil, MultiplePlansWithIndexError{
PossibleIndices: sliceutils.Map(queryPlans, func(p *models.QueryExecutionPlan) string { return p.IndexName }),
}
}
func (a *astExpr) determinePlausibleExecutionPlans(ctx *evalContext, info *models.TableInfo) ([]*models.QueryExecutionPlan, error) {
plans := make([]*models.QueryExecutionPlan, 0)
type queryTestAttempt struct { type queryTestAttempt struct {
index string index string
keysUnderTest models.KeyAttribute keysUnderTest models.KeyAttribute
@ -153,11 +215,11 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q
return nil, err return nil, err
} }
return &models.QueryExecutionPlan{ plans = append(plans, &models.QueryExecutionPlan{
CanQuery: true, CanQuery: true,
IndexName: attempt.index, IndexName: attempt.index,
Expression: expr, Expression: expr,
}, nil })
} }
} }
@ -174,21 +236,22 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q
return nil, err return nil, err
} }
return &models.QueryExecutionPlan{ plans = append(plans, &models.QueryExecutionPlan{
CanQuery: false, CanQuery: false,
Expression: expr, Expression: expr,
}, nil })
return plans, nil
} }
func (a *astExpr) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) { func (a *astExpr) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) {
return a.Root.evalToIR(ctx, tableInfo) return a.Root.evalToIR(ctx, tableInfo)
} }
func (a *astExpr) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astExpr) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
return a.Root.evalItem(ctx, item) return a.Root.evalItem(ctx, item)
} }
func (a *astExpr) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astExpr) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
return a.Root.setEvalItem(ctx, item, value) return a.Root.setEvalItem(ctx, item, value)
} }

View file

@ -1,7 +1,6 @@
package queryexpr package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -21,15 +20,6 @@ func (a *astAtom) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er
return nil, errors.New("unhandled atom case") return nil, errors.New("unhandled atom case")
} }
func (a *astAtom) rightOperandDynamoValue() (types.AttributeValue, error) {
switch {
case a.Literal != nil:
return a.Literal.dynamoValue()
}
return nil, errors.New("unhandled atom case")
}
func (a *astAtom) unqualifiedName() (string, bool) { func (a *astAtom) unqualifiedName() (string, bool) {
switch { switch {
case a.Ref != nil: case a.Ref != nil:
@ -39,12 +29,12 @@ func (a *astAtom) unqualifiedName() (string, bool) {
return "", false return "", false
} }
func (a *astAtom) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astAtom) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
switch { switch {
case a.Ref != nil: case a.Ref != nil:
return a.Ref.evalItem(ctx, item) return a.Ref.evalItem(ctx, item)
case a.Literal != nil: case a.Literal != nil:
return a.Literal.dynamoValue() return a.Literal.exprValue()
case a.Placeholder != nil: case a.Placeholder != nil:
return a.Placeholder.evalItem(ctx, item) return a.Placeholder.evalItem(ctx, item)
case a.Paren != nil: case a.Paren != nil:
@ -66,7 +56,7 @@ func (a *astAtom) canModifyItem(ctx *evalContext, item models.Item) bool {
return false return false
} }
func (a *astAtom) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astAtom) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
switch { switch {
case a.Ref != nil: case a.Ref != nil:
return a.Ref.setEvalItem(ctx, item, value) return a.Ref.setEvalItem(ctx, item, value)

View file

@ -0,0 +1,155 @@
package queryexpr
import (
"fmt"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/lmika/audax/internal/dynamo-browse/models"
)
func (a *astBetweenOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
leftIR, err := a.Ref.evalToIR(ctx, info)
if err != nil {
return nil, err
}
if a.From == nil {
return leftIR, nil
}
nameIR, isNameIR := leftIR.(nameIRAtom)
if !isNameIR {
return nil, OperandNotANameError(a.Ref.String())
}
fromIR, err := a.From.evalToIR(ctx, info)
if err != nil {
return nil, err
}
toIR, err := a.To.evalToIR(ctx, info)
if err != nil {
return nil, err
}
fromOprIR, isFromOprIR := fromIR.(valueIRAtom)
if !isFromOprIR {
return nil, OperandNotAnOperandError{}
}
toOprIR, isToOprIR := toIR.(valueIRAtom)
if !isToOprIR {
return nil, OperandNotAnOperandError{}
}
return irBetween{name: nameIR, from: fromOprIR, to: toOprIR}, nil
}
func (a *astBetweenOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Ref.evalItem(ctx, item)
if a.From == nil {
return val, err
}
fromIR, err := a.From.evalItem(ctx, item)
if err != nil {
return nil, err
}
toIR, err := a.To.evalItem(ctx, item)
if err != nil {
return nil, err
}
switch v := val.(type) {
case stringableExprValue:
fromNumVal, isFromNumVal := fromIR.(stringableExprValue)
if !isFromNumVal {
return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: fromIR.asAttributeValue()}
}
toNumVal, isToNumVal := toIR.(stringableExprValue)
if !isToNumVal {
return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: toNumVal.asAttributeValue()}
}
return boolExprValue(v.asString() >= fromNumVal.asString() && v.asString() <= toNumVal.asString()), nil
case numberableExprValue:
fromNumVal, isFromNumVal := fromIR.(numberableExprValue)
if !isFromNumVal {
return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: fromIR.asAttributeValue()}
}
toNumVal, isToNumVal := toIR.(numberableExprValue)
if !isToNumVal {
return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: toNumVal.asAttributeValue()}
}
fromCmp := v.asBigFloat().Cmp(fromNumVal.asBigFloat())
toCmp := v.asBigFloat().Cmp(toNumVal.asBigFloat())
return boolExprValue(fromCmp >= 0 && toCmp <= 0), nil
}
return nil, InvalidTypeForBetweenError{TypeName: val.typeName()}
}
func (a *astBetweenOp) canModifyItem(ctx *evalContext, item models.Item) bool {
if a.From != nil {
return false
}
return a.Ref.canModifyItem(ctx, item)
}
func (a *astBetweenOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.From != nil {
return PathNotSettableError{}
}
return a.Ref.setEvalItem(ctx, item, value)
}
func (a *astBetweenOp) deleteAttribute(ctx *evalContext, item models.Item) error {
if a.From != nil {
return PathNotSettableError{}
}
return a.Ref.deleteAttribute(ctx, item)
}
func (a *astBetweenOp) String() string {
name := a.Ref.String()
if a.From != nil {
return fmt.Sprintf("%v between %v and %v", name, a.From.String(), a.To.String())
}
return name
}
type irBetween struct {
name nameIRAtom
from valueIRAtom
to valueIRAtom
}
func (i irBetween) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
nb := i.name.calcName(info)
fb := i.from.calcOperand(info)
tb := i.to.calcOperand(info)
return nb.Between(fb, tb), nil
}
func (i irBetween) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
keyName := i.name.keyName()
if keyName == "" {
return false
}
if keyName == qci.keysUnderTest.SortKey {
return qci.addKey(keyName)
}
return false
}
func (i irBetween) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
nb := i.name.keyName()
fb := i.from.exprValue()
tb := i.to.exprValue()
return expression.Key(nb).Between(buildExpressionFromValue(fb), buildExpressionFromValue(tb)), nil
}

View file

@ -2,7 +2,6 @@ package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"strings" "strings"
) )
@ -20,7 +19,7 @@ func (a *astBooleanNot) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
return &irBoolNot{atom: irNode}, nil return &irBoolNot{atom: irNode}, nil
} }
func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Operand.evalItem(ctx, item) val, err := a.Operand.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -30,7 +29,7 @@ func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.Attr
return val, nil return val, nil
} }
return &types.AttributeValueMemberBOOL{Value: !isAttributeTrue(val)}, nil return boolExprValue(!isAttributeTrue(val)), nil
} }
func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool { func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -40,7 +39,7 @@ func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool {
return a.Operand.canModifyItem(ctx, item) return a.Operand.canModifyItem(ctx, item)
} }
func (a *astBooleanNot) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astBooleanNot) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.HasNot { if a.HasNot {
return PathNotSettableError{} return PathNotSettableError{}
} }

View file

@ -2,38 +2,90 @@ package queryexpr
import ( import (
"context" "context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"strconv"
) )
type nativeFunc func(ctx context.Context, args []types.AttributeValue) (types.AttributeValue, error) type nativeFunc func(ctx context.Context, args []exprValue) (exprValue, error)
var nativeFuncs = map[string]nativeFunc{ var nativeFuncs = map[string]nativeFunc{
"size": func(ctx context.Context, args []types.AttributeValue) (types.AttributeValue, error) { "size": func(ctx context.Context, args []exprValue) (exprValue, error) {
if len(args) != 1 { if len(args) != 1 {
return nil, InvalidArgumentNumberError{Name: "size", Expected: 1, Actual: len(args)} return nil, InvalidArgumentNumberError{Name: "size", Expected: 1, Actual: len(args)}
} }
var l int var l int
switch t := args[0].(type) { switch t := args[0].(type) {
case *types.AttributeValueMemberB: case stringExprValue:
l = len(t.Value) l = len(t)
case *types.AttributeValueMemberS: case mappableExprValue:
l = len(t.Value) l = t.len()
case *types.AttributeValueMemberL: case slicableExprValue:
l = len(t.Value) l = t.len()
case *types.AttributeValueMemberM:
l = len(t.Value)
case *types.AttributeValueMemberSS:
l = len(t.Value)
case *types.AttributeValueMemberNS:
l = len(t.Value)
case *types.AttributeValueMemberBS:
l = len(t.Value)
default: default:
return nil, errors.New("cannot take size of arg") return nil, errors.New("cannot take size of arg")
} }
return &types.AttributeValueMemberN{Value: strconv.Itoa(l)}, nil return int64ExprValue(l), nil
},
"range": func(ctx context.Context, args []exprValue) (exprValue, error) {
if len(args) != 2 {
return nil, InvalidArgumentNumberError{Name: "range", Expected: 2, Actual: len(args)}
}
xVal, isXNum := args[0].(numberableExprValue)
if !isXNum {
return nil, InvalidArgumentTypeError{Name: "range", ArgIndex: 0, Expected: "N"}
}
yVal, isYNum := args[1].(numberableExprValue)
if !isYNum {
return nil, InvalidArgumentTypeError{Name: "range", ArgIndex: 1, Expected: "N"}
}
xInt, _ := xVal.asBigFloat().Int64()
yInt, _ := yVal.asBigFloat().Int64()
xs := make([]exprValue, 0)
for x := xInt; x <= yInt; x++ {
xs = append(xs, int64ExprValue(x))
}
return listExprValue(xs), nil
},
"_x_now": func(ctx context.Context, args []exprValue) (exprValue, error) {
now := timeSourceFromContext(ctx).now().Unix()
return int64ExprValue(now), nil
},
"_x_add": func(ctx context.Context, args []exprValue) (exprValue, error) {
if len(args) != 2 {
return nil, InvalidArgumentNumberError{Name: "_x_add", Expected: 2, Actual: len(args)}
}
xVal, isXNum := args[0].(numberableExprValue)
if !isXNum {
return nil, InvalidArgumentTypeError{Name: "_x_add", ArgIndex: 0, Expected: "N"}
}
yVal, isYNum := args[1].(numberableExprValue)
if !isYNum {
return nil, InvalidArgumentTypeError{Name: "_x_add", ArgIndex: 1, Expected: "N"}
}
return bigNumExprValue{num: xVal.asBigFloat().Add(xVal.asBigFloat(), yVal.asBigFloat())}, nil
},
"_x_concat": func(ctx context.Context, args []exprValue) (exprValue, error) {
if len(args) != 2 {
return nil, InvalidArgumentNumberError{Name: "_x_concat", Expected: 2, Actual: len(args)}
}
xVal, isXNum := args[0].(stringableExprValue)
if !isXNum {
return nil, InvalidArgumentTypeError{Name: "_x_concat", ArgIndex: 0, Expected: "S"}
}
yVal, isYNum := args[1].(stringableExprValue)
if !isYNum {
return nil, InvalidArgumentTypeError{Name: "_x_concat", ArgIndex: 1, Expected: "S"}
}
return stringExprValue(xVal.asString() + yVal.asString()), nil
}, },
} }

View file

@ -2,7 +2,6 @@ package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -47,7 +46,7 @@ func (a *astComparisonOp) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
return irGenericCmp{leftOpr, rightOpr, cmpType}, nil return irGenericCmp{leftOpr, rightOpr, cmpType}, nil
} }
func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
left, err := a.Ref.evalItem(ctx, item) left, err := a.Ref.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -61,20 +60,21 @@ func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.At
return nil, err return nil, err
} }
cmp, isComparable := attrutils.CompareScalarAttributes(left, right) // TODO: use expr value here
cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
if !isComparable { if !isComparable {
return nil, ValuesNotComparable{Left: left, Right: right} return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
} }
switch opToCmdType[a.Op] { switch opToCmdType[a.Op] {
case cmpTypeLt: case cmpTypeLt:
return &types.AttributeValueMemberBOOL{Value: cmp < 0}, nil return boolExprValue(cmp < 0), nil
case cmpTypeLe: case cmpTypeLe:
return &types.AttributeValueMemberBOOL{Value: cmp <= 0}, nil return boolExprValue(cmp <= 0), nil
case cmpTypeGt: case cmpTypeGt:
return &types.AttributeValueMemberBOOL{Value: cmp > 0}, nil return boolExprValue(cmp > 0), nil
case cmpTypeGe: case cmpTypeGe:
return &types.AttributeValueMemberBOOL{Value: cmp >= 0}, nil return boolExprValue(cmp >= 0), nil
} }
return nil, errors.Errorf("unrecognised operator: %v", a.Op) return nil, errors.Errorf("unrecognised operator: %v", a.Op)
} }
@ -86,7 +86,7 @@ func (a *astComparisonOp) canModifyItem(ctx *evalContext, item models.Item) bool
return a.Ref.canModifyItem(ctx, item) return a.Ref.canModifyItem(ctx, item)
} }
func (a *astComparisonOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astComparisonOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.Op != "" { if a.Op != "" {
return PathNotSettableError{} return PathNotSettableError{}
} }
@ -143,34 +143,34 @@ func (a irKeyFieldCmp) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
func (a irKeyFieldCmp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { func (a irKeyFieldCmp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
nb := a.name.calcName(info) nb := a.name.calcName(info)
vb := a.value.goValue() vb := a.value.exprValue()
switch a.cmpType { switch a.cmpType {
case cmpTypeLt: case cmpTypeLt:
return nb.LessThan(expression.Value(vb)), nil return nb.LessThan(buildExpressionFromValue(vb)), nil
case cmpTypeLe: case cmpTypeLe:
return nb.LessThanEqual(expression.Value(vb)), nil return nb.LessThanEqual(buildExpressionFromValue(vb)), nil
case cmpTypeGt: case cmpTypeGt:
return nb.GreaterThan(expression.Value(vb)), nil return nb.GreaterThan(buildExpressionFromValue(vb)), nil
case cmpTypeGe: case cmpTypeGe:
return nb.GreaterThanEqual(expression.Value(vb)), nil return nb.GreaterThanEqual(buildExpressionFromValue(vb)), nil
} }
return expression.ConditionBuilder{}, errors.New("unsupported cmp type") return expression.ConditionBuilder{}, errors.New("unsupported cmp type")
} }
func (a irKeyFieldCmp) calcQueryForQuery() (expression.KeyConditionBuilder, error) { func (a irKeyFieldCmp) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
keyName := a.name.keyName() keyName := a.name.keyName()
vb := a.value.goValue() vb := a.value.exprValue()
switch a.cmpType { switch a.cmpType {
case cmpTypeLt: case cmpTypeLt:
return expression.Key(keyName).LessThan(expression.Value(vb)), nil return expression.Key(keyName).LessThan(buildExpressionFromValue(vb)), nil
case cmpTypeLe: case cmpTypeLe:
return expression.Key(keyName).LessThanEqual(expression.Value(vb)), nil return expression.Key(keyName).LessThanEqual(buildExpressionFromValue(vb)), nil
case cmpTypeGt: case cmpTypeGt:
return expression.Key(keyName).GreaterThan(expression.Value(vb)), nil return expression.Key(keyName).GreaterThan(buildExpressionFromValue(vb)), nil
case cmpTypeGe: case cmpTypeGe:
return expression.Key(keyName).GreaterThanEqual(expression.Value(vb)), nil return expression.Key(keyName).GreaterThanEqual(buildExpressionFromValue(vb)), nil
} }
return expression.KeyConditionBuilder{}, errors.New("unsupported cmp type") return expression.KeyConditionBuilder{}, errors.New("unsupported cmp type")
} }

View file

@ -2,8 +2,8 @@ package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"math/big"
"strings" "strings"
) )
@ -36,7 +36,7 @@ func (a *astConjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
return &irMultiConjunction{atoms: atoms}, nil return &irMultiConjunction{atoms: atoms}, nil
} }
func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Operands[0].evalItem(ctx, item) val, err := a.Operands[0].evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -47,7 +47,7 @@ func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
for _, opr := range a.Operands[1:] { for _, opr := range a.Operands[1:] {
if !isAttributeTrue(val) { if !isAttributeTrue(val) {
return &types.AttributeValueMemberBOOL{Value: false}, nil return boolExprValue(false), nil
} }
val, err = opr.evalItem(ctx, item) val, err = opr.evalItem(ctx, item)
@ -56,7 +56,7 @@ func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
} }
} }
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil return boolExprValue(isAttributeTrue(val)), nil
} }
func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool { func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -67,7 +67,7 @@ func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool
return false return false
} }
func (a *astConjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astConjunction) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if len(a.Operands) == 1 { if len(a.Operands) == 1 {
return a.Operands[0].setEvalItem(ctx, item, value) return a.Operands[0].setEvalItem(ctx, item, value)
} }
@ -168,16 +168,16 @@ func (d *irMultiConjunction) calcQueryForScan(info *models.TableInfo) (expressio
return conjExpr, nil return conjExpr, nil
} }
func isAttributeTrue(attr types.AttributeValue) bool { func isAttributeTrue(attr exprValue) bool {
switch val := attr.(type) { switch val := attr.(type) {
case *types.AttributeValueMemberS: case nullExprValue:
return val.Value != ""
case *types.AttributeValueMemberN:
return val.Value != "0"
case *types.AttributeValueMemberBOOL:
return val.Value
case *types.AttributeValueMemberNULL:
return false return false
case boolExprValue:
return bool(val)
case stringableExprValue:
return val.asString() != ""
case numberableExprValue:
return val.asBigFloat().Cmp(&big.Float{}) != 0
} }
return true return true
} }

View file

@ -0,0 +1,27 @@
package queryexpr
import (
"context"
"time"
)
type timeSource interface {
now() time.Time
}
type defaultTimeSource struct{}
func (tds defaultTimeSource) now() time.Time {
return time.Now()
}
type timeSourceContextKeyType struct{}
var timeSourceContextKey = timeSourceContextKeyType{}
func timeSourceFromContext(ctx context.Context) timeSource {
if tts, ok := ctx.Value(timeSourceContextKey).(timeSource); ok {
return tts
}
return defaultTimeSource{}
}

View file

@ -2,7 +2,6 @@ package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"strings" "strings"
) )
@ -24,7 +23,7 @@ func (a *astDisjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
return &irDisjunction{conj: conj}, nil return &irDisjunction{conj: conj}, nil
} }
func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Operands[0].evalItem(ctx, item) val, err := a.Operands[0].evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -35,7 +34,7 @@ func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
for _, opr := range a.Operands[1:] { for _, opr := range a.Operands[1:] {
if isAttributeTrue(val) { if isAttributeTrue(val) {
return &types.AttributeValueMemberBOOL{Value: true}, nil return boolExprValue(true), nil
} }
val, err = opr.evalItem(ctx, item) val, err = opr.evalItem(ctx, item)
@ -44,7 +43,7 @@ func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
} }
} }
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil return boolExprValue(isAttributeTrue(val)), nil
} }
func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool { func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -55,7 +54,7 @@ func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool
return false return false
} }
func (a *astDisjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astDisjunction) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if len(a.Operands) == 1 { if len(a.Operands) == 1 {
return a.Operands[0].setEvalItem(ctx, item, value) return a.Operands[0].setEvalItem(ctx, item, value)
} }

View file

@ -3,7 +3,6 @@ package queryexpr
import ( import (
"fmt" "fmt"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"strings" "strings"
) )
@ -16,21 +15,21 @@ func (dt *astRef) unqualifiedName() (string, bool) {
return dt.Name, true return dt.Name, true
} }
func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
res, hasV := item[dt.Name] res, hasV := item[dt.Name]
if !hasV { if !hasV {
return nil, nil return nil, nil
} }
return res, nil return newExprValueFromAttributeValue(res)
} }
func (dt *astRef) canModifyItem(ctx *evalContext, item models.Item) bool { func (dt *astRef) canModifyItem(ctx *evalContext, item models.Item) bool {
return true return true
} }
func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
item[dt.Name] = value item[dt.Name] = value.asAttributeValue()
return nil return nil
} }
@ -71,7 +70,7 @@ func (i irNamePath) calcName(info *models.TableInfo) expression.NameBuilder {
switch v := qual.(type) { switch v := qual.(type) {
case string: case string:
fullName.WriteString("." + v) fullName.WriteString("." + v)
case int: case int64:
fullName.WriteString(fmt.Sprintf("[%v]", qual)) fullName.WriteString(fmt.Sprintf("[%v]", qual))
} }
} }

View file

@ -2,7 +2,6 @@ package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -59,7 +58,7 @@ func (a *astEqualityOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAt
return nil, errors.Errorf("unrecognised operator: %v", a.Op) return nil, errors.Errorf("unrecognised operator: %v", a.Op)
} }
func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
left, err := a.Ref.evalItem(ctx, item) left, err := a.Ref.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -74,30 +73,32 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.Attr
return nil, err return nil, err
} }
// TODO: use expr values here
switch a.Op { switch a.Op {
case "=": case "=":
cmp, isComparable := attrutils.CompareScalarAttributes(left, right) cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
if !isComparable { if !isComparable {
return nil, ValuesNotComparable{Left: left, Right: right} return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
} }
return &types.AttributeValueMemberBOOL{Value: cmp == 0}, nil return boolExprValue(cmp == 0), nil
case "!=": case "!=":
cmp, isComparable := attrutils.CompareScalarAttributes(left, right) cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
if !isComparable { if !isComparable {
return nil, ValuesNotComparable{Left: left, Right: right} return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
} }
return &types.AttributeValueMemberBOOL{Value: cmp != 0}, nil return boolExprValue(cmp != 0), nil
case "^=": case "^=":
strValue, isStrValue := right.(*types.AttributeValueMemberS) strValue, isStrValue := right.(stringableExprValue)
if !isStrValue { if !isStrValue {
return nil, errors.New("operand '^=' must be string") return nil, errors.New("operand '^=' must be string")
} }
leftAsStr, canBeString := attrutils.AttributeToString(left) leftAsStr, canBeString := left.(stringableExprValue)
if !canBeString { if !canBeString {
return nil, ValueNotConvertableToString{Val: left} return nil, ValueNotConvertableToString{Val: leftAsStr.asAttributeValue()}
} }
return &types.AttributeValueMemberBOOL{Value: strings.HasPrefix(leftAsStr, strValue.Value)}, nil return boolExprValue(strings.HasPrefix(leftAsStr.asString(), strValue.asString())), nil
} }
return nil, errors.Errorf("unrecognised operator: %v", a.Op) return nil, errors.Errorf("unrecognised operator: %v", a.Op)
@ -110,7 +111,7 @@ func (a *astEqualityOp) canModifyItem(ctx *evalContext, item models.Item) bool {
return a.Ref.canModifyItem(ctx, item) return a.Ref.canModifyItem(ctx, item)
} }
func (a *astEqualityOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astEqualityOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.Op != "" { if a.Op != "" {
return PathNotSettableError{} return PathNotSettableError{}
} }
@ -157,8 +158,8 @@ func (a irKeyFieldEq) calcQueryForScan(info *models.TableInfo) (expression.Condi
} }
func (a irKeyFieldEq) calcQueryForQuery() (expression.KeyConditionBuilder, error) { func (a irKeyFieldEq) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
vb := a.value.goValue() vb := a.value.exprValue()
return expression.Key(a.name.keyName()).Equal(expression.Value(vb)), nil return expression.Key(a.name.keyName()).Equal(buildExpressionFromValue(vb)), nil
} }
type irGenericEq struct { type irGenericEq struct {
@ -203,21 +204,21 @@ func (a irFieldBeginsWith) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
func (a irFieldBeginsWith) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { func (a irFieldBeginsWith) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
nb := a.name.calcName(info) nb := a.name.calcName(info)
vb := a.value.goValue() vb := a.value.exprValue()
strValue, isStrValue := vb.(string) strValue, isStrValue := vb.(stringableExprValue)
if !isStrValue { if !isStrValue {
return expression.ConditionBuilder{}, errors.New("operand '^=' must be string") return expression.ConditionBuilder{}, errors.New("operand '^=' must be string")
} }
return nb.BeginsWith(strValue), nil return nb.BeginsWith(strValue.asString()), nil
} }
func (a irFieldBeginsWith) calcQueryForQuery() (expression.KeyConditionBuilder, error) { func (a irFieldBeginsWith) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
vb := a.value.goValue() vb := a.value.exprValue()
strValue, isStrValue := vb.(string) strValue, isStrValue := vb.(stringableExprValue)
if !isStrValue { if !isStrValue {
return expression.KeyConditionBuilder{}, errors.New("operand '^=' must be string") return expression.KeyConditionBuilder{}, errors.New("operand '^=' must be string")
} }
return expression.Key(a.name.keyName()).BeginsWith(strValue), nil return expression.Key(a.name.keyName()).BeginsWith(strValue.asString()), nil
} }

View file

@ -98,6 +98,14 @@ func (n InvalidTypeForIsError) Error() string {
return "invalid type for 'is': " + n.TypeName return "invalid type for 'is': " + n.TypeName
} }
type InvalidTypeForBetweenError struct {
TypeName string
}
func (n InvalidTypeForBetweenError) Error() string {
return "invalid type for 'between': " + n.TypeName
}
type InvalidArgumentNumberError struct { type InvalidArgumentNumberError struct {
Name string Name string
Expected int Expected int
@ -108,6 +116,16 @@ func (e InvalidArgumentNumberError) Error() string {
return fmt.Sprintf("function '%v' expected %v args but received %v", e.Name, e.Expected, e.Actual) return fmt.Sprintf("function '%v' expected %v args but received %v", e.Name, e.Expected, e.Actual)
} }
type InvalidArgumentTypeError struct {
Name string
ArgIndex int
Expected string
}
func (e InvalidArgumentTypeError) Error() string {
return fmt.Sprintf("function '%v' expected arg %v to be of type %v", e.Name, e.ArgIndex, e.Expected)
}
type UnrecognisedFunctionError struct { type UnrecognisedFunctionError struct {
Name string Name string
} }
@ -137,3 +155,20 @@ type ValueNotUsableAsASubref struct {
func (e ValueNotUsableAsASubref) Error() string { func (e ValueNotUsableAsASubref) Error() string {
return "value cannot be used as a subref" return "value cannot be used as a subref"
} }
type MultiplePlansWithIndexError struct {
PossibleIndices []string
}
func (e MultiplePlansWithIndexError) Error() string {
return fmt.Sprintf("multiple plans with index found. Specify index or scan with 'using' clause: possible indices are %v", e.PossibleIndices)
}
type NoPlausiblePlanWithIndexError struct {
PreferredIndex string
PossibleIndices []string
}
func (e NoPlausiblePlanWithIndexError) Error() string {
return fmt.Sprintf("no plan with index '%v' found: possible indices are %v", e.PreferredIndex, e.PossibleIndices)
}

View file

@ -16,12 +16,17 @@ import (
type QueryExpr struct { type QueryExpr struct {
ast *astExpr ast *astExpr
index string
names map[string]string names map[string]string
values map[string]types.AttributeValue values map[string]types.AttributeValue
// tests fields only
timeSource timeSource
} }
type serializedExpr struct { type serializedExpr struct {
Expr string Expr string
Index string
Names map[string]string Names map[string]string
Values []byte Values []byte
} }
@ -39,6 +44,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) {
} }
qe.names = se.Names qe.names = se.Names
qe.index = se.Index
if len(se.Values) > 0 { if len(se.Values) > 0 {
vals, err := attrcodec.NewDecoder(bytes.NewReader(se.Values)).Decode() vals, err := attrcodec.NewDecoder(bytes.NewReader(se.Values)).Decode()
@ -56,7 +62,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) {
} }
func (md *QueryExpr) SerializeTo(w io.Writer) error { func (md *QueryExpr) SerializeTo(w io.Writer) error {
se := serializedExpr{Expr: md.String(), Names: md.names} se := serializedExpr{Expr: md.String(), Index: md.index, Names: md.names}
if md.values != nil { if md.values != nil {
var bts bytes.Buffer var bts bytes.Buffer
if err := attrcodec.NewEncoder(&bts).Encode(&types.AttributeValueMemberM{Value: md.values}); err != nil { if err := attrcodec.NewEncoder(&bts).Encode(&types.AttributeValueMemberM{Value: md.values}); err != nil {
@ -90,6 +96,7 @@ func (md *QueryExpr) Equal(other *QueryExpr) bool {
} }
return md.ast.String() == other.ast.String() && return md.ast.String() == other.ast.String() &&
md.index == other.index &&
maps.Equal(md.names, other.names) && maps.Equal(md.names, other.names) &&
maps.EqualFunc(md.values, md.values, attrutils.Equals) maps.EqualFunc(md.values, md.values, attrutils.Equals)
} }
@ -104,6 +111,7 @@ func (md *QueryExpr) HashCode() uint64 {
h := fnv.New64a() h := fnv.New64a()
h.Write([]byte(md.ast.String())) h.Write([]byte(md.ast.String()))
h.Write([]byte(md.index))
// the names must be in sorted order to maintain consistant key ordering // the names must be in sorted order to maintain consistant key ordering
if len(md.names) > 0 { if len(md.names) > 0 {
@ -134,6 +142,7 @@ func (md *QueryExpr) HashCode() uint64 {
func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr { func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr {
return &QueryExpr{ return &QueryExpr{
ast: md.ast, ast: md.ast,
index: md.index,
names: value, names: value,
values: md.values, values: md.values,
} }
@ -158,17 +167,34 @@ func (md *QueryExpr) ValueParamOrNil(name string) types.AttributeValue {
func (md *QueryExpr) WithValueParams(value map[string]types.AttributeValue) *QueryExpr { func (md *QueryExpr) WithValueParams(value map[string]types.AttributeValue) *QueryExpr {
return &QueryExpr{ return &QueryExpr{
ast: md.ast, ast: md.ast,
index: md.index,
names: md.names, names: md.names,
values: value, values: value,
} }
} }
func (md *QueryExpr) WithIndex(index string) *QueryExpr {
return &QueryExpr{
ast: md.ast,
index: index,
names: md.names,
values: md.values,
}
}
func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) { func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) {
return md.ast.calcQuery(md.evalContext(), tableInfo) return md.ast.calcQuery(md.evalContext(), tableInfo, md.index)
} }
func (md *QueryExpr) EvalItem(item models.Item) (types.AttributeValue, error) { func (md *QueryExpr) EvalItem(item models.Item) (types.AttributeValue, error) {
return md.ast.evalItem(md.evalContext(), item) val, err := md.ast.evalItem(md.evalContext(), item)
if err != nil {
return nil, err
}
if val == nil {
return nil, nil
}
return val.asAttributeValue(), nil
} }
func (md *QueryExpr) DeleteAttribute(item models.Item) error { func (md *QueryExpr) DeleteAttribute(item models.Item) error {
@ -176,7 +202,11 @@ func (md *QueryExpr) DeleteAttribute(item models.Item) error {
} }
func (md *QueryExpr) SetEvalItem(item models.Item, newValue types.AttributeValue) error { func (md *QueryExpr) SetEvalItem(item models.Item, newValue types.AttributeValue) error {
return md.ast.setEvalItem(md.evalContext(), item, newValue) val, err := newExprValueFromAttributeValue(newValue)
if err != nil {
return err
}
return md.ast.setEvalItem(md.evalContext(), item, val)
} }
func (md *QueryExpr) IsModifiablePath(item models.Item) bool { func (md *QueryExpr) IsModifiablePath(item models.Item) bool {
@ -237,6 +267,7 @@ type evalContext struct {
nameLookup func(string) (string, bool) nameLookup func(string) (string, bool)
valuePlaceholders map[string]types.AttributeValue valuePlaceholders map[string]types.AttributeValue
valueLookup func(string) (types.AttributeValue, bool) valueLookup func(string) (types.AttributeValue, bool)
timeSource timeSource
} }
func (ec *evalContext) lookupName(name string) (string, bool) { func (ec *evalContext) lookupName(name string) (string, bool) {
@ -264,3 +295,10 @@ func (ec *evalContext) lookupValue(name string) (types.AttributeValue, bool) {
return nil, false return nil, false
} }
func (ec *evalContext) getTimeSource() timeSource {
if ts := ec.timeSource; ts != nil {
return ts
}
return defaultTimeSource{}
}

View file

@ -7,6 +7,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"testing" "testing"
"time"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -34,6 +35,13 @@ func TestModExpr_Query(t *testing.T) {
SortKey: "sk", SortKey: "sk",
}, },
}, },
{
Name: "with-apples-and-oranges",
Keys: models.KeyAttribute{
PartitionKey: "apples",
SortKey: "oranges",
},
},
}, },
} }
@ -112,6 +120,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "pk", "prefix"), exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "sk", "100"), 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", scanCase("with placeholders",
`:partition=$valuePrefix and :sort=$valueAnother`, `:partition=$valuePrefix and :sort=$valueAnother`,
@ -149,6 +164,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "color", "yellow"), exprNameIsString(0, 0, "color", "yellow"),
exprNameIsString(1, 1, "shade", "dark"), 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 { for _, scenario := range scenarios {
@ -238,6 +260,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "pk", "prefix"), 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")`, scanCase("with in", `pk in ("alpha", "bravo", "charlie")`,
`#0 IN (:0, :1, :2)`, `#0 IN (:0, :1, :2)`,
exprName(0, "pk"), exprName(0, "pk"),
@ -374,6 +403,53 @@ func TestModExpr_Query(t *testing.T) {
}) })
} }
}) })
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) { func TestQueryExpr_EvalItem(t *testing.T) {
@ -395,7 +471,9 @@ func TestQueryExpr_EvalItem(t *testing.T) {
&types.AttributeValueMemberN{Value: "7"}, &types.AttributeValueMemberN{Value: "7"},
}, },
}, },
"one": &types.AttributeValueMemberN{Value: "1"},
"three": &types.AttributeValueMemberN{Value: "3"}, "three": &types.AttributeValueMemberN{Value: "3"},
"five": &types.AttributeValueMemberN{Value: "5"},
} }
) )
@ -433,6 +511,19 @@ func TestQueryExpr_EvalItem(t *testing.T) {
{expr: "three < 2", expected: &types.AttributeValueMemberBOOL{Value: false}}, {expr: "three < 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "three <= 2", expected: &types.AttributeValueMemberBOOL{Value: false}}, {expr: "three <= 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
// Between
{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 // In
{expr: "three in (2, 3, 4, 5)", expected: &types.AttributeValueMemberBOOL{Value: true}}, {expr: "three in (2, 3, 4, 5)", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three in (20, 30, 40)", expected: &types.AttributeValueMemberBOOL{Value: false}}, {expr: "three in (20, 30, 40)", expected: &types.AttributeValueMemberBOOL{Value: false}},
@ -509,6 +600,29 @@ func TestQueryExpr_EvalItem(t *testing.T) {
} }
}) })
t.Run("functions", func(t *testing.T) {
timeNow := time.Now()
scenarios := []struct {
expr string
expected types.AttributeValue
}{
// _x_now() -- unreleased version of now
{expr: `_x_now()`, expected: &types.AttributeValueMemberN{Value: fmt.Sprint(timeNow.Unix())}},
}
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).EvalItem(item)
assert.NoError(t, err)
assert.Equal(t, scenario.expected, res)
})
}
})
t.Run("unparsed expression", func(t *testing.T) { t.Run("unparsed expression", func(t *testing.T) {
scenarios := []struct { scenarios := []struct {
expr string expr string

View file

@ -3,7 +3,6 @@ package queryexpr
import ( import (
"context" "context"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -29,7 +28,7 @@ func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
return nil, err return nil, err
} }
// TODO: do this properly // Special handling of functions that have IR nodes
switch nameIr.keyName() { switch nameIr.keyName() {
case "size": case "size":
if len(irNodes) != 1 { if len(irNodes) != 1 {
@ -40,20 +39,34 @@ func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
return nil, OperandNotANameError(a.Args[0].String()) return nil, OperandNotANameError(a.Args[0].String())
} }
return irSizeFn{name}, nil return irSizeFn{name}, nil
case "range":
if len(irNodes) != 2 {
return nil, InvalidArgumentNumberError{Name: "range", Expected: 2, Actual: len(irNodes)}
}
// TEMP
fromVal := irNodes[0].(valueIRAtom).goValue().(int64)
toVal := irNodes[1].(valueIRAtom).goValue().(int64)
return irRangeFn{fromVal, toVal}, nil
} }
return nil, UnrecognisedFunctionError{Name: nameIr.keyName()}
builtinFn, hasBuiltin := nativeFuncs[nameIr.keyName()]
if !hasBuiltin {
return nil, UnrecognisedFunctionError{Name: nameIr.keyName()}
}
// Normal functions which are evaluated to regular values
irValues, err := sliceutils.MapWithError(irNodes, func(a irAtom) (exprValue, error) {
v, isV := a.(valueIRAtom)
if !isV {
return nil, errors.New("cannot use value")
}
return v.exprValue(), nil
})
if err != nil {
return nil, err
}
val, err := builtinFn(context.Background(), irValues)
if err != nil {
return nil, err
}
return irValue{value: val}, nil
} }
func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
if !a.IsCall { if !a.IsCall {
return a.Caller.evalItem(ctx, item) return a.Caller.evalItem(ctx, item)
} }
@ -67,14 +80,15 @@ func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (types.At
return nil, UnrecognisedFunctionError{Name: name} return nil, UnrecognisedFunctionError{Name: name}
} }
args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (types.AttributeValue, error) { args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (exprValue, error) {
return a.evalItem(ctx, item) return a.evalItem(ctx, item)
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
return fn(context.Background(), args) cCtx := context.WithValue(context.Background(), timeSourceContextKey, ctx.timeSource)
return fn(cCtx, args)
} }
func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool { func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -85,7 +99,7 @@ func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool
return a.Caller.canModifyItem(ctx, item) return a.Caller.canModifyItem(ctx, item)
} }
func (a *astFunctionCall) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astFunctionCall) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
// TODO: Should a function vall return an item? // TODO: Should a function vall return an item?
if a.IsCall { if a.IsCall {
return PathNotSettableError{} return PathNotSettableError{}
@ -147,3 +161,15 @@ func (i irRangeFn) calcGoValues(info *models.TableInfo) ([]any, error) {
} }
return xs, nil return xs, nil
} }
type multiValueFnResult struct {
items []any
}
func (i multiValueFnResult) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
return expression.ConditionBuilder{}, errors.New("cannot run as scan")
}
func (i multiValueFnResult) calcGoValues(info *models.TableInfo) ([]any, error) {
return i.items, nil
}

View file

@ -0,0 +1,16 @@
package queryexpr
import (
"time"
)
type testTimeSource time.Time
func (tds testTimeSource) now() time.Time {
return time.Time(tds)
}
func (a *QueryExpr) WithTestTimeSource(timeNow time.Time) *QueryExpr {
a.timeSource = testTimeSource(timeNow)
return a
}

View file

@ -1,10 +1,7 @@
package queryexpr package queryexpr
import ( import (
"bytes"
"fmt"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
@ -71,6 +68,13 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
return nil, OperandNotANameError(a.Ref.String()) return nil, OperandNotANameError(a.Ref.String())
} }
ir = irContains{needle: lit, haystack: t} ir = irContains{needle: lit, haystack: t}
case valueIRAtom:
nameIR, isNameIR := leftIR.(irNamePath)
if !isNameIR {
return nil, OperandNotANameError(a.Ref.String())
}
ir = irLiteralValues{name: nameIR, values: t}
case oprIRAtom: case oprIRAtom:
nameIR, isNameIR := leftIR.(irNamePath) nameIR, isNameIR := leftIR.(irNamePath)
if !isNameIR { if !isNameIR {
@ -78,13 +82,6 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
} }
ir = irIn{name: nameIR, values: []oprIRAtom{t}} ir = irIn{name: nameIR, values: []oprIRAtom{t}}
case multiValueIRAtom:
nameIR, isNameIR := leftIR.(irNamePath)
if !isNameIR {
return nil, OperandNotANameError(a.Ref.String())
}
ir = irLiteralValues{name: nameIR, values: t}
default: default:
return nil, OperandNotAnOperandError{} return nil, OperandNotAnOperandError{}
} }
@ -96,7 +93,7 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
return ir, nil return ir, nil
} }
func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astIn) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Ref.evalItem(ctx, item) val, err := a.Ref.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -112,14 +109,15 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal
if err != nil { if err != nil {
return nil, err return nil, err
} }
cmp, isComparable := attrutils.CompareScalarAttributes(val, evalOp) // TODO: use native types here
cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), evalOp.asAttributeValue())
if !isComparable { if !isComparable {
continue continue
} else if cmp == 0 { } else if cmp == 0 {
return &types.AttributeValueMemberBOOL{Value: true}, nil return boolExprValue(true), nil
} }
} }
return &types.AttributeValueMemberBOOL{Value: false}, nil return boolExprValue(false), nil
case a.SingleOperand != nil: case a.SingleOperand != nil:
evalOp, err := a.SingleOperand.evalItem(ctx, item) evalOp, err := a.SingleOperand.evalItem(ctx, item)
if err != nil { if err != nil {
@ -127,69 +125,38 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal
} }
switch t := evalOp.(type) { switch t := evalOp.(type) {
case *types.AttributeValueMemberS: case stringableExprValue:
str, canToStr := attrutils.AttributeToString(val) str, canToStr := val.(stringableExprValue)
if !canToStr { if !canToStr {
return &types.AttributeValueMemberBOOL{Value: false}, nil return boolExprValue(false), nil
} }
return &types.AttributeValueMemberBOOL{Value: strings.Contains(t.Value, str)}, nil return boolExprValue(strings.Contains(t.asString(), str.asString())), nil
case *types.AttributeValueMemberL: case slicableExprValue:
for _, listItem := range t.Value { for i := 0; i < t.len(); i++ {
cmp, isComparable := attrutils.CompareScalarAttributes(val, listItem) va, err := t.valueAt(i)
if err != nil {
return nil, err
}
// TODO: use expr value types here
cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), va.asAttributeValue())
if !isComparable { if !isComparable {
continue continue
} else if cmp == 0 { } else if cmp == 0 {
return &types.AttributeValueMemberBOOL{Value: true}, nil return boolExprValue(true), nil
} }
} }
return &types.AttributeValueMemberBOOL{Value: false}, nil return boolExprValue(false), nil
case *types.AttributeValueMemberSS: case mappableExprValue:
str, canToStr := attrutils.AttributeToString(val) str, canToStr := val.(stringableExprValue)
if !canToStr { if !canToStr {
return &types.AttributeValueMemberBOOL{Value: false}, nil return boolExprValue(false), nil
} }
hasKey := t.hasKey(str.asString())
for _, listItem := range t.Value { return boolExprValue(hasKey), nil
if str != listItem {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
}
return &types.AttributeValueMemberBOOL{Value: true}, nil
case *types.AttributeValueMemberBS:
b, isB := val.(*types.AttributeValueMemberB)
if !isB {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
for _, listItem := range t.Value {
if !bytes.Equal(b.Value, listItem) {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
}
return &types.AttributeValueMemberBOOL{Value: true}, nil
case *types.AttributeValueMemberNS:
n, isN := val.(*types.AttributeValueMemberN)
if !isN {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
for _, listItem := range t.Value {
// TODO: this is not actually right
if n.Value != listItem {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
}
return &types.AttributeValueMemberBOOL{Value: true}, nil
case *types.AttributeValueMemberM:
str, canToStr := attrutils.AttributeToString(val)
if !canToStr {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
_, hasItem := t.Value[str]
return &types.AttributeValueMemberBOOL{Value: hasItem}, nil
} }
return nil, ValuesNotInnableError{Val: evalOp} return nil, ValuesNotInnableError{Val: evalOp.asAttributeValue()}
} }
return nil, errors.New("internal error: unhandled 'in' case") return nil, errors.New("internal error: unhandled 'in' case")
} }
@ -201,7 +168,7 @@ func (a *astIn) canModifyItem(ctx *evalContext, item models.Item) bool {
return a.Ref.canModifyItem(ctx, item) return a.Ref.canModifyItem(ctx, item)
} }
func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if len(a.Operand) != 0 || a.SingleOperand != nil { if len(a.Operand) != 0 || a.SingleOperand != nil {
return PathNotSettableError{} return PathNotSettableError{}
} }
@ -263,19 +230,38 @@ func (i irIn) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuil
type irLiteralValues struct { type irLiteralValues struct {
name nameIRAtom name nameIRAtom
values multiValueIRAtom values valueIRAtom
} }
func (i irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { func (iv irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
vals, err := i.values.calcGoValues(info) if sliceable, isSliceable := iv.values.exprValue().(slicableExprValue); isSliceable {
if err != nil { if sliceable.len() == 1 {
return expression.ConditionBuilder{}, err va, err := sliceable.valueAt(0)
if err != nil {
return expression.ConditionBuilder{}, err
}
return iv.name.calcName(info).In(buildExpressionFromValue(va)), nil
} else if sliceable.len() == 0 {
// name is not in an empty slice, so this branch always evaluates to false
// TODO: would be better to not even include this branch in some way?
return expression.Equal(expression.Value(false), expression.Value(true)), nil
}
items := make([]expression.OperandBuilder, sliceable.len())
for i := 0; i < sliceable.len(); i++ {
va, err := sliceable.valueAt(i)
if err != nil {
return expression.ConditionBuilder{}, err
}
items[i] = buildExpressionFromValue(va)
}
return iv.name.calcName(info).In(items[0], items[1:]...), nil
} }
oprValues := sliceutils.Map(vals, func(t any) expression.OperandBuilder { return iv.name.calcName(info).In(buildExpressionFromValue(iv.values.exprValue())), nil
return expression.Value(t)
})
return i.name.calcName(info).In(oprValues[0], oprValues[1:]...), nil
} }
type irContains struct { type irContains struct {
@ -284,8 +270,11 @@ type irContains struct {
} }
func (i irContains) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { func (i irContains) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
needle := i.needle.goValue() strNeedle, isString := i.needle.exprValue().(stringableExprValue)
haystack := i.haystack.calcName(info) if !isString {
return expression.ConditionBuilder{}, errors.New("value cannot be converted to string")
}
return haystack.Contains(fmt.Sprint(needle)), nil haystack := i.haystack.calcName(info)
return haystack.Contains(strNeedle.asString()), nil
} }

View file

@ -36,11 +36,7 @@ type nameIRAtom interface {
type valueIRAtom interface { type valueIRAtom interface {
oprIRAtom oprIRAtom
goValue() any exprValue() exprValue
}
type multiValueIRAtom interface {
calcGoValues(info *models.TableInfo) ([]any, error)
} }
func canExecuteAsQuery(ir irAtom, qci *queryCalcInfo) bool { func canExecuteAsQuery(ir irAtom, qci *queryCalcInfo) bool {

View file

@ -2,9 +2,7 @@ package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"reflect" "reflect"
"strings" "strings"
) )
@ -12,50 +10,50 @@ import (
type isTypeInfo struct { type isTypeInfo struct {
isAny bool isAny bool
attributeType expression.DynamoDBAttributeType attributeType expression.DynamoDBAttributeType
goType reflect.Type goTypes []reflect.Type
} }
var validIsTypeNames = map[string]isTypeInfo{ var validIsTypeNames = map[string]isTypeInfo{
"ANY": {isAny: true}, "ANY": {isAny: true},
"B": { "B": {
attributeType: expression.Binary, attributeType: expression.Binary,
goType: reflect.TypeOf(&types.AttributeValueMemberB{}), // TODO
}, },
"BOOL": { "BOOL": {
attributeType: expression.Boolean, attributeType: expression.Boolean,
goType: reflect.TypeOf(&types.AttributeValueMemberBOOL{}), goTypes: []reflect.Type{reflect.TypeOf(boolExprValue(false))},
}, },
"S": { "S": {
attributeType: expression.String, attributeType: expression.String,
goType: reflect.TypeOf(&types.AttributeValueMemberS{}), goTypes: []reflect.Type{reflect.TypeOf(stringExprValue(""))},
}, },
"N": { "N": {
attributeType: expression.Number, attributeType: expression.Number,
goType: reflect.TypeOf(&types.AttributeValueMemberN{}), goTypes: []reflect.Type{reflect.TypeOf(int64ExprValue(0)), reflect.TypeOf(bigNumExprValue{})},
}, },
"NULL": { "NULL": {
attributeType: expression.Null, attributeType: expression.Null,
goType: reflect.TypeOf(&types.AttributeValueMemberNULL{}), goTypes: []reflect.Type{reflect.TypeOf(nullExprValue{})},
}, },
"L": { "L": {
attributeType: expression.List, attributeType: expression.List,
goType: reflect.TypeOf(&types.AttributeValueMemberL{}), goTypes: []reflect.Type{reflect.TypeOf(listExprValue{}), reflect.TypeOf(listProxyValue{})},
}, },
"M": { "M": {
attributeType: expression.Map, attributeType: expression.Map,
goType: reflect.TypeOf(&types.AttributeValueMemberM{}), goTypes: []reflect.Type{reflect.TypeOf(mapExprValue{}), reflect.TypeOf(mapProxyValue{})},
}, },
"BS": { "BS": {
attributeType: expression.BinarySet, attributeType: expression.BinarySet,
goType: reflect.TypeOf(&types.AttributeValueMemberBS{}), // TODO
}, },
"NS": { "NS": {
attributeType: expression.NumberSet, attributeType: expression.NumberSet,
goType: reflect.TypeOf(&types.AttributeValueMemberNS{}), // TODO
}, },
"SS": { "SS": {
attributeType: expression.StringSet, attributeType: expression.StringSet,
goType: reflect.TypeOf(&types.AttributeValueMemberSS{}), // TODO
}, },
} }
@ -83,14 +81,14 @@ func (a *astIsOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er
if !isValueIR { if !isValueIR {
return nil, ValueMustBeLiteralError{} return nil, ValueMustBeLiteralError{}
} }
strValue, isStringValue := valueIR.goValue().(string) strValue, isStringValue := valueIR.exprValue().(stringableExprValue)
if !isStringValue { if !isStringValue {
return nil, ValueMustBeStringError{} return nil, ValueMustBeStringError{}
} }
typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue)] typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue.asString())]
if !isValidType { if !isValidType {
return nil, InvalidTypeForIsError{TypeName: strValue} return nil, InvalidTypeForIsError{TypeName: strValue.asString()}
} }
var ir = irIs{name: nameIR, typeInfo: typeInfo} var ir = irIs{name: nameIR, typeInfo: typeInfo}
@ -104,7 +102,7 @@ func (a *astIsOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er
return ir, nil return ir, nil
} }
func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
ref, err := a.Ref.evalItem(ctx, item) ref, err := a.Ref.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -118,13 +116,13 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeV
if err != nil { if err != nil {
return nil, err return nil, err
} }
str, canToStr := attrutils.AttributeToString(expTypeVal) str, canToStr := expTypeVal.(stringableExprValue)
if !canToStr { if !canToStr {
return nil, ValueMustBeStringError{} return nil, ValueMustBeStringError{}
} }
typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str)] typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str.asString())]
if !hasTypeInfo { if !hasTypeInfo {
return nil, InvalidTypeForIsError{TypeName: str} return nil, InvalidTypeForIsError{TypeName: str.asString()}
} }
var resultOfIs bool var resultOfIs bool
@ -132,12 +130,18 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeV
resultOfIs = ref != nil resultOfIs = ref != nil
} else { } else {
refType := reflect.TypeOf(ref) refType := reflect.TypeOf(ref)
resultOfIs = typeInfo.goType.AssignableTo(refType)
for _, t := range typeInfo.goTypes {
if t.AssignableTo(refType) {
resultOfIs = true
break
}
}
} }
if a.HasNot { if a.HasNot {
resultOfIs = !resultOfIs resultOfIs = !resultOfIs
} }
return &types.AttributeValueMemberBOOL{Value: resultOfIs}, nil return boolExprValue(resultOfIs), nil
} }
func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool { func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -147,7 +151,7 @@ func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool {
return a.Ref.canModifyItem(ctx, item) return a.Ref.canModifyItem(ctx, item)
} }
func (a *astIsOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (a *astIsOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.Value != nil { if a.Value != nil {
return PathNotSettableError{} return PathNotSettableError{}
} }

View file

@ -1,7 +1,6 @@
package queryexpr package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -21,7 +20,12 @@ func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irA
return nil, MissingPlaceholderError{Placeholder: p.Placeholder} return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
} }
return irValue{value: val}, nil ev, err := newExprValueFromAttributeValue(val)
if err != nil {
return nil, err
}
return irValue{value: ev}, nil
} else if placeholderType == namePlaceholderPrefix { } else if placeholderType == namePlaceholderPrefix {
name, hasName := ctx.lookupName(placeholder) name, hasName := ctx.lookupName(placeholder)
if !hasName { if !hasName {
@ -34,7 +38,7 @@ func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irA
return nil, errors.New("unrecognised placeholder") return nil, errors.New("unrecognised placeholder")
} }
func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
placeholderType := p.Placeholder[0] placeholderType := p.Placeholder[0]
placeholder := p.Placeholder[1:] placeholder := p.Placeholder[1:]
@ -43,7 +47,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att
if !hasVal { if !hasVal {
return nil, MissingPlaceholderError{Placeholder: p.Placeholder} return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
} }
return val, nil return newExprValueFromAttributeValue(val)
} else if placeholderType == namePlaceholderPrefix { } else if placeholderType == namePlaceholderPrefix {
name, hasName := ctx.lookupName(placeholder) name, hasName := ctx.lookupName(placeholder)
if !hasName { if !hasName {
@ -55,7 +59,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att
return nil, nil return nil, nil
} }
return res, nil return newExprValueFromAttributeValue(res)
} }
return nil, errors.New("unrecognised placeholder") return nil, errors.New("unrecognised placeholder")
@ -66,7 +70,7 @@ func (p *astPlaceholder) canModifyItem(ctx *evalContext, item models.Item) bool
return placeholderType == namePlaceholderPrefix return placeholderType == namePlaceholderPrefix
} }
func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
placeholderType := p.Placeholder[0] placeholderType := p.Placeholder[0]
placeholder := p.Placeholder[1:] placeholder := p.Placeholder[1:]
@ -78,7 +82,7 @@ func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value t
return MissingPlaceholderError{Placeholder: p.Placeholder} return MissingPlaceholderError{Placeholder: p.Placeholder}
} }
item[name] = value item[name] = value.asAttributeValue()
return nil return nil
} }

View file

@ -1,10 +1,8 @@
package queryexpr package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"strconv"
"strings" "strings"
) )
@ -34,7 +32,7 @@ func (r *astSubRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom,
return irNamePath{name: namePath.name, quals: quals}, nil return irNamePath{name: namePath.name, quals: quals}, nil
} }
func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
res, err := r.Ref.evalItem(ctx, item) res, err := r.Ref.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -48,7 +46,7 @@ func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.Attribut
return res, nil return res, nil
} }
func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.AttributeValue, subRefs []*astSubRefType) (types.AttributeValue, error) { func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res exprValue, subRefs []*astSubRefType) (exprValue, error) {
for i, sr := range subRefs { for i, sr := range subRefs {
sv, err := sr.evalToStrOrInt(ctx, nil) sv, err := sr.evalToStrOrInt(ctx, nil)
if err != nil { if err != nil {
@ -57,24 +55,30 @@ func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.At
switch val := sv.(type) { switch val := sv.(type) {
case string: case string:
var hasV bool mapRes, isMapRes := res.(mappableExprValue)
mapRes, isMapRes := res.(*types.AttributeValueMemberM)
if !isMapRes { if !isMapRes {
return nil, newValueNotAMapError(r, subRefs[:i+1]) return nil, newValueNotAMapError(r, subRefs[:i+1])
} }
res, hasV = mapRes.Value[val] if mapRes.hasKey(val) {
if !hasV { res, err = mapRes.valueOf(val)
return nil, nil if err != nil {
return nil, err
}
} else {
res = nil
} }
case int: case int64:
listRes, isMapRes := res.(*types.AttributeValueMemberL) listRes, isMapRes := res.(slicableExprValue)
if !isMapRes { if !isMapRes {
return nil, newValueNotAListError(r, subRefs[:i+1]) return nil, newValueNotAListError(r, subRefs[:i+1])
} }
// TODO - deal with index properly // TODO - deal with index properly (i.e. error handling)
res = listRes.Value[val] res, err = listRes.valueAt(int(val))
if err != nil {
return nil, err
}
} }
} }
return res, nil return res, nil
@ -84,7 +88,7 @@ func (r *astSubRef) canModifyItem(ctx *evalContext, item models.Item) bool {
return r.Ref.canModifyItem(ctx, item) return r.Ref.canModifyItem(ctx, item)
} }
func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if len(r.SubRefs) == 0 { if len(r.SubRefs) == 0 {
return r.Ref.setEvalItem(ctx, item, value) return r.Ref.setEvalItem(ctx, item, value)
} }
@ -108,20 +112,19 @@ func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value types.
switch val := sv.(type) { switch val := sv.(type) {
case string: case string:
mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM) mapRes, isMapRes := parentItem.(modifiableMapExprValue)
if !isMapRes { if !isMapRes {
return newValueNotAMapError(r, r.SubRefs) return newValueNotAMapError(r, r.SubRefs)
} }
mapRes.Value[val] = value mapRes.setValueOf(val, value)
case int: case int64:
listRes, isMapRes := parentItem.(*types.AttributeValueMemberL) listRes, isMapRes := parentItem.(modifiableSliceExprValue)
if !isMapRes { if !isMapRes {
return newValueNotAListError(r, r.SubRefs) return newValueNotAListError(r, r.SubRefs)
} }
// TODO: handle indexes listRes.setValueAt(int(val), value)
listRes.Value[val] = value
} }
return nil return nil
} }
@ -136,20 +139,6 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error {
return err return err
} }
/*
for i, key := range r.Quals {
mapItem, isMapItem := parentItem.(*types.AttributeValueMemberM)
if !isMapItem {
return PathNotSettableError{}
}
if isLast := i == len(r.Quals)-1; isLast {
delete(mapItem.Value, key)
} else {
parentItem = mapItem.Value[key]
}
}
*/
if len(r.SubRefs) > 1 { if len(r.SubRefs) > 1 {
parentItem, err = r.evalSubRefs(ctx, item, parentItem, r.SubRefs[0:len(r.SubRefs)-1]) parentItem, err = r.evalSubRefs(ctx, item, parentItem, r.SubRefs[0:len(r.SubRefs)-1])
if err != nil { if err != nil {
@ -164,23 +153,20 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error {
switch val := sv.(type) { switch val := sv.(type) {
case string: case string:
mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM) mapRes, isMapRes := parentItem.(modifiableMapExprValue)
if !isMapRes { if !isMapRes {
return newValueNotAMapError(r, r.SubRefs) return newValueNotAMapError(r, r.SubRefs)
} }
delete(mapRes.Value, val) mapRes.deleteValueOf(val)
case int: case int64:
listRes, isMapRes := parentItem.(*types.AttributeValueMemberL) listRes, isMapRes := parentItem.(modifiableSliceExprValue)
if !isMapRes { if !isMapRes {
return newValueNotAListError(r, r.SubRefs) return newValueNotAListError(r, r.SubRefs)
} }
// TODO: handle indexes out of bounds // TODO: handle indexes out of bounds
oldList := listRes.Value listRes.deleteValueAt(int(val))
newList := append([]types.AttributeValue{}, oldList[:val]...)
newList = append(newList, oldList[val+1:]...)
listRes.Value = newList
} }
return nil return nil
} }
@ -214,18 +200,10 @@ func (sr *astSubRefType) evalToStrOrInt(ctx *evalContext, item models.Item) (any
return nil, err return nil, err
} }
switch v := subEvalItem.(type) { switch v := subEvalItem.(type) {
case *types.AttributeValueMemberS: case stringableExprValue:
return v.Value, nil return v.asString(), nil
case *types.AttributeValueMemberN: case numberableExprValue:
intVal, err := strconv.Atoi(v.Value) return v.asInt(), nil
if err == nil {
return intVal, nil
}
flVal, err := strconv.ParseFloat(v.Value, 64)
if err == nil {
return int(flVal), nil
}
return nil, err
} }
return nil, ValueNotUsableAsASubref{} return nil, ValueNotUsableAsASubref{}
} }

View file

@ -1 +1,400 @@
package queryexpr package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/maputils"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/pkg/errors"
"math/big"
"strconv"
)
type exprValue interface {
typeName() string
asGoValue() any
asAttributeValue() types.AttributeValue
}
type stringableExprValue interface {
exprValue
asString() string
}
type numberableExprValue interface {
exprValue
asBigFloat() *big.Float
asInt() int64
}
type slicableExprValue interface {
exprValue
len() int
valueAt(idx int) (exprValue, error)
}
type modifiableSliceExprValue interface {
setValueAt(idx int, value exprValue)
deleteValueAt(idx int)
}
type mappableExprValue interface {
len() int
hasKey(name string) bool
valueOf(name string) (exprValue, error)
}
type modifiableMapExprValue interface {
setValueOf(name string, value exprValue)
deleteValueOf(name string)
}
func buildExpressionFromValue(ev exprValue) expression.ValueBuilder {
return expression.Value(ev)
}
func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error) {
if ev == nil {
return nil, nil
}
switch xVal := ev.(type) {
case *types.AttributeValueMemberS:
return stringExprValue(xVal.Value), nil
case *types.AttributeValueMemberN:
xNumVal, _, err := big.ParseFloat(xVal.Value, 10, 63, big.ToNearestEven)
if err != nil {
return nil, err
}
return bigNumExprValue{num: xNumVal}, nil
case *types.AttributeValueMemberBOOL:
return boolExprValue(xVal.Value), nil
case *types.AttributeValueMemberNULL:
return nullExprValue{}, nil
case *types.AttributeValueMemberL:
return listProxyValue{list: xVal}, nil
case *types.AttributeValueMemberM:
return mapProxyValue{mapValue: xVal}, nil
case *types.AttributeValueMemberSS:
return stringSetProxyValue{stringSet: xVal}, nil
case *types.AttributeValueMemberNS:
return numberSetProxyValue{numberSet: xVal}, nil
}
return nil, errors.New("cannot convert to expr value")
}
type stringExprValue string
func (s stringExprValue) asGoValue() any {
return string(s)
}
func (s stringExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberS{Value: string(s)}
}
func (s stringExprValue) asString() string {
return string(s)
}
func (s stringExprValue) typeName() string {
return "S"
}
type int64ExprValue int64
func (i int64ExprValue) asGoValue() any {
return int(i)
}
func (i int64ExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberN{Value: strconv.Itoa(int(i))}
}
func (i int64ExprValue) asInt() int64 {
return int64(i)
}
func (i int64ExprValue) asBigFloat() *big.Float {
var f big.Float
f.SetInt64(int64(i))
return &f
}
func (s int64ExprValue) typeName() string {
return "N"
}
type bigNumExprValue struct {
num *big.Float
}
func (i bigNumExprValue) asGoValue() any {
return i.num
}
func (i bigNumExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberN{Value: i.num.String()}
}
func (i bigNumExprValue) asInt() int64 {
x, _ := i.num.Int64()
return x
}
func (i bigNumExprValue) asBigFloat() *big.Float {
return i.num
}
func (s bigNumExprValue) typeName() string {
return "N"
}
type boolExprValue bool
func (b boolExprValue) asGoValue() any {
return bool(b)
}
func (b boolExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberBOOL{Value: bool(b)}
}
func (s boolExprValue) typeName() string {
return "BOOL"
}
type nullExprValue struct{}
func (b nullExprValue) asGoValue() any {
return nil
}
func (b nullExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberNULL{Value: true}
}
func (s nullExprValue) typeName() string {
return "NULL"
}
type listExprValue []exprValue
func (bs listExprValue) asGoValue() any {
return sliceutils.Map(bs, func(t exprValue) any {
return t.asGoValue()
})
}
func (bs listExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberL{Value: sliceutils.Map(bs, func(t exprValue) types.AttributeValue {
return t.asAttributeValue()
})}
}
func (bs listExprValue) len() int {
return len(bs)
}
func (bs listExprValue) valueAt(i int) (exprValue, error) {
return bs[i], nil
}
func (s listExprValue) typeName() string {
return "L"
}
type mapExprValue map[string]exprValue
func (bs mapExprValue) asGoValue() any {
return maputils.MapValues(bs, func(t exprValue) any {
return t.asGoValue()
})
}
func (bs mapExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberM{Value: maputils.MapValues(bs, func(t exprValue) types.AttributeValue {
return t.asAttributeValue()
})}
}
func (bs mapExprValue) len() int {
return len(bs)
}
func (bs mapExprValue) hasKey(name string) bool {
_, ok := bs[name]
return ok
}
func (bs mapExprValue) valueOf(name string) (exprValue, error) {
return bs[name], nil
}
func (s mapExprValue) typeName() string {
return "M"
}
type listProxyValue struct {
list *types.AttributeValueMemberL
}
func (bs listProxyValue) asGoValue() any {
resultingList := make([]any, len(bs.list.Value))
for i, item := range bs.list.Value {
if av, _ := newExprValueFromAttributeValue(item); av != nil {
resultingList[i] = av.asGoValue()
} else {
resultingList[i] = nil
}
}
return resultingList
}
func (bs listProxyValue) asAttributeValue() types.AttributeValue {
return bs.list
}
func (bs listProxyValue) len() int {
return len(bs.list.Value)
}
func (bs listProxyValue) valueAt(i int) (exprValue, error) {
return newExprValueFromAttributeValue(bs.list.Value[i])
}
func (bs listProxyValue) setValueAt(i int, newVal exprValue) {
bs.list.Value[i] = newVal.asAttributeValue()
}
func (bs listProxyValue) deleteValueAt(idx int) {
newList := append([]types.AttributeValue{}, bs.list.Value[:idx]...)
newList = append(newList, bs.list.Value[idx+1:]...)
bs.list = &types.AttributeValueMemberL{Value: newList}
}
func (s listProxyValue) typeName() string {
return "L"
}
type mapProxyValue struct {
mapValue *types.AttributeValueMemberM
}
func (bs mapProxyValue) asGoValue() any {
resultingMap := make(map[string]any)
for k, item := range bs.mapValue.Value {
if av, _ := newExprValueFromAttributeValue(item); av != nil {
resultingMap[k] = av.asGoValue()
} else {
resultingMap[k] = nil
}
}
return resultingMap
}
func (bs mapProxyValue) asAttributeValue() types.AttributeValue {
return bs.mapValue
}
func (bs mapProxyValue) len() int {
return len(bs.mapValue.Value)
}
func (bs mapProxyValue) hasKey(name string) bool {
_, ok := bs.mapValue.Value[name]
return ok
}
func (bs mapProxyValue) valueOf(name string) (exprValue, error) {
return newExprValueFromAttributeValue(bs.mapValue.Value[name])
}
func (bs mapProxyValue) setValueOf(name string, newVal exprValue) {
bs.mapValue.Value[name] = newVal.asAttributeValue()
}
func (bs mapProxyValue) deleteValueOf(name string) {
delete(bs.mapValue.Value, name)
}
func (s mapProxyValue) typeName() string {
return "M"
}
type stringSetProxyValue struct {
stringSet *types.AttributeValueMemberSS
}
func (bs stringSetProxyValue) asGoValue() any {
return bs.stringSet.Value
}
func (bs stringSetProxyValue) asAttributeValue() types.AttributeValue {
return bs.stringSet
}
func (bs stringSetProxyValue) len() int {
return len(bs.stringSet.Value)
}
func (bs stringSetProxyValue) valueAt(i int) (exprValue, error) {
return stringExprValue(bs.stringSet.Value[i]), nil
}
func (bs stringSetProxyValue) setValueAt(i int, newVal exprValue) {
if str, isStr := newVal.(stringableExprValue); isStr {
bs.stringSet.Value[i] = str.asString()
}
}
func (bs stringSetProxyValue) deleteValueAt(idx int) {
newList := append([]string{}, bs.stringSet.Value[:idx]...)
newList = append(newList, bs.stringSet.Value[idx+1:]...)
bs.stringSet = &types.AttributeValueMemberSS{Value: newList}
}
func (s stringSetProxyValue) typeName() string {
return "SS"
}
type numberSetProxyValue struct {
numberSet *types.AttributeValueMemberNS
}
func (bs numberSetProxyValue) asGoValue() any {
return bs.numberSet.Value
}
func (bs numberSetProxyValue) asAttributeValue() types.AttributeValue {
return bs.numberSet
}
func (bs numberSetProxyValue) len() int {
return len(bs.numberSet.Value)
}
func (bs numberSetProxyValue) valueAt(i int) (exprValue, error) {
fs, _, err := big.ParseFloat(bs.numberSet.Value[i], 10, 63, big.ToNearestEven)
if err != nil {
return nil, err
}
return bigNumExprValue{fs}, nil
}
func (bs numberSetProxyValue) setValueAt(i int, newVal exprValue) {
if str, isStr := newVal.(numberableExprValue); isStr {
bs.numberSet.Value[i] = str.asBigFloat().String()
}
}
func (bs numberSetProxyValue) deleteValueAt(idx int) {
newList := append([]string{}, bs.numberSet.Value[:idx]...)
newList = append(newList, bs.numberSet.Value[idx+1:]...)
bs.numberSet = &types.AttributeValueMemberNS{Value: newList}
}
func (s numberSetProxyValue) typeName() string {
return "NS"
}

View file

@ -5,56 +5,31 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"strconv" "strconv"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func (a *astLiteralValue) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) { func (a *astLiteralValue) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
v, err := a.goValue() v, err := a.exprValue()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return irValue{value: v}, nil return irValue{value: v}, nil
} }
func (a *astLiteralValue) dynamoValue() (types.AttributeValue, error) { func (a *astLiteralValue) exprValue() (exprValue, error) {
if a == nil {
return nil, nil
}
goValue, err := a.goValue()
if err != nil {
return nil, err
}
switch v := goValue.(type) {
case string:
return &types.AttributeValueMemberS{Value: v}, nil
case int64:
return &types.AttributeValueMemberN{Value: strconv.FormatInt(v, 10)}, nil
}
return nil, errors.New("unrecognised type")
}
func (a *astLiteralValue) goValue() (any, error) {
if a == nil {
return nil, nil
}
switch { switch {
case a.StringVal != nil: case a.StringVal != nil:
s, err := strconv.Unquote(*a.StringVal) s, err := strconv.Unquote(*a.StringVal)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot unquote string") return nil, errors.Wrap(err, "cannot unquote string")
} }
return s, nil return stringExprValue(s), nil
case a.IntVal != nil: case a.IntVal != nil:
return *a.IntVal, nil return int64ExprValue(*a.IntVal), nil
case a.TrueBoolValue: case a.TrueBoolValue:
return true, nil return boolExprValue(true), nil
case a.FalseBoolValue: case a.FalseBoolValue:
return false, nil return boolExprValue(false), nil
} }
return nil, errors.New("unrecognised type") return nil, errors.New("unrecognised type")
} }
@ -78,17 +53,17 @@ func (a *astLiteralValue) String() string {
} }
type irValue struct { type irValue struct {
value any value exprValue
} }
func (i irValue) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { func (i irValue) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
return expression.ConditionBuilder{}, NodeCannotBeConvertedToQueryError{} return expression.ConditionBuilder{}, NodeCannotBeConvertedToQueryError{}
} }
func (i irValue) goValue() any { func (i irValue) exprValue() exprValue {
return i.value return i.value
} }
func (a irValue) calcOperand(info *models.TableInfo) expression.OperandBuilder { func (a irValue) calcOperand(info *models.TableInfo) expression.OperandBuilder {
return expression.Value(a.goValue()) return expression.Value(a.value.asGoValue())
} }

View file

@ -32,6 +32,7 @@ type SessionService interface {
type QueryOptions struct { type QueryOptions struct {
TableName string TableName string
IndexName string
NamePlaceholders map[string]string NamePlaceholders map[string]string
ValuePlaceholders map[string]types.AttributeValue ValuePlaceholders map[string]types.AttributeValue
} }

View file

@ -44,6 +44,11 @@ func (um *sessionModule) query(ctx context.Context, args ...object.Object) objec
} }
} }
// Index name
if val, isStr := objMap.Get("index").(*object.String); isStr {
options.IndexName = val.Value()
}
// Placeholders // Placeholders
if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap { if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap {
options.NamePlaceholders = make(map[string]string) options.NamePlaceholders = make(map[string]string)

View file

@ -144,6 +144,8 @@ func NewModel(
itemType = models.BoolItemType itemType = models.BoolItemType
case "-NULL": case "-NULL":
itemType = models.NullItemType itemType = models.NullItemType
case "-TO":
itemType = models.ExprValueItemType
default: default:
return events.Error(errors.New("unrecognised item type")) return events.Error(errors.New("unrecognised item type"))
} }

View file

@ -91,10 +91,14 @@ func main() {
} }
} }
key := gofakeit.UUID() var key = gofakeit.UUID()
for i := 0; i < totalItems; i++ { for i := 0; i < totalItems; i++ {
if i%50 == 0 {
key = gofakeit.UUID()
}
if err := tableService.Put(ctx, inventoryTableInfo, models.Item{ if err := tableService.Put(ctx, inventoryTableInfo, models.Item{
"pk": &types.AttributeValueMemberS{Value: key}, "pk": &types.AttributeValueMemberS{Value: key},
"sk": &types.AttributeValueMemberN{Value: fmt.Sprint(i % 50)},
"uuid": &types.AttributeValueMemberS{Value: gofakeit.UUID()}, "uuid": &types.AttributeValueMemberS{Value: gofakeit.UUID()},
}); err != nil { }); err != nil {
log.Fatalln(err) log.Fatalln(err)