Skip to content

Commit 6675d5e

Browse files
authored
feat: support nullable api response data (#133)
1 parent 00ad41a commit 6675d5e

18 files changed

+523
-492
lines changed

restful.go renamed to client.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,6 @@ func (c *APIClient) UploadToStage(ctx context.Context, stage *StageLocation, inp
602602
}
603603

604604
func (c *APIClient) GetPresignedURL(ctx context.Context, stage *StageLocation) (*PresignedResponse, error) {
605-
var headers string
606605
presignUploadSQL := fmt.Sprintf("PRESIGN UPLOAD %s", stage)
607606
resp, err := c.QuerySync(ctx, presignUploadSQL, nil)
608607
if err != nil {
@@ -611,17 +610,21 @@ func (c *APIClient) GetPresignedURL(ctx context.Context, stage *StageLocation) (
611610
if len(resp.Data) < 1 || len(resp.Data[0]) < 2 {
612611
return nil, errors.Errorf("generate presign url invalid response: %+v", resp.Data)
613612
}
614-
615-
result := &PresignedResponse{
616-
Method: resp.Data[0][0],
617-
Headers: make(map[string]string),
618-
URL: resp.Data[0][2],
613+
if resp.Data[0][0] == nil || resp.Data[0][1] == nil || resp.Data[0][2] == nil {
614+
return nil, errors.Errorf("generate presign url invalid response: %+v", resp.Data)
619615
}
620-
headers = resp.Data[0][1]
621-
err = json.Unmarshal([]byte(headers), &result.Headers)
616+
method := *resp.Data[0][0]
617+
url := *resp.Data[0][2]
618+
headers := map[string]string{}
619+
err = json.Unmarshal([]byte(*resp.Data[0][1]), &headers)
622620
if err != nil {
623621
return nil, errors.Wrap(err, "failed to unmarshal headers")
624622
}
623+
result := &PresignedResponse{
624+
Method: method,
625+
Headers: headers,
626+
URL: url,
627+
}
625628
return result, nil
626629
}
627630

restful_test.go renamed to client_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ func TestDoQuery(t *testing.T) {
8787
}
8888

8989
c := APIClient{
90-
host: "tn3ftqihs--bl.ch.aws-us-east-2.default.databend.com",
91-
tenant: "tn3ftqihs",
90+
host: "tnxxxxxxx.gw.aws-us-east-2.default.databend.com",
91+
tenant: "tnxxxxxxx",
9292
accessTokenLoader: NewStaticAccessTokenLoader("abc123"),
9393
warehouse: "small-abc",
9494
doRequestFunc: mockDoRequest,

helpers.go

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package godatabend
22

33
import (
4-
"bytes"
54
"fmt"
6-
"net/http"
75
"strings"
86
"time"
97

@@ -33,17 +31,6 @@ func formatDate(value time.Time) string {
3331
return quote(value.Format(dateFormat))
3432
}
3533

36-
func readResponse(response *http.Response) (result []byte, err error) {
37-
if response.ContentLength > 0 {
38-
result = make([]byte, 0, response.ContentLength)
39-
}
40-
buf := bytes.NewBuffer(result)
41-
defer response.Body.Close()
42-
_, err = buf.ReadFrom(response.Body)
43-
result = buf.Bytes()
44-
return
45-
}
46-
4734
func getTableFromInsertQuery(query string) (string, error) {
4835
if !strings.Contains(query, "insert") && !strings.Contains(query, "INSERT") {
4936
return "", errors.New("wrong insert statement")
@@ -62,31 +49,3 @@ func generateDescTable(query string) (string, error) {
6249
}
6350
return fmt.Sprintf("DESC %s", table), nil
6451
}
65-
66-
func databendParquetReflect(databendType string) string {
67-
68-
var parquetType string
69-
switch databendType {
70-
case "VARCHAR":
71-
parquetType = "type=BYTE_ARRAY, convertedtype=UTF8, encoding=PLAIN_DICTIONARY"
72-
73-
case "BOOLEAN":
74-
parquetType = "type=BOOLEAN"
75-
case "TINYINT", "SMALLINT", "INT":
76-
parquetType = "type=INT32"
77-
case "BIGINT":
78-
parquetType = "type=INT64"
79-
case "FLOAT":
80-
parquetType = "type=FLOAT"
81-
case "DOUBLE":
82-
parquetType = "type=DOUBLE"
83-
case "DATE":
84-
parquetType = "type=INT32, convertedtype=DATE"
85-
case "TIMESTAMP":
86-
parquetType = "type=INT64"
87-
case "ARRAY":
88-
parquetType = "type=LIST, convertedtype=LIST"
89-
90-
}
91-
return parquetType
92-
}

log.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
rlog "github.com/sirupsen/logrus"
1212
)
1313

14+
type contextKey string
15+
1416
// DBSessionIDKey is context key of session id
1517
const DBSessionIDKey contextKey = "LOG_SESSION_ID"
1618

query.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type QueryResponse struct {
3333
NodeID string `json:"node_id"`
3434
Session *json.RawMessage `json:"session"`
3535
Schema *[]DataField `json:"schema"`
36-
Data [][]string `json:"data"`
36+
Data [][]*string `json:"data"`
3737
State string `json:"state"`
3838
Error *QueryError `json:"error"`
3939
Stats *QueryStats `json:"stats"`

rows.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,12 @@ func (r *nextRows) Next(dest []driver.Value) error {
156156
r.respData.Data = r.respData.Data[1:]
157157

158158
for j := range lineData {
159-
reader := strings.NewReader(lineData[j])
159+
val := lineData[j]
160+
if val == nil {
161+
dest[j] = nil
162+
continue
163+
}
164+
reader := strings.NewReader(*val)
160165
v, err := r.parsers[j].Parse(reader)
161166
if err != nil {
162167
r.dc.log("fail to parse field", j, ", error: ", err)
@@ -177,18 +182,17 @@ func (r *nextRows) ColumnTypeDatabaseTypeName(index int) string {
177182
return r.types[index]
178183
}
179184

185+
// ColumnTypeDatabaseTypeName implements the driver.RowsColumnTypeNullable
186+
func (r *nextRows) ColumnTypeNullable(index int) (bool, bool) {
187+
return r.parsers[index].Nullable(), true
188+
}
189+
180190
// // ColumnTypeDatabaseTypeName implements the driver.RowsColumnTypeLength
181191
// func (r *nextRows) ColumnTypeLength(index int) (int64, bool) {
182192
// // TODO: implement this
183193
// return 10, true
184194
// }
185195

186-
// // ColumnTypeDatabaseTypeName implements the driver.RowsColumnTypeNullable
187-
// func (r *nextRows) ColumnTypeNullable(index int) (bool, bool) {
188-
// // TODO: implement this
189-
// return true, true
190-
// }
191-
192196
// // ColumnTypeDatabaseTypeName implements the driver.RowsColumnTypePrecisionScale
193197
// func (r *nextRows) ColumnTypePrecisionScale(index int) (int64, int64, bool) {
194198
// // TODO: implement this

rows_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import (
99
)
1010

1111
func TestTextRows(t *testing.T) {
12+
ptr1 := strPtr("1")
13+
ptr2 := strPtr("2")
14+
ptr3 := strPtr("2")
1215
rows, err := newNextRows(context.Background(), &DatabendConn{}, &QueryResponse{
13-
Data: [][]string{{"1", "2", "3"}, {"3", "2", "1"}},
16+
Data: [][]*string{{ptr1, ptr2, ptr3}, {ptr3, ptr2, ptr1}},
1417
Schema: &[]DataField{
1518
{Name: "age", Type: "Int32"},
1619
{Name: "height", Type: "Int64"},
@@ -28,3 +31,7 @@ func TestTextRows(t *testing.T) {
2831
assert.Equal(t, "Int32", rows.ColumnTypeDatabaseTypeName(0))
2932
assert.Equal(t, "String", rows.ColumnTypeDatabaseTypeName(2))
3033
}
34+
35+
func strPtr(s string) *string {
36+
return &s
37+
}

tests/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ services:
66
volumes:
77
- ./data:/data
88
databend:
9-
image: datafuselabs/databend
9+
image: datafuselabs/databend:nightly
1010
environment:
1111
- QUERY_DEFAULT_USER=databend
1212
- QUERY_DEFAULT_PASSWORD=databend

tests/main_test.go

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ const (
3232
)
3333

3434
var (
35-
dsn = "http://databend:databend@localhost:8000?presigned_url_disabled=true"
35+
dsn = "http://databend:databend@localhost:8000?presign=on"
3636
)
3737

3838
func init() {
3939
dsn = os.Getenv("TEST_DATABEND_DSN")
4040
// databend default
41-
// dsn = "http://root:@localhost:8000?presigned_url_disabled=true"
41+
// dsn = "http://root:@localhost:8000?presign=on"
4242

4343
// add user databend by uncommenting corresponding [[query.users]] section scripts/ci/deploy/config/databend-query-node-1.toml
44-
//dsn = "http://databend:databend@localhost:8000?presigned_url_disabled=true"
44+
//dsn = "http://databend:databend@localhost:8000?presign=on"
4545
}
4646

4747
func TestDatabendSuite(t *testing.T) {
@@ -65,13 +65,6 @@ func (s *DatabendTestSuite) SetupSuite() {
6565

6666
err = s.db.Ping()
6767
s.Nil(err)
68-
69-
rows, err := s.db.Query("select version()")
70-
s.Nil(err)
71-
result, err := scanValues(rows)
72-
s.Nil(err)
73-
74-
s.T().Logf("connected to databend: %s\n", result)
7568
}
7669

7770
func (s *DatabendTestSuite) TearDownSuite() {
@@ -103,6 +96,14 @@ func (s *DatabendTestSuite) TearDownTest() {
10396
s.r.Nil(err)
10497
}
10598

99+
func (s *DatabendTestSuite) TestVersion() {
100+
rows, err := s.db.Query("select version()")
101+
s.Nil(err)
102+
result, err := scanValues(rows)
103+
s.Nil(err)
104+
s.T().Logf("connected to databend: %s\n", result)
105+
}
106+
106107
// For load balance test
107108
func (s *DatabendTestSuite) TestCycleExec() {
108109
rows, err := s.db.Query("SELECT number from numbers(200) order by number")
@@ -115,9 +116,11 @@ func (s *DatabendTestSuite) TestQuoteStringQuery() {
115116
m := make(map[string]string, 0)
116117
m["message"] = "this is action 'with quote string'"
117118
x, err := json.Marshal(m)
119+
s.r.Nil(err)
118120
_, err = s.db.Exec(fmt.Sprintf("insert into %s values(?)", s.table2), string(x))
119121
s.r.Nil(err)
120122
rows, err := s.db.Query(fmt.Sprintf("select * from %s", s.table2))
123+
s.r.Nil(err)
121124
for rows.Next() {
122125
var t string
123126
_ = rows.Scan(&t)
@@ -272,17 +275,6 @@ func (s *DatabendTestSuite) TestServerError() {
272275
s.Contains(err.Error(), "error")
273276
}
274277

275-
func (s *DatabendTestSuite) TestQueryNull() {
276-
rows, err := s.db.Query("SELECT NULL")
277-
s.r.Nil(err)
278-
279-
result, err := scanValues(rows)
280-
s.r.Nil(err)
281-
s.r.Equal([][]interface{}{{"NULL"}}, result)
282-
283-
s.r.NoError(rows.Close())
284-
}
285-
286278
func (s *DatabendTestSuite) TestTransactionCommit() {
287279
tx, err := s.db.Begin()
288280
s.r.Nil(err)
@@ -298,7 +290,7 @@ func (s *DatabendTestSuite) TestTransactionCommit() {
298290

299291
result, err := scanValues(rows)
300292
s.r.Nil(err)
301-
s.r.Equal([][]interface{}{{"1", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL"}}, result)
293+
s.r.Equal([][]interface{}{{int64(1), nil, nil, "NULL", "NULL", nil, nil, nil, nil}}, result)
302294

303295
s.r.NoError(rows.Close())
304296
}
@@ -343,32 +335,39 @@ func (s *DatabendTestSuite) TestLongExec() {
343335
}
344336
}
345337

338+
func getNullableType(t reflect.Type) reflect.Type {
339+
if t.Kind() == reflect.Ptr {
340+
return t.Elem()
341+
}
342+
return t
343+
}
344+
346345
func scanValues(rows *sql.Rows) (interface{}, error) {
347346
var err error
348347
var result [][]interface{}
349348
ct, err := rows.ColumnTypes()
350349
if err != nil {
351350
return nil, err
352351
}
353-
types := make([]reflect.Type, len(ct))
354-
for i, v := range ct {
355-
types[i] = v.ScanType()
356-
}
357-
ptrs := make([]interface{}, len(types))
352+
vals := make([]any, len(ct))
358353
for rows.Next() {
359354
if err = rows.Err(); err != nil {
360355
return nil, err
361356
}
362-
for i, t := range types {
363-
ptrs[i] = reflect.New(t).Interface()
357+
for i := range ct {
358+
vals[i] = &dc.NullableValue{}
364359
}
365-
err = rows.Scan(ptrs...)
360+
err = rows.Scan(vals...)
366361
if err != nil {
367362
return nil, err
368363
}
369-
values := make([]interface{}, len(types))
370-
for i, p := range ptrs {
371-
values[i] = reflect.ValueOf(p).Elem().Interface()
364+
values := make([]interface{}, len(ct))
365+
for i, p := range vals {
366+
val, err := p.(*dc.NullableValue).Value()
367+
if err != nil {
368+
return nil, fmt.Errorf("failed to get value: %w", err)
369+
}
370+
values[i] = val
372371
}
373372
result = append(result, values)
374373
}

tests/nullable_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package tests
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
)
7+
8+
func (s *DatabendTestSuite) TestNullable() {
9+
_, err := s.db.Exec(fmt.Sprintf("INSERT INTO %s (i64) VALUES (?)", s.table), int64(1))
10+
s.r.Nil(err)
11+
12+
rows, err := s.db.Query(fmt.Sprintf("SELECT * FROM %s", s.table))
13+
s.r.Nil(err)
14+
result, err := scanValues(rows)
15+
s.r.Nil(err)
16+
s.r.Equal([][]interface{}{{int64(1), nil, nil, "NULL", "NULL", nil, nil, nil, nil}}, result)
17+
s.r.NoError(rows.Close())
18+
19+
_, err = s.db.Exec("SET GLOBAL format_null_as_str=0")
20+
s.r.Nil(err)
21+
22+
rows, err = s.db.Query(fmt.Sprintf("SELECT * FROM %s", s.table))
23+
s.r.Nil(err)
24+
result, err = scanValues(rows)
25+
s.r.Nil(err)
26+
s.r.Equal([][]interface{}{{int64(1), nil, nil, nil, nil, nil, nil, nil, nil}}, result)
27+
s.r.NoError(rows.Close())
28+
29+
_, err = s.db.Exec("UNSET format_null_as_str")
30+
s.r.Nil(err)
31+
}
32+
33+
func (s *DatabendTestSuite) TestQueryNullAsStr() {
34+
row := s.db.QueryRow("SELECT NULL")
35+
var val sql.NullString
36+
err := row.Scan(&val)
37+
s.r.Nil(err)
38+
s.r.True(val.Valid)
39+
s.r.Equal("NULL", val.String)
40+
}
41+
42+
func (s *DatabendTestSuite) TestQueryNull() {
43+
_, err := s.db.Exec("SET GLOBAL format_null_as_str=0")
44+
s.r.Nil(err)
45+
46+
row := s.db.QueryRow("SELECT NULL")
47+
var val sql.NullString
48+
err = row.Scan(&val)
49+
s.r.Nil(err)
50+
s.r.False(val.Valid)
51+
s.r.Equal("", val.String)
52+
53+
_, err = s.db.Exec("UNSET format_null_as_str")
54+
s.r.Nil(err)
55+
}

0 commit comments

Comments
 (0)