blob: c058be7f894f08b2c76c15c7f223edb304cd9e8c [file] [log] [blame]
// 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 datastore
import (
"context"
"testing"
pb "google.golang.org/genproto/googleapis/datastore/v1"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestNewTransaction(t *testing.T) {
var got *pb.BeginTransactionRequest
client := &Client{
dataset: "project",
client: &fakeDatastoreClient{
beginTransaction: func(req *pb.BeginTransactionRequest) (*pb.BeginTransactionResponse, error) {
got = req
return &pb.BeginTransactionResponse{
Transaction: []byte("tid"),
}, nil
},
},
}
ctx := context.Background()
rt := timestamppb.Now()
for _, test := range []struct {
settings *transactionSettings
want *pb.BeginTransactionRequest
}{
{
&transactionSettings{},
&pb.BeginTransactionRequest{ProjectId: "project"},
},
{
&transactionSettings{readOnly: true},
&pb.BeginTransactionRequest{
ProjectId: "project",
TransactionOptions: &pb.TransactionOptions{
Mode: &pb.TransactionOptions_ReadOnly_{ReadOnly: &pb.TransactionOptions_ReadOnly{}},
},
},
},
{
&transactionSettings{prevID: []byte("tid")},
&pb.BeginTransactionRequest{
ProjectId: "project",
TransactionOptions: &pb.TransactionOptions{
Mode: &pb.TransactionOptions_ReadWrite_{ReadWrite: &pb.TransactionOptions_ReadWrite{
PreviousTransaction: []byte("tid"),
},
},
},
},
},
{
&transactionSettings{readOnly: true, readTime: rt},
&pb.BeginTransactionRequest{
ProjectId: "project",
TransactionOptions: &pb.TransactionOptions{
Mode: &pb.TransactionOptions_ReadOnly_{ReadOnly: &pb.TransactionOptions_ReadOnly{
ReadTime: rt,
}},
},
},
},
} {
_, err := client.newTransaction(ctx, test.settings)
if err != nil {
t.Fatal(err)
}
if !proto.Equal(got, test.want) {
t.Errorf("%+v:\ngot %+v\nwant %+v", test.settings, got, test.want)
}
}
}
func TestBeginLaterTransactionOption(t *testing.T) {
type ent struct {
A int
}
type addRPCInput struct {
wantReq proto.Message
resp interface{}
}
mockKind := "mockKind"
mockTxnID := []byte("tid")
mockKey := NameKey(mockKind, "testName", nil)
mockEntity := &pb.Entity{
Key: keyToProto(mockKey),
Properties: map[string]*pb.Value{
"A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 0}},
},
}
mockEntityResults := []*pb.EntityResult{
{
Entity: mockEntity,
Version: 1,
},
}
// Requests and responses to be used in tests
txnReadOptions := &pb.ReadOptions{
ConsistencyType: &pb.ReadOptions_Transaction{
Transaction: mockTxnID,
},
}
newTxnReadOptions := &pb.ReadOptions{
ConsistencyType: &pb.ReadOptions_NewTransaction{},
}
lookupReqWithTxn := &pb.LookupRequest{
ProjectId: mockProjectID,
DatabaseId: "",
Keys: []*pb.Key{
keyToProto(mockKey),
},
ReadOptions: txnReadOptions,
}
lookupResWithTxn := &pb.LookupResponse{
Found: mockEntityResults,
}
lookupReqWithNewTxn := &pb.LookupRequest{
ProjectId: mockProjectID,
DatabaseId: "",
Keys: []*pb.Key{
keyToProto(mockKey),
},
ReadOptions: newTxnReadOptions,
}
lookupResWithNewTxn := &pb.LookupResponse{
Transaction: mockTxnID,
Found: mockEntityResults,
}
runQueryReqWithTxn := &pb.RunQueryRequest{
ProjectId: mockProjectID,
QueryType: &pb.RunQueryRequest_Query{Query: &pb.Query{
Kind: []*pb.KindExpression{{Name: mockKind}},
}},
ReadOptions: txnReadOptions,
}
runQueryResWithTxn := &pb.RunQueryResponse{
Batch: &pb.QueryResultBatch{
MoreResults: pb.QueryResultBatch_NO_MORE_RESULTS,
EntityResultType: pb.EntityResult_FULL,
EntityResults: mockEntityResults,
},
}
runQueryReqWithNewTxn := &pb.RunQueryRequest{
ProjectId: mockProjectID,
QueryType: &pb.RunQueryRequest_Query{Query: &pb.Query{
Kind: []*pb.KindExpression{{Name: mockKind}},
}},
ReadOptions: newTxnReadOptions,
}
runQueryResWithNewTxn := &pb.RunQueryResponse{
Transaction: mockTxnID,
Batch: &pb.QueryResultBatch{
MoreResults: pb.QueryResultBatch_NO_MORE_RESULTS,
EntityResultType: pb.EntityResult_FULL,
EntityResults: mockEntityResults,
},
}
countAlias := "count"
runAggQueryReqWithTxn := &pb.RunAggregationQueryRequest{
ProjectId: mockProjectID,
ReadOptions: txnReadOptions,
QueryType: &pb.RunAggregationQueryRequest_AggregationQuery{
AggregationQuery: &pb.AggregationQuery{
QueryType: &pb.AggregationQuery_NestedQuery{
NestedQuery: &pb.Query{
Kind: []*pb.KindExpression{{Name: mockKind}},
},
},
Aggregations: []*pb.AggregationQuery_Aggregation{
{
Operator: &pb.AggregationQuery_Aggregation_Count_{},
Alias: countAlias,
},
},
},
},
}
runAggQueryResWithTxn := &pb.RunAggregationQueryResponse{
Batch: &pb.AggregationResultBatch{
AggregationResults: []*pb.AggregationResult{
{
AggregateProperties: map[string]*pb.Value{
countAlias: {
ValueType: &pb.Value_IntegerValue{IntegerValue: 1},
},
},
},
},
},
}
runAggQueryReqWithNewTxn := &pb.RunAggregationQueryRequest{
ProjectId: mockProjectID,
ReadOptions: newTxnReadOptions,
QueryType: &pb.RunAggregationQueryRequest_AggregationQuery{
AggregationQuery: &pb.AggregationQuery{
QueryType: &pb.AggregationQuery_NestedQuery{
NestedQuery: &pb.Query{
Kind: []*pb.KindExpression{{Name: mockKind}},
},
},
Aggregations: []*pb.AggregationQuery_Aggregation{
{
Operator: &pb.AggregationQuery_Aggregation_Count_{},
Alias: countAlias,
},
},
},
},
}
runAggQueryResWithNewTxn := &pb.RunAggregationQueryResponse{
Batch: &pb.AggregationResultBatch{
AggregationResults: []*pb.AggregationResult{
{
AggregateProperties: map[string]*pb.Value{
countAlias: {
ValueType: &pb.Value_IntegerValue{IntegerValue: 1},
},
},
},
},
},
Transaction: mockTxnID,
}
commitReq := &pb.CommitRequest{
ProjectId: mockProjectID,
Mode: pb.CommitRequest_TRANSACTIONAL,
TransactionSelector: &pb.CommitRequest_Transaction{
Transaction: mockTxnID,
},
Mutations: []*pb.Mutation{
{
Operation: &pb.Mutation_Upsert{
Upsert: mockEntity,
},
},
},
}
commitRes := &pb.CommitResponse{}
beginTxnReq := &pb.BeginTransactionRequest{
ProjectId: mockProjectID,
}
beginTxnRes := &pb.BeginTransactionResponse{
Transaction: mockTxnID,
}
testcases := []struct {
desc string
rpcInputs []addRPCInput
ops []string
settings *transactionSettings
}{
{
desc: "[Get, Get, Put, Commit] No options. First Get does not pass new_transaction",
rpcInputs: []addRPCInput{
{
wantReq: beginTxnReq,
resp: beginTxnRes,
},
{
wantReq: lookupReqWithTxn,
resp: lookupResWithTxn,
},
{
wantReq: lookupReqWithTxn,
resp: lookupResWithTxn,
},
{
wantReq: commitReq,
resp: commitRes,
},
},
ops: []string{"Get", "Get", "Put", "Commit"},
settings: &transactionSettings{},
},
{
desc: "[Get, Get, Put, Commit] BeginLater. First Get passes new_transaction",
rpcInputs: []addRPCInput{
{
wantReq: lookupReqWithNewTxn,
resp: lookupResWithNewTxn,
},
{
wantReq: lookupReqWithTxn,
resp: lookupResWithTxn,
},
{
wantReq: commitReq,
resp: commitRes,
},
},
ops: []string{"Get", "Get", "Put", "Commit"},
settings: &transactionSettings{beginLater: true},
},
{
desc: "[RunQuery, Get, Put, Commit] No options. RunQuery does not pass new_transaction",
rpcInputs: []addRPCInput{
{
wantReq: beginTxnReq,
resp: beginTxnRes,
},
{
wantReq: runQueryReqWithTxn,
resp: runQueryResWithTxn,
},
{
wantReq: lookupReqWithTxn,
resp: lookupResWithTxn,
},
{
wantReq: commitReq,
resp: commitRes,
},
},
ops: []string{"RunQuery", "Get", "Put", "Commit"},
settings: &transactionSettings{},
},
{
desc: "[RunQuery, Get, Put, Commit] BeginLater. RunQuery passes new_transaction",
rpcInputs: []addRPCInput{
{
wantReq: runQueryReqWithNewTxn,
resp: runQueryResWithNewTxn,
},
{
wantReq: lookupReqWithTxn,
resp: lookupResWithTxn,
},
{
wantReq: commitReq,
resp: commitRes,
},
},
ops: []string{"RunQuery", "Get", "Put", "Commit"},
settings: &transactionSettings{beginLater: true},
},
{
desc: "[RunAggregationQuery, Get, Put, Commit] No options. RunAggregationQuery does not pass new_transaction",
rpcInputs: []addRPCInput{
{
wantReq: beginTxnReq,
resp: beginTxnRes,
},
{
wantReq: runAggQueryReqWithTxn,
resp: runAggQueryResWithTxn,
},
{
wantReq: lookupReqWithTxn,
resp: lookupResWithTxn,
},
{
wantReq: commitReq,
resp: commitRes,
},
},
ops: []string{"RunAggregationQuery", "Get", "Put", "Commit"},
settings: &transactionSettings{},
},
{
desc: "[RunAggregationQuery, Get, Put, Commit] BeginLater. RunAggregationQuery passes new_transaction",
rpcInputs: []addRPCInput{
{
wantReq: runAggQueryReqWithNewTxn,
resp: runAggQueryResWithNewTxn,
},
{
wantReq: lookupReqWithTxn,
resp: lookupResWithTxn,
},
{
wantReq: commitReq,
resp: commitRes,
},
},
ops: []string{"RunAggregationQuery", "Get", "Put", "Commit"},
settings: &transactionSettings{beginLater: true},
},
{
desc: "[Put, Commit] No options. BeginTransaction request sent",
rpcInputs: []addRPCInput{
{
wantReq: beginTxnReq,
resp: beginTxnRes,
},
{
wantReq: commitReq,
resp: commitRes,
},
},
ops: []string{"Put", "Commit"},
settings: &transactionSettings{},
},
{
desc: "[Put, Commit] BeginLater. BeginTransaction request sent",
rpcInputs: []addRPCInput{
{
wantReq: beginTxnReq,
resp: beginTxnRes,
},
{
wantReq: commitReq,
resp: commitRes,
},
},
ops: []string{"Put", "Commit"},
settings: &transactionSettings{beginLater: true},
},
}
for _, testcase := range testcases {
ctx := context.Background()
client, srv, cleanup := newMock(t)
defer cleanup()
for _, rpcInput := range testcase.rpcInputs {
srv.addRPC(rpcInput.wantReq, rpcInput.resp)
}
dst := &ent{}
txn, err := client.newTransaction(ctx, testcase.settings)
if err != nil {
t.Fatalf("%q: %v", testcase.desc, err)
}
for i, op := range testcase.ops {
switch op {
case "RunQuery":
query := NewQuery(mockKind).Transaction(txn)
got := []*ent{}
if _, err := client.GetAll(ctx, query, &got); err != nil {
t.Fatalf("%q RunQuery[%v] failed with error %v", testcase.desc, i, err)
}
case "RunAggregationQuery":
aggQuery := NewQuery(mockKind).Transaction(txn).NewAggregationQuery()
aggQuery.WithCount(countAlias)
if _, err := client.RunAggregationQuery(ctx, aggQuery); err != nil {
t.Fatalf("%q RunAggregationQuery[%v] failed with error %v", testcase.desc, i, err)
}
case "Get":
if err := txn.Get(mockKey, dst); err != nil {
t.Fatalf("%q Get[%v] failed with error %v", testcase.desc, i, err)
}
case "Put":
_, err := txn.Put(mockKey, dst)
if err != nil {
t.Fatalf("%q Put[%v] failed with error %v", testcase.desc, i, err)
}
case "Commit":
_, err := txn.Commit()
if err != nil {
t.Fatalf("%q Commit[%v] failed with error %v", testcase.desc, i, err)
}
}
}
}
}