Repository: influxdata/influxdb-client-go (go)

Issue:
Parametrically build query, rather than creating large string
Currently, influxdb-client-go (and other InfluxDB 2.0 client libraries) uses a string to query an InfluxDB database. This works well if the query is static, but if the query is dynamic, it can lead to some problems if IDs of measurements/fields/names or values of time have to be dynamically entered into the query. Additionally, if a user was given control over what some measurements/fields/names are called at runtime, it could lead to potential SQL injection. 

Ideally, it should be possible to parametrically build a query out of variables or values; for example, calling a func/type to create a name or filter.

Pull request description:
feat: adding flux query parameters
## Proposed Changes

Added support for [Flux query parameters](https://docs.influxdata.com/influxdb/cloud/query-data/parameterized-queries/), available in InfluxDB Cloud only at this moment.

`QueryAPI` is extended with `QueryWithParams` and `QueryRawWithParams`.

Query parameters can be passed as a struct or map. Param values can be only simple types or `time.Time`.
The name of a struct field or a map key (must be a string) will become a param name
The name of the parameter represented by a struct field can be specified by JSON annotation:

```go
type Condition struct {
     Start  time.Time  `json:"start"`
     Field  string     `json:"field"`
     Value  float64    `json:"value"`
}
```

Additionally, this requires changing the minimum Go version to 1.17.

## Checklist

<!-- Checkboxes below this note can be erased if not applicable to your Pull Request. -->

- [x] CHANGELOG.md updated
- [x] Rebased/mergeable
- [x] A test has been added if appropriate
- [x] Tests pass
- [x] Commit messages are in [semantic format](https://seesparkbox.com/foundry/semantic_commit_messages)

Closes #146

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index bf410ac..6c372ea 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -36,7 +36,7 @@ jobs:
             mkdir -p /tmp/artifacts
             mkdir -p /tmp/test-results
       - run: sudo rm -rf /usr/local/go
-      - run: wget https://golang.org/dl/go1.13.14.linux-amd64.tar.gz -O /tmp/go.tgz
+      - run: wget https://golang.org/dl/go1.17.7.linux-amd64.tar.gz -O /tmp/go.tgz
       - run: sudo tar -C /usr/local -xzf /tmp/go.tgz
       - run: go version
       - run: go get -v -t -d ./...
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd27c27..c24c53f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
 ### Features
 - [#304](https://github.com/influxdata/influxdb-client-go/pull/304) Added public constructor for `QueryTableResult`
 - [#307](https://github.com/influxdata/influxdb-client-go/pull/307) Synced generated server API with the latest [oss.yml](https://github.com/influxdata/openapi/blob/master/contracts/oss.yml). 
+- [#308](https://github.com/influxdata/influxdb-client-go/pull/308) Added Flux query parameters. Supported by InfluxDB Cloud only now.
+- [#308](https://github.com/influxdata/influxdb-client-go/pull/308) Go 1.17 is required
 
 ## 2.7.0[2022-01-20]
 ### Features 
diff --git a/README.md b/README.md
index 6d1d33c..9cae6c4 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ This repository contains the reference Go client for InfluxDB 2.
     - [Basic Example](#basic-example)
     - [Writes in Detail](#writes)
     - [Queries in Detail](#queries)
+    - [Parametrized Queries](#parametrized-queries) 
     - [Concurrency](#concurrency)
     - [Proxy and redirects](#proxy-and-redirects)
     - [Checking Server State](#checking-server-state)
@@ -61,7 +62,7 @@ There are also other examples in the API docs:
 ## How To Use
 
 ### Installation
-**Go 1.13** or later is required.
+**Go 1.17** or later is required.
 
 1.  Add the client package your to your project dependencies (go.mod).
     ```sh
@@ -412,6 +413,77 @@ func main() {
     client.Close()
 }    
 ```
+### Parametrized Queries
+InfluxDB Cloud supports [Parameterized Queries](https://docs.influxdata.com/influxdb/cloud/query-data/parameterized-queries/)
+that let you dynamically change values in a query using the InfluxDB API. Parameterized queries make Flux queries more
+reusable and can also be used to help prevent injection attacks.
+
+InfluxDB Cloud inserts the params object into the Flux query as a Flux record named `params`. Use dot or bracket
+notation to access parameters in the `params` record in your Flux query. Parameterized Flux queries support only `int`
+, `float`, and `string` data types. To convert the supported data types into
+other [Flux basic data types, use Flux type conversion functions](https://docs.influxdata.com/influxdb/cloud/query-data/parameterized-queries/#supported-parameter-data-types).
+
+Query parameters can be passed as a struct or map. Param values can be only simple types or `time.Time`.
+The name of the parameter represented by a struct field can be specified by JSON annotation.
+
+Parameterized query example:
+> :warning: Parameterized Queries are supported only in InfluxDB Cloud. There is no support in InfluxDB OSS currently.
+```go
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/influxdata/influxdb-client-go/v2"
+)
+
+func main() {
+	// Create a new client using an InfluxDB server base URL and an authentication token
+	client := influxdb2.NewClient("http://localhost:8086", "my-token")
+	// Get query client
+	queryAPI := client.QueryAPI("my-org")
+	// Define parameters
+	parameters := struct {
+		Start string  `json:"start"`
+		Field string  `json:"field"`
+		Value float64 `json:"value"`
+	}{
+		"-1h",
+		"temperature",
+		25,
+	}
+	// Query with parameters
+	query := `from(bucket:"my-bucket")
+				|> range(start: duration(params.start)) 
+				|> filter(fn: (r) => r._measurement == "stat")
+				|> filter(fn: (r) => r._field == params.field)
+				|> filter(fn: (r) => r._value > params.value)`
+
+	// Get result
+	result, err := queryAPI.QueryWithParams(context.Background(), query, parameters)
+	if err == nil {
+		// Iterate over query response
+		for result.Next() {
+			// Notice when group key has changed
+			if result.TableChanged() {
+				fmt.Printf("table: %s\n", result.TableMetadata().String())
+			}
+			// Access data
+			fmt.Printf("value: %v\n", result.Record().Value())
+		}
+		// check for an error
+		if result.Err() != nil {
+			fmt.Printf("query parsing error: %s\n", result.Err().Error())
+		}
+	} else {
+		panic(err)
+	}
+	// Ensures background processes finishes
+	client.Close()
+}
+```
+
 ### Concurrency
 InfluxDB Go Client can be used in a concurrent environment. All its functions are thread-safe.
 
diff --git a/api/query.go b/api/query.go
index db75fd5..9582d45 100644
--- a/api/query.go
+++ b/api/query.go
@@ -18,6 +18,7 @@ import (
 	"net/http"
 	"net/url"
 	"path"
+	"reflect"
 	"strconv"
 	"strings"
 	"sync"
@@ -43,11 +44,35 @@ const (
 )
 
 // QueryAPI provides methods for performing synchronously flux query against InfluxDB server.
+//
+// Flux query can contain reference to parameters, which must be passed via queryParams.
+// it can be a struct or map. Param values can be only simple types or time.Time.
+// The name of a struct field or a map key (must be a string) will be a param name.
+// The name of the parameter represented by a struct field can be specified by JSON annotation:
+//
+// type Condition struct {
+//     Start  time.Time  `json:"start"`
+//     Field  string     `json:"field"`
+//     Value  float64    `json:"value"`
+//	}
+//
+//  Parameters are then accessed via the Flux params object:
+//
+//  query:= `from(bucket: "environment")
+// 		|> range(start: time(v: params.start))
+//		|> filter(fn: (r) => r._measurement == "air")
+//		|> filter(fn: (r) => r._field == params.field)
+//		|> filter(fn: (r) => r._value > params.value)`
+//
 type QueryAPI interface {
 	// QueryRaw executes flux query on the InfluxDB server and returns complete query result as a string with table annotations according to dialect
 	QueryRaw(ctx context.Context, query string, dialect *domain.Dialect) (string, error)
+	// QueryRawWithParams executes flux parametrized query on the InfluxDB server and returns complete query result as a string with table annotations according to dialect
+	QueryRawWithParams(ctx context.Context, query string, dialect *domain.Dialect, params interface{}) (string, error)
 	// Query executes flux query on the InfluxDB server and returns QueryTableResult which parses streamed response into structures representing flux table parts
 	Query(ctx context.Context, query string) (*QueryTableResult, error)
+	// QueryWithParams executes flux parametrized query  on the InfluxDB server and returns QueryTableResult which parses streamed response into structures representing flux table parts
+	QueryWithParams(ctx context.Context, query string, params interface{}) (*QueryTableResult, error)
 }
 
 // NewQueryAPI returns new query client for querying buckets belonging to org
@@ -58,6 +83,28 @@ func NewQueryAPI(org string, service http2.Service) QueryAPI {
 	}
 }
 
+// QueryTableResult parses streamed flux query response into structures representing flux table parts
+// Walking though the result is done by repeatedly calling Next() until returns false.
+// Actual flux table info (columns with names, data types, etc) is returned by TableMetadata() method.
+// Data are acquired by Record() method.
+// Preliminary end can be caused by an error, so when Next() return false, check Err() for an error
+type QueryTableResult struct {
+	io.Closer
+	csvReader     *csv.Reader
+	tablePosition int
+	tableChanged  bool
+	table         *query.FluxTableMetadata
+	record        *query.FluxRecord
+	err           error
+}
+
+// NewQueryTableResult returns new QueryTableResult
+func NewQueryTableResult(rawResponse io.ReadCloser) *QueryTableResult {
+	csvReader := csv.NewReader(rawResponse)
+	csvReader.FieldsPerRecord = -1
+	return &QueryTableResult{Closer: rawResponse, csvReader: csvReader}
+}
+
 // queryAPI implements QueryAPI interface
 type queryAPI struct {
 	org         string
@@ -66,13 +113,32 @@ type queryAPI struct {
 	lock        sync.Mutex
 }
 
+//  queryBody holds the body for an HTTP query request.
+type queryBody struct {
+	Dialect *domain.Dialect  `json:"dialect,omitempty"`
+	Query   string           `json:"query"`
+	Type    domain.QueryType `json:"type"`
+	Params  interface{}      `json:"params,omitempty"`
+}
+
 func (q *queryAPI) QueryRaw(ctx context.Context, query string, dialect *domain.Dialect) (string, error) {
+	return q.QueryRawWithParams(ctx, query, dialect, nil)
+}
+
+func (q *queryAPI) QueryRawWithParams(ctx context.Context, query string, dialect *domain.Dialect, params interface{}) (string, error) {
+	if err := checkParamsType(params); err != nil {
+		return "", err
+	}
 	queryURL, err := q.queryURL()
 	if err != nil {
 		return "", err
 	}
-	queryType := domain.QueryTypeFlux
-	qr := domain.Query{Query: query, Type: &queryType, Dialect: dialect}
+	qr := queryBody{
+		Query:   query,
+		Type:    domain.QueryTypeFlux,
+		Dialect: dialect,
+		Params:  params,
+	}
 	qrJSON, err := json.Marshal(qr)
 	if err != nil {
 		return "", err
@@ -118,13 +184,24 @@ func DefaultDialect() *domain.Dialect {
 }
 
 func (q *queryAPI) Query(ctx context.Context, query string) (*QueryTableResult, error) {
+	return q.QueryWithParams(ctx, query, nil)
+}
+
+func (q *queryAPI) QueryWithParams(ctx context.Context, query string, params interface{}) (*QueryTableResult, error) {
 	var queryResult *QueryTableResult
+	if err := checkParamsType(params); err != nil {
+		return nil, err
+	}
 	queryURL, err := q.queryURL()
 	if err != nil {
 		return nil, err
 	}
-	queryType := domain.QueryTypeFlux
-	qr := domain.Query{Query: query, Type: &queryType, Dialect: DefaultDialect()}
+	qr := queryBody{
+		Query:   query,
+		Type:    domain.QueryTypeFlux,
+		Dialect: DefaultDialect(),
+		Params:  params,
+	}
 	qrJSON, err := json.Marshal(qr)
 	if err != nil {
 		return nil, err
@@ -172,25 +249,69 @@ func (q *queryAPI) queryURL() (string, error) {
 	return q.url, nil
 }
 
-// QueryTableResult parses streamed flux query response into structures representing flux table parts
-// Walking though the result is done by repeatedly calling Next() until returns false.
-// Actual flux table info (columns with names, data types, etc) is returned by TableMetadata() method.
-// Data are acquired by Record() method.
-// Preliminary end can be caused by an error, so when Next() return false, check Err() for an error
-type QueryTableResult struct {
-	io.Closer
-	csvReader     *csv.Reader
-	tablePosition int
-	tableChanged  bool
-	table         *query.FluxTableMetadata
-	record        *query.FluxRecord
-	err           error
+// checkParamsType validates the value is struct with simple type fields
+// or a map with key as string and value as a simple type
+func checkParamsType(p interface{}) error {
+	if p == nil {
+		return nil
+	}
+	t := reflect.TypeOf(p)
+	v := reflect.ValueOf(p)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+	if t.Kind() != reflect.Struct && t.Kind() != reflect.Map {
+		return fmt.Errorf("cannot use %v as query params", t)
+	}
+	switch t.Kind() {
+	case reflect.Struct:
+		fields := reflect.VisibleFields(t)
+		for _, f := range fields {
+			fv := v.FieldByIndex(f.Index)
+			t := getFieldType(fv)
+			if !validParamType(t) {
+				return fmt.Errorf("cannot use field '%s' of type '%v' as a query param", f.Name, t)
+			}
+
+		}
+	case reflect.Map:
+		key := t.Key()
+		if key.Kind() != reflect.String {
+			return fmt.Errorf("cannot use map key of type '%v' for query param name", key)
+		}
+		for _, k := range v.MapKeys() {
+			f := v.MapIndex(k)
+			t := getFieldType(f)
+			if !validParamType(t) {
+				return fmt.Errorf("cannot use map value type '%v' as a query param", t)
+			}
+		}
+	}
+	return nil
 }
 
-func NewQueryTableResult(rawResponse io.ReadCloser) *QueryTableResult {
-	csvReader := csv.NewReader(rawResponse)
-	csvReader.FieldsPerRecord = -1
-	return &QueryTableResult{Closer: rawResponse, csvReader: csvReader}
+// getFieldType extracts type of value
+func getFieldType(v reflect.Value) reflect.Type {
+	t := v.Type()
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+	if t.Kind() == reflect.Interface && !v.IsNil() {
+		t = reflect.ValueOf(v.Interface()).Type()
+	}
+	return t
+}
+
+// timeType is the exact type for the Time
+var timeType = reflect.TypeOf(time.Time{})
+
+// validParamType validates that t is primitive type or string or interface
+func validParamType(t reflect.Type) bool {
+	return (t.Kind() > reflect.Invalid && t.Kind() < reflect.Complex64) ||
+		t.Kind() == reflect.String ||
+		t == timeType
 }
 
 // TablePosition returns actual flux table position in the result, or -1 if no table was found yet
diff --git a/go.mod b/go.mod
index 0e2d8c7..c9cd94b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/influxdata/influxdb-client-go/v2
 
-go 1.13
+go 1.17
 
 require (
 	github.com/deepmap/oapi-codegen v1.8.2
@@ -10,3 +10,8 @@ require (
 	golang.org/x/net v0.0.0-20210119194325-5f4716e94777
 	gopkg.in/yaml.v2 v2.3.0
 )
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+)
diff --git a/go.sum b/go.sum
index bbcecf8..ae23a2f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,4 @@
 github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -21,7 +20,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
-github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
 github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
 github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -39,11 +37,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
@@ -78,11 +74,9 @@ golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=


Test patch:
diff --git a/api/examples_test.go b/api/examples_test.go
index a17a1c0..b9f50d5 100644
--- a/api/examples_test.go
+++ b/api/examples_test.go
@@ -175,6 +175,51 @@ func ExampleQueryAPI_query() {
 	client.Close()
 }
 
+func ExampleQueryAPI_queryWithParams() {
+	// Create a new client using an InfluxDB server base URL and an authentication token
+	client := influxdb2.NewClient("http://localhost:8086", "my-token")
+	// Get query client
+	queryAPI := client.QueryAPI("my-org")
+	// Define parameters
+	parameters := struct {
+		Start string  `json:"start"`
+		Field string  `json:"field"`
+		Value float64 `json:"value"`
+	}{
+		"-1h",
+		"temperature",
+		25,
+	}
+	// Query with parameters
+	query := `from(bucket:"my-bucket")
+				|> range(start: duration(params.start)) 
+				|> filter(fn: (r) => r._measurement == "stat")
+				|> filter(fn: (r) => r._field == params.field)
+				|> filter(fn: (r) => r._value > params.value)`
+
+	// Get result
+	result, err := queryAPI.QueryWithParams(context.Background(), query, parameters)
+	if err == nil {
+		// Iterate over query response
+		for result.Next() {
+			// Notice when group key has changed
+			if result.TableChanged() {
+				fmt.Printf("table: %s\n", result.TableMetadata().String())
+			}
+			// Access data
+			fmt.Printf("value: %v\n", result.Record().Value())
+		}
+		// check for an error
+		if result.Err() != nil {
+			fmt.Printf("query parsing error: %s\n", result.Err().Error())
+		}
+	} else {
+		panic(err)
+	}
+	// Ensures background processes finishes
+	client.Close()
+}
+
 func ExampleQueryAPI_queryRaw() {
 	// Create a new client using an InfluxDB server base URL and an authentication token
 	client := influxdb2.NewClient("http://localhost:8086", "my-token")
diff --git a/api/query_test.go b/api/query_test.go
index a1d878e..4dbabcf 100644
--- a/api/query_test.go
+++ b/api/query_test.go
@@ -885,6 +885,182 @@ func TestFluxError(t *testing.T) {
 
 }
 
+func TestQueryParamsTypes(t *testing.T) {
+	var i int8 = 1
+	var paramsTypeTests = []struct {
+		testName    string
+		params      interface{}
+		expectError string
+	}{
+		{
+			"structWillAllSupportedTypes",
+			struct {
+				B   bool
+				I   int
+				I8  int8
+				I16 int16
+				I32 int32
+				I64 int64
+				U   uint
+				U8  uint8
+				U16 uint16
+				U32 uint32
+				U64 uint64
+				F32 float32
+				F64 float64
+				D   time.Duration
+				T   time.Time
+			}{},
+			"",
+		},
+		{
+			"structWithInvalidFieldEmptyInterface",
+			struct {
+				F interface{}
+			}{},
+			"cannot use field 'F' of type 'interface {}' as a query param",
+		},
+		{
+			"structWithFieldAsValidInterfaceValue",
+			struct {
+				F interface{}
+			}{"string"},
+			"",
+		},
+		{
+			"structAsPointer",
+			&struct {
+				S string
+			}{"a"},
+			"",
+		},
+		{
+			"structWithInvalidFieldAsMap",
+			struct {
+				M map[string]string
+			}{},
+			"cannot use field 'M' of type 'map[string]string' as a query param",
+		},
+		{
+			"structWithFieldAsPointer",
+			struct {
+				P *int8
+			}{&i},
+			"",
+		},
+		{
+			"mapOfBool",
+			map[string]bool{},
+			"",
+		},
+		{
+			"mapOfFloat64",
+			map[string]float64{},
+			"",
+		},
+		{
+			"mapOfString",
+			map[string]string{},
+			"",
+		},
+		{
+			"mapOfTime",
+			map[string]time.Time{},
+			"",
+		},
+		{
+			"mapOfInterfaceEmpty",
+			map[string]interface{}{},
+			"",
+		},
+		{
+			"mapOfInterfaceWithValidValues",
+			map[string]interface{}{"s": "s", "t": time.Now()},
+			"",
+		},
+		{
+			"mapOfInterfaceWithStructInvalid",
+			map[string]interface{}{"s": struct {
+				a int
+			}{1}},
+			"cannot use map value type 'struct { a int }' as a query param",
+		},
+		{
+			"mapOfStructInvalid",
+			map[string]struct {
+				a int
+			}{"a": {1}},
+			"cannot use map value type 'struct { a int }' as a query param",
+		},
+		{
+			"mapWithInvalidKey",
+			map[int]string{},
+			"cannot use map key of type 'int' for query param name",
+		},
+		{
+			"invalidParamsType",
+			0,
+			"cannot use int as query params",
+		},
+	}
+	for _, test := range paramsTypeTests {
+		t.Run(test.testName, func(t *testing.T) {
+			err := checkParamsType(test.params)
+			if test.expectError != "" {
+				require.Error(t, err)
+				require.Equal(t, test.expectError, err.Error())
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+func TestQueryParamsSerialized(t *testing.T) {
+	expectedBody := `{"dialect":{"annotations":["datatype","group","default"],"delimiter":",","header":true},"query":"from(bucket: \"environment\") |\u003e range(start: time(v: params.start)) |\u003e filter(fn: (r) =\u003e r._measurement == \"air\") |\u003e filter(fn: (r) =\u003e r._field == params.field) |\u003e filter(fn: (r) =\u003e r._value \u003e params.value)","type":"flux","params":{"start":"2022-02-17T11:27:23+01:00","field":"field","value":24.4}}`
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.Method == http.MethodPost {
+			body, err := ioutil.ReadAll(r.Body)
+			if err != nil {
+				w.Header().Set("Content-Type", "application/json")
+				w.WriteHeader(http.StatusBadRequest)
+				_, _ = w.Write([]byte(`{"code":"invalid","message":"` + err.Error() + `"}`))
+			}
+			if string(body) != expectedBody {
+				fmt.Println("Error: Different bodies. Recv vs exp")
+				fmt.Println(string(body))
+				fmt.Println(expectedBody)
+				w.Header().Set("Content-Type", "application/json")
+				w.WriteHeader(http.StatusInternalServerError)
+				_, _ = w.Write(body)
+			} else {
+				w.Header().Set("Content-Type", "text/csv")
+				w.WriteHeader(http.StatusOK)
+			}
+		}
+	}))
+	defer server.Close()
+	condition := &struct {
+		Start time.Time `json:"start"`
+		Field string    `json:"field"`
+		Value float64   `json:"value"`
+	}{
+		mustParseTime("2022-02-17T11:27:23+01:00"),
+		"field",
+		24.4,
+	}
+	query := `from(bucket: "environment") |> range(start: time(v: params.start)) |> filter(fn: (r) => r._measurement == "air") |> filter(fn: (r) => r._field == params.field) |> filter(fn: (r) => r._value > params.value)`
+
+	queryAPI := NewQueryAPI("org", http2.NewService(server.URL, "a", http2.DefaultOptions()))
+
+	_, err := queryAPI.QueryRawWithParams(context.Background(), query, DefaultDialect(), condition)
+	require.NoError(t, err, err)
+
+	_, err = queryAPI.QueryWithParams(context.Background(), query, condition)
+	require.NoError(t, err, err)
+
+}
+
 func makeCSVstring(rows []string) string {
 	csvTable := strings.Join(rows, "\r\n")
 	return fmt.Sprintf("%s\r\n", csvTable)