Skip to content

Commit 716467d

Browse files
committed
Optional DATE / DATETIME to time.Time parsing
Closes go-sql-driver#9
1 parent 0e8690a commit 716467d

7 files changed

+313
-102
lines changed

README.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ A MySQL-Driver for Go's [database/sql](http://golang.org/pkg/database/sql) packa
1919
* [Address](#address)
2020
* [Parameters](#parameters)
2121
* [Examples](#examples)
22-
* [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
22+
* [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
23+
* [time.Time support](#timetime-support)
2324
* [Testing / Development](#testing--development)
2425
* [License](#license)
2526

@@ -105,6 +106,8 @@ Possible Parameters are:
105106
* `timeout`: **Driver** side connection timeout. The value must be a string of decimal numbers, each with optional fraction and a unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. To set a server side timeout, use the parameter [`wait_timeout`](http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_wait_timeout).
106107
* `charset`: Sets the charset used for client-server interaction ("SET NAMES `value`"). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset failes. This enables support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
107108
* `allowAllFiles`: `allowAllFiles=true` disables the file Whitelist for `LOAD DATA LOCAL INFILE` and allows *all* files. *Might be insecure!*
109+
* `parseTime`: `parseTime=true` changes the output type of `DATE` and `DATETIME` values to `time.Time` instead of `[]byte` / `string`
110+
* `loc`: Sets the location for time.Time values (when using `parseTime=true`). The default is `UTC`. *"Local"* sets the system's location. See [time.LoadLocation](http://golang.org/pkg/time/#LoadLocation) for details.
108111

109112
All other parameters are interpreted as system variables:
110113
* `autocommit`: *"SET autocommit=`value`"*
@@ -146,6 +149,13 @@ To use a `io.Reader` a handler function must be registered with `mysql.RegisterR
146149

147150
See also the [godoc of Go-MySQL-Driver](http://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation")
148151

152+
### `time.Time` support
153+
The default internal output type of MySQL `DATE` and `DATETIME` values is `[]byte` which allows you to scan the value into a `[]byte`, `string` or `sql.RawBytes` variable in your programm.
154+
155+
However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` variables, which is the logical opposite in Go to `DATE` and `DATETIME` in MySQL. You can do that by changing the internal output type from `[]byte` to `time.Time` with the DSN parameter `parseTime=true`. You can set the default [`time.Time` location](http://golang.org/pkg/time/#Location) with the `loc` DSN parameter.
156+
**Caution:** As of Go 1.1, this makes `time.Time` the only variable type you can scan `DATE` and `DATETIME` values into. This breaks for example [`sql.RawBytes` support](https://github.com/go-sql-driver/mysql/wiki/Examples#rawbytes).
157+
158+
149159

150160
## Testing / Development
151161
To run the driver tests you may need to adjust the configuration. See [this Wiki-Page](https://github.com/go-sql-driver/mysql/wiki/Testing "Testing") for details.

connection.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"errors"
1515
"net"
1616
"strings"
17+
"time"
1718
)
1819

1920
type mysqlConn struct {
@@ -29,6 +30,7 @@ type mysqlConn struct {
2930
insertId uint64
3031
maxPacketAllowed int
3132
maxWriteSize int
33+
parseTime bool
3234
}
3335

3436
type config struct {
@@ -38,6 +40,7 @@ type config struct {
3840
addr string
3941
dbname string
4042
params map[string]string
43+
loc *time.Location
4144
}
4245

4346
// Handles parameters set in DSN
@@ -59,9 +62,15 @@ func (mc *mysqlConn) handleParams() (err error) {
5962
}
6063

6164
// handled elsewhere
62-
case "timeout", "allowAllFiles":
65+
case "timeout", "allowAllFiles", "loc":
6366
continue
6467

68+
// time.Time parsing
69+
case "parseTime":
70+
if val == "true" {
71+
mc.parseTime = true
72+
}
73+
6574
// TLS-Encryption
6675
case "tls":
6776
err = errors.New("TLS-Encryption not implemented yet")

driver.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ func (d *mysqlDriver) Open(dsn string) (driver.Conn, error) {
2525

2626
// New mysqlConn
2727
mc := &mysqlConn{
28-
cfg: parseDSN(dsn),
2928
maxPacketAllowed: maxPacketSize,
3029
maxWriteSize: maxPacketSize - 1,
3130
}
31+
mc.cfg, err = parseDSN(dsn)
32+
if err != nil {
33+
return nil, err
34+
}
3235

3336
// Connect to Server
3437
if _, ok := mc.cfg.params["timeout"]; ok { // with timeout

driver_test.go

+87-20
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"sync"
1212
"testing"
13+
"time"
1314
)
1415

1516
var (
@@ -408,36 +409,102 @@ func TestDateTime(t *testing.T) {
408409
return
409410
}
410411

411-
db, err := sql.Open("mysql", dsn)
412+
var modes = [2]string{"text", "binary"}
413+
var types = [2]string{"DATE", "DATETIME"}
414+
var tests = [2][]struct {
415+
in interface{}
416+
sOut string
417+
tOut time.Time
418+
tIsZero bool
419+
}{
420+
{
421+
{"2012-06-14", "2012-06-14", time.Date(2012, 6, 14, 0, 0, 0, 0, time.UTC), false},
422+
{"0000-00-00", "0000-00-00", time.Time{}, true},
423+
{time.Date(2012, 6, 14, 0, 0, 0, 0, time.UTC), "2012-06-14", time.Date(2012, 6, 14, 0, 0, 0, 0, time.UTC), false},
424+
{time.Time{}, "0000-00-00", time.Time{}, true},
425+
},
426+
{
427+
{"2011-11-20 21:27:37", "2011-11-20 21:27:37", time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC), false},
428+
{"0000-00-00 00:00:00", "0000-00-00 00:00:00", time.Time{}, true},
429+
{time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC), "2011-11-20 21:27:37", time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC), false},
430+
{time.Time{}, "0000-00-00 00:00:00", time.Time{}, true},
431+
},
432+
}
433+
var sOut string
434+
var tOut time.Time
435+
436+
var rows [2]*sql.Rows
437+
var sDB, tDB *sql.DB
438+
var err error
439+
440+
sDB, err = sql.Open("mysql", dsn)
412441
if err != nil {
413-
t.Fatalf("Error connecting: %v", err)
442+
t.Fatalf("Error connecting (string): %v", err)
414443
}
444+
defer sDB.Close()
415445

416-
defer db.Close()
417-
418-
mustExec(t, db, "DROP TABLE IF EXISTS test")
419-
420-
types := [...]string{"DATE", "DATETIME"}
421-
in := [...]string{"2012-06-14", "2011-11-20 21:27:37"}
422-
var out string
423-
var rows *sql.Rows
446+
tDB, err = sql.Open("mysql", dsn+"&parseTime=true")
447+
if err != nil {
448+
t.Fatalf("Error connecting (time.Time): %v", err)
449+
}
450+
defer tDB.Close()
424451

452+
mustExec(t, sDB, "DROP TABLE IF EXISTS test")
425453
for i, v := range types {
426-
mustExec(t, db, "CREATE TABLE test (value "+v+") CHARACTER SET utf8 COLLATE utf8_unicode_ci")
454+
mustExec(t, sDB, "CREATE TABLE test (value "+v+") CHARACTER SET utf8 COLLATE utf8_unicode_ci")
455+
456+
for j := range tests[i] {
457+
mustExec(t, sDB, "INSERT INTO test VALUES (?)", tests[i][j].in)
458+
459+
// string
460+
rows[0] = mustQuery(t, sDB, "SELECT value FROM test") // text
461+
rows[1] = mustQuery(t, sDB, "SELECT value FROM test WHERE 1 = ?", 1) // binary
462+
463+
for k := range rows {
464+
if rows[k].Next() {
465+
err = rows[k].Scan(&sOut)
466+
if err != nil {
467+
t.Errorf("%s (string %s): %v", v, modes[k], err)
468+
} else if tests[i][j].sOut != sOut {
469+
t.Errorf("%s (string %s): %s != %s", v, modes[k], tests[i][j].sOut, sOut)
470+
}
471+
} else {
472+
err = rows[k].Err()
473+
if err != nil {
474+
t.Errorf("%s (string %s): %v", v, modes[k], err)
475+
} else {
476+
t.Errorf("%s (string %s): no data", v, modes[k])
477+
}
478+
}
479+
}
427480

428-
mustExec(t, db, ("INSERT INTO test VALUES (?)"), in[i])
481+
// time.Time
482+
rows[0] = mustQuery(t, tDB, "SELECT value FROM test") // text
483+
rows[1] = mustQuery(t, tDB, "SELECT value FROM test WHERE 1 = ?", 1) // binary
484+
485+
for k := range rows {
486+
if rows[k].Next() {
487+
err = rows[k].Scan(&tOut)
488+
if err != nil {
489+
t.Errorf("%s (time.Time %s): %v", v, modes[k], err)
490+
} else if tests[i][j].tOut != tOut || tests[i][j].tIsZero != tOut.IsZero() {
491+
t.Errorf("%s (time.Time %s): %s [%t] != %s [%t]", v, modes[k], tests[i][j].tOut, tests[i][j].tIsZero, tOut, tOut.IsZero())
492+
}
493+
} else {
494+
err = rows[k].Err()
495+
if err != nil {
496+
t.Errorf("%s (time.Time %s): %v", v, modes[k], err)
497+
} else {
498+
t.Errorf("%s (time.Time %s): no data", v, modes[k])
499+
}
429500

430-
rows = mustQuery(t, db, ("SELECT value FROM test"))
431-
if rows.Next() {
432-
rows.Scan(&out)
433-
if in[i] != out {
434-
t.Errorf("%s: %s != %s", v, in[i], out)
501+
}
435502
}
436-
} else {
437-
t.Errorf("%s: no data", v)
503+
504+
mustExec(t, sDB, "TRUNCATE TABLE test")
438505
}
439506

440-
mustExec(t, db, "DROP TABLE IF EXISTS test")
507+
mustExec(t, sDB, "DROP TABLE IF EXISTS test")
441508
}
442509
}
443510

packets.go

+55-64
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,21 @@ func (rows *mysqlRows) readRow(dest []driver.Value) (err error) {
551551
pos += n
552552
if err == nil {
553553
if !isNull {
554-
continue
554+
if !rows.mc.parseTime {
555+
continue
556+
} else {
557+
switch rows.columns[i].fieldType {
558+
case fieldTypeTimestamp, fieldTypeDateTime,
559+
fieldTypeDate, fieldTypeNewDate:
560+
dest[i], err = parseDateTime(string(dest[i].([]byte)), rows.mc.cfg.loc)
561+
if err == nil {
562+
continue
563+
}
564+
default:
565+
continue
566+
}
567+
}
568+
555569
} else {
556570
dest[i] = nil
557571
continue
@@ -751,7 +765,14 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
751765

752766
case time.Time:
753767
paramTypes[i<<1] = fieldTypeString
754-
val := []byte(v.Format(timeFormat))
768+
769+
var val []byte
770+
if v.IsZero() {
771+
val = []byte("0000-00-00")
772+
} else {
773+
val = []byte(v.Format(timeFormat))
774+
}
775+
755776
paramValues[i] = append(
756777
lengthEncodedIntegerToBytes(uint64(len(val))),
757778
val...,
@@ -815,8 +836,8 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
815836
}
816837

817838
// http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::ResultsetRow
818-
func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
819-
data, err := rc.mc.readPacket()
839+
func (rows *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
840+
data, err := rows.mc.readPacket()
820841
if err != nil {
821842
return
822843
}
@@ -828,7 +849,7 @@ func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
828849
return io.EOF
829850
} else {
830851
// Error otherwise
831-
return rc.mc.handleErrorPacket(data)
852+
return rows.mc.handleErrorPacket(data)
832853
}
833854
}
834855

@@ -848,10 +869,10 @@ func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
848869
continue
849870
}
850871

851-
unsigned = rc.columns[i].flags&flagUnsigned != 0
872+
unsigned = rows.columns[i].flags&flagUnsigned != 0
852873

853874
// Convert to byte-coded string
854-
switch rc.columns[i].fieldType {
875+
switch rows.columns[i].fieldType {
855876
case fieldTypeNULL:
856877
dest[i] = nil
857878
continue
@@ -934,21 +955,22 @@ func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
934955

935956
pos += n
936957

937-
if num == 0 {
938-
if isNull {
939-
dest[i] = nil
940-
continue
941-
} else {
942-
dest[i] = []byte("0000-00-00")
943-
continue
944-
}
958+
if isNull {
959+
dest[i] = nil
960+
continue
961+
}
962+
963+
if rows.mc.parseTime {
964+
dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc)
945965
} else {
946-
dest[i] = []byte(fmt.Sprintf("%04d-%02d-%02d",
947-
binary.LittleEndian.Uint16(data[pos:pos+2]),
948-
data[pos+2],
949-
data[pos+3]))
966+
dest[i], err = formatBinaryDate(num, data[pos:])
967+
}
968+
969+
if err == nil {
950970
pos += int(num)
951971
continue
972+
} else {
973+
return err
952974
}
953975

954976
// Time [-][H]HH:MM:SS[.fractal]
@@ -1008,58 +1030,27 @@ func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
10081030

10091031
pos += n
10101032

1011-
if num == 0 {
1012-
if isNull {
1013-
dest[i] = nil
1014-
continue
1015-
} else {
1016-
dest[i] = []byte("0000-00-00 00:00:00")
1017-
continue
1018-
}
1033+
if isNull {
1034+
dest[i] = nil
1035+
continue
10191036
}
10201037

1021-
switch num {
1022-
case 4:
1023-
dest[i] = []byte(fmt.Sprintf(
1024-
"%04d-%02d-%02d 00:00:00",
1025-
binary.LittleEndian.Uint16(data[pos:pos+2]),
1026-
data[pos+2],
1027-
data[pos+3],
1028-
))
1029-
pos += 4
1030-
continue
1031-
case 7:
1032-
dest[i] = []byte(fmt.Sprintf(
1033-
"%04d-%02d-%02d %02d:%02d:%02d",
1034-
binary.LittleEndian.Uint16(data[pos:pos+2]),
1035-
data[pos+2],
1036-
data[pos+3],
1037-
data[pos+4],
1038-
data[pos+5],
1039-
data[pos+6],
1040-
))
1041-
pos += 7
1042-
continue
1043-
case 11:
1044-
dest[i] = []byte(fmt.Sprintf(
1045-
"%04d-%02d-%02d %02d:%02d:%02d.%06d",
1046-
binary.LittleEndian.Uint16(data[pos:pos+2]),
1047-
data[pos+2],
1048-
data[pos+3],
1049-
data[pos+4],
1050-
data[pos+5],
1051-
data[pos+6],
1052-
binary.LittleEndian.Uint32(data[pos+7:pos+11]),
1053-
))
1054-
pos += 11
1038+
if rows.mc.parseTime {
1039+
dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc)
1040+
} else {
1041+
dest[i], err = formatBinaryDateTime(num, data[pos:])
1042+
}
1043+
1044+
if err == nil {
1045+
pos += int(num)
10551046
continue
1056-
default:
1057-
return fmt.Errorf("Invalid DATETIME-packet length %d", num)
1047+
} else {
1048+
return err
10581049
}
10591050

10601051
// Please report if this happens!
10611052
default:
1062-
return fmt.Errorf("Unknown FieldType %d", rc.columns[i].fieldType)
1053+
return fmt.Errorf("Unknown FieldType %d", rows.columns[i].fieldType)
10631054
}
10641055
}
10651056

0 commit comments

Comments
 (0)