From 5da00c5822a39a847f7cc7d85fdcad0603098aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulius=20Lo=C5=BEys?= Date: Wed, 24 Jan 2024 00:17:08 +0200 Subject: [PATCH 1/4] Introduce `timeTruncate` parameter This introduces `timeTruncate` parameter that will truncate `time.Time` values in query arguments. --- AUTHORS | 1 + README.md | 9 ++++++++ connection.go | 2 +- dsn.go | 12 +++++++++++ dsn_test.go | 3 +++ packets.go | 2 +- utils.go | 6 +++++- utils_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++----- 8 files changed, 84 insertions(+), 8 deletions(-) diff --git a/AUTHORS b/AUTHORS index 0ada02d86..63ee516e5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -86,6 +86,7 @@ Oliver Bone Olivier Mengué oscarzhao Paul Bonser +Paulius Lozys Peter Schultz Phil Porada Rebecca Chin diff --git a/README.md b/README.md index ac79890a7..7674d39e2 100644 --- a/README.md +++ b/README.md @@ -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 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 diff --git a/connection.go b/connection.go index 660b2b0e0..99eb8a808 100644 --- a/connection.go +++ b/connection.go @@ -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 } diff --git a/dsn.go b/dsn.go index ef0608636..ce5d85ff0 100644 --- a/dsn.go +++ b/dsn.go @@ -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 @@ -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()) } @@ -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) diff --git a/dsn_test.go b/dsn_test.go index 8a6a0c10e..fd27cd946 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -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, TimeTruncate: time.Hour}, }, } diff --git a/packets.go b/packets.go index 94b46b10f..e5a6e4727 100644 --- a/packets.go +++ b/packets.go @@ -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 } diff --git a/utils.go b/utils.go index a24197b93..cda24fe74 100644 --- a/utils.go +++ b/utils.go @@ -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() diff --git a/utils_test.go b/utils_test.go index 4e5fc3cb7..6d17e8e4e 100644 --- a/utils_test.go +++ b/utils_test.go @@ -237,8 +237,9 @@ 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 }{ { t: time.Date(1234, 5, 6, 0, 0, 0, 0, time.UTC), @@ -276,10 +277,56 @@ 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, + }, } for _, v := range tests { buf := make([]byte, 0, 32) - buf, _ = appendDateTime(buf, v.t) + buf, _ = appendDateTime(buf, v.t, v.timeTruncate) if str := string(buf); str != v.str { t.Errorf("appendDateTime(%v), have: %s, want: %s", v.t, str, v.str) } @@ -289,7 +336,7 @@ func TestAppendDateTime(t *testing.T) { { v := time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC) buf := make([]byte, 0, 32) - _, err := appendDateTime(buf, v) + _, err := appendDateTime(buf, v, 0) if err == nil { t.Error("want an error") return @@ -298,7 +345,7 @@ func TestAppendDateTime(t *testing.T) { { v := time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC) buf := make([]byte, 0, 32) - _, err := appendDateTime(buf, v) + _, err := appendDateTime(buf, v, 0) if err == nil { t.Error("want an error") return From c0778689b2680a9e9d4d2fd3dca7fe1158eac3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulius=20Lo=C5=BEys?= Date: Sat, 27 Jan 2024 23:34:04 +0200 Subject: [PATCH 2/4] Update tests --- utils_test.go | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/utils_test.go b/utils_test.go index 6d17e8e4e..80aebddff 100644 --- a/utils_test.go +++ b/utils_test.go @@ -240,6 +240,7 @@ func TestAppendDateTime(t *testing.T) { t time.Time str string timeTruncate time.Duration + expectedErr bool }{ { t: time.Date(1234, 5, 6, 0, 0, 0, 0, time.UTC), @@ -323,34 +324,29 @@ func TestAppendDateTime(t *testing.T) { 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, v.timeTruncate) + 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, 0) - 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, 0) - if err == nil { - t.Error("want an error") - return - } - } } func TestParseDateTime(t *testing.T) { From 22cf7cee9c36351dd20a28b98c8f73a7d0d7ac6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulius=20Lo=C5=BEys?= <42966213+PauliusLozys@users.noreply.github.com> Date: Mon, 29 Jan 2024 20:47:29 +0200 Subject: [PATCH 3/4] Include GO `time.Truncate` documentation link Co-authored-by: Inada Naoki --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7674d39e2..018e1dd7c 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ Type: duration Default: 0 ``` -Truncate time values 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"*. +[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` ``` From e6f93d357045577d8453fc8f4868cebe18286158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulius=20Lo=C5=BEys?= Date: Tue, 30 Jan 2024 18:12:53 +0200 Subject: [PATCH 4/4] Include default config values in expected test output --- dsn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsn_test.go b/dsn_test.go index fd27cd946..75cbda700 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -76,7 +76,7 @@ var testDSNs = []struct { &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, TimeTruncate: time.Hour}, + &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}, }, }