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

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

このコミットは、Go言語の標準ライブラリ database/sql パッケージにおける、データベースのNULL値をGoの[]byte型にスキャンする際の挙動を修正し、string型の値を[]byte型にスキャンできるように拡張するものです。具体的には、SQLのNULL値が[]byte{}(空のバイトスライス)ではなくnil(ゼロ値)としてGoの[]byte変数に変換されるように改善され、Goにおけるnilと空のスライスのセマンティックな違いが正しく反映されるようになりました。

コミット

commit 2a22f35598bba353f13d4808b4c4d710fa125f43
Author: James P. Cooper <jamespcooper@gmail.com>
Date:   Thu Jan 26 15:12:48 2012 -0800

        database/sql: convert SQL null values to []byte as nil.
    
    Also allow string values to scan into []byte.
    Fixes #2788.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5577054

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

https://github.com/golang/go/commit/2a22f35598bba353f13d4808b4c4d710fa125f43

元コミット内容

        database/sql: convert SQL null values to []byte as nil.
    
    Also allow string values to scan into []byte.
    Fixes #2788.

変更の背景

このコミットの主な背景は、Go言語のdatabase/sqlパッケージがSQLのNULL値を[]byte型にスキャンする際の不適切な挙動を修正することにありました。以前のバージョンでは、データベースから取得したNULL値がGoの[]byte変数にスキャンされると、nilではなく長さ0のバイトスライス([]byte{})として扱われていました。

Go言語において、nilスライスと長さ0のスライスは異なる意味を持ちます。

  • nilスライスは、そのスライスが何も参照していない状態、つまり「値が存在しない」ことを示唆します。これはSQLのNULL値のセマンティクスと一致します。
  • 長さ0のスライス([]byte{})は、スライス自体は存在し、メモリを占有しているが、要素が一つもない状態、つまり「空のデータ」を示します。これはSQLの空文字列や空のバイナリデータに相当します。

この違いは、アプリケーションロジックにおいてNULL値と空の値を区別する必要がある場合に重要となります。例えば、データベースの特定のカラムがNULLを許容し、そのNULLが「データがない」ことを意味する場合、[]byte{}としてスキャンされると、それが空の文字列なのか、それとも本当にデータがないのかを区別できなくなり、誤った処理につながる可能性がありました。

この問題はGoのIssue #2788として報告されており、このコミットはその修正を目的としています。また、副次的な改善として、データベースから取得したstring型の値を直接[]byte型にスキャンできるようにすることで、開発者の利便性を向上させています。

前提知識の解説

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

database/sqlパッケージは、Go言語でSQLデータベースを操作するための標準インターフェースを提供します。このパッケージは、特定のデータベースドライバに依存しない汎用的なAPIを提供し、開発者はドライバを切り替えるだけで異なるデータベース(PostgreSQL, MySQL, SQLiteなど)と連携できます。

主要な概念は以下の通りです。

  • DB: データベースへの接続プールを表します。
  • Stmt: プリペアドステートメントを表します。
  • Rows: クエリ結果の行をイテレートするためのインターフェースです。
  • Row: 単一の行をスキャンするためのヘルパー型です。
  • Scanメソッド: RowsRowから取得したデータベースの値をGoの変数に変換(スキャン)するために使用されます。この変換プロセスが本コミットの主要な変更点です。

SQLのNULL値

SQLにおけるNULLは、「値がない」「不明な値」「適用できない値」を表す特別なマーカーです。これは、空文字列('')やゼロ(0)とは明確に区別されます。例えば、VARCHAR型のカラムがNULLを許容する場合、そのカラムに値が設定されていない状態はNULLであり、空文字列とは異なります。

Go言語におけるnilと空のスライス

Go言語では、スライスは基底配列へのポインタ、長さ、容量の3つの要素から構成されるデータ構造です。

  • nilスライス: スライス変数がどの基底配列も参照していない状態です。var s []byteと宣言した直後のsnilです。len(s)cap(s)は両方とも0になります。nilスライスは、値が存在しないことを明確に示します。
  • 空のスライス: スライス変数が長さ0の基底配列を参照している状態です。make([]byte, 0)[]byte{}のように作成されます。len(s)cap(s)は両方とも0になりますが、スライス自体はnilではありません。これは、空のデータセットや空のコレクションを表す際に使用されます。

