Skip to content

Commit f56d52b

Browse files
committed
Add NullTime struct
1 parent 26af0eb commit f56d52b

File tree

4 files changed

+137
-42
lines changed

4 files changed

+137
-42
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` v
159159

160160
**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).
161161

162+
Alternatively you can use the [`NullTime`](http://godoc.org/github.com/go-sql-driver/mysql#NullTime) type as the scan destination, which works with both time.Time and string / []byte.
163+
162164

163165

164166
## Testing / Development
@@ -167,7 +169,7 @@ To run the driver tests you may need to adjust the configuration. See the [Testi
167169
Go-MySQL-Driver is not feature-complete yet. Your help is very appreciated.
168170
If you want to contribute, you can work on an [open issue](https://github.com/go-sql-driver/mysql/issues?state=open) or review a [pull request](https://github.com/go-sql-driver/mysql/pulls).
169171

170-
Code changes must be proposed via a Pull Request and must be reviewed. Only *LGTM*-ed (" *Looks good to me* ") code may be committed to the master branch.
172+
Code changes must be proposed via a Pull Request and must be reviewed. Only *LGTM*-ed (" *Looks good to me* ") code may be committed to the master branch.
171173

172174
---------------------------------------
173175

driver_test.go

+19-16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ var (
1919
available bool
2020
)
2121

22+
var (
23+
tDate = time.Date(2012, 6, 14, 0, 0, 0, 0, time.UTC)
24+
sDate = "2012-06-14"
25+
tDateTime = time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)
26+
sDateTime = "2011-11-20 21:27:37"
27+
tDate0 = time.Time{}
28+
sDate0 = "0000-00-00"
29+
sDateTime0 = "0000-00-00 00:00:00"
30+
)
31+
2232
// See https://github.com/go-sql-driver/mysql/wiki/Testing
2333
func init() {
2434
env := func(key, defaultValue string) string {
@@ -396,29 +406,22 @@ func TestDateTime(t *testing.T) {
396406
test tester
397407
}
398408
var (
399-
tdate = time.Date(2012, 6, 14, 0, 0, 0, 0, time.UTC)
400-
sdate = "2012-06-14"
401-
tdatetime = time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)
402-
sdatetime = "2011-11-20 21:27:37"
403-
tdate0 = time.Time{}
404-
sdate0 = "0000-00-00"
405-
sdatetime0 = "0000-00-00 00:00:00"
406-
modes = map[string]*testmode{
409+
modes = map[string]*testmode{
407410
"text": &testmode{},
408411
"binary": &testmode{" WHERE 1 = ?", []interface{}{1}},
409412
}
410413
timetests = map[string][]*timetest{
411414
"DATE": {
412-
{sdate, sdate, tdate, false},
413-
{sdate0, sdate0, tdate0, true},
414-
{tdate, sdate, tdate, false},
415-
{tdate0, sdate0, tdate0, true},
415+
{sDate, sDate, tDate, false},
416+
{sDate0, sDate0, tDate0, true},
417+
{tDate, sDate, tDate, false},
418+
{tDate0, sDate0, tDate0, true},
416419
},
417420
"DATETIME": {
418-
{sdatetime, sdatetime, tdatetime, false},
419-
{sdatetime0, sdatetime0, tdate0, true},
420-
{tdatetime, sdatetime, tdatetime, false},
421-
{tdate0, sdatetime0, tdate0, true},
421+
{sDateTime, sDateTime, tDateTime, false},
422+
{sDateTime0, sDateTime0, tDate0, true},
423+
{tDateTime, sDateTime, tDateTime, false},
424+
{tDate0, sDateTime0, tDate0, true},
422425
},
423426
}
424427
setups = []*setup{

utils.go

+59-9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,58 @@ import (
2222
"time"
2323
)
2424

25+
// NullTime represents a time.Time that may be NULL.
26+
// NullTime implements the Scanner interface so
27+
// it can be used as a scan destination:
28+
//
29+
// var nt NullTime
30+
// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt)
31+
// ...
32+
// if nt.Valid {
33+
// // use nt.Time
34+
// } else {
35+
// // NULL value
36+
// }
37+
//
38+
// This NullTime implementation is not driver-specific
39+
type NullTime struct {
40+
Time time.Time
41+
Valid bool // Valid is true if Time is not NULL
42+
}
43+
44+
// Scan implements the Scanner interface.
45+
// The value type must be time.Time or string / []byte (formatted time-string),
46+
// otherwise Scan fails.
47+
func (nt *NullTime) Scan(value interface{}) (err error) {
48+
if value == nil {
49+
nt.Time, nt.Valid = time.Time{}, false
50+
} else {
51+
switch v := value.(type) {
52+
case time.Time:
53+
nt.Time, nt.Valid = v, true
54+
case []byte:
55+
nt.Time, err = parseDateTime(string(v), time.UTC)
56+
nt.Valid = (err == nil)
57+
case string:
58+
nt.Time, err = parseDateTime(v, time.UTC)
59+
nt.Valid = (err == nil)
60+
default:
61+
nt.Valid = false
62+
err = fmt.Errorf("Can't convert %T to time.Time", v)
63+
}
64+
}
65+
66+
return
67+
}
68+
69+
// Value implements the driver Valuer interface.
70+
func (nt NullTime) Value() (driver.Value, error) {
71+
if !nt.Valid {
72+
return nil, nil
73+
}
74+
return nt.Time, nil
75+
}
76+
2577
// Logger
2678
var (
2779
errLog *log.Logger
@@ -116,33 +168,31 @@ func scramblePassword(scramble, password []byte) []byte {
116168
return scramble
117169
}
118170

119-
func parseDateTime(str string, loc *time.Location) (driver.Value, error) {
120-
var t time.Time
121-
var err error
122-
171+
func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
123172
switch len(str) {
124173
case 10: // YYYY-MM-DD
125174
if str == "0000-00-00" {
126-
return time.Time{}, nil
175+
return
127176
}
128177
t, err = time.Parse(timeFormat[:10], str)
129178
case 19: // YYYY-MM-DD HH:MM:SS
130179
if str == "0000-00-00 00:00:00" {
131-
return time.Time{}, nil
180+
return
132181
}
133182
t, err = time.Parse(timeFormat, str)
134183
default:
135-
return nil, fmt.Errorf("Invalid Time-String: %s", str)
184+
err = fmt.Errorf("Invalid Time-String: %s", str)
185+
return
136186
}
137187

138188
// Adjust location
139189
if err == nil && loc != time.UTC {
140190
y, mo, d := t.Date()
141191
h, mi, s := t.Clock()
142-
return time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
192+
t, err = time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
143193
}
144194

145-
return t, err
195+
return
146196
}
147197

148198
func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Value, error) {

utils_test.go

+56-16
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,23 @@ import (
1515
"time"
1616
)
1717

18-
var testDSNs = []struct {
19-
in string
20-
out string
21-
loc *time.Location
22-
}{
23-
{"username:password@protocol(address)/dbname?param=value", "&{user:username passwd:password net:protocol addr:address dbname:dbname params:map[param:value] loc:%p}", time.UTC},
24-
{"user@unix(/path/to/socket)/dbname?charset=utf8", "&{user:user passwd: net:unix addr:/path/to/socket dbname:dbname params:map[charset:utf8] loc:%p}", time.UTC},
25-
{"user:password@tcp(localhost:5555)/dbname?charset=utf8", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8] loc:%p}", time.UTC},
26-
{"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8mb4,utf8] loc:%p}", time.UTC},
27-
{"user:password@/dbname?loc=UTC", "&{user:user passwd:password net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[loc:UTC] loc:%p}", time.UTC},
28-
{"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{user:user passwd:p@ss(word) net:tcp addr:[de:ad:be:ef::ca:fe]:80 dbname:dbname params:map[loc:Local] loc:%p}", time.Local},
29-
{"/dbname", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[] loc:%p}", time.UTC},
30-
{"/", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p}", time.UTC},
31-
{"user:p@/ssword@/", "&{user:user passwd:p@/ssword net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p}", time.UTC},
32-
}
33-
3418
func TestDSNParser(t *testing.T) {
19+
var testDSNs = []struct {
20+
in string
21+
out string
22+
loc *time.Location
23+
}{
24+
{"username:password@protocol(address)/dbname?param=value", "&{user:username passwd:password net:protocol addr:address dbname:dbname params:map[param:value] loc:%p}", time.UTC},
25+
{"user@unix(/path/to/socket)/dbname?charset=utf8", "&{user:user passwd: net:unix addr:/path/to/socket dbname:dbname params:map[charset:utf8] loc:%p}", time.UTC},
26+
{"user:password@tcp(localhost:5555)/dbname?charset=utf8", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8] loc:%p}", time.UTC},
27+
{"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8mb4,utf8] loc:%p}", time.UTC},
28+
{"user:password@/dbname?loc=UTC", "&{user:user passwd:password net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[loc:UTC] loc:%p}", time.UTC},
29+
{"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{user:user passwd:p@ss(word) net:tcp addr:[de:ad:be:ef::ca:fe]:80 dbname:dbname params:map[loc:Local] loc:%p}", time.Local},
30+
{"/dbname", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[] loc:%p}", time.UTC},
31+
{"/", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p}", time.UTC},
32+
{"user:p@/ssword@/", "&{user:user passwd:p@/ssword net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p}", time.UTC},
33+
}
34+
3535
var cfg *config
3636
var err error
3737
var res string
@@ -48,3 +48,43 @@ func TestDSNParser(t *testing.T) {
4848
}
4949
}
5050
}
51+
52+
func TestScanNullTime(t *testing.T) {
53+
var scanTests = []struct {
54+
in interface{}
55+
error bool
56+
valid bool
57+
time time.Time
58+
}{
59+
{tDate, false, true, tDate},
60+
{sDate, false, true, tDate},
61+
{[]byte(sDate), false, true, tDate},
62+
{tDateTime, false, true, tDateTime},
63+
{sDateTime, false, true, tDateTime},
64+
{[]byte(sDateTime), false, true, tDateTime},
65+
{tDate0, false, true, tDate0},
66+
{sDate0, false, true, tDate0},
67+
{[]byte(sDate0), false, true, tDate0},
68+
{sDateTime0, false, true, tDate0},
69+
{[]byte(sDateTime0), false, true, tDate0},
70+
{"", true, false, tDate0},
71+
{"1234", true, false, tDate0},
72+
{0, true, false, tDate0},
73+
}
74+
75+
var nt = NullTime{}
76+
var err error
77+
78+
for _, tst := range scanTests {
79+
err = nt.Scan(tst.in)
80+
if (err != nil) != tst.error {
81+
t.Errorf("%v: expected error status %b, got %b", tst.in, tst.error, (err != nil))
82+
}
83+
if nt.Valid != tst.valid {
84+
t.Errorf("%v: expected valid status %b, got %b", tst.in, tst.valid, nt.Valid)
85+
}
86+
if nt.Time != tst.time {
87+
t.Errorf("%v: expected time %v, got %v", tst.in, tst.time, nt.Time)
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)