driver: support _time_format DSN query param

Building on the _pragma query param support added in 508747c9, support
a new _time_format query param.

If _time_format is set to "sqlite", written times use the time package
format "2006-01-02 15:04:05.999999999-07:00". This is the format
mattn/go-sqlite3 uses and is format 4 at
https://sqlite.org/lang_datefunc.html#time_values.

By default, times are formatted with time.Time.String. This maintains
compatibility with existing users of the driver.

Fixes #47
This commit is contained in:
Dan Peterson
2021-10-23 09:57:47 -03:00
parent a530c91309
commit 064df839a4
3 changed files with 110 additions and 5 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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":