nilと空のスライスの区別は、特にデータベースのNULL値を扱う際に重要です。SQLのNULLはGoのnilに、SQLの空文字列はGoの空のスライスにマッピングされるのが自然なセマンティクスです。

Scanメソッドの内部動作

database/sqlパッケージのScanメソッドは、データベースドライバから取得した生データを、ユーザーが指定したGoの変数型に変換する役割を担います。この変換は、内部的にconvertAssign関数のようなヘルパー関数を通じて行われます。Scanは、ターゲット変数の型を検査し、それに応じて適切な型変換ロジックを適用します。例えば、データベースのINT型をGoのint型に、VARCHAR型をstring型に変換します。本コミットでは、この変換ロジック、特に[]byte型への変換パスが修正されました。

技術的詳細

このコミットは、主にsrc/pkg/database/sql/convert.gosrc/pkg/database/sql/sql.goの2つのファイルにわたる変更によって、SQLのNULL値から[]byteへの変換と、stringから[]byteへの変換の挙動を改善しています。

src/pkg/database/sql/convert.goの変更

convert.goファイルは、database/sqlパッケージ内でデータベースの値をGoの型に変換するロジックをカプセル化しています。特にconvertAssign関数は、ソースの型とデスティネーションの型に基づいて適切な変換を行います。

  1. stringから[]byteへの変換の追加: 以前は、string型のソースを[]byte型のデスティネーションに直接スキャンするパスが明示的に存在しませんでした。このコミットにより、convertAssign関数内のcase string:ブロックに、*[]byte型への変換ケースが追加されました。

    case *[]byte:
        *d = []byte(s)
        return nil
    

    これにより、データベースから取得した文字列データが、Goの[]byte変数に直接コピーされるようになります。

  2. nilから[]byteへの変換の修正: 最も重要な変更は、convertAssign関数内のcase nil:ブロックに*[]byte型への変換ケースが追加されたことです。

    case nil:
        switch d := dest.(type) {
        case *[]byte:
            *d = nil
            return nil
        }
    

    この変更により、データベースから取得した値がSQLのNULLである場合(Goのnilとして表現される)、デスティネーションが*[]byte型であれば、その[]byte変数は明示的にnilに設定されるようになりました。これにより、以前の[]byte{}(空のスライス)ではなく、nilスライスが割り当てられるようになり、SQLのNULLのセマンティクスがGoの型システムに正しくマッピングされます。

src/pkg/database/sql/sql.goの変更

sql.goファイルは、RowsRowといった主要なデータ構造と、それらのScanメソッドの実装を含んでいます。

  1. Rows.ScanにおけるNULL []byteの処理: Rows.Scanメソッド内で、[]byte型のデスティネーションに対する追加のチェックが導入されました。

    if *b == nil {
        // If the []byte is now nil (for a NULL value),
        // don't fall through to below which would
        // turn it into a non-nil 0-length byte slice
        continue
    }
    

    このコードは、convert.goで既にnilに設定された[]byte変数が、Rows.Scan内の後続の処理(例えば、RawBytesの処理や、バイトスライスが一時メモリを参照している場合の防御的コピー)によって誤って非nilの長さ0のバイトスライスに変換されるのを防ぎます。*bnilであれば、その後の処理をスキップし、nilの状態を維持します。

  2. Row.Scanの簡素化と再配置: Row.Scanメソッドは、単一の行をスキャンするためのヘルパーです。このメソッド内の[]byteの防御的コピーに関するロジックが変更されました。以前は、Row.Scan内で[]byteの値を防御的にコピーするロジックが存在しましたが、このコミットではそのロジックが削除され、Rows.Scan(またはconvertAssign)がnil[]byteを正しく処理するようになったため、Row.Scanでの特別な扱いは不要になりました。 また、defer r.rows.Close()r.rows.Next()r.rows.Scan(dest...)といった行を処理する主要なロジックの順序が、[]byteのコピー処理(削除された部分)の後に移動されました。これは、Rows.ScanRawBytesを許可しないというコメントの更新と合わせて、database/sqlパッケージ全体でのバイトスライスの扱いの一貫性を高めるための変更です。

