Skip to content
Open
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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Daniel Montoya <dsmontoyam at gmail.com>
Daniel Nichter <nil at codenode.com>
Daniël van Eeden <git at myname.nl>
Dave Protasowski <dprotaso at gmail.com>
Deepak Kumar <deepak.kumar852182 at gmail.com>
Demouth <yuya at demouth.net>
Diego Dupin <diego.dupin at gmail.com>
Dirkjan Bussink <d.bussink at gmail.com>
Expand Down
4 changes: 2 additions & 2 deletions nulltime.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ func (nt *NullTime) Scan(value any) (err error) {
nt.Time, nt.Valid = v, true
return
case []byte:
nt.Time, err = parseDateTime(v, time.UTC)
nt.Time, err = ParseDateTime(v, time.UTC)
nt.Valid = (err == nil)
return
case string:
nt.Time, err = parseDateTime([]byte(v), time.UTC)
nt.Time, err = ParseDateTime([]byte(v), time.UTC)
nt.Valid = (err == nil)
return
}
Expand Down
8 changes: 4 additions & 4 deletions packets.go
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ func (rows *textRows) readRow(dest []driver.Value) error {
fieldTypeDate,
fieldTypeNewDate:
if mc.parseTime {
dest[i], err = parseDateTime(buf, mc.cfg.Loc)
dest[i], err = ParseDateTime(buf, mc.cfg.Loc)
} else {
dest[i] = buf
}
Expand Down Expand Up @@ -1411,9 +1411,9 @@ func (rows *binaryRows) readRow(dest []driver.Value) error {
rows.rs.columns[i].decimals,
)
}
dest[i], err = formatBinaryTime(data[pos:pos+int(num)], dstlen)
dest[i], err = FormatBinaryTime(data[pos:pos+int(num)], dstlen)
case rows.mc.parseTime:
dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.Loc)
dest[i], err = ParseBinaryDateTime(num, data[pos:], rows.mc.cfg.Loc)
default:
var dstlen uint8
if rows.rs.columns[i].fieldType == fieldTypeDate {
Expand All @@ -1431,7 +1431,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error {
)
}
}
dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen)
dest[i], err = FormatBinaryDateTime(data[pos:pos+int(num)], dstlen)
}

