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

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

このコミットは、Go言語の標準ライブラリである database/sql パッケージにおける Stmt.Exec メソッドの振る舞いを改善し、潜在的なバグを修正するものです。具体的には、driver.Stmt インターフェースの NumInput メソッドが返す値に基づいて、ステートメントに渡される引数の数をチェックするロジックが導入されています。これにより、データベースドライバが期待する引数の数と、実際に Exec に渡される引数の数との不一致によるエラーを早期に検出できるようになります。

コミット

commit 7f0449a1086dce557c6071c951cfec8664e8d456
Author: Gwenael Treguier <gwenn.kahz@gmail.com>
Date:   Fri Jan 11 13:28:33 2013 -0800

    database/sql: check NumInput on Stmt.Exec
    
    Fixes #3678.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6460087

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

https://github.com/golang/go/commit/7f0449a1086dce557c6071c951cfec8664e8d456

元コミット内容

このコミットの元の内容は、database/sql パッケージにおいて、Stmt.Exec メソッドが実行される際に、データベースドライバが期待するプレースホルダの数と、実際に渡される引数の数が一致するかどうかを検証する機能を追加することです。これは、GoのIssue #3678で報告された問題に対応するもので、引数の不一致によって発生する可能性のあるランタイムエラーを防ぐことを目的としています。

変更の背景

この変更の背景には、Goの database/sql パッケージが提供する抽象化レイヤーと、その下にある具体的なデータベースドライバとの間のインタラクションにおける課題がありました。以前のバージョンでは、Exec メソッドに渡される引数の数が、SQLステートメント内のプレースホルダの数と一致しない場合、エラーがドライバレベルで発生し、そのエラーメッセージが必ずしもユーザーにとって分かりやすいものではありませんでした。

特に、Issue #3678では、database/sql パッケージが driver.Stmt インターフェースの NumInput メソッドを適切に利用していないことが指摘されていました。NumInput は、ドライバがステートメントが期待する引数の数を報告するためのメソッドです。この情報が利用されていないため、引数の数が間違っている場合に、より上位の database/sql レイヤーで早期にエラーを検出できず、ドライバに処理を任せてしまっていました。これにより、デバッグが困難になるケースがありました。

このコミットは、NumInput の情報を活用することで、database/sql パッケージがより賢明に引数の数をチェックし、不適切な引数で Exec が呼び出された場合に、より明確なエラーを返すようにすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の database/sql パッケージに関する前提知識が必要です。

  • database/sql パッケージ: Go言語の標準ライブラリで、SQLデータベースへの汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースの実装を含まず、データベースドライバを介して様々なデータベースと連携します。
  • driver パッケージ: database/sql パッケージの内部で利用されるインターフェースを定義しています。データベースドライバは、この driver パッケージで定義されたインターフェースを実装することで、database/sql パッケージと連携します。
  • driver.Stmt インターフェース: プリペアドステートメントを表すインターフェースです。データベースドライバは、このインターフェースを実装し、ExecQuery などのメソッドを提供します。
  • driver.Stmt.NumInput() メソッド: driver.Stmt インターフェースの一部であり、プリペアドステートメントが期待するプレースホルダの数を返します。ドライバがこの情報を提供できない場合(例えば、プレースホルダの数が可変である場合や、ドライバがこの機能に対応していない場合)、-1 を返します。
  • プリペアドステートメント (Prepared Statement): SQLインジェクション攻撃を防ぎ、クエリの実行効率を向上させるために使用されるデータベースの機能です。SQLクエリの構造を事前にデータベースに渡し、後からパラメータ(引数)をバインドして実行します。

技術的詳細

このコミットの技術的な詳細は、database/sql パッケージが driver.Stmt.NumInput() メソッドの戻り値をどのように利用して、Exec メソッドに渡される引数の数を検証するかという点にあります。

変更前は、DB.exec および Tx.Exec メソッド内で、driverArgs 関数を呼び出して引数をドライバが理解できる形式に変換し、その後すぐに sti.Exec(dargs) を呼び出してステートメントを実行していました。この際、引数の数の検証は database/sql レイヤーでは行われず、ドライバに任されていました。

変更後は、DB.execTx.Exec、および Stmt.Exec の各メソッドが、共通の新しいヘルパー関数 resultFromStatement を呼び出すようにリファクタリングされています。この resultFromStatement 関数が、引数の数に関する検証ロジックをカプセル化しています。

resultFromStatement 関数内では、まず si.NumInput() を呼び出して、ドライバが期待する引数の数を取得します。

  • もし NumInput()-1 を返した場合、これはドライバが引数の数を特定できないことを意味します。この場合、database/sql パッケージは引数の数に関する事前チェックを行わず、ドライバに処理を委ねます。これは、ドライバが可変長の引数をサポートしている場合や、プレースホルダの数が実行時に決定されるような複雑なステートメントを扱う場合に必要です。
  • もし NumInput()-1 以外の値を返した場合、それはドライバが期待する正確な引数の数を示しています。この場合、resultFromStatementlen(args)(実際に渡された引数の数)と numInput を比較します。もし両者が一致しない場合、fmt.Errorf を使用して「sql: expected %d arguments, got %d」のような明確なエラーメッセージを生成し、Exec の呼び出し元に返します。これにより、ドライバに処理が渡される前に、より上位のレイヤーでエラーを検出できるようになります。

