database/sql/driver: allow driver to entirely override Scan
要約
概要
database/sql/driver パッケージに RowsColumnScanner インターフェースと sql.ConvertAssign 関数を追加し、SQLドライバーが行スキャン処理を完全に制御できるようにするProposalです。これにより、ドライバーは driver.Value を経由せずにユーザー提供の任意の型へ直接値をスキャンできるようになります。
ステータス変更
active → likely_accept
2026年4月22日のProposal Review MeetingでAustin Clements(@aclements)が likely_accept と判断しました。2024年7月に一度acceptedとなりGo 1.26へ向けて実装が進みましたが、APIの後方互換性の問題が発覚して差し戻し(roll-back)となりました。その後Neil Brownell(@neild)が NextRow() メソッドを追加した改訂版APIを提案・実装(CL 766701)し、実ドライバー(go-sqlite3)での有意なアロケーション削減が確認されたため今回の承認に至りました。
技術的背景
現状の問題点
database/sql の Rows.Scan は内部的に以下の2段階の処理を行います。
- ドライバーの
Next(dest []driver.Value)を呼び出し、全カラムの値をdriver.Value(interface{})にボックス化して取得する convertAssignでdriver.Valueからユーザー指定の型へ変換する
この設計には2つの根本的な制約があります。
制約1: 中間アロケーション
int64やfloat64などのプリミティブ型も毎回ヒープにボックス化されます。100万行・10カラムのクエリでは最低1,000万回の不要なアロケーションが発生し、GCへの圧力が高まります。
制約2: 型サポートの限界
driver.Valueとして表現できない型(PostgreSQLの配列[]int64、UUID、レンジ型など)をスキャンターゲットとして使えません。map[string]stringや[]int64へ直接スキャンする場合、ドライバーはいったん文字列にエンコードし、ユーザー側でデコードするワークアラウンドが必要でした。
pgx(PostgreSQL用Goドライバー)の場合、クエリ引数についてはNamedValueCheckerインターフェースで独自処理を実装できますが、スキャン側には同等の仕組みがありませんでした。
// Before: ワークアラウンドが必要
m := pgtype.NewMap()
var a []int64
err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(m.SQLScanner(&a))
// After: 直接スキャン可能に
var a []int64
err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(&a)
提案された解決策
database/sql/driver パッケージに新しいオプションインターフェース RowsColumnScanner を追加します。また、ドライバーが標準変換処理を再利用できるよう sql.ConvertAssign をエクスポートします。
package driver
// RowsColumnScanner は [Rows] インターフェースを拡張し、ドライバーが
// スキャン先へ直接値をコピーできるようにします。
// RowsColumnScanner は [Rows.Next] メソッドを置き換えます。
type RowsColumnScanner interface {
Rows
// NextRow は次の行に進みます。行がない場合は io.EOF を返します。
NextRow() error
// ScanColumn は現在の行の index 番目のカラムを dest にコピーします。
// ドライバーは sql.ConvertAssign を使って driver.Value を dest に代入できます。
ScanColumn(index int, dest any) error
}
package sql
// ConvertAssign は src の値を dest が指す先にコピーします。
// ドライバー実装での使用を意図しています。
func ConvertAssign(dest any, src driver.Value) error
重要な設計変更点(差し戻し後の改訂): 当初の実装では RowsColumnScanner が Next メソッドを「使わなくてよい」ものとして扱っていましたが、これは Go 1.25以前との後方互換性を破壊します。改訂版では NextRow() を別メソッドとして導入することで、Next の契約を維持しつつ新機能を提供します。
これによって何ができるようになるか
コード例
// Before: driver.Value経由の2段階変換(アロケーションが発生)
// ドライバーのNext()実装
func (r *pgRows) Next(dest []driver.Value) error {
// 全カラムをdriver.Valueにボックス化(アロケーション発生)
dest[0] = int64(42) // ヒープにエスケープ
dest[1] = "some text" // ヒープにエスケープ
// []int64型の配列は直接表現できないため文字列にエンコード
dest[2] = "{1,2,3}" // 後でパースが必要
return nil
}
// After: RowsColumnScannerを実装したドライバー
func (r *pgRows) NextRow() error {
return r.rowReader.Next() // 行を進めるだけ(アロケーションなし)
}
func (r *pgRows) ScanColumn(index int, dest any) error {
switch d := dest.(type) {
case *int64:
*d = r.rowReader.Int64(index) // ボックス化なしで直接代入
return nil
case *[]int64:
// PostgreSQLバイナリ形式から直接デコード
return r.rowReader.DecodeArray(index, d)
default:
// 標準変換にフォールバック
return sql.ConvertAssign(dest, r.rowReader.Value(index))
}
}
主なユースケース
- PostgreSQL配列型の直接スキャン:
[]int64、[]string、[]uuid.UUIDなどをworkaroundなしでスキャン可能 - 時系列データベースのパフォーマンス改善: 数百万行のクエリでGCプレッシャーを大幅削減(go-sqlite3での実測でDB_ReadPostAndMaybeWriteCommentが11.5%高速化)
- バイナリプロトコルの活用: PostgreSQLのバイナリフォーマットを使えるため、テキストパース(特にUUID、配列、レンジ型)を回避してパフォーマンスが向上
- カスタム型の透過的サポート:
sql.Scannerを実装していない標準Goの型(map[string]stringなど)へのスキャンをドライバー側で実現可能
議論のハイライト
- Go 1.26での差し戻し(2025年12月): 当初の
RowsColumnScannerはNextメソッドが dest を埋めなくてよい前提で設計されていたが、これは Go 1.25以前との後方互換性を破壊するため @aclements の判断でroll-backされた。ドライバーが go1.26でのみ動作し古いGoと非互換になるリスクがあった NextRow()メソッドの追加が解決策: @neild が「行の前進」と「値のスキャン」を分離するNextRow()を提案。これによりNextの契約を壊さずにドライバーがNextをstubにできる設計を実現したsql.ConvertAssignのエクスポート: ドライバーが標準の型変換ロジックをフォールバックとして利用できるよう、内部関数convertAssignを公開APIとして提供する方針が決定。ただしdriver.Valueを経由するためアロケーションは発生する- アロケーション最適化の限界:
ConvertAssign(dest any, src driver.Value)ではsrcがヒープにエスケープするため、完全なゼロアロケーションは実現できない。ドライバーが型スイッチで特殊化することで回避可能 *sql.RawBytesと*sql.Rowsの特殊ケース: これらの型はドライバー側から「不透明」なため、destの型として判別できない。@aclements と @neild が設計を検討中であり、この点が最後の技術的課題として残っている