src/pkg/database/sql/sql_test.goの変更

このコミットには、TestNullByteSliceという新しいテスト関数が追加されました。このテストは、Issue #2788で報告された問題を具体的に検証するために設計されています。

  1. NULL値のテスト:

    • CREATE文でnullstringカラムを持つテーブルを作成します。
    • INSERT文でnameカラムにnil(SQLのNULLに相当)を挿入します。
    • QueryRow().Scan(&name)を使って、このNULL値を[]byte変数nameにスキャンします。
    • if name != nilというアサーションで、namenilであることを確認します。これにより、SQLのNULLが正しくGoのnil []byteに変換されることが検証されます。
  2. 非NULL値(文字列)のテスト:

    • INSERT文でnameカラムに文字列"bob"を挿入します。
    • 同様にQueryRow().Scan(&name)nameにスキャンします。
    • if string(name) != "bob"というアサーションで、nameが正しく"bob"というバイトスライスに変換されていることを確認します。これにより、stringから[]byteへの変換機能も検証されます。

このテストの追加により、NULL値の[]byteへのスキャンが正しく行われることが保証され、将来のリグレッションを防ぐことができます。

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

src/pkg/database/sql/convert.go

--- a/src/pkg/database/sql/convert.go
+++ b/src/pkg/database/sql/convert.go
@@ -40,6 +40,9 @@ func convertAssign(dest, src interface{}) error {
 		case *string:
 			*d = s
 			return nil
+		case *[]byte:
+			*d = []byte(s)
+			return nil
 		}
 	case []byte:
 		switch d := dest.(type) {
@@ -50,6 +53,12 @@ func convertAssign(dest, src interface{}) error {
 			*d = s
 			return nil
 		}
+	case nil:
+		switch d := dest.(type) {
+		case *[]byte:
+			*d = nil
+			return nil
+		}
 	}
 
 	var sv reflect.Value

src/pkg/database/sql/sql.go

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -904,6 +904,12 @@ func (rs *Rows) Scan(dest ...interface{}) error {
 		if !ok {
 			continue
 		}
+		if *b == nil {
+			// If the []byte is now nil (for a NULL value),
+			// don't fall through to below which would
+			// turn it into a non-nil 0-length byte slice
+			continue
+		}
 		if _, ok = dp.(*RawBytes); ok {
 			continue
 		}
@@ -945,17 +951,10 @@ func (r *Row) Scan(dest ...interface{}) error {
 	if r.err != nil {
 		return r.err
 	}
-	defer r.rows.Close()
-	if !r.rows.Next() {
-		return ErrNoRows
-	}
-	err := r.rows.Scan(dest...)
-	if err != nil {
-		return err
-	}
 
 	// TODO(bradfitz): for now we need to defensively clone all
-	// []byte that the driver returned, since we're about to close
+	// []byte that the driver returned (not permitting 
+	// *RawBytes in Rows.Scan), since we're about to close
 	// the Rows in our defer, when we return from this function.
 	// the contract with the driver.Next(...) interface is that it
 	// can return slices into read-only temporary memory that's
@@ -970,14 +969,17 @@ func (r *Row) Scan(dest ...interface{}) error {
 		if _, ok := dp.(*RawBytes); ok {
 			return errors.New("sql: RawBytes isn't allowed on Row.Scan")
 		}
-		b, ok := dp.(*[]byte)
-		if !ok {
-			continue
-		}
-		clone := make([]byte, len(*b))
-		copy(clone, *b)
-		*b = clone
 	}
+
+	defer r.rows.Close()
+	if !r.rows.Next() {
+		return ErrNoRows
+	}
+	err := r.rows.Scan(dest...)
+	if err != nil {
+		return err
+	}
+
 	return nil
 }

src/pkg/database/sql/sql_test.go

--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -358,6 +358,34 @@ func TestIssue2542Deadlock(t *testing.T) {
 	}
 }
 
