From c5b36aec423d77507002be645d87b37841d23236 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 09:09:13 +0000 Subject: [PATCH 1/4] Initial plan From 9b2e782b7d1571dbfc319f53dca3b9c86dcd4e65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 09:17:22 +0000 Subject: [PATCH 2/4] Support RowsColumnScanner interface (Go 1.27+) --- rows.go | 7 +++--- rows_column_scanner.go | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 rows_column_scanner.go diff --git a/rows.go b/rows.go index 190e75f9..30be2a79 100644 --- a/rows.go +++ b/rows.go @@ -22,9 +22,10 @@ type resultSet struct { } type mysqlRows struct { - mc *mysqlConn - rs resultSet - finish func() + mc *mysqlConn + rs resultSet + finish func() + currentRow []driver.Value // used by RowsColumnScanner (Go 1.27+) } type binaryRows struct { diff --git a/rows_column_scanner.go b/rows_column_scanner.go new file mode 100644 index 00000000..a252fe31 --- /dev/null +++ b/rows_column_scanner.go @@ -0,0 +1,56 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2025 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build go1.27 + +package mysql + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "io" +) + +// NextRow implements driver.RowsColumnScanner. +func (rows *binaryRows) NextRow() error { + if mc := rows.mc; mc != nil { + if err := mc.error(); err != nil { + return err + } + ncols := len(rows.rs.columns) + if len(rows.currentRow) != ncols { + rows.currentRow = make([]driver.Value, ncols) + } + return rows.readRow(rows.currentRow) + } + return io.EOF +} + +// NextRow implements driver.RowsColumnScanner. +func (rows *textRows) NextRow() error { + if mc := rows.mc; mc != nil { + if err := mc.error(); err != nil { + return err + } + ncols := len(rows.rs.columns) + if len(rows.currentRow) != ncols { + rows.currentRow = make([]driver.Value, ncols) + } + return rows.readRow(rows.currentRow) + } + return io.EOF +} + +// ScanColumn implements driver.RowsColumnScanner. +func (rows *mysqlRows) ScanColumn(scanCtx driver.ScanContext, i int, dest any) error { + if i < 0 || i >= len(rows.currentRow) { + return fmt.Errorf("mysql: column index %d out of range [0, %d)", i, len(rows.currentRow)) + } + return sql.ConvertAssign(scanCtx, dest, rows.currentRow[i]) +} From 071d2a661a6830b846d84ade777b48f343c9d40f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 09:52:14 +0000 Subject: [PATCH 3/4] Move RowsColumnScanner implementation into rows.go --- rows.go | 253 +++++++++++++++++++++++++++++++++++++++++ rows_before_go127.go | 227 ++++++++++++++++++++++++++++++++++++ rows_column_scanner.go | 56 --------- 3 files changed, 480 insertions(+), 56 deletions(-) create mode 100644 rows_before_go127.go delete mode 100644 rows_column_scanner.go diff --git a/rows.go b/rows.go index 30be2a79..98db6cd3 100644 --- a/rows.go +++ b/rows.go @@ -6,15 +6,268 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. +//go:build go1.27 + package mysql import ( + "database/sql" "database/sql/driver" + "fmt" "io" "math" "reflect" ) +type resultSet struct { + columns []mysqlField + columnNames []string + done bool +} + +type mysqlRows struct { + mc *mysqlConn + rs resultSet + finish func() + currentRow []driver.Value // used by RowsColumnScanner +} + +type binaryRows struct { + mysqlRows +} + +type textRows struct { + mysqlRows +} + +func (rows *mysqlRows) Columns() []string { + if rows.rs.columnNames != nil { + return rows.rs.columnNames + } + + columns := make([]string, len(rows.rs.columns)) + if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias { + for i := range columns { + if tableName := rows.rs.columns[i].tableName; len(tableName) > 0 { + columns[i] = tableName + "." + rows.rs.columns[i].name + } else { + columns[i] = rows.rs.columns[i].name + } + } + } else { + for i := range columns { + columns[i] = rows.rs.columns[i].name + } + } + + rows.rs.columnNames = columns + return columns +} + +func (rows *mysqlRows) ColumnTypeDatabaseTypeName(i int) string { + return rows.rs.columns[i].typeDatabaseName() +} + +// func (rows *mysqlRows) ColumnTypeLength(i int) (length int64, ok bool) { +// return int64(rows.rs.columns[i].length), true +// } + +func (rows *mysqlRows) ColumnTypeNullable(i int) (nullable, ok bool) { + return rows.rs.columns[i].flags&flagNotNULL == 0, true +} + +func (rows *mysqlRows) ColumnTypePrecisionScale(i int) (int64, int64, bool) { + column := rows.rs.columns[i] + decimals := int64(column.decimals) + + switch column.fieldType { + case fieldTypeDecimal, fieldTypeNewDecimal: + if decimals > 0 { + return int64(column.length) - 2, decimals, true + } + return int64(column.length) - 1, decimals, true + case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeTime: + return decimals, decimals, true + case fieldTypeFloat, fieldTypeDouble: + if decimals == 0x1f { + return math.MaxInt64, math.MaxInt64, true + } + return math.MaxInt64, decimals, true + } + + return 0, 0, false +} + +func (rows *mysqlRows) ColumnTypeScanType(i int) reflect.Type { + return rows.rs.columns[i].scanType() +} + +func (rows *mysqlRows) Close() (err error) { + if f := rows.finish; f != nil { + f() + rows.finish = nil + } + + mc := rows.mc + if mc == nil { + return nil + } + if err := mc.error(); err != nil { + return err + } + + // Remove unread packets from stream + if !rows.rs.done { + err = mc.skipRows() + } + if err == nil { + handleOk := mc.clearResult() + if err = handleOk.discardResults(); err != nil { + return err + } + } + + rows.mc = nil + return err +} + +func (rows *mysqlRows) HasNextResultSet() (b bool) { + if rows.mc == nil { + return false + } + return rows.mc.status&statusMoreResultsExists != 0 +} + +func (rows *mysqlRows) nextResultSet() (int, error) { + if rows.mc == nil { + return 0, io.EOF + } + if err := rows.mc.error(); err != nil { + return 0, err + } + + // Remove unread packets from stream + if !rows.rs.done { + if err := rows.mc.skipRows(); err != nil { + return 0, err + } + rows.rs.done = true + } + + if !rows.HasNextResultSet() { + rows.mc = nil + return 0, io.EOF + } + rows.rs = resultSet{} + // rows.mc.affectedRows and rows.mc.insertIds accumulate on each call to + // nextResultSet. + resLen, _, err := rows.mc.resultUnchanged().readResultSetHeaderPacket() + if err != nil { + // Clean up about multi-results flag + rows.rs.done = true + rows.mc.status = rows.mc.status & (^statusMoreResultsExists) + } + return resLen, err +} + +func (rows *mysqlRows) nextNotEmptyResultSet() (int, error) { + for { + resLen, err := rows.nextResultSet() + if err != nil { + return 0, err + } + + if resLen > 0 { + return resLen, nil + } + + rows.rs.done = true + } +} + +func (rows *binaryRows) NextResultSet() error { + resLen, err := rows.nextNotEmptyResultSet() + if err != nil { + return err + } + + rows.rs.columns, err = rows.mc.readColumns(resLen, nil) + return err +} + +func (rows *binaryRows) Next(dest []driver.Value) error { + if mc := rows.mc; mc != nil { + if err := mc.error(); err != nil { + return err + } + + // Fetch next row from stream + return rows.readRow(dest) + } + return io.EOF +} + +func (rows *textRows) NextResultSet() (err error) { + resLen, err := rows.nextNotEmptyResultSet() + if err != nil { + return err + } + + rows.rs.columns, err = rows.mc.readColumns(resLen, nil) + return err +} + +func (rows *textRows) Next(dest []driver.Value) error { + if mc := rows.mc; mc != nil { + if err := mc.error(); err != nil { + return err + } + + // Fetch next row from stream + return rows.readRow(dest) + } + return io.EOF +} + +// NextRow implements driver.RowsColumnScanner. +func (rows *binaryRows) NextRow() error { + if mc := rows.mc; mc != nil { + if err := mc.error(); err != nil { + return err + } + ncols := len(rows.rs.columns) + if len(rows.currentRow) != ncols { + rows.currentRow = make([]driver.Value, ncols) + } + return rows.readRow(rows.currentRow) + } + return io.EOF +} + +// NextRow implements driver.RowsColumnScanner. +func (rows *textRows) NextRow() error { + if mc := rows.mc; mc != nil { + if err := mc.error(); err != nil { + return err + } + ncols := len(rows.rs.columns) + if len(rows.currentRow) != ncols { + rows.currentRow = make([]driver.Value, ncols) + } + return rows.readRow(rows.currentRow) + } + return io.EOF +} + +// ScanColumn implements driver.RowsColumnScanner. +func (rows *mysqlRows) ScanColumn(scanCtx driver.ScanContext, i int, dest any) error { + if i < 0 || i >= len(rows.currentRow) { + return fmt.Errorf("mysql: column index %d out of range [0, %d)", i, len(rows.currentRow)) + } + return sql.ConvertAssign(scanCtx, dest, rows.currentRow[i]) +} + + type resultSet struct { columns []mysqlField columnNames []string diff --git a/rows_before_go127.go b/rows_before_go127.go new file mode 100644 index 00000000..c034a1cf --- /dev/null +++ b/rows_before_go127.go @@ -0,0 +1,227 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build !go1.27 + +package mysql + +import ( + "database/sql/driver" + "io" + "math" + "reflect" +) + +type resultSet struct { + columns []mysqlField + columnNames []string + done bool +} + +type mysqlRows struct { + mc *mysqlConn + rs resultSet + finish func() +} + +type binaryRows struct { + mysqlRows +} + +type textRows struct { + mysqlRows +} + +func (rows *mysqlRows) Columns() []string { + if rows.rs.columnNames != nil { + return rows.rs.columnNames + } + + columns := make([]string, len(rows.rs.columns)) + if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias { + for i := range columns { + if tableName := rows.rs.columns[i].tableName; len(tableName) > 0 { + columns[i] = tableName + "." + rows.rs.columns[i].name + } else { + columns[i] = rows.rs.columns[i].name + } + } + } else { + for i := range columns { + columns[i] = rows.rs.columns[i].name + } + } + + rows.rs.columnNames = columns + return columns +} + +func (rows *mysqlRows) ColumnTypeDatabaseTypeName(i int) string { + return rows.rs.columns[i].typeDatabaseName() +} + +// func (rows *mysqlRows) ColumnTypeLength(i int) (length int64, ok bool) { +// return int64(rows.rs.columns[i].length), true +// } + +func (rows *mysqlRows) ColumnTypeNullable(i int) (nullable, ok bool) { + return rows.rs.columns[i].flags&flagNotNULL == 0, true +} + +func (rows *mysqlRows) ColumnTypePrecisionScale(i int) (int64, int64, bool) { + column := rows.rs.columns[i] + decimals := int64(column.decimals) + + switch column.fieldType { + case fieldTypeDecimal, fieldTypeNewDecimal: + if decimals > 0 { + return int64(column.length) - 2, decimals, true + } + return int64(column.length) - 1, decimals, true + case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeTime: + return decimals, decimals, true + case fieldTypeFloat, fieldTypeDouble: + if decimals == 0x1f { + return math.MaxInt64, math.MaxInt64, true + } + return math.MaxInt64, decimals, true + } + + return 0, 0, false +} + +func (rows *mysqlRows) ColumnTypeScanType(i int) reflect.Type { + return rows.rs.columns[i].scanType() +} + +func (rows *mysqlRows) Close() (err error) { + if f := rows.finish; f != nil { + f() + rows.finish = nil + } + + mc := rows.mc + if mc == nil { + return nil + } + if err := mc.error(); err != nil { + return err + } + + // Remove unread packets from stream + if !rows.rs.done { + err = mc.skipRows() + } + if err == nil { + handleOk := mc.clearResult() + if err = handleOk.discardResults(); err != nil { + return err + } + } + + rows.mc = nil + return err +} + +func (rows *mysqlRows) HasNextResultSet() (b bool) { + if rows.mc == nil { + return false + } + return rows.mc.status&statusMoreResultsExists != 0 +} + +func (rows *mysqlRows) nextResultSet() (int, error) { + if rows.mc == nil { + return 0, io.EOF + } + if err := rows.mc.error(); err != nil { + return 0, err + } + + // Remove unread packets from stream + if !rows.rs.done { + if err := rows.mc.skipRows(); err != nil { + return 0, err + } + rows.rs.done = true + } + + if !rows.HasNextResultSet() { + rows.mc = nil + return 0, io.EOF + } + rows.rs = resultSet{} + // rows.mc.affectedRows and rows.mc.insertIds accumulate on each call to + // nextResultSet. + resLen, _, err := rows.mc.resultUnchanged().readResultSetHeaderPacket() + if err != nil { + // Clean up about multi-results flag + rows.rs.done = true + rows.mc.status = rows.mc.status & (^statusMoreResultsExists) + } + return resLen, err +} + +func (rows *mysqlRows) nextNotEmptyResultSet() (int, error) { + for { + resLen, err := rows.nextResultSet() + if err != nil { + return 0, err + } + + if resLen > 0 { + return resLen, nil + } + + rows.rs.done = true + } +} + +func (rows *binaryRows) NextResultSet() error { + resLen, err := rows.nextNotEmptyResultSet() + if err != nil { + return err + } + + rows.rs.columns, err = rows.mc.readColumns(resLen, nil) + return err +} + +func (rows *binaryRows) Next(dest []driver.Value) error { + if mc := rows.mc; mc != nil { + if err := mc.error(); err != nil { + return err + } + + // Fetch next row from stream + return rows.readRow(dest) + } + return io.EOF +} + +func (rows *textRows) NextResultSet() (err error) { + resLen, err := rows.nextNotEmptyResultSet() + if err != nil { + return err + } + + rows.rs.columns, err = rows.mc.readColumns(resLen, nil) + return err +} + +func (rows *textRows) Next(dest []driver.Value) error { + if mc := rows.mc; mc != nil { + if err := mc.error(); err != nil { + return err + } + + // Fetch next row from stream + return rows.readRow(dest) + } + return io.EOF +} diff --git a/rows_column_scanner.go b/rows_column_scanner.go deleted file mode 100644 index a252fe31..00000000 --- a/rows_column_scanner.go +++ /dev/null @@ -1,56 +0,0 @@ -// Go MySQL Driver - A MySQL-Driver for Go's database/sql package -// -// Copyright 2025 The Go-MySQL-Driver Authors. All rights reserved. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at http://mozilla.org/MPL/2.0/. - -//go:build go1.27 - -package mysql - -import ( - "database/sql" - "database/sql/driver" - "fmt" - "io" -) - -// NextRow implements driver.RowsColumnScanner. -func (rows *binaryRows) NextRow() error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } - ncols := len(rows.rs.columns) - if len(rows.currentRow) != ncols { - rows.currentRow = make([]driver.Value, ncols) - } - return rows.readRow(rows.currentRow) - } - return io.EOF -} - -// NextRow implements driver.RowsColumnScanner. -func (rows *textRows) NextRow() error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } - ncols := len(rows.rs.columns) - if len(rows.currentRow) != ncols { - rows.currentRow = make([]driver.Value, ncols) - } - return rows.readRow(rows.currentRow) - } - return io.EOF -} - -// ScanColumn implements driver.RowsColumnScanner. -func (rows *mysqlRows) ScanColumn(scanCtx driver.ScanContext, i int, dest any) error { - if i < 0 || i >= len(rows.currentRow) { - return fmt.Errorf("mysql: column index %d out of range [0, %d)", i, len(rows.currentRow)) - } - return sql.ConvertAssign(scanCtx, dest, rows.currentRow[i]) -} From 31e5ecb214073a309ca378bd0d6e5e502bc1ce1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 07:17:01 +0000 Subject: [PATCH 4/4] Revert rows.go, delete rows_before_go127.go, rewrite RowsColumnScanner without readRow() --- rows.go | 525 +++++++++++------------------------------ rows_before_go127.go | 227 ------------------ rows_column_scanner.go | 383 ++++++++++++++++++++++++++++++ 3 files changed, 519 insertions(+), 616 deletions(-) delete mode 100644 rows_before_go127.go create mode 100644 rows_column_scanner.go diff --git a/rows.go b/rows.go index 98db6cd3..8c92e925 100644 --- a/rows.go +++ b/rows.go @@ -6,474 +6,221 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. -//go:build go1.27 - package mysql import ( - "database/sql" - "database/sql/driver" - "fmt" - "io" - "math" - "reflect" +"database/sql/driver" +"io" +"math" +"reflect" ) type resultSet struct { - columns []mysqlField - columnNames []string - done bool +columns []mysqlField +columnNames []string +done bool } type mysqlRows struct { - mc *mysqlConn - rs resultSet - finish func() - currentRow []driver.Value // used by RowsColumnScanner +mc *mysqlConn +rs resultSet +finish func() +rawCols [][]byte // buffered per-column raw bytes for RowsColumnScanner (Go 1.27+) } type binaryRows struct { - mysqlRows +mysqlRows } type textRows struct { - mysqlRows +mysqlRows } func (rows *mysqlRows) Columns() []string { - if rows.rs.columnNames != nil { - return rows.rs.columnNames - } - - columns := make([]string, len(rows.rs.columns)) - if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias { - for i := range columns { - if tableName := rows.rs.columns[i].tableName; len(tableName) > 0 { - columns[i] = tableName + "." + rows.rs.columns[i].name - } else { - columns[i] = rows.rs.columns[i].name - } - } - } else { - for i := range columns { - columns[i] = rows.rs.columns[i].name - } - } - - rows.rs.columnNames = columns - return columns +if rows.rs.columnNames != nil { +return rows.rs.columnNames +} + +columns := make([]string, len(rows.rs.columns)) +if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias { +for i := range columns { +if tableName := rows.rs.columns[i].tableName; len(tableName) > 0 { +columns[i] = tableName + "." + rows.rs.columns[i].name +} else { +columns[i] = rows.rs.columns[i].name +} +} +} else { +for i := range columns { +columns[i] = rows.rs.columns[i].name +} +} + +rows.rs.columnNames = columns +return columns } func (rows *mysqlRows) ColumnTypeDatabaseTypeName(i int) string { - return rows.rs.columns[i].typeDatabaseName() +return rows.rs.columns[i].typeDatabaseName() } // func (rows *mysqlRows) ColumnTypeLength(i int) (length int64, ok bool) { -// return int64(rows.rs.columns[i].length), true +// return int64(rows.rs.columns[i].length), true // } func (rows *mysqlRows) ColumnTypeNullable(i int) (nullable, ok bool) { - return rows.rs.columns[i].flags&flagNotNULL == 0, true +return rows.rs.columns[i].flags&flagNotNULL == 0, true } func (rows *mysqlRows) ColumnTypePrecisionScale(i int) (int64, int64, bool) { - column := rows.rs.columns[i] - decimals := int64(column.decimals) - - switch column.fieldType { - case fieldTypeDecimal, fieldTypeNewDecimal: - if decimals > 0 { - return int64(column.length) - 2, decimals, true - } - return int64(column.length) - 1, decimals, true - case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeTime: - return decimals, decimals, true - case fieldTypeFloat, fieldTypeDouble: - if decimals == 0x1f { - return math.MaxInt64, math.MaxInt64, true - } - return math.MaxInt64, decimals, true - } - - return 0, 0, false -} +column := rows.rs.columns[i] +decimals := int64(column.decimals) -func (rows *mysqlRows) ColumnTypeScanType(i int) reflect.Type { - return rows.rs.columns[i].scanType() +switch column.fieldType { +case fieldTypeDecimal, fieldTypeNewDecimal: +if decimals > 0 { +return int64(column.length) - 2, decimals, true } - -func (rows *mysqlRows) Close() (err error) { - if f := rows.finish; f != nil { - f() - rows.finish = nil - } - - mc := rows.mc - if mc == nil { - return nil - } - if err := mc.error(); err != nil { - return err - } - - // Remove unread packets from stream - if !rows.rs.done { - err = mc.skipRows() - } - if err == nil { - handleOk := mc.clearResult() - if err = handleOk.discardResults(); err != nil { - return err - } - } - - rows.mc = nil - return err +return int64(column.length) - 1, decimals, true +case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeTime: +return decimals, decimals, true +case fieldTypeFloat, fieldTypeDouble: +if decimals == 0x1f { +return math.MaxInt64, math.MaxInt64, true } - -func (rows *mysqlRows) HasNextResultSet() (b bool) { - if rows.mc == nil { - return false - } - return rows.mc.status&statusMoreResultsExists != 0 +return math.MaxInt64, decimals, true } -func (rows *mysqlRows) nextResultSet() (int, error) { - if rows.mc == nil { - return 0, io.EOF - } - if err := rows.mc.error(); err != nil { - return 0, err - } - - // Remove unread packets from stream - if !rows.rs.done { - if err := rows.mc.skipRows(); err != nil { - return 0, err - } - rows.rs.done = true - } - - if !rows.HasNextResultSet() { - rows.mc = nil - return 0, io.EOF - } - rows.rs = resultSet{} - // rows.mc.affectedRows and rows.mc.insertIds accumulate on each call to - // nextResultSet. - resLen, _, err := rows.mc.resultUnchanged().readResultSetHeaderPacket() - if err != nil { - // Clean up about multi-results flag - rows.rs.done = true - rows.mc.status = rows.mc.status & (^statusMoreResultsExists) - } - return resLen, err +return 0, 0, false } -func (rows *mysqlRows) nextNotEmptyResultSet() (int, error) { - for { - resLen, err := rows.nextResultSet() - if err != nil { - return 0, err - } - - if resLen > 0 { - return resLen, nil - } - - rows.rs.done = true - } +func (rows *mysqlRows) ColumnTypeScanType(i int) reflect.Type { +return rows.rs.columns[i].scanType() } -func (rows *binaryRows) NextResultSet() error { - resLen, err := rows.nextNotEmptyResultSet() - if err != nil { - return err - } - - rows.rs.columns, err = rows.mc.readColumns(resLen, nil) - return err +func (rows *mysqlRows) Close() (err error) { +if f := rows.finish; f != nil { +f() +rows.finish = nil } -func (rows *binaryRows) Next(dest []driver.Value) error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } - - // Fetch next row from stream - return rows.readRow(dest) - } - return io.EOF +mc := rows.mc +if mc == nil { +return nil } - -func (rows *textRows) NextResultSet() (err error) { - resLen, err := rows.nextNotEmptyResultSet() - if err != nil { - return err - } - - rows.rs.columns, err = rows.mc.readColumns(resLen, nil) - return err +if err := mc.error(); err != nil { +return err } -func (rows *textRows) Next(dest []driver.Value) error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } - - // Fetch next row from stream - return rows.readRow(dest) - } - return io.EOF -} - -// NextRow implements driver.RowsColumnScanner. -func (rows *binaryRows) NextRow() error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } - ncols := len(rows.rs.columns) - if len(rows.currentRow) != ncols { - rows.currentRow = make([]driver.Value, ncols) - } - return rows.readRow(rows.currentRow) - } - return io.EOF -} - -// NextRow implements driver.RowsColumnScanner. -func (rows *textRows) NextRow() error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } - ncols := len(rows.rs.columns) - if len(rows.currentRow) != ncols { - rows.currentRow = make([]driver.Value, ncols) - } - return rows.readRow(rows.currentRow) - } - return io.EOF -} - -// ScanColumn implements driver.RowsColumnScanner. -func (rows *mysqlRows) ScanColumn(scanCtx driver.ScanContext, i int, dest any) error { - if i < 0 || i >= len(rows.currentRow) { - return fmt.Errorf("mysql: column index %d out of range [0, %d)", i, len(rows.currentRow)) - } - return sql.ConvertAssign(scanCtx, dest, rows.currentRow[i]) +// Remove unread packets from stream +if !rows.rs.done { +err = mc.skipRows() } - - -type resultSet struct { - columns []mysqlField - columnNames []string - done bool +if err == nil { +handleOk := mc.clearResult() +if err = handleOk.discardResults(); err != nil { +return err } - -type mysqlRows struct { - mc *mysqlConn - rs resultSet - finish func() - currentRow []driver.Value // used by RowsColumnScanner (Go 1.27+) } -type binaryRows struct { - mysqlRows +rows.mc = nil +return err } -type textRows struct { - mysqlRows +func (rows *mysqlRows) HasNextResultSet() (b bool) { +if rows.mc == nil { +return false } - -func (rows *mysqlRows) Columns() []string { - if rows.rs.columnNames != nil { - return rows.rs.columnNames - } - - columns := make([]string, len(rows.rs.columns)) - if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias { - for i := range columns { - if tableName := rows.rs.columns[i].tableName; len(tableName) > 0 { - columns[i] = tableName + "." + rows.rs.columns[i].name - } else { - columns[i] = rows.rs.columns[i].name - } - } - } else { - for i := range columns { - columns[i] = rows.rs.columns[i].name - } - } - - rows.rs.columnNames = columns - return columns +return rows.mc.status&statusMoreResultsExists != 0 } -func (rows *mysqlRows) ColumnTypeDatabaseTypeName(i int) string { - return rows.rs.columns[i].typeDatabaseName() +func (rows *mysqlRows) nextResultSet() (int, error) { +if rows.mc == nil { +return 0, io.EOF } - -// func (rows *mysqlRows) ColumnTypeLength(i int) (length int64, ok bool) { -// return int64(rows.rs.columns[i].length), true -// } - -func (rows *mysqlRows) ColumnTypeNullable(i int) (nullable, ok bool) { - return rows.rs.columns[i].flags&flagNotNULL == 0, true +if err := rows.mc.error(); err != nil { +return 0, err } -func (rows *mysqlRows) ColumnTypePrecisionScale(i int) (int64, int64, bool) { - column := rows.rs.columns[i] - decimals := int64(column.decimals) - - switch column.fieldType { - case fieldTypeDecimal, fieldTypeNewDecimal: - if decimals > 0 { - return int64(column.length) - 2, decimals, true - } - return int64(column.length) - 1, decimals, true - case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeTime: - return decimals, decimals, true - case fieldTypeFloat, fieldTypeDouble: - if decimals == 0x1f { - return math.MaxInt64, math.MaxInt64, true - } - return math.MaxInt64, decimals, true - } - - return 0, 0, false +// Remove unread packets from stream +if !rows.rs.done { +if err := rows.mc.skipRows(); err != nil { +return 0, err } - -func (rows *mysqlRows) ColumnTypeScanType(i int) reflect.Type { - return rows.rs.columns[i].scanType() +rows.rs.done = true } -func (rows *mysqlRows) Close() (err error) { - if f := rows.finish; f != nil { - f() - rows.finish = nil - } - - mc := rows.mc - if mc == nil { - return nil - } - if err := mc.error(); err != nil { - return err - } - - // Remove unread packets from stream - if !rows.rs.done { - err = mc.skipRows() - } - if err == nil { - handleOk := mc.clearResult() - if err = handleOk.discardResults(); err != nil { - return err - } - } - - rows.mc = nil - return err +if !rows.HasNextResultSet() { +rows.mc = nil +return 0, io.EOF } - -func (rows *mysqlRows) HasNextResultSet() (b bool) { - if rows.mc == nil { - return false - } - return rows.mc.status&statusMoreResultsExists != 0 +rows.rs = resultSet{} +// rows.mc.affectedRows and rows.mc.insertIds accumulate on each call to +// nextResultSet. +resLen, _, err := rows.mc.resultUnchanged().readResultSetHeaderPacket() +if err != nil { +// Clean up about multi-results flag +rows.rs.done = true +rows.mc.status = rows.mc.status & (^statusMoreResultsExists) } - -func (rows *mysqlRows) nextResultSet() (int, error) { - if rows.mc == nil { - return 0, io.EOF - } - if err := rows.mc.error(); err != nil { - return 0, err - } - - // Remove unread packets from stream - if !rows.rs.done { - if err := rows.mc.skipRows(); err != nil { - return 0, err - } - rows.rs.done = true - } - - if !rows.HasNextResultSet() { - rows.mc = nil - return 0, io.EOF - } - rows.rs = resultSet{} - // rows.mc.affectedRows and rows.mc.insertIds accumulate on each call to - // nextResultSet. - resLen, _, err := rows.mc.resultUnchanged().readResultSetHeaderPacket() - if err != nil { - // Clean up about multi-results flag - rows.rs.done = true - rows.mc.status = rows.mc.status & (^statusMoreResultsExists) - } - return resLen, err +return resLen, err } func (rows *mysqlRows) nextNotEmptyResultSet() (int, error) { - for { - resLen, err := rows.nextResultSet() - if err != nil { - return 0, err - } +for { +resLen, err := rows.nextResultSet() +if err != nil { +return 0, err +} - if resLen > 0 { - return resLen, nil - } +if resLen > 0 { +return resLen, nil +} - rows.rs.done = true - } +rows.rs.done = true +} } func (rows *binaryRows) NextResultSet() error { - resLen, err := rows.nextNotEmptyResultSet() - if err != nil { - return err - } +resLen, err := rows.nextNotEmptyResultSet() +if err != nil { +return err +} - rows.rs.columns, err = rows.mc.readColumns(resLen, nil) - return err +rows.rs.columns, err = rows.mc.readColumns(resLen, nil) +return err } func (rows *binaryRows) Next(dest []driver.Value) error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } +if mc := rows.mc; mc != nil { +if err := mc.error(); err != nil { +return err +} - // Fetch next row from stream - return rows.readRow(dest) - } - return io.EOF +// Fetch next row from stream +return rows.readRow(dest) +} +return io.EOF } func (rows *textRows) NextResultSet() (err error) { - resLen, err := rows.nextNotEmptyResultSet() - if err != nil { - return err - } +resLen, err := rows.nextNotEmptyResultSet() +if err != nil { +return err +} - rows.rs.columns, err = rows.mc.readColumns(resLen, nil) - return err +rows.rs.columns, err = rows.mc.readColumns(resLen, nil) +return err } func (rows *textRows) Next(dest []driver.Value) error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } - - // Fetch next row from stream - return rows.readRow(dest) - } - return io.EOF +if mc := rows.mc; mc != nil { +if err := mc.error(); err != nil { +return err +} + +// Fetch next row from stream +return rows.readRow(dest) +} +return io.EOF } diff --git a/rows_before_go127.go b/rows_before_go127.go deleted file mode 100644 index c034a1cf..00000000 --- a/rows_before_go127.go +++ /dev/null @@ -1,227 +0,0 @@ -// Go MySQL Driver - A MySQL-Driver for Go's database/sql package -// -// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at http://mozilla.org/MPL/2.0/. - -//go:build !go1.27 - -package mysql - -import ( - "database/sql/driver" - "io" - "math" - "reflect" -) - -type resultSet struct { - columns []mysqlField - columnNames []string - done bool -} - -type mysqlRows struct { - mc *mysqlConn - rs resultSet - finish func() -} - -type binaryRows struct { - mysqlRows -} - -type textRows struct { - mysqlRows -} - -func (rows *mysqlRows) Columns() []string { - if rows.rs.columnNames != nil { - return rows.rs.columnNames - } - - columns := make([]string, len(rows.rs.columns)) - if rows.mc != nil && rows.mc.cfg.ColumnsWithAlias { - for i := range columns { - if tableName := rows.rs.columns[i].tableName; len(tableName) > 0 { - columns[i] = tableName + "." + rows.rs.columns[i].name - } else { - columns[i] = rows.rs.columns[i].name - } - } - } else { - for i := range columns { - columns[i] = rows.rs.columns[i].name - } - } - - rows.rs.columnNames = columns - return columns -} - -func (rows *mysqlRows) ColumnTypeDatabaseTypeName(i int) string { - return rows.rs.columns[i].typeDatabaseName() -} - -// func (rows *mysqlRows) ColumnTypeLength(i int) (length int64, ok bool) { -// return int64(rows.rs.columns[i].length), true -// } - -func (rows *mysqlRows) ColumnTypeNullable(i int) (nullable, ok bool) { - return rows.rs.columns[i].flags&flagNotNULL == 0, true -} - -func (rows *mysqlRows) ColumnTypePrecisionScale(i int) (int64, int64, bool) { - column := rows.rs.columns[i] - decimals := int64(column.decimals) - - switch column.fieldType { - case fieldTypeDecimal, fieldTypeNewDecimal: - if decimals > 0 { - return int64(column.length) - 2, decimals, true - } - return int64(column.length) - 1, decimals, true - case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeTime: - return decimals, decimals, true - case fieldTypeFloat, fieldTypeDouble: - if decimals == 0x1f { - return math.MaxInt64, math.MaxInt64, true - } - return math.MaxInt64, decimals, true - } - - return 0, 0, false -} - -func (rows *mysqlRows) ColumnTypeScanType(i int) reflect.Type { - return rows.rs.columns[i].scanType() -} - -func (rows *mysqlRows) Close() (err error) { - if f := rows.finish; f != nil { - f() - rows.finish = nil - } - - mc := rows.mc - if mc == nil { - return nil - } - if err := mc.error(); err != nil { - return err - } - - // Remove unread packets from stream - if !rows.rs.done { - err = mc.skipRows() - } - if err == nil { - handleOk := mc.clearResult() - if err = handleOk.discardResults(); err != nil { - return err - } - } - - rows.mc = nil - return err -} - -func (rows *mysqlRows) HasNextResultSet() (b bool) { - if rows.mc == nil { - return false - } - return rows.mc.status&statusMoreResultsExists != 0 -} - -func (rows *mysqlRows) nextResultSet() (int, error) { - if rows.mc == nil { - return 0, io.EOF - } - if err := rows.mc.error(); err != nil { - return 0, err - } - - // Remove unread packets from stream - if !rows.rs.done { - if err := rows.mc.skipRows(); err != nil { - return 0, err - } - rows.rs.done = true - } - - if !rows.HasNextResultSet() { - rows.mc = nil - return 0, io.EOF - } - rows.rs = resultSet{} - // rows.mc.affectedRows and rows.mc.insertIds accumulate on each call to - // nextResultSet. - resLen, _, err := rows.mc.resultUnchanged().readResultSetHeaderPacket() - if err != nil { - // Clean up about multi-results flag - rows.rs.done = true - rows.mc.status = rows.mc.status & (^statusMoreResultsExists) - } - return resLen, err -} - -func (rows *mysqlRows) nextNotEmptyResultSet() (int, error) { - for { - resLen, err := rows.nextResultSet() - if err != nil { - return 0, err - } - - if resLen > 0 { - return resLen, nil - } - - rows.rs.done = true - } -} - -func (rows *binaryRows) NextResultSet() error { - resLen, err := rows.nextNotEmptyResultSet() - if err != nil { - return err - } - - rows.rs.columns, err = rows.mc.readColumns(resLen, nil) - return err -} - -func (rows *binaryRows) Next(dest []driver.Value) error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } - - // Fetch next row from stream - return rows.readRow(dest) - } - return io.EOF -} - -func (rows *textRows) NextResultSet() (err error) { - resLen, err := rows.nextNotEmptyResultSet() - if err != nil { - return err - } - - rows.rs.columns, err = rows.mc.readColumns(resLen, nil) - return err -} - -func (rows *textRows) Next(dest []driver.Value) error { - if mc := rows.mc; mc != nil { - if err := mc.error(); err != nil { - return err - } - - // Fetch next row from stream - return rows.readRow(dest) - } - return io.EOF -} diff --git a/rows_column_scanner.go b/rows_column_scanner.go new file mode 100644 index 00000000..1ded6f97 --- /dev/null +++ b/rows_column_scanner.go @@ -0,0 +1,383 @@ +// Go MySQL Driver - A MySQL-Driver for Go's database/sql package +// +// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build go1.27 + +package mysql + +import ( + "database/sql" + "database/sql/driver" + "encoding/binary" + "fmt" + "io" + "math" + "strconv" +) + +// NextRow implements driver.RowsColumnScanner. +// It reads the next row from the MySQL text protocol and stores raw per-column +// byte slices (subslices of the packet buffer) in rows.rawCols, without +// allocating any intermediate []driver.Value. +func (rows *textRows) NextRow() error { + mc := rows.mc + if mc == nil { + return io.EOF + } + if err := mc.error(); err != nil { + return err + } + if rows.rs.done { + return io.EOF + } + + data, err := mc.readPacket() + if err != nil { + return err + } + + // EOF Packet. + // Text row packets may start with a LengthEncodedString; 0xFE can mean a + // string larger than 0xffffff, so we bound-check the length. + // https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html#sect_protocol_basic_dt_int_le + if data[0] == iEOF && len(data) <= 0xffffff { + if mc.capabilities&clientDeprecateEOF == 0 { + // Deprecated EOF packet + mc.status = readStatus(data[3:]) + } else { + // OK packet with 0xFE header + _, _, n := readLengthEncodedInteger(data[1:]) + _, _, m := readLengthEncodedInteger(data[1+n:]) + mc.status = readStatus(data[1+n+m:]) + } + rows.rs.done = true + if !rows.HasNextResultSet() { + rows.mc = nil + } + return io.EOF + } + if data[0] == iERR { + rows.mc = nil + return mc.handleErrorPacket(data) + } + + // Extract raw per-column byte slices. Each slice is a subslice of the + // packet buffer — no extra allocation per column. + ncols := len(rows.rs.columns) + if len(rows.rawCols) != ncols { + rows.rawCols = make([][]byte, ncols) + } + + var n int + var isNull bool + pos := 0 + for i := range rows.rawCols { + rows.rawCols[i], isNull, n, err = readLengthEncodedString(data[pos:]) + pos += n + if err != nil { + return err + } + if isNull { + rows.rawCols[i] = nil + } + } + return nil +} + +// ScanColumn implements driver.RowsColumnScanner. +// It converts the raw text-protocol bytes for column i directly into dest, +// without any intermediate []driver.Value allocation. +func (rows *textRows) ScanColumn(scanCtx driver.ScanContext, i int, dest any) error { + if i < 0 || i >= len(rows.rawCols) { + return fmt.Errorf("mysql: column index %d out of range [0, %d)", i, len(rows.rawCols)) + } + raw := rows.rawCols[i] + if raw == nil { + return sql.ConvertAssign(scanCtx, dest, nil) + } + + // Fast paths for byte destinations: avoid an intermediate string/value. + switch d := dest.(type) { + case *[]byte: + *d = append((*d)[:0], raw...) + return nil + case *sql.RawBytes: + *d = raw // zero-copy; caller must not retain beyond the next Next/NextRow call + return nil + } + + // Parse raw bytes to the canonical driver.Value for this column type, + // then delegate the final assignment to sql.ConvertAssign. + col := rows.rs.columns[i] + var val driver.Value + var err error + switch col.fieldType { + case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeDate, fieldTypeNewDate: + if rows.mc.parseTime { + val, err = parseDateTime(raw, rows.mc.cfg.Loc) + } else { + val = string(raw) + } + case fieldTypeTiny, fieldTypeShort, fieldTypeInt24, fieldTypeYear, fieldTypeLong: + val, err = strconv.ParseInt(string(raw), 10, 64) + case fieldTypeLongLong: + if col.flags&flagUnsigned != 0 { + val, err = strconv.ParseUint(string(raw), 10, 64) + } else { + val, err = strconv.ParseInt(string(raw), 10, 64) + } + case fieldTypeFloat: + var f float64 + f, err = strconv.ParseFloat(string(raw), 32) + val = float32(f) + case fieldTypeDouble: + val, err = strconv.ParseFloat(string(raw), 64) + default: + val = string(raw) + } + if err != nil { + return err + } + return sql.ConvertAssign(scanCtx, dest, val) +} + +// NextRow implements driver.RowsColumnScanner. +// It reads the next row from the MySQL binary protocol and stores raw per-column +// byte slices (subslices of the packet buffer) in rows.rawCols, without +// allocating any intermediate []driver.Value. +func (rows *binaryRows) NextRow() error { + mc := rows.mc + if mc == nil { + return io.EOF + } + if err := mc.error(); err != nil { + return err + } + + data, err := mc.readPacket() + if err != nil { + return err + } + + // Packet indicator [1 byte]. + if data[0] != iOK { + if data[0] == iEOF { + if mc.capabilities&clientDeprecateEOF == 0 { + // EOF packet + mc.status = readStatus(data[3:]) + } else { + // OK packet with 0xFE header + _, _, n := readLengthEncodedInteger(data[1:]) + _, _, m := readLengthEncodedInteger(data[1+n:]) + mc.status = readStatus(data[1+n+m:]) + } + rows.rs.done = true + if !rows.HasNextResultSet() { + rows.mc = nil + } + return io.EOF + } + rows.mc = nil + return mc.handleErrorPacket(data) + } + + ncols := len(rows.rs.columns) + if len(rows.rawCols) != ncols { + rows.rawCols = make([][]byte, ncols) + } + + // NULL-bitmap: ceil((ncols + 2) / 8) bytes, starting at data[1]. + // The binary protocol reserves bits 0 and 1, so column i maps to bit i+2. + pos := 1 + (ncols+7+2)>>3 + nullMask := data[1:pos] + + for i := range rows.rawCols { + // Column i is NULL when bit i+2 of the null bitmap is set. + if ((nullMask[(i+2)>>3] >> uint((i+2)&7)) & 1) == 1 { + rows.rawCols[i] = nil + continue + } + + switch rows.rs.columns[i].fieldType { + case fieldTypeNULL: + rows.rawCols[i] = nil + + case fieldTypeTiny: + rows.rawCols[i] = data[pos : pos+1] + pos++ + + case fieldTypeShort, fieldTypeYear: + rows.rawCols[i] = data[pos : pos+2] + pos += 2 + + case fieldTypeInt24, fieldTypeLong: + rows.rawCols[i] = data[pos : pos+4] + pos += 4 + + case fieldTypeLongLong: + rows.rawCols[i] = data[pos : pos+8] + pos += 8 + + case fieldTypeFloat: + rows.rawCols[i] = data[pos : pos+4] + pos += 4 + + case fieldTypeDouble: + rows.rawCols[i] = data[pos : pos+8] + pos += 8 + + case fieldTypeDecimal, fieldTypeNewDecimal, fieldTypeVarChar, + fieldTypeBit, fieldTypeEnum, fieldTypeSet, fieldTypeTinyBLOB, + fieldTypeMediumBLOB, fieldTypeLongBLOB, fieldTypeBLOB, + fieldTypeVarString, fieldTypeString, fieldTypeGeometry, fieldTypeJSON, + fieldTypeVector: + var num uint64 + var isNull bool + var n int + num, isNull, n = readLengthEncodedInteger(data[pos:]) + pos += n + if isNull { + rows.rawCols[i] = nil + } else { + rows.rawCols[i] = data[pos : pos+int(num)] + pos += int(num) + } + + case fieldTypeDate, fieldTypeNewDate, fieldTypeTime, + fieldTypeTimestamp, fieldTypeDateTime: + var num uint64 + var isNull bool + var n int + num, isNull, n = readLengthEncodedInteger(data[pos:]) + pos += n + if isNull { + rows.rawCols[i] = nil + } else { + rows.rawCols[i] = data[pos : pos+int(num)] + pos += int(num) + } + + default: + return fmt.Errorf("unknown field type %d", rows.rs.columns[i].fieldType) + } + } + return nil +} + +// ScanColumn implements driver.RowsColumnScanner. +// It converts the raw binary-protocol bytes for column i directly into dest, +// without any intermediate []driver.Value allocation. +func (rows *binaryRows) ScanColumn(scanCtx driver.ScanContext, i int, dest any) error { + if i < 0 || i >= len(rows.rawCols) { + return fmt.Errorf("mysql: column index %d out of range [0, %d)", i, len(rows.rawCols)) + } + raw := rows.rawCols[i] + if raw == nil { + return sql.ConvertAssign(scanCtx, dest, nil) + } + + col := rows.rs.columns[i] + switch col.fieldType { + case fieldTypeTiny: + if col.flags&flagUnsigned != 0 { + return sql.ConvertAssign(scanCtx, dest, int64(raw[0])) + } + return sql.ConvertAssign(scanCtx, dest, int64(int8(raw[0]))) + + case fieldTypeShort, fieldTypeYear: + if col.flags&flagUnsigned != 0 { + return sql.ConvertAssign(scanCtx, dest, int64(binary.LittleEndian.Uint16(raw))) + } + return sql.ConvertAssign(scanCtx, dest, int64(int16(binary.LittleEndian.Uint16(raw)))) + + case fieldTypeInt24, fieldTypeLong: + if col.flags&flagUnsigned != 0 { + return sql.ConvertAssign(scanCtx, dest, int64(binary.LittleEndian.Uint32(raw))) + } + return sql.ConvertAssign(scanCtx, dest, int64(int32(binary.LittleEndian.Uint32(raw)))) + + case fieldTypeLongLong: + if col.flags&flagUnsigned != 0 { + val := binary.LittleEndian.Uint64(raw) + if val > math.MaxInt64 { + // Exceeds int64 range: represent as decimal string for compatibility. + return sql.ConvertAssign(scanCtx, dest, uint64ToString(val)) + } + return sql.ConvertAssign(scanCtx, dest, int64(val)) + } + return sql.ConvertAssign(scanCtx, dest, int64(binary.LittleEndian.Uint64(raw))) + + case fieldTypeFloat: + return sql.ConvertAssign(scanCtx, dest, math.Float32frombits(binary.LittleEndian.Uint32(raw))) + + case fieldTypeDouble: + return sql.ConvertAssign(scanCtx, dest, math.Float64frombits(binary.LittleEndian.Uint64(raw))) + + case fieldTypeDecimal, fieldTypeNewDecimal, fieldTypeVarChar, + fieldTypeBit, fieldTypeEnum, fieldTypeSet, fieldTypeTinyBLOB, + fieldTypeMediumBLOB, fieldTypeLongBLOB, fieldTypeBLOB, + fieldTypeVarString, fieldTypeString, fieldTypeGeometry, fieldTypeJSON, + fieldTypeVector: + // Fast paths for byte destinations. + switch d := dest.(type) { + case *[]byte: + *d = append((*d)[:0], raw...) + return nil + case *sql.RawBytes: + *d = raw // zero-copy; caller must not retain beyond the next Next/NextRow call + return nil + } + return sql.ConvertAssign(scanCtx, dest, raw) + + case fieldTypeDate, fieldTypeNewDate, fieldTypeTimestamp, fieldTypeDateTime: + if rows.mc.parseTime { + val, err := parseBinaryDateTime(uint64(len(raw)), raw, rows.mc.cfg.Loc) + if err != nil { + return err + } + return sql.ConvertAssign(scanCtx, dest, val) + } + var dstlen uint8 + if col.fieldType == fieldTypeDate || col.fieldType == fieldTypeNewDate { + dstlen = 10 + } else { + switch decimals := col.decimals; decimals { + case 0x00, 0x1f: + dstlen = 19 + case 1, 2, 3, 4, 5, 6: + dstlen = 19 + 1 + decimals + default: + return fmt.Errorf("protocol error, illegal decimals value %d", col.decimals) + } + } + val, err := formatBinaryDateTime(raw, dstlen) + if err != nil { + return err + } + return sql.ConvertAssign(scanCtx, dest, val) + + case fieldTypeTime: + var dstlen uint8 + switch decimals := col.decimals; decimals { + case 0x00, 0x1f: + dstlen = 8 + case 1, 2, 3, 4, 5, 6: + dstlen = 8 + 1 + decimals + default: + return fmt.Errorf("protocol error, illegal decimals value %d", col.decimals) + } + val, err := formatBinaryTime(raw, dstlen) + if err != nil { + return err + } + return sql.ConvertAssign(scanCtx, dest, val) + + default: + return fmt.Errorf("unknown field type %d", col.fieldType) + } +}