diff --git a/driver/driver.go b/driver/driver.go index 072afee..b95ea52 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -324,7 +324,7 @@ func (r rows) Next(dest []driver.Value) error { case sqlite3.FLOAT: dest[i] = r.stmt.ColumnFloat(i) case sqlite3.TEXT: - dest[i] = maybeDate(r.stmt.ColumnText(i)) + dest[i] = maybeTime(r.stmt.ColumnText(i)) case sqlite3.BLOB: buf, _ := dest[i].([]byte) dest[i] = r.stmt.ColumnBlob(i, buf) diff --git a/driver/time.go b/driver/time.go index 12958ca..5c4f3d0 100644 --- a/driver/time.go +++ b/driver/time.go @@ -9,7 +9,7 @@ import ( // if it roundtrips back to the same string. // This way times can be persisted to, and recovered from, the database, // but if a string is needed, [database/sql] will recover the same string. -func maybeDate(text string) driver.Value { +func maybeTime(text string) driver.Value { // Weed out (some) values that can't possibly be // [time.RFC3339Nano] timestamps. if len(text) < len("2006-01-02T15:04:05Z") { diff --git a/driver/time_test.go b/driver/time_test.go index 66d418e..2690159 100644 --- a/driver/time_test.go +++ b/driver/time_test.go @@ -5,7 +5,8 @@ import ( "time" ) -func Fuzz_maybeDate(f *testing.F) { +// This checks that any string can be recovered as the same string. +func Fuzz_maybeTime_1(f *testing.F) { f.Add("") f.Add(" ") f.Add("SQLite") @@ -21,7 +22,7 @@ func Fuzz_maybeDate(f *testing.F) { f.Add("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") f.Fuzz(func(t *testing.T, str string) { - value := maybeDate(str) + value := maybeTime(str) switch v := value.(type) { case time.Time: @@ -44,3 +45,56 @@ func Fuzz_maybeDate(f *testing.F) { } }) } + +// This checks that any [time.Time] can be recovered as a [time.Time], +// with nanosecond accuracy, and preserving any timezone offset. +func Fuzz_maybeTime_2(f *testing.F) { + f.Add(0, 0) + f.Add(0, 1) + f.Add(0, -1) + f.Add(0, 999_999_999) + f.Add(0, 1_000_000_000) + f.Add(7956915742, 222_222_222) // twosday + f.Add(639095955742, 222_222_222) // twosday, year 22222AD + f.Add(-763421161058, 222_222_222) // twosday, year 22222BC + + checkTime := func(t *testing.T, date time.Time) { + value := maybeTime(date.Format(time.RFC3339Nano)) + + switch v := value.(type) { + case time.Time: + // Make sure times round-trip to the same time: + if !v.Equal(date) { + t.Fatalf("did not round-trip: %v", date) + } + // Make with the same zone offset: + _, off1 := v.Zone() + _, off2 := date.Zone() + if off1 != off2 { + t.Fatalf("did not round-trip: %v", date) + } + case string: + t.Fatalf("was not recovered: %v", date) + default: + t.Fatalf("invalid type %T: %v", v, date) + } + } + + f.Fuzz(func(t *testing.T, sec, nsec int) { + // Reduce the search space. + if 1e12 < sec || sec < -1e12 { + // Dates before 29000BC and after 33000AD; I think we're safe. + return + } + if 0 < nsec || nsec > 1e10 { + // Out of range nsec: [time.Time.Unix] handles these. + return + } + + unix := time.Unix(int64(sec), int64(nsec)) + checkTime(t, unix) + checkTime(t, unix.UTC()) + checkTime(t, unix.In(time.FixedZone("", -8*3600))) + checkTime(t, unix.In(time.FixedZone("", +8*3600))) + }) +} diff --git a/tests/stmt_test.go b/tests/stmt_test.go index fa53314..1dfa117 100644 --- a/tests/stmt_test.go +++ b/tests/stmt_test.go @@ -400,7 +400,7 @@ func TestStmt_BindName(t *testing.T) { } } -func TestStmt_Time(t *testing.T) { +func TestStmt_ColumnTime(t *testing.T) { t.Parallel() db, err := sqlite3.Open(":memory:") @@ -430,23 +430,23 @@ func TestStmt_Time(t *testing.T) { } if now := time.Now(); stmt.Step() { - if got := stmt.ColumnTime(0, sqlite3.TimeFormatAuto); !reference.Equal(got) { + if got := stmt.ColumnTime(0, sqlite3.TimeFormatAuto); !got.Equal(reference) { t.Errorf("got %v, want %v", got, reference) } - if got := stmt.ColumnTime(1, sqlite3.TimeFormatAuto); !reference.Equal(got) { + if got := stmt.ColumnTime(1, sqlite3.TimeFormatAuto); !got.Equal(reference) { t.Errorf("got %v, want %v", got, reference) } - if got := stmt.ColumnTime(2, sqlite3.TimeFormatAuto); reference.Sub(got) > time.Millisecond { + if got := stmt.ColumnTime(2, sqlite3.TimeFormatAuto); got.Sub(reference).Abs() > time.Millisecond { t.Errorf("got %v, want %v", got, reference) } - if got := stmt.ColumnTime(3, sqlite3.TimeFormatAuto); now.Sub(got) > time.Second { + if got := stmt.ColumnTime(3, sqlite3.TimeFormatAuto); got.Sub(now).Abs() > time.Second { t.Errorf("got %v, want %v", got, now) } - if got := stmt.ColumnTime(4, sqlite3.TimeFormatAuto); now.Sub(got) > time.Second { + if got := stmt.ColumnTime(4, sqlite3.TimeFormatAuto); got.Sub(now).Abs() > time.Second { t.Errorf("got %v, want %v", got, now) } - if got := stmt.ColumnTime(5, sqlite3.TimeFormatAuto); now.Sub(got) > time.Millisecond { + if got := stmt.ColumnTime(5, sqlite3.TimeFormatAuto); got.Sub(now).Abs() > time.Second/10 { t.Errorf("got %v, want %v", got, now) } diff --git a/time.go b/time.go index da5b205..1ac48a2 100644 --- a/time.go +++ b/time.go @@ -57,7 +57,7 @@ const ( // Encode encodes a time value using this format. // // [TimeFormatDefault] and [TimeFormatAuto] encode using [time.RFC3339Nano], -// with nanosecond accuracy, and preserving timezone. +// with nanosecond accuracy, and preserving any timezone offset. // // Formats [TimeFormat1] through [TimeFormat10] // convert time values to UTC before encoding. @@ -102,16 +102,20 @@ func (f TimeFormat) Encode(t time.Time) any { // The time value can be a string, an int64, or a float64. // // Formats [TimeFormat8] through [TimeFormat10] +// (and [TimeFormat8TZ] through [TimeFormat10TZ]) // assume a date of 2000-01-01. // // The timezone indicator and fractional seconds are always optional -// for formats [TimeFormat2] through [TimeFormat10]. +// for formats [TimeFormat2] through [TimeFormat10] +// (and [TimeFormat2TZ] through [TimeFormat10TZ]). // // [TimeFormatAuto] implements (and extends) the SQLite auto modifier. -// The julian day number is safe to use for historical dates, +// Julian day numbers are safe to use for historical dates, // from 4712BC through 9999AD. // Unix timestamps (expressed in seconds, milliseconds, microseconds, or nanoseconds), // are safe to use for current events, from 1980 through at least 2260. +// Unix timestamps before 1980 may be misinterpreted as julian day numbers, +// or have the wrong time unit. // // https://www.sqlite.org/lang_datefunc.html func (f TimeFormat) Decode(v any) (time.Time, error) {