Skip to content

Commit

Permalink
Introduce timeTruncate parameter for time.Time arguments (#1541)
Browse files Browse the repository at this point in the history
Co-authored-by: Inada Naoki <songofacandy@gmail.com>
  • Loading branch information
PauliusLozys and methane committed Jan 31, 2024
1 parent c48c0e7 commit 743e263
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 26 deletions.
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

0 comments on commit 743e263

Please sign in to comment.