From 54fab1b1c31956a9a86e04d6195e4b041ef3e625 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 21 Jun 2022 13:37:07 +1000 Subject: [PATCH] dynamo-query: started working on queries --- go.mod | 19 +++---- go.sum | 21 ++++++++ internal/dynamo-browse/controllers/iface.go | 1 + .../dynamo-browse/controllers/tableread.go | 23 ++++++++ .../controllers/tableread_test.go | 34 ++++++++++++ internal/dynamo-browse/models/query.go | 8 +++ .../dynamo-browse/models/queryexpr/ast.go | 32 ++++++++++++ .../models/queryexpr/astquery.go | 52 +++++++++++++++++++ .../dynamo-browse/models/queryexpr/expr.go | 11 ++++ .../models/queryexpr/expr_test.go | 47 +++++++++++++++++ .../dynamo-browse/models/queryexpr/values.go | 24 +++++++++ .../providers/dynamo/provider.go | 15 ++++-- .../dynamo-browse/services/tables/iface.go | 3 +- .../dynamo-browse/services/tables/service.go | 27 +++++++++- internal/dynamo-browse/ui/model.go | 4 +- 15 files changed, 305 insertions(+), 16 deletions(-) create mode 100644 internal/dynamo-browse/models/query.go create mode 100644 internal/dynamo-browse/models/queryexpr/ast.go create mode 100644 internal/dynamo-browse/models/queryexpr/astquery.go create mode 100644 internal/dynamo-browse/models/queryexpr/expr.go create mode 100644 internal/dynamo-browse/models/queryexpr/expr_test.go create mode 100644 internal/dynamo-browse/models/queryexpr/values.go diff --git a/go.mod b/go.mod index fa569c3..6b98c88 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,11 @@ go 1.18 require ( github.com/alecthomas/participle/v2 v2.0.0-alpha7 github.com/asdine/storm v2.1.2+incompatible - github.com/aws/aws-sdk-go-v2 v1.16.1 + github.com/aws/aws-sdk-go-v2 v1.16.5 github.com/aws/aws-sdk-go-v2/config v1.13.1 github.com/aws/aws-sdk-go-v2/credentials v1.8.0 - github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.9.4 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.7 github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 github.com/brianvoe/gofakeit/v6 v6.15.0 github.com/charmbracelet/bubbles v0.11.0 @@ -25,18 +25,19 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect - github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.6 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect - github.com/aws/smithy-go v1.11.2 // indirect + github.com/aws/smithy-go v1.11.3 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/go.sum b/go.sum index b0ee31b..175f3fe 100644 --- a/go.sum +++ b/go.sum @@ -12,12 +12,18 @@ github.com/aws/aws-sdk-go-v2 v1.15.0 h1:f9kWLNfyCzCB43eupDAk3/XgJ2EpgktiySD6leqs github.com/aws/aws-sdk-go-v2 v1.15.0/go.mod h1:lJYcuZZEHWNIb6ugJjbQY1fykdoobWbOS7kJYb4APoI= github.com/aws/aws-sdk-go-v2 v1.16.1 h1:udzee98w8H6ikRgtFdVN9JzzYEbi/quFfSvduZETJIU= github.com/aws/aws-sdk-go-v2 v1.16.1/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= +github.com/aws/aws-sdk-go-v2 v1.16.5 h1:Ah9h1TZD9E2S1LzHpViBO3Jz9FPL5+rmflmb8hXirtI= +github.com/aws/aws-sdk-go-v2 v1.16.5/go.mod h1:Wh7MEsmEApyL5hrWzpDkba4gwAPc5/piwLVLFnCxp48= github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo= github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs= github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o= github.com/aws/aws-sdk-go-v2/credentials v1.8.0/go.mod h1:gnMo58Vwx3Mu7hj1wpcG8DI0s57c9o42UQ6wgTQT5to= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0 h1:XxTy21xVUkoCZOSGwf+AW22v8aK3eEbYMaGGQ3MbKKk= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0/go.mod h1:6WkjzWenkrj3IgLPIPBBz4Qh99jNDF8L4Wj03vfMhAA= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.9.4 h1:EoyeSOfbSuKh+bQIDoZaVJjON6PF+dsSn5w1RhIpMD0= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.9.4/go.mod h1:bfCL7OwZS6owS06pahfGxhcgpLWj2W1sQASoYRuenag= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.10 h1:IBIZfpnWCTTQhH/bMvDcCMw10BtLBPYO30Ev8MLXMTY= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.10/go.mod h1:RL7aJOwlWj2N6wkE4nKR1S5M4iGph+xSu7JovwNYpyU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7yAHj7kf+XPE+EiKuCBNUI= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4 h1:CRiQJ4E2RhfDdqbie1ZYDo8QtIo75Mk7oTdJSfwJTMQ= @@ -26,22 +32,34 @@ github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 h1:xiGjGVQsem2cxoIX61 github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6/go.mod h1:SSPEdf9spsFgJyhjrXvawfpyzrXHBCUe+2eQ1CjC1Ak= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8 h1:CDaO90VZVBAL1sK87S5oSPIrp7yZqORv1hPIi2UsTMk= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8/go.mod h1:LnTQMTqbKsbtt+UI5+wPsB7jedW+2ZgozoPG8k6cMxg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12 h1:Zt7DDk5V7SyQULUUwIKzsROtVzp/kVvcz15uQx/Tkow= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12/go.mod h1:Afj/U8svX6sJ77Q+FPWMzabJ9QjbwP32YlopgKALUpg= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0 h1:3ADoioDMOtF4uiK59vCpplpCwugEU+v4ZFD29jDL3RQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 h1:bt3zw79tm209glISdMRCIVRCwvSDXxgAxh5KWe2qHkY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0/go.mod h1:viTrxhAuejD+LszDahzAE2x40YjYWhMqzHxv2ZiWaME= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2 h1:XXR3cdOcKRCTZf6ctcqpMf+go1BdzTm6+T9Ul5zxcMI= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2/go.mod h1:1x4ZP3Z8odssdhuLI+/1Tqw6Pt/VAaP4Tr8EUxHvPXE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6 h1:eeXdGVtXEe+2Jc49+/vAzna3FAQnUD4AagAw8tzbmfc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6/go.mod h1:FwpAKI+FBPIELJIdmQzlLtRe8LQSOreMcM2wBsPMvvc= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0 h1:qnx+WyIH9/AD+wAxi05WCMNanO236ceqHg6hChCWs3M= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0/go.mod h1:+Kc1UmbE37ijaAsb3KogW6FR8z0myjX6VtdcCkQEK0k= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.7 h1:Ls6kDGWNr3wxE8JypXgTTonHpQ1eRVCGNqaFHY2UASw= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.7/go.mod h1:+v2jeT4/39fCXUQ0ZfHQHMMiJljnmiuj16F03uAd9DY= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0 h1:s71pGCiLqqGRoUWtdJ2j4PazwEpZVwQc16na/4FfXdk= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0/go.mod h1:YGzTq/joAih4HRZZtMBWGP4bI8xVucOBQ9RvuanpclA= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.7 h1:o2HKntJx3vr3y11NK58RA6tYKZKQo5PWWt/bs0rWR0U= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.7/go.mod h1:FAVtDKEl/8WxRDQ33e2fz16RO1t4zeEwWIU5kR29xXs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0 h1:uhb7moM7VjqIEpWzTpCvceLDSwrWpaleXm39OnVjuLE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0/go.mod h1:pA2St3Pu2Ldy6fBPY45Azoh1WBG4oS7eIKOd4XN7Meg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2 h1:T/ywkX1ed+TsZVQccu/8rRJGxKZF/t0Ivgrb4MHTSeo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2/go.mod h1:RnloUnyZ4KN9JStGY1LuQ7Wzqh7V0f8FinmRdHYtuaA= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0 h1:6Bc0KHhAyxGe15JUHrK+Udw7KhE5LN+5HKZjQGo4yDI= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0/go.mod h1:0nXuX9UrkN4r0PX9TSKfcueGRfsdEYIKG4rjTeJ61X8= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.6 h1:JGrc3+kkyr848/wpG2+kWuzHK3H4Fyxj2jnXj8ijQ/Y= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.6/go.mod h1:zwvTysbXES8GDwFcwCPB8NkC+bCdio1abH+E+BRe/xg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU= github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 h1:dzWS4r8E9bA0TesHM40FSAtedwpTVCuTsLI8EziSqyk= @@ -58,6 +76,8 @@ github.com/aws/smithy-go v1.11.1 h1:IQ+lPZVkSM3FRtyaDox41R8YS6iwPMYIreejOgPW49g= github.com/aws/smithy-go v1.11.1/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= +github.com/aws/smithy-go v1.11.3 h1:DQixirEFM9IaKxX1olZ3ke3nvxRS2xMDteKIDWxozW8= +github.com/aws/smithy-go v1.11.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/brianvoe/gofakeit/v6 v6.15.0 h1:lJPGJZ2/07TRGDazyTzD5b18N3y4tmmJpdhCUw18FlI= github.com/brianvoe/gofakeit/v6 v6.15.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= github.com/calyptia/go-bubble-table v0.1.0 h1:mXpaaBlrHGH4K8v5PvM8YqBFT9jlysS1YOycU2u3gEQ= @@ -87,6 +107,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= diff --git a/internal/dynamo-browse/controllers/iface.go b/internal/dynamo-browse/controllers/iface.go index 7c74234..fcd4af1 100644 --- a/internal/dynamo-browse/controllers/iface.go +++ b/internal/dynamo-browse/controllers/iface.go @@ -10,4 +10,5 @@ type TableReadService interface { Describe(ctx context.Context, table string) (*models.TableInfo, error) Scan(ctx context.Context, tableInfo *models.TableInfo) (*models.ResultSet, error) Filter(resultSet *models.ResultSet, filter string) *models.ResultSet + ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, queryExpr string) (*models.ResultSet, error) } diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 01048e5..c07d208 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -75,6 +75,29 @@ func (c *TableReadController) ScanTable(name string) tea.Cmd { } } +func (c *TableReadController) PromptForQuery() tea.Cmd { + return func() tea.Msg { + return events.PromptForInputMsg{ + Prompt: "query: ", + OnDone: func(value string) tea.Cmd { + if value == "" { + return c.Rescan() + } + + return func() tea.Msg { + resultSet := c.state.ResultSet() + newResultSet, err := c.tableService.ScanOrQuery(context.Background(), resultSet.TableInfo, value) + if err != nil { + return events.Error(err) + } + + return c.setResultSetAndFilter(newResultSet, "") + } + }, + } + } +} + func (c *TableReadController) Rescan() tea.Cmd { return func() tea.Msg { return c.doScan(context.Background(), c.state.ResultSet()) diff --git a/internal/dynamo-browse/controllers/tableread_test.go b/internal/dynamo-browse/controllers/tableread_test.go index 74ad025..d9cae53 100644 --- a/internal/dynamo-browse/controllers/tableread_test.go +++ b/internal/dynamo-browse/controllers/tableread_test.go @@ -100,6 +100,40 @@ func TestTableReadController_ExportCSV(t *testing.T) { // Hidden items? } +func TestTableReadController_Query(t *testing.T) { + client, cleanupFn := testdynamo.SetupTestTable(t, testData) + defer cleanupFn() + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + readController := controllers.NewTableReadController(controllers.NewState(), service, "alpha-table") + + t.Run("should run scan with filter based on user query", func(t *testing.T) { + tempFile := tempFile(t) + + invokeCommand(t, readController.Init()) + invokeCommandWithPrompts(t, readController.PromptForQuery(), `pk ^= "abc"`) + invokeCommand(t, readController.ExportCSV(tempFile)) + + bts, err := os.ReadFile(tempFile) + assert.NoError(t, err) + + assert.Equal(t, string(bts), strings.Join([]string{ + "pk,sk,alpha,beta\n", + "abc,111,This is some value,\n", + "abc,222,This is another some value,1231\n", + }, "")) + }) + + t.Run("should return error if result set is not set", func(t *testing.T) { + tempFile := tempFile(t) + readController := controllers.NewTableReadController(controllers.NewState(), service, "non-existant-table") + + invokeCommandExpectingError(t, readController.Init()) + invokeCommandExpectingError(t, readController.ExportCSV(tempFile)) + }) +} + func tempFile(t *testing.T) string { t.Helper() diff --git a/internal/dynamo-browse/models/query.go b/internal/dynamo-browse/models/query.go new file mode 100644 index 0000000..cecde72 --- /dev/null +++ b/internal/dynamo-browse/models/query.go @@ -0,0 +1,8 @@ +package models + +import "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + +type QueryExecutionPlan struct { + CanQuery bool + Expression expression.Expression +} diff --git a/internal/dynamo-browse/models/queryexpr/ast.go b/internal/dynamo-browse/models/queryexpr/ast.go new file mode 100644 index 0000000..4750028 --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/ast.go @@ -0,0 +1,32 @@ +package queryexpr + +import ( + "github.com/alecthomas/participle/v2" + "github.com/pkg/errors" +) + +type astExpr struct { + Equality *astBinOp `parser:"@@"` +} + +type astBinOp struct { + Name string `parser:"@Ident"` + Op string `parser:"@('^' '=' | '=')"` + Value *astLiteralValue `parser:"@@"` +} + +type astLiteralValue struct { + String string `parser:"@String"` +} + +var parser = participle.MustBuild(&astExpr{}) + +func Parse(expr string) (*QueryExpr, error) { + var ast astExpr + + if err := parser.ParseString("expr", expr, &ast); err != nil { + return nil, errors.Wrapf(err, "cannot parse expression: '%v'", expr) + } + + return &QueryExpr{ast: &ast}, nil +} diff --git a/internal/dynamo-browse/models/queryexpr/astquery.go b/internal/dynamo-browse/models/queryexpr/astquery.go new file mode 100644 index 0000000..c2e2dbe --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/astquery.go @@ -0,0 +1,52 @@ +package queryexpr + +import ( + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/lmika/awstools/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 *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 + } + + builder := expression.NewBuilder() + builder = builder.WithFilter(cb) + + expr, err := builder.Build() + if err != nil { + return nil, err + } + + return &models.QueryExecutionPlan{ + CanQuery: false, + 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/expr.go b/internal/dynamo-browse/models/queryexpr/expr.go new file mode 100644 index 0000000..128e1df --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/expr.go @@ -0,0 +1,11 @@ +package queryexpr + +import "github.com/lmika/awstools/internal/dynamo-browse/models" + +type QueryExpr struct { + ast *astExpr +} + +func (md *QueryExpr) BuildQuery(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) { + return md.ast.calcQuery(tableInfo) +} diff --git a/internal/dynamo-browse/models/queryexpr/expr_test.go b/internal/dynamo-browse/models/queryexpr/expr_test.go new file mode 100644 index 0000000..21c3f3b --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/expr_test.go @@ -0,0 +1,47 @@ +package queryexpr_test + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/awstools/internal/dynamo-browse/models/queryexpr" + "testing" + + "github.com/lmika/awstools/internal/dynamo-browse/models" + "github.com/stretchr/testify/assert" +) + +func TestModExpr_Query(t *testing.T) { + tableInfo := &models.TableInfo{ + Name: "test", + Keys: models.KeyAttribute{ + PartitionKey: "pk", + SortKey: "sk", + }, + } + + 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.BuildQuery(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) + }) + + 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) + + plan, err := modExpr.BuildQuery(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) + }) +} diff --git a/internal/dynamo-browse/models/queryexpr/values.go b/internal/dynamo-browse/models/queryexpr/values.go new file mode 100644 index 0000000..d6b3739 --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/values.go @@ -0,0 +1,24 @@ +package queryexpr + +import ( + "strconv" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/pkg/errors" +) + +func (a *astLiteralValue) dynamoValue() (types.AttributeValue, error) { + s, err := strconv.Unquote(a.String) + if err != nil { + return nil, errors.Wrap(err, "cannot unquote string") + } + return &types.AttributeValueMemberS{Value: s}, nil +} + +func (a *astLiteralValue) goValue() (any, error) { + s, err := strconv.Unquote(a.String) + if err != nil { + return nil, errors.Wrap(err, "cannot unquote string") + } + return s, nil +} diff --git a/internal/dynamo-browse/providers/dynamo/provider.go b/internal/dynamo-browse/providers/dynamo/provider.go index 1e46e72..e05cd5f 100644 --- a/internal/dynamo-browse/providers/dynamo/provider.go +++ b/internal/dynamo-browse/providers/dynamo/provider.go @@ -2,8 +2,8 @@ package dynamo import ( "context" - "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/awstools/internal/dynamo-browse/models" @@ -64,11 +64,18 @@ func NewProvider(client *dynamodb.Client) *Provider { return &Provider{client: client} } -func (p *Provider) ScanItems(ctx context.Context, tableName string, maxItems int) ([]models.Item, error) { - paginator := dynamodb.NewScanPaginator(p.client, &dynamodb.ScanInput{ +func (p *Provider) ScanItems(ctx context.Context, tableName string, filterExpr *expression.Expression, maxItems int) ([]models.Item, error) { + input := &dynamodb.ScanInput{ TableName: aws.String(tableName), Limit: aws.Int32(int32(maxItems)), - }) + } + if filterExpr != nil { + input.FilterExpression = filterExpr.Filter() + input.ExpressionAttributeNames = filterExpr.Names() + input.ExpressionAttributeValues = filterExpr.Values() + } + + paginator := dynamodb.NewScanPaginator(p.client, input) items := make([]models.Item, 0) diff --git a/internal/dynamo-browse/services/tables/iface.go b/internal/dynamo-browse/services/tables/iface.go index 8548115..58d63ec 100644 --- a/internal/dynamo-browse/services/tables/iface.go +++ b/internal/dynamo-browse/services/tables/iface.go @@ -2,6 +2,7 @@ package tables 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/awstools/internal/dynamo-browse/models" @@ -10,7 +11,7 @@ import ( type TableProvider interface { ListTables(ctx context.Context) ([]string, error) DescribeTable(ctx context.Context, tableName string) (*models.TableInfo, error) - ScanItems(ctx context.Context, tableName string, 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 f6b56e5..d58aaad 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -2,6 +2,8 @@ package tables import ( "context" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/lmika/awstools/internal/dynamo-browse/models/queryexpr" "sort" "strings" @@ -28,7 +30,11 @@ func (s *Service) Describe(ctx context.Context, table string) (*models.TableInfo } func (s *Service) Scan(ctx context.Context, tableInfo *models.TableInfo) (*models.ResultSet, error) { - results, err := s.provider.ScanItems(ctx, tableInfo.Name, 1000) + return s.doScan(ctx, tableInfo, nil) +} + +func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, filterExpr *expression.Expression) (*models.ResultSet, error) { + 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) } @@ -101,6 +107,25 @@ func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items return nil } +func (s *Service) ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, queryExpr string) (*models.ResultSet, error) { + expr, err := queryexpr.Parse(queryExpr) + if err != nil { + return nil, err + } + + plan, err := expr.BuildQuery(tableInfo) + if err != nil { + return nil, err + } + + // TEMP + if plan.CanQuery { + return nil, errors.Errorf("queries not yet supported") + } + + return s.doScan(ctx, tableInfo, &plan.Expression) +} + // TODO: move into a new service func (s *Service) Filter(resultSet *models.ResultSet, filter string) *models.ResultSet { for i, item := range resultSet.Items() { diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 2e9d9ba..b04a327 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -98,8 +98,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if idx := m.tableView.SelectedItemIndex(); idx >= 0 { return m, m.tableWriteController.ToggleMark(idx) } - case "r": + case "R": return m, m.tableReadController.Rescan() + case "?": + return m, m.tableReadController.PromptForQuery() case "/": return m, m.tableReadController.Filter() case ":":