Skip to content

Commit

Permalink
feat: timestamp & date (#8)
Browse files Browse the repository at this point in the history
- fix timestamp or date type as go query parameters.
- basic facility to support SQL Null as input or output.
- more tests
  • Loading branch information
aceforeverd authored Apr 26, 2024
1 parent 49f51dd commit 2c5a00d
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 125 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '^1.18'
go-version: '^1.22'

- name: OpenMLDB cluster
run: |
Expand All @@ -33,7 +33,7 @@ jobs:
docker compose -f docker-compose.yml exec openmldb-ns1 /opt/openmldb/bin/openmldb --zk_cluster=openmldb-zk:2181 --zk_root_path=/openmldb --role=sql_client --cmd 'SET GLOBAL execute_mode = "online"'
- name: go test
run: go test ./... -race -covermode=atomic -coverprofile=coverage.out
run: go test ./... -race -covermode=atomic -coverprofile=coverage.out -v

- name: Coverage
uses: codecov/codecov-action@v4
Expand Down
56 changes: 39 additions & 17 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package openmldb
import (
"bytes"
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
Expand Down Expand Up @@ -71,7 +72,7 @@ type queryResp struct {
}

type respData struct {
Schema []string `json:"schema"`
Schema []string `json:"schema"`
Data [][]driver.Value `json:"data"`
}

Expand Down Expand Up @@ -127,36 +128,48 @@ type queryReq struct {
}

type queryInput struct {
Schema []string `json:"schema"`
Schema []string `json:"schema"`
Data []driver.Value `json:"data"`
}

func marshalQueryRequest(mode, sql string, input ...driver.Value) ([]byte, error) {
func marshalQueryRequest(mode string, sqlStr string, input ...driver.Value) ([]byte, error) {
req := queryReq{
Mode: mode,
SQL: sql,
SQL: sqlStr,
}

// TODO(someone): Type infer from input slice does not work always. Consider those cases:
// 1. a int type can be a int32 or int64, depends on value size.
// 2. we're not covering more input types like uint.
// 3. For a int16 or int32 input from DB.Query(...), it always convert to int64 because driver.Value
// only expect int64 from primitive types.
//
// A better approach is to ask the schema types from api server, which in turn ask types info to SQL compiler.

if len(input) > 0 {
schema := make([]string, len(input))
// TODO(someone): support value as nil, at current time it is not possible to infer SQL type from a nil
for i, v := range input {
switch v.(type) {
case bool:
switch vv := v.(type) {
case bool, Null[bool]:
schema[i] = "bool"
case int16:
case int16, Null[int16]:
schema[i] = "int16"
case int32:
case int32, Null[int32]:
schema[i] = "int32"
case int64:
case int64, Null[int64]:
schema[i] = "int64"
case float32:
case float32, Null[float32]:
schema[i] = "float"
case float64:
case float64, Null[float64]:
schema[i] = "double"
case string:
case string, Null[string]:
schema[i] = "string"
case time.Time:
schema[i] = "timestamp"
input[i] = Null[time.Time]{Null: sql.Null[time.Time]{V: vv, Valid: true}}
case Null[time.Time]:
schema[i] = "timestamp"
case NullDate:
schema[i] = "date"
default:
Expand All @@ -179,8 +192,14 @@ func unmarshalQueryResponse(respBody io.Reader) (*queryResp, error) {
}

if r.Data != nil {
// queryResp.Data may nil for DDL
for _, row := range r.Data.Data {
for i, col := range row {
if col == nil {
row[i] = nil
continue
}

switch strings.ToLower(r.Data.Schema[i]) {
case "bool":
row[i] = col.(bool)
Expand All @@ -196,14 +215,17 @@ func unmarshalQueryResponse(respBody io.Reader) (*queryResp, error) {
row[i] = float64(col.(float64))
case "string":
row[i] = col.(string)
// date and timestamp values saved internally as time.Time
case "timestamp":
// timestamp value returned as int64 millisecond unix epoch time
row[i] = time.UnixMilli(int64(col.(float64)))
case "date":
// date values returned as "YYYY-mm-dd" formated string
var nullDate NullDate
nullDate.Scan(col.(string))
row[i] = nullDate
t, err := parseDateStr(col.(string))
if err != nil {
row[i] = nil
}

row[i] = t
default:
return nil, fmt.Errorf("unknown type %s at index %d", r.Data.Schema[i], i)
}
Expand Down Expand Up @@ -244,7 +266,7 @@ func (c *conn) execute(ctx context.Context, sql string, parameters ...driver.Val
if r, err := unmarshalQueryResponse(resp.Body); err != nil {
return nil, err
} else if r.Code != 0 {
return nil, fmt.Errorf("conn error: %s", r.Msg)
return nil, fmt.Errorf("execute error: %s", r.Msg)
} else if r.Data != nil {
return &respDataRows{*r.Data, 0}, nil
}
Expand Down
50 changes: 39 additions & 11 deletions conn_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package openmldb

import (
interfaces "database/sql/driver"
"database/sql"
"database/sql/driver"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand All @@ -12,28 +14,36 @@ func TestParseReqToJson(t *testing.T) {
for _, tc := range []struct {
mode string
sql string
input []interfaces.Value
input []driver.Value
expect string
}{
{
"offsync",
"offline",
"SELECT 1;",
nil,
`{
"mode": "offsync",
"mode": "offline",
"sql": "SELECT 1;"
}`,
},
{
"offsync",
"online",
"SELECT c1, c2 FROM demo WHERE c1 = ? AND c2 = ?;",
[]interfaces.Value{int32(1), "bb"},
[]driver.Value{
int16(2), // int16
int32(1), // int32
"bb", // string
Null[string]{Null: sql.Null[string]{V: "foo", Valid: true}}, // string
time.UnixMilli(8000), // timestamp
Null[time.Time]{Null: sql.Null[time.Time]{V: time.UnixMilli(4000), Valid: true}}, // timestamp
Null[time.Time]{Null: sql.Null[time.Time]{V: time.UnixMilli(4000), Valid: false}}, // timestamp
NullDate{Null: sql.Null[time.Time]{V: time.Date(2022, time.October, 10, 0, 0, 0, 0, time.UTC), Valid: true}}}, // date
`{
"mode": "offsync",
"mode": "online",
"sql": "SELECT c1, c2 FROM demo WHERE c1 = ? AND c2 = ?;",
"input": {
"schema": ["int32", "string"],
"data": [1, "bb"]
"schema": ["int16", "int32", "string", "string", "timestamp", "timestamp", "timestamp", "date"],
"data": [2, 1, "bb", "foo", 8000, 4000, null, "2022-10-10"]
}
}`,
},
Expand All @@ -60,6 +70,24 @@ func TestParseRespFromJson(t *testing.T) {
Data: nil,
},
},
{
`{
"code": 0,
"msg": "ok",
"data": {
"schema": ["date", "string"],
"data": []
}
}`,
queryResp{
Code: 0,
Msg: "ok",
Data: &respData{
Schema: []string{"date", "string"},
Data: [][]driver.Value{},
},
},
},
{
`{
"code": 0,
Expand All @@ -74,7 +102,7 @@ func TestParseRespFromJson(t *testing.T) {
Msg: "ok",
Data: &respData{
Schema: []string{"Int32", "String"},
Data: [][]interfaces.Value{
Data: [][]driver.Value{
{int32(1), "bb"},
{int32(2), "bb"},
},
Expand All @@ -95,7 +123,7 @@ func TestParseRespFromJson(t *testing.T) {
Msg: "ok",
Data: &respData{
Schema: []string{"Bool", "Int16", "Int32", "Int64", "Float", "Double", "String"},
Data: [][]interfaces.Value{
Data: [][]driver.Value{
{true, int16(1), int32(1), int64(1), float32(1), float64(1), "bb"},
},
},
Expand Down
15 changes: 15 additions & 0 deletions encode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package openmldb

import (
"time"
)

func parseDateStr(src string) (time.Time, error) {
// api server returns date type as string formatted 'yyyy-mm-dd'
dval, err := time.Parse(time.DateOnly, src)
if err != nil {
return time.Time{}, err
}

return dval, nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/4paradigm/openmldb-go-sdk

go 1.18
go 1.22

require github.com/stretchr/testify v1.9.0

Expand Down
Loading

0 comments on commit 2c5a00d

Please sign in to comment.