Selaa lähdekoodia

Optional DATE / DATETIME to time.Time parsing

Closes #9
Julien Schmidt 12 vuotta sitten
vanhempi
commit
716467db08
7 muutettua tiedostoa jossa 313 lisäystä ja 102 poistoa
  1. 11 1
      README.md
  2. 10 1
      connection.go
  3. 4 1
      driver.go
  4. 87 20
      driver_test.go
  5. 55 64
      packets.go
  6. 127 3
      utils.go
  7. 19 12
      utils_test.go

+ 11 - 1
README.md

@@ -19,7 +19,8 @@ A MySQL-Driver for Go's [database/sql](http://golang.org/pkg/database/sql) packa
       * [Address](#address)
       * [Parameters](#parameters)
       * [Examples](#examples)
-    * [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support) 
+    * [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
+    * [time.Time support](#timetime-support)
   * [Testing / Development](#testing--development)
   * [License](#license)
 
@@ -105,6 +106,8 @@ Possible Parameters are:
   * `timeout`: **Driver** side connection timeout. The value must be a string of decimal numbers, each with optional fraction and a unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. To set a server side timeout, use the parameter [`wait_timeout`](http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_wait_timeout).
   * `charset`: Sets the charset used for client-server interaction ("SET NAMES `value`"). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset failes. This enables support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
   * `allowAllFiles`: `allowAllFiles=true` disables the file Whitelist for `LOAD DATA LOCAL INFILE` and allows *all* files. *Might be insecure!*
+  * `parseTime`: `parseTime=true` changes the output type of `DATE` and `DATETIME` values to `time.Time` instead of `[]byte` / `string`
+  * `loc`: Sets the location for time.Time values (when using `parseTime=true`). The default is `UTC`. *"Local"* sets the system's location. See [time.LoadLocation](http://golang.org/pkg/time/#LoadLocation) for details.
 
 All other parameters are interpreted as system variables:
   * `autocommit`: *"SET autocommit=`value`"*
@@ -146,6 +149,13 @@ To use a `io.Reader` a handler function must be registered with `mysql.RegisterR
 
 See also the [godoc of Go-MySQL-Driver](http://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation")
 
+### `time.Time` support
+The default internal output type of MySQL `DATE` and `DATETIME` values is `[]byte` which allows you to scan the value into a `[]byte`, `string` or `sql.RawBytes` variable in your programm.
+
+However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` variables, which is the logical opposite in Go to `DATE` and `DATETIME` in MySQL. You can do that by changing the internal output type from `[]byte` to `time.Time` with the DSN parameter `parseTime=true`. You can set the default [`time.Time` location](http://golang.org/pkg/time/#Location) with the `loc` DSN parameter.
+**Caution:** As of Go 1.1, this makes `time.Time` the only variable type you can scan `DATE` and `DATETIME` values into. This breaks for example [`sql.RawBytes` support](https://github.com/go-sql-driver/mysql/wiki/Examples#rawbytes).
+
+
 
 ## Testing / Development
 To run the driver tests you may need to adjust the configuration. See [this Wiki-Page](https://github.com/go-sql-driver/mysql/wiki/Testing "Testing") for details.

+ 10 - 1
connection.go

@@ -14,6 +14,7 @@ import (
 	"errors"
 	"net"
 	"strings"
+	"time"
 )
 
 type mysqlConn struct {
@@ -29,6 +30,7 @@ type mysqlConn struct {
 	insertId         uint64
 	maxPacketAllowed int
 	maxWriteSize     int
+	parseTime        bool
 }
 
 type config struct {
@@ -38,6 +40,7 @@ type config struct {
 	addr   string
 	dbname string
 	params map[string]string
+	loc    *time.Location
 }
 
 // Handles parameters set in DSN
@@ -59,9 +62,15 @@ func (mc *mysqlConn) handleParams() (err error) {
 			}
 
 		// handled elsewhere
-		case "timeout", "allowAllFiles":
+		case "timeout", "allowAllFiles", "loc":
 			continue
 
+		// time.Time parsing
+		case "parseTime":
+			if val == "true" {
+				mc.parseTime = true
+			}
+
 		// TLS-Encryption
 		case "tls":
 			err = errors.New("TLS-Encryption not implemented yet")

+ 4 - 1
driver.go

@@ -25,10 +25,13 @@ func (d *mysqlDriver) Open(dsn string) (driver.Conn, error) {
 
 	// New mysqlConn
 	mc := &mysqlConn{
-		cfg:              parseDSN(dsn),
 		maxPacketAllowed: maxPacketSize,
 		maxWriteSize:     maxPacketSize - 1,
 	}
+	mc.cfg, err = parseDSN(dsn)
+	if err != nil {
+		return nil, err
+	}
 
 	// Connect to Server
 	if _, ok := mc.cfg.params["timeout"]; ok { // with timeout

+ 87 - 20
driver_test.go

@@ -10,6 +10,7 @@ import (
 	"strings"
 	"sync"
 	"testing"
+	"time"
 )
 
 var (
@@ -408,36 +409,102 @@ func TestDateTime(t *testing.T) {
 		return
 	}
 
-	db, err := sql.Open("mysql", dsn)
+	var modes = [2]string{"text", "binary"}
+	var types = [2]string{"DATE", "DATETIME"}
+	var tests = [2][]struct {
+		in      interface{}
+		sOut    string
+		tOut    time.Time
+		tIsZero bool
+	}{
+		{
+			{"2012-06-14", "2012-06-14", time.Date(2012, 6, 14, 0, 0, 0, 0, time.UTC), false},
+			{"0000-00-00", "0000-00-00", time.Time{}, true},
+			{time.Date(2012, 6, 14, 0, 0, 0, 0, time.UTC), "2012-06-14", time.Date(2012, 6, 14, 0, 0, 0, 0, time.UTC), false},
+			{time.Time{}, "0000-00-00", time.Time{}, true},
+		},
+		{
+			{"2011-11-20 21:27:37", "2011-11-20 21:27:37", time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC), false},
+			{"0000-00-00 00:00:00", "0000-00-00 00:00:00", time.Time{}, true},
+			{time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC), "2011-11-20 21:27:37", time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC), false},
+			{time.Time{}, "0000-00-00 00:00:00", time.Time{}, true},
+		},
+	}
+	var sOut string
+	var tOut time.Time
+
+	var rows [2]*sql.Rows
+	var sDB, tDB *sql.DB
+	var err error
+
+	sDB, err = sql.Open("mysql", dsn)
 	if err != nil {
-		t.Fatalf("Error connecting: %v", err)
+		t.Fatalf("Error connecting (string): %v", err)
 	}
+	defer sDB.Close()
 
-	defer db.Close()
-
-	mustExec(t, db, "DROP TABLE IF EXISTS test")
-
-	types := [...]string{"DATE", "DATETIME"}
-	in := [...]string{"2012-06-14", "2011-11-20 21:27:37"}
-	var out string
-	var rows *sql.Rows
+	tDB, err = sql.Open("mysql", dsn+"&parseTime=true")
+	if err != nil {
+		t.Fatalf("Error connecting (time.Time): %v", err)
+	}
+	defer tDB.Close()
 
+	mustExec(t, sDB, "DROP TABLE IF EXISTS test")
 	for i, v := range types {
-		mustExec(t, db, "CREATE TABLE test (value "+v+") CHARACTER SET utf8 COLLATE utf8_unicode_ci")
+		mustExec(t, sDB, "CREATE TABLE test (value "+v+") CHARACTER SET utf8 COLLATE utf8_unicode_ci")
+
+		for j := range tests[i] {
+			mustExec(t, sDB, "INSERT INTO test VALUES (?)", tests[i][j].in)
+
+			// string
+			rows[0] = mustQuery(t, sDB, "SELECT value FROM test")                // text
+			rows[1] = mustQuery(t, sDB, "SELECT value FROM test WHERE 1 = ?", 1) // binary
+
+			for k := range rows {
+				if rows[k].Next() {
+					err = rows[k].Scan(&sOut)
+					if err != nil {
+						t.Errorf("%s (string %s): %v", v, modes[k], err)
+					} else if tests[i][j].sOut != sOut {
+						t.Errorf("%s (string %s): %s != %s", v, modes[k], tests[i][j].sOut, sOut)
+					}
+				} else {
+					err = rows[k].Err()
+					if err != nil {
+						t.Errorf("%s (string %s): %v", v, modes[k], err)
+					} else {
+						t.Errorf("%s (string %s): no data", v, modes[k])
+					}
+				}
+			}
 
-		mustExec(t, db, ("INSERT INTO test VALUES (?)"), in[i])
+			// time.Time
+			rows[0] = mustQuery(t, tDB, "SELECT value FROM test")                // text
+			rows[1] = mustQuery(t, tDB, "SELECT value FROM test WHERE 1 = ?", 1) // binary
+
+			for k := range rows {
+				if rows[k].Next() {
+					err = rows[k].Scan(&tOut)
+					if err != nil {
+						t.Errorf("%s (time.Time %s): %v", v, modes[k], err)
+					} else if tests[i][j].tOut != tOut || tests[i][j].tIsZero != tOut.IsZero() {
+						t.Errorf("%s (time.Time %s): %s [%t] != %s [%t]", v, modes[k], tests[i][j].tOut, tests[i][j].tIsZero, tOut, tOut.IsZero())
+					}
+				} else {
+					err = rows[k].Err()
+					if err != nil {
+						t.Errorf("%s (time.Time %s): %v", v, modes[k], err)
+					} else {
+						t.Errorf("%s (time.Time %s): no data", v, modes[k])
+					}
 
-		rows = mustQuery(t, db, ("SELECT value FROM test"))
-		if rows.Next() {
-			rows.Scan(&out)
-			if in[i] != out {
-				t.Errorf("%s: %s != %s", v, in[i], out)
+				}
 			}
-		} else {
-			t.Errorf("%s: no data", v)
+
+			mustExec(t, sDB, "TRUNCATE TABLE test")
 		}
 
-		mustExec(t, db, "DROP TABLE IF EXISTS test")
+		mustExec(t, sDB, "DROP TABLE IF EXISTS test")
 	}
 }
 

+ 55 - 64
packets.go

@@ -551,7 +551,21 @@ func (rows *mysqlRows) readRow(dest []driver.Value) (err error) {
 		pos += n
 		if err == nil {
 			if !isNull {
-				continue
+				if !rows.mc.parseTime {
+					continue
+				} else {
+					switch rows.columns[i].fieldType {
+					case fieldTypeTimestamp, fieldTypeDateTime,
+						fieldTypeDate, fieldTypeNewDate:
+						dest[i], err = parseDateTime(string(dest[i].([]byte)), rows.mc.cfg.loc)
+						if err == nil {
+							continue
+						}
+					default:
+						continue
+					}
+				}
+
 			} else {
 				dest[i] = nil
 				continue
@@ -751,7 +765,14 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
 
 		case time.Time:
 			paramTypes[i<<1] = fieldTypeString
-			val := []byte(v.Format(timeFormat))
+
+			var val []byte
+			if v.IsZero() {
+				val = []byte("0000-00-00")
+			} else {
+				val = []byte(v.Format(timeFormat))
+			}
+
 			paramValues[i] = append(
 				lengthEncodedIntegerToBytes(uint64(len(val))),
 				val...,
@@ -815,8 +836,8 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
 }
 
 // http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::ResultsetRow
-func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
-	data, err := rc.mc.readPacket()
+func (rows *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
+	data, err := rows.mc.readPacket()
 	if err != nil {
 		return
 	}
@@ -828,7 +849,7 @@ func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
 			return io.EOF
 		} else {
 			// Error otherwise
-			return rc.mc.handleErrorPacket(data)
+			return rows.mc.handleErrorPacket(data)
 		}
 	}
 
@@ -848,10 +869,10 @@ func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
 			continue
 		}
 
-		unsigned = rc.columns[i].flags&flagUnsigned != 0
+		unsigned = rows.columns[i].flags&flagUnsigned != 0
 
 		// Convert to byte-coded string
-		switch rc.columns[i].fieldType {
+		switch rows.columns[i].fieldType {
 		case fieldTypeNULL:
 			dest[i] = nil
 			continue
@@ -934,21 +955,22 @@ func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
 
 			pos += n
 
-			if num == 0 {
-				if isNull {
-					dest[i] = nil
-					continue
-				} else {
-					dest[i] = []byte("0000-00-00")
-					continue
-				}
+			if isNull {
+				dest[i] = nil
+				continue
+			}
+
+			if rows.mc.parseTime {
+				dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc)
 			} else {
-				dest[i] = []byte(fmt.Sprintf("%04d-%02d-%02d",
-					binary.LittleEndian.Uint16(data[pos:pos+2]),
-					data[pos+2],
-					data[pos+3]))
+				dest[i], err = formatBinaryDate(num, data[pos:])
+			}
+
+			if err == nil {
 				pos += int(num)
 				continue
+			} else {
+				return err
 			}
 
 		// Time [-][H]HH:MM:SS[.fractal]
@@ -1008,58 +1030,27 @@ func (rc *mysqlRows) readBinaryRow(dest []driver.Value) (err error) {
 
 			pos += n
 
-			if num == 0 {
-				if isNull {
-					dest[i] = nil
-					continue
-				} else {
-					dest[i] = []byte("0000-00-00 00:00:00")
-					continue
-				}
+			if isNull {
+				dest[i] = nil
+				continue
 			}
 
-			switch num {
-			case 4:
-				dest[i] = []byte(fmt.Sprintf(
-					"%04d-%02d-%02d 00:00:00",
-					binary.LittleEndian.Uint16(data[pos:pos+2]),
-					data[pos+2],
-					data[pos+3],
-				))
-				pos += 4
-				continue
-			case 7:
-				dest[i] = []byte(fmt.Sprintf(
-					"%04d-%02d-%02d %02d:%02d:%02d",
-					binary.LittleEndian.Uint16(data[pos:pos+2]),
-					data[pos+2],
-					data[pos+3],
-					data[pos+4],
-					data[pos+5],
-					data[pos+6],
-				))
-				pos += 7
-				continue
-			case 11:
-				dest[i] = []byte(fmt.Sprintf(
-					"%04d-%02d-%02d %02d:%02d:%02d.%06d",
-					binary.LittleEndian.Uint16(data[pos:pos+2]),
-					data[pos+2],
-					data[pos+3],
-					data[pos+4],
-					data[pos+5],
-					data[pos+6],
-					binary.LittleEndian.Uint32(data[pos+7:pos+11]),
-				))
-				pos += 11
+			if rows.mc.parseTime {
+				dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc)
+			} else {
+				dest[i], err = formatBinaryDateTime(num, data[pos:])
+			}
+
+			if err == nil {
+				pos += int(num)
 				continue
-			default:
-				return fmt.Errorf("Invalid DATETIME-packet length %d", num)
+			} else {
+				return err
 			}
 
 		// Please report if this happens!
 		default:
-			return fmt.Errorf("Unknown FieldType %d", rc.columns[i].fieldType)
+			return fmt.Errorf("Unknown FieldType %d", rows.columns[i].fieldType)
 		}
 	}
 

+ 127 - 3
utils.go

@@ -11,11 +11,15 @@ package mysql
 
 import (
 	"crypto/sha1"
+	"database/sql/driver"
+	"encoding/binary"
+	"fmt"
 	"io"
 	"log"
 	"os"
 	"regexp"
 	"strings"
+	"time"
 )
 
 // Logger
@@ -36,8 +40,8 @@ func init() {
 // Data Source Name Parser
 var dsnPattern *regexp.Regexp
 
-func parseDSN(dsn string) *config {
-	cfg := new(config)
+func parseDSN(dsn string) (cfg *config, err error) {
+	cfg = new(config)
 	cfg.params = make(map[string]string)
 
 	matches := dsnPattern.FindStringSubmatch(dsn)
@@ -76,7 +80,9 @@ func parseDSN(dsn string) *config {
 		cfg.addr = "127.0.0.1:3306"
 	}
 
-	return cfg
+	cfg.loc, err = time.LoadLocation(cfg.params["loc"])
+
+	return
 }
 
 // Encrypt password using 4.1+ method
@@ -110,6 +116,124 @@ func scramblePassword(scramble, password []byte) []byte {
 	return scramble
 }
 
+func parseDateTime(str string, loc *time.Location) (driver.Value, error) {
+	var t time.Time
+	var err error
+
+	switch len(str) {
+	case 10: // YYYY-MM-DD
+		if str == "0000-00-00" {
+			return time.Time{}, nil
+		}
+		t, err = time.Parse(timeFormat[:10], str)
+	case 19: // YYYY-MM-DD HH:MM:SS
+		if str == "0000-00-00 00:00:00" {
+			return time.Time{}, nil
+		}
+		t, err = time.Parse(timeFormat, str)
+	default:
+		return nil, fmt.Errorf("Invalid Time-String: %s", str)
+	}
+
+	// Adjust location
+	if err == nil && loc != time.UTC {
+		y, mo, d := t.Date()
+		h, mi, s := t.Clock()
+		return time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
+	}
+
+	return t, err
+}
+
+func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Value, error) {
+	switch num {
+	case 0:
+		return time.Time{}, nil
+	case 4:
+		return time.Date(
+			int(binary.LittleEndian.Uint16(data[:2])), // year
+			time.Month(data[2]),                       // month
+			int(data[3]),                              // day
+			0, 0, 0, 0,
+			loc,
+		), nil
+	case 7:
+		return time.Date(
+			int(binary.LittleEndian.Uint16(data[:2])), // year
+			time.Month(data[2]),                       // month
+			int(data[3]),                              // day
+			int(data[4]),                              // hour
+			int(data[5]),                              // minutes
+			int(data[6]),                              // seconds
+			0,
+			loc,
+		), nil
+	case 11:
+		return time.Date(
+			int(binary.LittleEndian.Uint16(data[:2])), // year
+			time.Month(data[2]),                       // month
+			int(data[3]),                              // day
+			int(data[4]),                              // hour
+			int(data[5]),                              // minutes
+			int(data[6]),                              // seconds
+			int(binary.LittleEndian.Uint32(data[7:11]))*1000, // nanoseconds
+			loc,
+		), nil
+	}
+	return nil, fmt.Errorf("Invalid DATETIME-packet length %d", num)
+}
+
+func formatBinaryDate(num uint64, data []byte) (driver.Value, error) {
+	switch num {
+	case 0:
+		return []byte("0000-00-00"), nil
+	case 4:
+		return []byte(fmt.Sprintf(
+			"%04d-%02d-%02d",
+			binary.LittleEndian.Uint16(data[:2]),
+			data[2],
+			data[3],
+		)), nil
+	}
+	return nil, fmt.Errorf("Invalid DATE-packet length %d", num)
+}
+
+func formatBinaryDateTime(num uint64, data []byte) (driver.Value, error) {
+	switch num {
+	case 0:
+		return []byte("0000-00-00 00:00:00"), nil
+	case 4:
+		return []byte(fmt.Sprintf(
+			"%04d-%02d-%02d 00:00:00",
+			binary.LittleEndian.Uint16(data[:2]),
+			data[2],
+			data[3],
+		)), nil
+	case 7:
+		return []byte(fmt.Sprintf(
+			"%04d-%02d-%02d %02d:%02d:%02d",
+			binary.LittleEndian.Uint16(data[:2]),
+			data[2],
+			data[3],
+			data[4],
+			data[5],
+			data[6],
+		)), nil
+	case 11:
+		return []byte(fmt.Sprintf(
+			"%04d-%02d-%02d %02d:%02d:%02d.%06d",
+			binary.LittleEndian.Uint16(data[:2]),
+			data[2],
+			data[3],
+			data[4],
+			data[5],
+			data[6],
+			binary.LittleEndian.Uint32(data[7:11]),
+		)), nil
+	}
+	return nil, fmt.Errorf("Invalid DATETIME-packet length %d", num)
+}
+
 /******************************************************************************
 *                       Convert from and to bytes                             *
 ******************************************************************************/

+ 19 - 12
utils_test.go

@@ -12,32 +12,39 @@ package mysql
 import (
 	"fmt"
 	"testing"
+	"time"
 )
 
 var testDSNs = []struct {
 	in  string
 	out string
+	loc *time.Location
 }{
-	{"username:password@protocol(address)/dbname?param=value", "&{user:username passwd:password net:protocol addr:address dbname:dbname params:map[param:value]}"},
-	{"user@unix(/path/to/socket)/dbname?charset=utf8", "&{user:user passwd: net:unix addr:/path/to/socket dbname:dbname params:map[charset:utf8]}"},
-	{"user:password@tcp(localhost:5555)/dbname?charset=utf8", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8]}"},
-	{"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8mb4,utf8]}"},
-	{"user:password@/dbname", "&{user:user passwd:password net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[]}"},
-	{"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname", "&{user:user passwd:p@ss(word) net:tcp addr:[de:ad:be:ef::ca:fe]:80 dbname:dbname params:map[]}"},
-	{"/dbname", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[]}"},
-	{"/", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname: params:map[]}"},
-	{"user:p@/ssword@/", "&{user:user passwd:p@/ssword net:tcp addr:127.0.0.1:3306 dbname: params:map[]}"},
+	{"username:password@protocol(address)/dbname?param=value", "&{user:username passwd:password net:protocol addr:address dbname:dbname params:map[param:value] loc:%p}", time.UTC},
+	{"user@unix(/path/to/socket)/dbname?charset=utf8", "&{user:user passwd: net:unix addr:/path/to/socket dbname:dbname params:map[charset:utf8] loc:%p}", time.UTC},
+	{"user:password@tcp(localhost:5555)/dbname?charset=utf8", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8] loc:%p}", time.UTC},
+	{"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8", "&{user:user passwd:password net:tcp addr:localhost:5555 dbname:dbname params:map[charset:utf8mb4,utf8] loc:%p}", time.UTC},
+	{"user:password@/dbname?loc=UTC", "&{user:user passwd:password net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[loc:UTC] loc:%p}", time.UTC},
+	{"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "&{user:user passwd:p@ss(word) net:tcp addr:[de:ad:be:ef::ca:fe]:80 dbname:dbname params:map[loc:Local] loc:%p}", time.Local},
+	{"/dbname", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname:dbname params:map[] loc:%p}", time.UTC},
+	{"/", "&{user: passwd: net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p}", time.UTC},
+	{"user:p@/ssword@/", "&{user:user passwd:p@/ssword net:tcp addr:127.0.0.1:3306 dbname: params:map[] loc:%p}", time.UTC},
 }
 
 func TestDSNParser(t *testing.T) {
 	var cfg *config
+	var err error
 	var res string
 
 	for i, tst := range testDSNs {
-		cfg = parseDSN(tst.in)
+		cfg, err = parseDSN(tst.in)
+		if err != nil {
+			t.Error(err.Error())
+		}
+
 		res = fmt.Sprintf("%+v", cfg)
-		if res != tst.out {
-			t.Errorf("%d. parseDSN(%q) => %q, want %q", i, tst.in, res, tst.out)
+		if res != fmt.Sprintf(tst.out, tst.loc) {
+			t.Errorf("%d. parseDSN(%q) => %q, want %q", i, tst.in, res, fmt.Sprintf(tst.out, tst.loc))
 		}
 	}
 }