From 1009a046ebc2afa07d647934272b71ec824c9145 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Tue, 3 Jun 2014 19:11:25 +0200 Subject: [PATCH 01/14] support microseconds with MySQL 5.7+ passes all tests except TIME(1) -> string in binary protocol. TIMESTAMP support with microsecond resolution is still incomplete. --- const.go | 1 - driver_test.go | 260 +++++++++++++++++++++++++------------------------ packets.go | 39 +++++--- rows.go | 6 +- utils.go | 43 ++++---- utils_test.go | 18 ++-- 6 files changed, 194 insertions(+), 173 deletions(-) diff --git a/const.go b/const.go index 379eabec1..bf8afa574 100644 --- a/const.go +++ b/const.go @@ -11,7 +11,6 @@ package mysql const ( minProtocolVersion byte = 10 maxPacketSize = 1<<24 - 1 - timeFormat = "2006-01-02 15:04:05" ) // MySQL constants documentation: diff --git a/driver_test.go b/driver_test.go index ef5b371cf..3bbbd9c36 100644 --- a/driver_test.go +++ b/driver_test.go @@ -327,96 +327,139 @@ func TestString(t *testing.T) { }) } -func TestDateTime(t *testing.T) { - type testmode struct { - selectSuffix string - args []interface{} +type timeTests struct { + dbtype string + tlayout string + tests []timeTest +} + +type timeTest struct { + s string + t time.Time +} + +func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, binaryProtocol bool) { + const queryBin = `SELECT CAST(? AS %[2]s)` + const queryTxt = `SELECT CAST("%[1]s" AS %[2]s)` + var rows *sql.Rows + var protocol string + if binaryProtocol { + protocol = "binary" + rows = dbt.mustQuery(fmt.Sprintf(queryBin, t.s, dbtype), t.t) + } else { + protocol = "text" + rows = dbt.mustQuery(fmt.Sprintf(queryTxt, t.s, dbtype)) } - type timetest struct { - in interface{} - sOut string - tOut time.Time - tIsZero bool + defer rows.Close() + var err error + if !rows.Next() { + err = rows.Err() + if err == nil { + err = fmt.Errorf("no data") + } + dbt.Errorf("%s [%s]: %s", + dbtype, protocol, err, + ) + return } - type tester func(dbt *DBTest, rows *sql.Rows, - test *timetest, sqltype, resulttype, mode string) - type setup struct { - vartype string - dsnSuffix string - test tester + var dst interface{} + err = rows.Scan(&dst) + if err != nil { + dbt.Errorf("%s [%s]: %s", + dbtype, protocol, err, + ) + return } - var ( - modes = map[string]*testmode{ - "text": &testmode{}, - "binary": &testmode{" WHERE 1 = ?", []interface{}{1}}, - } - timetests = map[string][]*timetest{ - "DATE": { - {sDate, sDate, tDate, false}, - {sDate0, sDate0, tDate0, true}, - {tDate, sDate, tDate, false}, - {tDate0, sDate0, tDate0, true}, - }, - "DATETIME": { - {sDateTime, sDateTime, tDateTime, false}, - {sDateTime0, sDateTime0, tDate0, true}, - {tDateTime, sDateTime, tDateTime, false}, - {tDate0, sDateTime0, tDate0, true}, - }, - } - setups = []*setup{ - {"string", "&parseTime=false", func( - dbt *DBTest, rows *sql.Rows, test *timetest, sqltype, resulttype, mode string) { - var sOut string - if err := rows.Scan(&sOut); err != nil { - dbt.Errorf("%s (%s %s): %s", sqltype, resulttype, mode, err.Error()) - } else if test.sOut != sOut { - dbt.Errorf("%s (%s %s): %s != %s", sqltype, resulttype, mode, test.sOut, sOut) - } - }}, - {"time.Time", "&parseTime=true", func( - dbt *DBTest, rows *sql.Rows, test *timetest, sqltype, resulttype, mode string) { - var tOut time.Time - if err := rows.Scan(&tOut); err != nil { - dbt.Errorf("%s (%s %s): %s", sqltype, resulttype, mode, err.Error()) - } else if test.tOut != tOut || test.tIsZero != tOut.IsZero() { - dbt.Errorf("%s (%s %s): %s [%t] != %s [%t]", sqltype, resulttype, mode, test.tOut, test.tIsZero, tOut, tOut.IsZero()) - } - }}, - } - ) + switch val := dst.(type) { + case []uint8: + str := string(val) + if str == t.s { + return + } + dbt.Errorf("%s to string [%s]: expected '%s', got '%s'", + dbtype, protocol, + t.s, str, + ) + case time.Time: + if val == t.t { + return + } + dbt.Errorf("%s to string [%s]: expected '%s', got '%s'", + dbtype, protocol, + t.s, val.Format(tlayout), + ) + default: + dbt.Errorf("%s [%s]: unhandled type %T (is '%s')", + dbtype, protocol, val, val, + ) + } +} - var s *setup - testTime := func(dbt *DBTest) { - var rows *sql.Rows - for sqltype, tests := range timetests { - dbt.mustExec("CREATE TABLE test (value " + sqltype + ")") - for _, test := range tests { - for mode, q := range modes { - dbt.mustExec("TRUNCATE test") - dbt.mustExec("INSERT INTO test VALUES (?)", test.in) - rows = dbt.mustQuery("SELECT value FROM test"+q.selectSuffix, q.args...) - if rows.Next() { - s.test(dbt, rows, test, sqltype, s.vartype, mode) - } else { - if err := rows.Err(); err != nil { - dbt.Errorf("%s (%s %s): %s", - sqltype, s.vartype, mode, err.Error()) - } else { - dbt.Errorf("%s (%s %s): no data", - sqltype, s.vartype, mode) - } +func TestDateTime(t *testing.T) { + afterTime0 := func(d string) time.Time { + dur, err := time.ParseDuration(d) + if err != nil { + panic(err) + } + return time.Time{}.Add(dur) + } + // NOTE: MySQL rounds DATETIME(x) up - but that's not included in the tests + format := "2006-01-02 15:04:05.999999" + t0 := time.Time{} + tstr0 := "0000-00-00 00:00:00.000000" + testcases := []timeTests{ + {"DATE", format[:10], []timeTest{ + {t: time.Date(2011, 11, 20, 0, 0, 0, 0, time.UTC)}, + {t: t0, s: tstr0[:10]}, + }}, + {"DATETIME", format[:19], []timeTest{ + {t: time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)}, + {t: t0, s: tstr0[:19]}, + }}, + {"DATETIME(1)", format[:21], []timeTest{ + {t: time.Date(2011, 11, 20, 21, 27, 37, 100000000, time.UTC)}, + {t: t0, s: tstr0[:21]}, + }}, + {"DATETIME(6)", format, []timeTest{ + {t: time.Date(2011, 11, 20, 21, 27, 37, 123456000, time.UTC)}, + {t: t0, s: tstr0}, + }}, + {"TIME", format[11:19], []timeTest{ + {t: afterTime0("12345s")}, + {t: afterTime0("-12345s")}, + {t: t0, s: tstr0[11:19]}, + }}, + {"TIME(1)", format[11:21], []timeTest{ + {t: afterTime0("12345600ms")}, + {t: afterTime0("-12345600ms")}, + {t: t0, s: tstr0[11:21]}, + }}, + {"TIME(6)", format[11:], []timeTest{ + {t: afterTime0("1234567890123000ns")}, + {t: afterTime0("-1234567890123000ns")}, + {t: t0, s: tstr0[11:]}, + }}, + } + dsns := map[string]bool{ + dsn + "&parseTime=true": true, + dsn + "&sql_mode=ALLOW_INVALID_DATES&parseTime=true": true, + dsn + "&parseTime=false": false, + dsn + "&sql_mode=ALLOW_INVALID_DATES&parseTime=false": false, + } + for testdsn, parseTime := range dsns { + var _ = parseTime + runTests(t, testdsn, func(dbt *DBTest) { + for _, setups := range testcases { + for _, setup := range setups.tests { + if setup.s == "" { + // fill time string where Go can reliable produce it + setup.s = setup.t.Format(setups.tlayout) } + setup.run(dbt, setups.dbtype, setups.tlayout, true) + setup.run(dbt, setups.dbtype, setups.tlayout, false) } } - dbt.mustExec("DROP TABLE IF EXISTS test") - } - } - - timeDsn := dsn + "&sql_mode=ALLOW_INVALID_DATES" - for _, v := range setups { - s = v - runTests(t, timeDsn+s.dsnSuffix, testTime) + }) } } @@ -1010,9 +1053,10 @@ func TestTimezoneConversion(t *testing.T) { dbt.mustExec("CREATE TABLE test (ts TIMESTAMP)") // Insert local time into database (should be converted) + utc, _ := time.LoadLocation("UTC") usCentral, _ := time.LoadLocation("US/Central") - now := time.Now().In(usCentral) - dbt.mustExec("INSERT INTO test VALUE (?)", now) + reftime := time.Date(2014, 05, 30, 18, 03, 17, 0, utc).In(usCentral) + dbt.mustExec("INSERT INTO test VALUE (?)", reftime) // Retrieve time from DB rows := dbt.mustQuery("SELECT ts FROM test") @@ -1020,17 +1064,17 @@ func TestTimezoneConversion(t *testing.T) { dbt.Fatal("Didn't get any rows out") } - var nowDB time.Time - err := rows.Scan(&nowDB) + var dbTime time.Time + err := rows.Scan(&dbTime) if err != nil { dbt.Fatal("Err", err) } // Check that dates match - if now.Unix() != nowDB.Unix() { + if reftime.Unix() != dbTime.Unix() { dbt.Errorf("Times don't match.\n") - dbt.Errorf(" Now(%v)=%v\n", usCentral, now) - dbt.Errorf(" Now(UTC)=%v\n", nowDB) + dbt.Errorf(" Now(%v)=%v\n", usCentral, reftime) + dbt.Errorf(" Now(UTC)=%v\n", dbTime) } } @@ -1039,42 +1083,6 @@ func TestTimezoneConversion(t *testing.T) { } } -// This tests for https://github.com/go-sql-driver/mysql/pull/139 -// -// An extra (invisible) nil byte was being added to the beginning of positive -// time strings. -func TestTimeSign(t *testing.T) { - runTests(t, dsn, func(dbt *DBTest) { - var sTimes = []struct { - value string - fieldType string - }{ - {"12:34:56", "TIME"}, - {"-12:34:56", "TIME"}, - // As described in http://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html - // they *should* work, but only in 5.6+. - // { "12:34:56.789", "TIME(3)" }, - // { "-12:34:56.789", "TIME(3)" }, - } - - for _, sTime := range sTimes { - dbt.db.Exec("DROP TABLE IF EXISTS test") - dbt.mustExec("CREATE TABLE test (id INT, time_field " + sTime.fieldType + ")") - dbt.mustExec("INSERT INTO test (id, time_field) VALUES(1, '" + sTime.value + "')") - rows := dbt.mustQuery("SELECT time_field FROM test WHERE id = ?", 1) - if rows.Next() { - var oTime string - rows.Scan(&oTime) - if oTime != sTime.value { - dbt.Errorf(`time values differ: got %q, expected %q.`, oTime, sTime.value) - } - } else { - dbt.Error("expecting at least one row.") - } - } - }) -} - // Special cases func TestRowsClose(t *testing.T) { diff --git a/packets.go b/packets.go index 618098146..72cf0f25f 100644 --- a/packets.go +++ b/packets.go @@ -16,6 +16,7 @@ import ( "fmt" "io" "math" + "strconv" "time" ) @@ -557,20 +558,24 @@ func (mc *mysqlConn) readColumns(count int) ([]mysqlField, error) { return nil, err } - // Filler [1 byte] - // Charset [16 bit uint] - // Length [32 bit uint] - pos += n + 1 + 2 + 4 + // Filler [uint8] + // Charset [charset, collation uint8] + pos += n + 1 + 2 - // Field type [byte] + // Length [uint32] + columns[i].length = binary.LittleEndian.Uint32(data[pos : pos+4]) + pos += 4 + + // Field type [uint8] columns[i].fieldType = data[pos] pos++ - // Flags [16 bit uint] + // Flags [uint16] columns[i].flags = fieldFlag(binary.LittleEndian.Uint16(data[pos : pos+2])) - //pos += 2 + pos += 2 - // Decimals [8 bit uint] + // Decimals [uint8] + columns[i].decimals = data[pos] //pos++ // Default value [len coded binary] @@ -950,6 +955,7 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { // http://dev.mysql.com/doc/internals/en/binary-protocol-resultset-row.html func (rows *binaryRows) readRow(dest []driver.Value) error { + timestr := "00:00:00.000000" data, err := rows.mc.readPacket() if err != nil { return err @@ -1068,7 +1074,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { if rows.mc.parseTime { dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc) } else { - dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], false) + dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], 10) } if err == nil { @@ -1088,7 +1094,11 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { dest[i] = nil continue } else { - dest[i] = []byte("00:00:00") + length := uint8(8) + if rows.columns[i].decimals > 0 { + length += 1 + uint8(rows.columns[i].decimals) + } + dest[i] = []byte(timestr[:length]) continue } } @@ -1109,8 +1119,9 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { pos += 8 continue case 12: + decimals := strconv.FormatInt(int64(rows.columns[i].decimals), 10) dest[i] = []byte(fmt.Sprintf( - sign+"%02d:%02d:%02d.%06d", + sign+"%02d:%02d:%02d.%0"+decimals+"d", uint16(data[pos+1])*24+uint16(data[pos+5]), data[pos+6], data[pos+7], @@ -1136,7 +1147,11 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { if rows.mc.parseTime { dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc) } else { - dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], true) + length := uint8(19) + if rows.columns[i].decimals > 0 { + length += 1 + uint8(rows.columns[i].decimals) + } + dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], length) } if err == nil { diff --git a/rows.go b/rows.go index df4ef06cb..d76b389c4 100644 --- a/rows.go +++ b/rows.go @@ -14,9 +14,11 @@ import ( ) type mysqlField struct { - fieldType byte - flags fieldFlag name string + length uint32 // length as string: DATETIME(4) => 24 + flags fieldFlag + fieldType byte + decimals byte // numeric precision: DATETIME(4) => 4, also for DECIMAL etc. } type mysqlRows struct { diff --git a/utils.go b/utils.go index b6f200389..5397908ba 100644 --- a/utils.go +++ b/utils.go @@ -29,6 +29,9 @@ var ( errInvalidDSNNoSlash = errors.New("Invalid DSN: Missing the slash separating the database name") ) +// timeFormat must not be changed +var timeFormat = "2006-01-02 15:04:05.999999" + func init() { tlsConfigRegister = make(map[string]*tls.Config) } @@ -451,17 +454,13 @@ func (nt NullTime) Value() (driver.Value, error) { } func parseDateTime(str string, loc *time.Location) (t time.Time, err error) { + base := "0000-00-00 00:00:00.0000000" switch len(str) { - case 10: // YYYY-MM-DD - if str == "0000-00-00" { - return - } - t, err = time.Parse(timeFormat[:10], str) - case 19: // YYYY-MM-DD HH:MM:SS - if str == "0000-00-00 00:00:00" { + case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM" + if str == base[:len(str)] { return } - t, err = time.Parse(timeFormat, str) + t, err = time.Parse(timeFormat[:len(str)], str) default: err = fmt.Errorf("Invalid Time-String: %s", str) return @@ -519,24 +518,22 @@ func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Va // if the DATE or DATETIME has the zero value. // It must never be changed. // The current behavior depends on database/sql copying the result. -var zeroDateTime = []byte("0000-00-00 00:00:00") +var zeroDateTime = []byte("0000-00-00 00:00:00.000000") -func formatBinaryDateTime(src []byte, withTime bool) (driver.Value, error) { +func formatBinaryDateTime(src []byte, length uint8) (driver.Value, error) { if len(src) == 0 { - if withTime { - return zeroDateTime, nil - } - return zeroDateTime[:10], nil + return zeroDateTime[:length], nil } var dst []byte - if withTime { - if len(src) == 11 { - dst = []byte("0000-00-00 00:00:00.000000") - } else { - dst = []byte("0000-00-00 00:00:00") - } - } else { + switch length { + case 10: dst = []byte("0000-00-00") + case 19: + dst = []byte("0000-00-00 00:00:00") + case 21, 22, 23, 24, 25, 26: + dst = []byte("0000-00-00 00:00:00.000000") + default: + return nil, fmt.Errorf("illegal datetime length %d", length) } switch len(src) { case 11: @@ -584,10 +581,10 @@ func formatBinaryDateTime(src []byte, withTime bool) (driver.Value, error) { tmp16, year = tmp16/10, tmp16 dst[1] += byte(year - 10*tmp16) dst[0] += byte(tmp16) - return dst, nil + return dst[:length], nil } var t string - if withTime { + if length >= 19 { t = "DATETIME" } else { t = "DATE" diff --git a/utils_test.go b/utils_test.go index 301d81a62..891f7c039 100644 --- a/utils_test.go +++ b/utils_test.go @@ -191,22 +191,22 @@ func TestFormatBinaryDateTime(t *testing.T) { rawDate[5] = 46 // minutes rawDate[6] = 23 // seconds binary.LittleEndian.PutUint32(rawDate[7:], 987654) // microseconds - expect := func(expected string, length int, withTime bool) { - actual, _ := formatBinaryDateTime(rawDate[:length], withTime) + expect := func(expected string, inlen, outlen uint8) { + actual, _ := formatBinaryDateTime(rawDate[:inlen], outlen) bytes, ok := actual.([]byte) if !ok { t.Errorf("formatBinaryDateTime must return []byte, was %T", actual) } if string(bytes) != expected { t.Errorf( - "expected %q, got %q for length %d, withTime %v", - bytes, actual, length, withTime, + "expected %q, got %q for length in %d, out %d", + bytes, actual, inlen, outlen, ) } } - expect("0000-00-00", 0, false) - expect("0000-00-00 00:00:00", 0, true) - expect("1978-12-30", 4, false) - expect("1978-12-30 15:46:23", 7, true) - expect("1978-12-30 15:46:23.987654", 11, true) + expect("0000-00-00", 0, 10) + expect("0000-00-00 00:00:00", 0, 19) + expect("1978-12-30", 4, 10) + expect("1978-12-30 15:46:23", 7, 19) + expect("1978-12-30 15:46:23.987654", 11, 26) } From db9178761fbd30eb58320e205618d41bb4830fef Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Tue, 3 Jun 2014 23:47:00 +0200 Subject: [PATCH 02/14] all tests passing --- packets.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packets.go b/packets.go index 72cf0f25f..1d5ff85a1 100644 --- a/packets.go +++ b/packets.go @@ -16,7 +16,6 @@ import ( "fmt" "io" "math" - "strconv" "time" ) @@ -1103,35 +1102,36 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { } } - var sign string + var result string if data[pos] == 1 { - sign = "-" + result = "-" } - + var microsecs uint32 switch num { case 8: - dest[i] = []byte(fmt.Sprintf( - sign+"%02d:%02d:%02d", + result += fmt.Sprintf( + "%02d:%02d:%02d", uint16(data[pos+1])*24+uint16(data[pos+5]), data[pos+6], data[pos+7], - )) + ) pos += 8 - continue case 12: - decimals := strconv.FormatInt(int64(rows.columns[i].decimals), 10) - dest[i] = []byte(fmt.Sprintf( - sign+"%02d:%02d:%02d.%0"+decimals+"d", + result += fmt.Sprintf( + "%02d:%02d:%02d", uint16(data[pos+1])*24+uint16(data[pos+5]), data[pos+6], data[pos+7], - binary.LittleEndian.Uint32(data[pos+8:pos+12]), - )) + ) + microsecs = binary.LittleEndian.Uint32(data[pos+8 : pos+12]) pos += 12 - continue default: return fmt.Errorf("Invalid TIME-packet length %d", num) } + if decimals := rows.columns[i].decimals; decimals > 0 { + result += fmt.Sprintf(".%06d", microsecs)[:1+decimals] + } + dest[i] = []byte(result) // Timestamp YYYY-MM-DD HH:MM:SS[.fractal] case fieldTypeTimestamp, fieldTypeDateTime: From f3e6a605b4672ca8972f595b41928355b2db92b8 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Wed, 4 Jun 2014 07:46:33 +0200 Subject: [PATCH 03/14] PR done, all tests succeed and problematic ones are auto-skipped --- driver_test.go | 70 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/driver_test.go b/driver_test.go index 3bbbd9c36..fe7eb8d73 100644 --- a/driver_test.go +++ b/driver_test.go @@ -338,17 +338,28 @@ type timeTest struct { t time.Time } +func (t timeTest) genQuery(dbtype string, binaryProtocol bool) string { + var inner string + if binaryProtocol { + inner = "?" + } else { + inner = `"%s"` + } + if len(dbtype) >= 9 && dbtype[:9] == "TIMESTAMP" { + return `SELECT TIMESTAMPADD(SECOND,0,CAST(` + inner + ` AS DATETIME` + dbtype[9:] + `))` + } + return `SELECT CAST(` + inner + ` AS ` + dbtype + `)` +} + func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, binaryProtocol bool) { - const queryBin = `SELECT CAST(? AS %[2]s)` - const queryTxt = `SELECT CAST("%[1]s" AS %[2]s)` var rows *sql.Rows var protocol string - if binaryProtocol { + if query := t.genQuery(dbtype, binaryProtocol); binaryProtocol { protocol = "binary" - rows = dbt.mustQuery(fmt.Sprintf(queryBin, t.s, dbtype), t.t) + rows = dbt.mustQuery(query, t.t) } else { protocol = "text" - rows = dbt.mustQuery(fmt.Sprintf(queryTxt, t.s, dbtype)) + rows = dbt.mustQuery(fmt.Sprintf(query, t.s)) } defer rows.Close() var err error @@ -396,16 +407,17 @@ func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, binaryProtocol bool) } func TestDateTime(t *testing.T) { - afterTime0 := func(d string) time.Time { + afterTime := func(t time.Time, d string) time.Time { dur, err := time.ParseDuration(d) if err != nil { panic(err) } - return time.Time{}.Add(dur) + return t.Add(dur) } // NOTE: MySQL rounds DATETIME(x) up - but that's not included in the tests format := "2006-01-02 15:04:05.999999" t0 := time.Time{} + ts0 := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) tstr0 := "0000-00-00 00:00:00.000000" testcases := []timeTests{ {"DATE", format[:10], []timeTest{ @@ -425,20 +437,32 @@ func TestDateTime(t *testing.T) { {t: t0, s: tstr0}, }}, {"TIME", format[11:19], []timeTest{ - {t: afterTime0("12345s")}, - {t: afterTime0("-12345s")}, + {t: afterTime(t0, "12345s")}, + {t: afterTime(t0, "-12345s")}, {t: t0, s: tstr0[11:19]}, }}, {"TIME(1)", format[11:21], []timeTest{ - {t: afterTime0("12345600ms")}, - {t: afterTime0("-12345600ms")}, + {t: afterTime(t0, "12345600ms")}, + {t: afterTime(t0, "-12345600ms")}, {t: t0, s: tstr0[11:21]}, }}, {"TIME(6)", format[11:], []timeTest{ - {t: afterTime0("1234567890123000ns")}, - {t: afterTime0("-1234567890123000ns")}, + {t: afterTime(t0, "1234567890123000ns")}, + {t: afterTime(t0, "-1234567890123000ns")}, {t: t0, s: tstr0[11:]}, }}, + {"TIMESTAMP", format[:19], []timeTest{ + {t: afterTime(ts0, "12345s")}, + {t: ts0, s: "1970-01-01 00:00:00"}, + }}, + {"TIMESTAMP(1)", format[:21], []timeTest{ + {t: afterTime(ts0, "12345600ms")}, + {t: ts0, s: "1970-01-01 00:00:00.0"}, + }}, + {"TIMESTAMP(6)", format, []timeTest{ + {t: afterTime(ts0, "1234567890123000ns")}, + {t: ts0, s: "1970-01-01 00:00:00.000000"}, + }}, } dsns := map[string]bool{ dsn + "&parseTime=true": true, @@ -446,13 +470,28 @@ func TestDateTime(t *testing.T) { dsn + "&parseTime=false": false, dsn + "&sql_mode=ALLOW_INVALID_DATES&parseTime=false": false, } + var withFrac bool + if db, err := sql.Open("mysql", dsn); err != nil { + t.Fatal(err) + } else { + rows, err := db.Query(`SELECT CAST("00:00:00.123" AS TIME(3)) = "00:00:00.123"`) + if err == nil { + withFrac = true + rows.Close() + } + db.Close() + } for testdsn, parseTime := range dsns { var _ = parseTime runTests(t, testdsn, func(dbt *DBTest) { for _, setups := range testcases { + if t := setups.dbtype; !withFrac && t[len(t)-1:] == ")" { + // skip fractional tests if unsupported by DB + continue + } for _, setup := range setups.tests { if setup.s == "" { - // fill time string where Go can reliable produce it + // fill time string whereever Go can reliable produce it setup.s = setup.t.Format(setups.tlayout) } setup.run(dbt, setups.dbtype, setups.tlayout, true) @@ -1053,9 +1092,8 @@ func TestTimezoneConversion(t *testing.T) { dbt.mustExec("CREATE TABLE test (ts TIMESTAMP)") // Insert local time into database (should be converted) - utc, _ := time.LoadLocation("UTC") usCentral, _ := time.LoadLocation("US/Central") - reftime := time.Date(2014, 05, 30, 18, 03, 17, 0, utc).In(usCentral) + reftime := time.Date(2014, 05, 30, 18, 03, 17, 0, time.UTC).In(usCentral) dbt.mustExec("INSERT INTO test VALUE (?)", reftime) // Retrieve time from DB From f1ab27c68c51024b220e06243bf34fa8e840ae15 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Wed, 4 Jun 2014 07:54:04 +0200 Subject: [PATCH 04/14] Oh Travis, why have you forsaken me (sorry dear watchers) --- packets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packets.go b/packets.go index 1d5ff85a1..6941e08f9 100644 --- a/packets.go +++ b/packets.go @@ -1128,7 +1128,7 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { default: return fmt.Errorf("Invalid TIME-packet length %d", num) } - if decimals := rows.columns[i].decimals; decimals > 0 { + if decimals := rows.columns[i].decimals; decimals > 0 && decimals <= 6 { result += fmt.Sprintf(".%06d", microsecs)[:1+decimals] } dest[i] = []byte(result) From 6d51ca56f55ed2e5ead7298340820ba0b7c251ef Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Thu, 5 Jun 2014 11:39:14 +0200 Subject: [PATCH 05/14] faster and unified date formatting, flexible lengths, better tests --- driver_test.go | 119 +++++++++++++++++++----------- packets.go | 112 ++++++++--------------------- utils.go | 192 ++++++++++++++++++++++++++++++++----------------- utils_test.go | 2 +- 4 files changed, 235 insertions(+), 190 deletions(-) diff --git a/driver_test.go b/driver_test.go index fe7eb8d73..55382466f 100644 --- a/driver_test.go +++ b/driver_test.go @@ -334,7 +334,7 @@ type timeTests struct { } type timeTest struct { - s string + s string // leading "!": do not use t as value in queries t time.Time } @@ -351,15 +351,21 @@ func (t timeTest) genQuery(dbtype string, binaryProtocol bool) string { return `SELECT CAST(` + inner + ` AS ` + dbtype + `)` } -func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, binaryProtocol bool) { +func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode int) { var rows *sql.Rows - var protocol string - if query := t.genQuery(dbtype, binaryProtocol); binaryProtocol { - protocol = "binary" + query := t.genQuery(dbtype, mode < 2) + var protocol = "binary" + switch mode { + case 0: + rows = dbt.mustQuery(query, t.s) + case 1: rows = dbt.mustQuery(query, t.t) - } else { + case 2: protocol = "text" - rows = dbt.mustQuery(fmt.Sprintf(query, t.s)) + query = fmt.Sprintf(query, t.s) + rows = dbt.mustQuery(query) + default: + panic("unsupported mode") } defer rows.Close() var err error @@ -368,17 +374,13 @@ func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, binaryProtocol bool) if err == nil { err = fmt.Errorf("no data") } - dbt.Errorf("%s [%s]: %s", - dbtype, protocol, err, - ) + dbt.Errorf("%s [%s]: %s", dbtype, protocol, err) return } var dst interface{} err = rows.Scan(&dst) if err != nil { - dbt.Errorf("%s [%s]: %s", - dbtype, protocol, err, - ) + dbt.Errorf("%s [%s]: %s", dbtype, protocol, err) return } switch val := dst.(type) { @@ -387,7 +389,7 @@ func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, binaryProtocol bool) if str == t.s { return } - dbt.Errorf("%s to string [%s]: expected '%s', got '%s'", + dbt.Errorf("%s to string [%s]: expected %q, got %q", dbtype, protocol, t.s, str, ) @@ -395,13 +397,15 @@ func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, binaryProtocol bool) if val == t.t { return } - dbt.Errorf("%s to string [%s]: expected '%s', got '%s'", + dbt.Errorf("%s to string [%s]: expected %q, got %q", dbtype, protocol, t.s, val.Format(tlayout), ) default: - dbt.Errorf("%s [%s]: unhandled type %T (is '%s')", - dbtype, protocol, val, val, + fmt.Printf("%#v\n", []interface{}{dbtype, tlayout, mode, t.s, t.t}) + dbt.Errorf("%s [%s]: unhandled type %T (is '%v')", + dbtype, protocol, + val, val, ) } } @@ -428,6 +432,10 @@ func TestDateTime(t *testing.T) { {t: time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)}, {t: t0, s: tstr0[:19]}, }}, + {"DATETIME(0)", format[:21], []timeTest{ + {t: time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)}, + {t: t0, s: tstr0[:19]}, + }}, {"DATETIME(1)", format[:21], []timeTest{ {t: time.Date(2011, 11, 20, 21, 27, 37, 100000000, time.UTC)}, {t: t0, s: tstr0[:21]}, @@ -438,23 +446,40 @@ func TestDateTime(t *testing.T) { }}, {"TIME", format[11:19], []timeTest{ {t: afterTime(t0, "12345s")}, - {t: afterTime(t0, "-12345s")}, + {s: "!-12:34:56"}, + {s: "!-838:59:59"}, + {s: "!838:59:59"}, + {t: t0, s: tstr0[11:19]}, + }}, + {"TIME(0)", format[11:19], []timeTest{ + {t: afterTime(t0, "12345s")}, + {s: "!-12:34:56"}, + {s: "!-838:59:59"}, + {s: "!838:59:59"}, {t: t0, s: tstr0[11:19]}, }}, {"TIME(1)", format[11:21], []timeTest{ {t: afterTime(t0, "12345600ms")}, - {t: afterTime(t0, "-12345600ms")}, + {s: "!-12:34:56.7"}, + {s: "!-838:59:58.9"}, + {s: "!838:59:58.9"}, {t: t0, s: tstr0[11:21]}, }}, {"TIME(6)", format[11:], []timeTest{ {t: afterTime(t0, "1234567890123000ns")}, - {t: afterTime(t0, "-1234567890123000ns")}, + {s: "!-12:34:56.789012"}, + {s: "!-838:59:58.999999"}, + {s: "!838:59:58.999999"}, {t: t0, s: tstr0[11:]}, }}, {"TIMESTAMP", format[:19], []timeTest{ {t: afterTime(ts0, "12345s")}, {t: ts0, s: "1970-01-01 00:00:00"}, }}, + {"TIMESTAMP(0)", format[:19], []timeTest{ + {t: afterTime(ts0, "12345s")}, + {t: ts0, s: "1970-01-01 00:00:00"}, + }}, {"TIMESTAMP(1)", format[:21], []timeTest{ {t: afterTime(ts0, "12345600ms")}, {t: ts0, s: "1970-01-01 00:00:00.0"}, @@ -464,38 +489,50 @@ func TestDateTime(t *testing.T) { {t: ts0, s: "1970-01-01 00:00:00.000000"}, }}, } - dsns := map[string]bool{ - dsn + "&parseTime=true": true, - dsn + "&sql_mode=ALLOW_INVALID_DATES&parseTime=true": true, - dsn + "&parseTime=false": false, - dsn + "&sql_mode=ALLOW_INVALID_DATES&parseTime=false": false, - } - var withFrac bool - if db, err := sql.Open("mysql", dsn); err != nil { - t.Fatal(err) - } else { - rows, err := db.Query(`SELECT CAST("00:00:00.123" AS TIME(3)) = "00:00:00.123"`) - if err == nil { - withFrac = true - rows.Close() - } - db.Close() + dsns := []string{ + dsn + "&parseTime=true", + dsn + "&parseTime=false", } - for testdsn, parseTime := range dsns { - var _ = parseTime + for _, testdsn := range dsns { runTests(t, testdsn, func(dbt *DBTest) { + var withFrac, allowsZero bool + var rows *sql.Rows + var err error + rows, err = dbt.db.Query(`SELECT CAST("00:00:00.1" AS TIME(1)) = "00:00:00.1"`) + if err == nil { + rows.Scan(&withFrac) + rows.Close() + } + rows, err = dbt.db.Query(`SELECT CAST("0000-00-00" AS DATE) = "0000-00-00"`) + if err == nil { + rows.Scan(&allowsZero) + rows.Close() + } for _, setups := range testcases { if t := setups.dbtype; !withFrac && t[len(t)-1:] == ")" { - // skip fractional tests if unsupported by DB + // skip fractional second tests if unsupported by server continue } for _, setup := range setups.tests { + timeArgBinary := true if setup.s == "" { // fill time string whereever Go can reliable produce it setup.s = setup.t.Format(setups.tlayout) + } else if setup.s[0] == '!' { + // skip tests using setup.t as source in queries + timeArgBinary = false + // fix setup.s - remove the "!" + setup.s = setup.s[1:] + } + if !allowsZero && setup.s == tstr0[:len(setup.s)] { + // skip disallowed 0000-00-00 date + continue + } + setup.run(dbt, setups.dbtype, setups.tlayout, 0) + if timeArgBinary { + setup.run(dbt, setups.dbtype, setups.tlayout, 1) } - setup.run(dbt, setups.dbtype, setups.tlayout, true) - setup.run(dbt, setups.dbtype, setups.tlayout, false) + setup.run(dbt, setups.dbtype, setups.tlayout, 2) } } }) diff --git a/packets.go b/packets.go index 6941e08f9..95a225da5 100644 --- a/packets.go +++ b/packets.go @@ -954,7 +954,6 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error { // http://dev.mysql.com/doc/internals/en/binary-protocol-resultset-row.html func (rows *binaryRows) readRow(dest []driver.Value) error { - timestr := "00:00:00.000000" data, err := rows.mc.readPacket() if err != nil { return err @@ -1060,98 +1059,43 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { } return err - // Date YYYY-MM-DD - case fieldTypeDate, fieldTypeNewDate: + case + fieldTypeDate, fieldTypeNewDate, // Date YYYY-MM-DD + fieldTypeTime, // Time [-][H]HH:MM:SS[.fractal] + fieldTypeTimestamp, fieldTypeDateTime: // Timestamp YYYY-MM-DD HH:MM:SS[.fractal] + num, isNull, n := readLengthEncodedInteger(data[pos:]) pos += n - if isNull { + switch { + case isNull: dest[i] = nil continue - } - - if rows.mc.parseTime { + case rows.columns[i].fieldType == fieldTypeTime: + // database/sql does not support an equivalent to TIME, return a string + var dstlen uint8 + switch decimals := rows.columns[i].decimals; decimals { + case 0x00, 0x1f: + dstlen = 8 + case 1, 2, 3, 4, 5, 6: + dstlen = 8 + 1 + decimals + } + dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, true) + case rows.mc.parseTime: dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc) - } else { - dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], 10) - } - - if err == nil { - pos += int(num) - continue - } else { - return err - } - - // Time [-][H]HH:MM:SS[.fractal] - case fieldTypeTime: - num, isNull, n := readLengthEncodedInteger(data[pos:]) - pos += n - - if num == 0 { - if isNull { - dest[i] = nil - continue + default: + var dstlen uint8 + if rows.columns[i].fieldType == fieldTypeDate { + dstlen = 10 } else { - length := uint8(8) - if rows.columns[i].decimals > 0 { - length += 1 + uint8(rows.columns[i].decimals) + switch decimals := rows.columns[i].decimals; decimals { + case 0x00, 0x1f: + dstlen = 19 + case 1, 2, 3, 4, 5, 6: + dstlen = 19 + 1 + decimals } - dest[i] = []byte(timestr[:length]) - continue - } - } - - var result string - if data[pos] == 1 { - result = "-" - } - var microsecs uint32 - switch num { - case 8: - result += fmt.Sprintf( - "%02d:%02d:%02d", - uint16(data[pos+1])*24+uint16(data[pos+5]), - data[pos+6], - data[pos+7], - ) - pos += 8 - case 12: - result += fmt.Sprintf( - "%02d:%02d:%02d", - uint16(data[pos+1])*24+uint16(data[pos+5]), - data[pos+6], - data[pos+7], - ) - microsecs = binary.LittleEndian.Uint32(data[pos+8 : pos+12]) - pos += 12 - default: - return fmt.Errorf("Invalid TIME-packet length %d", num) - } - if decimals := rows.columns[i].decimals; decimals > 0 && decimals <= 6 { - result += fmt.Sprintf(".%06d", microsecs)[:1+decimals] - } - dest[i] = []byte(result) - - // Timestamp YYYY-MM-DD HH:MM:SS[.fractal] - case fieldTypeTimestamp, fieldTypeDateTime: - num, isNull, n := readLengthEncodedInteger(data[pos:]) - - pos += n - - if isNull { - dest[i] = nil - continue - } - - if rows.mc.parseTime { - dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc) - } else { - length := uint8(19) - if rows.columns[i].decimals > 0 { - length += 1 + uint8(rows.columns[i].decimals) } - dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], length) + dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, false) } if err == nil { diff --git a/utils.go b/utils.go index 5397908ba..13abf8f7a 100644 --- a/utils.go +++ b/utils.go @@ -520,76 +520,140 @@ func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Va // The current behavior depends on database/sql copying the result. var zeroDateTime = []byte("0000-00-00 00:00:00.000000") -func formatBinaryDateTime(src []byte, length uint8) (driver.Value, error) { +func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value, error) { + // length expects the deterministic length of the zero value, + // negative time and 100+ hours are automatically added if needed + const pairs = "00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899" if len(src) == 0 { + if justTime { + return zeroDateTime[11 : 11+length], nil + } return zeroDateTime[:length], nil } - var dst []byte - switch length { - case 10: - dst = []byte("0000-00-00") - case 19: - dst = []byte("0000-00-00 00:00:00") - case 21, 22, 23, 24, 25, 26: - dst = []byte("0000-00-00 00:00:00.000000") + var dst []byte // return value + var pt, p1, p2, p3 byte // current digit pair + var zOffs byte // offset of value in zeroDateTime + if justTime { + switch length { + case + 8, // time (can be up to 10 when negative and 100+ hours) + 10, 11, 12, 13, 14, 15: // time with fractional seconds + default: + return nil, fmt.Errorf("illegal TIME length %d", length) + } + switch len(src) { + case 8, 12: + default: + return nil, fmt.Errorf("Invalid TIME-packet length %d", len(src)) + } + // +2 to enable negative time and 100+ hours + dst = make([]byte, 0, length+2) + if src[0] == 1 { + dst = append(dst, '-') + } + if src[1] != 0 { + hour := uint16(src[1])*24 + uint16(src[5]) + pt = byte(hour / 100) + p1 = byte(hour - 100*uint16(pt)) + dst = append(dst, '0'+pt) + } else { + p1 = src[5] + } + zOffs = 11 + src = src[6:] + } else { + switch length { + case 10, 19, 21, 22, 23, 24, 25, 26: + default: + t := "DATE" + if length > 10 { + t += "TIME" + } + return nil, fmt.Errorf("illegal %s length %d", t, length) + } + switch len(src) { + case 4, 7, 11: + default: + t := "DATE" + if length > 10 { + t += "TIME" + } + return nil, fmt.Errorf("illegal %s-packet length %d", t, len(src)) + } + dst = make([]byte, 0, length) + // start with the date + year := binary.LittleEndian.Uint16(src[:2]) + pt = byte(year / 100) + p1 = byte(year - 100*uint16(pt)) + p2, p3 = src[2], src[3] + dst = append(dst, + pairs[2*pt], pairs[2*pt+1], + pairs[2*p1], pairs[2*p1+1], '-', + pairs[2*p2], pairs[2*p2+1], '-', + pairs[2*p3], pairs[2*p3+1], + ) + if length == 10 { + return dst, nil + } + if len(src) == 4 { + return append(dst, zeroDateTime[10:length]...), nil + } + dst = append(dst, ' ') + p1 = src[4] // hour + src = src[5:] + } + // p1 is 2-digit hour, src is after hour + p2, p3 = src[0], src[1] + dst = append(dst, + pairs[2*p1], pairs[2*p1+1], ':', + pairs[2*p2], pairs[2*p2+1], ':', + pairs[2*p3], pairs[2*p3+1], + ) + if length <= byte(len(dst)) { + return dst, nil + } + src = src[2:] + if len(src) == 0 { + return append(dst, zeroDateTime[19:zOffs+length]...), nil + } + microsecs := binary.LittleEndian.Uint32(src[:4]) + p1 = byte(microsecs / 10000) + microsecs -= 10000 * uint32(p1) + p2 = byte(microsecs / 100) + microsecs -= 100 * uint32(p2) + p3 = byte(microsecs) + switch decimals := zOffs + length - 20; decimals { default: - return nil, fmt.Errorf("illegal datetime length %d", length) - } - switch len(src) { - case 11: - microsecs := binary.LittleEndian.Uint32(src[7:11]) - tmp32 := microsecs / 10 - dst[25] += byte(microsecs - 10*tmp32) - tmp32, microsecs = tmp32/10, tmp32 - dst[24] += byte(microsecs - 10*tmp32) - tmp32, microsecs = tmp32/10, tmp32 - dst[23] += byte(microsecs - 10*tmp32) - tmp32, microsecs = tmp32/10, tmp32 - dst[22] += byte(microsecs - 10*tmp32) - tmp32, microsecs = tmp32/10, tmp32 - dst[21] += byte(microsecs - 10*tmp32) - dst[20] += byte(microsecs / 10) - fallthrough - case 7: - second := src[6] - tmp := second / 10 - dst[18] += second - 10*tmp - dst[17] += tmp - minute := src[5] - tmp = minute / 10 - dst[15] += minute - 10*tmp - dst[14] += tmp - hour := src[4] - tmp = hour / 10 - dst[12] += hour - 10*tmp - dst[11] += tmp - fallthrough + return append(dst, '.', + pairs[2*p1], pairs[2*p1+1], + pairs[2*p2], pairs[2*p2+1], + pairs[2*p3], pairs[2*p3+1], + ), nil + case 1: + return append(dst, '.', + pairs[2*p1], + ), nil + case 2: + return append(dst, '.', + pairs[2*p1], pairs[2*p1+1], + ), nil + case 3: + return append(dst, '.', + pairs[2*p1], pairs[2*p1+1], + pairs[2*p2], + ), nil case 4: - day := src[3] - tmp := day / 10 - dst[9] += day - 10*tmp - dst[8] += tmp - month := src[2] - tmp = month / 10 - dst[6] += month - 10*tmp - dst[5] += tmp - year := binary.LittleEndian.Uint16(src[:2]) - tmp16 := year / 10 - dst[3] += byte(year - 10*tmp16) - tmp16, year = tmp16/10, tmp16 - dst[2] += byte(year - 10*tmp16) - tmp16, year = tmp16/10, tmp16 - dst[1] += byte(year - 10*tmp16) - dst[0] += byte(tmp16) - return dst[:length], nil - } - var t string - if length >= 19 { - t = "DATETIME" - } else { - t = "DATE" + return append(dst, '.', + pairs[2*p1], pairs[2*p1+1], + pairs[2*p2], pairs[2*p2+1], + ), nil + case 5: + return append(dst, '.', + pairs[2*p1], pairs[2*p1+1], + pairs[2*p2], pairs[2*p2+1], + pairs[2*p3], + ), nil } - return nil, fmt.Errorf("invalid %s-packet length %d", t, len(src)) } /****************************************************************************** diff --git a/utils_test.go b/utils_test.go index 891f7c039..6e50b09b9 100644 --- a/utils_test.go +++ b/utils_test.go @@ -192,7 +192,7 @@ func TestFormatBinaryDateTime(t *testing.T) { rawDate[6] = 23 // seconds binary.LittleEndian.PutUint32(rawDate[7:], 987654) // microseconds expect := func(expected string, inlen, outlen uint8) { - actual, _ := formatBinaryDateTime(rawDate[:inlen], outlen) + actual, _ := formatBinaryDateTime(rawDate[:inlen], outlen, false) bytes, ok := actual.([]byte) if !ok { t.Errorf("formatBinaryDateTime must return []byte, was %T", actual) From ec804a3705556db5f30fd348c9df125156fd5fca Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Thu, 5 Jun 2014 12:05:28 +0200 Subject: [PATCH 06/14] trying to get more information out of Travis --- packets.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packets.go b/packets.go index 95a225da5..0ef5ed505 100644 --- a/packets.go +++ b/packets.go @@ -1093,6 +1093,8 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { dstlen = 19 case 1, 2, 3, 4, 5, 6: dstlen = 19 + 1 + decimals + default: + panic(fmt.Sprintf("%#v", rows.columns[i])) } } dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, false) From e941c9394284879c4147cfd30635c11710b0cb71 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Thu, 5 Jun 2014 15:14:23 +0200 Subject: [PATCH 07/14] trying to get more information out of Travis II --- driver_test.go | 70 ++++++++++++++++++++++++++++++++++---------------- packets.go | 4 ++- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/driver_test.go b/driver_test.go index 55382466f..8752f4461 100644 --- a/driver_test.go +++ b/driver_test.go @@ -338,9 +338,37 @@ type timeTest struct { t time.Time } -func (t timeTest) genQuery(dbtype string, binaryProtocol bool) string { +type timeMode byte + +func (t timeMode) String() string { + switch t { + case binaryString: + return "binary:string" + case binaryTime: + return "binary:time.Time" + case textString: + return "text:string" + } + panic("unsupported timeMode") +} + +func (t timeMode) Binary() bool { + switch t { + case binaryString, binaryTime: + return true + } + return false +} + +const ( + binaryString timeMode = iota + binaryTime + textString +) + +func (t timeTest) genQuery(dbtype string, mode timeMode) string { var inner string - if binaryProtocol { + if mode.Binary() { inner = "?" } else { inner = `"%s"` @@ -351,17 +379,15 @@ func (t timeTest) genQuery(dbtype string, binaryProtocol bool) string { return `SELECT CAST(` + inner + ` AS ` + dbtype + `)` } -func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode int) { +func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode timeMode) { var rows *sql.Rows - query := t.genQuery(dbtype, mode < 2) - var protocol = "binary" + query := t.genQuery(dbtype, mode) switch mode { - case 0: + case binaryString: rows = dbt.mustQuery(query, t.s) - case 1: + case binaryTime: rows = dbt.mustQuery(query, t.t) - case 2: - protocol = "text" + case textString: query = fmt.Sprintf(query, t.s) rows = dbt.mustQuery(query) default: @@ -374,13 +400,13 @@ func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode int) { if err == nil { err = fmt.Errorf("no data") } - dbt.Errorf("%s [%s]: %s", dbtype, protocol, err) + dbt.Errorf("%s [%s]: %s", dbtype, mode, err) return } var dst interface{} err = rows.Scan(&dst) if err != nil { - dbt.Errorf("%s [%s]: %s", dbtype, protocol, err) + dbt.Errorf("%s [%s]: %s", dbtype, mode, err) return } switch val := dst.(type) { @@ -389,22 +415,22 @@ func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode int) { if str == t.s { return } - dbt.Errorf("%s to string [%s]: expected %q, got %q", - dbtype, protocol, + dbt.Errorf("%s [%s] to string: expected %q, got %q", + dbtype, mode, t.s, str, ) case time.Time: if val == t.t { return } - dbt.Errorf("%s to string [%s]: expected %q, got %q", - dbtype, protocol, + dbt.Errorf("%s [%s] to string: expected %q, got %q", + dbtype, mode, t.s, val.Format(tlayout), ) default: fmt.Printf("%#v\n", []interface{}{dbtype, tlayout, mode, t.s, t.t}) dbt.Errorf("%s [%s]: unhandled type %T (is '%v')", - dbtype, protocol, + dbtype, mode, val, val, ) } @@ -514,13 +540,13 @@ func TestDateTime(t *testing.T) { continue } for _, setup := range setups.tests { - timeArgBinary := true + allowBinTime := true if setup.s == "" { // fill time string whereever Go can reliable produce it setup.s = setup.t.Format(setups.tlayout) } else if setup.s[0] == '!' { // skip tests using setup.t as source in queries - timeArgBinary = false + allowBinTime = false // fix setup.s - remove the "!" setup.s = setup.s[1:] } @@ -528,11 +554,11 @@ func TestDateTime(t *testing.T) { // skip disallowed 0000-00-00 date continue } - setup.run(dbt, setups.dbtype, setups.tlayout, 0) - if timeArgBinary { - setup.run(dbt, setups.dbtype, setups.tlayout, 1) + setup.run(dbt, setups.dbtype, setups.tlayout, textString) + setup.run(dbt, setups.dbtype, setups.tlayout, binaryString) + if allowBinTime { + setup.run(dbt, setups.dbtype, setups.tlayout, binaryTime) } - setup.run(dbt, setups.dbtype, setups.tlayout, 2) } } }) diff --git a/packets.go b/packets.go index 0ef5ed505..8360e3d99 100644 --- a/packets.go +++ b/packets.go @@ -1094,7 +1094,9 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { case 1, 2, 3, 4, 5, 6: dstlen = 19 + 1 + decimals default: - panic(fmt.Sprintf("%#v", rows.columns[i])) + panic(fmt.Sprintf("unexpected decimals value in column %d: %#v", + i, rows.columns[i], + )) } } dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, false) From 7e893ef8a1929dbedb1e6c327028d577f31ef414 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Thu, 5 Jun 2014 16:14:44 +0200 Subject: [PATCH 08/14] skip offending test on misbehaving servers --- CHANGELOG.md | 11 +++++++++++ driver_test.go | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index feb53a1e8..9bb92c1cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## HEAD + +Changes: + + - Use decimals field from MySQL to format time types + +Bugfixes: + + - Enable microsecond resolution on TIME, DATETIME and TIMESTAMP + + ## Version 1.2 (2014-06-03) Changes: diff --git a/driver_test.go b/driver_test.go index 8752f4461..d009901a9 100644 --- a/driver_test.go +++ b/driver_test.go @@ -374,9 +374,9 @@ func (t timeTest) genQuery(dbtype string, mode timeMode) string { inner = `"%s"` } if len(dbtype) >= 9 && dbtype[:9] == "TIMESTAMP" { - return `SELECT TIMESTAMPADD(SECOND,0,CAST(` + inner + ` AS DATETIME` + dbtype[9:] + `))` + return `SELECT timestampadd(second,0,cast(` + inner + ` as DATETIME` + dbtype[9:] + `))` } - return `SELECT CAST(` + inner + ` AS ` + dbtype + `)` + return `SELECT cast(` + inner + ` as ` + dbtype + `)` } func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode timeMode) { @@ -524,16 +524,23 @@ func TestDateTime(t *testing.T) { var withFrac, allowsZero bool var rows *sql.Rows var err error - rows, err = dbt.db.Query(`SELECT CAST("00:00:00.1" AS TIME(1)) = "00:00:00.1"`) + rows, err = dbt.db.Query(`SELECT cast("00:00:00.1" as TIME(1)) = "00:00:00.1"`) if err == nil { rows.Scan(&withFrac) rows.Close() } - rows, err = dbt.db.Query(`SELECT CAST("0000-00-00" AS DATE) = "0000-00-00"`) + rows, err = dbt.db.Query(`SELECT cast("0000-00-00" as DATE) = "0000-00-00"`) if err == nil { rows.Scan(&allowsZero) rows.Close() } + // Fix for TravisCI with its "special" MySQL version + var datetimeAutofracs bool + rows, err = dbt.db.Query(`SELECT length(concat('',cast(? as DATETIME))) != 19`, "2011-11-20 21:27:37") + if err == nil { + rows.Scan(&datetimeAutofracs) + rows.Close() + } for _, setups := range testcases { if t := setups.dbtype; !withFrac && t[len(t)-1:] == ")" { // skip fractional second tests if unsupported by server @@ -554,6 +561,9 @@ func TestDateTime(t *testing.T) { // skip disallowed 0000-00-00 date continue } + if datetimeAutofracs && setups.dbtype == "DATETIME" && setup.t != t0 { + continue + } setup.run(dbt, setups.dbtype, setups.tlayout, textString) setup.run(dbt, setups.dbtype, setups.tlayout, binaryString) if allowBinTime { From dc9e515142073cb7e3e9bb7fcfd0977dfefed2ed Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Thu, 5 Jun 2014 16:31:49 +0200 Subject: [PATCH 09/14] skip offending test on misbehaving servers - another way --- driver_test.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/driver_test.go b/driver_test.go index d009901a9..eea8901c3 100644 --- a/driver_test.go +++ b/driver_test.go @@ -415,6 +415,12 @@ func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode timeMode) { if str == t.s { return } + if mode.Binary() && dbtype == "DATETIME" && len(str) == 26 && str[:19] == t.s { + // a fix mainly for TravisCI: + // accept full microsecond resolution in result for DATETIME columns + // where the binary protocol was used + return + } dbt.Errorf("%s [%s] to string: expected %q, got %q", dbtype, mode, t.s, str, @@ -534,13 +540,6 @@ func TestDateTime(t *testing.T) { rows.Scan(&allowsZero) rows.Close() } - // Fix for TravisCI with its "special" MySQL version - var datetimeAutofracs bool - rows, err = dbt.db.Query(`SELECT length(concat('',cast(? as DATETIME))) != 19`, "2011-11-20 21:27:37") - if err == nil { - rows.Scan(&datetimeAutofracs) - rows.Close() - } for _, setups := range testcases { if t := setups.dbtype; !withFrac && t[len(t)-1:] == ")" { // skip fractional second tests if unsupported by server @@ -561,9 +560,6 @@ func TestDateTime(t *testing.T) { // skip disallowed 0000-00-00 date continue } - if datetimeAutofracs && setups.dbtype == "DATETIME" && setup.t != t0 { - continue - } setup.run(dbt, setups.dbtype, setups.tlayout, textString) setup.run(dbt, setups.dbtype, setups.tlayout, binaryString) if allowBinTime { From 2aa37a1cca70bba1e2f33f2ffb22b7840112e1e0 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Sat, 7 Jun 2014 08:57:13 +0200 Subject: [PATCH 10/14] improved date formatting with inspiration from strconv --- utils.go | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/utils.go b/utils.go index 13abf8f7a..ccb125db1 100644 --- a/utils.go +++ b/utils.go @@ -523,7 +523,8 @@ var zeroDateTime = []byte("0000-00-00 00:00:00.000000") func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value, error) { // length expects the deterministic length of the zero value, // negative time and 100+ hours are automatically added if needed - const pairs = "00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899" + const digits01 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" + const digits10 = "0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999" if len(src) == 0 { if justTime { return zeroDateTime[11 : 11+length], nil @@ -555,7 +556,7 @@ func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value hour := uint16(src[1])*24 + uint16(src[5]) pt = byte(hour / 100) p1 = byte(hour - 100*uint16(pt)) - dst = append(dst, '0'+pt) + dst = append(dst, digits01[pt]) } else { p1 = src[5] } @@ -587,10 +588,10 @@ func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value p1 = byte(year - 100*uint16(pt)) p2, p3 = src[2], src[3] dst = append(dst, - pairs[2*pt], pairs[2*pt+1], - pairs[2*p1], pairs[2*p1+1], '-', - pairs[2*p2], pairs[2*p2+1], '-', - pairs[2*p3], pairs[2*p3+1], + digits10[pt], digits01[pt], + digits10[p1], digits01[p1], '-', + digits10[p2], digits01[p2], '-', + digits10[p3], digits01[p3], ) if length == 10 { return dst, nil @@ -605,9 +606,9 @@ func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value // p1 is 2-digit hour, src is after hour p2, p3 = src[0], src[1] dst = append(dst, - pairs[2*p1], pairs[2*p1+1], ':', - pairs[2*p2], pairs[2*p2+1], ':', - pairs[2*p3], pairs[2*p3+1], + digits10[p1], digits01[p1], ':', + digits10[p2], digits01[p2], ':', + digits10[p3], digits01[p3], ) if length <= byte(len(dst)) { return dst, nil @@ -625,33 +626,33 @@ func formatBinaryDateTime(src []byte, length uint8, justTime bool) (driver.Value switch decimals := zOffs + length - 20; decimals { default: return append(dst, '.', - pairs[2*p1], pairs[2*p1+1], - pairs[2*p2], pairs[2*p2+1], - pairs[2*p3], pairs[2*p3+1], + digits10[p1], digits01[p1], + digits10[p2], digits01[p2], + digits10[p3], digits01[p3], ), nil case 1: return append(dst, '.', - pairs[2*p1], + digits10[p1], ), nil case 2: return append(dst, '.', - pairs[2*p1], pairs[2*p1+1], + digits10[p1], digits01[p1], ), nil case 3: return append(dst, '.', - pairs[2*p1], pairs[2*p1+1], - pairs[2*p2], + digits10[p1], digits01[p1], + digits10[p2], ), nil case 4: return append(dst, '.', - pairs[2*p1], pairs[2*p1+1], - pairs[2*p2], pairs[2*p2+1], + digits10[p1], digits01[p1], + digits10[p2], digits01[p2], ), nil case 5: return append(dst, '.', - pairs[2*p1], pairs[2*p1+1], - pairs[2*p2], pairs[2*p2+1], - pairs[2*p3], + digits10[p1], digits01[p1], + digits10[p2], digits01[p2], + digits10[p3], ), nil } } From 81d54a2bbf46e62cd1ad0e173dafd5a8183ac905 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Sun, 8 Jun 2014 10:20:08 +0200 Subject: [PATCH 11/14] changes based on review --- packets.go | 5 +---- rows.go | 3 +-- utils.go | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packets.go b/packets.go index 8360e3d99..7d6cdc377 100644 --- a/packets.go +++ b/packets.go @@ -559,11 +559,8 @@ func (mc *mysqlConn) readColumns(count int) ([]mysqlField, error) { // Filler [uint8] // Charset [charset, collation uint8] - pos += n + 1 + 2 - // Length [uint32] - columns[i].length = binary.LittleEndian.Uint32(data[pos : pos+4]) - pos += 4 + pos += n + 1 + 2 + 4 // Field type [uint8] columns[i].fieldType = data[pos] diff --git a/rows.go b/rows.go index d76b389c4..ab2571f36 100644 --- a/rows.go +++ b/rows.go @@ -15,10 +15,9 @@ import ( type mysqlField struct { name string - length uint32 // length as string: DATETIME(4) => 24 flags fieldFlag fieldType byte - decimals byte // numeric precision: DATETIME(4) => 4, also for DECIMAL etc. + decimals byte } type mysqlRows struct { diff --git a/utils.go b/utils.go index ccb125db1..cc9da4538 100644 --- a/utils.go +++ b/utils.go @@ -30,7 +30,7 @@ var ( ) // timeFormat must not be changed -var timeFormat = "2006-01-02 15:04:05.999999" +const timeFormat = "2006-01-02 15:04:05.999999" func init() { tlsConfigRegister = make(map[string]*tls.Config) From 589fef43d792602eccaea3b52c8c6dc74983e45f Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Sun, 24 Aug 2014 17:30:48 +0200 Subject: [PATCH 12/14] replace panic with error, handle missing error case --- packets.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packets.go b/packets.go index 7d6cdc377..f2e385bf8 100644 --- a/packets.go +++ b/packets.go @@ -1076,6 +1076,11 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { dstlen = 8 case 1, 2, 3, 4, 5, 6: dstlen = 8 + 1 + decimals + default: + return fmt.Errorf( + "MySQL protocol error, illegal decimals value %d", + rows.columns[i].decimals, + ) } dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, true) case rows.mc.parseTime: @@ -1091,9 +1096,10 @@ func (rows *binaryRows) readRow(dest []driver.Value) error { case 1, 2, 3, 4, 5, 6: dstlen = 19 + 1 + decimals default: - panic(fmt.Sprintf("unexpected decimals value in column %d: %#v", - i, rows.columns[i], - )) + return fmt.Errorf( + "MySQL protocol error, illegal decimals value %d", + rows.columns[i].decimals, + ) } } dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, false) From f1914b405a10666d94073e9b5d6c33d3509373ca Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Wed, 3 Sep 2014 18:13:59 +0200 Subject: [PATCH 13/14] removed TIMESTAMP tests (still tested in TZ conversion) --- const.go | 1 + driver_test.go | 44 ++++++++++++++++++-------------------------- utils.go | 3 --- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/const.go b/const.go index bf8afa574..5fcc3e98b 100644 --- a/const.go +++ b/const.go @@ -11,6 +11,7 @@ package mysql const ( minProtocolVersion byte = 10 maxPacketSize = 1<<24 - 1 + timeFormat = "2006-01-02 15:04:05.999999" ) // MySQL constants documentation: diff --git a/driver_test.go b/driver_test.go index eea8901c3..30a4a8352 100644 --- a/driver_test.go +++ b/driver_test.go @@ -373,9 +373,6 @@ func (t timeTest) genQuery(dbtype string, mode timeMode) string { } else { inner = `"%s"` } - if len(dbtype) >= 9 && dbtype[:9] == "TIMESTAMP" { - return `SELECT timestampadd(second,0,cast(` + inner + ` as DATETIME` + dbtype[9:] + `))` - } return `SELECT cast(` + inner + ` as ` + dbtype + `)` } @@ -453,7 +450,6 @@ func TestDateTime(t *testing.T) { // NOTE: MySQL rounds DATETIME(x) up - but that's not included in the tests format := "2006-01-02 15:04:05.999999" t0 := time.Time{} - ts0 := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) tstr0 := "0000-00-00 00:00:00.000000" testcases := []timeTests{ {"DATE", format[:10], []timeTest{ @@ -504,22 +500,6 @@ func TestDateTime(t *testing.T) { {s: "!838:59:58.999999"}, {t: t0, s: tstr0[11:]}, }}, - {"TIMESTAMP", format[:19], []timeTest{ - {t: afterTime(ts0, "12345s")}, - {t: ts0, s: "1970-01-01 00:00:00"}, - }}, - {"TIMESTAMP(0)", format[:19], []timeTest{ - {t: afterTime(ts0, "12345s")}, - {t: ts0, s: "1970-01-01 00:00:00"}, - }}, - {"TIMESTAMP(1)", format[:21], []timeTest{ - {t: afterTime(ts0, "12345600ms")}, - {t: ts0, s: "1970-01-01 00:00:00.0"}, - }}, - {"TIMESTAMP(6)", format, []timeTest{ - {t: afterTime(ts0, "1234567890123000ns")}, - {t: ts0, s: "1970-01-01 00:00:00.000000"}, - }}, } dsns := []string{ dsn + "&parseTime=true", @@ -527,21 +507,22 @@ func TestDateTime(t *testing.T) { } for _, testdsn := range dsns { runTests(t, testdsn, func(dbt *DBTest) { - var withFrac, allowsZero bool + microsecsSupported := false + zeroDateSupported := false var rows *sql.Rows var err error rows, err = dbt.db.Query(`SELECT cast("00:00:00.1" as TIME(1)) = "00:00:00.1"`) if err == nil { - rows.Scan(&withFrac) + rows.Scan(µsecsSupported) rows.Close() } rows, err = dbt.db.Query(`SELECT cast("0000-00-00" as DATE) = "0000-00-00"`) if err == nil { - rows.Scan(&allowsZero) + rows.Scan(&zeroDateSupported) rows.Close() } for _, setups := range testcases { - if t := setups.dbtype; !withFrac && t[len(t)-1:] == ")" { + if t := setups.dbtype; !microsecsSupported && t[len(t)-1:] == ")" { // skip fractional second tests if unsupported by server continue } @@ -556,7 +537,7 @@ func TestDateTime(t *testing.T) { // fix setup.s - remove the "!" setup.s = setup.s[1:] } - if !allowsZero && setup.s == tstr0[:len(setup.s)] { + if !zeroDateSupported && setup.s == tstr0[:len(setup.s)] { // skip disallowed 0000-00-00 date continue } @@ -917,6 +898,17 @@ func TestFoundRows(t *testing.T) { func TestStrict(t *testing.T) { // ALLOW_INVALID_DATES to get rid of stricter modes - we want to test for warnings, not errors relaxedDsn := dsn + "&sql_mode=ALLOW_INVALID_DATES" + // make sure the MySQL version is recent enough with a separate connection + // before running the test + conn, err := MySQLDriver{}.Open(relaxedDsn) + if conn != nil { + conn.Close() + } + if me, ok := err.(*MySQLError); ok && me.Number == 1231 { + // Error 1231: Variable 'sql_mode' can't be set to the value of 'ALLOW_INVALID_DATES' + // => skip test, MySQL server version is too old + return + } runTests(t, relaxedDsn, func(dbt *DBTest) { dbt.mustExec("CREATE TABLE test (a TINYINT NOT NULL, b CHAR(4))") @@ -1107,7 +1099,7 @@ func TestCollation(t *testing.T) { "latin1_general_ci", "binary", "utf8_unicode_ci", - "utf8mb4_general_ci", + "cp1257_bin", } for _, collation := range testCollations { diff --git a/utils.go b/utils.go index cc9da4538..56f1b082e 100644 --- a/utils.go +++ b/utils.go @@ -29,9 +29,6 @@ var ( errInvalidDSNNoSlash = errors.New("Invalid DSN: Missing the slash separating the database name") ) -// timeFormat must not be changed -const timeFormat = "2006-01-02 15:04:05.999999" - func init() { tlsConfigRegister = make(map[string]*tls.Config) } From 84417312d55543b52014f7447fdb0036f611dee5 Mon Sep 17 00:00:00 2001 From: Arne Hormann Date: Mon, 8 Sep 2014 14:15:09 +0200 Subject: [PATCH 14/14] added new TIMESTAMP test --- driver_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/driver_test.go b/driver_test.go index 30a4a8352..a26027aa5 100644 --- a/driver_test.go +++ b/driver_test.go @@ -552,6 +552,58 @@ func TestDateTime(t *testing.T) { } } +func TestTimestampMicros(t *testing.T) { + format := "2006-01-02 15:04:05.999999" + f0 := format[:19] + f1 := format[:21] + f6 := format[:26] + runTests(t, dsn, func(dbt *DBTest) { + // check if microseconds are supported. + // Do not use timestamp(x) for that check - before 5.5.6, x would mean display width + // and not precision. + // Se last paragraph at http://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html + microsecsSupported := false + if rows, err := dbt.db.Query(`SELECT cast("00:00:00.1" as TIME(1)) = "00:00:00.1"`); err == nil { + rows.Scan(µsecsSupported) + rows.Close() + } + if !microsecsSupported { + // skip test + return + } + _, err := dbt.db.Exec(` + CREATE TABLE test ( + value0 TIMESTAMP NOT NULL DEFAULT '` + f0 + `', + value1 TIMESTAMP(1) NOT NULL DEFAULT '` + f1 + `', + value6 TIMESTAMP(6) NOT NULL DEFAULT '` + f6 + `' + )`, + ) + if err != nil { + dbt.Error(err) + } + defer dbt.mustExec("DROP TABLE IF EXISTS test") + dbt.mustExec("INSERT INTO test SET value0=?, value1=?, value6=?", f0, f1, f6) + var res0, res1, res6 string + rows := dbt.mustQuery("SELECT * FROM test") + if !rows.Next() { + dbt.Errorf("test contained no selectable values") + } + err = rows.Scan(&res0, &res1, &res6) + if err != nil { + dbt.Error(err) + } + if res0 != f0 { + dbt.Errorf("expected %q, got %q", f0, res0) + } + if res1 != f1 { + dbt.Errorf("expected %q, got %q", f1, res1) + } + if res6 != f6 { + dbt.Errorf("expected %q, got %q", f6, res6) + } + }) +} + func TestNULL(t *testing.T) { runTests(t, dsn, func(dbt *DBTest) { nullStmt, err := dbt.db.Prepare("SELECT NULL")