Repository: tablelandnetwork/go-tableland
Pull request #625 (2024-05-23 19:59:47)

adds support for query parameters
# Summary

- This PR adds support for the new SQL parser that is able to interpret placeholders (`?`) and replace them with query parameters that come from the request. You can now perform the following queries:
  
  - `GET /query?statement="select * from my_table where id = ?"&params=1`
  - `POST --json '{"statement": "SELECT * FROM my_table WHERE counter = ?", "params" : [1], "unwrap": true, "extract": true}'  /query `

- If you want to provide multiple params values, you just repeat the `params` with new values. For example `&params=1&params=2&params=3`. And if you want to provide a string, you gotta enclose it `&params="3"`.

- This PR depends on tablelandnetwork/go-sqlparser#388 and tablelandnetwork/docs#198

# Context

The context for this PR can be found at [ENG-852](https://linear.app/tableland/issue/ENG-852/dimo-request-add-placeholders-and-query-params-for-query-endpoint).

Make sure you understand tablelandnetwork/go-sqlparser#388 and tablelandnetwork/docs#198 before reviewing this one.


# Implementation overview 
This PR changes a lot of files, but most of the changes are just a side effect of the main changes. Here's an overview on the most important changes:

## Controller

The controller needed to be changed to capture the `params` query parameter. Both GET /query and POST /query were changed. The `params` is captured as a slice of string (`[]string`). In the case of the POST, since we have an `[]interface{}` after the JSON is decoded into memory, we try to convert it to `[]string`. 

## Resolver is now passed to the `GatewayService`

I made a change to make the resolver a dependency of the `GatewayService`. See the implication of this change [here](https://github.com/tablelandnetwork/go-tableland/pull/625/files#diff-ae22ad1e20b89eaefa8e09e4ed89443ac10fdc555b9767625f3866e3853100afR445-R450), [here](https://github.com/tablelandnetwork/go-tableland/pull/625/files#diff-68a1cc0c673b754f20006fd3916c6db4120489a84943d561de2a79507ef7f756L61-L93) and [here](https://github.com/tablelandnetwork/go-tableland/pull/625/files#diff-68a1cc0c673b754f20006fd3916c6db4120489a84943d561de2a79507ef7f756R169-R179). The reason for that is that it makes more sense for the `GatewayService` to attach the parameters values to the resolver, than the storage layer. The storage layer is just responsible for executing the query.

## The resolver is not a dependency of `GatewayStore` and is now passed in the `Read` method

We already mentioned how the resolver is passed to a `GatewayService` instead of `GatewayStore`. In order to build the final statement, we need to pass the resolver to the `Read` method. See it [here](https://github.com/tablelandnetwork/go-tableland/pull/625/files#diff-99f3427d646aa36492629d1d416848f95f0a0b4f08a08da61876e5a3cd901a55R32-R35):

## The resolver implementation needs to implement `GetBindValues() []sqlparser.Expr`

We must implement the new method of our resolver (remember [this](https://github.com/tablelandnetwork/go-sqlparser/pull/388/files#diff-0945956cd950d047e7fa866dcf34fee7d29cbce277d4e5e5cd621f2213f3be6cR124)? ). For that, we make use of [`PrepareParams`](https://github.com/tablelandnetwork/go-tableland/pull/625/files#diff-10e8c04527340840b0c7c8372c7fecd35c48df30792992a89f371588a3aa1e63R35) that converts `[]string` that comes from the controller to `[]sqlparser.Expr` and store those values inside the struct. [Here](https://github.com/tablelandnetwork/go-tableland/pull/625/files#diff-68a1cc0c673b754f20006fd3916c6db4120489a84943d561de2a79507ef7f756R175)'s how `PrepareParams` is used.


# Review orientation

1. Start at `internal/router/controllers/controller.go` and understand the change made in the controller
2. Go to `cmd/api/main.go` and see how the resolver is now passed to the gateway and not to the storage layer then check the changes at `internal/gateway/gateway.go`
3. Check the `internal/gateway/impl/gateway_store.go` file
4. Check the `pkg/parsing/readstatementresolver.go` file
5. Check the changes made in the Go client (`pkg/client/v1/readquery.go`)
6. The changes in the other files are just to adjust to the changes explained above





Patch:
diff --git a/Makefile b/Makefile
index 0d833cf..c279bdf 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 62480ab..eb5ffc7 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 8f58680..a7f451d 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 46b797d..c526d2d 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 9a9e66f..af5f05e 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 907acd2..690c5d9 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 9bb05ed..4ea5c7c 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 89f449b..2f513cc 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/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 0000000..f05b5bc
--- /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 339834e..2ccd9e3 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 699befd..8367797 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/mocks/Gateway.go b/mocks/Gateway.go
index e73b00d..6de90be 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/readquery.go b/pkg/client/v1/readquery.go
index de226f5..b3c7f35 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(&params)
@@ -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/parsing/readstatementresolver.go b/pkg/parsing/readstatementresolver.go
index 8cf885b..4233866 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
+}


Test patch:
diff --git a/internal/gateway/impl/gateway_store_test.go b/internal/gateway/impl/gateway_store_test.go
index 7176d22..1489015 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/controller_test.go b/internal/router/controllers/controller_test.go
index 5cd20b7..c0bd61d 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 bb695ce..c252638 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/pkg/client/v1/client_test.go b/pkg/client/v1/client_test.go
index 0c8914b..4b9c1b0 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/eventprocessor/impl/eventprocessor_test.go b/pkg/eventprocessor/impl/eventprocessor_test.go
index ab63288..0138cea 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 5cea424..0e8b129 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 df773a2..770f9f4 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/tests/fullstack/fullstack.go b/tests/fullstack/fullstack.go
index 1a4304a..b063d26 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",