Repository: mailru/go-clickhouse
Pull request #118 (2021-04-14 17:03:58)

using POST requests and changing the uuid library to google
evolution of #115 
For using long SELECT's that are compatible to chproxy, we need to either put data in POST body or make a GET request with query parameter "query". 
As the request URL might be limited, we make all selects use POST requests.


Patch:
diff --git a/README.md b/README.md
index bb09a18..ba3bbc0 100644
--- a/README.md
+++ b/README.md
@@ -40,14 +40,14 @@ http://user:password@host:8123/clicks?read_timeout=10&write_timeout=20
 * LowCardinality(T)
 * [Array(T) (one-dimensional)](https://clickhouse.yandex/reference_en.html#Array(T))
 * [Nested(Name1 Type1, Name2 Type2, ...)](https://clickhouse.yandex/docs/en/data_types/nested_data_structures/nested/)
+* IPv4, IPv6
 
 Notes:
-database/sql does not allow to use big uint64 values.
-It is recommended use type `UInt64` which is provided by driver for such kind of values.
-type `[]byte` are used as raw string (without quoting)
-for passing value of type `[]uint8` to driver as array - please use the wrapper `clickhouse.Array`
-for passing decimal value please use the wrappers `clickhouse.Decimal*`
-for passing IPv4/IPv6 types use `clickhouse.IP`
+* database/sql does not allow to use big uint64 values. It is recommended use type `UInt64` which is provided by driver for such kind of values.
+* type `[]byte` are used as raw string (without quoting)
+* for passing value of type `[]uint8` to driver as array - please use the wrapper `clickhouse.Array`
+* for passing decimal value please use the wrappers `clickhouse.Decimal*`
+* for passing IPv4/IPv6 types use `clickhouse.IP`
 
 ## Supported request params
 
diff --git a/conn.go b/conn.go
index e3a05a0..961e85c 100644
--- a/conn.go
+++ b/conn.go
@@ -17,7 +17,7 @@ import (
 	"sync/atomic"
 	"time"
 
-	"github.com/gofrs/uuid"
+	"github.com/google/uuid"
 )
 
 type key int
@@ -203,7 +203,7 @@ func (c *conn) killQuery(req *http.Request, args []driver.Value) error {
 	}
 	ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
 	defer cancelFunc()
-	req, err := c.buildRequest(ctx, query, args, false)
+	req, err := c.buildRequest(ctx, query, args)
 	if err != nil {
 		return err
 	}
@@ -223,7 +223,7 @@ func (c *conn) query(ctx context.Context, query string, args []driver.Value) (dr
 	if atomic.LoadInt32(&c.closed) != 0 {
 		return nil, driver.ErrBadConn
 	}
-	req, err := c.buildRequest(ctx, query, args, true)
+	req, err := c.buildRequest(ctx, query, args)
 	if err != nil {
 		return nil, err
 	}
@@ -245,7 +245,7 @@ func (c *conn) exec(ctx context.Context, query string, args []driver.Value) (dri
 	if atomic.LoadInt32(&c.closed) != 0 {
 		return nil, driver.ErrBadConn
 	}
-	req, err := c.buildRequest(ctx, query, args, false)
+	req, err := c.buildRequest(ctx, query, args)
 	if err != nil {
 		return nil, err
 	}
@@ -285,41 +285,29 @@ func (c *conn) doRequest(ctx context.Context, req *http.Request) (io.ReadCloser,
 	return resp.Body, nil
 }
 
-func (c *conn) buildRequest(ctx context.Context, query string, params []driver.Value, readonly bool) (*http.Request, error) {
-	var (
-		method string
-		err    error
-	)
+func (c *conn) buildRequest(ctx context.Context, query string, params []driver.Value) (*http.Request, error) {
+	var err error
 	if len(params) > 0 {
 		if query, err = interpolateParams(query, params); err != nil {
 			return nil, err
 		}
 	}
 
-	var (
-		bodyReader io.Reader
-		bodyWriter io.WriteCloser
-	)
-	if readonly {
-		method = http.MethodGet
-	} else {
-		method = http.MethodPost
-		bodyReader, bodyWriter = io.Pipe()
-		go func() {
-			if c.useGzipCompression {
-				gz := gzip.NewWriter(bodyWriter)
-				gz.Write([]byte(query))
-				gz.Close()
-				bodyWriter.Close()
-			} else {
-				bodyWriter.Write([]byte(query))
-				bodyWriter.Close()
-			}
-		}()
-	}
+	bodyReader, bodyWriter := io.Pipe()
+	go func() {
+		if c.useGzipCompression {
+			gz := gzip.NewWriter(bodyWriter)
+			gz.Write([]byte(query))
+			gz.Close()
+			bodyWriter.Close()
+		} else {
+			bodyWriter.Write([]byte(query))
+			bodyWriter.Close()
+		}
+	}()
 	c.log("query: ", query)
 
-	req, err := http.NewRequest(method, c.url.String(), bodyReader)
+	req, err := http.NewRequest(http.MethodPost, c.url.String(), bodyReader)
 	if err != nil {
 		return nil, err
 	}
@@ -341,12 +329,7 @@ func (c *conn) buildRequest(ctx context.Context, query string, params []driver.V
 		}
 		queryID, queryOk := ctx.Value(QueryID).(string)
 		if c.killQueryOnErr && (!queryOk || queryID == "") {
-			queryUUID, err := uuid.NewV4()
-			if err != nil {
-				c.log("can't generate query_id: ", err)
-			} else {
-				queryID = queryUUID.String()
-			}
+			queryID = uuid.New().String()
 		}
 		if queryID != "" {
 			if reqQuery == nil {
@@ -356,17 +339,11 @@ func (c *conn) buildRequest(ctx context.Context, query string, params []driver.V
 		}
 
 	}
-	if method == http.MethodGet {
-		if reqQuery == nil {
-			reqQuery = req.URL.Query()
-		}
-		reqQuery.Add("query", query)
-	}
 	if reqQuery != nil {
 		req.URL.RawQuery = reqQuery.Encode()
 	}
 
-	if method == http.MethodPost && c.useGzipCompression {
+	if c.useGzipCompression {
 		req.Header.Set("Content-Encoding", "gzip")
 	}
 
diff --git a/conn_go18.go b/conn_go18.go
index 16fa33e..02543e0 100644
--- a/conn_go18.go
+++ b/conn_go18.go
@@ -15,7 +15,7 @@ func (c *conn) Ping(ctx context.Context) error {
 		return ErrTransportNil
 	}
 
-	req, err := c.buildRequest(ctx, "select 1", nil, true)
+	req, err := c.buildRequest(ctx, "select 1", nil)
 	if err != nil {
 		return err
 	}
diff --git a/go.mod b/go.mod
index 1db4313..05bf611 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,7 @@
 module github.com/mailru/go-clickhouse
 
 require (
-	github.com/gofrs/uuid v3.2.0+incompatible
+	github.com/google/uuid v1.2.0
 	github.com/stretchr/testify v1.3.0
 )
 
diff --git a/go.sum b/go.sum
index 82c010b..479de1f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,10 +1,9 @@
 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/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
-github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=


Test patch:
diff --git a/conn_test.go b/conn_test.go
index 6d128fd..b707bc5 100644
--- a/conn_test.go
+++ b/conn_test.go
@@ -7,11 +7,11 @@ import (
 	"database/sql/driver"
 	"io/ioutil"
 	"net/http"
-	"net/url"
+	"strings"
 	"testing"
 	"time"
 
-	"github.com/gofrs/uuid"
+	"github.com/google/uuid"
 	"github.com/stretchr/testify/suite"
 )
 
@@ -180,10 +180,8 @@ func (s *connSuite) TestServerError() {
 
 func (s *connSuite) TestServerKillQuery() {
 	// kill query and check if it is cancelled
-	queryUUID, err := uuid.NewV4()
-	s.Require().NoError(err)
-	queryID := queryUUID.String()
-	_, err = s.connWithKillQuery.QueryContext(context.WithValue(context.Background(), QueryID, queryID), "SELECT sleep(3)")
+	queryID := uuid.New().String()
+	_, err := s.connWithKillQuery.QueryContext(context.WithValue(context.Background(), QueryID, queryID), "SELECT sleep(3)")
 	s.Error(err)
 	s.Contains(err.Error(), "net/http: timeout awaiting response headers")
 	rows := s.connWithKillQuery.QueryRow("SELECT count(query_id) FROM system.processes where query_id=? and is_cancelled=?", queryID, 1)
@@ -193,9 +191,7 @@ func (s *connSuite) TestServerKillQuery() {
 	s.Equal(1, amount)
 
 	// not kill query and check if it is not cancelled
-	queryUUID, err = uuid.NewV4()
-	s.Require().NoError(err)
-	queryID = queryUUID.String()
+	queryID = uuid.New().String()
 	_, err = s.connWithKillQuery.QueryContext(context.WithValue(context.Background(), QueryID, queryID), "SELECT sleep(0.5)")
 	s.NoError(err)
 	rows = s.connWithKillQuery.QueryRow("SELECT count(query_id) FROM system.processes where query_id=? and is_cancelled=?", queryID, 0)
@@ -212,21 +208,24 @@ func (s *connSuite) TestBuildRequestReadonlyWithAuth() {
 	cfg.User = "user"
 	cfg.Password = "password"
 	cn := newConn(cfg)
-	req, err := cn.buildRequest(context.Background(), "SELECT 1", nil, true)
+	req, err := cn.buildRequest(context.Background(), "SELECT 1", nil)
 	if s.NoError(err) {
 		user, password, ok := req.BasicAuth()
 		s.True(ok)
 		s.Equal("user", user)
 		s.Equal("password", password)
-		s.Equal(http.MethodGet, req.Method)
-		s.Equal(cn.url.String()+"&query="+url.QueryEscape("SELECT 1"), req.URL.String())
+		s.Equal(http.MethodPost, req.Method)
+		s.Equal(cn.url.String(), req.URL.String())
 		s.Nil(req.URL.User)
+		b, err := ioutil.ReadAll(req.Body)
+		s.Require().NoError(err)
+		s.Equal("SELECT 1", string(b))
 	}
 }
 
 func (s *connSuite) TestBuildRequestReadWriteWOAuth() {
 	cn := newConn(NewConfig())
-	req, err := cn.buildRequest(context.Background(), "INSERT 1 INTO num", nil, false)
+	req, err := cn.buildRequest(context.Background(), "INSERT 1 INTO num", nil)
 	if s.NoError(err) {
 		_, _, ok := req.BasicAuth()
 		s.False(ok)
@@ -271,7 +270,7 @@ func (s *connSuite) TestBuildRequestWithQueryId() {
 		},
 	}
 	for _, tc := range testCases {
-		req, err := cn.buildRequest(context.WithValue(context.Background(), QueryID, tc.queryID), "INSERT 1 INTO num", nil, false)
+		req, err := cn.buildRequest(context.WithValue(context.Background(), QueryID, tc.queryID), "INSERT 1 INTO num", nil)
 		if s.NoError(err) {
 			s.Equal(http.MethodPost, req.Method)
 			s.Equal(tc.expected, req.URL.String())
@@ -314,7 +313,7 @@ func (s *connSuite) TestBuildRequestWithQuotaKey() {
 		},
 	}
 	for _, tc := range testCases {
-		req, err := cn.buildRequest(context.WithValue(context.Background(), QuotaKey, tc.quotaKey), "SELECT 1", nil, false)
+		req, err := cn.buildRequest(context.WithValue(context.Background(), QuotaKey, tc.quotaKey), "SELECT 1", nil)
 		if s.NoError(err) {
 			s.Equal(http.MethodPost, req.Method)
 			s.Equal(tc.expected, req.URL.String())
@@ -363,7 +362,7 @@ func (s *connSuite) TestBuildRequestWithQueryIdAndQuotaKey() {
 		ctx := context.Background()
 		ctx = context.WithValue(ctx, QuotaKey, tc.quotaKey)
 		ctx = context.WithValue(ctx, QueryID, tc.queryID)
-		req, err := cn.buildRequest(ctx, "SELECT 1", nil, false)
+		req, err := cn.buildRequest(ctx, "SELECT 1", nil)
 		if s.NoError(err) {
 			s.Equal(http.MethodPost, req.Method)
 			s.Equal(tc.expected, req.URL.String())
@@ -373,7 +372,7 @@ func (s *connSuite) TestBuildRequestWithQueryIdAndQuotaKey() {
 func (s *connSuite) TestBuildRequestParamsInterpolation() {
 	query := `INSERT INTO test (str) VALUES ("Question?")`
 	cn := newConn(NewConfig())
-	req, err := cn.buildRequest(context.Background(), query, make([]driver.Value, 0), false)
+	req, err := cn.buildRequest(context.Background(), query, make([]driver.Value, 0))
 	if s.NoError(err) {
 		body, e := ioutil.ReadAll(req.Body)
 		if s.NoError(e) {
@@ -386,7 +385,7 @@ func (s *connSuite) TestRequestBodyGzipCompression() {
 	query := `INSERT INTO test (str) VALUES ("Question?")`
 	cn := newConn(NewConfig())
 	cn.useGzipCompression = true
-	req, err := cn.buildRequest(context.Background(), query, make([]driver.Value, 0), false)
+	req, err := cn.buildRequest(context.Background(), query, make([]driver.Value, 0))
 	if s.NoError(err) {
 		s.Contains(req.Header, "Content-Encoding")
 		gz, err := gzip.NewReader(req.Body)
@@ -400,6 +399,19 @@ func (s *connSuite) TestRequestBodyGzipCompression() {
 	}
 }
 
+func (s *connSuite) TestLongRequest() {
+	expected := strings.Repeat("x", 100000)
+	rows, err := s.conn.Query("SELECT ?", expected)
+	if s.NoError(err) {
+		rows.Next()
+		var actual string
+		err = rows.Scan(&actual)
+		if s.NoError(err) {
+			s.Equal(expected, actual)
+		}
+	}
+}
+
 func TestConn(t *testing.T) {
 	suite.Run(t, new(connSuite))
 }