dynamo-browse/internal/dynamo-browse/models/queryexpr/in.go
Leon Mika 4b4d515ade
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.
2023-04-14 15:35:43 +10:00

281 lines
7.3 KiB
Go

package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors"
"strings"
)
func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
leftIR, err := a.Ref.evalToIR(ctx, info)
if err != nil {
return nil, err
}
if len(a.Operand) == 0 && a.SingleOperand == nil {
return leftIR, nil
}
var ir irAtom
switch {
case len(a.Operand) > 0:
nameIR, isNameIR := leftIR.(irNamePath)
if !isNameIR {
return nil, OperandNotANameError(a.Ref.String())
}
oprValues := make([]oprIRAtom, len(a.Operand))
for i, o := range a.Operand {
v, err := o.evalToIR(ctx, info)
if err != nil {
return nil, err
}
valueIR, isValueIR := v.(oprIRAtom)
if !isValueIR {
return nil, errors.Wrapf(ValueMustBeLiteralError{}, "'in' operand %v", i)
}
oprValues[i] = valueIR
}
// If there is a single operand value, and the name is either the partition or sort key, then
// convert this to an equality so that it could be run as a query
if len(oprValues) == 1 && (nameIR.keyName() == info.Keys.PartitionKey || nameIR.keyName() == info.Keys.SortKey) {
if a.HasNot {
return irFieldNe{name: nameIR, value: oprValues[0]}, nil
}
if valueIR, isValueIR := oprValues[0].(valueIRAtom); isValueIR {
return irKeyFieldEq{name: nameIR, value: valueIR}, nil
}
return irGenericEq{name: nameIR, value: oprValues[0]}, nil
}
ir = irIn{name: nameIR, values: oprValues}
case a.SingleOperand != nil:
oprs, err := a.SingleOperand.evalToIR(ctx, info)
if err != nil {
return nil, err
}
switch t := oprs.(type) {
case irNamePath:
lit, isLit := leftIR.(valueIRAtom)
if !isLit {
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 {
return nil, OperandNotANameError(a.Ref.String())
}
ir = irIn{name: nameIR, values: []oprIRAtom{t}}
default:
return nil, OperandNotAnOperandError{}
}
}
if a.HasNot {
return &irBoolNot{atom: ir}, nil
}
return ir, nil
}
func (a *astIn) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Ref.evalItem(ctx, item)
if err != nil {
return nil, err
}
if len(a.Operand) == 0 && a.SingleOperand == nil {
return val, nil
}
switch {
case len(a.Operand) > 0:
for _, opr := range a.Operand {
evalOp, err := opr.evalItem(ctx, item)
if err != nil {
return nil, err
}
// TODO: use native types here
cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), evalOp.asAttributeValue())
if !isComparable {
continue
} else if cmp == 0 {
return boolExprValue(true), nil
}
}
return boolExprValue(false), nil
case a.SingleOperand != nil:
evalOp, err := a.SingleOperand.evalItem(ctx, item)
if err != nil {
return nil, err
}
switch t := evalOp.(type) {
case stringableExprValue:
str, canToStr := val.(stringableExprValue)
if !canToStr {
return boolExprValue(false), nil
}
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 boolExprValue(true), nil
}
}
return boolExprValue(false), nil
case mappableExprValue:
str, canToStr := val.(stringableExprValue)
if !canToStr {
return boolExprValue(false), nil
}
hasKey := t.hasKey(str.asString())
return boolExprValue(hasKey), nil
}
return nil, ValuesNotInnableError{Val: evalOp.asAttributeValue()}
}
return nil, errors.New("internal error: unhandled 'in' case")
}
func (a *astIn) canModifyItem(ctx *evalContext, item models.Item) bool {
if len(a.Operand) != 0 || a.SingleOperand != nil {
return false
}
return a.Ref.canModifyItem(ctx, item)
}
func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if len(a.Operand) != 0 || a.SingleOperand != nil {
return PathNotSettableError{}
}
return a.Ref.setEvalItem(ctx, item, value)
}
func (a *astIn) deleteAttribute(ctx *evalContext, item models.Item) error {
if len(a.Operand) != 0 || a.SingleOperand != nil {
return PathNotSettableError{}
}
return a.Ref.deleteAttribute(ctx, item)
}
func (a *astIn) String() string {
if len(a.Operand) == 0 && a.SingleOperand == nil {
return a.Ref.String()
}
var sb strings.Builder
sb.WriteString(a.Ref.String())
if a.HasNot {
sb.WriteString(" not in ")
} else {
sb.WriteString(" in ")
}
switch {
case len(a.Operand) > 0:
sb.WriteString("(")
for i, o := range a.Operand {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(o.String())
}
sb.WriteString(")")
case a.SingleOperand != nil:
sb.WriteString(a.SingleOperand.String())
}
return sb.String()
}
type irIn struct {
name nameIRAtom
values []oprIRAtom
}
func (i irIn) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
right := i.values[0].calcOperand(info)
others := sliceutils.Map(i.values[1:], func(x oprIRAtom) expression.OperandBuilder {
return x.calcOperand(info)
})
return i.name.calcName(info).In(right, others...), nil
}
type irLiteralValues struct {
name nameIRAtom
values valueIRAtom
}
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
}
return iv.name.calcName(info).In(buildExpressionFromValue(iv.values.exprValue())), nil
}
type irContains struct {
needle valueIRAtom
haystack nameIRAtom
}
func (i irContains) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
strNeedle, isString := i.needle.exprValue().(stringableExprValue)
if !isString {
return expression.ConditionBuilder{}, errors.New("value cannot be converted to string")
}
haystack := i.haystack.calcName(info)
return haystack.Contains(strNeedle.asString()), nil
}