Skip to content

Commit 73f5a73

Browse files
committed
Merge pull request #63 from go-sql-driver/nulltime
Add NullTime struct Fixes #59
2 parents 1caf647 + 929dfe8 commit 73f5a73

File tree

4 files changed

+139
-42
lines changed

4 files changed

+139
-42
lines changed

Diff for: 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

Diff for: 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{

Diff for: utils.go

+61-9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,60 @@ 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+
return
51+
}
52+
53+
switch v := value.(type) {
54+
case time.Time:
55+
nt.Time, nt.Valid = v, true
56+
return
57+
case []byte:
58+
nt.Time, err = parseDateTime(string(v), time.UTC)
59+
nt.Valid = (err == nil)
60+
return
61+
case string:
62+
nt.Time, err = parseDateTime(v, time.UTC)
63+
nt.Valid = (err == nil)
64+
return
65+
}
66+
67+
nt.Valid = false
68+
return fmt.Errorf("Can't convert %T to time.Time", value)
69+
}
70+
71+
// Value implements the driver Valuer interface.
72+
func (nt NullTime) Value() (driver.Value, error) {
73+
if !nt.Valid {
74+
return nil, nil
75+
}
76+
return nt.Time, nil
77+
}
78+
2579
// Logger
2680
var (
2781
errLog *log.Logger
@@ -116,33 +170,31 @@ func scramblePassword(scramble, password []byte) []byte {
116170
return scramble
117171
}
118172

119-
func parseDateTime(str string, loc *time.Location) (driver.Value, error) {
120-
var t time.Time
121-
var err error
122-
173+
func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
123174
switch len(str) {
124175
case 10: // YYYY-MM-DD
125176
if str == "0000-00-00" {
126-
return time.Time{}, nil
177+
return
127178
}
128179
t, err = time.Parse(timeFormat[:10], str)
129180
case 19: // YYYY-MM-DD HH:MM:SS
130181
if str == "0000-00-00 00:00:00" {
131-
return time.Time{}, nil
182+
return
132183
}
133184
t, err = time.Parse(timeFormat, str)
134185
default:
135-
return nil, fmt.Errorf("Invalid Time-String: %s", str)
186+
err = fmt.Errorf("Invalid Time-String: %s", str)
187+
return
136188
}
137189

138190
// Adjust location
139191
if err == nil && loc != time.UTC {
140192
y, mo, d := t.Date()
141193
h, mi, s := t.Clock()
142-
return time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
194+
t, err = time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
143195
}
144196

145-
return t, err
197+
return
146198
}
147199

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

Diff for: 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)