Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10817] ファイルの概要

このコミットは、Go言語のexp/sqlパッケージ(現在のdatabase/sqlパッケージの前身)において、Rowsオブジェクトからカラム名を取得するためのColumns()メソッドを追加し、同時にパッケージ名がエラーメッセージ内で誤ってdbと表示されていた箇所をsqlに修正するものです。

コミット

commit ea51dd23b4029649427d3bcb681879808923805b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Dec 15 10:14:57 2011 -0800

    sql: add Rows.Columns
    
    Also, fix package name in error messages.
    
    Fixes #2453
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5483088

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ea51dd23b4029649427d3bcb681879808923805b

元コミット内容

sql: add Rows.Columns

Also, fix package name in error messages.

Fixes #2453

R=rsc
CC=golang-dev
https://golang.org/cl/5483088

変更の背景

このコミットの主な背景は、Go言語のdatabase/sqlパッケージ(当時はexp/sql)において、クエリ結果の行(Rows)からカラム名を取得する標準的な方法が提供されていなかったことです。これは、Issue #2453「database/sql: does not allow "exporting" row data」で報告された問題に対応するものです。

データベースからデータを取得する際、アプリケーションはしばしば、結果セットに含まれるカラムのメタデータ、特にカラム名を知る必要があります。例えば、結果を動的にマップに変換する場合や、汎用的なデータ表示コンポーネントを作成する場合などです。Rowsインターフェースには、行のデータをスキャンするScanメソッドはありましたが、カラム名を取得する直接的なメソッドがありませんでした。

この不足は、開発者がデータベースのスキーマに依存しない、より柔軟なコードを書くことを困難にしていました。Rows.Columns()メソッドの追加は、このギャップを埋め、database/sqlパッケージの機能性を向上させることを目的としています。

また、このコミットでは、エラーメッセージ内でパッケージ名が誤ってdbと表示されていた箇所を、正しいパッケージ名であるsqlに修正しています。これは、ユーザーがエラーメッセージを見た際に、どのパッケージからのエラーであるかを正確に理解できるようにするための、細かながらも重要な改善です。

前提知識の解説

Go言語のdatabase/sqlパッケージ

database/sqlパッケージは、Go言語における標準的なSQLデータベース操作のためのインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを実装しているわけではなく、データベースドライバが実装すべき共通のインターフェース(driver.Driver, driver.Conn, driver.Stmt, driver.Rowsなど)を定義しています。これにより、Goアプリケーションは、使用するデータベースの種類に関わらず、統一されたAPIでデータベースを操作できます。

主要な型と概念:

  • DB: データベースへの接続プールを表すハンドルです。Open関数で作成され、複数のゴルーチンから安全に利用できます。
  • Stmt: プリペアドステートメントを表します。SQLインジェクション攻撃を防ぎ、クエリのパフォーマンスを向上させます。
  • Rows: Queryメソッドの実行結果として返される、結果セットの行を表します。通常、Next()で次の行に進み、Scan()でその行のデータをGoの変数に読み込みます。
  • driver.Rowsインターフェース: データベースドライバが実装するべき行インターフェースです。このインターフェースには、Columns()メソッドが含まれており、ドライバが提供するカラム名を取得できるようになっています。

Rowsオブジェクトとデータ取得

DB.Query()またはStmt.Query()を呼び出すと、*sql.Rows型のオブジェクトが返されます。このオブジェクトは、データベースから取得された結果セットのイテレータとして機能します。

典型的な使用パターンは以下の通りです。

rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
    log.Fatal(err)
}
defer rows.Close() // 忘れずにクローズする

