vitess.io/vitess@v0.16.2/go/vt/vtgate/executor_framework_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  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"strconv"
    24  	"strings"
    25  	"testing"
    26  
    27  	"vitess.io/vitess/go/vt/vtgate/logstats"
    28  
    29  	vtgatepb "vitess.io/vitess/go/vt/proto/vtgate"
    30  
    31  	"github.com/stretchr/testify/require"
    32  
    33  	"github.com/stretchr/testify/assert"
    34  
    35  	"vitess.io/vitess/go/cache"
    36  	"vitess.io/vitess/go/sqltypes"
    37  	"vitess.io/vitess/go/streamlog"
    38  	"vitess.io/vitess/go/vt/discovery"
    39  	"vitess.io/vitess/go/vt/key"
    40  	"vitess.io/vitess/go/vt/srvtopo"
    41  	"vitess.io/vitess/go/vt/vtgate/vindexes"
    42  	"vitess.io/vitess/go/vt/vttablet/sandboxconn"
    43  
    44  	querypb "vitess.io/vitess/go/vt/proto/query"
    45  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    46  )
    47  
    48  var executorVSchema = `
    49  {
    50  	"sharded": true,
    51  	"vindexes": {
    52  		"hash_index": {
    53  			"type": "hash"
    54  		},
    55  		"music_user_map": {
    56  			"type": "lookup_hash_unique",
    57  			"owner": "music",
    58  			"params": {
    59  				"table": "music_user_map",
    60  				"from": "music_id",
    61  				"to": "user_id"
    62  			}
    63  		},
    64  		"name_user_map": {
    65  			"type": "lookup_hash",
    66  			"owner": "user",
    67  			"params": {
    68  				"table": "name_user_map",
    69  				"from": "name",
    70  				"to": "user_id"
    71  			}
    72  		},
    73  		"name_lastname_keyspace_id_map": {
    74  			"type": "lookup",
    75  			"owner": "user2",
    76  			"params": {
    77  				"table": "name_lastname_keyspace_id_map",
    78  				"from": "name,lastname",
    79  				"to": "keyspace_id"
    80  			}
    81  		},
    82  		"insert_ignore_idx": {
    83  			"type": "lookup_hash",
    84  			"owner": "insert_ignore_test",
    85  			"params": {
    86  				"table": "ins_lookup",
    87  				"from": "fromcol",
    88  				"to": "tocol"
    89  			}
    90  		},
    91  		"idx1": {
    92  			"type": "hash"
    93  		},
    94  		"idx_noauto": {
    95  			"type": "hash",
    96  			"owner": "noauto_table"
    97  		},
    98  		"keyspace_id": {
    99  			"type": "numeric"
   100  		},
   101  		"krcol_unique_vdx": {
   102  			"type": "keyrange_lookuper_unique"
   103  		},
   104  		"krcol_vdx": {
   105  			"type": "keyrange_lookuper"
   106  		},
   107      	"t1_lkp_vdx": {
   108        		"type": "consistent_lookup_unique",
   109        		"params": {
   110          		"table": "t1_lkp_idx",
   111          		"from": "unq_col",
   112          		"to": "keyspace_id"
   113        		},
   114        		"owner": "t1"
   115      	},
   116  		"t2_wo_lu_vdx": {
   117        		"type": "lookup_unique",
   118        		"params": {
   119          		"table": "TestUnsharded.wo_lu_idx",
   120          		"from": "wo_lu_col",
   121          		"to": "keyspace_id",
   122          		"write_only": "true"
   123        		},
   124        		"owner": "t2_lookup"
   125      	},
   126  		"t2_erl_lu_vdx": {
   127        		"type": "lookup_unique",
   128        		"params": {
   129          		"table": "TestUnsharded.erl_lu_idx",
   130          		"from": "erl_lu_col",
   131          		"to": "keyspace_id",
   132          		"read_lock": "exclusive"
   133        		},
   134        		"owner": "t2_lookup"
   135      	},
   136  		"t2_srl_lu_vdx": {
   137        		"type": "lookup_unique",
   138        		"params": {
   139          		"table": "TestUnsharded.srl_lu_idx",
   140          		"from": "srl_lu_col",
   141          		"to": "keyspace_id",
   142          		"read_lock": "shared"
   143        		},
   144        		"owner": "t2_lookup"
   145      	},
   146  		"t2_nrl_lu_vdx": {
   147        		"type": "lookup_unique",
   148        		"params": {
   149          		"table": "TestUnsharded.nrl_lu_idx",
   150          		"from": "nrl_lu_col",
   151          		"to": "keyspace_id",
   152          		"read_lock": "none"
   153        		},
   154        		"owner": "t2_lookup"
   155      	},
   156  		"t2_nv_lu_vdx": {
   157        		"type": "lookup_unique",
   158        		"params": {
   159          		"table": "TestUnsharded.nv_lu_idx",
   160          		"from": "nv_lu_col",
   161          		"to": "keyspace_id",
   162          		"no_verify": "true"
   163        		},
   164        		"owner": "t2_lookup"
   165      	},
   166  		"t2_lu_vdx": {
   167        		"type": "lookup_hash_unique",
   168        		"params": {
   169          		"table": "TestUnsharded.lu_idx",
   170          		"from": "lu_col",
   171          		"to": "keyspace_id"
   172        		},
   173  		"owner": "t2_lookup"
   174      	},
   175  		"regional_vdx": {
   176  			"type": "region_experimental",
   177  			"params": {
   178  				"region_bytes": "1"
   179  			}
   180      	},
   181  		"cfc": {
   182  			"type": "cfc"
   183  		}
   184  	},
   185  	"tables": {
   186  		"user": {
   187  			"column_vindexes": [
   188  				{
   189  					"column": "Id",
   190  					"name": "hash_index"
   191  				},
   192  				{
   193  					"column": "name",
   194  					"name": "name_user_map"
   195  				}
   196  			],
   197  			"auto_increment": {
   198  				"column": "id",
   199  				"sequence": "user_seq"
   200  			},
   201  			"columns": [
   202  				{
   203  					"name": "textcol",
   204  					"type": "VARCHAR"
   205  				}
   206  			]
   207  		},
   208  		"user2": {
   209  			"column_vindexes": [
   210  				{
   211  					"column": "id",
   212  					"name": "hash_index"
   213  				},
   214  				{
   215  					"columns": ["name", "lastname"],
   216  					"name": "name_lastname_keyspace_id_map"
   217  				}
   218  			]
   219  		},
   220  		"user_extra": {
   221  			"column_vindexes": [
   222  				{
   223  					"column": "user_id",
   224  					"name": "hash_index"
   225  				}
   226  			]
   227  		},
   228  		"sharded_user_msgs": {
   229  			"column_vindexes": [
   230  				{
   231  					"column": "user_id",
   232  					"name": "hash_index"
   233  				}
   234  			]
   235  		},
   236  		"music": {
   237  			"column_vindexes": [
   238  				{
   239  					"column": "user_id",
   240  					"name": "hash_index"
   241  				},
   242  				{
   243  					"column": "id",
   244  					"name": "music_user_map"
   245  				}
   246  			],
   247  			"auto_increment": {
   248  				"column": "id",
   249  				"sequence": "user_seq"
   250  			}
   251  		},
   252  		"music_extra": {
   253  			"column_vindexes": [
   254  				{
   255  					"column": "user_id",
   256  					"name": "hash_index"
   257  				},
   258  				{
   259  					"column": "music_id",
   260  					"name": "music_user_map"
   261  				}
   262  			]
   263  		},
   264  		"music_extra_reversed": {
   265  			"column_vindexes": [
   266  				{
   267  					"column": "music_id",
   268  					"name": "music_user_map"
   269  				},
   270  				{
   271  					"column": "user_id",
   272  					"name": "hash_index"
   273  				}
   274  			]
   275  		},
   276  		"insert_ignore_test": {
   277  			"column_vindexes": [
   278  				{
   279  					"column": "pv",
   280  					"name": "music_user_map"
   281  				},
   282  				{
   283  					"column": "owned",
   284  					"name": "insert_ignore_idx"
   285  				},
   286  				{
   287  					"column": "verify",
   288  					"name": "hash_index"
   289  				}
   290  			]
   291  		},
   292  		"noauto_table": {
   293  			"column_vindexes": [
   294  				{
   295  					"column": "id",
   296  					"name": "idx_noauto"
   297  				}
   298  			]
   299  		},
   300  		"keyrange_table": {
   301  			"column_vindexes": [
   302  				{
   303  					"column": "krcol_unique",
   304  					"name": "krcol_unique_vdx"
   305  				},
   306  				{
   307  					"column": "krcol",
   308  					"name": "krcol_vdx"
   309  				}
   310  			]
   311  		},
   312  		"ksid_table": {
   313  			"column_vindexes": [
   314  				{
   315  					"column": "keyspace_id",
   316  					"name": "keyspace_id"
   317  				}
   318  			]
   319  		},
   320  		"t1": {
   321        		"column_vindexes": [
   322  				{
   323  				  	"column": "id",
   324  				  	"name": "hash_index"
   325  				},
   326  				{
   327  				  	"column": "unq_col",
   328  				  	"name": "t1_lkp_vdx"
   329  				}
   330              ]
   331      	},
   332  		"t1_lkp_idx": {
   333  			"column_vindexes": [
   334  				{
   335  					"column": "unq_col",
   336  				  	"name": "hash_index"
   337  				}
   338  			]
   339  		},
   340  		"t2_lookup": {
   341        		"column_vindexes": [
   342  				{
   343  				  	"column": "id",
   344  				  	"name": "hash_index"
   345  				},
   346  				{
   347  					"column": "wo_lu_col",
   348  					"name": "t2_wo_lu_vdx"
   349  				},
   350  				{
   351  					"column": "erl_lu_col",
   352  					"name": "t2_erl_lu_vdx"
   353  				},
   354  				{
   355  					"column": "srl_lu_col",
   356  					"name": "t2_srl_lu_vdx"
   357  				},
   358  				{
   359  					"column": "nrl_lu_col",
   360  					"name": "t2_nrl_lu_vdx"
   361  				},
   362  				{
   363  					"column": "nv_lu_col",
   364  					"name": "t2_nv_lu_vdx"
   365  				},
   366  				{
   367  				  	"column": "lu_col",
   368  				  	"name": "t2_lu_vdx"
   369  				}
   370              ]
   371      	},
   372  		"user_region": {
   373  			"column_vindexes": [
   374  				{
   375  					"columns": ["cola","colb"],
   376  					"name": "regional_vdx"
   377  				}
   378  			]
   379      	},
   380  		"tbl_cfc": {
   381  			"column_vindexes": [
   382                  {
   383                      "column": "c1",
   384                      "name": "cfc"
   385                  }
   386  			],
   387  			"columns": [
   388  				{
   389  					"name": "c2",
   390  					"type": "VARCHAR"
   391  				}
   392  			]
   393      	},
   394  		"zip_detail": {
   395  			"type": "reference",
   396  			"source": "TestUnsharded.zip_detail"
   397  		}
   398  	}
   399  }
   400  `
   401  
   402  var badVSchema = `
   403  {
   404  	"sharded": false,
   405  	"tables": {
   406  		"sharded_table": {}
   407  	}
   408  }
   409  `
   410  
   411  var unshardedVSchema = `
   412  {
   413  	"sharded": false,
   414  	"tables": {
   415  		"user_seq": {
   416  			"type": "sequence"
   417  		},
   418  		"music_user_map": {},
   419  		"name_user_map": {},
   420  		"name_lastname_keyspace_id_map": {},
   421  		"user_msgs": {},
   422  		"ins_lookup": {},
   423  		"main1": {
   424  			"auto_increment": {
   425  				"column": "id",
   426  				"sequence": "user_seq"
   427  			}
   428  		},
   429  		"wo_lu_idx": {},
   430  		"erl_lu_idx": {},
   431  		"srl_lu_idx": {},
   432  		"nrl_lu_idx": {},
   433  		"nv_lu_idx": {},
   434  		"lu_idx": {},
   435  		"simple": {},
   436  		"zip_detail": {}
   437  	}
   438  }
   439  `
   440  
   441  const (
   442  	testBufferSize = 10
   443  )
   444  
   445  type DestinationAnyShardPickerFirstShard struct{}
   446  
   447  func (dp DestinationAnyShardPickerFirstShard) PickShard(shardCount int) int {
   448  	return 0
   449  }
   450  
   451  // keyRangeLookuper is for testing a lookup that returns a keyrange.
   452  type keyRangeLookuper struct {
   453  }
   454  
   455  func (v *keyRangeLookuper) String() string   { return "keyrange_lookuper" }
   456  func (*keyRangeLookuper) Cost() int          { return 0 }
   457  func (*keyRangeLookuper) IsUnique() bool     { return false }
   458  func (*keyRangeLookuper) NeedsVCursor() bool { return false }
   459  func (*keyRangeLookuper) Verify(context.Context, vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
   460  	return []bool{}, nil
   461  }
   462  func (*keyRangeLookuper) Map(ctx context.Context, vcursor vindexes.VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
   463  	return []key.Destination{
   464  		key.DestinationKeyRange{
   465  			KeyRange: &topodatapb.KeyRange{
   466  				End: []byte{0x10},
   467  			},
   468  		},
   469  	}, nil
   470  }
   471  
   472  func newKeyRangeLookuper(name string, params map[string]string) (vindexes.Vindex, error) {
   473  	return &keyRangeLookuper{}, nil
   474  }
   475  
   476  // keyRangeLookuperUnique is for testing a unique lookup that returns a keyrange.
   477  type keyRangeLookuperUnique struct {
   478  }
   479  
   480  func (v *keyRangeLookuperUnique) String() string   { return "keyrange_lookuper" }
   481  func (*keyRangeLookuperUnique) Cost() int          { return 0 }
   482  func (*keyRangeLookuperUnique) IsUnique() bool     { return true }
   483  func (*keyRangeLookuperUnique) NeedsVCursor() bool { return false }
   484  func (*keyRangeLookuperUnique) Verify(context.Context, vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
   485  	return []bool{}, nil
   486  }
   487  func (*keyRangeLookuperUnique) Map(ctx context.Context, vcursor vindexes.VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
   488  	return []key.Destination{
   489  		key.DestinationKeyRange{
   490  			KeyRange: &topodatapb.KeyRange{
   491  				End: []byte{0x10},
   492  			},
   493  		},
   494  	}, nil
   495  }
   496  
   497  func newKeyRangeLookuperUnique(name string, params map[string]string) (vindexes.Vindex, error) {
   498  	return &keyRangeLookuperUnique{}, nil
   499  }
   500  
   501  func init() {
   502  	vindexes.Register("keyrange_lookuper", newKeyRangeLookuper)
   503  	vindexes.Register("keyrange_lookuper_unique", newKeyRangeLookuperUnique)
   504  }
   505  
   506  func createExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandboxconn.SandboxConn) {
   507  	cell := "aa"
   508  	hc := discovery.NewFakeHealthCheck(nil)
   509  	s := createSandbox(KsTestSharded)
   510  	s.VSchema = executorVSchema
   511  	serv := newSandboxForCells([]string{cell})
   512  	resolver := newTestResolver(hc, serv, cell)
   513  	sbc1 = hc.AddTestTablet(cell, "-20", 1, "TestExecutor", "-20", topodatapb.TabletType_PRIMARY, true, 1, nil)
   514  	sbc2 = hc.AddTestTablet(cell, "40-60", 1, "TestExecutor", "40-60", topodatapb.TabletType_PRIMARY, true, 1, nil)
   515  	// Create these connections so scatter queries don't fail.
   516  	_ = hc.AddTestTablet(cell, "20-40", 1, "TestExecutor", "20-40", topodatapb.TabletType_PRIMARY, true, 1, nil)
   517  	_ = hc.AddTestTablet(cell, "60-60", 1, "TestExecutor", "60-80", topodatapb.TabletType_PRIMARY, true, 1, nil)
   518  	_ = hc.AddTestTablet(cell, "80-a0", 1, "TestExecutor", "80-a0", topodatapb.TabletType_PRIMARY, true, 1, nil)
   519  	_ = hc.AddTestTablet(cell, "a0-c0", 1, "TestExecutor", "a0-c0", topodatapb.TabletType_PRIMARY, true, 1, nil)
   520  	_ = hc.AddTestTablet(cell, "c0-e0", 1, "TestExecutor", "c0-e0", topodatapb.TabletType_PRIMARY, true, 1, nil)
   521  	_ = hc.AddTestTablet(cell, "e0-", 1, "TestExecutor", "e0-", topodatapb.TabletType_PRIMARY, true, 1, nil)
   522  	// Below is needed so that SendAnyWherePlan doesn't fail
   523  	_ = hc.AddTestTablet(cell, "random", 1, "TestXBadVSchema", "-20", topodatapb.TabletType_PRIMARY, true, 1, nil)
   524  
   525  	createSandbox(KsTestUnsharded)
   526  	sbclookup = hc.AddTestTablet(cell, "0", 1, KsTestUnsharded, "0", topodatapb.TabletType_PRIMARY, true, 1, nil)
   527  
   528  	// Ues the 'X' in the name to ensure it's not alphabetically first.
   529  	// Otherwise, it would become the default keyspace for the dual table.
   530  	bad := createSandbox("TestXBadSharding")
   531  	bad.VSchema = badVSchema
   532  
   533  	getSandbox(KsTestUnsharded).VSchema = unshardedVSchema
   534  	executor = NewExecutor(context.Background(), serv, cell, resolver, false, false, testBufferSize, cache.DefaultConfig, nil, false, querypb.ExecuteOptions_V3)
   535  
   536  	key.AnyShardPicker = DestinationAnyShardPickerFirstShard{}
   537  	// create a new session each time so that ShardSessions don't get re-used across tests
   538  	primarySession = &vtgatepb.Session{
   539  		TargetString: "@primary",
   540  	}
   541  	return executor, sbc1, sbc2, sbclookup
   542  }
   543  
   544  func createCustomExecutor(vschema string) (executor *Executor, sbc1, sbc2, sbclookup *sandboxconn.SandboxConn) {
   545  	cell := "aa"
   546  	hc := discovery.NewFakeHealthCheck(nil)
   547  	s := createSandbox(KsTestSharded)
   548  	s.VSchema = vschema
   549  	serv := newSandboxForCells([]string{cell})
   550  	resolver := newTestResolver(hc, serv, cell)
   551  	sbc1 = hc.AddTestTablet(cell, "-20", 1, "TestExecutor", "-20", topodatapb.TabletType_PRIMARY, true, 1, nil)
   552  	sbc2 = hc.AddTestTablet(cell, "40-60", 1, "TestExecutor", "40-60", topodatapb.TabletType_PRIMARY, true, 1, nil)
   553  
   554  	createSandbox(KsTestUnsharded)
   555  	sbclookup = hc.AddTestTablet(cell, "0", 1, KsTestUnsharded, "0", topodatapb.TabletType_PRIMARY, true, 1, nil)
   556  	getSandbox(KsTestUnsharded).VSchema = unshardedVSchema
   557  
   558  	executor = NewExecutor(context.Background(), serv, cell, resolver, false, false, testBufferSize, cache.DefaultConfig, nil, false, querypb.ExecuteOptions_V3)
   559  	// create a new session each time so that ShardSessions don't get re-used across tests
   560  	primarySession = &vtgatepb.Session{
   561  		TargetString: "@primary",
   562  	}
   563  	return executor, sbc1, sbc2, sbclookup
   564  }
   565  
   566  func createCustomExecutorSetValues(vschema string, values []*sqltypes.Result) (executor *Executor, sbc1, sbc2, sbclookup *sandboxconn.SandboxConn) {
   567  	cell := "aa"
   568  	hc := discovery.NewFakeHealthCheck(nil)
   569  	s := createSandbox(KsTestSharded)
   570  	s.VSchema = vschema
   571  	serv := newSandboxForCells([]string{cell})
   572  	resolver := newTestResolver(hc, serv, cell)
   573  	shards := []string{"-20", "20-40", "40-60", "60-80", "80-a0", "a0-c0", "c0-e0", "e0-"}
   574  	sbcs := []*sandboxconn.SandboxConn{}
   575  	for _, shard := range shards {
   576  		sbc := hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_PRIMARY, true, 1, nil)
   577  		if values != nil {
   578  			sbc.SetResults(values)
   579  		}
   580  		sbcs = append(sbcs, sbc)
   581  	}
   582  
   583  	createSandbox(KsTestUnsharded)
   584  	sbclookup = hc.AddTestTablet(cell, "0", 1, KsTestUnsharded, "0", topodatapb.TabletType_PRIMARY, true, 1, nil)
   585  	getSandbox(KsTestUnsharded).VSchema = unshardedVSchema
   586  
   587  	executor = NewExecutor(context.Background(), serv, cell, resolver, false, false, testBufferSize, cache.DefaultConfig, nil, false, querypb.ExecuteOptions_V3)
   588  	// create a new session each time so that ShardSessions don't get re-used across tests
   589  	primarySession = &vtgatepb.Session{
   590  		TargetString: "@primary",
   591  	}
   592  	return executor, sbcs[0], sbcs[1], sbclookup
   593  }
   594  
   595  func executorExecSession(executor *Executor, sql string, bv map[string]*querypb.BindVariable, session *vtgatepb.Session) (*sqltypes.Result, error) {
   596  	return executor.Execute(
   597  		context.Background(),
   598  		"TestExecute",
   599  		NewSafeSession(session),
   600  		sql,
   601  		bv)
   602  }
   603  
   604  func executorExec(executor *Executor, sql string, bv map[string]*querypb.BindVariable) (*sqltypes.Result, error) {
   605  	return executorExecSession(executor, sql, bv, primarySession)
   606  }
   607  
   608  func executorPrepare(executor *Executor, sql string, bv map[string]*querypb.BindVariable) ([]*querypb.Field, error) {
   609  	return executor.Prepare(
   610  		context.Background(),
   611  		"TestExecute",
   612  		NewSafeSession(primarySession),
   613  		sql,
   614  		bv)
   615  }
   616  
   617  func executorStream(executor *Executor, sql string) (qr *sqltypes.Result, err error) {
   618  	results := make(chan *sqltypes.Result, 100)
   619  	err = executor.StreamExecute(
   620  		context.Background(),
   621  		"TestExecuteStream",
   622  		NewSafeSession(nil),
   623  		sql,
   624  		nil,
   625  		func(qr *sqltypes.Result) error {
   626  			results <- qr
   627  			return nil
   628  		},
   629  	)
   630  	close(results)
   631  	if err != nil {
   632  		return nil, err
   633  	}
   634  	first := true
   635  	for r := range results {
   636  		if first {
   637  			qr = &sqltypes.Result{Fields: r.Fields}
   638  			first = false
   639  		}
   640  		qr.Rows = append(qr.Rows, r.Rows...)
   641  	}
   642  	return qr, nil
   643  }
   644  
   645  func assertQueries(t *testing.T, sbc *sandboxconn.SandboxConn, wantQueries []*querypb.BoundQuery) {
   646  	t.Helper()
   647  	idx := 0
   648  	for _, query := range sbc.Queries {
   649  		if strings.HasPrefix(query.Sql, "savepoint") || strings.HasPrefix(query.Sql, "rollback to") {
   650  			continue
   651  		}
   652  		if len(wantQueries) < idx {
   653  			t.Errorf("got more queries than expected")
   654  		}
   655  		got := query.Sql
   656  		expected := wantQueries[idx].Sql
   657  		assert.Equal(t, expected, got)
   658  		assert.Equal(t, wantQueries[idx].BindVariables, query.BindVariables)
   659  		idx++
   660  	}
   661  }
   662  
   663  func assertQueriesWithSavepoint(t *testing.T, sbc *sandboxconn.SandboxConn, wantQueries []*querypb.BoundQuery) {
   664  	t.Helper()
   665  	require.Equal(t, len(wantQueries), len(sbc.Queries), sbc.Queries)
   666  	savepointStore := make(map[string]string)
   667  	for idx, query := range sbc.Queries {
   668  		require.Equal(t, wantQueries[idx].BindVariables, query.BindVariables)
   669  		got := query.Sql
   670  		expected := wantQueries[idx].Sql
   671  		if strings.HasPrefix(got, "savepoint") {
   672  			if !strings.HasPrefix(expected, "savepoint") {
   673  				t.Fatal("savepoint expected")
   674  			}
   675  			if sp, exists := savepointStore[expected[10:]]; exists {
   676  				assert.Equal(t, sp, got[10:])
   677  			} else {
   678  				savepointStore[expected[10:]] = got[10:]
   679  			}
   680  			continue
   681  		}
   682  		if strings.HasPrefix(got, "rollback to") {
   683  			if !strings.HasPrefix(expected, "rollback to") {
   684  				t.Fatal("rollback to expected")
   685  			}
   686  			assert.Equal(t, savepointStore[expected[12:]], got[12:])
   687  			continue
   688  		}
   689  		assert.Equal(t, expected, got)
   690  	}
   691  }
   692  
   693  func testCommitCount(t *testing.T, sbcName string, sbc *sandboxconn.SandboxConn, want int) {
   694  	t.Helper()
   695  	if got, want := sbc.CommitCount.Get(), int64(want); got != want {
   696  		t.Errorf("%s.CommitCount: %d, want %d\n", sbcName, got, want)
   697  	}
   698  }
   699  
   700  func testNonZeroDuration(t *testing.T, what, d string) {
   701  	t.Helper()
   702  	time, _ := strconv.ParseFloat(d, 64)
   703  	if time == 0 {
   704  		t.Errorf("querylog %s want non-zero duration got %s (%v)", what, d, time)
   705  	}
   706  }
   707  
   708  func getQueryLog(logChan chan any) *logstats.LogStats {
   709  	var log any
   710  
   711  	select {
   712  	case log = <-logChan:
   713  		return log.(*logstats.LogStats)
   714  	default:
   715  		return nil
   716  	}
   717  }
   718  
   719  // Queries can hit the plan cache in less than a microsecond, which makes them
   720  // appear to take 0.000000 time in the query log. To mitigate this in tests,
   721  // keep an in-memory record of queries that we know have been planned during
   722  // the current test execution and skip testing for non-zero plan time if this
   723  // is a repeat query.
   724  var testPlannedQueries = map[string]bool{}
   725  
   726  func testQueryLog(t *testing.T, logChan chan any, method, stmtType, sql string, shardQueries int) *logstats.LogStats {
   727  	t.Helper()
   728  
   729  	logStats := getQueryLog(logChan)
   730  	require.NotNil(t, logStats)
   731  
   732  	var log bytes.Buffer
   733  	streamlog.GetFormatter(QueryLogger)(&log, nil, logStats)
   734  	fields := strings.Split(log.String(), "\t")
   735  
   736  	// fields[0] is the method
   737  	assert.Equal(t, method, fields[0], "logstats: method")
   738  
   739  	// fields[1] - fields[6] are the caller id, start/end times, etc
   740  
   741  	checkEqualQuery := true
   742  	// The internal savepoints are created with uuids so the value of it not known to assert.
   743  	// Therefore, the equal query check is ignored.
   744  	switch stmtType {
   745  	case "SAVEPOINT", "SAVEPOINT_ROLLBACK", "RELEASE":
   746  		checkEqualQuery = false
   747  	}
   748  	// only test the durations if there is no error (fields[16])
   749  	if fields[16] == "\"\"" {
   750  		// fields[7] is the total execution time
   751  		testNonZeroDuration(t, "TotalTime", fields[7])
   752  
   753  		// fields[8] is the planner time. keep track of the planned queries to
   754  		// avoid the case where we hit the plan in cache and it takes less than
   755  		// a microsecond to plan it
   756  		if testPlannedQueries[sql] == false {
   757  			testNonZeroDuration(t, "PlanTime", fields[8])
   758  		}
   759  		testPlannedQueries[sql] = true
   760  
   761  		// fields[9] is ExecuteTime which is not set for certain statements SET,
   762  		// BEGIN, COMMIT, ROLLBACK, etc
   763  		switch stmtType {
   764  		case "BEGIN", "COMMIT", "SET", "ROLLBACK", "SAVEPOINT", "SAVEPOINT_ROLLBACK", "RELEASE":
   765  		default:
   766  			testNonZeroDuration(t, "ExecuteTime", fields[9])
   767  		}
   768  
   769  		// fields[10] is CommitTime which is set only in autocommit mode and
   770  		// tested separately
   771  	}
   772  
   773  	// fields[11] is the statement type
   774  	assert.Equal(t, stmtType, fields[11], "logstats: stmtType")
   775  
   776  	if checkEqualQuery {
   777  		// fields[12] is the original sql
   778  		wantSQL := fmt.Sprintf("%q", sql)
   779  		assert.Equal(t, wantSQL, fields[12], "logstats: SQL")
   780  	}
   781  	// fields[13] contains the formatted bind vars
   782  
   783  	// fields[14] is the count of shard queries
   784  	assert.Equal(t, fmt.Sprintf("%v", shardQueries), fields[14], "logstats: ShardQueries")
   785  
   786  	return logStats
   787  }
   788  
   789  func newTestResolver(hc discovery.HealthCheck, serv srvtopo.Server, cell string) *Resolver {
   790  	sc := newTestScatterConn(hc, serv, cell)
   791  	srvResolver := srvtopo.NewResolver(serv, sc.gateway, cell)
   792  	return NewResolver(srvResolver, serv, cell, sc)
   793  }