blob: d8be35c499164366f10b4bfd62a50afff1348544 [file] [log] [blame] [edit]
/*
Copyright 2017 Google LLC
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 (
"context"
"math/big"
"reflect"
"sort"
"strings"
"testing"
"time"
"cloud.google.com/go/civil"
sppb "cloud.google.com/go/spanner/apiv1/spannerpb"
. "cloud.google.com/go/spanner/internal/testutil"
proto3 "google.golang.org/protobuf/types/known/structpb"
)
// keysetProto returns protobuf encoding of valid spanner.KeySet.
func keysetProto(t *testing.T, ks KeySet) *sppb.KeySet {
k, err := ks.keySetProto()
if err != nil {
t.Fatalf("cannot convert keyset %v to protobuf: %v", ks, err)
}
return k
}
// Test encoding from spanner.Mutation to protobuf.
func TestMutationToProto(t *testing.T) {
utc, err := time.LoadLocation("UTC")
if err != nil {
t.Fatalf("Could not load UTC: %v", err)
}
r, _ := (&big.Rat{}).SetString("3.14")
for i, test := range []struct {
m *Mutation
want *sppb.Mutation
}{
// Delete Mutation
{
&Mutation{opDelete, "t_foo", Key{"foo"}, nil, nil, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Delete_{
Delete: &sppb.Mutation_Delete{
Table: "t_foo",
KeySet: keysetProto(t, Key{"foo"}),
},
},
},
},
// Insert Mutation
{
&Mutation{opInsert, "t_foo", KeySets(), []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{
Table: "t_foo",
Columns: []string{"col1", "col2"},
Values: []*proto3.ListValue{
{
Values: []*proto3.Value{intProto(1), intProto(2)},
},
},
},
},
},
},
// InsertOrUpdate Mutation
{
&Mutation{opInsertOrUpdate, "t_foo", KeySets(), []string{"col1", "col2"}, []interface{}{1.0, 2.0}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_InsertOrUpdate{
InsertOrUpdate: &sppb.Mutation_Write{
Table: "t_foo",
Columns: []string{"col1", "col2"},
Values: []*proto3.ListValue{
{
Values: []*proto3.Value{floatProto(1.0), floatProto(2.0)},
},
},
},
},
},
},
// Replace Mutation
{
&Mutation{opReplace, "t_foo", KeySets(), []string{"col1", "col2"}, []interface{}{"one", 2.0}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Replace{
Replace: &sppb.Mutation_Write{
Table: "t_foo",
Columns: []string{"col1", "col2"},
Values: []*proto3.ListValue{
{
Values: []*proto3.Value{stringProto("one"), floatProto(2.0)},
},
},
},
},
},
},
// Update Mutation
{
&Mutation{opUpdate, "t_foo", KeySets(), []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Update{
Update: &sppb.Mutation_Write{
Table: "t_foo",
Columns: []string{"col1", "col2"},
Values: []*proto3.ListValue{
{
Values: []*proto3.Value{stringProto("one"), nullProto()},
},
},
},
},
},
},
// Mutation with all supported data types
{
&Mutation{
opInsert,
"t_foo",
KeySets(),
[]string{"colBool", "colInt64", "colFloat64", "colNumeric", "colString", "colBytes", "colDate", "colTimestamp"},
[]interface{}{
true,
int64(100),
float64(3.14),
*r, // 3.14
"one",
[]byte{1, 2, 3},
civil.Date{Year: 2020, Month: 12, Day: 2},
time.Date(2020, time.December, 3, 8, 46, 58, 109, utc),
},
nil,
},
&sppb.Mutation{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{
Table: "t_foo",
Columns: []string{"colBool", "colInt64", "colFloat64", "colNumeric", "colString", "colBytes", "colDate", "colTimestamp"},
Values: []*proto3.ListValue{
{
Values: []*proto3.Value{
boolProto(true),
stringProto("100"),
floatProto(3.14),
stringProto("3.140000000"),
stringProto("one"),
bytesProto([]byte{1, 2, 3}),
stringProto("2020-12-02"),
stringProto("2020-12-03T08:46:58.000000109Z"),
},
},
},
},
},
},
},
} {
if got, err := test.m.proto(); err != nil || !testEqual(got, test.want) {
t.Errorf("%d:\n(%#v).proto() =\n (%v, %v)\nwant (%v, nil)", i, test.m, got, err, test.want)
}
// Verify that wrapping the proto mutation as a Spanner mutation produces a mutation that returns the same
// proto as the input argument.
wrapped, err := WrapMutation(test.want)
if err != nil {
t.Errorf("WrapMutation failed for %v: %v", test.m, err)
}
if g, w := wrapped.op, test.m.op; g != w {
t.Errorf("wrapped op mismatch\n Got: %v\n Want: %v", g, w)
}
if g, w := wrapped.table, test.m.table; g != w {
t.Errorf("wrapped table mismatch\n Got: %v\n Want: %v", g, w)
}
proto, err := wrapped.proto()
if err != nil {
t.Errorf("converting wrapped mutation %v to proto failed: %v", wrapped, err)
}
// Note: We test for reference equality here, as the result should be the same instance as the wrapped mutation.
if g, w := proto, test.want; g != w {
t.Errorf("proto of wrapped mutation mismatch\n Got: %v\nWant: %v", g, w)
}
}
}
// mutationColumnSorter implements sort.Interface for sorting column-value pairs in a Mutation by column names.
type mutationColumnSorter struct {
Mutation
}
// newMutationColumnSorter creates new instance of mutationColumnSorter by duplicating the input Mutation so that
// sorting won't change the input Mutation.
func newMutationColumnSorter(m *Mutation) *mutationColumnSorter {
return &mutationColumnSorter{
Mutation{
m.op,
m.table,
m.keySet,
append([]string(nil), m.columns...),
append([]interface{}(nil), m.values...),
nil,
},
}
}
// Len implements sort.Interface.Len.
func (ms *mutationColumnSorter) Len() int {
return len(ms.columns)
}
// Swap implements sort.Interface.Swap.
func (ms *mutationColumnSorter) Swap(i, j int) {
ms.columns[i], ms.columns[j] = ms.columns[j], ms.columns[i]
ms.values[i], ms.values[j] = ms.values[j], ms.values[i]
}
// Less implements sort.Interface.Less.
func (ms *mutationColumnSorter) Less(i, j int) bool {
return strings.Compare(ms.columns[i], ms.columns[j]) < 0
}
// mutationEqual returns true if two mutations in question are equal
// to each other.
func mutationEqual(t *testing.T, m1, m2 Mutation) bool {
// Two mutations are considered to be equal even if their column values have different
// orders.
ms1 := newMutationColumnSorter(&m1)
ms2 := newMutationColumnSorter(&m2)
sort.Sort(ms1)
sort.Sort(ms2)
return testEqual(ms1, ms2)
}
// Test helper functions which help to generate spanner.Mutation.
func TestMutationHelpers(t *testing.T) {
for _, test := range []struct {
m string
got *Mutation
want *Mutation
}{
{
"Insert",
Insert("t_foo", []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}),
&Mutation{opInsert, "t_foo", nil, []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}, nil},
},
{
"InsertMap",
InsertMap("t_foo", map[string]interface{}{"col1": int64(1), "col2": int64(2)}),
&Mutation{opInsert, "t_foo", nil, []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}, nil},
},
{
"InsertStruct",
func() *Mutation {
m, err := InsertStruct(
"t_foo",
struct {
notCol bool
Col1 int64 `spanner:"col1"`
Col2 int64 `spanner:"col2"`
}{false, int64(1), int64(2)},
)
if err != nil {
t.Errorf("cannot convert struct into mutation: %v", err)
}
return m
}(),
&Mutation{opInsert, "t_foo", nil, []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}, nil},
},
{
"Update",
Update("t_foo", []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}),
&Mutation{opUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}, nil},
},
{
"UpdateMap",
UpdateMap("t_foo", map[string]interface{}{"col1": "one", "col2": []byte(nil)}),
&Mutation{opUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}, nil},
},
{
"UpdateStruct",
func() *Mutation {
m, err := UpdateStruct(
"t_foo",
struct {
Col1 string `spanner:"col1"`
notCol int
Col2 []byte `spanner:"col2"`
}{"one", 1, nil},
)
if err != nil {
t.Errorf("cannot convert struct into mutation: %v", err)
}
return m
}(),
&Mutation{opUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}, nil},
},
{
"InsertOrUpdate",
InsertOrUpdate("t_foo", []string{"col1", "col2"}, []interface{}{1.0, 2.0}),
&Mutation{opInsertOrUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{1.0, 2.0}, nil},
},
{
"InsertOrUpdateMap",
InsertOrUpdateMap("t_foo", map[string]interface{}{"col1": 1.0, "col2": 2.0}),
&Mutation{opInsertOrUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{1.0, 2.0}, nil},
},
{
"InsertOrUpdateStruct",
func() *Mutation {
m, err := InsertOrUpdateStruct(
"t_foo",
struct {
Col1 float64 `spanner:"col1"`
Col2 float64 `spanner:"col2"`
notCol float64
}{1.0, 2.0, 3.0},
)
if err != nil {
t.Errorf("cannot convert struct into mutation: %v", err)
}
return m
}(),
&Mutation{opInsertOrUpdate, "t_foo", nil, []string{"col1", "col2"}, []interface{}{1.0, 2.0}, nil},
},
{
"Replace",
Replace("t_foo", []string{"col1", "col2"}, []interface{}{"one", 2.0}),
&Mutation{opReplace, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", 2.0}, nil},
},
{
"ReplaceMap",
ReplaceMap("t_foo", map[string]interface{}{"col1": "one", "col2": 2.0}),
&Mutation{opReplace, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", 2.0}, nil},
},
{
"ReplaceStruct",
func() *Mutation {
m, err := ReplaceStruct(
"t_foo",
struct {
Col1 string `spanner:"col1"`
Col2 float64 `spanner:"col2"`
notCol string
}{"one", 2.0, "foo"},
)
if err != nil {
t.Errorf("cannot convert struct into mutation: %v", err)
}
return m
}(),
&Mutation{opReplace, "t_foo", nil, []string{"col1", "col2"}, []interface{}{"one", 2.0}, nil},
},
{
"Delete",
Delete("t_foo", Key{"foo"}),
&Mutation{opDelete, "t_foo", Key{"foo"}, nil, nil, nil},
},
{
"DeleteRange",
Delete("t_foo", KeyRange{Key{"bar"}, Key{"foo"}, ClosedClosed}),
&Mutation{opDelete, "t_foo", KeyRange{Key{"bar"}, Key{"foo"}, ClosedClosed}, nil, nil, nil},
},
} {
if !mutationEqual(t, *test.got, *test.want) {
t.Errorf("%v: got Mutation %v, want %v", test.m, test.got, test.want)
}
}
}
// Test encoding non-struct types by using *Struct helpers.
func TestBadStructs(t *testing.T) {
val := "i_am_not_a_struct"
wantErr := errNotStruct(val)
if _, gotErr := InsertStruct("t_test", val); !testEqual(gotErr, wantErr) {
t.Errorf("InsertStruct(%q) returns error %v, want %v", val, gotErr, wantErr)
}
if _, gotErr := InsertOrUpdateStruct("t_test", val); !testEqual(gotErr, wantErr) {
t.Errorf("InsertOrUpdateStruct(%q) returns error %v, want %v", val, gotErr, wantErr)
}
if _, gotErr := UpdateStruct("t_test", val); !testEqual(gotErr, wantErr) {
t.Errorf("UpdateStruct(%q) returns error %v, want %v", val, gotErr, wantErr)
}
if _, gotErr := ReplaceStruct("t_test", val); !testEqual(gotErr, wantErr) {
t.Errorf("ReplaceStruct(%q) returns error %v, want %v", val, gotErr, wantErr)
}
}
func TestStructToMutationParams(t *testing.T) {
// Tests cases not covered elsewhere.
type S struct{ F interface{} }
for _, test := range []struct {
in interface{}
wantCols []string
wantVals []interface{}
wantErr error
}{
{nil, nil, nil, errNotStruct(nil)},
{3, nil, nil, errNotStruct(3)},
{(*S)(nil), nil, nil, nil},
{&S{F: 1}, []string{"F"}, []interface{}{1}, nil},
{&S{F: CommitTimestamp}, []string{"F"}, []interface{}{CommitTimestamp}, nil},
} {
gotCols, gotVals, gotErr := structToMutationParams(test.in)
if !testEqual(gotCols, test.wantCols) {
t.Errorf("%#v: got cols %v, want %v", test.in, gotCols, test.wantCols)
}
if !testEqual(gotVals, test.wantVals) {
t.Errorf("%#v: got vals %v, want %v", test.in, gotVals, test.wantVals)
}
if !testEqual(gotErr, test.wantErr) {
t.Errorf("%#v: got err %v, want %v", test.in, gotErr, test.wantErr)
}
}
}
func TestStructToMutationParams_ReadOnly(t *testing.T) {
t.Parallel()
type ReadOnly struct {
ID int64
Name string `spanner:"->"`
}
in := &ReadOnly{ID: 1, Name: "foo"}
wantCols := []string{"ID"}
wantVals := []interface{}{int64(1)}
gotCols, gotVals, err := structToMutationParams(in)
if err != nil {
t.Fatal(err)
}
if !testEqual(gotCols, wantCols) {
t.Errorf("got cols %v, want %v", gotCols, wantCols)
}
if !testEqual(gotVals, wantVals) {
t.Errorf("got vals %v, want %v", gotVals, wantVals)
}
}
func TestReadWrite_Generated(t *testing.T) {
t.Parallel()
server, client, teardown := setupMockedTestServer(t)
defer teardown()
// The full name is generated by the server.
server.TestSpanner.PutStatementResult(
"SELECT Id, FirstName, LastName, FullName FROM Users WHERE Id = 1",
&StatementResult{
Type: StatementResultResultSet,
ResultSet: &sppb.ResultSet{
Metadata: &sppb.ResultSetMetadata{
RowType: &sppb.StructType{
Fields: []*sppb.StructType_Field{
{Name: "Id", Type: &sppb.Type{Code: sppb.TypeCode_INT64}},
{Name: "FirstName", Type: &sppb.Type{Code: sppb.TypeCode_STRING}},
{Name: "LastName", Type: &sppb.Type{Code: sppb.TypeCode_STRING}},
{Name: "FullName", Type: &sppb.Type{Code: sppb.TypeCode_STRING}},
},
},
},
Rows: []*proto3.ListValue{
{
Values: []*proto3.Value{
intProto(1),
stringProto("First"),
stringProto("Last"),
stringProto("First Last"),
},
},
},
},
},
)
type User struct {
ID int64 `spanner:"Id"`
FirstName string
LastName string
FullName string `spanner:"->"`
}
user := &User{
ID: 1,
FirstName: "First",
LastName: "Last",
}
m, err := InsertStruct("Users", user)
if err != nil {
t.Fatal(err)
}
_, err = client.Apply(context.Background(), []*Mutation{m})
if err != nil {
t.Fatal(err)
}
// Verify that the generated column 'FullName' was excluded from the write mutation.
reqs := drainRequestsFromServer(server.TestSpanner)
var commitReq *sppb.CommitRequest
for _, r := range reqs {
if c, ok := r.(*sppb.CommitRequest); ok {
commitReq = c
}
}
if commitReq == nil {
t.Fatalf("no CommitRequest captured; got %v", reqs)
}
// Find the write mutation for Users.
var write *sppb.Mutation_Write
for _, mut := range commitReq.Mutations {
if ins := mut.GetInsert(); ins != nil && ins.Table == "Users" {
write = ins
break
}
}
if write == nil {
t.Fatalf("no write mutation for table Users in CommitRequest: %v", commitReq.Mutations)
}
// Ensure FullName is not present in columns.
for _, col := range write.Columns {
if col == "FullName" {
t.Fatalf("generated column FullName must be excluded from write.Columns: %v", write.Columns)
}
}
wantCols := []string{"Id", "FirstName", "LastName"}
if !reflect.DeepEqual(write.Columns, wantCols) {
t.Fatalf("write.Columns mismatch\ngot %v\nwant %v", write.Columns, wantCols)
}
if g, w := len(write.Values), 1; g != w {
t.Fatalf("write.Values length mismatch: got %d, want 1", len(write.Values))
}
if g, w := len(write.Values[0].Values), len(wantCols); g != w {
t.Fatalf("write.Values[0] length mismatch\n Got: %v\nWant: %v", g, w)
}
iter := client.Single().Query(context.Background(), NewStatement("SELECT Id, FirstName, LastName, FullName FROM Users WHERE Id = 1"))
row, err := iter.Next()
if err != nil {
t.Fatal(err)
}
var got User
if err := row.ToStruct(&got); err != nil {
t.Fatal(err)
}
want := &User{
ID: 1,
FirstName: "First",
LastName: "Last",
FullName: "First Last",
}
if !testEqual(got, *want) {
t.Errorf("got %v, want %v", got, *want)
}
}
// Test encoding Mutation into proto.
func TestEncodeMutation(t *testing.T) {
for _, test := range []struct {
name string
mutation Mutation
wantProto *sppb.Mutation
wantErr error
}{
{
"OpDelete",
Mutation{opDelete, "t_test", Key{1}, nil, nil, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Delete_{
Delete: &sppb.Mutation_Delete{
Table: "t_test",
KeySet: &sppb.KeySet{
Keys: []*proto3.ListValue{listValueProto(intProto(1))},
},
},
},
},
nil,
},
{
"OpDelete - Key error",
Mutation{opDelete, "t_test", Key{struct{}{}}, nil, nil, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Delete_{
Delete: &sppb.Mutation_Delete{
Table: "t_test",
KeySet: &sppb.KeySet{},
},
},
},
errInvdKeyPartType(struct{}{}),
},
{
"OpInsert",
Mutation{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{listValueProto(stringProto("foo"), intProto(1))},
},
},
},
nil,
},
{
"OpInsert - Value Type Error",
Mutation{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{struct{}{}, 1}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{},
},
},
errEncoderUnsupportedType(struct{}{}),
},
{
"OpInsertOrUpdate",
Mutation{opInsertOrUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_InsertOrUpdate{
InsertOrUpdate: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{listValueProto(stringProto("foo"), intProto(1))},
},
},
},
nil,
},
{
"OpInsertOrUpdate - Value Type Error",
Mutation{opInsertOrUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{struct{}{}, 1}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_InsertOrUpdate{
InsertOrUpdate: &sppb.Mutation_Write{},
},
},
errEncoderUnsupportedType(struct{}{}),
},
{
"OpReplace",
Mutation{opReplace, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Replace{
Replace: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{listValueProto(stringProto("foo"), intProto(1))},
},
},
},
nil,
},
{
"OpReplace - Value Type Error",
Mutation{opReplace, "t_test", nil, []string{"key", "val"}, []interface{}{struct{}{}, 1}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Replace{
Replace: &sppb.Mutation_Write{},
},
},
errEncoderUnsupportedType(struct{}{}),
},
{
"OpUpdate",
Mutation{opUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Update{
Update: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{listValueProto(stringProto("foo"), intProto(1))},
},
},
},
nil,
},
{
"OpUpdate - Value Type Error",
Mutation{opUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{struct{}{}, 1}, nil},
&sppb.Mutation{
Operation: &sppb.Mutation_Update{
Update: &sppb.Mutation_Write{},
},
},
errEncoderUnsupportedType(struct{}{}),
},
{
"OpKnown - Unknown Mutation Operation Code",
Mutation{op(100), "t_test", nil, nil, nil, nil},
&sppb.Mutation{},
errInvdMutationOp(Mutation{op(100), "t_test", nil, nil, nil, nil}),
},
} {
gotProto, gotErr := test.mutation.proto()
if gotErr != nil {
if !testEqual(gotErr, test.wantErr) {
t.Errorf("%s: %v.proto() returns error %v, want %v", test.name, test.mutation, gotErr, test.wantErr)
}
continue
}
if !testEqual(gotProto, test.wantProto) {
t.Errorf("%s: %v.proto() = (%v, nil), want (%v, nil)", test.name, test.mutation, gotProto, test.wantProto)
}
}
}
// Test Encoding an array of mutations.
func TestEncodeMutationArray(t *testing.T) {
tests := []struct {
name string
ms []*Mutation
want []*sppb.Mutation
wantMutationKey *sppb.Mutation
wantErr error
}{
// Test case for empty mutation list
{
name: "Empty Mutation List",
ms: []*Mutation{},
want: []*sppb.Mutation{},
wantMutationKey: nil,
wantErr: nil,
},
// Test case for only insert mutations
{
name: "Only Inserts",
ms: []*Mutation{
{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}, nil},
{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{"bar", 2}, nil},
{opInsert, "t_test", nil, []string{"key", "val", "col3"}, []interface{}{"bar2", 3, 4}, nil},
},
want: []*sppb.Mutation{
{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{
listValueProto(stringProto("foo"), intProto(1)),
},
},
},
},
{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{
listValueProto(stringProto("bar"), intProto(2)),
},
},
},
},
{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val", "col3"},
Values: []*proto3.ListValue{
listValueProto(stringProto("bar2"), intProto(3), intProto(4)),
},
},
},
},
},
wantMutationKey: &sppb.Mutation{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val", "col3"},
Values: []*proto3.ListValue{
listValueProto(stringProto("bar2"), intProto(3), intProto(4)),
},
},
},
},
wantErr: nil,
},
// Test case for mixed operations
{
name: "Mixed Operations",
ms: []*Mutation{
{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{"foo", 1}, nil},
{opUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"bar", 2}, nil},
},
want: []*sppb.Mutation{
{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{
listValueProto(stringProto("foo"), intProto(1)),
},
},
},
},
{
Operation: &sppb.Mutation_Update{
Update: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{
listValueProto(stringProto("bar"), intProto(2)),
},
},
},
},
},
wantMutationKey: &sppb.Mutation{
Operation: &sppb.Mutation_Update{
Update: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{
listValueProto(stringProto("bar"), intProto(2)),
},
},
},
},
wantErr: nil,
},
// Test case for error in mutation
{
name: "Error in Mutation",
ms: []*Mutation{
{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{struct{}{}, 1}, nil},
},
want: []*sppb.Mutation{},
wantMutationKey: nil,
wantErr: errEncoderUnsupportedType(struct{}{}),
},
// Test case for only delete mutations
{
name: "Only Deletes",
ms: []*Mutation{
{opDelete, "t_test", Key{"foo"}, nil, nil, nil},
{opDelete, "t_test", Key{"bar"}, nil, nil, nil},
},
want: []*sppb.Mutation{
{
Operation: &sppb.Mutation_Delete_{
Delete: &sppb.Mutation_Delete{
Table: "t_test",
KeySet: &sppb.KeySet{
Keys: []*proto3.ListValue{
listValueProto(stringProto("foo")),
},
},
},
},
},
{
Operation: &sppb.Mutation_Delete_{
Delete: &sppb.Mutation_Delete{
Table: "t_test",
KeySet: &sppb.KeySet{
Keys: []*proto3.ListValue{
listValueProto(stringProto("bar")),
},
},
},
},
},
},
wantMutationKey: &sppb.Mutation{
Operation: &sppb.Mutation_Delete_{
Delete: &sppb.Mutation_Delete{
Table: "t_test",
KeySet: &sppb.KeySet{
Keys: []*proto3.ListValue{
listValueProto(stringProto("bar")),
},
},
},
},
},
wantErr: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotProto, gotMutationKey, gotErr := mutationsProto(test.ms)
if gotErr != nil {
if !testEqual(gotErr, test.wantErr) {
t.Errorf("mutationsProto(%v) returns error %v, want %v", test.ms, gotErr, test.wantErr)
}
return
}
if !testEqual(gotProto, test.want) {
t.Errorf("mutationsProto(%v) = (%v, nil), want (%v, nil)", test.ms, gotProto, test.want)
}
if test.wantMutationKey != nil {
if reflect.TypeOf(gotMutationKey.Operation) != reflect.TypeOf(test.wantMutationKey.Operation) {
t.Errorf("mutationsProto(%v) returns mutation key %v, want %v", test.ms, gotMutationKey, test.wantMutationKey)
}
}
})
}
}
func TestEncodeMutationGroupArray(t *testing.T) {
for _, test := range []struct {
name string
mgs []*MutationGroup
want []*sppb.BatchWriteRequest_MutationGroup
wantErr error
}{
{
"Multiple Mutations",
[]*MutationGroup{
{[]*Mutation{
{opDelete, "t_test", Key{"bar"}, nil, nil, nil},
{opInsertOrUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo1", 1}, nil},
}},
{[]*Mutation{
{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{"foo2", 1}, nil},
{opUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo3", 1}, nil},
}},
{[]*Mutation{
{opReplace, "t_test", nil, []string{"key", "val"}, []interface{}{"foo4", 1}, nil},
}},
},
[]*sppb.BatchWriteRequest_MutationGroup{
{Mutations: []*sppb.Mutation{
{
Operation: &sppb.Mutation_Delete_{
Delete: &sppb.Mutation_Delete{
Table: "t_test",
KeySet: &sppb.KeySet{
Keys: []*proto3.ListValue{listValueProto(stringProto("bar"))},
},
},
},
},
{
Operation: &sppb.Mutation_InsertOrUpdate{
InsertOrUpdate: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{listValueProto(stringProto("foo1"), intProto(1))},
},
},
},
}},
{Mutations: []*sppb.Mutation{
{
Operation: &sppb.Mutation_Insert{
Insert: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{listValueProto(stringProto("foo2"), intProto(1))},
},
},
},
{
Operation: &sppb.Mutation_Update{
Update: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{listValueProto(stringProto("foo3"), intProto(1))},
},
},
},
}},
{Mutations: []*sppb.Mutation{
{
Operation: &sppb.Mutation_Replace{
Replace: &sppb.Mutation_Write{
Table: "t_test",
Columns: []string{"key", "val"},
Values: []*proto3.ListValue{listValueProto(stringProto("foo4"), intProto(1))},
},
},
},
}},
},
nil,
},
{
"Multiple Mutations - Bad Mutation",
[]*MutationGroup{
{[]*Mutation{
{opDelete, "t_test", Key{"bar"}, nil, nil, nil},
{opInsertOrUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo1", struct{}{}}, nil},
}},
{[]*Mutation{
{opInsert, "t_test", nil, []string{"key", "val"}, []interface{}{"foo2", 1}, nil},
{opUpdate, "t_test", nil, []string{"key", "val"}, []interface{}{"foo3", 1}, nil},
}},
},
[]*sppb.BatchWriteRequest_MutationGroup{},
errEncoderUnsupportedType(struct{}{}),
},
} {
gotProto, gotErr := mutationGroupsProto(test.mgs)
if gotErr != nil {
if !testEqual(gotErr, test.wantErr) {
t.Errorf("%v: mutationGroupsProto(%v) returns error %v, want %v", test.name, test.mgs, gotErr, test.wantErr)
}
continue
}
if !testEqual(gotProto, test.want) {
t.Errorf("%v: mutationGroupsProto(%v) = (%v, nil), want (%v, nil)", test.name, test.mgs, gotProto, test.want)
}
}
}
func BenchmarkMutationsProto(b *testing.B) {
type benchmarkCase struct {
name string
mutations []*Mutation
}
benchmarkCases := []benchmarkCase{
{
name: "small number of mutations",
mutations: []*Mutation{
Insert("t_foo", []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}),
Update("t_foo", []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}),
InsertOrUpdate("t_foo", []string{"col1", "col2"}, []interface{}{1.0, 2.0}),
Replace("t_foo", []string{"col1", "col2"}, []interface{}{"one", 2.0}),
Delete("t_foo", Key{"foo"}),
},
},
{
name: "large number of mutations",
mutations: func() []*Mutation {
var mutations []*Mutation
for i := 0; i < 20; i++ {
mutations = append(mutations, Insert("t_foo", []string{"col1", "col2"}, []interface{}{int64(i), int64(i + 1)}))
mutations = append(mutations, Update("t_foo", []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}))
mutations = append(mutations, InsertOrUpdate("t_foo", []string{"col1", "col2"}, []interface{}{1.0, 2.0}))
mutations = append(mutations, Replace("t_foo", []string{"col1", "col2"}, []interface{}{"one", 2.0}))
mutations = append(mutations, Delete("t_foo", Key{i}))
}
return mutations
}(),
},
{
name: "mixed type of mutations",
mutations: []*Mutation{
Insert("t_foo", []string{"col1", "col2"}, []interface{}{int64(1), int64(2)}),
Update("t_foo", []string{"col1", "col2"}, []interface{}{"one", []byte(nil)}),
Delete("t_foo", Key{"foo"}),
Insert("t_bar", []string{"col1"}, []interface{}{"bar"}),
},
},
}
for _, bc := range benchmarkCases {
b.Run(bc.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _, _ = mutationsProto(bc.mutations)
}
})
}
}