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:
parent
348251c1cf
commit
7caf905c82
8 changed files with 270 additions and 61 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -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...))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue