Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce timeTruncate parameter for time.Time arguments #1541

Merged
merged 4 commits into from Jan 31, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -86,6 +86,7 @@ Oliver Bone <owbone at github.com>
Olivier Mengué <dolmen at cpan.org>
oscarzhao <oscarzhaosl at gmail.com>
Paul Bonser <misterpib at gmail.com>
Paulius Lozys <pauliuslozys at gmail.com>
Peter Schultz <peter.schultz at classmarkets.com>
Phil Porada <philporada at gmail.com>
Rebecca Chin <rchin at pivotal.io>
Expand Down
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -285,6 +285,15 @@ Note that this sets the location for time.Time values but does not change MySQL'

Please keep in mind, that param values must be [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.

##### `timeTruncate`

```
Type: duration
Default: 0
```

[Truncate time values](https://pkg.go.dev/time#Duration.Truncate) to the specified duration. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.

##### `maxAllowedPacket`
```
Type: decimal number
Expand Down
2 changes: 1 addition & 1 deletion connection.go
Expand Up @@ -251,7 +251,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
buf = append(buf, "'0000-00-00'"...)
} else {
buf = append(buf, '\'')
buf, err = appendDateTime(buf, v.In(mc.cfg.Loc))
buf, err = appendDateTime(buf, v.In(mc.cfg.Loc), mc.cfg.TimeTruncate)
if err != nil {
return "", err
}
Expand Down
12 changes: 12 additions & 0 deletions dsn.go
Expand Up @@ -48,6 +48,7 @@ type Config struct {
pubKey *rsa.PublicKey // Server public key
TLSConfig string // TLS configuration name
TLS *tls.Config // TLS configuration, its priority is higher than TLSConfig
TimeTruncate time.Duration // Truncate time.Time values to the specified duration
Timeout time.Duration // Dial timeout
ReadTimeout time.Duration // I/O read timeout
WriteTimeout time.Duration // I/O write timeout
Expand Down Expand Up @@ -262,6 +263,10 @@ func (cfg *Config) FormatDSN() string {
writeDSNParam(&buf, &hasParam, "parseTime", "true")
}

if cfg.TimeTruncate > 0 {
writeDSNParam(&buf, &hasParam, "timeTruncate", cfg.TimeTruncate.String())
}

if cfg.ReadTimeout > 0 {
writeDSNParam(&buf, &hasParam, "readTimeout", cfg.ReadTimeout.String())
}
Expand Down Expand Up @@ -502,6 +507,13 @@ func parseDSNParams(cfg *Config, params string) (err error) {
return errors.New("invalid bool value: " + value)
}

// time.Time truncation
case "timeTruncate":
cfg.TimeTruncate, err = time.ParseDuration(value)
if err != nil {
return
}

// I/O read Timeout
case "readTimeout":
cfg.ReadTimeout, err = time.ParseDuration(value)
Expand Down
3 changes: 3 additions & 0 deletions dsn_test.go
Expand Up @@ -74,6 +74,9 @@ var testDSNs = []struct {
}, {
"tcp(de:ad:be:ef::ca:fe)/dbname",
&Config{Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"user:password@/dbname?loc=UTC&timeout=30s&parseTime=true&timeTruncate=1h",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, Timeout: 30 * time.Second, ParseTime: true, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, TimeTruncate: time.Hour},
},
}

Expand Down
2 changes: 1 addition & 1 deletion packets.go
Expand Up @@ -1172,7 +1172,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
if v.IsZero() {
b = append(b, "0000-00-00"...)
} else {
b, err = appendDateTime(b, v.In(mc.cfg.Loc))
b, err = appendDateTime(b, v.In(mc.cfg.Loc), mc.cfg.TimeTruncate)
if err != nil {
return err
}
Expand Down
6 changes: 5 additions & 1 deletion utils.go
Expand Up @@ -265,7 +265,11 @@ func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Va
return nil, fmt.Errorf("invalid DATETIME packet length %d", num)
}

func appendDateTime(buf []byte, t time.Time) ([]byte, error) {
func appendDateTime(buf []byte, t time.Time, timeTruncate time.Duration) ([]byte, error) {
if timeTruncate > 0 {
t = t.Truncate(timeTruncate)
}

year, month, day := t.Date()
hour, min, sec := t.Clock()
nsec := t.Nanosecond()
Expand Down
89 changes: 66 additions & 23 deletions utils_test.go
Expand Up @@ -237,8 +237,10 @@ func TestIsolationLevelMapping(t *testing.T) {

func TestAppendDateTime(t *testing.T) {
tests := []struct {
t time.Time
str string
t time.Time
str string
timeTruncate time.Duration
expectedErr bool
}{
{
t: time.Date(1234, 5, 6, 0, 0, 0, 0, time.UTC),
Expand Down Expand Up @@ -276,34 +278,75 @@ func TestAppendDateTime(t *testing.T) {
t: time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
str: "0001-01-01",
},
// Truncated time
{
t: time.Date(1234, 5, 6, 0, 0, 0, 0, time.UTC),
str: "1234-05-06",
timeTruncate: time.Second,
},
{
t: time.Date(4567, 12, 31, 12, 0, 0, 0, time.UTC),
str: "4567-12-31 12:00:00",
timeTruncate: time.Minute,
},
{
t: time.Date(2020, 5, 30, 12, 34, 0, 0, time.UTC),
str: "2020-05-30 12:34:00",
timeTruncate: 0,
},
{
t: time.Date(2020, 5, 30, 12, 34, 56, 0, time.UTC),
str: "2020-05-30 12:34:56",
timeTruncate: time.Second,
},
{
t: time.Date(2020, 5, 30, 22, 33, 44, 123000000, time.UTC),
str: "2020-05-30 22:33:44",
timeTruncate: time.Second,
},
{
t: time.Date(2020, 5, 30, 22, 33, 44, 123456000, time.UTC),
str: "2020-05-30 22:33:44.123",
timeTruncate: time.Millisecond,
},
{
t: time.Date(2020, 5, 30, 22, 33, 44, 123456789, time.UTC),
str: "2020-05-30 22:33:44",
timeTruncate: time.Second,
},
{
t: time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC),
str: "9999-12-31 23:59:59.999999999",
timeTruncate: 0,
},
{
t: time.Date(1, 1, 1, 1, 1, 1, 1, time.UTC),
str: "0001-01-01",
timeTruncate: 365 * 24 * time.Hour,
},
// year out of range
{
t: time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC),
expectedErr: true,
},
{
t: time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC),
expectedErr: true,
},
}
for _, v := range tests {
buf := make([]byte, 0, 32)
buf, _ = appendDateTime(buf, v.t)
buf, err := appendDateTime(buf, v.t, v.timeTruncate)
if err != nil {
if !v.expectedErr {
t.Errorf("appendDateTime(%v) returned an errror: %v", v.t, err)
}
continue
}
if str := string(buf); str != v.str {
t.Errorf("appendDateTime(%v), have: %s, want: %s", v.t, str, v.str)
}
}

// year out of range
{
v := time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC)
buf := make([]byte, 0, 32)
_, err := appendDateTime(buf, v)
if err == nil {
t.Error("want an error")
return
}
}
{
v := time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC)
buf := make([]byte, 0, 32)
_, err := appendDateTime(buf, v)
if err == nil {
t.Error("want an error")
return
}
}
}

func TestParseDateTime(t *testing.T) {
Expand Down