vitess.io/vitess@v0.16.2/go/vt/vtgate/legacy_scatter_conn_test.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package vtgate
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"reflect"
    23  	"sync"
    24  	"testing"
    25  
    26  	"github.com/stretchr/testify/assert"
    27  	"github.com/stretchr/testify/require"
    28  
    29  	"vitess.io/vitess/go/sqltypes"
    30  	"vitess.io/vitess/go/test/utils"
    31  	"vitess.io/vitess/go/vt/discovery"
    32  	"vitess.io/vitess/go/vt/key"
    33  	"vitess.io/vitess/go/vt/srvtopo"
    34  	"vitess.io/vitess/go/vt/vterrors"
    35  
    36  	querypb "vitess.io/vitess/go/vt/proto/query"
    37  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    38  	vtgatepb "vitess.io/vitess/go/vt/proto/vtgate"
    39  	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
    40  )
    41  
    42  // This file uses the sandbox_test framework.
    43  
    44  func TestLegacyExecuteFailOnAutocommit(t *testing.T) {
    45  
    46  	createSandbox("TestExecuteFailOnAutocommit")
    47  	hc := discovery.NewFakeHealthCheck(nil)
    48  	sc := newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
    49  	sbc0 := hc.AddTestTablet("aa", "0", 1, "TestExecuteFailOnAutocommit", "0", topodatapb.TabletType_PRIMARY, true, 1, nil)
    50  	sbc1 := hc.AddTestTablet("aa", "1", 1, "TestExecuteFailOnAutocommit", "1", topodatapb.TabletType_PRIMARY, true, 1, nil)
    51  
    52  	rss := []*srvtopo.ResolvedShard{
    53  		{
    54  			Target: &querypb.Target{
    55  				Keyspace:   "TestExecuteFailOnAutocommit",
    56  				Shard:      "0",
    57  				TabletType: topodatapb.TabletType_PRIMARY,
    58  			},
    59  			Gateway: sbc0,
    60  		},
    61  		{
    62  			Target: &querypb.Target{
    63  				Keyspace:   "TestExecuteFailOnAutocommit",
    64  				Shard:      "1",
    65  				TabletType: topodatapb.TabletType_PRIMARY,
    66  			},
    67  			Gateway: sbc1,
    68  		},
    69  	}
    70  	queries := []*querypb.BoundQuery{
    71  		{
    72  			// This will fail to go to shard. It will be rejected at vtgate.
    73  			Sql: "query1",
    74  			BindVariables: map[string]*querypb.BindVariable{
    75  				"bv0": sqltypes.Int64BindVariable(0),
    76  			},
    77  		},
    78  		{
    79  			// This will go to shard.
    80  			Sql: "query2",
    81  			BindVariables: map[string]*querypb.BindVariable{
    82  				"bv1": sqltypes.Int64BindVariable(1),
    83  			},
    84  		},
    85  	}
    86  	// shard 0 - has transaction
    87  	// shard 1 - does not have transaction.
    88  	session := &vtgatepb.Session{
    89  		InTransaction: true,
    90  		ShardSessions: []*vtgatepb.Session_ShardSession{
    91  			{
    92  				Target:        &querypb.Target{Keyspace: "TestExecuteFailOnAutocommit", Shard: "0", TabletType: topodatapb.TabletType_PRIMARY, Cell: "aa"},
    93  				TransactionId: 123,
    94  				TabletAlias:   nil,
    95  			},
    96  		},
    97  		Autocommit: false,
    98  	}
    99  	_, errs := sc.ExecuteMultiShard(ctx, nil, rss, queries, NewSafeSession(session), true /*autocommit*/, false)
   100  	err := vterrors.Aggregate(errs)
   101  	require.Error(t, err)
   102  	require.Contains(t, err.Error(), "in autocommit mode, transactionID should be zero but was: 123")
   103  	utils.MustMatch(t, 0, len(sbc0.Queries), "")
   104  	utils.MustMatch(t, []*querypb.BoundQuery{queries[1]}, sbc1.Queries, "")
   105  }
   106  
   107  func TestScatterConnExecuteMulti(t *testing.T) {
   108  	testScatterConnGeneric(t, "TestScatterConnExecuteMultiShard", func(sc *ScatterConn, shards []string) (*sqltypes.Result, error) {
   109  		res := srvtopo.NewResolver(newSandboxForCells([]string{"aa"}), sc.gateway, "aa")
   110  		rss, err := res.ResolveDestination(ctx, "TestScatterConnExecuteMultiShard", topodatapb.TabletType_REPLICA, key.DestinationShards(shards))
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  
   115  		queries := make([]*querypb.BoundQuery, len(rss))
   116  		for i := range rss {
   117  			queries[i] = &querypb.BoundQuery{
   118  				Sql:           "query",
   119  				BindVariables: nil,
   120  			}
   121  		}
   122  
   123  		qr, errs := sc.ExecuteMultiShard(ctx, nil, rss, queries, NewSafeSession(nil), false /*autocommit*/, false)
   124  		return qr, vterrors.Aggregate(errs)
   125  	})
   126  }
   127  
   128  func TestScatterConnStreamExecuteMulti(t *testing.T) {
   129  	testScatterConnGeneric(t, "TestScatterConnStreamExecuteMulti", func(sc *ScatterConn, shards []string) (*sqltypes.Result, error) {
   130  		res := srvtopo.NewResolver(newSandboxForCells([]string{"aa"}), sc.gateway, "aa")
   131  		rss, err := res.ResolveDestination(ctx, "TestScatterConnStreamExecuteMulti", topodatapb.TabletType_REPLICA, key.DestinationShards(shards))
   132  		if err != nil {
   133  			return nil, err
   134  		}
   135  		bvs := make([]map[string]*querypb.BindVariable, len(rss))
   136  		qr := new(sqltypes.Result)
   137  		var mu sync.Mutex
   138  		errors := sc.StreamExecuteMulti(ctx, nil, "query", rss, bvs, NewSafeSession(&vtgatepb.Session{InTransaction: true}), true /* autocommit */, func(r *sqltypes.Result) error {
   139  			mu.Lock()
   140  			defer mu.Unlock()
   141  			qr.AppendResult(r)
   142  			return nil
   143  		})
   144  		return qr, vterrors.Aggregate(errors)
   145  	})
   146  }
   147  
   148  // verifyScatterConnError checks that a returned error has the expected message,
   149  // type, and error code.
   150  func verifyScatterConnError(t *testing.T, err error, wantErr string, wantCode vtrpcpb.Code) {
   151  	t.Helper()
   152  	assert.EqualError(t, err, wantErr)
   153  	assert.Equal(t, wantCode, vterrors.Code(err))
   154  }
   155  
   156  func testScatterConnGeneric(t *testing.T, name string, f func(sc *ScatterConn, shards []string) (*sqltypes.Result, error)) {
   157  	hc := discovery.NewFakeHealthCheck(nil)
   158  
   159  	// no shard
   160  	s := createSandbox(name)
   161  	sc := newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   162  	qr, err := f(sc, nil)
   163  	require.NoError(t, err)
   164  	if qr.RowsAffected != 0 {
   165  		t.Errorf("want 0, got %v", qr.RowsAffected)
   166  	}
   167  
   168  	// single shard
   169  	s.Reset()
   170  	sc = newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   171  	sbc := hc.AddTestTablet("aa", "0", 1, name, "0", topodatapb.TabletType_REPLICA, true, 1, nil)
   172  	sbc.MustFailCodes[vtrpcpb.Code_INVALID_ARGUMENT] = 1
   173  	_, err = f(sc, []string{"0"})
   174  	want := fmt.Sprintf("target: %v.0.replica: INVALID_ARGUMENT error", name)
   175  	// Verify server error string.
   176  	if err == nil || err.Error() != want {
   177  		t.Errorf("want %s, got %v", want, err)
   178  	}
   179  	// Ensure that we tried only once.
   180  	if execCount := sbc.ExecCount.Get(); execCount != 1 {
   181  		t.Errorf("want 1, got %v", execCount)
   182  	}
   183  
   184  	// two shards
   185  	s.Reset()
   186  	hc.Reset()
   187  	sc = newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   188  	sbc0 := hc.AddTestTablet("aa", "0", 1, name, "0", topodatapb.TabletType_REPLICA, true, 1, nil)
   189  	sbc1 := hc.AddTestTablet("aa", "1", 1, name, "1", topodatapb.TabletType_REPLICA, true, 1, nil)
   190  	sbc0.MustFailCodes[vtrpcpb.Code_INVALID_ARGUMENT] = 1
   191  	sbc1.MustFailCodes[vtrpcpb.Code_INVALID_ARGUMENT] = 1
   192  	_, err = f(sc, []string{"0", "1"})
   193  	// Verify server errors are consolidated.
   194  	want = fmt.Sprintf("target: %v.0.replica: INVALID_ARGUMENT error\ntarget: %v.1.replica: INVALID_ARGUMENT error", name, name)
   195  	verifyScatterConnError(t, err, want, vtrpcpb.Code_INVALID_ARGUMENT)
   196  	// Ensure that we tried only once.
   197  	if execCount := sbc0.ExecCount.Get(); execCount != 1 {
   198  		t.Errorf("want 1, got %v", execCount)
   199  	}
   200  	if execCount := sbc1.ExecCount.Get(); execCount != 1 {
   201  		t.Errorf("want 1, got %v", execCount)
   202  	}
   203  
   204  	// two shards with different errors
   205  	s.Reset()
   206  	hc.Reset()
   207  	sc = newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   208  	sbc0 = hc.AddTestTablet("aa", "0", 1, name, "0", topodatapb.TabletType_REPLICA, true, 1, nil)
   209  	sbc1 = hc.AddTestTablet("aa", "1", 1, name, "1", topodatapb.TabletType_REPLICA, true, 1, nil)
   210  	sbc0.MustFailCodes[vtrpcpb.Code_INVALID_ARGUMENT] = 1
   211  	sbc1.MustFailCodes[vtrpcpb.Code_RESOURCE_EXHAUSTED] = 1
   212  	_, err = f(sc, []string{"0", "1"})
   213  	// Verify server errors are consolidated.
   214  	want = fmt.Sprintf("target: %v.0.replica: INVALID_ARGUMENT error\ntarget: %v.1.replica: RESOURCE_EXHAUSTED error", name, name)
   215  	// We should only surface the higher priority error code
   216  	verifyScatterConnError(t, err, want, vtrpcpb.Code_INVALID_ARGUMENT)
   217  	// Ensure that we tried only once.
   218  	if execCount := sbc0.ExecCount.Get(); execCount != 1 {
   219  		t.Errorf("want 1, got %v", execCount)
   220  	}
   221  	if execCount := sbc1.ExecCount.Get(); execCount != 1 {
   222  		t.Errorf("want 1, got %v", execCount)
   223  	}
   224  
   225  	// duplicate shards
   226  	s.Reset()
   227  	hc.Reset()
   228  	sc = newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   229  	sbc = hc.AddTestTablet("aa", "0", 1, name, "0", topodatapb.TabletType_REPLICA, true, 1, nil)
   230  	_, _ = f(sc, []string{"0", "0"})
   231  	// Ensure that we executed only once.
   232  	if execCount := sbc.ExecCount.Get(); execCount != 1 {
   233  		t.Errorf("want 1, got %v", execCount)
   234  	}
   235  
   236  	// no errors
   237  	s.Reset()
   238  	hc.Reset()
   239  	sc = newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   240  	sbc0 = hc.AddTestTablet("aa", "0", 1, name, "0", topodatapb.TabletType_REPLICA, true, 1, nil)
   241  	sbc1 = hc.AddTestTablet("aa", "1", 1, name, "1", topodatapb.TabletType_REPLICA, true, 1, nil)
   242  	qr, err = f(sc, []string{"0", "1"})
   243  	if err != nil {
   244  		t.Fatalf("want nil, got %v", err)
   245  	}
   246  	if execCount := sbc0.ExecCount.Get(); execCount != 1 {
   247  		t.Errorf("want 1, got %v", execCount)
   248  	}
   249  	if execCount := sbc1.ExecCount.Get(); execCount != 1 {
   250  		t.Errorf("want 1, got %v", execCount)
   251  	}
   252  	if qr.RowsAffected != 0 {
   253  		t.Errorf("want 0, got %v", qr.RowsAffected)
   254  	}
   255  	if len(qr.Rows) != 2 {
   256  		t.Errorf("want 2, got %v", len(qr.Rows))
   257  	}
   258  }
   259  
   260  func TestMaxMemoryRows(t *testing.T) {
   261  	save := maxMemoryRows
   262  	maxMemoryRows = 3
   263  	defer func() { maxMemoryRows = save }()
   264  
   265  	createSandbox("TestMaxMemoryRows")
   266  	hc := discovery.NewFakeHealthCheck(nil)
   267  	sc := newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   268  	sbc0 := hc.AddTestTablet("aa", "0", 1, "TestMaxMemoryRows", "0", topodatapb.TabletType_REPLICA, true, 1, nil)
   269  	sbc1 := hc.AddTestTablet("aa", "1", 1, "TestMaxMemoryRows", "1", topodatapb.TabletType_REPLICA, true, 1, nil)
   270  
   271  	res := srvtopo.NewResolver(newSandboxForCells([]string{"aa"}), sc.gateway, "aa")
   272  	rss, _, err := res.ResolveDestinations(ctx, "TestMaxMemoryRows", topodatapb.TabletType_REPLICA, nil,
   273  		[]key.Destination{key.DestinationShard("0"), key.DestinationShard("1")})
   274  	require.NoError(t, err)
   275  
   276  	session := NewSafeSession(&vtgatepb.Session{InTransaction: true})
   277  	queries := []*querypb.BoundQuery{{
   278  		Sql:           "query1",
   279  		BindVariables: map[string]*querypb.BindVariable{},
   280  	}, {
   281  		Sql:           "query1",
   282  		BindVariables: map[string]*querypb.BindVariable{},
   283  	}}
   284  	tworows := &sqltypes.Result{
   285  		Rows: [][]sqltypes.Value{{
   286  			sqltypes.NewInt64(1),
   287  		}, {
   288  			sqltypes.NewInt64(1),
   289  		}},
   290  		RowsAffected: 1,
   291  		InsertID:     1,
   292  	}
   293  
   294  	testCases := []struct {
   295  		ignoreMaxMemoryRows bool
   296  		err                 string
   297  	}{
   298  		{true, ""},
   299  		{false, "in-memory row count exceeded allowed limit of 3"},
   300  	}
   301  
   302  	for _, test := range testCases {
   303  		sbc0.SetResults([]*sqltypes.Result{tworows, tworows})
   304  		sbc1.SetResults([]*sqltypes.Result{tworows, tworows})
   305  
   306  		_, errs := sc.ExecuteMultiShard(ctx, nil, rss, queries, session, false, test.ignoreMaxMemoryRows)
   307  		if test.ignoreMaxMemoryRows {
   308  			require.NoError(t, err)
   309  		} else {
   310  			assert.EqualError(t, errs[0], test.err)
   311  		}
   312  	}
   313  }
   314  
   315  func TestLegaceHealthCheckFailsOnReservedConnections(t *testing.T) {
   316  	keyspace := "keyspace"
   317  	createSandbox(keyspace)
   318  	hc := discovery.NewFakeHealthCheck(nil)
   319  	sc := newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   320  
   321  	res := srvtopo.NewResolver(newSandboxForCells([]string{"aa"}), sc.gateway, "aa")
   322  
   323  	session := NewSafeSession(&vtgatepb.Session{InTransaction: false, InReservedConn: true})
   324  	destinations := []key.Destination{key.DestinationShard("0")}
   325  	rss, _, err := res.ResolveDestinations(ctx, keyspace, topodatapb.TabletType_REPLICA, nil, destinations)
   326  	require.NoError(t, err)
   327  
   328  	var queries []*querypb.BoundQuery
   329  
   330  	for range rss {
   331  		queries = append(queries, &querypb.BoundQuery{
   332  			Sql:           "query1",
   333  			BindVariables: map[string]*querypb.BindVariable{},
   334  		})
   335  	}
   336  
   337  	_, errs := sc.ExecuteMultiShard(ctx, nil, rss, queries, session, false, false)
   338  	require.Error(t, vterrors.Aggregate(errs))
   339  }
   340  
   341  func executeOnShards(t *testing.T, res *srvtopo.Resolver, keyspace string, sc *ScatterConn, session *SafeSession, destinations []key.Destination) {
   342  	t.Helper()
   343  	require.Empty(t, executeOnShardsReturnsErr(t, res, keyspace, sc, session, destinations))
   344  }
   345  
   346  func executeOnShardsReturnsErr(t *testing.T, res *srvtopo.Resolver, keyspace string, sc *ScatterConn, session *SafeSession, destinations []key.Destination) error {
   347  	t.Helper()
   348  	rss, _, err := res.ResolveDestinations(ctx, keyspace, topodatapb.TabletType_REPLICA, nil, destinations)
   349  	require.NoError(t, err)
   350  
   351  	var queries []*querypb.BoundQuery
   352  
   353  	for range rss {
   354  		queries = append(queries, &querypb.BoundQuery{
   355  			Sql:           "query1",
   356  			BindVariables: map[string]*querypb.BindVariable{},
   357  		})
   358  	}
   359  
   360  	_, errs := sc.ExecuteMultiShard(ctx, nil, rss, queries, session, false, false)
   361  	return vterrors.Aggregate(errs)
   362  }
   363  
   364  func TestMultiExecs(t *testing.T) {
   365  	createSandbox("TestMultiExecs")
   366  	hc := discovery.NewFakeHealthCheck(nil)
   367  	sc := newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   368  	sbc0 := hc.AddTestTablet("aa", "0", 1, "TestMultiExecs", "0", topodatapb.TabletType_REPLICA, true, 1, nil)
   369  	sbc1 := hc.AddTestTablet("aa", "1", 1, "TestMultiExecs", "1", topodatapb.TabletType_REPLICA, true, 1, nil)
   370  
   371  	rss := []*srvtopo.ResolvedShard{
   372  		{
   373  			Target: &querypb.Target{
   374  				Keyspace:   "TestMultiExecs",
   375  				Shard:      "0",
   376  				TabletType: topodatapb.TabletType_REPLICA,
   377  			},
   378  			Gateway: sbc0,
   379  		},
   380  		{
   381  			Target: &querypb.Target{
   382  				Keyspace:   "TestMultiExecs",
   383  				Shard:      "1",
   384  				TabletType: topodatapb.TabletType_REPLICA,
   385  			},
   386  			Gateway: sbc1,
   387  		},
   388  	}
   389  	queries := []*querypb.BoundQuery{
   390  		{
   391  			Sql: "query1",
   392  			BindVariables: map[string]*querypb.BindVariable{
   393  				"bv0": sqltypes.Int64BindVariable(0),
   394  			},
   395  		},
   396  		{
   397  			Sql: "query2",
   398  			BindVariables: map[string]*querypb.BindVariable{
   399  				"bv1": sqltypes.Int64BindVariable(1),
   400  			},
   401  		},
   402  	}
   403  
   404  	session := NewSafeSession(&vtgatepb.Session{})
   405  	_, err := sc.ExecuteMultiShard(ctx, nil, rss, queries, session, false, false)
   406  	require.NoError(t, vterrors.Aggregate(err))
   407  	if len(sbc0.Queries) == 0 || len(sbc1.Queries) == 0 {
   408  		t.Fatalf("didn't get expected query")
   409  	}
   410  	wantVars0 := map[string]*querypb.BindVariable{
   411  		"bv0": queries[0].BindVariables["bv0"],
   412  	}
   413  	if !reflect.DeepEqual(sbc0.Queries[0].BindVariables, wantVars0) {
   414  		t.Errorf("got %v, want %v", sbc0.Queries[0].BindVariables, wantVars0)
   415  	}
   416  	wantVars1 := map[string]*querypb.BindVariable{
   417  		"bv1": queries[1].BindVariables["bv1"],
   418  	}
   419  	if !reflect.DeepEqual(sbc1.Queries[0].BindVariables, wantVars1) {
   420  		t.Errorf("got %+v, want %+v", sbc0.Queries[0].BindVariables, wantVars1)
   421  	}
   422  	sbc0.Queries = nil
   423  	sbc1.Queries = nil
   424  
   425  	rss = []*srvtopo.ResolvedShard{
   426  		{
   427  			Target: &querypb.Target{
   428  				Keyspace: "TestMultiExecs",
   429  				Shard:    "0",
   430  			},
   431  			Gateway: sbc0,
   432  		},
   433  		{
   434  			Target: &querypb.Target{
   435  				Keyspace: "TestMultiExecs",
   436  				Shard:    "1",
   437  			},
   438  			Gateway: sbc1,
   439  		},
   440  	}
   441  	bvs := []map[string]*querypb.BindVariable{
   442  		{
   443  			"bv0": sqltypes.Int64BindVariable(0),
   444  		},
   445  		{
   446  			"bv1": sqltypes.Int64BindVariable(1),
   447  		},
   448  	}
   449  	_ = sc.StreamExecuteMulti(ctx, nil, "query", rss, bvs, session, false /* autocommit */, func(*sqltypes.Result) error {
   450  		return nil
   451  	})
   452  	if !reflect.DeepEqual(sbc0.Queries[0].BindVariables, wantVars0) {
   453  		t.Errorf("got %+v, want %+v", sbc0.Queries[0].BindVariables, wantVars0)
   454  	}
   455  	if !reflect.DeepEqual(sbc1.Queries[0].BindVariables, wantVars1) {
   456  		t.Errorf("got %+v, want %+v", sbc0.Queries[0].BindVariables, wantVars1)
   457  	}
   458  }
   459  
   460  func TestScatterConnSingleDB(t *testing.T) {
   461  	createSandbox("TestScatterConnSingleDB")
   462  	hc := discovery.NewFakeHealthCheck(nil)
   463  
   464  	hc.Reset()
   465  	sc := newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   466  	hc.AddTestTablet("aa", "0", 1, "TestScatterConnSingleDB", "0", topodatapb.TabletType_PRIMARY, true, 1, nil)
   467  	hc.AddTestTablet("aa", "1", 1, "TestScatterConnSingleDB", "1", topodatapb.TabletType_PRIMARY, true, 1, nil)
   468  
   469  	res := srvtopo.NewResolver(newSandboxForCells([]string{"aa"}), sc.gateway, "aa")
   470  	rss0, err := res.ResolveDestination(ctx, "TestScatterConnSingleDB", topodatapb.TabletType_PRIMARY, key.DestinationShard("0"))
   471  	require.NoError(t, err)
   472  	rss1, err := res.ResolveDestination(ctx, "TestScatterConnSingleDB", topodatapb.TabletType_PRIMARY, key.DestinationShard("1"))
   473  	require.NoError(t, err)
   474  
   475  	want := "multi-db transaction attempted"
   476  
   477  	// TransactionMode_SINGLE in session
   478  	session := NewSafeSession(&vtgatepb.Session{InTransaction: true, TransactionMode: vtgatepb.TransactionMode_SINGLE})
   479  	queries := []*querypb.BoundQuery{{Sql: "query1"}}
   480  	_, errors := sc.ExecuteMultiShard(ctx, nil, rss0, queries, session, false, false)
   481  	require.Empty(t, errors)
   482  	_, errors = sc.ExecuteMultiShard(ctx, nil, rss1, queries, session, false, false)
   483  	require.Error(t, errors[0])
   484  	assert.Contains(t, errors[0].Error(), want)
   485  
   486  	// TransactionMode_SINGLE in txconn
   487  	sc.txConn.mode = vtgatepb.TransactionMode_SINGLE
   488  	session = NewSafeSession(&vtgatepb.Session{InTransaction: true})
   489  	_, errors = sc.ExecuteMultiShard(ctx, nil, rss0, queries, session, false, false)
   490  	require.Empty(t, errors)
   491  	_, errors = sc.ExecuteMultiShard(ctx, nil, rss1, queries, session, false, false)
   492  	require.Error(t, errors[0])
   493  	assert.Contains(t, errors[0].Error(), want)
   494  
   495  	// TransactionMode_MULTI in txconn. Should not fail.
   496  	sc.txConn.mode = vtgatepb.TransactionMode_MULTI
   497  	session = NewSafeSession(&vtgatepb.Session{InTransaction: true})
   498  	_, errors = sc.ExecuteMultiShard(ctx, nil, rss0, queries, session, false, false)
   499  	require.Empty(t, errors)
   500  	_, errors = sc.ExecuteMultiShard(ctx, nil, rss1, queries, session, false, false)
   501  	require.Empty(t, errors)
   502  }
   503  
   504  func TestAppendResult(t *testing.T) {
   505  	qr := new(sqltypes.Result)
   506  	innerqr1 := &sqltypes.Result{
   507  		Fields: []*querypb.Field{},
   508  		Rows:   [][]sqltypes.Value{},
   509  	}
   510  	innerqr2 := &sqltypes.Result{
   511  		Fields: []*querypb.Field{
   512  			{Name: "foo", Type: sqltypes.Int8},
   513  		},
   514  		RowsAffected: 1,
   515  		InsertID:     1,
   516  		Rows: [][]sqltypes.Value{
   517  			{sqltypes.NewVarBinary("abcd")},
   518  		},
   519  	}
   520  	// test one empty result
   521  	qr.AppendResult(innerqr1)
   522  	qr.AppendResult(innerqr2)
   523  	if len(qr.Fields) != 1 {
   524  		t.Errorf("want 1, got %v", len(qr.Fields))
   525  	}
   526  	if qr.RowsAffected != 1 {
   527  		t.Errorf("want 1, got %v", qr.RowsAffected)
   528  	}
   529  	if qr.InsertID != 1 {
   530  		t.Errorf("want 1, got %v", qr.InsertID)
   531  	}
   532  	if len(qr.Rows) != 1 {
   533  		t.Errorf("want 1, got %v", len(qr.Rows))
   534  	}
   535  	// test two valid results
   536  	qr = new(sqltypes.Result)
   537  	qr.AppendResult(innerqr2)
   538  	qr.AppendResult(innerqr2)
   539  	if len(qr.Fields) != 1 {
   540  		t.Errorf("want 1, got %v", len(qr.Fields))
   541  	}
   542  	if qr.RowsAffected != 2 {
   543  		t.Errorf("want 2, got %v", qr.RowsAffected)
   544  	}
   545  	if qr.InsertID != 1 {
   546  		t.Errorf("want 1, got %v", qr.InsertID)
   547  	}
   548  	if len(qr.Rows) != 2 {
   549  		t.Errorf("want 2, got %v", len(qr.Rows))
   550  	}
   551  }
   552  
   553  func TestReservePrequeries(t *testing.T) {
   554  	keyspace := "keyspace"
   555  	createSandbox(keyspace)
   556  	hc := discovery.NewFakeHealthCheck(nil)
   557  	sc := newTestScatterConn(hc, newSandboxForCells([]string{"aa"}), "aa")
   558  	sbc0 := hc.AddTestTablet("aa", "0", 1, keyspace, "0", topodatapb.TabletType_REPLICA, true, 1, nil)
   559  	sbc1 := hc.AddTestTablet("aa", "1", 1, keyspace, "1", topodatapb.TabletType_REPLICA, true, 1, nil)
   560  
   561  	// empty results
   562  	sbc0.SetResults([]*sqltypes.Result{{}})
   563  	sbc1.SetResults([]*sqltypes.Result{{}})
   564  
   565  	res := srvtopo.NewResolver(newSandboxForCells([]string{"aa"}), sc.gateway, "aa")
   566  
   567  	session := NewSafeSession(&vtgatepb.Session{
   568  		InTransaction:  false,
   569  		InReservedConn: true,
   570  		SystemVariables: map[string]string{
   571  			"s1": "'value'",
   572  			"s2": "42",
   573  		},
   574  	})
   575  	destinations := []key.Destination{key.DestinationShard("0")}
   576  
   577  	executeOnShards(t, res, keyspace, sc, session, destinations)
   578  	assert.Equal(t, 1+1, len(sbc0.StringQueries()))
   579  }
   580  
   581  func newTestScatterConn(hc discovery.HealthCheck, serv srvtopo.Server, cell string) *ScatterConn {
   582  	// The topo.Server is used to start watching the cells described
   583  	// in '-cells_to_watch' command line parameter, which is
   584  	// empty by default. So it's unused in this test, set to nil.
   585  	gw := NewTabletGateway(ctx, hc, serv, cell)
   586  	tc := NewTxConn(gw, vtgatepb.TransactionMode_TWOPC)
   587  	return NewScatterConn("", tc, gw)
   588  }
   589  
   590  var ctx = context.Background()