この変更により、database/sql パッケージは、ドライバが提供するメタデータを活用して、より堅牢な引数検証を行うことができるようになりました。これにより、開発者は引数の不一致による問題をより早期に、より明確なエラーメッセージで特定できるようになります。

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

変更は src/pkg/database/sql/sql.go ファイルに集中しています。

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -369,16 +369,7 @@ func (db *DB) exec(query string, args []interface{}) (res Result, err error) {
 	}
 	defer sti.Close()
 
-	dargs, err := driverArgs(sti, args)
-	if err != nil {
-		return nil, err
-	}
-
-	resi, err := sti.Exec(dargs)
-	if err != nil {
-		return nil, err
-	}
-	return result{resi}, nil
+	return resultFromStatement(sti, args...)
 }
 
 // Query executes a query that returns rows, typically a SELECT.
@@ -608,16 +599,7 @@ func (tx *Tx) Exec(query string, args ...interface{}) (Result, error) {
 	}
 	defer sti.Close()
 
-	dargs, err := driverArgs(sti, args)
-	if err != nil {
-		return nil, err
-	}
-
-	resi, err := sti.Exec(dargs)
-	if err != nil {
-		return nil, err
-	}
-	return result{resi}, nil
+	return resultFromStatement(sti, args...)
 }
 
 // Query executes a query that returns rows, typically a SELECT.
@@ -682,6 +664,10 @@ func (s *Stmt) Exec(args ...interface{}) (Result, error) {
 	}
 	defer releaseConn(nil)
 
+	return resultFromStatement(si, args...)\n+}\n+\n+func resultFromStatement(si driver.Stmt, args ...interface{}) (Result, error) {
 	// -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.

コアとなるコードの解説

このコミットの主要な変更点は、DB.execTx.Exec、および Stmt.Exec の各メソッドから、引数の準備と Exec の呼び出しロジックが削除され、新しく導入されたヘルパー関数 resultFromStatement に委譲されている点です。

変更前:

DB.execTx.Exec は、それぞれ driverArgs を呼び出して引数を変換し、その後 sti.Exec を直接呼び出していました。このロジックは重複しており、引数の数に関する検証は行われていませんでした。

変更後:

  1. resultFromStatement 関数の導入:

    func resultFromStatement(si driver.Stmt, args ...interface{}) (Result, error) {
        // -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.
        numInput := si.NumInput()
        if numInput != -1 && numInput != len(args) {
            return nil, fmt.Errorf("sql: expected %d arguments, got %d", numInput, len(args))
        }
    
        dargs, err := driverArgs(si, args)
        if err != nil {
            return nil, err
        }
    
        resi, err := si.Exec(dargs)
        if err != nil {
            return nil, err
        }
        return result{resi}, nil
    }
    

    この新しい関数が、引数の数に関する検証ロジックと、引数の変換 (driverArgs)、そして実際の driver.Stmt.Exec の呼び出しをカプセル化しています。

    • numInput := si.NumInput(): ドライバが期待する引数の数を取得します。
    • if numInput != -1 && numInput != len(args): ここが新しい検証ロジックです。
      • numInput != -1: ドライバが引数の数を報告できる場合(つまり、-1 ではない場合)。
      • numInput != len(args): 期待される引数の数と実際に渡された引数の数が異なる場合。
      • この両方の条件が真の場合、fmt.Errorf を使って明確なエラーメッセージを生成し、関数を終了します。
    • 残りの部分は、以前の Exec メソッドにあった引数変換とドライバの Exec 呼び出しのロジックです。
  2. 既存メソッドからの委譲: DB.execTx.Exec、および Stmt.Exec の各メソッドは、冗長なコードを削除し、すべて resultFromStatement(sti, args...) を呼び出すように変更されました。これにより、コードの重複が解消され、引数検証ロジックが一元化されました。

この変更により、database/sql パッケージは、データベースドライバが提供する NumInput の情報を利用して、より上位のレイヤーで引数の数を検証できるようになり、開発者にとってより分かりやすいエラーメッセージを提供できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語 database/sql パッケージ公式ドキュメント: https://pkg.go.dev/database/sql
  • Go言語 database/sql/driver パッケージ公式ドキュメント: https://pkg.go.dev/database/sql/driver
  • Go言語の database/sql パッケージの内部構造とドライバの作成に関する記事 (一般的な情報源): (具体的なURLはコミット内容からは特定できないため、一般的な情報源として記載)