소스 검색

Merge pull request #249 from arnehormann/microsecs

Microseconds
Arne Hormann 11 년 전
부모
커밋
8111ee3ec3
7개의 변경된 파일486개의 추가작업 그리고 290개의 파일을 삭제
  1. 11 0
      CHANGELOG.md
  2. 1 1
      const.go
  3. 284 125
      driver_test.go
  4. 47 81
      packets.go
  5. 3 2
      rows.go
  6. 131 72
      utils.go
  7. 9 9
      utils_test.go

+ 11 - 0
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:

+ 1 - 1
const.go

@@ -11,7 +11,7 @@ package mysql
 const (
 	minProtocolVersion byte = 10
 	maxPacketSize           = 1<<24 - 1
-	timeFormat              = "2006-01-02 15:04:05"
+	timeFormat              = "2006-01-02 15:04:05.999999"
 )
 
 // MySQL constants documentation:

+ 284 - 125
driver_test.go

@@ -338,97 +338,281 @@ 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 // leading "!": do not use t as value in queries
+	t time.Time
+}
+
+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"
 	}
-	type timetest struct {
-		in      interface{}
-		sOut    string
-		tOut    time.Time
-		tIsZero bool
+	panic("unsupported timeMode")
+}
+
+func (t timeMode) Binary() bool {
+	switch t {
+	case binaryString, binaryTime:
+		return true
 	}
-	type tester func(dbt *DBTest, rows *sql.Rows,
-		test *timetest, sqltype, resulttype, mode string)
-	type setup struct {
-		vartype   string
-		dsnSuffix string
-		test      tester
+	return false
+}
+
+const (
+	binaryString timeMode = iota
+	binaryTime
+	textString
+)
+
+func (t timeTest) genQuery(dbtype string, mode timeMode) string {
+	var inner string
+	if mode.Binary() {
+		inner = "?"
+	} else {
+		inner = `"%s"`
 	}
-	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())
-				}
-			}},
+	return `SELECT cast(` + inner + ` as ` + dbtype + `)`
+}
+
+func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode timeMode) {
+	var rows *sql.Rows
+	query := t.genQuery(dbtype, mode)
+	switch mode {
+	case binaryString:
+		rows = dbt.mustQuery(query, t.s)
+	case binaryTime:
+		rows = dbt.mustQuery(query, t.t)
+	case textString:
+		query = fmt.Sprintf(query, t.s)
+		rows = dbt.mustQuery(query)
+	default:
+		panic("unsupported mode")
+	}
+	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, mode, err)
+		return
+	}
+	var dst interface{}
+	err = rows.Scan(&dst)
+	if err != nil {
+		dbt.Errorf("%s [%s]: %s", dbtype, mode, err)
+		return
+	}
+	switch val := dst.(type) {
+	case []uint8:
+		str := string(val)
+		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,
+		)
+	case time.Time:
+		if val == t.t {
+			return
+		}
+		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, mode,
+			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) {
+	afterTime := func(t time.Time, d string) time.Time {
+		dur, err := time.ParseDuration(d)
+		if err != nil {
+			panic(err)
+		}
+		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{}
+	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(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]},
+		}},
+		{"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: 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")},
+			{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")},
+			{s: "!-12:34:56.789012"},
+			{s: "!-838:59:58.999999"},
+			{s: "!838:59:58.999999"},
+			{t: t0, s: tstr0[11:]},
+		}},
+	}
+	dsns := []string{
+		dsn + "&parseTime=true",
+		dsn + "&parseTime=false",
+	}
+	for _, testdsn := range dsns {
+		runTests(t, testdsn, func(dbt *DBTest) {
+			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(&microsecsSupported)
+				rows.Close()
+			}
+			rows, err = dbt.db.Query(`SELECT cast("0000-00-00" as DATE) = "0000-00-00"`)
+			if err == nil {
+				rows.Scan(&zeroDateSupported)
+				rows.Close()
+			}
+			for _, setups := range testcases {
+				if t := setups.dbtype; !microsecsSupported && t[len(t)-1:] == ")" {
+					// skip fractional second tests if unsupported by server
+					continue
+				}
+				for _, setup := range setups.tests {
+					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
+						allowBinTime = false
+						// fix setup.s - remove the "!"
+						setup.s = setup.s[1:]
+					}
+					if !zeroDateSupported && setup.s == tstr0[:len(setup.s)] {
+						// skip disallowed 0000-00-00 date
+						continue
+					}
+					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)
 					}
 				}
 			}
-			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)
-	}
+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(&microsecsSupported)
+			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) {
@@ -777,6 +961,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))")
 
@@ -967,7 +1162,7 @@ func TestCollation(t *testing.T) {
 		"latin1_general_ci",
 		"binary",
 		"utf8_unicode_ci",
-		"utf8mb4_general_ci",
+		"cp1257_bin",
 	}
 
 	for _, collation := range testCollations {
@@ -1022,8 +1217,8 @@ func TestTimezoneConversion(t *testing.T) {
 
 		// Insert local time into database (should be converted)
 		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, time.UTC).In(usCentral)
+		dbt.mustExec("INSERT INTO test VALUE (?)", reftime)
 
 		// Retrieve time from DB
 		rows := dbt.mustQuery("SELECT ts FROM test")
@@ -1031,17 +1226,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)
 		}
 	}
 
