Store/retrieve timezones for time.Time values.

Previously, the timezone information for a provided value was discarded
and the value always stored as in UTC.  However, sqlite allows specifying
the timezone offsets and handles those values appropriately.  This change
stores the timezone information and parses it out if present, otherwise
it defaults to UTC as before.

One additional bugfix:  Previously, a unix timestamp in seconds was
parsed in the local timezone (rather than UTC), in contrast to a unix
timestamp in milliseconds that was parsed in UTC.

While fixing that extra bug, I cleaned up the parsing code -- no need to
convert to a string and then parse it back again and risk a parse error,
just to check the number of digits.

The tests were extended to cover non-UTC timezones storage & retrieval,
meaningful unix timestamps, and correct handling of a trailing Z.
type
Augusto Roman 2015-10-09 22:59:25 -07:00
parent b808f01f66
commit 7b0d180ce9
2 changed files with 34 additions and 14 deletions

View File

@ -99,6 +99,10 @@ import (
// into the database. When parsing a string from a timestamp or // into the database. When parsing a string from a timestamp or
// datetime column, the formats are tried in order. // datetime column, the formats are tried in order.
var SQLiteTimestampFormats = []string{ var SQLiteTimestampFormats = []string{
// By default, store timestamps with whatever timezone they come with.
// When parsed, they will be returned with the same timezone.
"2006-01-02 15:04:05.999999999-07:00",
"2006-01-02T15:04:05.999999999-07:00",
"2006-01-02 15:04:05.999999999", "2006-01-02 15:04:05.999999999",
"2006-01-02T15:04:05.999999999", "2006-01-02T15:04:05.999999999",
"2006-01-02 15:04:05", "2006-01-02 15:04:05",
@ -106,7 +110,6 @@ var SQLiteTimestampFormats = []string{
"2006-01-02 15:04", "2006-01-02 15:04",
"2006-01-02T15:04", "2006-01-02T15:04",
"2006-01-02", "2006-01-02",
"2006-01-02 15:04:05-07:00",
} }
func init() { func init() {
@ -803,7 +806,7 @@ func (s *SQLiteStmt) bind(args []driver.Value) error {
} }
rv = C._sqlite3_bind_blob(s.s, n, unsafe.Pointer(p), C.int(len(v))) rv = C._sqlite3_bind_blob(s.s, n, unsafe.Pointer(p), C.int(len(v)))
case time.Time: case time.Time:
b := []byte(v.UTC().Format(SQLiteTimestampFormats[0])) b := []byte(v.Format(SQLiteTimestampFormats[0]))
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(&b[0])), C.int(len(b))) rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(&b[0])), C.int(len(b)))
} }
if rv != C.SQLITE_OK { if rv != C.SQLITE_OK {
@ -902,18 +905,15 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i))) val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i)))
switch rc.decltype[i] { switch rc.decltype[i] {
case "timestamp", "datetime", "date": case "timestamp", "datetime", "date":
unixTimestamp := strconv.FormatInt(val, 10)
var t time.Time var t time.Time
if len(unixTimestamp) == 13 { // Assume a millisecond unix timestamp if it's 13 digits -- too
duration, err := time.ParseDuration(unixTimestamp + "ms") // large to be a reasonable timestamp in seconds.
if err != nil { if val > 1e12 || val < -1e12 {
return fmt.Errorf("error parsing %s value %d, %s", rc.decltype[i], val, err) val *= int64(time.Millisecond) // convert ms to nsec
}
epoch := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
t = epoch.Add(duration)
} else { } else {
t = time.Unix(val, 0) val *= int64(time.Second) // convert sec to nsec
} }
t = time.Unix(0, val).UTC()
if rc.s.c.loc != nil { if rc.s.c.loc != nil {
t = t.In(rc.s.c.loc) t = t.In(rc.s.c.loc)
} }

View File

@ -324,6 +324,8 @@ func TestBooleanRoundtrip(t *testing.T) {
} }
} }
func timezone(t time.Time) string { return t.Format("-07:00") }
func TestTimestamp(t *testing.T) { func TestTimestamp(t *testing.T) {
tempFilename := TempFilename() tempFilename := TempFilename()
db, err := sql.Open("sqlite3", tempFilename) db, err := sql.Open("sqlite3", tempFilename)
@ -342,6 +344,7 @@ func TestTimestamp(t *testing.T) {
timestamp1 := time.Date(2012, time.April, 6, 22, 50, 0, 0, time.UTC) timestamp1 := time.Date(2012, time.April, 6, 22, 50, 0, 0, time.UTC)
timestamp2 := time.Date(2006, time.January, 2, 15, 4, 5, 123456789, time.UTC) timestamp2 := time.Date(2006, time.January, 2, 15, 4, 5, 123456789, time.UTC)
timestamp3 := time.Date(2012, time.November, 4, 0, 0, 0, 0, time.UTC) timestamp3 := time.Date(2012, time.November, 4, 0, 0, 0, 0, time.UTC)
tzTest := time.FixedZone("TEST", -9*3600-13*60)
tests := []struct { tests := []struct {
value interface{} value interface{}
expected time.Time expected time.Time
@ -349,9 +352,9 @@ func TestTimestamp(t *testing.T) {
{"nonsense", time.Time{}}, {"nonsense", time.Time{}},
{"0000-00-00 00:00:00", time.Time{}}, {"0000-00-00 00:00:00", time.Time{}},
{timestamp1, timestamp1}, {timestamp1, timestamp1},
{timestamp1.Unix(), timestamp1}, {timestamp2.Unix(), timestamp2.Truncate(time.Second)},
{timestamp1.UnixNano() / int64(time.Millisecond), timestamp1}, {timestamp2.UnixNano() / int64(time.Millisecond), timestamp2.Truncate(time.Millisecond)},
{timestamp1.In(time.FixedZone("TEST", -7*3600)), timestamp1}, {timestamp1.In(tzTest), timestamp1.In(tzTest)},
{timestamp1.Format("2006-01-02 15:04:05.000"), timestamp1}, {timestamp1.Format("2006-01-02 15:04:05.000"), timestamp1},
{timestamp1.Format("2006-01-02T15:04:05.000"), timestamp1}, {timestamp1.Format("2006-01-02T15:04:05.000"), timestamp1},
{timestamp1.Format("2006-01-02 15:04:05"), timestamp1}, {timestamp1.Format("2006-01-02 15:04:05"), timestamp1},
@ -359,6 +362,7 @@ func TestTimestamp(t *testing.T) {
{timestamp2, timestamp2}, {timestamp2, timestamp2},
{"2006-01-02 15:04:05.123456789", timestamp2}, {"2006-01-02 15:04:05.123456789", timestamp2},
{"2006-01-02T15:04:05.123456789", timestamp2}, {"2006-01-02T15:04:05.123456789", timestamp2},
{"2006-01-02T05:51:05.123456789-09:13", timestamp2.In(tzTest)},
{"2012-11-04", timestamp3}, {"2012-11-04", timestamp3},
{"2012-11-04 00:00", timestamp3}, {"2012-11-04 00:00", timestamp3},
{"2012-11-04 00:00:00", timestamp3}, {"2012-11-04 00:00:00", timestamp3},
@ -366,6 +370,14 @@ func TestTimestamp(t *testing.T) {
{"2012-11-04T00:00", timestamp3}, {"2012-11-04T00:00", timestamp3},
{"2012-11-04T00:00:00", timestamp3}, {"2012-11-04T00:00:00", timestamp3},
{"2012-11-04T00:00:00.000", timestamp3}, {"2012-11-04T00:00:00.000", timestamp3},
{"2006-01-02T15:04:05.123456789Z", timestamp2},
{"2012-11-04Z", timestamp3},
{"2012-11-04 00:00Z", timestamp3},
{"2012-11-04 00:00:00Z", timestamp3},
{"2012-11-04 00:00:00.000Z", timestamp3},
{"2012-11-04T00:00Z", timestamp3},
{"2012-11-04T00:00:00Z", timestamp3},
{"2012-11-04T00:00:00.000Z", timestamp3},
} }
for i := range tests { for i := range tests {
_, err = db.Exec("INSERT INTO foo(id, ts, dt) VALUES(?, ?, ?)", i, tests[i].value, tests[i].value) _, err = db.Exec("INSERT INTO foo(id, ts, dt) VALUES(?, ?, ?)", i, tests[i].value, tests[i].value)
@ -400,6 +412,14 @@ func TestTimestamp(t *testing.T) {
if !tests[id].expected.Equal(dt) { if !tests[id].expected.Equal(dt) {
t.Errorf("Datetime value for id %v (%v) should be %v, not %v", id, tests[id].value, tests[id].expected, dt) t.Errorf("Datetime value for id %v (%v) should be %v, not %v", id, tests[id].value, tests[id].expected, dt)
} }
if timezone(tests[id].expected) != timezone(ts) {
t.Errorf("Timezone for id %v (%v) should be %v, not %v", id, tests[id].value,
timezone(tests[id].expected), timezone(ts))
}
if timezone(tests[id].expected) != timezone(dt) {
t.Errorf("Timezone for id %v (%v) should be %v, not %v", id, tests[id].value,
timezone(tests[id].expected), timezone(dt))
}
} }
if seen != len(tests) { if seen != len(tests) {