diff --git a/Makefile b/Makefile index 0d833cfa..c279bdff 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,7 @@ gen-api-v1: && mv go/* . \ && rm -rf go main.go Dockerfile README.md api .swagger-codegen .swagger-codegen-ignore *.yaml \ && sed -i 's/\*OneOfTableAttributesValue/interface{}/' model_table_attributes.go \ + && sed -i 's/\OneOfQueryParamsItems/interface{}/' model_query.go \ " sudo chown -R ${USER} ${APIV1} .PHONY: gen-api-v1 \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go index 62480ab8..eb5ffc7e 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -442,9 +442,12 @@ func createAPIServer( supportedChainIDs = append(supportedChainIDs, chainID) } + resolver := parsing.NewReadStatementResolver(sm) + g, err := gateway.NewGateway( parser, - gatewayimpl.NewGatewayStore(db, parsing.NewReadStatementResolver(sm)), + gatewayimpl.NewGatewayStore(db), + resolver, gatewayConfig.ExternalURIPrefix, gatewayConfig.MetadataRendererURI, gatewayConfig.AnimationRendererURI) diff --git a/cmd/healthbot/counterprobe/counterprobe.go b/cmd/healthbot/counterprobe/counterprobe.go index 8f58680a..a7f451de 100644 --- a/cmd/healthbot/counterprobe/counterprobe.go +++ b/cmd/healthbot/counterprobe/counterprobe.go @@ -171,6 +171,7 @@ func (cp *CounterProbe) getCurrentCounterValue(ctx context.Context) (int64, erro if err := cp.client.Read( ctx, fmt.Sprintf("select counter from %s", cp.tableName), + []string{}, &counter, clientV1.ReadExtract(), clientV1.ReadUnwrap()); err != nil { diff --git a/go.mod b/go.mod index 46b797d7..c526d2d2 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/sethvargo/go-limiter v0.7.2 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.2 - github.com/tablelandnetwork/sqlparser v0.0.0-20230605164749-c0e6862c37f6 + github.com/tablelandnetwork/sqlparser v0.0.0-20240529190608-e3776575020d github.com/textileio/cli v1.0.2 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.37.0 go.opentelemetry.io/otel v1.14.0 diff --git a/go.sum b/go.sum index 9a9e66f2..af5f05ee 100644 --- a/go.sum +++ b/go.sum @@ -1295,16 +1295,10 @@ github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= -github.com/tablelandnetwork/sqlparser v0.0.0-20230517143402-3ab9022be0df h1:SUG49BUSuO9S6U3RjAV8a0NIDRByHj3kSt8/QR75rtI= -github.com/tablelandnetwork/sqlparser v0.0.0-20230517143402-3ab9022be0df/go.mod h1:S+M/v3Q8X+236kQxMQziFcLId2Cscb1LzW06IUVhljE= -github.com/tablelandnetwork/sqlparser v0.0.0-20230518143735-838d223866f6 h1:f8TRklEZmT4fJd7wE+oktjf4wQndJ5BqkwXOpuHrYBU= -github.com/tablelandnetwork/sqlparser v0.0.0-20230518143735-838d223866f6/go.mod h1:S+M/v3Q8X+236kQxMQziFcLId2Cscb1LzW06IUVhljE= -github.com/tablelandnetwork/sqlparser v0.0.0-20230602174101-e27f9a12da58 h1:vEdQ9rJs5vwJfrwg4HIpVVrXRVJqEzyA2MM9oUehnhc= -github.com/tablelandnetwork/sqlparser v0.0.0-20230602174101-e27f9a12da58/go.mod h1:S+M/v3Q8X+236kQxMQziFcLId2Cscb1LzW06IUVhljE= -github.com/tablelandnetwork/sqlparser v0.0.0-20230605150512-1cb695cd5627 h1:ctGSX+KFDNvMYX25ooerc3saOcfPF6i56oAe+mo1l20= -github.com/tablelandnetwork/sqlparser v0.0.0-20230605150512-1cb695cd5627/go.mod h1:S+M/v3Q8X+236kQxMQziFcLId2Cscb1LzW06IUVhljE= -github.com/tablelandnetwork/sqlparser v0.0.0-20230605164749-c0e6862c37f6 h1:goeC/kQXlqRod2rPwqrVxvEgF3I5S3f0fa538k+Evbw= -github.com/tablelandnetwork/sqlparser v0.0.0-20230605164749-c0e6862c37f6/go.mod h1:S+M/v3Q8X+236kQxMQziFcLId2Cscb1LzW06IUVhljE= +github.com/tablelandnetwork/sqlparser v0.0.0-20240523182602-af3edf08e3db h1:4/eOYTPLww9YzdsteeGFhcT8dwIaCm3FIOwrxs6ppQM= +github.com/tablelandnetwork/sqlparser v0.0.0-20240523182602-af3edf08e3db/go.mod h1:S+M/v3Q8X+236kQxMQziFcLId2Cscb1LzW06IUVhljE= +github.com/tablelandnetwork/sqlparser v0.0.0-20240529190608-e3776575020d h1:Xhc6wudmyItX8Pvr+Z2SCzOmrf/zda5sX1jCgUySNRg= +github.com/tablelandnetwork/sqlparser v0.0.0-20240529190608-e3776575020d/go.mod h1:S+M/v3Q8X+236kQxMQziFcLId2Cscb1LzW06IUVhljE= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/textileio/cli v1.0.2 h1:qSp/x4d/9SZ93TxhgZnE5okRKqzqHqrzAwKAPjuPw50= github.com/textileio/cli v1.0.2/go.mod h1:vTlCvvVyOmXXLwddCcBg3PDavfUsCkRBZoyr6Nu1lkc= diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index 907acd2e..690c5d9b 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/common" logger "github.com/rs/zerolog/log" + "github.com/tablelandnetwork/sqlparser" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/pkg/parsing" "github.com/textileio/go-tableland/pkg/tables" @@ -33,14 +34,14 @@ const ( // Gateway defines the gateway operations. type Gateway interface { - RunReadQuery(ctx context.Context, stmt string) (*TableData, error) + RunReadQuery(ctx context.Context, stmt string, params []string) (*TableData, error) GetTableMetadata(context.Context, tableland.ChainID, tables.TableID) (TableMetadata, error) GetReceiptByTransactionHash(context.Context, tableland.ChainID, common.Hash) (Receipt, bool, error) } // GatewayStore is the storage layer of the Gateway. type GatewayStore interface { - Read(context.Context, parsing.ReadStmt) (*TableData, error) + Read(context.Context, parsing.ReadStmt, sqlparser.ReadStatementResolver) (*TableData, error) GetTable(context.Context, tableland.ChainID, tables.TableID) (Table, error) GetSchemaByTableName(context.Context, string) (TableSchema, error) GetReceipt(context.Context, tableland.ChainID, string) (Receipt, bool, error) @@ -53,6 +54,8 @@ type GatewayService struct { metadataRendererURI string animationRendererURI string store GatewayStore + + resolver *parsing.ReadStatementResolver } var _ (Gateway) = (*GatewayService)(nil) @@ -61,6 +64,7 @@ var _ (Gateway) = (*GatewayService)(nil) func NewGateway( parser parsing.SQLValidator, store GatewayStore, + resolver *parsing.ReadStatementResolver, extURLPrefix string, metadataRendererURI string, animationRendererURI string, @@ -89,6 +93,7 @@ func NewGateway( metadataRendererURI: metadataRendererURI, animationRendererURI: animationRendererURI, store: store, + resolver: resolver, }, nil } @@ -161,13 +166,17 @@ func (g *GatewayService) GetReceiptByTransactionHash( } // RunReadQuery allows the user to run SQL. -func (g *GatewayService) RunReadQuery(ctx context.Context, statement string) (*TableData, error) { +func (g *GatewayService) RunReadQuery(ctx context.Context, statement string, params []string) (*TableData, error) { readStmt, err := g.parser.ValidateReadQuery(statement) if err != nil { return nil, fmt.Errorf("validating read query: %s", err) } - queryResult, err := g.store.Read(ctx, readStmt) + if err := g.resolver.PrepareParams(params); err != nil { + return nil, fmt.Errorf("prepare params: %s", err) + } + + queryResult, err := g.store.Read(ctx, readStmt, g.resolver) if err != nil { return nil, fmt.Errorf("running read statement: %s", err) } diff --git a/internal/gateway/gateway_instrumented.go b/internal/gateway/gateway_instrumented.go index 9bb05ed8..4ea5c7cb 100644 --- a/internal/gateway/gateway_instrumented.go +++ b/internal/gateway/gateway_instrumented.go @@ -81,9 +81,9 @@ func (g *InstrumentedGateway) GetTableMetadata( } // RunReadQuery allows the user to run SQL. -func (g *InstrumentedGateway) RunReadQuery(ctx context.Context, statement string) (*TableData, error) { +func (g *InstrumentedGateway) RunReadQuery(ctx context.Context, statement string, params []string) (*TableData, error) { start := time.Now() - data, err := g.gateway.RunReadQuery(ctx, statement) + data, err := g.gateway.RunReadQuery(ctx, statement, params) latency := time.Since(start).Milliseconds() attributes := append([]attribute.KeyValue{ diff --git a/internal/gateway/impl/gateway_store.go b/internal/gateway/impl/gateway_store.go index 89f449b1..2f513cca 100644 --- a/internal/gateway/impl/gateway_store.go +++ b/internal/gateway/impl/gateway_store.go @@ -18,21 +18,21 @@ import ( // GatewayStore is the storage layer of the gateway. type GatewayStore struct { - db *database.SQLiteDB - resolver sqlparser.ReadStatementResolver + db *database.SQLiteDB } // NewGatewayStore creates a new GatewayStore. -func NewGatewayStore(db *database.SQLiteDB, resolver sqlparser.ReadStatementResolver) *GatewayStore { +func NewGatewayStore(db *database.SQLiteDB) *GatewayStore { return &GatewayStore{ - db: db, - resolver: resolver, + db: db, } } // Read executes a parsed read statement. -func (s *GatewayStore) Read(ctx context.Context, stmt parsing.ReadStmt) (*gateway.TableData, error) { - query, err := stmt.GetQuery(s.resolver) +func (s *GatewayStore) Read( + ctx context.Context, stmt parsing.ReadStmt, resolver sqlparser.ReadStatementResolver, +) (*gateway.TableData, error) { + query, err := stmt.GetQuery(resolver) if err != nil { return nil, fmt.Errorf("get query: %s", err) } diff --git a/internal/gateway/impl/gateway_store_test.go b/internal/gateway/impl/gateway_store_test.go index 7176d223..14890154 100644 --- a/internal/gateway/impl/gateway_store_test.go +++ b/internal/gateway/impl/gateway_store_test.go @@ -31,7 +31,7 @@ func TestGatewayInitialization(t *testing.T) { t.Run("invalid external uri", func(t *testing.T) { t.Parallel() - _, err := gateway.NewGateway(nil, nil, "invalid uri", "", "") + _, err := gateway.NewGateway(nil, nil, nil, "invalid uri", "", "") require.Error(t, err) require.ErrorContains(t, err, "invalid external url prefix") }) @@ -39,7 +39,7 @@ func TestGatewayInitialization(t *testing.T) { t.Run("invalid metadata uri", func(t *testing.T) { t.Parallel() - _, err := gateway.NewGateway(nil, nil, "https://tableland.network", "invalid uri", "") + _, err := gateway.NewGateway(nil, nil, nil, "https://tableland.network", "invalid uri", "") require.Error(t, err) require.ErrorContains(t, err, "metadata renderer uri could not be parsed") }) @@ -47,7 +47,9 @@ func TestGatewayInitialization(t *testing.T) { t.Run("invalid animation uri", func(t *testing.T) { t.Parallel() - _, err := gateway.NewGateway(nil, nil, "https://tableland.network", "https://tables.tableland.xyz", "invalid uri") + _, err := gateway.NewGateway( + nil, nil, nil, "https://tableland.network", "https://tables.tableland.xyz", "invalid uri", + ) require.Error(t, err) require.ErrorContains(t, err, "animation renderer uri could not be parsed") }) @@ -93,7 +95,7 @@ func TestGateway(t *testing.T) { require.NoError(t, err) svc, err := gateway.NewGateway( - parser, NewGatewayStore(db, nil), "https://tableland.network", "https://tables.tableland.xyz", "", + parser, NewGatewayStore(db), nil, "https://tableland.network", "https://tables.tableland.xyz", "", ) require.NoError(t, err) metadata, err := svc.GetTableMetadata(ctx, chainID, id) @@ -148,7 +150,7 @@ func TestGetMetadata(t *testing.T) { parser, err := parserimpl.New([]string{"system_", "registry", "sqlite_"}) require.NoError(t, err) - svc, err := gateway.NewGateway(parser, NewGatewayStore(db, nil), "https://tableland.network", "", "") + svc, err := gateway.NewGateway(parser, NewGatewayStore(db), nil, "https://tableland.network", "", "") require.NoError(t, err) metadata, err := svc.GetTableMetadata(context.Background(), chainID, id) @@ -168,7 +170,7 @@ func TestGetMetadata(t *testing.T) { require.NoError(t, err) svc, err := gateway.NewGateway( - parser, NewGatewayStore(db, nil), "https://tableland.network", "https://tables.tableland.xyz", "", + parser, NewGatewayStore(db), nil, "https://tableland.network", "https://tables.tableland.xyz", "", ) require.NoError(t, err) @@ -189,7 +191,7 @@ func TestGetMetadata(t *testing.T) { require.NoError(t, err) svc, err := gateway.NewGateway( - parser, NewGatewayStore(db, nil), "https://tableland.network", "https://tables.tableland.xyz/", "", + parser, NewGatewayStore(db), nil, "https://tableland.network", "https://tables.tableland.xyz/", "", ) require.NoError(t, err) @@ -210,7 +212,7 @@ func TestGetMetadata(t *testing.T) { parser, err := parserimpl.New([]string{"system_", "registry", "sqlite_"}) require.NoError(t, err) - _, err = gateway.NewGateway(parser, NewGatewayStore(db, nil), "https://tableland.network", "foo", "") + _, err = gateway.NewGateway(parser, NewGatewayStore(db), nil, "https://tableland.network", "foo", "") require.Error(t, err) require.ErrorContains(t, err, "metadata renderer uri could not be parsed") }) @@ -222,7 +224,7 @@ func TestGetMetadata(t *testing.T) { require.NoError(t, err) svc, err := gateway.NewGateway( - parser, NewGatewayStore(db, nil), "https://tableland.network", "https://tables.tableland.xyz", "", + parser, NewGatewayStore(db), nil, "https://tableland.network", "https://tables.tableland.xyz", "", ) require.NoError(t, err) @@ -244,7 +246,8 @@ func TestGetMetadata(t *testing.T) { svc, err := gateway.NewGateway( parser, - NewGatewayStore(db, nil), + NewGatewayStore(db), + nil, "https://tableland.network", "https://tables.tableland.xyz", "https://tables.tableland.xyz", @@ -282,7 +285,8 @@ func TestQueryConstraints(t *testing.T) { gateway, err := gateway.NewGateway( parser, - NewGatewayStore(db, nil), + NewGatewayStore(db), + nil, "https://tableland.network", "https://tables.tableland.xyz", "https://tables.tableland.xyz", @@ -290,7 +294,7 @@ func TestQueryConstraints(t *testing.T) { require.NoError(t, err) _, err = gateway.RunReadQuery( - context.Background(), "SELECT * FROM foo_1337_1 WHERE bar = 'hello2'", + context.Background(), "SELECT * FROM foo_1337_1 WHERE bar = 'hello2'", []string{}, ) // length of 45 bytes require.Error(t, err) require.ErrorContains(t, err, "read query size is too long") diff --git a/internal/router/controllers/apiv1/model_one_of_query_params_items.go b/internal/router/controllers/apiv1/model_one_of_query_params_items.go new file mode 100644 index 00000000..f05b5bcc --- /dev/null +++ b/internal/router/controllers/apiv1/model_one_of_query_params_items.go @@ -0,0 +1,13 @@ +/* + * Tableland Validator - OpenAPI 3.0 + * + * In Tableland, Validators are the execution unit/actors of the protocol. They have the following responsibilities: - Listen to onchain events to materialize Tableland-compliant SQL queries in a database engine (currently, SQLite by default). - Serve read-queries (e.g., SELECT * FROM foo_69_1) to the external world. - Serve state queries (e.g., list tables, get receipts, etc) to the external world. In the 1.0.0 release of the Tableland Validator API, we've switched to a design first approach! You can now help us improve the API whether it's by making changes to the definition itself or to the code. That way, with time, we can improve the API in general, and expose some of the new features in OAS3. The API includes the following endpoints: - `/health`: Returns OK if the validator considers itself healthy. - `/version`: Returns version information about the validator daemon. - `/query`: Returns the results of a SQL read query against the Tableland network. - `/receipt/{chainId}/{transactionHash}`: Returns the status of a given transaction receipt by hash. - `/tables/{chainId}/{tableId}`: Returns information about a single table, including schema information. + * + * API version: 1.1.0 + * Contact: carson@textile.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ +package apiv1 + +type OneOfQueryParamsItems struct { +} diff --git a/internal/router/controllers/apiv1/model_query.go b/internal/router/controllers/apiv1/model_query.go index 339834e7..2ccd9e36 100644 --- a/internal/router/controllers/apiv1/model_query.go +++ b/internal/router/controllers/apiv1/model_query.go @@ -12,6 +12,8 @@ package apiv1 type Query struct { // The SQL read query statement Statement string `json:"statement,omitempty"` + // The values of query parameters + Params []interface{} `json:"params,omitempty"` // The requested response format: * `objects` - Returns the query results as a JSON array of JSON objects. * `table` - Return the query results as a JSON object with columns and rows properties. Format string `json:"format,omitempty"` // Whether to extract the JSON object from the single property of the surrounding JSON object. diff --git a/internal/router/controllers/controller.go b/internal/router/controllers/controller.go index 699befdf..83677976 100644 --- a/internal/router/controllers/controller.go +++ b/internal/router/controllers/controller.go @@ -231,9 +231,14 @@ func (c *Controller) GetTableQuery(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") stm := r.URL.Query().Get("statement") + params := []string{} + + if r.URL.Query().Has("params") { + params = r.URL.Query()["params"] + } start := time.Now() - res, ok := c.runReadRequest(r.Context(), stm, rw) + res, ok := c.runReadRequest(r.Context(), stm, params, rw) if !ok { return } @@ -276,6 +281,7 @@ func (c *Controller) PostTableQuery(rw http.ResponseWriter, r *http.Request) { // setting a default body because these options could be missing from JSON body := &apiv1.Query{ + Params: []any{}, Format: string(formatter.Objects), Extract: false, Unwrap: false, @@ -289,8 +295,31 @@ func (c *Controller) PostTableQuery(rw http.ResponseWriter, r *http.Request) { } _ = r.Body.Close() + params := make([]string, len(body.Params)) + for i, p := range body.Params { + switch v := p.(type) { + case float64: + params[i] = fmt.Sprint(v) + case string: + params[i] = fmt.Sprintf("\"%s\"", v) + case nil: + params[i] = "null" + case bool: + params[i] = "false" + if v { + params[i] = "true" + } + default: + rw.WriteHeader(http.StatusBadRequest) + msg := fmt.Sprintf("invalid type (%T) of parameter", v) + log.Ctx(r.Context()).Error().Msg(msg) + _ = json.NewEncoder(rw).Encode(errors.ServiceError{Message: msg}) + return + } + } + start := time.Now() - res, ok := c.runReadRequest(r.Context(), body.Statement, rw) + res, ok := c.runReadRequest(r.Context(), body.Statement, params, rw) if !ok { return } @@ -332,9 +361,10 @@ func (c *Controller) PostTableQuery(rw http.ResponseWriter, r *http.Request) { func (c *Controller) runReadRequest( ctx context.Context, stm string, + params []string, rw http.ResponseWriter, ) (*gateway.TableData, bool) { - res, err := c.gateway.RunReadQuery(ctx, stm) + res, err := c.gateway.RunReadQuery(ctx, stm, params) if err != nil { rw.WriteHeader(http.StatusBadRequest) log.Ctx(ctx). diff --git a/internal/router/controllers/controller_test.go b/internal/router/controllers/controller_test.go index 5cd20b79..c0bd61d1 100644 --- a/internal/router/controllers/controller_test.go +++ b/internal/router/controllers/controller_test.go @@ -25,7 +25,7 @@ import ( func TestQuery(t *testing.T) { r := mocks.NewGateway(t) - r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string")).Return( + r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]string")).Return( &gateway.TableData{ Columns: []gateway.Column{ {Name: "id"}, @@ -94,7 +94,7 @@ func TestQuery(t *testing.T) { func TestPostQuery(t *testing.T) { r := mocks.NewGateway(t) - r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string")).Return( + r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]string")).Return( &gateway.TableData{ Columns: []gateway.Column{ {Name: "id"}, @@ -193,7 +193,7 @@ func TestPostQuery(t *testing.T) { func TestQueryEmptyTable(t *testing.T) { r := mocks.NewGateway(t) - r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string")).Return( + r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]string")).Return( &gateway.TableData{ Columns: []gateway.Column{ {Name: "id"}, @@ -229,7 +229,7 @@ func TestQueryEmptyTable(t *testing.T) { func TestQueryExtracted(t *testing.T) { r := mocks.NewGateway(t) - r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string")).Return( + r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]string")).Return( &gateway.TableData{ Columns: []gateway.Column{{Name: "name"}}, Rows: [][]*gateway.ColumnValue{ @@ -276,7 +276,7 @@ func TestQueryExtracted(t *testing.T) { func TestPostQueryExtracted(t *testing.T) { r := mocks.NewGateway(t) - r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string")).Return( + r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("[]string")).Return( &gateway.TableData{ Columns: []gateway.Column{{Name: "name"}}, Rows: [][]*gateway.ColumnValue{ diff --git a/internal/tableland/impl/tableland_test.go b/internal/tableland/impl/tableland_test.go index bb695ceb..c2526383 100644 --- a/internal/tableland/impl/tableland_test.go +++ b/internal/tableland/impl/tableland_test.go @@ -157,7 +157,7 @@ func TestReadSystemTable(t *testing.T) { _, err := sc.CreateTable(txOpts, caller, `CREATE TABLE foo_1337 (myjson TEXT);`) require.NoError(t, err) - res, err := gateway.RunReadQuery(ctx, "select * from registry") + res, err := gateway.RunReadQuery(ctx, "select * from registry", []string{}) require.NoError(t, err) _, err = json.Marshal(res) require.NoError(t, err) @@ -551,7 +551,7 @@ func jsonEq( expJSON string, ) func() bool { return func() bool { - r, err := gateway.RunReadQuery(ctx, stm) + r, err := gateway.RunReadQuery(ctx, stm, []string{}) // if we get a table undefined error, try again if err != nil && strings.Contains(err.Error(), "no such table") { return false @@ -587,7 +587,7 @@ func runSQLCountEq( expCount int, ) func() bool { return func() bool { - response, err := gateway.RunReadQuery(ctx, sql) + response, err := gateway.RunReadQuery(ctx, sql, []string{}) // if we get a table undefined error, try again if err != nil && strings.Contains(err.Error(), "table not found") { return false @@ -772,7 +772,7 @@ func (b *tablelandSetupBuilder) build(t *testing.T) *tablelandSetup { // common dependencies among mesa clients parser: parser, - store: gatewayimpl.NewGatewayStore(db, nil), + store: gatewayimpl.NewGatewayStore(db), } } @@ -818,6 +818,7 @@ func (s *tablelandSetup) newTablelandClient(t *testing.T) *tablelandClient { gateway, err := gateway.NewGateway( s.parser, s.store, + parsing.NewReadStatementResolver(nil), "https://tableland.network/tables", "https://tables.tableland.xyz", "https://tables.tableland.xyz", diff --git a/mocks/Gateway.go b/mocks/Gateway.go index e73b00df..6de90be8 100644 --- a/mocks/Gateway.go +++ b/mocks/Gateway.go @@ -128,13 +128,13 @@ func (_c *Gateway_GetTableMetadata_Call) Return(_a0 gateway.TableMetadata, _a1 e return _c } -// RunReadQuery provides a mock function with given fields: ctx, stmt -func (_m *Gateway) RunReadQuery(ctx context.Context, stmt string) (*gateway.TableData, error) { - ret := _m.Called(ctx, stmt) +// RunReadQuery provides a mock function with given fields: ctx, stmt, params +func (_m *Gateway) RunReadQuery(ctx context.Context, stmt string, params []string) (*gateway.TableData, error) { + ret := _m.Called(ctx, stmt, params) var r0 *gateway.TableData - if rf, ok := ret.Get(0).(func(context.Context, string) *gateway.TableData); ok { - r0 = rf(ctx, stmt) + if rf, ok := ret.Get(0).(func(context.Context, string, []string) *gateway.TableData); ok { + r0 = rf(ctx, stmt, params) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*gateway.TableData) @@ -142,8 +142,8 @@ func (_m *Gateway) RunReadQuery(ctx context.Context, stmt string) (*gateway.Tabl } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, stmt) + if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok { + r1 = rf(ctx, stmt, params) } else { r1 = ret.Error(1) } @@ -159,13 +159,14 @@ type Gateway_RunReadQuery_Call struct { // RunReadQuery is a helper method to define mock.On call // - ctx context.Context // - stmt string -func (_e *Gateway_Expecter) RunReadQuery(ctx interface{}, stmt interface{}) *Gateway_RunReadQuery_Call { - return &Gateway_RunReadQuery_Call{Call: _e.mock.On("RunReadQuery", ctx, stmt)} +// - params []string +func (_e *Gateway_Expecter) RunReadQuery(ctx interface{}, stmt interface{}, params interface{}) *Gateway_RunReadQuery_Call { + return &Gateway_RunReadQuery_Call{Call: _e.mock.On("RunReadQuery", ctx, stmt, params)} } -func (_c *Gateway_RunReadQuery_Call) Run(run func(ctx context.Context, stmt string)) *Gateway_RunReadQuery_Call { +func (_c *Gateway_RunReadQuery_Call) Run(run func(ctx context.Context, stmt string, params []string)) *Gateway_RunReadQuery_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + run(args[0].(context.Context), args[1].(string), args[2].([]string)) }) return _c } diff --git a/pkg/client/v1/client_test.go b/pkg/client/v1/client_test.go index 0c8914bb..4b9c1b06 100644 --- a/pkg/client/v1/client_test.go +++ b/pkg/client/v1/client_test.go @@ -36,31 +36,31 @@ func TestRead(t *testing.T) { } res0 := []result{} - calls.query(fmt.Sprintf("select * from %s", tableName), &res0) + calls.query(fmt.Sprintf("select * from %s", tableName), []string{}, &res0) require.Len(t, res0, 1) require.Equal(t, "baz", res0[0].Bar) res1 := map[string]interface{}{} - calls.query(fmt.Sprintf("select * from %s", tableName), &res1, ReadFormat(Table)) + calls.query(fmt.Sprintf("select * from %s", tableName), []string{}, &res1, ReadFormat(Table)) require.Len(t, res1, 2) res2 := result{} - calls.query(fmt.Sprintf("select * from %s", tableName), &res2, ReadUnwrap()) + calls.query(fmt.Sprintf("select * from %s", tableName), []string{}, &res2, ReadUnwrap()) require.Equal(t, "baz", res2.Bar) res3 := []string{} - calls.query(fmt.Sprintf("select * from %s", tableName), &res3, ReadExtract()) + calls.query(fmt.Sprintf("select * from %s", tableName), []string{}, &res3, ReadExtract()) require.Len(t, res3, 1) require.Equal(t, "baz", res3[0]) res4 := "" - calls.query(fmt.Sprintf("select * from %s", tableName), &res4, ReadUnwrap(), ReadExtract()) + calls.query(fmt.Sprintf("select * from %s", tableName), []string{}, &res4, ReadUnwrap(), ReadExtract()) require.Equal(t, "baz", res4) }) t.Run("status 400", func(t *testing.T) { calls := setup(t) - err := calls.client.Read(context.Background(), "SELECTZ * FROM foo_1", struct{}{}) + err := calls.client.Read(context.Background(), "SELECTZ * FROM foo_1", []string{}, struct{}{}) require.Error(t, err) }) } @@ -161,11 +161,30 @@ func TestBlockNum(t *testing.T) { } res := []result{} - calls.query(fmt.Sprintf("select block_num(1337) as bn from %s", tableName), &res) + calls.query(fmt.Sprintf("select block_num(1337) as bn from %s", tableName), []string{}, &res) require.Len(t, res, 2) // it should be 2 because we inserted two rows require.Equal(t, int64(5), res[0].BlockNumber) // the block number should be 5 } +func TestQueryParams(t *testing.T) { + // Our initial simulated blockchain setup already produces two blocks. + calls := setup(t) + + // We create a table and do two inserts, that will increase our block number to 5. + tableName := requireCreate(t, calls) + requireReceipt(t, calls, requireInsert(t, calls, tableName), WaitFor(time.Second*10)) + requireReceipt(t, calls, requireInsert(t, calls, tableName), WaitFor(time.Second*10)) + + type result struct { + Bar string `json:"bar"` + } + + res := []result{} + calls.query(fmt.Sprintf("select bar from %s where bar = ? or ? != ?", tableName), []string{"'baz'", "1", "null"}, &res) + require.Len(t, res, 2) // it should be 2 because we inserted two rows + require.Equal(t, "baz", res[0].Bar) // the block number should be 5 +} + func requireCreate(t *testing.T, calls clientCalls) string { _, tableName := calls.create("(bar text)", WithPrefix("foo"), WithReceiptTimeout(time.Second*10)) require.Equal(t, "foo_1337_1", tableName) @@ -195,7 +214,7 @@ type clientCalls struct { client *Client create func(schema string, opts ...CreateOption) (TableID, string) write func(query string) string - query func(query string, target interface{}, opts ...ReadOption) + query func(query string, params []string, target interface{}, opts ...ReadOption) receipt func(txnHash string, options ...ReceiptOption) (*apiv1.TransactionReceipt, bool) getTableByID func(tableID TableID) *apiv1.Table version func() (*apiv1.VersionInfo, error) @@ -230,8 +249,8 @@ func setup(t *testing.T) clientCalls { require.NoError(t, err) return id, table }, - query: func(query string, target interface{}, opts ...ReadOption) { - err := client.Read(ctx, query, target, opts...) + query: func(query string, params []string, target interface{}, opts ...ReadOption) { + err := client.Read(ctx, query, params, target, opts...) require.NoError(t, err) }, write: func(query string) string { diff --git a/pkg/client/v1/readquery.go b/pkg/client/v1/readquery.go index de226f59..b3c7f358 100644 --- a/pkg/client/v1/readquery.go +++ b/pkg/client/v1/readquery.go @@ -61,7 +61,9 @@ func ReadUnwrap() ReadOption { var queryURL, _ = url.Parse("/api/v1/query") // Read runs a read query with the provided opts and unmarshals the results into target. -func (c *Client) Read(ctx context.Context, query string, target interface{}, opts ...ReadOption) error { +func (c *Client) Read( + ctx context.Context, query string, queryParams []string, target interface{}, opts ...ReadOption, +) error { params := defaultReadQueryParameters for _, opt := range opts { opt(¶ms) @@ -77,12 +79,20 @@ func (c *Client) Read(ctx context.Context, query string, target interface{}, opt if params.unwrap { values.Set("unwrap", "true") } + for _, param := range queryParams { + values.Add("params", param) + } url.RawQuery = values.Encode() req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil) if err != nil { return fmt.Errorf("creating request: %s", err) } + q := req.URL.Query() + for _, param := range queryParams { + q.Add("params", param) + } + response, err := c.tblHTTP.Do(req) if err != nil { return fmt.Errorf("calling query: %s", err) diff --git a/pkg/eventprocessor/impl/eventprocessor_test.go b/pkg/eventprocessor/impl/eventprocessor_test.go index ab632884..0138cead 100644 --- a/pkg/eventprocessor/impl/eventprocessor_test.go +++ b/pkg/eventprocessor/impl/eventprocessor_test.go @@ -16,6 +16,7 @@ import ( "github.com/textileio/go-tableland/pkg/eventprocessor/eventfeed" efimpl "github.com/textileio/go-tableland/pkg/eventprocessor/eventfeed/impl" executor "github.com/textileio/go-tableland/pkg/eventprocessor/impl/executor/impl" + "github.com/textileio/go-tableland/pkg/parsing" parserimpl "github.com/textileio/go-tableland/pkg/parsing/impl" "github.com/textileio/go-tableland/pkg/sharedmemory" @@ -400,7 +401,7 @@ func setup(t *testing.T) ( rq, err := parser.ValidateReadQuery(readQuery) require.NoError(t, err) require.NotNil(t, rq) - res, err := gatewayimpl.NewGatewayStore(db, nil).Read(ctx, rq) + res, err := gatewayimpl.NewGatewayStore(db).Read(ctx, rq, parsing.NewReadStatementResolver(nil)) require.NoError(t, err) ret := make([]int64, len(res.Rows)) @@ -414,7 +415,7 @@ func setup(t *testing.T) ( return func() bool { for _, expReceipt := range rs { gotReceipt, found, err := gatewayimpl. - NewGatewayStore(db, nil). + NewGatewayStore(db). GetReceipt(context.Background(), 1337, expReceipt.TxnHash) require.NoError(t, err) if !found { diff --git a/pkg/eventprocessor/impl/executor/impl/txnscope_createtable_test.go b/pkg/eventprocessor/impl/executor/impl/txnscope_createtable_test.go index 5cea4240..0e8b129f 100644 --- a/pkg/eventprocessor/impl/executor/impl/txnscope_createtable_test.go +++ b/pkg/eventprocessor/impl/executor/impl/txnscope_createtable_test.go @@ -36,7 +36,7 @@ func TestCreateTable(t *testing.T) { // Check that the table was registered in the system-table. tableID, _ := tables.NewTableID("100") - table, err := gatewayimpl.NewGatewayStore(ex.db, nil).GetTable(ctx, 1337, tableID) + table, err := gatewayimpl.NewGatewayStore(ex.db).GetTable(ctx, 1337, tableID) require.NoError(t, err) require.Equal(t, tableID, table.ID) require.Equal(t, "0xb451cee4A42A652Fe77d373BAe66D42fd6B8D8FF", table.Controller) diff --git a/pkg/parsing/impl/validator_test.go b/pkg/parsing/impl/validator_test.go index df773a20..770f9f4d 100644 --- a/pkg/parsing/impl/validator_test.go +++ b/pkg/parsing/impl/validator_test.go @@ -100,7 +100,7 @@ func TestReadRunSQL(t *testing.T) { if tc.expErrType == nil { require.NoError(t, err) require.NotNil(t, rs) - q, err := rs.GetQuery(nil) + q, err := rs.GetQuery(parsing.NewReadStatementResolver(nil)) require.NoError(t, err) require.Equal(t, tc.query, q) return @@ -111,6 +111,53 @@ func TestReadRunSQL(t *testing.T) { } } +func TestReadQueryWithParams(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + query string + params []string + expQuery string + expErrType interface{} + } + + tests := []testCase{ + { + name: "query all valid types", + query: "select * from foo where a = ? and b = ? and c = ? and d = ? and e = ? and f = ?", + params: []string{"1", "'str'", "\"str2\"", "null", "true", "false"}, + expQuery: "select * from foo where a=1 and b='str' and c='str2' and d=null and e=true and f=false", + expErrType: nil, + }, + } + + for _, it := range tests { + t.Run(it.name, func(tc testCase) func(t *testing.T) { + return func(t *testing.T) { + t.Parallel() + + parser := newParser(t, []string{"system_", "registry"}) + rs, err := parser.ValidateReadQuery(tc.query) + require.NoError(t, err) + + resolver := parsing.NewReadStatementResolver(nil) + err = resolver.PrepareParams(tc.params) + require.NoError(t, err) + if tc.expErrType == nil { + require.NoError(t, err) + require.NotNil(t, rs) + q, err := rs.GetQuery(resolver) + require.NoError(t, err) + require.Equal(t, tc.expQuery, q) + return + } + require.ErrorAs(t, err, tc.expErrType) + } + }(it)) + } +} + func TestWriteQuery(t *testing.T) { t.Parallel() diff --git a/pkg/parsing/readstatementresolver.go b/pkg/parsing/readstatementresolver.go index 8cf885b7..4233866d 100644 --- a/pkg/parsing/readstatementresolver.go +++ b/pkg/parsing/readstatementresolver.go @@ -1,21 +1,74 @@ package parsing import ( + "errors" + "strconv" + "strings" + + "github.com/tablelandnetwork/sqlparser" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/pkg/sharedmemory" ) // ReadStatementResolver implements the interface for custom functions resolution of read statements. type ReadStatementResolver struct { - sm *sharedmemory.SharedMemory + sm *sharedmemory.SharedMemory + values []sqlparser.Expr } // NewReadStatementResolver creates a new ReadStatementResolver. func NewReadStatementResolver(sm *sharedmemory.SharedMemory) *ReadStatementResolver { - return &ReadStatementResolver{sm: sm} + return &ReadStatementResolver{sm: sm, values: make([]sqlparser.Expr, 0)} } // GetBlockNumber returns the block number for a given chain id. func (rqr *ReadStatementResolver) GetBlockNumber(chainID int64) (int64, bool) { return rqr.sm.GetLastSeenBlockNumber(tableland.ChainID(chainID)) } + +// GetBindValues returns a slice of values to be bound to their respective parameters. +func (rqr *ReadStatementResolver) GetBindValues() []sqlparser.Expr { + return rqr.values +} + +// PrepareParams prepare the params to the correct type. +func (rqr *ReadStatementResolver) PrepareParams(params []string) error { + values := make([]sqlparser.Expr, len(params)) + for i, param := range params { + if strings.EqualFold(strings.ToLower(param), "null") { + values[i] = &sqlparser.NullValue{} + continue + } + + if strings.EqualFold(strings.ToLower(param), "true") { + values[i] = sqlparser.BoolValue(true) + continue + } + + if strings.EqualFold(strings.ToLower(param), "false") { + values[i] = sqlparser.BoolValue(false) + continue + } + + if strings.HasPrefix(param, "'") && strings.HasSuffix(param, "'") { + values[i] = &sqlparser.Value{Type: sqlparser.StrValue, Value: []byte(param[1 : len(param)-1])} + continue + } + + if strings.HasPrefix(param, "\"") && strings.HasSuffix(param, "\"") { + values[i] = &sqlparser.Value{Type: sqlparser.StrValue, Value: []byte(param[1 : len(param)-1])} + continue + } + + if _, err := strconv.ParseInt(param, 10, 64); err == nil { + values[i] = &sqlparser.Value{Type: sqlparser.IntValue, Value: []byte(param)} + continue + } + + return errors.New("unknown param type") + } + + rqr.values = values + + return nil +} diff --git a/tests/fullstack/fullstack.go b/tests/fullstack/fullstack.go index 1a4304ae..b063d268 100644 --- a/tests/fullstack/fullstack.go +++ b/tests/fullstack/fullstack.go @@ -120,8 +120,9 @@ func CreateFullStack(t *testing.T, deps Deps) FullStack { gatewayService, err = gateway.NewGateway( parser, gatewayimpl.NewGatewayStore( - db, parsing.NewReadStatementResolver(sm), + db, ), + parsing.NewReadStatementResolver(sm), "https://testnets.tableland.network", "https://tables.tableland.xyz", "https://tables.tableland.xyz",