if err == nil {
Expand Down
65 changes: 41 additions & 24 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ func readBool(input string) (value bool, valid bool) {
* Time related utils *
******************************************************************************/

func parseDateTime(b []byte, loc *time.Location) (time.Time, error) {
// ParseDateTime parses the textual representation of a MySQL DATE, DATETIME,
// or TIMESTAMP value, as returned over the text protocol, into a time.Time
// using loc as the value's time zone.
func ParseDateTime(b []byte, loc *time.Location) (time.Time, error) {
const base = "0000-00-00 00:00:00.000000"
switch len(b) {
case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"
Expand Down Expand Up @@ -227,7 +230,14 @@ func bToi(b byte) (int, error) {
return int(b - '0'), nil
}

func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Value, error) {
// ParseBinaryDateTime parses the binary protocol representation of a MySQL
// DATE, DATETIME, or TIMESTAMP value into a time.Time using loc as the
// value's time zone. num is the number of bytes of data that make up the
// value, as sent by the server (0, 4, 7, or 11).
func ParseBinaryDateTime(num uint64, data []byte, loc *time.Location) (time.Time, error) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if num > 0 && uint64(len(data)) < num {
return time.Time{}, fmt.Errorf("invalid DATETIME packet length %d: got %d bytes", num, len(data))
}
switch num {
case 0:
return time.Time{}, nil
Expand Down Expand Up @@ -262,7 +272,7 @@ func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Va
loc,
), nil
}
return nil, fmt.Errorf("invalid DATETIME packet length %d", num)
return time.Time{}, fmt.Errorf("invalid DATETIME packet length %d", num)
}

func appendDateTime(buf []byte, t time.Time, timeTruncate time.Duration) ([]byte, error) {
Expand Down Expand Up @@ -327,10 +337,9 @@ func appendDateTime(buf []byte, t time.Time, timeTruncate time.Duration) ([]byte
return append(buf, localBuf[:n]...), nil
}

// zeroDateTime is used in formatBinaryDateTime to avoid an allocation
// if the DATE or DATETIME has the zero value.
// It must never be changed.
// The current behavior depends on database/sql copying the result.
// zeroDateTime backs the zero-value fast paths in FormatBinaryDateTime and
// FormatBinaryTime. It must never be changed: callers only ever receive a
// copy of a subslice of it, never a reference to it directly.
var zeroDateTime = []byte("0000-00-00 00:00:00.000000")

const digits01 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
Expand Down Expand Up @@ -385,15 +394,13 @@ func appendMicrosecs(dst, src []byte, decimals int) []byte {
}
}

func formatBinaryDateTime(src []byte, length uint8) (driver.Value, error) {
// length expects the deterministic length of the zero value,
// negative time and 100+ hours are automatically added if needed
if len(src) == 0 {
return zeroDateTime[:length], nil
}
var dst []byte // return value
var p1, p2, p3 byte // current digit pair

// FormatBinaryDateTime formats the binary protocol representation of a MySQL
// DATE, DATETIME, or TIMESTAMP value (src) into its textual representation,
// as used by the text protocol. length is the deterministic length of the
// zero value for the column (e.g. 10 for DATE, 19 for DATETIME/TIMESTAMP
// with no fractional seconds, or 19+1+decimals with fractional seconds);
// negative time and 100+ hours are automatically added if needed.
func FormatBinaryDateTime(src []byte, length uint8) ([]byte, error) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
switch length {
case 10, 19, 21, 22, 23, 24, 25, 26:
default:
Expand All @@ -403,6 +410,12 @@ func formatBinaryDateTime(src []byte, length uint8) (driver.Value, error) {
}
return nil, fmt.Errorf("illegal %s length %d", t, length)
}
if len(src) == 0 {
return append([]byte(nil), zeroDateTime[:length]...), nil
}
var dst []byte // return value
var p1, p2, p3 byte // current digit pair

switch len(src) {
case 4, 7, 11:
default:
Expand Down Expand Up @@ -444,21 +457,25 @@ func formatBinaryDateTime(src []byte, length uint8) (driver.Value, error) {
return appendMicrosecs(dst, src[2:], int(length)-20), nil
}

func formatBinaryTime(src []byte, length uint8) (driver.Value, error) {
// length expects the deterministic length of the zero value,
// negative time and 100+ hours are automatically added if needed
if len(src) == 0 {
return zeroDateTime[11 : 11+length], nil
}
var dst []byte // return value

// FormatBinaryTime formats the binary protocol representation of a MySQL
// TIME value (src) into its textual representation, as used by the text
// protocol. length is the deterministic length of the zero value for the
// column (e.g. 8 for TIME with no fractional seconds, or 8+1+decimals with
// fractional seconds); negative time and 100+ hours are automatically added
// if needed.
func FormatBinaryTime(src []byte, length uint8) ([]byte, error) {
switch length {
case
8, // time (can be up to 10 when negative and 100+ hours)
10, 11, 12, 13, 14, 15: // time with fractional seconds
default:
return nil, fmt.Errorf("illegal TIME length %d", length)
}
if len(src) == 0 {
return append([]byte(nil), zeroDateTime[11:11+length]...), nil
}
var dst []byte // return value

switch len(src) {
case 8, 12:
default:
Expand Down
67 changes: 52 additions & 15 deletions utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,8 @@ func TestFormatBinaryDateTime(t *testing.T) {
rawDate[6] = 23 // seconds
binary.LittleEndian.PutUint32(rawDate[7:], 987654) // microseconds
expect := func(expected string, inlen, outlen uint8) {
actual, _ := formatBinaryDateTime(rawDate[:inlen], outlen)
bytes, ok := actual.([]byte)
if !ok {
t.Errorf("formatBinaryDateTime must return []byte, was %T", actual)
}
if string(bytes) != expected {
actual, _ := FormatBinaryDateTime(rawDate[:inlen], outlen)
if string(actual) != expected {
t.Errorf(
"expected %q, got %q for length in %d, out %d",
expected, actual, inlen, outlen,
Expand All @@ -81,16 +77,29 @@ func TestFormatBinaryDateTime(t *testing.T) {
expect("1978-12-30", 4, 10)
expect("1978-12-30 15:46:23", 7, 19)
expect("1978-12-30 15:46:23.987654", 11, 26)

// an illegal length must be rejected even on the zero-value fast
// path, rather than returning truncated/garbage output or panicking
if _, err := FormatBinaryDateTime(nil, 9); err == nil {
t.Fatal("want error for illegal length 9, got nil")
}
if _, err := FormatBinaryDateTime(nil, 27); err == nil {
t.Fatal("want error for illegal length 27, got nil")
}

// the zero-value fast path must return an independent copy, not a
// reference to the shared zeroDateTime buffer
zero, _ := FormatBinaryDateTime(nil, 10)
zero[0] = 'X'
if again, _ := FormatBinaryDateTime(nil, 10); string(again) != "0000-00-00" {
t.Fatalf("mutating a previous result corrupted a later result: %q", again)
}
}

func TestFormatBinaryTime(t *testing.T) {
expect := func(expected string, src []byte, outlen uint8) {
actual, _ := formatBinaryTime(src, outlen)
bytes, ok := actual.([]byte)
if !ok {
t.Errorf("formatBinaryDateTime must return []byte, was %T", actual)
}
if string(bytes) != expected {
actual, _ := FormatBinaryTime(src, outlen)
if string(actual) != expected {
t.Errorf(
"expected %q, got %q for src=%q and outlen=%d",
expected, actual, src, outlen)
Expand All @@ -116,6 +125,34 @@ func TestFormatBinaryTime(t *testing.T) {
// With micro(4)
expect("12:34:56.00", []byte{0, 0, 0, 0, 0, 12, 34, 56, 99, 0, 0, 0}, 11)
expect("12:34:56.000099", []byte{0, 0, 0, 0, 0, 12, 34, 56, 99, 0, 0, 0}, 15)

// an illegal length must be rejected even on the zero-value fast
// path, rather than returning truncated/garbage output or panicking
if _, err := FormatBinaryTime(nil, 5); err == nil {
t.Fatal("want error for illegal length 5, got nil")
}
if _, err := FormatBinaryTime(nil, 16); err == nil {
t.Fatal("want error for illegal length 16, got nil")
}

// the zero-value fast path must return an independent copy, not a
// reference to the shared zeroDateTime buffer
zero, _ := FormatBinaryTime(nil, 8)
zero[0] = 'X'
if again, _ := FormatBinaryTime(nil, 8); string(again) != "00:00:00" {
t.Fatalf("mutating a previous result corrupted a later result: %q", again)
}
}

func TestParseBinaryDateTime(t *testing.T) {
// truncated data must return an error, not panic, since the caller
// controls num and data independently once this func is exported
if _, err := ParseBinaryDateTime(4, nil, time.UTC); err == nil {
t.Fatal("want error for truncated DATETIME packet, got nil")
}
if _, err := ParseBinaryDateTime(11, []byte{1, 2, 3}, time.UTC); err == nil {
t.Fatal("want error for truncated DATETIME packet, got nil")
}
}

func TestEscapeBackslash(t *testing.T) {
Expand Down Expand Up @@ -445,7 +482,7 @@ func TestParseDateTime(t *testing.T) {
t.Fatal(err)
}
}
got, err := parseDateTime([]byte(cc.str), loc)
got, err := ParseDateTime([]byte(cc.str), loc)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -473,7 +510,7 @@ func TestInvalidDateTime(t *testing.T) {

for _, cc := range cases {
t.Run(cc.name, func(t *testing.T) {
got, err := parseDateTime([]byte(cc.str), time.UTC)
got, err := ParseDateTime([]byte(cc.str), time.UTC)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -540,7 +577,7 @@ func TestParseDateTimeFail(t *testing.T) {

for _, cc := range cases {
t.Run(cc.name, func(t *testing.T) {
got, err := parseDateTime([]byte(cc.str), time.UTC)
got, err := ParseDateTime([]byte(cc.str), time.UTC)
if err == nil {
t.Fatal("want error")
}
Expand Down