Added sub references to the query expression (#46)

These are the `thing[subref]` construct. Subrefs can either be a string or an integer.

At the moment, multiple sub references, such as `thing[1][2][3]` doesn't work. This is because the SDK does not properly handle this when creating the actual expression.
This commit is contained in:
Leon Mika 2023-02-16 21:57:40 +11:00 committed by GitHub
parent 348251c1cf
commit 7caf905c82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 270 additions and 61 deletions

View file

@ -55,8 +55,14 @@ type astIsOp struct {
}
type astSubRef struct {
Ref *astFunctionCall `parser:"@@"`
Quals []string `parser:"('.' @Ident)*"`
Ref *astFunctionCall `parser:"@@"`
SubRefs []*astSubRefType `parser:"@@*"`
//Quals []string `parser:"('.' @Ident)*"`
}
type astSubRefType struct {
DotQual string `parser:"'.' @Ident"`
SubIndex *astExpr `parser:"| '[' @@ ']'"`
}
type astFunctionCall struct {

View file

@ -1,9 +1,11 @@
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"
)
func (dt *astRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
@ -43,7 +45,7 @@ func (a *astRef) String() string {
type irNamePath struct {
name string
quals []string
quals []any
}
func (i irNamePath) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
@ -62,9 +64,16 @@ func (i irNamePath) keyName() string {
}
func (i irNamePath) calcName(info *models.TableInfo) expression.NameBuilder {
nb := expression.Name(i.name)
var fullName strings.Builder
fullName.WriteString(i.name)
for _, qual := range i.quals {
nb = nb.AppendName(expression.Name(qual))
switch v := qual.(type) {
case string:
fullName.WriteString("." + v)
case int:
fullName.WriteString(fmt.Sprintf("[%v]", qual))
}
}
return nb
return expression.NameNoDotSplit(fullName.String())
}

View file

@ -34,6 +34,13 @@ func (n ValueNotAMapError) Error() string {
return fmt.Sprintf("%v: name is not a map", strings.Join(n, "."))
}
// ValueNotAListError is return if the given name is not a map
type ValueNotAListError []string
func (n ValueNotAListError) Error() string {
return fmt.Sprintf("%v: name is not a list", strings.Join(n, "."))
}
// ValuesNotComparable indicates that two values are not comparable
type ValuesNotComparable struct {
Left, Right types.AttributeValue
@ -123,3 +130,10 @@ type MissingPlaceholderError struct {
func (e MissingPlaceholderError) Error() string {
return "undefined placeholder '" + e.Placeholder + "'"
}
type ValueNotUsableAsASubref struct {
}
func (e ValueNotUsableAsASubref) Error() string {
return "value cannot be used as a subref"
}

View file

@ -111,6 +111,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsString(1, 1, "sk", "another"),
),
// Querying the index
scanCase("when request pk is fixed",
`pk="prefix"`,
`#0 = :0`,
exprNameIsString(0, 0, "pk", "prefix"),
),
}
for _, scenario := range scenarios {
@ -271,17 +278,27 @@ func TestModExpr_Query(t *testing.T) {
exprValueIsNumber(0, "131"),
),
// Dots
scanCase("with the dot", `this.value = "something"`, `#0.#1 = :0`,
// Sub refs
scanCase("with index", `this[2] = "something"`, `#0[2] = :0`,
exprName(0, "this"),
exprName(1, "value"),
exprValueIsString(0, "something"),
),
scanCase("with multiple dots", `this.that.other.value = "else"`, `#0.#1.#2.#3 = :0`,
exprName(0, "this"),
exprName(1, "that"),
exprName(2, "other"),
exprName(3, "value"),
scanCase("with the dot", `this.value = "something"`, `#0 = :0`,
exprName(0, "this.value"),
exprValueIsString(0, "something"),
),
/*
scanCase("with multiple indices", `this[2][3] = "something"`, `#0[2][3] = :0`,
exprName(0, "this"),
exprValueIsString(0, "something"),
),
scanCase("with multiple indices with paren", `((this[2])[3])[4] = "something"`, `#0[2][3][4] = :0`,
exprName(0, "this"),
exprValueIsString(0, "something"),
),
*/
scanCase("with multiple dots", `this.that.other.value = "else"`, `#0 = :0`,
exprName(0, "this.that.other.value"),
exprValueIsString(0, "else"),
),

View file

@ -2,7 +2,9 @@ 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"
)
@ -11,7 +13,7 @@ func (r *astSubRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom,
if err != nil {
return nil, err
}
if len(r.Quals) == 0 {
if len(r.SubRefs) == 0 {
return refIR, nil
}
@ -21,9 +23,13 @@ func (r *astSubRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom,
return nil, OperandNotANameError(r.String())
}
quals := make([]string, 0)
for _, sr := range r.Quals {
quals = append(quals, sr)
quals := make([]any, 0)
for _, sr := range r.SubRefs {
sv, err := sr.evalToStrOrInt(ctx, nil)
if err != nil {
return nil, err
}
quals = append(quals, sv)
}
return irNamePath{name: namePath.name, quals: quals}, nil
}
@ -34,29 +40,52 @@ func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.Attribut
return nil, err
}
for i, qualName := range r.Quals {
var hasV bool
mapRes, isMapRes := res.(*types.AttributeValueMemberM)
if !isMapRes {
return nil, ValueNotAMapError(append([]string{r.Ref.String()}, r.Quals[:i+1]...))
}
res, hasV = mapRes.Value[qualName]
if !hasV {
return nil, nil
}
res, err = r.evalSubRefs(ctx, item, res, r.SubRefs)
if err != nil {
return nil, err
}
return res, nil
}
func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.AttributeValue, subRefs []*astSubRefType) (types.AttributeValue, error) {
for i, sr := range subRefs {
sv, err := sr.evalToStrOrInt(ctx, nil)
if err != nil {
return nil, err
}
switch val := sv.(type) {
case string:
var hasV bool
mapRes, isMapRes := res.(*types.AttributeValueMemberM)
if !isMapRes {
return nil, newValueNotAMapError(r, subRefs[:i+1])
}
res, hasV = mapRes.Value[val]
if !hasV {
return nil, nil
}
case int:
listRes, isMapRes := res.(*types.AttributeValueMemberL)
if !isMapRes {
return nil, newValueNotAListError(r, subRefs[:i+1])
}
// TODO - deal with index properly
res = listRes.Value[val]
}
}
return res, nil
}
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 {
if len(r.Quals) == 0 {
if len(r.SubRefs) == 0 {
return r.Ref.setEvalItem(ctx, item, value)
}
@ -65,23 +94,40 @@ func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value types.
return err
}
for i, key := range r.Quals {
mapItem, isMapItem := parentItem.(*types.AttributeValueMemberM)
if !isMapItem {
return PathNotSettableError{}
if len(r.SubRefs) > 1 {
parentItem, err = r.evalSubRefs(ctx, item, parentItem, r.SubRefs[0:len(r.SubRefs)-1])
if err != nil {
return err
}
}
sv, err := r.SubRefs[len(r.SubRefs)-1].evalToStrOrInt(ctx, nil)
if err != nil {
return err
}
switch val := sv.(type) {
case string:
mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM)
if !isMapRes {
return newValueNotAMapError(r, r.SubRefs)
}
if isLast := i == len(r.Quals)-1; isLast {
mapItem.Value[key] = value
} else {
parentItem = mapItem.Value[key]
mapRes.Value[val] = value
case int:
listRes, isMapRes := parentItem.(*types.AttributeValueMemberL)
if !isMapRes {
return newValueNotAListError(r, r.SubRefs)
}
// TODO: handle indexes
listRes.Value[val] = value
}
return nil
}
func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error {
if len(r.Quals) == 0 {
if len(r.SubRefs) == 0 {
return r.Ref.deleteAttribute(ctx, item)
}
@ -90,17 +136,51 @@ 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{}
/*
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 {
return err
}
}
sv, err := r.SubRefs[len(r.SubRefs)-1].evalToStrOrInt(ctx, nil)
if err != nil {
return err
}
switch val := sv.(type) {
case string:
mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM)
if !isMapRes {
return newValueNotAMapError(r, r.SubRefs)
}
if isLast := i == len(r.Quals)-1; isLast {
delete(mapItem.Value, key)
} else {
parentItem = mapItem.Value[key]
delete(mapRes.Value, val)
case int:
listRes, isMapRes := parentItem.(*types.AttributeValueMemberL)
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
}
return nil
}
@ -109,10 +189,67 @@ func (r *astSubRef) String() string {
var sb strings.Builder
sb.WriteString(r.Ref.String())
for _, q := range r.Quals {
sb.WriteRune('.')
sb.WriteString(q)
for _, q := range r.SubRefs {
switch {
case q.DotQual != "":
sb.WriteRune('.')
sb.WriteString(q.DotQual)
case q.SubIndex != nil:
sb.WriteRune('[')
sb.WriteString(q.SubIndex.String())
sb.WriteRune(']')
}
}
return sb.String()
}
func (sr *astSubRefType) evalToStrOrInt(ctx *evalContext, item models.Item) (any, error) {
if sr.DotQual != "" {
return sr.DotQual, nil
}
subEvalItem, err := sr.SubIndex.evalItem(ctx, item)
if err != nil {
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
}
return nil, ValueNotUsableAsASubref{}
}
func (sr *astSubRefType) string() string {
switch {
case sr.DotQual != "":
return sr.DotQual
case sr.SubIndex != nil:
return sr.SubIndex.String()
}
return ""
}
func newValueNotAMapError(r *astSubRef, subRefs []*astSubRefType) ValueNotAMapError {
subRefStrings := sliceutils.Map(subRefs, func(srt *astSubRefType) string {
return srt.string()
})
return ValueNotAMapError(append([]string{r.Ref.String()}, subRefStrings...))
}
func newValueNotAListError(r *astSubRef, subRefs []*astSubRefType) ValueNotAListError {
subRefStrings := sliceutils.Map(subRefs, func(srt *astSubRefType) string {
return srt.string()
})
return ValueNotAListError(append([]string{r.Ref.String()}, subRefStrings...))
}

View file

@ -4,6 +4,12 @@ type TableInfo struct {
Name string
Keys KeyAttribute
DefinedAttributes []string
GSIs []TableGSI
}
type TableGSI struct {
Name string
Keys KeyAttribute
}
func (ti *TableInfo) Equal(other *TableInfo) bool {