523 lines
16 KiB
Go
523 lines
16 KiB
Go
/*
|
|
Copyright 2017 Google Inc. All Rights Reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package spanner
|
|
|
|
import (
|
|
"math"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"cloud.google.com/go/civil"
|
|
proto3 "github.com/golang/protobuf/ptypes/struct"
|
|
sppb "google.golang.org/genproto/googleapis/spanner/v1"
|
|
)
|
|
|
|
var (
|
|
t1 = mustParseTime("2016-11-15T15:04:05.999999999Z")
|
|
// Boundaries
|
|
t2 = mustParseTime("0000-01-01T00:00:00.000000000Z")
|
|
t3 = mustParseTime("9999-12-31T23:59:59.999999999Z")
|
|
// Local timezone
|
|
t4 = time.Now()
|
|
d1 = mustParseDate("2016-11-15")
|
|
d2 = mustParseDate("1678-01-01")
|
|
)
|
|
|
|
func mustParseTime(s string) time.Time {
|
|
t, err := time.Parse(time.RFC3339Nano, s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func mustParseDate(s string) civil.Date {
|
|
d, err := civil.ParseDate(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Test encoding Values.
|
|
func TestEncodeValue(t *testing.T) {
|
|
var (
|
|
tString = stringType()
|
|
tInt = intType()
|
|
tBool = boolType()
|
|
tFloat = floatType()
|
|
tBytes = bytesType()
|
|
tTime = timeType()
|
|
tDate = dateType()
|
|
)
|
|
for i, test := range []struct {
|
|
in interface{}
|
|
want *proto3.Value
|
|
wantType *sppb.Type
|
|
}{
|
|
// STRING / STRING ARRAY
|
|
{"abc", stringProto("abc"), tString},
|
|
{NullString{"abc", true}, stringProto("abc"), tString},
|
|
{NullString{"abc", false}, nullProto(), tString},
|
|
{[]string(nil), nullProto(), listType(tString)},
|
|
{[]string{"abc", "bcd"}, listProto(stringProto("abc"), stringProto("bcd")), listType(tString)},
|
|
{[]NullString{{"abcd", true}, {"xyz", false}}, listProto(stringProto("abcd"), nullProto()), listType(tString)},
|
|
// BYTES / BYTES ARRAY
|
|
{[]byte("foo"), bytesProto([]byte("foo")), tBytes},
|
|
{[]byte(nil), nullProto(), tBytes},
|
|
{[][]byte{nil, []byte("ab")}, listProto(nullProto(), bytesProto([]byte("ab"))), listType(tBytes)},
|
|
{[][]byte(nil), nullProto(), listType(tBytes)},
|
|
// INT64 / INT64 ARRAY
|
|
{7, intProto(7), tInt},
|
|
{[]int(nil), nullProto(), listType(tInt)},
|
|
{[]int{31, 127}, listProto(intProto(31), intProto(127)), listType(tInt)},
|
|
{int64(81), intProto(81), tInt},
|
|
{[]int64(nil), nullProto(), listType(tInt)},
|
|
{[]int64{33, 129}, listProto(intProto(33), intProto(129)), listType(tInt)},
|
|
{NullInt64{11, true}, intProto(11), tInt},
|
|
{NullInt64{11, false}, nullProto(), tInt},
|
|
{[]NullInt64{{35, true}, {131, false}}, listProto(intProto(35), nullProto()), listType(tInt)},
|
|
// BOOL / BOOL ARRAY
|
|
{true, boolProto(true), tBool},
|
|
{NullBool{true, true}, boolProto(true), tBool},
|
|
{NullBool{true, false}, nullProto(), tBool},
|
|
{[]bool{true, false}, listProto(boolProto(true), boolProto(false)), listType(tBool)},
|
|
{[]NullBool{{true, true}, {true, false}}, listProto(boolProto(true), nullProto()), listType(tBool)},
|
|
// FLOAT64 / FLOAT64 ARRAY
|
|
{3.14, floatProto(3.14), tFloat},
|
|
{NullFloat64{3.1415, true}, floatProto(3.1415), tFloat},
|
|
{NullFloat64{math.Inf(1), true}, floatProto(math.Inf(1)), tFloat},
|
|
{NullFloat64{3.14159, false}, nullProto(), tFloat},
|
|
{[]float64(nil), nullProto(), listType(tFloat)},
|
|
{[]float64{3.141, 0.618, math.Inf(-1)}, listProto(floatProto(3.141), floatProto(0.618), floatProto(math.Inf(-1))), listType(tFloat)},
|
|
{[]NullFloat64{{3.141, true}, {0.618, false}}, listProto(floatProto(3.141), nullProto()), listType(tFloat)},
|
|
// TIMESTAMP / TIMESTAMP ARRAY
|
|
{t1, timeProto(t1), tTime},
|
|
{NullTime{t1, true}, timeProto(t1), tTime},
|
|
{NullTime{t1, false}, nullProto(), tTime},
|
|
{[]time.Time(nil), nullProto(), listType(tTime)},
|
|
{[]time.Time{t1, t2, t3, t4}, listProto(timeProto(t1), timeProto(t2), timeProto(t3), timeProto(t4)), listType(tTime)},
|
|
{[]NullTime{{t1, true}, {t1, false}}, listProto(timeProto(t1), nullProto()), listType(tTime)},
|
|
// DATE / DATE ARRAY
|
|
{d1, dateProto(d1), tDate},
|
|
{NullDate{d1, true}, dateProto(d1), tDate},
|
|
{NullDate{civil.Date{}, false}, nullProto(), tDate},
|
|
{[]civil.Date(nil), nullProto(), listType(tDate)},
|
|
{[]civil.Date{d1, d2}, listProto(dateProto(d1), dateProto(d2)), listType(tDate)},
|
|
{[]NullDate{{d1, true}, {civil.Date{}, false}}, listProto(dateProto(d1), nullProto()), listType(tDate)},
|
|
// GenericColumnValue
|
|
{GenericColumnValue{tString, stringProto("abc")}, stringProto("abc"), tString},
|
|
{GenericColumnValue{tString, nullProto()}, nullProto(), tString},
|
|
// not actually valid (stringProto inside int list), but demonstrates pass-through.
|
|
{
|
|
GenericColumnValue{
|
|
Type: listType(tInt),
|
|
Value: listProto(intProto(5), nullProto(), stringProto("bcd")),
|
|
},
|
|
listProto(intProto(5), nullProto(), stringProto("bcd")),
|
|
listType(tInt),
|
|
},
|
|
// placeholder
|
|
{CommitTimestamp, stringProto(commitTimestampPlaceholderString), tTime},
|
|
} {
|
|
got, gotType, err := encodeValue(test.in)
|
|
if err != nil {
|
|
t.Fatalf("#%d: got error during encoding: %v, want nil", i, err)
|
|
}
|
|
if !testEqual(got, test.want) {
|
|
t.Errorf("#%d: got encode result: %v, want %v", i, got, test.want)
|
|
}
|
|
if !testEqual(gotType, test.wantType) {
|
|
t.Errorf("#%d: got encode type: %v, want %v", i, gotType, test.wantType)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test decoding Values.
|
|
func TestDecodeValue(t *testing.T) {
|
|
for i, test := range []struct {
|
|
in *proto3.Value
|
|
t *sppb.Type
|
|
want interface{}
|
|
fail bool
|
|
}{
|
|
// STRING
|
|
{stringProto("abc"), stringType(), "abc", false},
|
|
{nullProto(), stringType(), "abc", true},
|
|
{stringProto("abc"), stringType(), NullString{"abc", true}, false},
|
|
{nullProto(), stringType(), NullString{}, false},
|
|
// STRING ARRAY with []NullString
|
|
{
|
|
listProto(stringProto("abc"), nullProto(), stringProto("bcd")),
|
|
listType(stringType()),
|
|
[]NullString{{"abc", true}, {}, {"bcd", true}},
|
|
false,
|
|
},
|
|
{nullProto(), listType(stringType()), []NullString(nil), false},
|
|
// STRING ARRAY with []string
|
|
{
|
|
listProto(stringProto("abc"), stringProto("bcd")),
|
|
listType(stringType()),
|
|
[]string{"abc", "bcd"},
|
|
false,
|
|
},
|
|
// BYTES
|
|
{bytesProto([]byte("ab")), bytesType(), []byte("ab"), false},
|
|
{nullProto(), bytesType(), []byte(nil), false},
|
|
// BYTES ARRAY
|
|
{listProto(bytesProto([]byte("ab")), nullProto()), listType(bytesType()), [][]byte{[]byte("ab"), nil}, false},
|
|
{nullProto(), listType(bytesType()), [][]byte(nil), false},
|
|
//INT64
|
|
{intProto(15), intType(), int64(15), false},
|
|
{nullProto(), intType(), int64(0), true},
|
|
{intProto(15), intType(), NullInt64{15, true}, false},
|
|
{nullProto(), intType(), NullInt64{}, false},
|
|
// INT64 ARRAY with []NullInt64
|
|
{listProto(intProto(91), nullProto(), intProto(87)), listType(intType()), []NullInt64{{91, true}, {}, {87, true}}, false},
|
|
{nullProto(), listType(intType()), []NullInt64(nil), false},
|
|
// INT64 ARRAY with []int64
|
|
{listProto(intProto(91), intProto(87)), listType(intType()), []int64{91, 87}, false},
|
|
// BOOL
|
|
{boolProto(true), boolType(), true, false},
|
|
{nullProto(), boolType(), true, true},
|
|
{boolProto(true), boolType(), NullBool{true, true}, false},
|
|
{nullProto(), boolType(), NullBool{}, false},
|
|
// BOOL ARRAY with []NullBool
|
|
{listProto(boolProto(true), boolProto(false), nullProto()), listType(boolType()), []NullBool{{true, true}, {false, true}, {}}, false},
|
|
{nullProto(), listType(boolType()), []NullBool(nil), false},
|
|
// BOOL ARRAY with []bool
|
|
{listProto(boolProto(true), boolProto(false)), listType(boolType()), []bool{true, false}, false},
|
|
// FLOAT64
|
|
{floatProto(3.14), floatType(), 3.14, false},
|
|
{nullProto(), floatType(), 0.00, true},
|
|
{floatProto(3.14), floatType(), NullFloat64{3.14, true}, false},
|
|
{nullProto(), floatType(), NullFloat64{}, false},
|
|
// FLOAT64 ARRAY with []NullFloat64
|
|
{
|
|
listProto(floatProto(math.Inf(1)), floatProto(math.Inf(-1)), nullProto(), floatProto(3.1)),
|
|
listType(floatType()),
|
|
[]NullFloat64{{math.Inf(1), true}, {math.Inf(-1), true}, {}, {3.1, true}},
|
|
false,
|
|
},
|
|
{nullProto(), listType(floatType()), []NullFloat64(nil), false},
|
|
// FLOAT64 ARRAY with []float64
|
|
{
|
|
listProto(floatProto(math.Inf(1)), floatProto(math.Inf(-1)), floatProto(3.1)),
|
|
listType(floatType()),
|
|
[]float64{math.Inf(1), math.Inf(-1), 3.1},
|
|
false,
|
|
},
|
|
// TIMESTAMP
|
|
{timeProto(t1), timeType(), t1, false},
|
|
{timeProto(t1), timeType(), NullTime{t1, true}, false},
|
|
{nullProto(), timeType(), NullTime{}, false},
|
|
// TIMESTAMP ARRAY with []NullTime
|
|
{listProto(timeProto(t1), timeProto(t2), timeProto(t3), nullProto()), listType(timeType()), []NullTime{{t1, true}, {t2, true}, {t3, true}, {}}, false},
|
|
{nullProto(), listType(timeType()), []NullTime(nil), false},
|
|
// TIMESTAMP ARRAY with []time.Time
|
|
{listProto(timeProto(t1), timeProto(t2), timeProto(t3)), listType(timeType()), []time.Time{t1, t2, t3}, false},
|
|
// DATE
|
|
{dateProto(d1), dateType(), d1, false},
|
|
{dateProto(d1), dateType(), NullDate{d1, true}, false},
|
|
{nullProto(), dateType(), NullDate{}, false},
|
|
// DATE ARRAY with []NullDate
|
|
{listProto(dateProto(d1), dateProto(d2), nullProto()), listType(dateType()), []NullDate{{d1, true}, {d2, true}, {}}, false},
|
|
{nullProto(), listType(dateType()), []NullDate(nil), false},
|
|
// DATE ARRAY with []civil.Date
|
|
{listProto(dateProto(d1), dateProto(d2)), listType(dateType()), []civil.Date{d1, d2}, false},
|
|
// STRUCT ARRAY
|
|
// STRUCT schema is equal to the following Go struct:
|
|
// type s struct {
|
|
// Col1 NullInt64
|
|
// Col2 []struct {
|
|
// SubCol1 float64
|
|
// SubCol2 string
|
|
// }
|
|
// }
|
|
{
|
|
in: listProto(
|
|
listProto(
|
|
intProto(3),
|
|
listProto(
|
|
listProto(floatProto(3.14), stringProto("this")),
|
|
listProto(floatProto(0.57), stringProto("siht")),
|
|
),
|
|
),
|
|
listProto(
|
|
nullProto(),
|
|
nullProto(),
|
|
),
|
|
nullProto(),
|
|
),
|
|
t: listType(
|
|
structType(
|
|
mkField("Col1", intType()),
|
|
mkField(
|
|
"Col2",
|
|
listType(
|
|
structType(
|
|
mkField("SubCol1", floatType()),
|
|
mkField("SubCol2", stringType()),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
want: []NullRow{
|
|
{
|
|
Row: Row{
|
|
fields: []*sppb.StructType_Field{
|
|
mkField("Col1", intType()),
|
|
mkField(
|
|
"Col2",
|
|
listType(
|
|
structType(
|
|
mkField("SubCol1", floatType()),
|
|
mkField("SubCol2", stringType()),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
vals: []*proto3.Value{
|
|
intProto(3),
|
|
listProto(
|
|
listProto(floatProto(3.14), stringProto("this")),
|
|
listProto(floatProto(0.57), stringProto("siht")),
|
|
),
|
|
},
|
|
},
|
|
Valid: true,
|
|
},
|
|
{
|
|
Row: Row{
|
|
fields: []*sppb.StructType_Field{
|
|
mkField("Col1", intType()),
|
|
mkField(
|
|
"Col2",
|
|
listType(
|
|
structType(
|
|
mkField("SubCol1", floatType()),
|
|
mkField("SubCol2", stringType()),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
vals: []*proto3.Value{
|
|
nullProto(),
|
|
nullProto(),
|
|
},
|
|
},
|
|
Valid: true,
|
|
},
|
|
{},
|
|
},
|
|
fail: false,
|
|
},
|
|
{
|
|
in: listProto(
|
|
listProto(
|
|
intProto(3),
|
|
listProto(
|
|
listProto(floatProto(3.14), stringProto("this")),
|
|
listProto(floatProto(0.57), stringProto("siht")),
|
|
),
|
|
),
|
|
listProto(
|
|
nullProto(),
|
|
nullProto(),
|
|
),
|
|
nullProto(),
|
|
),
|
|
t: listType(
|
|
structType(
|
|
mkField("Col1", intType()),
|
|
mkField(
|
|
"Col2",
|
|
listType(
|
|
structType(
|
|
mkField("SubCol1", floatType()),
|
|
mkField("SubCol2", stringType()),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
want: []*struct {
|
|
Col1 NullInt64
|
|
StructCol []*struct {
|
|
SubCol1 NullFloat64
|
|
SubCol2 string
|
|
} `spanner:"Col2"`
|
|
}{
|
|
{
|
|
Col1: NullInt64{3, true},
|
|
StructCol: []*struct {
|
|
SubCol1 NullFloat64
|
|
SubCol2 string
|
|
}{
|
|
{
|
|
SubCol1: NullFloat64{3.14, true},
|
|
SubCol2: "this",
|
|
},
|
|
{
|
|
SubCol1: NullFloat64{0.57, true},
|
|
SubCol2: "siht",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Col1: NullInt64{},
|
|
StructCol: []*struct {
|
|
SubCol1 NullFloat64
|
|
SubCol2 string
|
|
}(nil),
|
|
},
|
|
nil,
|
|
},
|
|
fail: false,
|
|
},
|
|
// GenericColumnValue
|
|
{stringProto("abc"), stringType(), GenericColumnValue{stringType(), stringProto("abc")}, false},
|
|
{nullProto(), stringType(), GenericColumnValue{stringType(), nullProto()}, false},
|
|
// not actually valid (stringProto inside int list), but demonstrates pass-through.
|
|
{
|
|
in: listProto(intProto(5), nullProto(), stringProto("bcd")),
|
|
t: listType(intType()),
|
|
want: GenericColumnValue{
|
|
Type: listType(intType()),
|
|
Value: listProto(intProto(5), nullProto(), stringProto("bcd")),
|
|
},
|
|
fail: false,
|
|
},
|
|
} {
|
|
gotp := reflect.New(reflect.TypeOf(test.want))
|
|
if err := decodeValue(test.in, test.t, gotp.Interface()); err != nil {
|
|
if !test.fail {
|
|
t.Errorf("%d: cannot decode %v(%v): %v", i, test.in, test.t, err)
|
|
}
|
|
continue
|
|
}
|
|
if test.fail {
|
|
t.Errorf("%d: decoding %v(%v) succeeds unexpectedly, want error", i, test.in, test.t)
|
|
continue
|
|
}
|
|
got := reflect.Indirect(gotp).Interface()
|
|
if !testEqual(got, test.want) {
|
|
t.Errorf("%d: unexpected decoding result - got %v, want %v", i, got, test.want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test error cases for decodeValue.
|
|
func TestDecodeValueErrors(t *testing.T) {
|
|
for i, test := range []struct {
|
|
in *proto3.Value
|
|
t *sppb.Type
|
|
v interface{}
|
|
}{
|
|
{nullProto(), stringType(), nil},
|
|
{nullProto(), stringType(), 1},
|
|
} {
|
|
err := decodeValue(test.in, test.t, test.v)
|
|
if err == nil {
|
|
t.Errorf("#%d: want error, got nil", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test NaN encoding/decoding.
|
|
func TestNaN(t *testing.T) {
|
|
// Decode NaN value.
|
|
f := 0.0
|
|
nf := NullFloat64{}
|
|
// To float64
|
|
if err := decodeValue(floatProto(math.NaN()), floatType(), &f); err != nil {
|
|
t.Errorf("decodeValue returns %q for %v, want nil", err, floatProto(math.NaN()))
|
|
}
|
|
if !math.IsNaN(f) {
|
|
t.Errorf("f = %v, want %v", f, math.NaN())
|
|
}
|
|
// To NullFloat64
|
|
if err := decodeValue(floatProto(math.NaN()), floatType(), &nf); err != nil {
|
|
t.Errorf("decodeValue returns %q for %v, want nil", err, floatProto(math.NaN()))
|
|
}
|
|
if !math.IsNaN(nf.Float64) || !nf.Valid {
|
|
t.Errorf("f = %v, want %v", f, NullFloat64{math.NaN(), true})
|
|
}
|
|
// Encode NaN value
|
|
// From float64
|
|
v, _, err := encodeValue(math.NaN())
|
|
if err != nil {
|
|
t.Errorf("encodeValue returns %q for NaN, want nil", err)
|
|
}
|
|
x, ok := v.GetKind().(*proto3.Value_NumberValue)
|
|
if !ok {
|
|
t.Errorf("incorrect type for v.GetKind(): %T, want *proto3.Value_NumberValue", v.GetKind())
|
|
}
|
|
if !math.IsNaN(x.NumberValue) {
|
|
t.Errorf("x.NumberValue = %v, want %v", x.NumberValue, math.NaN())
|
|
}
|
|
// From NullFloat64
|
|
v, _, err = encodeValue(NullFloat64{math.NaN(), true})
|
|
if err != nil {
|
|
t.Errorf("encodeValue returns %q for NaN, want nil", err)
|
|
}
|
|
x, ok = v.GetKind().(*proto3.Value_NumberValue)
|
|
if !ok {
|
|
t.Errorf("incorrect type for v.GetKind(): %T, want *proto3.Value_NumberValue", v.GetKind())
|
|
}
|
|
if !math.IsNaN(x.NumberValue) {
|
|
t.Errorf("x.NumberValue = %v, want %v", x.NumberValue, math.NaN())
|
|
}
|
|
}
|
|
|
|
func TestGenericColumnValue(t *testing.T) {
|
|
for _, test := range []struct {
|
|
in GenericColumnValue
|
|
want interface{}
|
|
fail bool
|
|
}{
|
|
{GenericColumnValue{stringType(), stringProto("abc")}, "abc", false},
|
|
{GenericColumnValue{stringType(), stringProto("abc")}, 5, true},
|
|
{GenericColumnValue{listType(intType()), listProto(intProto(91), nullProto(), intProto(87))}, []NullInt64{{91, true}, {}, {87, true}}, false},
|
|
{GenericColumnValue{intType(), intProto(42)}, GenericColumnValue{intType(), intProto(42)}, false}, // trippy! :-)
|
|
} {
|
|
gotp := reflect.New(reflect.TypeOf(test.want))
|
|
if err := test.in.Decode(gotp.Interface()); err != nil {
|
|
if !test.fail {
|
|
t.Errorf("cannot decode %v to %v: %v", test.in, test.want, err)
|
|
}
|
|
continue
|
|
}
|
|
if test.fail {
|
|
t.Errorf("decoding %v to %v succeeds unexpectedly", test.in, test.want)
|
|
}
|
|
|
|
// Test we can go backwards as well.
|
|
v, err := newGenericColumnValue(test.want)
|
|
if err != nil {
|
|
t.Errorf("NewGenericColumnValue failed: %v", err)
|
|
continue
|
|
}
|
|
if !testEqual(*v, test.in) {
|
|
t.Errorf("unexpected encode result - got %v, want %v", v, test.in)
|
|
}
|
|
}
|
|
}
|