for rows.Next() {
    var id int
    var name string
    if err := rows.Scan(&id, &name); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("id: %d, name: %s\n", id, name)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

このコミット以前は、idnameといったカラム名を動的に取得する方法が、*sql.Rowsオブジェクトの公開APIにはありませんでした。

エラーメッセージのプレフィックス

Goのエラーメッセージは、通常、エラーが発生したパッケージ名やコンポーネント名で始まる慣習があります。これにより、エラーメッセージを見ただけで、どの部分で問題が発生したのかを素早く特定できます。このコミットでは、exp/sqlパッケージ内で生成されるエラーメッセージのプレフィックスが、誤ってdb:となっていたものを、正しいsql:に修正しています。

技術的詳細

このコミットは、exp/sqlパッケージのRows型にColumns()メソッドを追加することで、クエリ結果のカラム名を取得する機能を提供します。

Rows.Columns()メソッドの追加

Rows型に以下のメソッドが追加されました。

// Columns returns the column names.
// Columns returns an error if the rows are closed, or if the rows
// are from QueryRow and there was a deferred error.
func (rs *Rows) Columns() ([]string, error) {
	if rs.closed {
		return nil, errors.New("sql: Rows are closed")
	}
	if rs.rowsi == nil {
		return nil, errors.New("sql: no Rows available")
	}
	return rs.rowsi.Columns(), nil
}

このメソッドは、内部的にdriver.Rowsインターフェースが持つColumns()メソッドを呼び出しています。driver.Rowsはデータベースドライバが実装するインターフェースであり、ドライバが実際にデータベースから取得したカラム名を提供します。

Rows.Columns()メソッドは、以下のエラーケースを考慮しています。

  • rs.closed: Rowsオブジェクトが既にクローズされている場合。
  • rs.rowsi == nil: Rowsオブジェクトが有効な結果セットを持っていない場合(例: QueryRowが結果を返さなかった場合など)。

これらのチェックにより、Columns()メソッドが安全に呼び出され、適切なエラーハンドリングが行われるようになっています。

エラーメッセージのプレフィックス修正

src/pkg/exp/sql/sql.goファイル内で、エラーメッセージのプレフィックスがdb:からsql:に一括して修正されました。これは、exp/sqlパッケージが最終的にdatabase/sqlとして標準ライブラリに組み込まれることを考慮し、パッケージ名とエラーメッセージの整合性を保つための変更です。

修正されたエラーメッセージの例:

  • panic("db: Register driver is nil") -> panic("sql: Register driver is nil")
  • panic("db: Register called twice for driver " + name) -> panic("sql: Register called twice for driver " + name)
  • var ErrNoRows = errors.New("db: no rows in result set") -> var ErrNoRows = errors.New("sql: no rows in result set")
  • return nil, fmt.Errorf("db: unknown driver %q (forgotten import?)", driverName) -> return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
  • return nil, fmt.Errorf("db: expected %d arguments, got %d", want, len(args)) -> return nil, fmt.Errorf("sql: expected %d arguments, got %d", want, len(args))
  • return nil, errors.New("db: statement is closed") -> return nil, errors.New("sql: statement is closed")
  • return nil, fmt.Errorf("db: statement expects %d inputs; got %d", si.NumInput(), len(args)) -> return nil, fmt.Errorf("sql: statement expects %d inputs; got %d", si.NumInput(), len(args))
  • return errors.New("db: Rows closed") -> return errors.New("sql: Rows closed")
  • return errors.New("db: Scan called without calling Next") -> return errors.New("sql: Scan called without calling Next")
  • return fmt.Errorf("db: expected %d destination arguments in Scan, not %d", len(rs.lastcols), len(dest)) -> return fmt.Errorf("sql: expected %d destination arguments in Scan, not %d", len(rs.lastcols), len(dest))
  • return fmt.Errorf("db: Scan error on column index %d: %v", i, err) -> return fmt.Errorf("sql: Scan error on column index %d: %v", i, err)

これらの修正は、エラーメッセージの一貫性と明確性を高めることに貢献しています。

コアとなるコードの変更箇所

src/pkg/exp/sql/sql.go

  • Rows型にColumns() ([]string, error)メソッドが追加されました。
  • ファイル全体で、エラーメッセージのプレフィックスがdb:からsql:に置換されました。

src/pkg/exp/sql/sql_test.go

  • TestRowsColumnsという新しいテスト関数が追加されました。このテストは、Rows.Columns()メソッドが正しくカラム名を返すことを検証します。
  • 既存のTestExec関数内のエラーメッセージの期待値が、db:からsql:に修正されました。

コアとなるコードの解説

Rows.Columns()の実装 (src/pkg/exp/sql/sql.go)

// Columns returns the column names.
// Columns returns an error if the rows are closed, or if the rows
// are from QueryRow and there was a deferred error.
func (rs *Rows) Columns() ([]string, error) {
	if rs.closed {
		return nil, errors.New("sql: Rows are closed")
	}
	if rs.rowsi == nil {
		return nil, errors.New("sql: no Rows available")
	}
	return rs.rowsi.Columns(), nil
}

このコードは、Rows型のレシーバrsに対してColumns()メソッドを定義しています。

  1. if rs.closed: Rowsオブジェクトが既にClose()されている場合、"sql: Rows are closed"というエラーを返します。これは、クローズされたリソースへのアクセスを防ぐためのガードです。
  2. if rs.rowsi == nil: rs.rowsiは、内部的にデータベースドライバが提供するdriver.Rowsインターフェースの実装を保持しています。これがnilの場合(例えば、QueryRowが結果を返さなかった場合など)、有効な行データがないため、"sql: no Rows available"というエラーを返します。
  3. return rs.rowsi.Columns(), nil: 上記のチェックを通過した場合、内部のdriver.RowsインターフェースのColumns()メソッドを呼び出し、その結果(カラム名のスライスとエラー)をそのまま返します。これにより、実際のカラム名取得のロジックは各データベースドライバに委ねられます。

TestRowsColumnsテストケース (src/pkg/exp/sql/sql_test.go)

func TestRowsColumns(t *testing.T) {
	db := newTestDB(t, "people")
	defer closeDB(t, db)
	rows, err := db.Query("SELECT|people|age,name|")
	if err != nil {
		t.Fatalf("Query: %v", err)
	}
	cols, err := rows.Columns()
	if err != nil {
		t.Fatalf("Columns: %v", err)
	}
	want := []string{"age", "name"}
	if !reflect.DeepEqual(cols, want) {
		t.Errorf("got %#v; want %#v", cols, want)
	}
}

このテストは、Rows.Columns()メソッドの基本的な機能を確認します。

  1. db := newTestDB(t, "people"): テスト用のデータベース接続を作成します。
  2. rows, err := db.Query("SELECT|people|age,name|"): agenameという2つのカラムを選択するクエリを実行します。このクエリ文字列は、テスト用のモックドライバが解釈できる形式であると推測されます。
  3. cols, err := rows.Columns(): 新しく追加されたColumns()メソッドを呼び出し、カラム名を取得します。
  4. want := []string{"age", "name"}: 期待されるカラム名のスライスを定義します。
  5. if !reflect.DeepEqual(cols, want): 取得したカラム名colsが期待値wantと完全に一致するかをreflect.DeepEqualを使って比較します。一致しない場合はエラーを報告します。

このテストにより、Rows.Columns()が正しく機能し、データベースドライバから返されたカラム名を正確に提供できることが保証されます。

エラーメッセージの修正 (src/pkg/exp/sql/sql.go および src/pkg/exp/sql/sql_test.go)

コード全体で、エラーメッセージの文字列リテラル内の"db:""sql:"に置換されています。これは単純な文字列置換ですが、Goの標準ライブラリにおけるエラーメッセージの慣習に合わせるための重要な変更です。これにより、ユーザーはエラーがdatabase/sqlパッケージから発生したものであることを直感的に理解できるようになります。

関連リンク

参考にした情報源リンク