diff --git a/internal/dynamo-browse/models/queryexpr/ast.go b/internal/dynamo-browse/models/queryexpr/ast.go index d8c9912..30c33fe 100644 --- a/internal/dynamo-browse/models/queryexpr/ast.go +++ b/internal/dynamo-browse/models/queryexpr/ast.go @@ -2,11 +2,27 @@ package queryexpr import ( "github.com/alecthomas/participle/v2" + "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/pkg/errors" ) +// Modelled on the expression language here +// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html + type astExpr struct { - Equality *astBinOp `parser:"@@"` + Root *astDisjunction `parser:"@@"` +} + +func (a *astExpr) evalToIR(tableInfo *models.TableInfo) (*irDisjunction, error) { + return a.Root.evalToIR(tableInfo) +} + +type astDisjunction struct { + Operands []*astConjunction `parser:"@@ ('or' @@)*"` +} + +type astConjunction struct { + Operands []*astBinOp `parser:"@@ ('and' @@)*"` } type astBinOp struct { diff --git a/internal/dynamo-browse/models/queryexpr/binops.go b/internal/dynamo-browse/models/queryexpr/binops.go new file mode 100644 index 0000000..976fbd6 --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/binops.go @@ -0,0 +1,81 @@ +package queryexpr + +import ( + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/pkg/errors" +) + +func (a *astBinOp) evalToIR(info *models.TableInfo) (irAtom, error) { + v, err := a.Value.goValue() + if err != nil { + return nil, err + } + + switch a.Op { + case "=": + return irFieldEq{name: a.Name, value: v}, nil + case "^=": + strValue, isStrValue := v.(string) + if !isStrValue { + return nil, errors.New("operand '^=' must be string") + } + return irFieldBeginsWith{name: a.Name, prefix: strValue}, nil + } + + return nil, errors.Errorf("unrecognised operator: %v", a.Op) +} + +func (a *astBinOp) String() string { + return a.Name + a.Op + a.Value.String() +} + +type irFieldEq struct { + name string + value any +} + +func (a irFieldEq) canBeExecutedAsQuery(info *models.TableInfo, qci *queryCalcInfo) bool { + if a.name == info.Keys.PartitionKey || a.name == info.Keys.SortKey { + return qci.addKey(info, a.name) + } + + return false +} + +func (a irFieldEq) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { + return expression.Name(a.name).Equal(expression.Value(a.value)), nil +} + +func (a irFieldEq) calcQueryForQuery(info *models.TableInfo) (expression.KeyConditionBuilder, error) { + return expression.Key(a.name).Equal(expression.Value(a.value)), nil +} + +func (a irFieldEq) operandFieldName() string { + return a.name +} + +type irFieldBeginsWith struct { + name string + prefix string +} + +func (a irFieldBeginsWith) canBeExecutedAsQuery(info *models.TableInfo, qci *queryCalcInfo) bool { + if a.name == info.Keys.SortKey { + return qci.addKey(info, a.name) + } + + return false +} + +func (a irFieldBeginsWith) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { + return expression.Name(a.name).BeginsWith(a.prefix), nil +} + +func (a irFieldBeginsWith) calcQueryForQuery(info *models.TableInfo) (expression.KeyConditionBuilder, error) { + return expression.Key(a.name).BeginsWith(a.prefix), nil +} + +func (a irFieldBeginsWith) operandFieldName() string { + return a.name +} diff --git a/internal/dynamo-browse/models/queryexpr/calcquery.go b/internal/dynamo-browse/models/queryexpr/calcquery.go index 89629bc..45e3986 100644 --- a/internal/dynamo-browse/models/queryexpr/calcquery.go +++ b/internal/dynamo-browse/models/queryexpr/calcquery.go @@ -3,15 +3,30 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/pkg/errors" ) -func (a *astExpr) calcQuery(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) { - return a.Equality.calcQuery(tableInfo) -} +func (a *irDisjunction) calcQuery(info *models.TableInfo) (*models.QueryExecutionPlan, error) { + var qci queryCalcInfo + if a.canBeExecutedAsQuery(info, &qci) { + ke, err := a.calcQueryForQuery(info) + if err != nil { + return nil, err + } + + builder := expression.NewBuilder() + builder = builder.WithKeyCondition(ke) + + expr, err := builder.Build() + if err != nil { + return nil, err + } + + return &models.QueryExecutionPlan{ + CanQuery: true, + Expression: expr, + }, nil + } -func (a *astBinOp) calcQuery(info *models.TableInfo) (*models.QueryExecutionPlan, error) { - // TODO: check if can be a query cb, err := a.calcQueryForScan(info) if err != nil { return nil, err @@ -30,23 +45,3 @@ func (a *astBinOp) calcQuery(info *models.TableInfo) (*models.QueryExecutionPlan Expression: expr, }, nil } - -func (a *astBinOp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { - v, err := a.Value.goValue() - if err != nil { - return expression.ConditionBuilder{}, err - } - - switch a.Op { - case "=": - return expression.Name(a.Name).Equal(expression.Value(v)), nil - case "^=": - strValue, isStrValue := v.(string) - if !isStrValue { - return expression.ConditionBuilder{}, errors.New("operand '^=' must be string") - } - return expression.Name(a.Name).BeginsWith(strValue), nil - } - - return expression.ConditionBuilder{}, errors.Errorf("unrecognised operator: %v", a.Op) -} diff --git a/internal/dynamo-browse/models/queryexpr/conj.go b/internal/dynamo-browse/models/queryexpr/conj.go new file mode 100644 index 0000000..67841c9 --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/conj.go @@ -0,0 +1,89 @@ +package queryexpr + +import ( + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/pkg/errors" + "strings" +) + +func (a *astConjunction) evalToIR(tableInfo *models.TableInfo) (*irConjunction, error) { + atoms := make([]irAtom, len(a.Operands)) + for i, op := range a.Operands { + var err error + atoms[i], err = op.evalToIR(tableInfo) + if err != nil { + return nil, err + } + } + + return &irConjunction{atoms: atoms}, nil +} + +func (d *astConjunction) String() string { + sb := new(strings.Builder) + for i, operand := range d.Operands { + if i > 0 { + sb.WriteString(" and ") + } + sb.WriteString(operand.String()) + } + return sb.String() +} + +type irConjunction struct { + atoms []irAtom +} + +func (d *irConjunction) canBeExecutedAsQuery(info *models.TableInfo, qci *queryCalcInfo) bool { + switch len(d.atoms) { + case 1: + return d.atoms[0].operandFieldName() == info.Keys.PartitionKey && d.atoms[0].canBeExecutedAsQuery(info, qci) + case 2: + return d.atoms[0].canBeExecutedAsQuery(info, qci) && d.atoms[1].canBeExecutedAsQuery(info, qci) + } + return false +} + +func (d *irConjunction) calcQueryForQuery(info *models.TableInfo) (expression.KeyConditionBuilder, error) { + if len(d.atoms) == 1 { + return d.atoms[0].calcQueryForQuery(info) + } else if len(d.atoms) != 2 { + return expression.KeyConditionBuilder{}, errors.Errorf("internal error: expected len to be either 1 or 2, but was %v", len(d.atoms)) + } + + left, err := d.atoms[0].calcQueryForQuery(info) + if err != nil { + return expression.KeyConditionBuilder{}, err + } + + right, err := d.atoms[1].calcQueryForQuery(info) + if err != nil { + return expression.KeyConditionBuilder{}, err + } + + if d.atoms[0].operandFieldName() == info.Keys.PartitionKey { + return expression.KeyAnd(left, right), nil + } + return expression.KeyAnd(right, left), nil +} + +func (d *irConjunction) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { + if len(d.atoms) == 1 { + return d.atoms[0].calcQueryForScan(info) + } + + // TODO: check if can be query + conds := make([]expression.ConditionBuilder, len(d.atoms)) + for i, operand := range d.atoms { + cond, err := operand.calcQueryForScan(info) + if err != nil { + return expression.ConditionBuilder{}, err + } + conds[i] = cond + } + + // Build conjunction + conjExpr := expression.And(conds[0], conds[1], conds[2:]...) + return conjExpr, nil +} diff --git a/internal/dynamo-browse/models/queryexpr/disj.go b/internal/dynamo-browse/models/queryexpr/disj.go new file mode 100644 index 0000000..66bb20c --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/disj.go @@ -0,0 +1,72 @@ +package queryexpr + +import ( + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/pkg/errors" + "strings" +) + +func (a *astDisjunction) evalToIR(tableInfo *models.TableInfo) (*irDisjunction, error) { + conj := make([]*irConjunction, len(a.Operands)) + for i, op := range a.Operands { + var err error + conj[i], err = op.evalToIR(tableInfo) + if err != nil { + return nil, err + } + } + + return &irDisjunction{conj: conj}, nil +} + +func (d *astDisjunction) String() string { + sb := new(strings.Builder) + for i, operand := range d.Operands { + if i > 0 { + sb.WriteString(" or ") + } + sb.WriteString(operand.String()) + } + return sb.String() +} + +type irDisjunction struct { + conj []*irConjunction +} + +func (d *irDisjunction) canBeExecutedAsQuery(info *models.TableInfo, qci *queryCalcInfo) bool { + // TODO: not entire accurate, as filter expressions are also possible + if len(d.conj) == 1 { + return d.conj[0].canBeExecutedAsQuery(info, qci) + } + return false +} + +func (d *irDisjunction) calcQueryForQuery(info *models.TableInfo) (expression.KeyConditionBuilder, error) { + if len(d.conj) == 1 { + return d.conj[0].calcQueryForQuery(info) + } + + return expression.KeyConditionBuilder{}, errors.New("expected exactly 1 operand for query") +} + +func (d *irDisjunction) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { + if len(d.conj) == 1 { + return d.conj[0].calcQueryForScan(info) + } + + // TODO: check if can be query + conds := make([]expression.ConditionBuilder, len(d.conj)) + for i, operand := range d.conj { + cond, err := operand.calcQueryForScan(info) + if err != nil { + return expression.ConditionBuilder{}, err + } + conds[i] = cond + } + + // Build disjunction + disjExpr := expression.Or(conds[0], conds[1], conds[2:]...) + return disjExpr, nil +} diff --git a/internal/dynamo-browse/models/queryexpr/expr.go b/internal/dynamo-browse/models/queryexpr/expr.go index 88effdc..b3736d7 100644 --- a/internal/dynamo-browse/models/queryexpr/expr.go +++ b/internal/dynamo-browse/models/queryexpr/expr.go @@ -7,9 +7,38 @@ type QueryExpr struct { } func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) { - return md.ast.calcQuery(tableInfo) + ir, err := md.ast.evalToIR(tableInfo) + if err != nil { + return nil, err + } + + return ir.calcQuery(tableInfo) } func (md *QueryExpr) String() string { return md.ast.String() } + +func (a *astExpr) String() string { + return a.Root.String() +} + +type queryCalcInfo struct { + seenKeys map[string]struct{} +} + +func (qc *queryCalcInfo) addKey(tableInfo *models.TableInfo, key string) bool { + if tableInfo.Keys.PartitionKey != key && tableInfo.Keys.SortKey != key { + return false + } + + if qc.seenKeys == nil { + qc.seenKeys = make(map[string]struct{}) + } + if _, hasSeenKey := qc.seenKeys[key]; hasSeenKey { + return false + } + + qc.seenKeys[key] = struct{}{} + return true +} diff --git a/internal/dynamo-browse/models/queryexpr/expr_test.go b/internal/dynamo-browse/models/queryexpr/expr_test.go index 6e308ff..b8b8d2c 100644 --- a/internal/dynamo-browse/models/queryexpr/expr_test.go +++ b/internal/dynamo-browse/models/queryexpr/expr_test.go @@ -19,29 +19,116 @@ func TestModExpr_Query(t *testing.T) { }, } - t.Run("perform query when request pk is fixed", func(t *testing.T) { - modExpr, err := queryexpr.Parse(`pk="prefix"`) - assert.NoError(t, err) + t.Run("as queries", func(t *testing.T) { + t.Run("perform query when request pk is fixed", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`pk="prefix"`) + assert.NoError(t, err) - plan, err := modExpr.Plan(tableInfo) - assert.NoError(t, err) + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) - assert.False(t, plan.CanQuery) - assert.Equal(t, "#0 = :0", aws.ToString(plan.Expression.Filter())) - assert.Equal(t, "pk", plan.Expression.Names()["#0"]) - assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value) + assert.True(t, plan.CanQuery) + assert.Equal(t, "#0 = :0", aws.ToString(plan.Expression.KeyCondition())) + assert.Equal(t, "pk", plan.Expression.Names()["#0"]) + assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value) + }) + + t.Run("perform query when request pk and sk is fixed", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`pk="prefix" and sk="another"`) + assert.NoError(t, err) + + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) + + assert.True(t, plan.CanQuery) + assert.Equal(t, "(#0 = :0) AND (#1 = :1)", aws.ToString(plan.Expression.KeyCondition())) + assert.Equal(t, "pk", plan.Expression.Names()["#0"]) + assert.Equal(t, "sk", plan.Expression.Names()["#1"]) + assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another", plan.Expression.Values()[":1"].(*types.AttributeValueMemberS).Value) + }) + + t.Run("perform query when request pk is equals and sk is prefix", func(t *testing.T) { + scenarios := []struct { + expr string + }{ + {expr: `pk="prefix" and sk^="another"`}, + {expr: `sk^="another" and pk="prefix"`}, + } + + for _, scenario := range scenarios { + t.Run(scenario.expr, func(t *testing.T) { + modExpr, err := queryexpr.Parse(scenario.expr) + assert.NoError(t, err) + + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) + + assert.True(t, plan.CanQuery) + assert.Equal(t, "(#0 = :0) AND (begins_with (#1, :1))", aws.ToString(plan.Expression.KeyCondition())) + assert.Equal(t, "pk", plan.Expression.Names()["#0"]) + assert.Equal(t, "sk", plan.Expression.Names()["#1"]) + assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another", plan.Expression.Values()[":1"].(*types.AttributeValueMemberS).Value) + }) + } + }) }) - t.Run("perform scan when request pk prefix", func(t *testing.T) { - modExpr, err := queryexpr.Parse(`pk^="prefix"`) // TODO: fix this so that '^ =' is invalid - assert.NoError(t, err) + t.Run("as scans", func(t *testing.T) { + t.Run("when request pk prefix", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`pk^="prefix"`) // TODO: fix this so that '^ =' is invalid + assert.NoError(t, err) - plan, err := modExpr.Plan(tableInfo) - assert.NoError(t, err) + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) - assert.False(t, plan.CanQuery) - assert.Equal(t, "begins_with (#0, :0)", aws.ToString(plan.Expression.Filter())) - assert.Equal(t, "pk", plan.Expression.Names()["#0"]) - assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value) + assert.False(t, plan.CanQuery) + assert.Equal(t, "begins_with (#0, :0)", aws.ToString(plan.Expression.Filter())) + assert.Equal(t, "pk", plan.Expression.Names()["#0"]) + assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value) + }) + + t.Run("when request sk equals something", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`sk="something"`) + assert.NoError(t, err) + + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) + + assert.False(t, plan.CanQuery) + assert.Equal(t, "#0 = :0", aws.ToString(plan.Expression.Filter())) + assert.Equal(t, "sk", plan.Expression.Names()["#0"]) + assert.Equal(t, "something", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value) + }) + + t.Run("with disjunctions", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`pk="prefix" or sk="another"`) + assert.NoError(t, err) + + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) + + assert.False(t, plan.CanQuery) + assert.Equal(t, "(#0 = :0) OR (#1 = :1)", aws.ToString(plan.Expression.Filter())) + assert.Equal(t, "pk", plan.Expression.Names()["#0"]) + assert.Equal(t, "sk", plan.Expression.Names()["#1"]) + assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another", plan.Expression.Values()[":1"].(*types.AttributeValueMemberS).Value) + }) + + t.Run("with disjunctions if pk is present twice in expression", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`pk="prefix" and pk="another"`) + assert.NoError(t, err) + + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) + + assert.False(t, plan.CanQuery) + assert.Equal(t, "(#0 = :0) AND (#0 = :1)", aws.ToString(plan.Expression.Filter())) + assert.Equal(t, "pk", plan.Expression.Names()["#0"]) + assert.Equal(t, "prefix", plan.Expression.Values()[":0"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another", plan.Expression.Values()[":1"].(*types.AttributeValueMemberS).Value) + }) }) } diff --git a/internal/dynamo-browse/models/queryexpr/ir.go b/internal/dynamo-browse/models/queryexpr/ir.go new file mode 100644 index 0000000..b42550f --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/ir.go @@ -0,0 +1,22 @@ +package queryexpr + +import ( + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/lmika/audax/internal/dynamo-browse/models" +) + +type irAtom interface { + // operandFieldName returns the field that this atom operates on. For example, + // if this IR node represents 'a = "b"', this should return "a". + // If this does not operate on a definitive field name, this returns null + operandFieldName() string + + // canBeExecutedAsQuery returns true if the atom is capable of being executed as a query + canBeExecutedAsQuery(info *models.TableInfo, qci *queryCalcInfo) bool + + // calcQueryForQuery returns a key condition builder for this atom to include in a query + calcQueryForQuery(info *models.TableInfo) (expression.KeyConditionBuilder, error) + + // calcQueryForScan returns the condition builder for this atom to include in a scan + calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) +} diff --git a/internal/dynamo-browse/models/queryexpr/tostr.go b/internal/dynamo-browse/models/queryexpr/tostr.go deleted file mode 100644 index 9453954..0000000 --- a/internal/dynamo-browse/models/queryexpr/tostr.go +++ /dev/null @@ -1,13 +0,0 @@ -package queryexpr - -func (a *astExpr) String() string { - return a.Equality.String() -} - -func (a *astBinOp) String() string { - return a.Name + a.Op + a.Value.String() -} - -func (a *astLiteralValue) String() string { - return a.StringVal -} diff --git a/internal/dynamo-browse/models/queryexpr/values.go b/internal/dynamo-browse/models/queryexpr/values.go index 8bb0e81..94aa5cf 100644 --- a/internal/dynamo-browse/models/queryexpr/values.go +++ b/internal/dynamo-browse/models/queryexpr/values.go @@ -22,3 +22,7 @@ func (a *astLiteralValue) goValue() (any, error) { } return s, nil } + +func (a *astLiteralValue) String() string { + return a.StringVal +} diff --git a/internal/dynamo-browse/providers/dynamo/provider.go b/internal/dynamo-browse/providers/dynamo/provider.go index 5e3e7a1..9ecc82f 100644 --- a/internal/dynamo-browse/providers/dynamo/provider.go +++ b/internal/dynamo-browse/providers/dynamo/provider.go @@ -127,6 +127,40 @@ outer: return items, nil } +func (p *Provider) QueryItems(ctx context.Context, tableName string, filterExpr *expression.Expression, maxItems int) ([]models.Item, error) { + input := &dynamodb.QueryInput{ + TableName: aws.String(tableName), + Limit: aws.Int32(int32(maxItems)), + } + if filterExpr != nil { + input.KeyConditionExpression = filterExpr.KeyCondition() + input.FilterExpression = filterExpr.Filter() + input.ExpressionAttributeNames = filterExpr.Names() + input.ExpressionAttributeValues = filterExpr.Values() + } + + paginator := dynamodb.NewQueryPaginator(p.client, input) + + items := make([]models.Item, 0) + +outer: + for paginator.HasMorePages() { + res, err := paginator.NextPage(ctx) + if err != nil { + return nil, errors.Wrapf(err, "cannot execute query on table %v", tableName) + } + + for _, itm := range res.Items { + items = append(items, itm) + if len(items) >= maxItems { + break outer + } + } + } + + return items, nil +} + func (p *Provider) DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error { _, err := p.client.DeleteItem(ctx, &dynamodb.DeleteItemInput{ TableName: aws.String(tableName), diff --git a/internal/dynamo-browse/services/tables/iface.go b/internal/dynamo-browse/services/tables/iface.go index 2d85f74..61d7ba3 100644 --- a/internal/dynamo-browse/services/tables/iface.go +++ b/internal/dynamo-browse/services/tables/iface.go @@ -11,6 +11,7 @@ import ( type TableProvider interface { ListTables(ctx context.Context) ([]string, error) DescribeTable(ctx context.Context, tableName string) (*models.TableInfo, error) + QueryItems(ctx context.Context, tableName string, filterExpr *expression.Expression, maxItems int) ([]models.Item, error) ScanItems(ctx context.Context, tableName string, filterExpr *expression.Expression, maxItems int) ([]models.Item, error) DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error PutItem(ctx context.Context, name string, item models.Item) error diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index 9b2f8f2..edf1f10 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -4,6 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/lmika/audax/internal/common/sliceutils" + "log" "strings" "github.com/lmika/audax/internal/dynamo-browse/models" @@ -33,66 +34,39 @@ func (s *Service) Scan(ctx context.Context, tableInfo *models.TableInfo) (*model } func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr models.Queryable) (*models.ResultSet, error) { - var filterExpr *expression.Expression - + var ( + filterExpr *expression.Expression + runAsQuery bool + err error + ) if expr != nil { plan, err := expr.Plan(tableInfo) if err != nil { return nil, err } - // TEMP - if plan.CanQuery { - return nil, errors.Errorf("queries not yet supported") - } - + runAsQuery = plan.CanQuery filterExpr = &plan.Expression } - results, err := s.provider.ScanItems(ctx, tableInfo.Name, filterExpr, 1000) + var results []models.Item + if runAsQuery { + log.Printf("executing query") + results, err = s.provider.QueryItems(ctx, tableInfo.Name, filterExpr, 1000) + } else { + log.Printf("executing scan") + results, err = s.provider.ScanItems(ctx, tableInfo.Name, filterExpr, 1000) + } + if err != nil { return nil, errors.Wrapf(err, "unable to scan table %v", tableInfo.Name) } - // Get the columns - //seenColumns := make(map[string]int) - //seenColumns[tableInfo.Keys.PartitionKey] = 0 - //if tableInfo.Keys.SortKey != "" { - // seenColumns[tableInfo.Keys.SortKey] = 1 - //} - // - //for _, definedAttribute := range tableInfo.DefinedAttributes { - // if _, seen := seenColumns[definedAttribute]; !seen { - // seenColumns[definedAttribute] = len(seenColumns) - // } - //} - // - //otherColsRank := len(seenColumns) - //for _, result := range results { - // for k := range result { - // if _, isSeen := seenColumns[k]; !isSeen { - // seenColumns[k] = otherColsRank - // } - // } - //} - // - //columns := make([]string, 0, len(seenColumns)) - //for k := range seenColumns { - // columns = append(columns, k) - //} - //sort.Slice(columns, func(i, j int) bool { - // if seenColumns[columns[i]] == seenColumns[columns[j]] { - // return columns[i] < columns[j] - // } - // return seenColumns[columns[i]] < seenColumns[columns[j]] - //}) - models.Sort(results, tableInfo) resultSet := &models.ResultSet{ TableInfo: tableInfo, Query: expr, - //Columns: columns, } resultSet.SetItems(results) resultSet.RefreshColumns()