@@ -1050,42 +1245,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) {

+ 47 - 81
packets.go

@@ -557,20 +557,21 @@ func (mc *mysqlConn) readColumns(count int) ([]mysqlField, error) {
 			return nil, err
 		}
 
-		// Filler [1 byte]
-		// Charset [16 bit uint]
-		// Length [32 bit uint]
+		// Filler [uint8]
+		// Charset [charset, collation uint8]
+		// Length [uint32]
 		pos += n + 1 + 2 + 4
 
-		// Field type [byte]
+		// 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]
@@ -1055,88 +1056,53 @@ 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
+				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:
 				dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.loc)
-			} else {
-				dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], false)
-			}
-
-			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 {
-					dest[i] = []byte("00:00:00")
-					continue
+					switch decimals := rows.columns[i].decimals; decimals {
+					case 0x00, 0x1f:
+						dstlen = 19
+					case 1, 2, 3, 4, 5, 6:
+						dstlen = 19 + 1 + decimals
+					default:
+						return fmt.Errorf(
+							"MySQL protocol error, illegal decimals value %d",
+							rows.columns[i].decimals,
+						)
+					}
 				}
-			}
-
-			var sign string
-			if data[pos] == 1 {
-				sign = "-"
-			}
-
-			switch num {
-			case 8:
-				dest[i] = []byte(fmt.Sprintf(
-					sign+"%02d:%02d:%02d",
-					uint16(data[pos+1])*24+uint16(data[pos+5]),
-					data[pos+6],
-					data[pos+7],
-				))
-				pos += 8
-				continue
-			case 12:
-				dest[i] = []byte(fmt.Sprintf(
-					sign+"%02d:%02d:%02d.%06d",
-					uint16(data[pos+1])*24+uint16(data[pos+5]),
-					data[pos+6],
-					data[pos+7],
-					binary.LittleEndian.Uint32(data[pos+8:pos+12]),
-				))
-				pos += 12
-				continue
-			default:
-				return fmt.Errorf("Invalid TIME-packet length %d", num)
-			}
-
-		// 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 {
-				dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], true)
+				dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen, false)
 			}
 
 			if err == nil {

+ 3 - 2
rows.go

@@ -14,9 +14,10 @@ import (
 )
 
 type mysqlField struct {
-	fieldType byte
-	flags     fieldFlag
 	name      string
+	flags     fieldFlag
+	fieldType byte
+	decimals  byte
 }
 
 type mysqlRows struct {

+ 131 - 72
utils.go

@@ -451,17 +451,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" {
+	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[:10], str)
-	case 19: // YYYY-MM-DD HH:MM:SS
-		if str == "0000-00-00 00:00:00" {
-			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,80 +515,143 @@ 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, 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 digits01 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
+	const digits10 = "0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999"
 	if len(src) == 0 {
-		if withTime {
-			return zeroDateTime, nil
+		if justTime {
+			return zeroDateTime[11 : 11+length], nil
 		}
-		return zeroDateTime[:10], nil
-	}
-	var dst []byte
-	if withTime {
-		if len(src) == 11 {
-			dst = []byte("0000-00-00 00:00:00.000000")
+		return zeroDateTime[:length], nil
+	}
+	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, digits01[pt])
 		} else {
-			dst = []byte("0000-00-00 00:00:00")
+			p1 = src[5]
 		}
+		zOffs = 11
+		src = src[6:]
 	} else {
-		dst = []byte("0000-00-00")
-	}
-	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
-	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
+		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])
-		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)
+		pt = byte(year / 100)
+		p1 = byte(year - 100*uint16(pt))
+		p2, p3 = src[2], src[3]
+		dst = append(dst,
+			digits10[pt], digits01[pt],
+			digits10[p1], digits01[p1], '-',
+			digits10[p2], digits01[p2], '-',
+			digits10[p3], digits01[p3],
+		)
+		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,
+		digits10[p1], digits01[p1], ':',
+		digits10[p2], digits01[p2], ':',
+		digits10[p3], digits01[p3],
+	)
+	if length <= byte(len(dst)) {
 		return dst, nil
 	}
-	var t string
-	if withTime {
-		t = "DATETIME"
-	} else {
-		t = "DATE"
+	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 append(dst, '.',
+			digits10[p1], digits01[p1],
+			digits10[p2], digits01[p2],
+			digits10[p3], digits01[p3],
+		), nil
+	case 1:
+		return append(dst, '.',
+			digits10[p1],
+		), nil
+	case 2:
+		return append(dst, '.',
+			digits10[p1], digits01[p1],
+		), nil
+	case 3:
+		return append(dst, '.',
+			digits10[p1], digits01[p1],
+			digits10[p2],
+		), nil
+	case 4:
+		return append(dst, '.',
+			digits10[p1], digits01[p1],
+			digits10[p2], digits01[p2],
+		), nil
+	case 5:
+		return append(dst, '.',
+			digits10[p1], digits01[p1],
+			digits10[p2], digits01[p2],
+			digits10[p3],
+		), nil
 	}
-	return nil, fmt.Errorf("invalid %s-packet length %d", t, len(src))
 }
 
 /******************************************************************************

+ 9 - 9
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, false)
 		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)
 }