diff --git a/all_test.go b/all_test.go index 9c869bb..c14363a 100644 --- a/all_test.go +++ b/all_test.go @@ -556,6 +556,10 @@ func TestConcurrentGoroutines(t *testing.T) { } func TestConcurrentProcesses(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + dir, err := ioutil.TempDir("", "sqlite-test-") if err != nil { t.Fatal(err) @@ -1166,6 +1170,7 @@ func TestTimeScan(t *testing.T) { {s: "2021-01-02 16:39:17+00:00", w: ref.Truncate(time.Second)}, {s: "2021-01-02T16:39:17.123456+00:00", w: ref.Truncate(time.Microsecond)}, {s: "2021-01-02 16:39:17.123456+00:00", w: ref.Truncate(time.Microsecond)}, + {s: "2021-01-02 16:39:17.123456Z", w: ref.Truncate(time.Microsecond)}, {s: "2021-01-02 12:39:17-04:00", w: ref.Truncate(time.Second)}, {s: "2021-01-02 16:39:17", w: ref.Truncate(time.Second)}, {s: "2021-01-02T16:39:17", w: ref.Truncate(time.Second)}, @@ -1190,7 +1195,6 @@ func TestTimeScan(t *testing.T) { } var got time.Time - if err := db.QueryRow("select y from x").Scan(&got); err != nil { t.Fatal(err) } @@ -1214,6 +1218,69 @@ func TestTimeLocaltime(t *testing.T) { } } +func TestTimeFormat(t *testing.T) { + ref := time.Date(2021, 1, 2, 16, 39, 17, 123456789, time.UTC) + + cases := []struct { + f string + w string + }{ + {f: "", w: "2021-01-02 16:39:17.123456789 +0000 UTC"}, + {f: "sqlite", w: "2021-01-02 16:39:17.123456789+00:00"}, + } + for _, c := range cases { + t.Run("", func(t *testing.T) { + dsn := "file::memory:" + if c.f != "" { + q := make(url.Values) + q.Set("_time_format", c.f) + dsn += "?" + q.Encode() + } + db, err := sql.Open(driverName, dsn) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + if _, err := db.Exec("drop table if exists x; create table x (y text)"); err != nil { + t.Fatal(err) + } + + if _, err := db.Exec(`insert into x values (?)`, ref); err != nil { + t.Fatal(err) + } + + var got string + if err := db.QueryRow(`select y from x`).Scan(&got); err != nil { + t.Fatal(err) + } + + if got != c.w { + t.Fatal(got, c.w) + } + }) + } +} + +func TestTimeFormatBad(t *testing.T) { + db, err := sql.Open(driverName, "file::memory:?_time_format=bogus") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + // Error doesn't appear until a connection is opened. + _, err = db.Exec("select 1") + if err == nil { + t.Fatal("wanted error") + } + + want := `unknown _time_format "bogus"` + if got := err.Error(); got != want { + t.Fatalf("got error %q, want %q", got, want) + } +} + // https://sqlite.org/lang_expr.html#varparam // https://gitlab.com/cznic/sqlite/-/issues/42 func TestBinding(t *testing.T) { @@ -1402,6 +1469,10 @@ func testBindingError(t *testing.T, query func(db *sql.DB, query string, args .. // https://gitlab.com/cznic/sqlite/-/issues/51 func TestIssue51(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + tempDir, err := ioutil.TempDir("", "") if err != nil { t.Fatal(err) diff --git a/sqlite.go b/sqlite.go index 5383d12..e425039 100644 --- a/sqlite.go +++ b/sqlite.go @@ -336,6 +336,22 @@ func (c *conn) parseTimeString(s0 string, x int) (interface{}, bool) { return s0, false } +// writeTimeFormats are the names and formats supported +// by the `_time_format` DSN query param. +var writeTimeFormats = map[string]string{ + "sqlite": parseTimeFormats[0], +} + +func (c *conn) formatTime(t time.Time) string { + // Before configurable write time formats were supported, + // time.Time.String was used. Maintain that default to + // keep existing driver users formatting times the same. + if c.writeTimeFormat == "" { + return t.String() + } + return t.Format(c.writeTimeFormat) +} + // RowsColumnTypeDatabaseTypeName may be implemented by Rows. It should return // the database system type name without the length. Type names should be // uppercase. Examples of returned types: "VARCHAR", "NVARCHAR", "VARCHAR2", @@ -726,6 +742,8 @@ type conn struct { // Context handling can cause conn.Close and conn.interrupt to be invoked // concurrently. sync.Mutex + + writeTimeFormat string } func newConn(dsn string) (*conn, error) { @@ -759,7 +777,7 @@ func newConn(dsn string) (*conn, error) { return nil, err } - if err = applyPragmas(c, query); err != nil { + if err = applyQueryParams(c, query); err != nil { c.Close() return nil, err } @@ -767,11 +785,12 @@ func newConn(dsn string) (*conn, error) { return c, nil } -func applyPragmas(c *conn, query string) error { +func applyQueryParams(c *conn, query string) error { q, err := url.ParseQuery(query) if err != nil { return err } + for _, v := range q["_pragma"] { cmd := "pragma " + v _, err := c.exec(context.Background(), cmd, nil) @@ -779,6 +798,16 @@ func applyPragmas(c *conn, query string) error { return err } } + + if v := q.Get("_time_format"); v != "" { + f, ok := writeTimeFormats[v] + if !ok { + return fmt.Errorf("unknown _time_format %q", v) + } + c.writeTimeFormat = f + return nil + } + return nil } @@ -1005,7 +1034,7 @@ func (c *conn) bind(pstmt uintptr, n int, args []driver.NamedValue) (allocs []ui return allocs, err } case time.Time: - if p, err = c.bindText(pstmt, i, x.String()); err != nil { + if p, err = c.bindText(pstmt, i, c.formatTime(x)); err != nil { return allocs, err } case nil: diff --git a/tcl_test.go b/tcl_test.go index 0b23ed4..600ef8e 100644 --- a/tcl_test.go +++ b/tcl_test.go @@ -9,13 +9,14 @@ import ( "flag" "fmt" "io/ioutil" - "modernc.org/tcl" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" + + "modernc.org/tcl" ) var ( @@ -26,6 +27,10 @@ var ( ) func TestTclTest(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + blacklist := map[string]struct{}{} switch runtime.GOARCH { case "386", "arm":