Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 6 additions & 26 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -935,11 +935,12 @@ func (c *conn) CheckNamedValue(value *driver.NamedValue) error {
if checkIsValidType(value.Value) {
return nil
}
if valuer, ok := value.Value.(driver.Valuer); ok {
v, err := callValuerValue(valuer)
if err != nil {
return err
}

// Convert the value using the default sql driver. This uses driver.Valuer,
// if implemented, and falls back to reflection. If the converted value is
// a supported spanner type, use it. Otherwise, ignore any errors and
// continue checking other supported spanner specific types.
if v, err := driver.DefaultParameterConverter.ConvertValue(value.Value); err == nil {
if checkIsValidType(v) {
value.Value = v
return nil
Expand Down Expand Up @@ -1149,27 +1150,6 @@ func (c *conn) createPartitionedDmlQueryOptions() spanner.QueryOptions {
return spanner.QueryOptions{ExcludeTxnFromChangeStreams: c.excludeTxnFromChangeStreams}
}

// callValuerValue is from Go's database/sql package to handle a special case,
// in the same way that database/sql does, for nil pointers to types that implement
// driver.Valuer with value receivers.

var valuerReflectType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()

// callValuerValue returns vr.Value(), with one exception:
// If vr.Value is a value receiver method on a pointer type and the pointer is nil,
// it would panic at runtime. This treats it like nil instead.
//
// This is so people can implement driver.Value on value types and still use nil pointers
// to those types to mean nil/NULL, just like the Go database/sql package.
func callValuerValue(vr driver.Valuer) (v driver.Value, err error) {
if rv := reflect.ValueOf(vr); rv.Kind() == reflect.Ptr &&
rv.IsNil() &&
rv.Type().Elem().Implements(valuerReflectType) {
return nil, nil
}
return vr.Value()
}

/* The following is the same implementation as in google-cloud-go/spanner */

func isStructOrArrayOfStructValue(v interface{}) bool {
Expand Down
6 changes: 6 additions & 0 deletions driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package spannerdriver
import (
"context"
"database/sql/driver"
"net"
"testing"
"time"

Expand Down Expand Up @@ -492,6 +493,11 @@ func TestConn_CheckNamedValue(t *testing.T) {
{in: &Person{Name: "hello", Age: 123}, want: &Person{Name: "hello", Age: 123}},
// nil pointer of type that implements driver.Valuer via value receiver should use nil
{in: testNil, want: nil},
// net.IP reflects to []byte. Allow model structs to have fields with
// types that reflect to types supported by spanner.
{in: net.IPv6loopback, want: []byte(net.IPv6loopback)},
// Similarly, time.Duration is just an int64.
{in: time.Duration(1), want: int64(time.Duration(1))},
}

for _, test := range tests {
Expand Down
4 changes: 4 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"math"
"math/big"
"math/rand"
"net"
"os"
"reflect"
"strconv"
Expand Down Expand Up @@ -505,6 +506,9 @@ func TestTypeRoundtrip(t *testing.T) {
// JSON variants
{in: spanner.NullJSON{Valid: true, Value: map[string]any{"a": 13}}, skipeq: true},
{in: []spanner.NullJSON{{Valid: true, Value: map[string]any{"a": 13}}}, skipeq: true},
// Standard library type alias examples
{in: net.IPv6loopback},
{in: time.Duration(1)},
}

for _, test := range tests {
Expand Down
Loading