diff --git a/internal/common/maputils/map.go b/internal/common/maputils/map.go index bffa7a1..9708bbe 100644 --- a/internal/common/maputils/map.go +++ b/internal/common/maputils/map.go @@ -8,6 +8,16 @@ func Values[K comparable, T any](ts map[K]T) []T { 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) { us := make(map[K]U) diff --git a/internal/common/sliceutils/map.go b/internal/common/sliceutils/map.go index 43b69e7..ae2a1be 100644 --- a/internal/common/sliceutils/map.go +++ b/internal/common/sliceutils/map.go @@ -39,3 +39,22 @@ func Filter[T any](ts []T, fn func(t T) bool) []T { } 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 +} diff --git a/internal/dynamo-browse/controllers/scripts.go b/internal/dynamo-browse/controllers/scripts.go index 50daf23..35e66c2 100644 --- a/internal/dynamo-browse/controllers/scripts.go +++ b/internal/dynamo-browse/controllers/scripts.go @@ -182,6 +182,9 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage if opts.ValuePlaceholders != nil { expr = expr.WithValueParams(opts.ValuePlaceholders) } + if opts.IndexName != "" { + expr = expr.WithIndex(opts.IndexName) + } // Get the table info var tableInfo *models.TableInfo diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index cc1410a..2409c1f 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -122,6 +122,8 @@ func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.Item return twc.setBoolValue(idx, path) case models.NullItemType: return twc.setNullValue(idx, path) + case models.ExprValueItemType: + return twc.setToExpressionValue(idx, path) default: 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 { return events.PromptForInputMsg{ 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 { - err := path.DeleteAttribute(set.Items()[idx]) - if err != nil { + if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error { + if err := path.DeleteAttribute(set.Items()[idx]); err != nil { + return err + } + + set.SetDirty(idx, true) + return nil + }); err != nil { return err } - set.SetDirty(idx, true) set.RefreshColumns() return nil }); err != nil { diff --git a/internal/dynamo-browse/models/itemtype.go b/internal/dynamo-browse/models/itemtype.go index 4174005..ab271c9 100644 --- a/internal/dynamo-browse/models/itemtype.go +++ b/internal/dynamo-browse/models/itemtype.go @@ -8,4 +8,6 @@ const ( NumberItemType ItemType = "N" BoolItemType ItemType = "BOOL" NullItemType ItemType = "NULL" + + ExprValueItemType ItemType = "exprvalue" ) diff --git a/internal/dynamo-browse/models/queryexpr/ast.go b/internal/dynamo-browse/models/queryexpr/ast.go index 7e8cf81..2c81161 100644 --- a/internal/dynamo-browse/models/queryexpr/ast.go +++ b/internal/dynamo-browse/models/queryexpr/ast.go @@ -4,17 +4,23 @@ import ( "github.com/alecthomas/participle/v2" "github.com/alecthomas/participle/v2/lexer" "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/dynamo-browse/models" "github.com/pkg/errors" + "strconv" ) // Modelled on the expression language here // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html 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 { @@ -38,9 +44,15 @@ type astIn struct { } type astComparisonOp struct { - Ref *astEqualityOp `parser:"@@"` - Op string `parser:"( @('<' | '<=' | '>' | '>=')"` - Value *astEqualityOp `parser:"@@ )?"` + Ref *astBetweenOp `parser:"@@"` + Op string `parser:"( @('<' | '<=' | '>' | '>=')"` + Value *astBetweenOp `parser:"@@ )?"` +} + +type astBetweenOp struct { + Ref *astEqualityOp `parser:"@@"` + From *astEqualityOp `parser:"( 'between' @@ "` + To *astEqualityOp `parser:" 'and' @@ )?"` } type astEqualityOp struct { @@ -58,7 +70,6 @@ type astIsOp struct { type astSubRef struct { Ref *astFunctionCall `parser:"@@"` SubRefs []*astSubRefType `parser:"@@*"` - //Quals []string `parser:"('.' @Ident)*"` } type astSubRefType struct { @@ -121,7 +132,58 @@ func Parse(expr string) (*QueryExpr, error) { 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 { index string keysUnderTest models.KeyAttribute @@ -153,11 +215,11 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q return nil, err } - return &models.QueryExecutionPlan{ + plans = append(plans, &models.QueryExecutionPlan{ CanQuery: true, IndexName: attempt.index, Expression: expr, - }, nil + }) } } @@ -174,21 +236,22 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q return nil, err } - return &models.QueryExecutionPlan{ + plans = append(plans, &models.QueryExecutionPlan{ CanQuery: false, Expression: expr, - }, nil + }) + return plans, nil } func (a *astExpr) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) { 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) } -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) } diff --git a/internal/dynamo-browse/models/queryexpr/atom.go b/internal/dynamo-browse/models/queryexpr/atom.go index 8b4487e..5a24d2e 100644 --- a/internal/dynamo-browse/models/queryexpr/atom.go +++ b/internal/dynamo-browse/models/queryexpr/atom.go @@ -1,7 +1,6 @@ package queryexpr import ( - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" "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") } -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) { switch { case a.Ref != nil: @@ -39,12 +29,12 @@ func (a *astAtom) unqualifiedName() (string, bool) { 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 { case a.Ref != nil: return a.Ref.evalItem(ctx, item) case a.Literal != nil: - return a.Literal.dynamoValue() + return a.Literal.exprValue() case a.Placeholder != nil: return a.Placeholder.evalItem(ctx, item) case a.Paren != nil: @@ -66,7 +56,7 @@ func (a *astAtom) canModifyItem(ctx *evalContext, item models.Item) bool { 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 { case a.Ref != nil: return a.Ref.setEvalItem(ctx, item, value) diff --git a/internal/dynamo-browse/models/queryexpr/between.go b/internal/dynamo-browse/models/queryexpr/between.go new file mode 100644 index 0000000..cbb4c3d --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/between.go @@ -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 +} diff --git a/internal/dynamo-browse/models/queryexpr/boolnot.go b/internal/dynamo-browse/models/queryexpr/boolnot.go index 9c71f79..79a11c7 100644 --- a/internal/dynamo-browse/models/queryexpr/boolnot.go +++ b/internal/dynamo-browse/models/queryexpr/boolnot.go @@ -2,7 +2,6 @@ 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/dynamo-browse/models" "strings" ) @@ -20,7 +19,7 @@ func (a *astBooleanNot) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) 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) if err != nil { return nil, err @@ -30,7 +29,7 @@ func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.Attr 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 { @@ -40,7 +39,7 @@ func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool { 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 { return PathNotSettableError{} } diff --git a/internal/dynamo-browse/models/queryexpr/builtins.go b/internal/dynamo-browse/models/queryexpr/builtins.go index a42f0cc..a3e9322 100644 --- a/internal/dynamo-browse/models/queryexpr/builtins.go +++ b/internal/dynamo-browse/models/queryexpr/builtins.go @@ -2,38 +2,90 @@ package queryexpr import ( "context" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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{ - "size": func(ctx context.Context, args []types.AttributeValue) (types.AttributeValue, error) { + "size": func(ctx context.Context, args []exprValue) (exprValue, error) { if len(args) != 1 { return nil, InvalidArgumentNumberError{Name: "size", Expected: 1, Actual: len(args)} } var l int switch t := args[0].(type) { - case *types.AttributeValueMemberB: - l = len(t.Value) - case *types.AttributeValueMemberS: - l = len(t.Value) - case *types.AttributeValueMemberL: - l = len(t.Value) - 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) + case stringExprValue: + l = len(t) + case mappableExprValue: + l = t.len() + case slicableExprValue: + l = t.len() default: 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 }, } diff --git a/internal/dynamo-browse/models/queryexpr/comp.go b/internal/dynamo-browse/models/queryexpr/comp.go index 31e9e4d..3e19472 100644 --- a/internal/dynamo-browse/models/queryexpr/comp.go +++ b/internal/dynamo-browse/models/queryexpr/comp.go @@ -2,7 +2,6 @@ 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/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "github.com/pkg/errors" @@ -47,7 +46,7 @@ func (a *astComparisonOp) evalToIR(ctx *evalContext, info *models.TableInfo) (ir 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) if err != nil { return nil, err @@ -61,20 +60,21 @@ func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.At return nil, err } - cmp, isComparable := attrutils.CompareScalarAttributes(left, right) + // TODO: use expr value here + cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue()) if !isComparable { - return nil, ValuesNotComparable{Left: left, Right: right} + return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()} } switch opToCmdType[a.Op] { case cmpTypeLt: - return &types.AttributeValueMemberBOOL{Value: cmp < 0}, nil + return boolExprValue(cmp < 0), nil case cmpTypeLe: - return &types.AttributeValueMemberBOOL{Value: cmp <= 0}, nil + return boolExprValue(cmp <= 0), nil case cmpTypeGt: - return &types.AttributeValueMemberBOOL{Value: cmp > 0}, nil + return boolExprValue(cmp > 0), nil case cmpTypeGe: - return &types.AttributeValueMemberBOOL{Value: cmp >= 0}, nil + return boolExprValue(cmp >= 0), nil } 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) } -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 != "" { return PathNotSettableError{} } @@ -143,34 +143,34 @@ func (a irKeyFieldCmp) canBeExecutedAsQuery(qci *queryCalcInfo) bool { func (a irKeyFieldCmp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { nb := a.name.calcName(info) - vb := a.value.goValue() + vb := a.value.exprValue() switch a.cmpType { case cmpTypeLt: - return nb.LessThan(expression.Value(vb)), nil + return nb.LessThan(buildExpressionFromValue(vb)), nil case cmpTypeLe: - return nb.LessThanEqual(expression.Value(vb)), nil + return nb.LessThanEqual(buildExpressionFromValue(vb)), nil case cmpTypeGt: - return nb.GreaterThan(expression.Value(vb)), nil + return nb.GreaterThan(buildExpressionFromValue(vb)), nil case cmpTypeGe: - return nb.GreaterThanEqual(expression.Value(vb)), nil + return nb.GreaterThanEqual(buildExpressionFromValue(vb)), nil } return expression.ConditionBuilder{}, errors.New("unsupported cmp type") } func (a irKeyFieldCmp) calcQueryForQuery() (expression.KeyConditionBuilder, error) { keyName := a.name.keyName() - vb := a.value.goValue() + vb := a.value.exprValue() switch a.cmpType { case cmpTypeLt: - return expression.Key(keyName).LessThan(expression.Value(vb)), nil + return expression.Key(keyName).LessThan(buildExpressionFromValue(vb)), nil case cmpTypeLe: - return expression.Key(keyName).LessThanEqual(expression.Value(vb)), nil + return expression.Key(keyName).LessThanEqual(buildExpressionFromValue(vb)), nil case cmpTypeGt: - return expression.Key(keyName).GreaterThan(expression.Value(vb)), nil + return expression.Key(keyName).GreaterThan(buildExpressionFromValue(vb)), nil 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") } diff --git a/internal/dynamo-browse/models/queryexpr/conj.go b/internal/dynamo-browse/models/queryexpr/conj.go index dd7f4a2..d54bb4b 100644 --- a/internal/dynamo-browse/models/queryexpr/conj.go +++ b/internal/dynamo-browse/models/queryexpr/conj.go @@ -2,8 +2,8 @@ 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/dynamo-browse/models" + "math/big" "strings" ) @@ -36,7 +36,7 @@ func (a *astConjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) 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) if err != nil { return nil, err @@ -47,7 +47,7 @@ func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.Att for _, opr := range a.Operands[1:] { if !isAttributeTrue(val) { - return &types.AttributeValueMemberBOOL{Value: false}, nil + return boolExprValue(false), nil } 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 { @@ -67,7 +67,7 @@ func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool 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 { return a.Operands[0].setEvalItem(ctx, item, value) } @@ -168,16 +168,16 @@ func (d *irMultiConjunction) calcQueryForScan(info *models.TableInfo) (expressio return conjExpr, nil } -func isAttributeTrue(attr types.AttributeValue) bool { +func isAttributeTrue(attr exprValue) bool { switch val := attr.(type) { - case *types.AttributeValueMemberS: - return val.Value != "" - case *types.AttributeValueMemberN: - return val.Value != "0" - case *types.AttributeValueMemberBOOL: - return val.Value - case *types.AttributeValueMemberNULL: + case nullExprValue: return false + case boolExprValue: + return bool(val) + case stringableExprValue: + return val.asString() != "" + case numberableExprValue: + return val.asBigFloat().Cmp(&big.Float{}) != 0 } return true } diff --git a/internal/dynamo-browse/models/queryexpr/context.go b/internal/dynamo-browse/models/queryexpr/context.go new file mode 100644 index 0000000..18c56ef --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/context.go @@ -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{} +} diff --git a/internal/dynamo-browse/models/queryexpr/disj.go b/internal/dynamo-browse/models/queryexpr/disj.go index 913a503..8819587 100644 --- a/internal/dynamo-browse/models/queryexpr/disj.go +++ b/internal/dynamo-browse/models/queryexpr/disj.go @@ -2,7 +2,6 @@ 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/dynamo-browse/models" "strings" ) @@ -24,7 +23,7 @@ func (a *astDisjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) 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) if err != nil { return nil, err @@ -35,7 +34,7 @@ func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.Att for _, opr := range a.Operands[1:] { if isAttributeTrue(val) { - return &types.AttributeValueMemberBOOL{Value: true}, nil + return boolExprValue(true), nil } 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 { @@ -55,7 +54,7 @@ func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool 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 { return a.Operands[0].setEvalItem(ctx, item, value) } diff --git a/internal/dynamo-browse/models/queryexpr/dot.go b/internal/dynamo-browse/models/queryexpr/dot.go index d2c18fc..97063c9 100644 --- a/internal/dynamo-browse/models/queryexpr/dot.go +++ b/internal/dynamo-browse/models/queryexpr/dot.go @@ -3,7 +3,6 @@ package queryexpr import ( "fmt" "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" "strings" ) @@ -16,21 +15,21 @@ func (dt *astRef) unqualifiedName() (string, bool) { 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] if !hasV { return nil, nil } - return res, nil + return newExprValueFromAttributeValue(res) } func (dt *astRef) canModifyItem(ctx *evalContext, item models.Item) bool { return true } -func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { - item[dt.Name] = value +func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { + item[dt.Name] = value.asAttributeValue() return nil } @@ -71,7 +70,7 @@ func (i irNamePath) calcName(info *models.TableInfo) expression.NameBuilder { switch v := qual.(type) { case string: fullName.WriteString("." + v) - case int: + case int64: fullName.WriteString(fmt.Sprintf("[%v]", qual)) } } diff --git a/internal/dynamo-browse/models/queryexpr/equality.go b/internal/dynamo-browse/models/queryexpr/equality.go index 702754b..ffa26cd 100644 --- a/internal/dynamo-browse/models/queryexpr/equality.go +++ b/internal/dynamo-browse/models/queryexpr/equality.go @@ -2,7 +2,6 @@ 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/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "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) } -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) if err != nil { return nil, err @@ -74,30 +73,32 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.Attr return nil, err } + // TODO: use expr values here + switch a.Op { case "=": - cmp, isComparable := attrutils.CompareScalarAttributes(left, right) + cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue()) 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 "!=": - cmp, isComparable := attrutils.CompareScalarAttributes(left, right) + cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue()) 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 "^=": - strValue, isStrValue := right.(*types.AttributeValueMemberS) + strValue, isStrValue := right.(stringableExprValue) if !isStrValue { return nil, errors.New("operand '^=' must be string") } - leftAsStr, canBeString := attrutils.AttributeToString(left) + leftAsStr, canBeString := left.(stringableExprValue) 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) @@ -110,7 +111,7 @@ func (a *astEqualityOp) canModifyItem(ctx *evalContext, item models.Item) bool { 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 != "" { return PathNotSettableError{} } @@ -157,8 +158,8 @@ func (a irKeyFieldEq) calcQueryForScan(info *models.TableInfo) (expression.Condi } func (a irKeyFieldEq) calcQueryForQuery() (expression.KeyConditionBuilder, error) { - vb := a.value.goValue() - return expression.Key(a.name.keyName()).Equal(expression.Value(vb)), nil + vb := a.value.exprValue() + return expression.Key(a.name.keyName()).Equal(buildExpressionFromValue(vb)), nil } type irGenericEq struct { @@ -203,21 +204,21 @@ func (a irFieldBeginsWith) canBeExecutedAsQuery(qci *queryCalcInfo) bool { func (a irFieldBeginsWith) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { nb := a.name.calcName(info) - vb := a.value.goValue() - strValue, isStrValue := vb.(string) + vb := a.value.exprValue() + strValue, isStrValue := vb.(stringableExprValue) if !isStrValue { 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) { - vb := a.value.goValue() - strValue, isStrValue := vb.(string) + vb := a.value.exprValue() + strValue, isStrValue := vb.(stringableExprValue) if !isStrValue { 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 } diff --git a/internal/dynamo-browse/models/queryexpr/errors.go b/internal/dynamo-browse/models/queryexpr/errors.go index 59f1ddb..8c48bbf 100644 --- a/internal/dynamo-browse/models/queryexpr/errors.go +++ b/internal/dynamo-browse/models/queryexpr/errors.go @@ -98,6 +98,14 @@ func (n InvalidTypeForIsError) Error() string { 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 { Name string 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) } +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 { Name string } @@ -137,3 +155,20 @@ type ValueNotUsableAsASubref struct { func (e ValueNotUsableAsASubref) Error() string { 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) +} diff --git a/internal/dynamo-browse/models/queryexpr/expr.go b/internal/dynamo-browse/models/queryexpr/expr.go index aed149f..a68a275 100644 --- a/internal/dynamo-browse/models/queryexpr/expr.go +++ b/internal/dynamo-browse/models/queryexpr/expr.go @@ -16,12 +16,17 @@ import ( type QueryExpr struct { ast *astExpr + index string names map[string]string values map[string]types.AttributeValue + + // tests fields only + timeSource timeSource } type serializedExpr struct { Expr string + Index string Names map[string]string Values []byte } @@ -39,6 +44,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) { } qe.names = se.Names + qe.index = se.Index if len(se.Values) > 0 { 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 { - se := serializedExpr{Expr: md.String(), Names: md.names} + se := serializedExpr{Expr: md.String(), Index: md.index, Names: md.names} if md.values != nil { var bts bytes.Buffer 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() && + md.index == other.index && maps.Equal(md.names, other.names) && maps.EqualFunc(md.values, md.values, attrutils.Equals) } @@ -104,6 +111,7 @@ func (md *QueryExpr) HashCode() uint64 { h := fnv.New64a() h.Write([]byte(md.ast.String())) + h.Write([]byte(md.index)) // the names must be in sorted order to maintain consistant key ordering if len(md.names) > 0 { @@ -134,6 +142,7 @@ func (md *QueryExpr) HashCode() uint64 { func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr { return &QueryExpr{ ast: md.ast, + index: md.index, names: value, 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 { return &QueryExpr{ ast: md.ast, + index: md.index, names: md.names, 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) { - 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) { - 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 { @@ -176,7 +202,11 @@ func (md *QueryExpr) DeleteAttribute(item models.Item) 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 { @@ -237,6 +267,7 @@ type evalContext struct { nameLookup func(string) (string, bool) valuePlaceholders map[string]types.AttributeValue valueLookup func(string) (types.AttributeValue, bool) + timeSource timeSource } func (ec *evalContext) lookupName(name string) (string, bool) { @@ -264,3 +295,10 @@ func (ec *evalContext) lookupValue(name string) (types.AttributeValue, bool) { return nil, false } + +func (ec *evalContext) getTimeSource() timeSource { + if ts := ec.timeSource; ts != nil { + return ts + } + return defaultTimeSource{} +} diff --git a/internal/dynamo-browse/models/queryexpr/expr_test.go b/internal/dynamo-browse/models/queryexpr/expr_test.go index 5575f14..eb3a52e 100644 --- a/internal/dynamo-browse/models/queryexpr/expr_test.go +++ b/internal/dynamo-browse/models/queryexpr/expr_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" "testing" + "time" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/stretchr/testify/assert" @@ -34,6 +35,13 @@ func TestModExpr_Query(t *testing.T) { 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"), exprNameIsNumber(1, 1, "sk", "100"), ), + scanCase("when request pk is equals and sk is greater or equal to", + `pk="prefix" and sk between 100 and 200`, + `(#0 = :0) AND (#1 BETWEEN :1 AND :2)`, + exprNameIsString(0, 0, "pk", "prefix"), + exprNameIsNumber(1, 1, "sk", "100"), + exprValueIsNumber(2, "200"), + ), scanCase("with placeholders", `:partition=$valuePrefix and :sort=$valueAnother`, @@ -149,6 +164,13 @@ func TestModExpr_Query(t *testing.T) { exprNameIsString(0, 0, "color", "yellow"), exprNameIsString(1, 1, "shade", "dark"), ), + + // Function calls + scanCase("use the value of fn call in query", + `pk = _x_concat("Hello ", "world")`, + `#0 = :0`, + exprNameIsString(0, 0, "pk", "Hello world"), + ), } for _, scenario := range scenarios { @@ -238,6 +260,13 @@ func TestModExpr_Query(t *testing.T) { exprNameIsString(0, 0, "pk", "prefix"), ), + scanCase("with between", `pk between "a" and "z"`, + `#0 BETWEEN :0 AND :1`, + exprName(0, "pk"), + exprValueIsString(0, "a"), + exprValueIsString(1, "z"), + ), + scanCase("with in", `pk in ("alpha", "bravo", "charlie")`, `#0 IN (:0, :1, :2)`, exprName(0, "pk"), @@ -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) { @@ -395,7 +471,9 @@ func TestQueryExpr_EvalItem(t *testing.T) { &types.AttributeValueMemberN{Value: "7"}, }, }, + "one": &types.AttributeValueMemberN{Value: "1"}, "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}}, + // 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 {expr: "three in (2, 3, 4, 5)", expected: &types.AttributeValueMemberBOOL{Value: true}}, {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) { scenarios := []struct { expr string diff --git a/internal/dynamo-browse/models/queryexpr/fncall.go b/internal/dynamo-browse/models/queryexpr/fncall.go index 358071e..1eff92d 100644 --- a/internal/dynamo-browse/models/queryexpr/fncall.go +++ b/internal/dynamo-browse/models/queryexpr/fncall.go @@ -3,7 +3,6 @@ package queryexpr import ( "context" "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/dynamo-browse/models" "github.com/pkg/errors" @@ -29,7 +28,7 @@ func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (ir return nil, err } - // TODO: do this properly + // Special handling of functions that have IR nodes switch nameIr.keyName() { case "size": 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 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 { 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} } - 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) }) if err != nil { 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 { @@ -85,7 +99,7 @@ func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool 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? if a.IsCall { return PathNotSettableError{} @@ -147,3 +161,15 @@ func (i irRangeFn) calcGoValues(info *models.TableInfo) ([]any, error) { } 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 +} diff --git a/internal/dynamo-browse/models/queryexpr/helpers_test.go b/internal/dynamo-browse/models/queryexpr/helpers_test.go new file mode 100644 index 0000000..e856529 --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/helpers_test.go @@ -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 +} diff --git a/internal/dynamo-browse/models/queryexpr/in.go b/internal/dynamo-browse/models/queryexpr/in.go index 6a169d3..18d25dd 100644 --- a/internal/dynamo-browse/models/queryexpr/in.go +++ b/internal/dynamo-browse/models/queryexpr/in.go @@ -1,10 +1,7 @@ package queryexpr import ( - "bytes" - "fmt" "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/dynamo-browse/models" "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()) } 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: nameIR, isNameIR := leftIR.(irNamePath) if !isNameIR { @@ -78,13 +82,6 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro } 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: return nil, OperandNotAnOperandError{} } @@ -96,7 +93,7 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro 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) if err != nil { return nil, err @@ -112,14 +109,15 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal if err != nil { return nil, err } - cmp, isComparable := attrutils.CompareScalarAttributes(val, evalOp) + // TODO: use native types here + cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), evalOp.asAttributeValue()) if !isComparable { continue } 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: evalOp, err := a.SingleOperand.evalItem(ctx, item) if err != nil { @@ -127,69 +125,38 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal } switch t := evalOp.(type) { - case *types.AttributeValueMemberS: - str, canToStr := attrutils.AttributeToString(val) + case stringableExprValue: + str, canToStr := val.(stringableExprValue) if !canToStr { - return &types.AttributeValueMemberBOOL{Value: false}, nil + return boolExprValue(false), nil } - return &types.AttributeValueMemberBOOL{Value: strings.Contains(t.Value, str)}, nil - case *types.AttributeValueMemberL: - for _, listItem := range t.Value { - cmp, isComparable := attrutils.CompareScalarAttributes(val, listItem) + return boolExprValue(strings.Contains(t.asString(), str.asString())), nil + case slicableExprValue: + for i := 0; i < t.len(); i++ { + 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 { continue } else if cmp == 0 { - return &types.AttributeValueMemberBOOL{Value: true}, nil + return boolExprValue(true), nil } } - return &types.AttributeValueMemberBOOL{Value: false}, nil - case *types.AttributeValueMemberSS: - str, canToStr := attrutils.AttributeToString(val) + return boolExprValue(false), nil + case mappableExprValue: + str, canToStr := val.(stringableExprValue) if !canToStr { - return &types.AttributeValueMemberBOOL{Value: false}, nil + return boolExprValue(false), nil } - - for _, listItem := range t.Value { - 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 + hasKey := t.hasKey(str.asString()) + return boolExprValue(hasKey), nil } - return nil, ValuesNotInnableError{Val: evalOp} + return nil, ValuesNotInnableError{Val: evalOp.asAttributeValue()} } 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) } -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 { return PathNotSettableError{} } @@ -263,19 +230,38 @@ func (i irIn) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuil type irLiteralValues struct { name nameIRAtom - values multiValueIRAtom + values valueIRAtom } -func (i irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { - vals, err := i.values.calcGoValues(info) - if err != nil { - return expression.ConditionBuilder{}, err +func (iv irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { + if sliceable, isSliceable := iv.values.exprValue().(slicableExprValue); isSliceable { + if sliceable.len() == 1 { + 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 expression.Value(t) - }) - return i.name.calcName(info).In(oprValues[0], oprValues[1:]...), nil + return iv.name.calcName(info).In(buildExpressionFromValue(iv.values.exprValue())), nil } type irContains struct { @@ -284,8 +270,11 @@ type irContains struct { } func (i irContains) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { - needle := i.needle.goValue() - haystack := i.haystack.calcName(info) + strNeedle, isString := i.needle.exprValue().(stringableExprValue) + 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 } diff --git a/internal/dynamo-browse/models/queryexpr/ir.go b/internal/dynamo-browse/models/queryexpr/ir.go index db9c218..a8dddbd 100644 --- a/internal/dynamo-browse/models/queryexpr/ir.go +++ b/internal/dynamo-browse/models/queryexpr/ir.go @@ -36,11 +36,7 @@ type nameIRAtom interface { type valueIRAtom interface { oprIRAtom - goValue() any -} - -type multiValueIRAtom interface { - calcGoValues(info *models.TableInfo) ([]any, error) + exprValue() exprValue } func canExecuteAsQuery(ir irAtom, qci *queryCalcInfo) bool { diff --git a/internal/dynamo-browse/models/queryexpr/is.go b/internal/dynamo-browse/models/queryexpr/is.go index 50daf6b..18e40d5 100644 --- a/internal/dynamo-browse/models/queryexpr/is.go +++ b/internal/dynamo-browse/models/queryexpr/is.go @@ -2,9 +2,7 @@ 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/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "reflect" "strings" ) @@ -12,50 +10,50 @@ import ( type isTypeInfo struct { isAny bool attributeType expression.DynamoDBAttributeType - goType reflect.Type + goTypes []reflect.Type } var validIsTypeNames = map[string]isTypeInfo{ "ANY": {isAny: true}, "B": { attributeType: expression.Binary, - goType: reflect.TypeOf(&types.AttributeValueMemberB{}), + // TODO }, "BOOL": { attributeType: expression.Boolean, - goType: reflect.TypeOf(&types.AttributeValueMemberBOOL{}), + goTypes: []reflect.Type{reflect.TypeOf(boolExprValue(false))}, }, "S": { attributeType: expression.String, - goType: reflect.TypeOf(&types.AttributeValueMemberS{}), + goTypes: []reflect.Type{reflect.TypeOf(stringExprValue(""))}, }, "N": { attributeType: expression.Number, - goType: reflect.TypeOf(&types.AttributeValueMemberN{}), + goTypes: []reflect.Type{reflect.TypeOf(int64ExprValue(0)), reflect.TypeOf(bigNumExprValue{})}, }, "NULL": { attributeType: expression.Null, - goType: reflect.TypeOf(&types.AttributeValueMemberNULL{}), + goTypes: []reflect.Type{reflect.TypeOf(nullExprValue{})}, }, "L": { attributeType: expression.List, - goType: reflect.TypeOf(&types.AttributeValueMemberL{}), + goTypes: []reflect.Type{reflect.TypeOf(listExprValue{}), reflect.TypeOf(listProxyValue{})}, }, "M": { attributeType: expression.Map, - goType: reflect.TypeOf(&types.AttributeValueMemberM{}), + goTypes: []reflect.Type{reflect.TypeOf(mapExprValue{}), reflect.TypeOf(mapProxyValue{})}, }, "BS": { attributeType: expression.BinarySet, - goType: reflect.TypeOf(&types.AttributeValueMemberBS{}), + // TODO }, "NS": { attributeType: expression.NumberSet, - goType: reflect.TypeOf(&types.AttributeValueMemberNS{}), + // TODO }, "SS": { 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 { return nil, ValueMustBeLiteralError{} } - strValue, isStringValue := valueIR.goValue().(string) + strValue, isStringValue := valueIR.exprValue().(stringableExprValue) if !isStringValue { return nil, ValueMustBeStringError{} } - typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue)] + typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue.asString())] if !isValidType { - return nil, InvalidTypeForIsError{TypeName: strValue} + return nil, InvalidTypeForIsError{TypeName: strValue.asString()} } 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 } -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) if err != nil { return nil, err @@ -118,13 +116,13 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeV if err != nil { return nil, err } - str, canToStr := attrutils.AttributeToString(expTypeVal) + str, canToStr := expTypeVal.(stringableExprValue) if !canToStr { return nil, ValueMustBeStringError{} } - typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str)] + typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str.asString())] if !hasTypeInfo { - return nil, InvalidTypeForIsError{TypeName: str} + return nil, InvalidTypeForIsError{TypeName: str.asString()} } var resultOfIs bool @@ -132,12 +130,18 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeV resultOfIs = ref != nil } else { refType := reflect.TypeOf(ref) - resultOfIs = typeInfo.goType.AssignableTo(refType) + + for _, t := range typeInfo.goTypes { + if t.AssignableTo(refType) { + resultOfIs = true + break + } + } } if a.HasNot { resultOfIs = !resultOfIs } - return &types.AttributeValueMemberBOOL{Value: resultOfIs}, nil + return boolExprValue(resultOfIs), nil } 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) } -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 { return PathNotSettableError{} } diff --git a/internal/dynamo-browse/models/queryexpr/placeholder.go b/internal/dynamo-browse/models/queryexpr/placeholder.go index ec94a83..1f6ffdb 100644 --- a/internal/dynamo-browse/models/queryexpr/placeholder.go +++ b/internal/dynamo-browse/models/queryexpr/placeholder.go @@ -1,7 +1,6 @@ package queryexpr import ( - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" "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 irValue{value: val}, nil + ev, err := newExprValueFromAttributeValue(val) + if err != nil { + return nil, err + } + + return irValue{value: ev}, nil } else if placeholderType == namePlaceholderPrefix { name, hasName := ctx.lookupName(placeholder) if !hasName { @@ -34,7 +38,7 @@ func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irA 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] placeholder := p.Placeholder[1:] @@ -43,7 +47,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att if !hasVal { return nil, MissingPlaceholderError{Placeholder: p.Placeholder} } - return val, nil + return newExprValueFromAttributeValue(val) } else if placeholderType == namePlaceholderPrefix { name, hasName := ctx.lookupName(placeholder) if !hasName { @@ -55,7 +59,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att return nil, nil } - return res, nil + return newExprValueFromAttributeValue(res) } return nil, errors.New("unrecognised placeholder") @@ -66,7 +70,7 @@ func (p *astPlaceholder) canModifyItem(ctx *evalContext, item models.Item) bool 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] placeholder := p.Placeholder[1:] @@ -78,7 +82,7 @@ func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value t return MissingPlaceholderError{Placeholder: p.Placeholder} } - item[name] = value + item[name] = value.asAttributeValue() return nil } diff --git a/internal/dynamo-browse/models/queryexpr/subref.go b/internal/dynamo-browse/models/queryexpr/subref.go index 0674fcb..0e3c8f9 100644 --- a/internal/dynamo-browse/models/queryexpr/subref.go +++ b/internal/dynamo-browse/models/queryexpr/subref.go @@ -1,10 +1,8 @@ package queryexpr import ( - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/dynamo-browse/models" - "strconv" "strings" ) @@ -34,7 +32,7 @@ func (r *astSubRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, 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) if err != nil { return nil, err @@ -48,7 +46,7 @@ func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.Attribut 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 { sv, err := sr.evalToStrOrInt(ctx, nil) if err != nil { @@ -57,24 +55,30 @@ func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.At switch val := sv.(type) { case string: - var hasV bool - mapRes, isMapRes := res.(*types.AttributeValueMemberM) + mapRes, isMapRes := res.(mappableExprValue) if !isMapRes { return nil, newValueNotAMapError(r, subRefs[:i+1]) } - res, hasV = mapRes.Value[val] - if !hasV { - return nil, nil + if mapRes.hasKey(val) { + res, err = mapRes.valueOf(val) + if err != nil { + return nil, err + } + } else { + res = nil } - case int: - listRes, isMapRes := res.(*types.AttributeValueMemberL) + case int64: + listRes, isMapRes := res.(slicableExprValue) if !isMapRes { return nil, newValueNotAListError(r, subRefs[:i+1]) } - // TODO - deal with index properly - res = listRes.Value[val] + // TODO - deal with index properly (i.e. error handling) + res, err = listRes.valueAt(int(val)) + if err != nil { + return nil, err + } } } return res, nil @@ -84,7 +88,7 @@ func (r *astSubRef) canModifyItem(ctx *evalContext, item models.Item) bool { 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 { 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) { case string: - mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM) + mapRes, isMapRes := parentItem.(modifiableMapExprValue) if !isMapRes { return newValueNotAMapError(r, r.SubRefs) } - mapRes.Value[val] = value - case int: - listRes, isMapRes := parentItem.(*types.AttributeValueMemberL) + mapRes.setValueOf(val, value) + case int64: + listRes, isMapRes := parentItem.(modifiableSliceExprValue) if !isMapRes { return newValueNotAListError(r, r.SubRefs) } - // TODO: handle indexes - listRes.Value[val] = value + listRes.setValueAt(int(val), value) } return nil } @@ -136,20 +139,6 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error { 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 { parentItem, err = r.evalSubRefs(ctx, item, parentItem, r.SubRefs[0:len(r.SubRefs)-1]) if err != nil { @@ -164,23 +153,20 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error { switch val := sv.(type) { case string: - mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM) + mapRes, isMapRes := parentItem.(modifiableMapExprValue) if !isMapRes { return newValueNotAMapError(r, r.SubRefs) } - delete(mapRes.Value, val) - case int: - listRes, isMapRes := parentItem.(*types.AttributeValueMemberL) + mapRes.deleteValueOf(val) + case int64: + listRes, isMapRes := parentItem.(modifiableSliceExprValue) if !isMapRes { return newValueNotAListError(r, r.SubRefs) } // TODO: handle indexes out of bounds - oldList := listRes.Value - newList := append([]types.AttributeValue{}, oldList[:val]...) - newList = append(newList, oldList[val+1:]...) - listRes.Value = newList + listRes.deleteValueAt(int(val)) } return nil } @@ -214,18 +200,10 @@ func (sr *astSubRefType) evalToStrOrInt(ctx *evalContext, item models.Item) (any return nil, err } switch v := subEvalItem.(type) { - case *types.AttributeValueMemberS: - return v.Value, nil - case *types.AttributeValueMemberN: - intVal, err := strconv.Atoi(v.Value) - if err == nil { - return intVal, nil - } - flVal, err := strconv.ParseFloat(v.Value, 64) - if err == nil { - return int(flVal), nil - } - return nil, err + case stringableExprValue: + return v.asString(), nil + case numberableExprValue: + return v.asInt(), nil } return nil, ValueNotUsableAsASubref{} } diff --git a/internal/dynamo-browse/models/queryexpr/types.go b/internal/dynamo-browse/models/queryexpr/types.go index 7175665..1a4106a 100644 --- a/internal/dynamo-browse/models/queryexpr/types.go +++ b/internal/dynamo-browse/models/queryexpr/types.go @@ -1 +1,400 @@ 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" +} diff --git a/internal/dynamo-browse/models/queryexpr/values.go b/internal/dynamo-browse/models/queryexpr/values.go index 4a81c45..9e97ba2 100644 --- a/internal/dynamo-browse/models/queryexpr/values.go +++ b/internal/dynamo-browse/models/queryexpr/values.go @@ -5,56 +5,31 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/models" "strconv" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/pkg/errors" ) func (a *astLiteralValue) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) { - v, err := a.goValue() + v, err := a.exprValue() if err != nil { return nil, err } return irValue{value: v}, nil } -func (a *astLiteralValue) dynamoValue() (types.AttributeValue, 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 - } - +func (a *astLiteralValue) exprValue() (exprValue, error) { switch { case a.StringVal != nil: s, err := strconv.Unquote(*a.StringVal) if err != nil { return nil, errors.Wrap(err, "cannot unquote string") } - return s, nil + return stringExprValue(s), nil case a.IntVal != nil: - return *a.IntVal, nil + return int64ExprValue(*a.IntVal), nil case a.TrueBoolValue: - return true, nil + return boolExprValue(true), nil case a.FalseBoolValue: - return false, nil + return boolExprValue(false), nil } return nil, errors.New("unrecognised type") } @@ -78,17 +53,17 @@ func (a *astLiteralValue) String() string { } type irValue struct { - value any + value exprValue } func (i irValue) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { return expression.ConditionBuilder{}, NodeCannotBeConvertedToQueryError{} } -func (i irValue) goValue() any { +func (i irValue) exprValue() exprValue { return i.value } func (a irValue) calcOperand(info *models.TableInfo) expression.OperandBuilder { - return expression.Value(a.goValue()) + return expression.Value(a.value.asGoValue()) } diff --git a/internal/dynamo-browse/services/scriptmanager/iface.go b/internal/dynamo-browse/services/scriptmanager/iface.go index a717057..35dce04 100644 --- a/internal/dynamo-browse/services/scriptmanager/iface.go +++ b/internal/dynamo-browse/services/scriptmanager/iface.go @@ -32,6 +32,7 @@ type SessionService interface { type QueryOptions struct { TableName string + IndexName string NamePlaceholders map[string]string ValuePlaceholders map[string]types.AttributeValue } diff --git a/internal/dynamo-browse/services/scriptmanager/modsession.go b/internal/dynamo-browse/services/scriptmanager/modsession.go index 034b73f..d8e65b6 100644 --- a/internal/dynamo-browse/services/scriptmanager/modsession.go +++ b/internal/dynamo-browse/services/scriptmanager/modsession.go @@ -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 if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap { options.NamePlaceholders = make(map[string]string) diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index ef5d5da..43cf146 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -144,6 +144,8 @@ func NewModel( itemType = models.BoolItemType case "-NULL": itemType = models.NullItemType + case "-TO": + itemType = models.ExprValueItemType default: return events.Error(errors.New("unrecognised item type")) } diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index 3b730a4..5665378 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -91,10 +91,14 @@ func main() { } } - key := gofakeit.UUID() + var key = gofakeit.UUID() for i := 0; i < totalItems; i++ { + if i%50 == 0 { + key = gofakeit.UUID() + } if err := tableService.Put(ctx, inventoryTableInfo, models.Item{ "pk": &types.AttributeValueMemberS{Value: key}, + "sk": &types.AttributeValueMemberN{Value: fmt.Sprint(i % 50)}, "uuid": &types.AttributeValueMemberS{Value: gofakeit.UUID()}, }); err != nil { log.Fatalln(err)