Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,17 @@ When passing arguments to queries, the driver supports the following Go data typ
* `string`
* slices
* `trino.Numeric` - a string representation of a number
* `time.Time` - passed to Trino as a timestamp with a time zone
* the result of `trino.Date(year, month, day)` - passed to Trino as a date
* the result of `trino.Time(hour, minute, second, nanosecond)` - passed to Trino as a time without a time zone
* the result of `trino.TimeTz(hour, minute, second, nanosecond, location)` - passed to Trino as a time with a time zone
* the result of `trino.Timestamp(year, month, day, hour, minute, second, nanosecond)` - passed to Trino as a timestamp without a time zone

It's not yet possible to pass:
* `nil`
* `float32` or `float64`
* `byte`
* `time.Time` or `time.Duration`
* `time.Duration`
* `json.RawMessage`
* maps

Expand All @@ -215,7 +220,11 @@ SELECT * FROM table WHERE col_double = cast(? AS DOUBLE) OR col_timestamp = CAST
### Response rows

When reading response rows, the driver supports most Trino data types, except:
* time and timestamps with precision - all time types are returned as `time.Time`
* time and timestamps with precision - all time types are returned as `time.Time`.
All precisions up to nanoseconds (`TIMESTAMP(9)` or `TIME(9)`) are supported (since
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why exactly are we constraining ourselve to nanosecond precision? Would it be possible to support up to TIME*(12)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Go's time.Time only supports nanoseconds. To support picoseconds, we'd have to define a custom data type. I'm not sure how much work it would be to make it compatible with time.Time.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recent work in the Python client in trinodb/trino-python-client#300 is also limited but to microseconds.

this is the maximum precision Golang's `time.Time` supports). If a query returns columns
defined with a greater precision, use `CAST` to reduce the returned precision, or convert the
value to a string that then can be parsed manually.
* `DECIMAL` - returned as string
* `IPADDRESS` - returned as string
* `INTERVAL YEAR TO MONTH` and `INTERVAL DAY TO SECOND` - returned as string
Expand Down
4 changes: 2 additions & 2 deletions trino/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ func TestIntegrationArgsConversion(t *testing.T) {
AND col_big = ?
AND col_real = cast(? as real)
AND col_double = cast(? as double)
AND col_ts = cast(? as timestamp)
AND col_ts = ?
AND col_varchar = ?
AND col_array = ?`,
int16(1),
Expand All @@ -431,7 +431,7 @@ func TestIntegrationArgsConversion(t *testing.T) {
int64(1),
Numeric("1"),
Numeric("1"),
"2017-07-10 01:02:03.004 UTC",
time.Date(2017, 7, 10, 1, 2, 3, 4*1000000, time.UTC),
"string",
[]string{"A", "B"}).Scan(&value)
if err != nil {
Expand Down
71 changes: 69 additions & 2 deletions trino/serial.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,65 @@ func (e UnsupportedArgError) Error() string {
// If another string format is used it will error to serialise
type Numeric string

// trinoDate represents a Date type in Trino.
type trinoDate struct {
year int
month time.Month
day int
}

// Date creates a representation of a Trino Date type.
func Date(year int, month time.Month, day int) trinoDate {
Comment thread
JPMoresmau marked this conversation as resolved.
return trinoDate{year, month, day}
}

// trinoTime represents a Time type in Trino.
type trinoTime struct {
hour int
minute int
second int
nanosecond int
}

// Time creates a representation of a Trino Time type. To represent time with precision higher than nanoseconds, pass the value as a string and use a cast in the query.
func Time(hour int,
Comment thread
JPMoresmau marked this conversation as resolved.
minute int,
second int,
nanosecond int) trinoTime {
return trinoTime{hour, minute, second, nanosecond}
}

// trinoTimeTz represents a Time(9) With Timezone type in Trino.
type trinoTimeTz time.Time

// TimeTz creates a representation of a Trino Time(9) With Timezone type.
func TimeTz(hour int,
Comment thread
JPMoresmau marked this conversation as resolved.
minute int,
second int,
nanosecond int,
location *time.Location) trinoTimeTz {
// When reading a time, a nil location indicates UTC.
// However, passing nil to time.Date() panics.
if location == nil {
location = time.UTC
}
return trinoTimeTz(time.Date(0, 0, 0, hour, minute, second, nanosecond, location))
}

// Timestamp indicates we want a TimeStamp type WITHOUT a time zone in Trino from a Golang time.
type trinoTimestamp time.Time

// Timestamp creates a representation of a Trino Timestamp(9) type.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those comments are not really useful? @nineinchnick do we have check which enforces those?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't currently have a linter in the CI that would check for this. The built-in go vet doesn't, we'd have to use staticcheck (recommended after go lint was deprecated) or golangci-lint (which is huge but popular).

Having comments for all exported functions is idiomatic in Go and it would show up in online docs like here: https://pkg.go.dev/github.com/trinodb/trino-go-client@v0.308.0/trino

What would it take to make them more useful? Should it explain why time.Time is not used instead?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would it take to make them more useful

Not sure. Presonally I would just drop it. But if we want to follow Go guildelines let's go with it :)

func Timestamp(year int,
month time.Month,
day int,
hour int,
minute int,
second int,
nanosecond int) trinoTimestamp {
return trinoTimestamp(time.Date(year, month, day, hour, minute, second, nanosecond, time.UTC))
}

// Serial converts any supported value to its equivalent string for as a Trino parameter
// See https://trino.io/docs/current/language/types.html
func Serial(v interface{}) (string, error) {
Expand Down Expand Up @@ -92,9 +151,17 @@ func Serial(v interface{}) (string, error) {
case []byte:
return "", UnsupportedArgError{"[]byte"}

// time.Time and time.Duration not supported as time and date take several different formats in Trino
case trinoDate:
return fmt.Sprintf("DATE '%04d-%02d-%02d'", x.year, x.month, x.day), nil
case trinoTime:
return fmt.Sprintf("TIME '%02d:%02d:%02d.%09d'", x.hour, x.minute, x.second, x.nanosecond), nil
case trinoTimeTz:
return "TIME " + time.Time(x).Format("'15:04:05.999999999 Z07:00'"), nil
case trinoTimestamp:
return "TIMESTAMP " + time.Time(x).Format("'2006-01-02 15:04:05.999999999'"), nil
case time.Time:
return "", UnsupportedArgError{"time.Time"}
return "TIMESTAMP " + time.Time(x).Format("'2006-01-02 15:04:05.999999999 Z07:00'"), nil

case time.Duration:
return "", UnsupportedArgError{"time.Duration"}

Expand Down
49 changes: 48 additions & 1 deletion trino/serial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@

package trino

import "testing"
import (
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestSerial(t *testing.T) {
paris, err := time.LoadLocation("Europe/Paris")
require.NoError(t, err)
scenarios := []struct {
name string
value interface{}
Expand Down Expand Up @@ -113,6 +120,46 @@ func TestSerial(t *testing.T) {
value: false,
expectedSerial: "false",
},
{
name: "date",
value: Date(2017, 7, 10),
expectedSerial: "DATE '2017-07-10'",
},
{
name: "time without timezone",
value: Time(11, 34, 25, 123456),
expectedSerial: "TIME '11:34:25.000123456'",
},
{
name: "time with timezone",
value: TimeTz(11, 34, 25, 123456, time.FixedZone("test zone", +2*3600)),
expectedSerial: "TIME '11:34:25.000123456 +02:00'",
},
{
name: "time with timezone",
value: TimeTz(11, 34, 25, 123456, nil),
expectedSerial: "TIME '11:34:25.000123456 Z'",
},
{
name: "timestamp without timezone",
value: Timestamp(2017, 7, 10, 11, 34, 25, 123456),
expectedSerial: "TIMESTAMP '2017-07-10 11:34:25.000123456'",
},
{
name: "timestamp with time zone in Fixed Zone",
value: time.Date(2017, 7, 10, 11, 34, 25, 123456, time.FixedZone("test zone", +2*3600)),
expectedSerial: "TIMESTAMP '2017-07-10 11:34:25.000123456 +02:00'",
},
{
name: "timestamp with time zone in Named Zone",
value: time.Date(2017, 7, 10, 11, 34, 25, 123456, paris),
expectedSerial: "TIMESTAMP '2017-07-10 11:34:25.000123456 +02:00'",
},
{
name: "timestamp with time zone in UTC",
value: time.Date(2017, 7, 10, 11, 34, 25, 123456, time.UTC),
expectedSerial: "TIMESTAMP '2017-07-10 11:34:25.000123456 Z'",
},
{
name: "nil",
value: nil,
Expand Down
52 changes: 35 additions & 17 deletions trino/trino.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,20 +624,24 @@ func (st *driverStmt) ExecContext(ctx context.Context, args []driver.NamedValue)
}

func (st *driverStmt) CheckNamedValue(arg *driver.NamedValue) error {
_, ok := arg.Value.(Numeric)
if ok {
return nil
}
if reflect.TypeOf(arg.Value).Kind() == reflect.Slice {
switch arg.Value.(type) {
case Numeric, trinoDate, trinoTime, trinoTimeTz, trinoTimestamp:
return nil
}
default:
{
if reflect.TypeOf(arg.Value).Kind() == reflect.Slice {
return nil
}

if arg.Name == trinoProgressCallbackParam {
return nil
}
if arg.Name == trinoProgressCallbackPeriodParam {
return nil
if arg.Name == trinoProgressCallbackParam {
return nil
}
if arg.Name == trinoProgressCallbackPeriodParam {
return nil
}
}
}

return driver.ErrSkip
}

Expand Down Expand Up @@ -733,10 +737,11 @@ func (st *driverStmt) QueryContext(ctx context.Context, args []driver.NamedValue

func (st *driverStmt) exec(ctx context.Context, args []driver.NamedValue) (*stmtResponse, error) {
query := st.query
var hs http.Header
hs := make(http.Header)
// Ensure the server returns timestamps preserving their precision, without truncating them to timestamp(3).
hs.Add("X-Trino-Client-Capabilities", "PARAMETRIC_DATETIME")

if len(args) > 0 {
hs = make(http.Header)
var ss []string
for _, arg := range args {
if arg.Name == trinoProgressCallbackParam {
Expand Down Expand Up @@ -1279,7 +1284,15 @@ func newTypeConverter(typeName string, signature typeSignature) (*typeConverter,
}
result.scale = newOptionalInt64(signature.Arguments[1].long)
}
case "time", "time with time zone", "timestamp", "timestamp with time zone":
if len(signature.Arguments) > 0 {
if signature.Arguments[0].Kind != KIND_LONG {
return nil, ErrInvalidResponseType
}
result.precision = newOptionalInt64(signature.Arguments[0].long)
}
}

return result, nil
}

Expand Down Expand Up @@ -1863,16 +1876,21 @@ func (s *NullSlice3Float64) Scan(value interface{}) error {
return nil
}

// Layout for time and timestamp WITHOUT time zone.
// Trino can support up to 12 digits sub second precision, but Go only 9.
// (Requires X-Trino-Client-Capabilities: PARAMETRIC_DATETIME)
var timeLayouts = []string{
"2006-01-02",
"15:04:05.000",
"2006-01-02 15:04:05.000",
"15:04:05.999999999",
"2006-01-02 15:04:05.999999999",
}

// Layout for time and timestamp WITH time zone.
// Trino can support up to 12 digits sub second precision, but Go only 9.
// (Requires X-Trino-Client-Capabilities: PARAMETRIC_DATETIME)
var timeLayoutsTZ = []string{
"15:04:05.000 -07:00",
"2006-01-02 15:04:05.000 -07:00",
"15:04:05.999999999 -07:00",
"2006-01-02 15:04:05.999999999 -07:00",
}

func scanNullTime(v interface{}) (NullTime, error) {
Expand Down
Loading