+// Tests fix for issue 2788, that we bind nil to a []byte if the
+// value in the column is sql null
+func TestNullByteSlice(t *testing.T) {
+	db := newTestDB(t, "")
+	defer closeDB(t, db)
+	exec(t, db, "CREATE|t|id=int32,name=nullstring")
+	exec(t, db, "INSERT|t|id=10,name=?", nil)
+
+	var name []byte
+
+	err := db.QueryRow("SELECT|t|name|id=?", 10).Scan(&name)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if name != nil {
+		t.Fatalf("name []byte should be nil for null column value, got: %#v", name)
+	}
+
+	exec(t, db, "INSERT|t|id=11,name=?", "bob")
+	err = db.QueryRow("SELECT|t|name|id=?", 11).Scan(&name)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if string(name) != "bob" {
+		t.Fatalf("name []byte should be bob, got: %q", string(name))
+	}
+}
+
 func TestQueryRowClosingStmt(t *testing.T) {
 	db := newTestDB(t, "people")
 	defer closeDB(t, db)

コアとなるコードの解説

src/pkg/database/sql/convert.go

  • case *[]byte: (string source): この追加により、convertAssign関数は、ソースデータがstring型であり、かつデスティネーションが*[]byte型(バイトスライスへのポインタ)である場合に、文字列をバイトスライスに変換してデスティネーションに割り当てることができるようになりました。具体的には、*d = []byte(s)という行が、文字列sをバイトスライスに型変換し、それをポインタdが指す[]byte変数に代入します。これにより、データベースから取得した文字列を直接[]byte変数にスキャンする際の柔軟性が向上します。

  • case *[]byte: (nil source): この変更は、SQLのNULL値のセマンティクスをGoの[]byte型に正しくマッピングするための最も重要な部分です。ソースデータがnil(SQLのNULLに相当)であり、デスティネーションが*[]byte型である場合、*d = nilという行が実行されます。これにより、デスティネーションの[]byte変数は明示的にnilに設定され、以前のように長さ0のバイトスライス([]byte{})になることを防ぎます。この修正により、GoのアプリケーションはSQLのNULL値を正確に区別できるようになります。

src/pkg/database/sql/sql.go

  • Rows.Scan内のif *b == nilチェック: Rows.Scanメソッドは、データベースから取得した各カラムの値をGoの変数にスキャンする中心的なロジックを含んでいます。このコミットで追加されたif *b == nilチェックは、convert.goで既にnilに設定された[]byte変数が、Rows.Scan内の後続の処理によって意図せず非nilの空のスライスに変換されるのを防ぐための防御的な措置です。もし*bnilであれば、それはSQLのNULL値が正しく変換されたことを意味するため、それ以上の処理は不要であり、continueして次のデスティネーションの処理に移ります。

  • Row.Scanの変更: Row.Scanメソッドは、単一の行をスキャンするための簡便なラッパーです。このコミットでは、Row.Scan内の[]byteの防御的コピーに関するロジックが削除されました。これは、Rows.Scan(およびconvertAssign)がnil[]byteを正しく処理し、またRawBytesの扱いに関するコメントが更新されたことと関連しています。Row.Scanの主要なロジック(defer r.rows.Close()r.rows.Next()r.rows.Scan(dest...))の配置が変更され、よりクリーンな構造になりました。これにより、database/sqlパッケージ全体でのバイトスライスのメモリ管理とNULL値の処理が一貫性を持ち、簡素化されました。

src/pkg/database/sql/sql_test.go

  • TestNullByteSlice関数: この新しいテスト関数は、本コミットの主要な修正(SQL NULL値から[]byteへのnil変換)と、副次的な改善(stringから[]byteへのスキャン)の両方を検証します。
    1. NULL値の検証: データベースにNULL値を挿入し、それを[]byte変数にスキャンした後、if name != nilという条件でnamenilであることを確認します。これにより、NULL値が正しくnil []byteとして扱われることが保証されます。
    2. 文字列値の検証: データベースに文字列"bob"を挿入し、それを[]byte変数にスキャンした後、if string(name) != "bob"という条件でname"bob"というバイトスライスとして正しく変換されていることを確認します。

このテストの追加は、修正が正しく機能していることを確認し、将来のコード変更によるリグレッションを防ぐための重要なステップです。

関連リンク

参考にした情報源リンク