database/sql/driverにRowsColumnScannerを追加し、ドライバがdriver.Valueを経由せず直接スキャンできるようにする
database/sql/driver
概要
database/sql/driver パッケージに新しい RowsColumnScanner インターフェースが追加された。これを実装したドライバは、Rows.Next が行う driver.Value へのボックス化を経由せず、クエリ結果をユーザー提供の宛先へ直接スキャンできる。RowsColumnScanner は Rows.Next を置き換える設計で、行を進める NextRow とカラムをスキャンする ScanColumn を分離して提供する。あわせて、標準の型変換ロジックへフォールバックするための sql.ConvertAssign 関数も公開された。
導入経緯
PostgreSQLドライバ pgx の作者が、Issue #67546 で提案した。pgxは接続固有の情報(登録済みCodec、PostgreSQLの型、値のフォーマットなど)を使って柔軟な型変換を行うが、sql.Scanner や driver.Valuer の実装からはそれらの情報にアクセスできず、[]int64 のような driver.Value で表現できない型へ直接スキャンする手段がなかったことが動機。
議論タイムライン
- 2024-07: 初回のProposal Review Meetingで、
ScanColumnのみを持つ最初の設計(既存のNextと単純に併存させる案)が likely accept と判断された。 - 2025-12: Go 1.26 リリース直前のAPI監査で、「
RowsColumnScannerを実装したドライバがNextのdestを埋めない場合、旧バージョンのGoとの後方互換性を破壊する」という問題が指摘され、Go 1.26 向けの変更がロールバックされた。 - 2026-04-22:
NextRow(行を進める)とScanColumn(カラムをスキャンする)を分離する再設計案が pgx と go-sqlite3 の両ドライバで実証され、Proposal Review Meetingで再度 likely accept と判断された。 - 2026-04-29: コンセンサスに変化なしとして正式に accepted となった。
議論のハイライト
- Go 1.26でのロールバック後、@neild氏が「行の前進」と「カラムのスキャン」を分離する設計を提案し、これが最終的に採用された。
- ドライバが標準の型変換ロジックにフォールバックできるよう、内部関数だった
convertAssignがsql.ConvertAssignとして公開されることになった。 - pgxでの実測では、1000行スキャン時のアロケーション数が11,676回から6,011回(約48%減)、メモリ使用量が142,771Bから61,726B(約57%減)に改善したことが報告されている。
- accepted 後の実装段階で、
*sql.Rowsを宛先とするカーソルのスキャン(ネストしたクエリ結果)をどう扱うかが新たな課題として浮上した。@neild氏の分析により、ScanColumnに不透明なScanContext引数を追加し、それをsql.ConvertAssignへそのまま渡すことで、親の*sql.Rowsへの紐付けを従来どおり行えるようにする最終形に落ち着いた。
使用例
Before
package main
import (
"database/sql/driver"
"io"
)
// exampleRows implements driver.Rows using only the pre-Go 1.27 API.
// Every column value must be boxed into a driver.Value (here, int64
// escapes to the heap) before database/sql can convert it into the
// caller's destination.
type exampleRows struct {
i int
rows [][]int64
}
func (r *exampleRows) Columns() []string { return []string{"n"} }
func (r *exampleRows) Close() error { return nil }
func (r *exampleRows) Next(dest []driver.Value) error {
if r.i >= len(r.rows) {
return io.EOF
}
dest[0] = r.rows[r.i][0]
r.i++
return nil
}
var _ driver.Rows = (*exampleRows)(nil)
After
package main
import (
"database/sql"
"database/sql/driver"
"io"
)
// exampleColumnScanner additionally implements driver.RowsColumnScanner
// (Go 1.27+). NextRow only advances the cursor; ScanColumn assigns
// directly into dest, so an *int64 destination never gets boxed into a
// driver.Value at all. Next is kept only for drivers that must keep
// working with Go versions older than 1.27.
type exampleColumnScanner struct {
i int
rows [][]int64
}
func (r *exampleColumnScanner) Columns() []string { return []string{"n"} }
func (r *exampleColumnScanner) Close() error { return nil }
func (r *exampleColumnScanner) Next(dest []driver.Value) error {
if r.i >= len(r.rows) {
return io.EOF
}
dest[0] = r.rows[r.i][0]
r.i++
return nil
}
func (r *exampleColumnScanner) NextRow() error {
if r.i >= len(r.rows) {
return io.EOF
}
return nil
}
func (r *exampleColumnScanner) ScanColumn(scanCtx driver.ScanContext, index int, dest any) error {
switch d := dest.(type) {
case *int64:
*d = r.rows[r.i][index]
default:
if err := sql.ConvertAssign(scanCtx, dest, r.rows[r.i][index]); err != nil {
return err
}
}
if index == len(r.Columns())-1 {
r.i++
}
return nil
}
var _ driver.RowsColumnScanner = (*exampleColumnScanner)(nil)
移行時の注意
RowsColumnScanner はドライバ実装者向けのオプションインターフェースであり、database/sql を呼び出すだけのアプリケーションコードは変更不要。ドライバを実装している場合は次の点に注意する。
RowsColumnScannerを実装してもRows(Nextを含む)は引き続き実装しておく必要がある。Go 1.27以降ではNextは呼ばれなくなるが、Go 1.26以前との互換性を保つために必須。- 提案の議論過程で
ScanColumnのシグネチャは何度か変わっており(引数順序、NextRowの有無、ScanContext引数の有無)、Issueのコメントを見て古い設計を参考に実装していた場合は、最終形ScanColumn(scanCtx driver.ScanContext, index int, dest any) errorに合わせて更新が必要。 *sql.RawBytesや*sql.Rowsを宛先とするスキャンをサポートする場合は、ScanColumnが受け取ったscanCtxをそのままsql.ConvertAssignに渡すことで、database/sql側の状態(バッファの使い回しやカーソルの親子関係)と整合させる必要がある。
実装解説
database/sql/driver.RowsColumnScanner は driver.go で定義されており、Rows を埋め込んだ上で NextRow() error と ScanColumn(scanCtx ScanContext, index int, dest any) error を追加で要求する。ScanContext 自体は同ファイルの449行目で internal.ScanContext のエイリアスとして定義されており、フィールドが非公開のため呼び出し側からは中身を参照できない不透明な型になっている。実体は internal/sql.go の ScanContext{v any} で、NewScanContext/ScanContextValue を介して *sql.Rows を保持する。
database/sql 側では、sql.go の nextLocked が rowsi が driver.RowsColumnScanner を実装しているかを型アサーションで判定し、実装していれば Next の代わりに NextRow を呼ぶ。カラムのスキャンは scanLocked が担い、宛先ごとに internal.NewScanContext(rs) で現在の *Rows を包んだ scanCtx を生成してから ScanColumn を呼び出す。ドライバの ScanColumn 実装が sql.ConvertAssign にフォールバックする際は、この scanCtx をそのまま渡すことで、*sql.Rows/*sql.RawBytes への代入時に必要な親 *Rows の紐付けが convertAssignRows に伝わる仕組みになっている。