Skip to content

Commit

Permalink
feat(spanner): add support for NUMERIC data type (#2415)
Browse files Browse the repository at this point in the history
* spanner: add support for NUMERIC data type
* Add integration test for Numeric support.
  • Loading branch information
hengfengli committed Sep 9, 2020
1 parent 2cf2adb commit 7430d72
Show file tree
Hide file tree
Showing 6 changed files with 555 additions and 38 deletions.
3 changes: 2 additions & 1 deletion spanner/cmp_test.go
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package spanner

import (
"math/big"
"strings"

"cloud.google.com/go/internal/testutil"
Expand All @@ -27,7 +28,7 @@ import (
func testEqual(a, b interface{}) bool {
return testutil.Equal(a, b,
cmp.AllowUnexported(TimestampBound{}, Error{}, TransactionOutcomeUnknownError{},
Mutation{}, Row{}, Partition{}, BatchReadOnlyTransactionID{}),
Mutation{}, Row{}, Partition{}, BatchReadOnlyTransactionID{}, big.Rat{}, big.Int{}),
cmp.FilterPath(func(path cmp.Path) bool {
// Ignore Error.state, Error.sizeCache, and Error.unknownFields
if strings.HasSuffix(path.GoString(), ".err.(*status.Error).state") {
Expand Down
75 changes: 73 additions & 2 deletions spanner/integration_test.go
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"log"
"math"
"math/big"
"os"
"reflect"
"regexp"
Expand Down Expand Up @@ -1318,7 +1319,44 @@ func TestIntegration_BasicTypes(t *testing.T) {

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
client, _, cleanup := prepareIntegrationTest(ctx, t, DefaultSessionPoolConfig, singerDBStatements)
stmts := singerDBStatements
if !isEmulatorEnvSet() {
stmts = []string{
`CREATE TABLE Singers (
SingerId INT64 NOT NULL,
FirstName STRING(1024),
LastName STRING(1024),
SingerInfo BYTES(MAX)
) PRIMARY KEY (SingerId)`,
`CREATE INDEX SingerByName ON Singers(FirstName, LastName)`,
`CREATE TABLE Accounts (
AccountId INT64 NOT NULL,
Nickname STRING(100),
Balance INT64 NOT NULL,
) PRIMARY KEY (AccountId)`,
`CREATE INDEX AccountByNickname ON Accounts(Nickname) STORING (Balance)`,
`CREATE TABLE Types (
RowID INT64 NOT NULL,
String STRING(MAX),
StringArray ARRAY<STRING(MAX)>,
Bytes BYTES(MAX),
BytesArray ARRAY<BYTES(MAX)>,
Int64a INT64,
Int64Array ARRAY<INT64>,
Bool BOOL,
BoolArray ARRAY<BOOL>,
Float64 FLOAT64,
Float64Array ARRAY<FLOAT64>,
Date DATE,
DateArray ARRAY<DATE>,
Timestamp TIMESTAMP,
TimestampArray ARRAY<TIMESTAMP>,
Numeric NUMERIC,
NumericArray ARRAY<NUMERIC>
) PRIMARY KEY (RowID)`,
}
}
client, _, cleanup := prepareIntegrationTest(ctx, t, DefaultSessionPoolConfig, stmts)
defer cleanup()

t1, _ := time.Parse(time.RFC3339Nano, "2016-11-15T15:04:05.999999999Z")
Expand All @@ -1330,6 +1368,10 @@ func TestIntegration_BasicTypes(t *testing.T) {
d2, _ := civil.ParseDate("0001-01-01")
d3, _ := civil.ParseDate("9999-12-31")

n0 := big.Rat{}
n1 := *big.NewRat(123456789, 1)
n2 := *big.NewRat(123456789, 1000000000)

tests := []struct {
col string
val interface{}
Expand Down Expand Up @@ -1420,6 +1462,31 @@ func TestIntegration_BasicTypes(t *testing.T) {
{col: "TimestampArray", val: []time.Time{t1, t2, t3}, want: []NullTime{{t1, true}, {t2, true}, {t3, true}}},
}

if !isEmulatorEnvSet() {
for _, tc := range []struct {
col string
val interface{}
want interface{}
}{
{col: "Numeric", val: n1},
{col: "Numeric", val: n2},
{col: "Numeric", val: n1, want: NullNumeric{n1, true}},
{col: "Numeric", val: n2, want: NullNumeric{n2, true}},
{col: "Numeric", val: NullNumeric{n1, true}, want: n1},
{col: "Numeric", val: NullNumeric{n1, true}, want: NullNumeric{n1, true}},
{col: "Numeric", val: NullNumeric{n0, false}},
{col: "Numeric", val: nil, want: NullNumeric{}},
{col: "NumericArray", val: []big.Rat(nil), want: []NullNumeric(nil)},
{col: "NumericArray", val: []big.Rat{}, want: []NullNumeric{}},
{col: "NumericArray", val: []big.Rat{n1, n2}, want: []NullNumeric{{n1, true}, {n2, true}}},
{col: "NumericArray", val: []NullNumeric(nil)},
{col: "NumericArray", val: []NullNumeric{}},
{col: "NumericArray", val: []NullNumeric{{n1, true}, {n2, true}, {}}},
} {
tests = append(tests, tc)
}
}

// Write rows into table first.
var muts []*Mutation
for i, test := range tests {
Expand Down Expand Up @@ -3124,8 +3191,12 @@ func maxDuration(a, b time.Duration) time.Duration {
return b
}

func isEmulatorEnvSet() bool {
return os.Getenv("SPANNER_EMULATOR_HOST") != ""
}

func skipEmulatorTest(t *testing.T) {
if os.Getenv("SPANNER_EMULATOR_HOST") != "" {
if isEmulatorEnvSet() {
t.Skip("Skipping testing against the emulator.")
}
}
9 changes: 9 additions & 0 deletions spanner/protoutils.go
Expand Up @@ -18,6 +18,7 @@ package spanner

import (
"encoding/base64"
"math/big"
"strconv"
"time"

Expand Down Expand Up @@ -64,6 +65,14 @@ func floatType() *sppb.Type {
return &sppb.Type{Code: sppb.TypeCode_FLOAT64}
}

func numericProto(n *big.Rat) *proto3.Value {
return &proto3.Value{Kind: &proto3.Value_StringValue{StringValue: NumericString(n)}}
}

func numericType() *sppb.Type {
return &sppb.Type{Code: sppb.TypeCode_NUMERIC}
}

func bytesProto(b []byte) *proto3.Value {
return &proto3.Value{Kind: &proto3.Value_StringValue{StringValue: base64.StdEncoding.EncodeToString(b)}}
}
Expand Down
4 changes: 2 additions & 2 deletions spanner/row_test.go
Expand Up @@ -745,7 +745,7 @@ func TestBrokenRow(t *testing.T) {
[]*proto3.Value{stringProto("nan")},
},
&NullFloat64{},
errDecodeColumn(0, errUnexpectedNumStr("nan")),
errDecodeColumn(0, errUnexpectedFloat64Str("nan")),
},
{
// Field specifies FLOAT64 type, but value is wrongly encoded.
Expand All @@ -756,7 +756,7 @@ func TestBrokenRow(t *testing.T) {
[]*proto3.Value{stringProto("nan")},
},
proto.Float64(0),
errDecodeColumn(0, errUnexpectedNumStr("nan")),
errDecodeColumn(0, errUnexpectedFloat64Str("nan")),
},
{
// Field specifies BYTES type, value is having a nil Kind.
Expand Down

0 comments on commit 7430d72

Please sign in to comment.