メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

新機能性能改善

database/sql/driverにRowsColumnScannerを追加し、ドライバがdriver.Valueを経由せず直接スキャンできるようにする

database/sql/driver

この項目の注釈は AI により生成されており、誤りを含む場合があります。
使用例のコンパイル検証: 未検証(コンパイル確認未了)

概要

database/sql/driver パッケージに新しい RowsColumnScanner インターフェースが追加された。これを実装したドライバは、Rows.Next が行う driver.Value へのボックス化を経由せず、クエリ結果をユーザー提供の宛先へ直接スキャンできる。RowsColumnScannerRows.Next を置き換える設計で、行を進める NextRow とカラムをスキャンする ScanColumn を分離して提供する。あわせて、標準の型変換ロジックへフォールバックするための sql.ConvertAssign 関数も公開された。

導入経緯

PostgreSQLドライバ pgx の作者が、Issue #67546 で提案した。pgxは接続固有の情報(登録済みCodec、PostgreSQLの型、値のフォーマットなど)を使って柔軟な型変換を行うが、sql.Scannerdriver.Valuer の実装からはそれらの情報にアクセスできず、[]int64 のような driver.Value で表現できない型へ直接スキャンする手段がなかったことが動機。

議論タイムライン

  • 2024-07: 初回のProposal Review Meetingで、ScanColumn のみを持つ最初の設計(既存の Next と単純に併存させる案)が likely accept と判断された。
  • 2025-12: Go 1.26 リリース直前のAPI監査で、「RowsColumnScanner を実装したドライバが Nextdest を埋めない場合、旧バージョンの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氏が「行の前進」と「カラムのスキャン」を分離する設計を提案し、これが最終的に採用された。
  • ドライバが標準の型変換ロジックにフォールバックできるよう、内部関数だった convertAssignsql.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.RowsColumnScannerdriver.go で定義されており、Rows を埋め込んだ上で NextRow() errorScanColumn(scanCtx ScanContext, index int, dest any) error を追加で要求する。ScanContext 自体は同ファイルの449行目internal.ScanContext のエイリアスとして定義されており、フィールドが非公開のため呼び出し側からは中身を参照できない不透明な型になっている。実体は internal/sql.goScanContext{v any} で、NewScanContext/ScanContextValue を介して *sql.Rows を保持する。

database/sql 側では、sql.go の nextLockedrowsidriver.RowsColumnScanner を実装しているかを型アサーションで判定し、実装していれば Next の代わりに NextRow を呼ぶ。カラムのスキャンは scanLocked が担い、宛先ごとに internal.NewScanContext(rs) で現在の *Rows を包んだ scanCtx を生成してから ScanColumn を呼び出す。ドライバの ScanColumn 実装が sql.ConvertAssign にフォールバックする際は、この scanCtxそのまま渡すことで、*sql.Rows/*sql.RawBytes への代入時に必要な親 *Rows の紐付けが convertAssignRows に伝わる仕組みになっている。

関連リンク