[インデックス 10470] ファイルの概要
このコミットは、Go言語の実験的なexp/sqlパッケージにおけるデータベースドライバのドキュメントとテストを改善することを目的としています。機能的な変更は含まれておらず、主にドライバ開発者向けのインターフェースの振る舞いを明確にし、その振る舞いを検証するためのテストを追加しています。具体的には、driver.StmtインターフェースのCloseメソッドとNumInputメソッドに関するドキュメントが詳細化され、それらの期待される動作を検証するためのテストケースが追加されています。
コミット
commit 750d0e33fbb6d04e46fec6864b02b83798125320
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Sun Nov 20 14:56:49 2011 -0500
sql: more driver docs & tests; no functional changes
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5415055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/750d0e33fbb6d04e46fec6864b02b83798125320
元コミット内容
sql: more driver docs & tests; no functional changes
このコミットは、Goのexp/sqlパッケージにおいて、ドライバのドキュメントとテストをさらに追加するものであり、機能的な変更は含まれていません。
変更の背景
Go言語のdatabase/sqlパッケージ(当時はexp/sqlとして実験的に開発されていた)は、様々なデータベースシステムと連携するための汎用的なインターフェースを提供します。このインターフェースは、データベースドライバが実装すべき規約を定義しており、Goアプリケーションが特定のデータベースに依存することなくデータ操作を行えるようにします。
しかし、初期の段階では、ドライバインターフェースの特定のメソッド(特にStmt.Close()やStmt.NumInput())の振る舞いに関するドキュメントが不足していたり、曖昧な点がありました。これにより、ドライバ開発者が正しい実装を行う上で混乱が生じる可能性がありました。例えば、Stmt.Close()が呼び出された後も、そのステートメントから生成されたRowsオブジェクトが有効であるべきか、あるいはNumInput()がどのような値を返す場合にsqlパッケージが引数の数を検証するのか、といった点が不明確でした。
このコミットは、これらの曖昧さを解消し、ドライバ開発者がより堅牢で互換性のあるドライバを実装できるようにするために、ドキュメントを明確化し、その期待される振る舞いを検証するテストを追加することを目的としています。機能的な変更がないのは、既存のAPIのセマンティクスを変更するのではなく、そのセマンティクスを明確に定義することに焦点を当てているためです。
前提知識の解説
このコミットを理解するためには、Go言語のdatabase/sqlパッケージ(当時のexp/sql)の基本的な概念と、そのドライバインターフェースについて理解しておく必要があります。
-
database/sqlパッケージ (当時はexp/sql): Go標準ライブラリの一部であり、SQLデータベースとの対話のための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースの実装を含まず、データベース固有の操作は「ドライバ」と呼ばれる外部パッケージに委譲されます。これにより、アプリケーションコードは特定のデータベースシステムに依存することなく記述できます。 -
driverインターフェース:database/sqlパッケージがデータベースドライバに期待する一連のインターフェースを定義しています。ドライバはこれらのインターフェースを実装することで、database/sqlパッケージを通じて利用可能になります。主要なインターフェースには以下のようなものがあります。driver.Driver: データベースへの接続を開くためのインターフェース。driver.Conn: データベースへの単一の接続を表すインターフェース。トランザクションの開始やステートメントの準備などを行います。driver.Stmt: プリペアドステートメント(準備されたSQLクエリ)を表すインターフェース。SQLクエリの実行(Exec)や結果セットの取得(Query)を行います。driver.Rows: クエリの結果セットを表すインターフェース。結果セットの行をイテレートし、各列の値を読み取ります。driver.Result:Execメソッドの実行結果(影響を受けた行数や最後に挿入されたIDなど)を表すインターフェース。
-
driver.Stmtインターフェースのメソッド: このコミットで特に焦点が当てられているのは、driver.Stmtインターフェースの以下のメソッドです。Close() error: ステートメントを閉じます。このコミットでは、ステートメントを閉じても、そのステートメントから生成された未読のRowsオブジェクトが引き続き有効であるべきかどうかが明確化されています。これは、リソース管理と並行処理の観点から重要です。NumInput() int: プリペアドステートメントが期待するプレースホルダーパラメータの数を返します。ドライバがこの数を正確に知っている場合(0以上の値を返す場合)、sqlパッケージは呼び出し元からの引数の数を検証し、不一致があればエラーを返します。ドライバが数を特定できない場合(-1を返す場合)、sqlパッケージは検証を行わず、エラー処理はドライバに委ねられます。
-
テスト駆動開発とフェイクオブジェクト: このコミットでは、
fakedb_test.goというファイルが変更されています。これは、実際のデータベースに接続せずにdatabase/sqlパッケージの動作をテストするための「フェイク」データベースドライバの実装です。このようなフェイクオブジェクトは、単体テストや統合テストにおいて、外部依存性(この場合は実際のデータベース)をモック化し、テストの実行を高速化し、再現性を高めるために使用されます。
技術的詳細
このコミットは、主に以下の3つのファイルに変更を加えています。
-
src/pkg/exp/sql/driver/driver.go:Stmt.Close()メソッドのコメントが大幅に加筆されました。- ステートメントを閉じても、そのステートメントから生成された未処理のクエリ(
Rowsオブジェクトなど)が中断されるべきではないことが明記されました。 - 具体的な使用例として、「ステートメントを作成し、クエリを実行して
Rowsを取得し、ステートメントを閉じた後もRowsから読み取ることが有効である」というシナリオが示されています。 - もしステートメントを閉じることが実行中のクエリを無効にする場合、上記のシナリオが失敗することが指摘されています。
- 将来的に
sqlパッケージがステートメントの参照カウントをより賢く管理し、適切なタイミングで閉じる可能性についてのTODOコメントが追加されています。
- ステートメントを閉じても、そのステートメントから生成された未処理のクエリ(
Stmt.NumInput()メソッドのコメントも更新されました。NumInputが0以上の値を返す場合、sqlパッケージが呼び出し元からの引数の数を検証し、ExecまたはQueryメソッドが呼び出される前にエラーを返すことが明確化されました。NumInputが-1を返す場合(ドライバがプレースホルダーの数を知らない場合)、sqlパッケージは引数の数を検証せず、エラー処理はドライバに委ねられることが明記されました。
-
src/pkg/exp/sql/fakedb_test.go:fakeStmt構造体にclosedというブール型のフィールドが追加されました。これは、フェイクステートメントが閉じられたかどうかを追跡するためのものです。fakeStmt.Close()メソッドが、s.closed = trueを設定するように変更されました。errClosedという新しいエラー変数が定義されました。これは、閉じられたステートメントに対して操作が行われた場合に返されるエラーです。fakeStmt.Exec()およびfakeStmt.Query()メソッドに、s.closedがtrueの場合にerrClosedを返すチェックが追加されました。これにより、閉じられたステートメントに対する操作が正しくエラーを返すことをシミュレートできます。fakeConn.prepareSelect内のcolspecの処理に、空文字列のcolspecをスキップするif colspec == "" { continue }の行が追加されました。これはテストの準備ロジックの改善です。
-
src/pkg/exp/sql/sql_test.go:reflectパッケージがインポートされました。これは、新しいテストで構造体の比較を行うために使用されます。TestQueryという新しいテスト関数が追加されました。これは、db.Queryを使用して複数の行と列をフェッチし、rows.Scanで構造体にマッピングし、期待される結果とreflect.DeepEqualで比較する、より包括的なクエリテストです。TestQueryRowという新しいテスト関数が追加されました。これは、単一の行をクエリするdb.QueryRowのテストです。TestStatementErrorAfterCloseという重要な新しいテスト関数が追加されました。- このテストは、
db.Prepareでステートメントを準備し、すぐにstmt.Close()を呼び出します。 - その後、閉じられたステートメントに対して
stmt.QueryRowを呼び出し、Scanを試みます。 - 期待される動作は、この操作がエラーを返すことです。テストはエラーが返されない場合に失敗します。
- このテストは、
driver.Stmt.Close()の新しいドキュメントで述べられている「ステートメントを閉じても、そのステートメントから生成された未処理のクエリが中断されるべきではない」という原則とは異なり、閉じられたステートメント自体に対する後続の操作がエラーを返すことを検証しています。これは、ドライバが閉じられたステートメントを再利用しようとする試みを防ぐためのものです。
- このテストは、
これらの変更は、exp/sqlパッケージのインターフェースのセマンティクスを明確にし、ドライバ開発者がより正確な実装を行えるようにするための重要なステップです。
コアとなるコードの変更箇所
src/pkg/exp/sql/driver/driver.go
--- a/src/pkg/exp/sql/driver/driver.go
+++ b/src/pkg/exp/sql/driver/driver.go
@@ -94,12 +94,35 @@ type Result interface {
// used by multiple goroutines concurrently.
type Stmt interface {
// Close closes the statement.
+ //
+ // Closing a statement should not interrupt any outstanding
+ // query created from that statement. That is, the following
+ // order of operations is valid:
+ //
+ // * create a driver statement
+ // * call Query on statement, returning Rows
+ // * close the statement
+ // * read from Rows
+ //
+ // If closing a statement invalidates currently-running
+ // queries, the final step above will incorrectly fail.
+ //
+ // TODO(bradfitz): possibly remove the restriction above, if
+ // enough driver authors object and find it complicates their
+ // code too much. The sql package could be smarter about
+ // refcounting the statement and closing it at the appropriate
+ // time.
Close() error
// NumInput returns the number of placeholder parameters.
- // -1 means the driver doesn't know how to count the number of
- // placeholders, so we won't sanity check input here and instead let the
- // driver deal with errors.
+ //
+ // If NumInput returns >= 0, the sql package will sanity check
+ // argument counts from callers and return errors to the caller
+ // before the statement's Exec or Query methods are called.
+ //
+ // NumInput may also return -1, if the driver doesn't know
+ // its number of placeholders. In that case, the sql package
+ // will not sanity check Exec or Query argument counts.
NumInput() int
// Exec executes a query that doesn't return rows, such
src/pkg/exp/sql/fakedb_test.go
--- a/src/pkg/exp/sql/fakedb_test.go
+++ b/src/pkg/exp/sql/fakedb_test.go
@@ -90,6 +90,8 @@ type fakeStmt struct {
cmd string
table string
+ closed bool
+
colName []string // used by CREATE, INSERT, SELECT (selected columns)
colType []string // used by CREATE
colValue []interface{} // used by INSERT (mix of strings and "?" for bound params)
@@ -232,6 +234,9 @@ func (c *fakeConn) prepareSelect(stmt *fakeStmt, parts []string) (driver.Stmt, e
stmt.table = parts[0]
stmt.colName = strings.Split(parts[1], ",")
for n, colspec := range strings.Split(parts[2], ",") {
+ if colspec == "" {
+ continue
+ }
nameVal := strings.Split(colspec, "=")
if len(nameVal) != 2 {
return nil, errf("SELECT on table %q has invalid column spec of %q (index %d)", stmt.table, colspec, n)
@@ -342,10 +347,16 @@ func (s *fakeStmt) ColumnConverter(idx int) driver.ValueConverter {
}
func (s *fakeStmt) Close() error {
+ s.closed = true
return nil
}
+var errClosed = errors.New("fakedb: statement has been closed")
+
func (s *fakeStmt) Exec(args []interface{}) (driver.Result, error) {
+ if s.closed {
+ return nil, errClosed
+ }
err := checkSubsetTypes(args)
if err != nil {
return nil, err
@@ -405,6 +416,9 @@ func (s *fakeStmt) execInsert(args []interface{}) (driver.Result, error) {
}
func (s *fakeStmt) Query(args []interface{}) (driver.Rows, error) {
+ if s.closed {
+ return nil, errClosed
+ }
err := checkSubsetTypes(args)
if err != nil {
return nil, err
src/pkg/exp/sql/sql_test.go
--- a/src/pkg/exp/sql/sql_test.go
+++ b/src/pkg/exp/sql/sql_test.go
@@ -5,6 +5,7 @@
package sql
import (
+ "reflect"
"strings"
"testing"
)
@@ -22,7 +23,6 @@ func newTestDB(t *testing.T, name string) *DB {
\texec(t, db, "INSERT|people|name=Alice,age=?", 1)
\texec(t, db, "INSERT|people|name=Bob,age=?", 2)
\texec(t, db, "INSERT|people|name=Chris,age=?", 3)
-\
}
return db
}
@@ -42,6 +42,40 @@ func closeDB(t *testing.T, db *DB) {
}
func TestQuery(t *testing.T) {
+\tdb := newTestDB(t, "people")
+\tdefer closeDB(t, db)
+\trows, err := db.Query("SELECT|people|age,name|")
+\tif err != nil {
+\t\tt.Fatalf("Query: %v", err)
+\t}\n+\ttype row struct {\n+\t\tage int\n+\t\tname string\n+\t}\n+\tgot := []row{}\n+\tfor rows.Next() {\n+\t\tvar r row\n+\t\terr = rows.Scan(&r.age, &r.name)\n+\t\tif err != nil {\n+\t\t\tt.Fatalf("Scan: %v", err)\n+\t\t}\n+\t\tgot = append(got, r)\n+\t}\n+\terr = rows.Err()\n+\tif err != nil {\n+\t\tt.Fatalf("Err: %v", err)\n+\t}\n+\twant := []row{\n+\t\t{age: 1, name: "Alice"},\n+\t\t{age: 2, name: "Bob"},\n+\t\t{age: 3, name: "Chris"},\n+\t}\n+\tif !reflect.DeepEqual(got, want) {\n+\t\tt.Logf(" got: %#v\\nwant: %#v", got, want)\n+\t}\n+}\n+\n+func TestQueryRow(t *testing.T) {
\tdb := newTestDB(t, "people")
\tdefer closeDB(t, db)
\tvar name string
@@ -75,6 +109,24 @@ func TestQuery(t *testing.T) {
\t}\n }\n \n+func TestStatementErrorAfterClose(t *testing.T) {\n+\tdb := newTestDB(t, "people")
+\tdefer closeDB(t, db)\n+\tstmt, err := db.Prepare("SELECT|people|age|name=?")
+\tif err != nil {\n+\t\tt.Fatalf("Prepare: %v", err)\n+\t}\n+\terr = stmt.Close()\n+\tif err != nil {\n+\t\tt.Fatalf("Close: %v", err)\n+\t}\n+\tvar name string\n+\terr = stmt.QueryRow("foo").Scan(&name)\n+\tif err == nil {\n+\t\tt.Errorf("expected error from QueryRow.Scan after Stmt.Close")\n+\t}\n+}\n+\n func TestStatementQueryRow(t *testing.T) {
\tdb := newTestDB(t, "people")
\tdefer closeDB(t, db)
\tvar name string
コアとなるコードの解説
src/pkg/exp/sql/driver/driver.go
このファイルでは、driver.StmtインターフェースのClose()とNumInput()メソッドのドキュメントが更新されています。
-
Close()メソッドのコメント追加:- 以前のコメントは非常に簡潔でしたが、新しいコメントでは、ステートメントを閉じても、そのステートメントから生成された
Rowsオブジェクトなどの未処理のクエリが中断されないという重要な保証が追加されました。これは、アプリケーションがステートメントを閉じた後も、そのステートメントによって開始されたデータ取得を完了できることを意味します。 - この保証は、リソース管理と並行処理の設計において非常に重要です。例えば、複数のゴルーチンが同じステートメントからクエリを実行している場合、一つのゴルーチンがステートメントを閉じても、他のゴルーチンが取得した
Rowsオブジェクトが突然無効になるべきではありません。 TODOコメントは、将来的にsqlパッケージがステートメントの参照カウントをより賢く管理し、ドライバの実装を簡素化できる可能性を示唆しています。
- 以前のコメントは非常に簡潔でしたが、新しいコメントでは、ステートメントを閉じても、そのステートメントから生成された
-
NumInput()メソッドのコメント追加:- このメソッドは、プリペアドステートメントが期待するプレースホルダーパラメータの数をドライバが
sqlパッケージに伝えるために使用されます。 - 新しいコメントでは、
NumInput()が0以上の値を返す場合、sqlパッケージが呼び出し元からの引数の数を事前に検証し、不一致があればエラーを返すことが明確化されました。これにより、ドライバがSQLクエリを実行する前に、引数の数に関する基本的なエラーを捕捉できます。 NumInput()が-1を返す場合、sqlパッケージは引数の数を検証せず、この責任はドライバに委ねられます。これは、ドライバがプレースホルダーの数を動的に決定する場合や、プレースホルダーの概念を持たないデータベースに対応する場合に有用です。
- このメソッドは、プリペアドステートメントが期待するプレースホルダーパラメータの数をドライバが
これらのドキュメントの変更は、ドライバ開発者がdatabase/sqlインターフェースの期待される振る舞いをより正確に理解し、堅牢なドライバを実装するためのガイドラインを提供します。
src/pkg/exp/sql/fakedb_test.go
このファイルは、database/sqlパッケージのテストに使用されるフェイクデータベースドライバの実装です。変更は、driver.Stmt.Close()の新しいドキュメントで述べられている振る舞いをテストするために、フェイクステートメントのライフサイクル管理を強化しています。
-
fakeStmt構造体へのclosedフィールド追加:closedフィールドは、fakeStmtインスタンスが閉じられた状態にあるかどうかを追跡するためのフラグです。これにより、テスト中にステートメントのライフサイクルをシミュレートできます。
-
fakeStmt.Close()でのclosedフラグ設定:Close()メソッドが呼び出されたときにs.closed = trueを設定することで、フェイクステートメントが閉じられた状態になったことを記録します。
-
Exec()およびQuery()でのclosedチェック:fakeStmt.Exec()とfakeStmt.Query()メソッドの冒頭に、if s.closed { return nil, errClosed }というチェックが追加されました。- これは、閉じられたステートメントに対して
ExecやQueryのような操作が試みられた場合に、errClosedエラーを返すようにフェイクドライバを動作させます。この振る舞いは、sql_test.goで追加されたTestStatementErrorAfterCloseテストによって検証されます。これは、ステートメント自体が閉じられた後に再利用されるべきではないという原則を強制します。
-
fakeConn.prepareSelect内の改善:strings.Splitの結果に空文字列が含まれる場合があるため、if colspec == "" { continue }が追加されました。これは、テストデータの解析ロバスト性を向上させるための小さな修正です。
src/pkg/exp/sql/sql_test.go
このファイルには、database/sqlパッケージの統合テストが含まれています。このコミットでは、新しいドキュメントで定義されたdriver.Stmtの振る舞いを検証するためのテストが追加されました。
-
TestQueryの追加:- このテストは、
db.Queryを使用して複数の行と列をフェッチする、より現実的なシナリオをシミュレートします。 rows.Next()とrows.Scan()を使用して結果セットをイテレートし、取得したデータをカスタム構造体(row)にマッピングします。reflect.DeepEqualを使用して、取得したデータが期待されるデータと完全に一致するかどうかを検証します。これは、Queryメソッドが正しく動作し、すべてのデータが期待通りに取得されることを確認するための包括的なテストです。
- このテストは、
-
TestQueryRowの追加:- このテストは、
db.QueryRowを使用して単一の行をクエリするシナリオをテストします。QueryRowは、結果セットから最初の行のみを期待し、その行を直接スキャンする便利なメソッドです。
- このテストは、
-
TestStatementErrorAfterCloseの追加:- このテストは、
driver.Stmt.Close()の新しいドキュメントで示唆されている重要な振る舞いを検証します。 - テストはまず
db.Prepareでステートメントを準備し、すぐにstmt.Close()を呼び出してステートメントを閉じます。 - その後、閉じられたステートメントに対して
stmt.QueryRowを呼び出し、その結果をScanしようとします。 - このテストの目的は、閉じられたステートメントに対して後続の操作(この場合は
QueryRow)が試みられた場合に、エラーが返されることを確認することです。もしエラーが返されなければ、テストは失敗します。 - これは、
driver.goのClose()ドキュメントで述べられている「ステートメントを閉じても、そのステートメントから生成された未処理のクエリが中断されるべきではない」という原則とは異なります。このテストは、ステートメント自体が閉じられた後に再利用されるべきではないという、別の重要な側面を強調しています。つまり、Rowsオブジェクトは有効なままであるべきですが、元のStmtオブジェクトは閉じられた後は使用すべきではありません。
- このテストは、
これらのテストは、exp/sqlパッケージとドライバの実装が、ドキュメントで定義された契約に準拠していることを保証するための重要な検証手段となります。
関連リンク
- Go CL 5415055: https://golang.org/cl/5415055
参考にした情報源リンク
- 特になし(コミットメッセージとコード差分から直接解析)