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:
		
							parent
							
								
									835ddd5630
								
							
						
					
					
						commit
						4b4d515ade
					
				|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -8,4 +8,6 @@ const ( | |||
| 	NumberItemType ItemType = "N" | ||||
| 	BoolItemType   ItemType = "BOOL" | ||||
| 	NullItemType   ItemType = "NULL" | ||||
| 
 | ||||
| 	ExprValueItemType ItemType = "exprvalue" | ||||
| ) | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										155
									
								
								internal/dynamo-browse/models/queryexpr/between.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								internal/dynamo-browse/models/queryexpr/between.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
|  | @ -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{} | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 | ||||
| 	}, | ||||
| } | ||||
|  |  | |||
|  | @ -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") | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
							
								
								
									
										27
									
								
								internal/dynamo-browse/models/queryexpr/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								internal/dynamo-browse/models/queryexpr/context.go
									
									
									
									
									
										Normal 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{} | ||||
| } | ||||
|  | @ -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) | ||||
| 	} | ||||
|  |  | |||
|  | @ -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)) | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
|  | @ -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{} | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
							
								
								
									
										16
									
								
								internal/dynamo-browse/models/queryexpr/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								internal/dynamo-browse/models/queryexpr/helpers_test.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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{} | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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{} | ||||
| } | ||||
|  |  | |||
|  | @ -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" | ||||
| } | ||||
|  |  | |||
|  | @ -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()) | ||||
| } | ||||
|  |  | |||
|  | @ -32,6 +32,7 @@ type SessionService interface { | |||
| 
 | ||||
| type QueryOptions struct { | ||||
| 	TableName         string | ||||
| 	IndexName         string | ||||
| 	NamePlaceholders  map[string]string | ||||
| 	ValuePlaceholders map[string]types.AttributeValue | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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")) | ||||
| 					} | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue