Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix encode driver.Valuer on pointer #2019

Merged
merged 9 commits into from
May 25, 2024
Merged
38 changes: 34 additions & 4 deletions internal/anynil/anynil.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
package anynil

import "reflect"
import (
"database/sql/driver"
"reflect"
)

// Is returns true if value is any type of nil. e.g. nil or []byte(nil).
// valuerReflectType is a reflect.Type for driver.Valuer. It has confusing syntax because reflect.TypeOf returns nil
// when it's argument is a nil interface value. So we use a pointer to the interface and call Elem to get the actual
// type. Yuck.
//
// This can be simplified in Go 1.22 with reflect.TypeFor.
//
// var valuerReflectType = reflect.TypeFor[driver.Valuer]()
var valuerReflectType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()

// Is returns true if value is any type of nil except a pointer that directly implements driver.Valuer. e.g. nil,
// []byte(nil), and a *T where T implements driver.Valuer get normalized to nil but a *T where *T implements
// driver.Valuer does not.
func Is(value any) bool {
if value == nil {
return true
}

refVal := reflect.ValueOf(value)
switch refVal.Kind() {
kind := refVal.Kind()
switch kind {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
return refVal.IsNil()
if !refVal.IsNil() {
return false
}

if kind == reflect.Ptr {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my use-case, the restriction to just pointers won't work since the type I'm using is an alias to a slice. See #1860 for some example code.

Allowing reflect.Slice here would fix my use-case, but it would probably make sense to allow for others as well.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. It now extends the same logic to all nilable values.

This extends the behavior change a little further than I had anticipated. But it is a fix for database/sql compatibility and I don't anticipate the change breaking any real world code.

if _, ok := value.(driver.Valuer); ok {
// The pointer will be considered to implement driver.Valuer even if it is actually implemented on the value.
// But we only want to consider it nil if it is implemented on the pointer. So check if what the pointer points
// to implements driver.Valuer.
if !refVal.Type().Elem().Implements(valuerReflectType) {
return false
}
}
}

return true
default:
return false
}
Expand Down
10 changes: 10 additions & 0 deletions pgtype/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@ Compatibility with database/sql
pgtype also includes support for custom types implementing the database/sql.Scanner and database/sql/driver.Valuer
interfaces.

Encoding Typed Nils

pgtype normalizes typed nils (e.g. []byte(nil)) into nil. nil is always encoded is the SQL NULL value without going
through the Codec system. This means that Codecs and other encoding logic does not have to handle nil or *T(nil).

However, database/sql compatibility requires Value to be called on a pointer that implements driver.Valuer. See
https://github.com/golang/go/issues/8415 and
https://github.com/golang/go/commit/0ce1d79a6a771f7449ec493b993ed2a720917870. Therefore, pointers that implement
driver.Valuer are not normalized to nil.

Child Records

pgtype's support for arrays and composite records can be used to load records and their children in a single query. See
Expand Down
50 changes: 50 additions & 0 deletions query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -1171,6 +1173,54 @@ func TestConnQueryDatabaseSQLDriverValuerWithAutoGeneratedPointerReceiver(t *tes
ensureConnValid(t, conn)
}

type nilAsEmptyJSONObject struct {
ID string
Name string
}

func (v *nilAsEmptyJSONObject) Value() (driver.Value, error) {
if v == nil {
return "{}", nil
}

return json.Marshal(v)
}

// https://github.com/jackc/pgx/issues/1566
func TestConnQueryDatabaseSQLDriverValuerCalledOnPointerImplementers(t *testing.T) {
t.Parallel()

conn := mustConnectString(t, os.Getenv("PGX_TEST_DATABASE"))
defer closeConn(t, conn)

mustExec(t, conn, "create temporary table t(v json not null)")

var v *nilAsEmptyJSONObject
commandTag, err := conn.Exec(context.Background(), `insert into t(v) values($1)`, v)
require.NoError(t, err)
require.Equal(t, "INSERT 0 1", commandTag.String())

var s string
err = conn.QueryRow(context.Background(), "select v from t").Scan(&s)
require.NoError(t, err)
require.Equal(t, "{}", s)

_, err = conn.Exec(context.Background(), `delete from t`)
require.NoError(t, err)

v = &nilAsEmptyJSONObject{ID: "1", Name: "foo"}
commandTag, err = conn.Exec(context.Background(), `insert into t(v) values($1)`, v)
require.NoError(t, err)
require.Equal(t, "INSERT 0 1", commandTag.String())

var v2 *nilAsEmptyJSONObject
err = conn.QueryRow(context.Background(), "select v from t").Scan(&v2)
require.NoError(t, err)
require.Equal(t, v, v2)

ensureConnValid(t, conn)
}

func TestConnQueryDatabaseSQLDriverScannerWithBinaryPgTypeThatAcceptsSameType(t *testing.T) {
t.Parallel()

Expand Down