Skip to content

Commit 7b0d180

Browse files
committed
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.
1 parent b808f01 commit 7b0d180

File tree

2 files changed

+34
-14
lines changed

2 files changed

+34
-14
lines changed

sqlite3.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,17 @@ import (
9999
// into the database. When parsing a string from a timestamp or
100100
// datetime column, the formats are tried in order.
101101
var SQLiteTimestampFormats = []string{
102+
// By default, store timestamps with whatever timezone they come with.
103+
// When parsed, they will be returned with the same timezone.
104+
"2006-01-02 15:04:05.999999999-07:00",
105+
"2006-01-02T15:04:05.999999999-07:00",
102106
"2006-01-02 15:04:05.999999999",
103107
"2006-01-02T15:04:05.999999999",
104108
"2006-01-02 15:04:05",
105109
"2006-01-02T15:04:05",
106110
"2006-01-02 15:04",
107111
"2006-01-02T15:04",
108112
"2006-01-02",
109-
"2006-01-02 15:04:05-07:00",
110113
}
111114

112115
func init() {
@@ -803,7 +806,7 @@ func (s *SQLiteStmt) bind(args []driver.Value) error {
803806
}
804807
rv = C._sqlite3_bind_blob(s.s, n, unsafe.Pointer(p), C.int(len(v)))
805808
case time.Time:
806-
b := []byte(v.UTC().Format(SQLiteTimestampFormats[0]))
809+
b := []byte(v.Format(SQLiteTimestampFormats[0]))
807810
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(&b[0])), C.int(len(b)))
808811
}
809812
if rv != C.SQLITE_OK {
@@ -902,18 +905,15 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
902905
val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i)))
903906
switch rc.decltype[i] {
904907
case "timestamp", "datetime", "date":
905-
unixTimestamp := strconv.FormatInt(val, 10)
906908
var t time.Time
907-
if len(unixTimestamp) == 13 {
908-
duration, err := time.ParseDuration(unixTimestamp + "ms")
909-
if err != nil {
910-
return fmt.Errorf("error parsing %s value %d, %s", rc.decltype[i], val, err)
911-
}
912-
epoch := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
913-
t = epoch.Add(duration)
909+
// Assume a millisecond unix timestamp if it's 13 digits -- too
910+
// large to be a reasonable timestamp in seconds.
911+
if val > 1e12 || val < -1e12 {
912+
val *= int64(time.Millisecond) // convert ms to nsec
914913
} else {
915-
t = time.Unix(val, 0)
914+
val *= int64(time.Second) // convert sec to nsec
916915
}
916+
t = time.Unix(0, val).UTC()
917917
if rc.s.c.loc != nil {
918918
t = t.In(rc.s.c.loc)
919919
}

sqlite3_test.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ func TestBooleanRoundtrip(t *testing.T) {
324324
}
325325
}
326326

327+
func timezone(t time.Time) string { return t.Format("-07:00") }
328+
327329
func TestTimestamp(t *testing.T) {
328330
tempFilename := TempFilename()
329331
db, err := sql.Open("sqlite3", tempFilename)
@@ -342,30 +344,40 @@ func TestTimestamp(t *testing.T) {
342344
timestamp1 := time.Date(2012, time.April, 6, 22, 50, 0, 0, time.UTC)
343345
timestamp2 := time.Date(2006, time.January, 2, 15, 4, 5, 123456789, time.UTC)
344346
timestamp3 := time.Date(2012, time.November, 4, 0, 0, 0, 0, time.UTC)
347+
tzTest := time.FixedZone("TEST", -9*3600-13*60)
345348
tests := []struct {
346349
value interface{}
347350
expected time.Time
348351
}{
349352
{"nonsense", time.Time{}},
350353
{"0000-00-00 00:00:00", time.Time{}},
351354
{timestamp1, timestamp1},
352-
{timestamp1.Unix(), timestamp1},
353-
{timestamp1.UnixNano() / int64(time.Millisecond), timestamp1},
354-
{timestamp1.In(time.FixedZone("TEST", -7*3600)), timestamp1},
355+
{timestamp2.Unix(), timestamp2.Truncate(time.Second)},
356+
{timestamp2.UnixNano() / int64(time.Millisecond), timestamp2.Truncate(time.Millisecond)},
357+
{timestamp1.In(tzTest), timestamp1.In(tzTest)},
355358
{timestamp1.Format("2006-01-02 15:04:05.000"), timestamp1},
356359
{timestamp1.Format("2006-01-02T15:04:05.000"), timestamp1},
357360
{timestamp1.Format("2006-01-02 15:04:05"), timestamp1},
358361
{timestamp1.Format("2006-01-02T15:04:05"), timestamp1},
359362
{timestamp2, timestamp2},
360363
{"2006-01-02 15:04:05.123456789", timestamp2},
361364
{"2006-01-02T15:04:05.123456789", timestamp2},
365+
{"2006-01-02T05:51:05.123456789-09:13", timestamp2.In(tzTest)},
362366
{"2012-11-04", timestamp3},
363367
{"2012-11-04 00:00", timestamp3},
364368
{"2012-11-04 00:00:00", timestamp3},
365369
{"2012-11-04 00:00:00.000", timestamp3},
366370
{"2012-11-04T00:00", timestamp3},
367371
{"2012-11-04T00:00:00", timestamp3},
368372
{"2012-11-04T00:00:00.000", timestamp3},
373+
{"2006-01-02T15:04:05.123456789Z", timestamp2},
374+
{"2012-11-04Z", timestamp3},
375+
{"2012-11-04 00:00Z", timestamp3},
376+
{"2012-11-04 00:00:00Z", timestamp3},
377+
{"2012-11-04 00:00:00.000Z", timestamp3},
378+
{"2012-11-04T00:00Z", timestamp3},
379+
{"2012-11-04T00:00:00Z", timestamp3},
380+
{"2012-11-04T00:00:00.000Z", timestamp3},
369381
}
370382
for i := range tests {
371383
_, 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) {
400412
if !tests[id].expected.Equal(dt) {
401413
t.Errorf("Datetime value for id %v (%v) should be %v, not %v", id, tests[id].value, tests[id].expected, dt)
402414
}
415+
if timezone(tests[id].expected) != timezone(ts) {
416+
t.Errorf("Timezone for id %v (%v) should be %v, not %v", id, tests[id].value,
417+
timezone(tests[id].expected), timezone(ts))
418+
}
419+
if timezone(tests[id].expected) != timezone(dt) {
420+
t.Errorf("Timezone for id %v (%v) should be %v, not %v", id, tests[id].value,
421+
timezone(tests[id].expected), timezone(dt))
422+
}
403423
}
404424

405425
if seen != len(tests) {

0 commit comments

Comments
 (0)