vitess.io/vitess@v0.16.2/go/vt/vtexplain/vtexplain_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 vtexplain
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"strings"
    25  	"testing"
    26  
    27  	"vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv/tabletenvtest"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/stretchr/testify/require"
    31  
    32  	"vitess.io/vitess/go/vt/key"
    33  	querypb "vitess.io/vitess/go/vt/proto/query"
    34  	"vitess.io/vitess/go/vt/proto/topodata"
    35  	"vitess.io/vitess/go/vt/topo"
    36  )
    37  
    38  func defaultTestOpts() *Options {
    39  	return &Options{
    40  		ReplicationMode: "ROW",
    41  		NumShards:       4,
    42  		Normalize:       true,
    43  		StrictDDL:       true,
    44  	}
    45  }
    46  
    47  type testopts struct {
    48  	shardmap map[string]map[string]*topo.ShardInfo
    49  }
    50  
    51  func initTest(mode string, opts *Options, topts *testopts, t *testing.T) *VTExplain {
    52  	schema, err := os.ReadFile("testdata/test-schema.sql")
    53  	require.NoError(t, err)
    54  
    55  	vSchema, err := os.ReadFile("testdata/test-vschema.json")
    56  	require.NoError(t, err)
    57  
    58  	shardmap := ""
    59  	if topts.shardmap != nil {
    60  		shardmapBytes, err := json.Marshal(topts.shardmap)
    61  		require.NoError(t, err)
    62  
    63  		shardmap = string(shardmapBytes)
    64  	}
    65  
    66  	opts.ExecutionMode = mode
    67  	vte, err := Init(string(vSchema), string(schema), shardmap, opts)
    68  	require.NoError(t, err, "vtexplain Init error\n%s", string(schema))
    69  	return vte
    70  }
    71  
    72  func testExplain(testcase string, opts *Options, t *testing.T) {
    73  	modes := []string{
    74  		ModeMulti,
    75  
    76  		// TwoPC mode is functional, but the output isn't stable for
    77  		// tests since there are timestamps in the value rows
    78  		// ModeTwoPC,
    79  	}
    80  
    81  	for _, mode := range modes {
    82  		runTestCase(testcase, mode, opts, &testopts{}, t)
    83  	}
    84  }
    85  
    86  func runTestCase(testcase, mode string, opts *Options, topts *testopts, t *testing.T) {
    87  	t.Run(testcase, func(t *testing.T) {
    88  		vte := initTest(mode, opts, topts, t)
    89  
    90  		sqlFile := fmt.Sprintf("testdata/%s-queries.sql", testcase)
    91  		sql, err := os.ReadFile(sqlFile)
    92  		require.NoError(t, err, "vtexplain error")
    93  
    94  		textOutFile := fmt.Sprintf("testdata/%s-output/%s-output.txt", mode, testcase)
    95  		expected, _ := os.ReadFile(textOutFile)
    96  
    97  		explains, err := vte.Run(string(sql))
    98  		require.NoError(t, err, "vtexplain error")
    99  		require.NotNil(t, explains, "vtexplain error running %s: no explain", string(sql))
   100  
   101  		// We want to remove the additional `set collation_connection` queries that happen
   102  		// when the tablet connects to MySQL to set the default collation.
   103  		// Removing them lets us keep simpler expected output files.
   104  		for _, e := range explains {
   105  			for i, action := range e.TabletActions {
   106  				var mysqlQueries []*MysqlQuery
   107  				for _, query := range action.MysqlQueries {
   108  					if !strings.Contains(strings.ToLower(query.SQL), "set collation_connection") {
   109  						mysqlQueries = append(mysqlQueries, query)
   110  					}
   111  				}
   112  				e.TabletActions[i].MysqlQueries = mysqlQueries
   113  			}
   114  		}
   115  
   116  		explainText, err := vte.ExplainsAsText(explains)
   117  		require.NoError(t, err, "vtexplain error")
   118  
   119  		if diff := cmp.Diff(strings.TrimSpace(string(expected)), strings.TrimSpace(explainText)); diff != "" {
   120  			// Print the Text that was actually returned and also dump to a
   121  			// temp file to be able to diff the results.
   122  			t.Errorf("Text output did not match (-want +got):\n%s", diff)
   123  
   124  			testOutputTempDir, err := os.MkdirTemp("testdata", "plan_test")
   125  			require.NoError(t, err)
   126  			gotFile := fmt.Sprintf("%s/%s-output.txt", testOutputTempDir, testcase)
   127  			os.WriteFile(gotFile, []byte(explainText), 0644)
   128  
   129  			t.Logf("run the following command to update the expected output:")
   130  			t.Logf("cp %s/* %s", testOutputTempDir, path.Dir(textOutFile))
   131  		}
   132  	})
   133  }
   134  
   135  func TestExplain(t *testing.T) {
   136  	tabletenvtest.LoadTabletEnvFlags()
   137  
   138  	type test struct {
   139  		name string
   140  		opts *Options
   141  	}
   142  	tests := []test{
   143  		{"unsharded", defaultTestOpts()},
   144  		{"selectsharded", defaultTestOpts()},
   145  		{"insertsharded", defaultTestOpts()},
   146  		{"updatesharded", defaultTestOpts()},
   147  		{"deletesharded", defaultTestOpts()},
   148  		{"comments", defaultTestOpts()},
   149  		{"options", &Options{
   150  			ReplicationMode: "STATEMENT",
   151  			NumShards:       4,
   152  			Normalize:       false,
   153  		}},
   154  		{"target", &Options{
   155  			ReplicationMode: "ROW",
   156  			NumShards:       4,
   157  			Normalize:       false,
   158  			Target:          "ks_sharded/40-80",
   159  		}},
   160  		{"gen4", &Options{
   161  			ReplicationMode: "ROW",
   162  			NumShards:       4,
   163  			Normalize:       true,
   164  			PlannerVersion:  querypb.ExecuteOptions_Gen4,
   165  		}},
   166  	}
   167  
   168  	for _, tst := range tests {
   169  		testExplain(tst.name, tst.opts, t)
   170  	}
   171  }
   172  
   173  func TestErrors(t *testing.T) {
   174  	vte := initTest(ModeMulti, defaultTestOpts(), &testopts{}, t)
   175  
   176  	tests := []struct {
   177  		SQL string
   178  		Err string
   179  	}{
   180  		{
   181  			SQL: "INVALID SQL",
   182  			Err: "vtexplain execute error in 'INVALID SQL': syntax error at position 8 near 'INVALID'",
   183  		},
   184  
   185  		{
   186  			SQL: "SELECT * FROM THIS IS NOT SQL",
   187  			Err: "vtexplain execute error in 'SELECT * FROM THIS IS NOT SQL': syntax error at position 22 near 'IS'",
   188  		},
   189  
   190  		{
   191  			SQL: "SELECT * FROM table_not_in_vschema",
   192  			Err: "vtexplain execute error in 'SELECT * FROM table_not_in_vschema': table table_not_in_vschema not found",
   193  		},
   194  
   195  		{
   196  			SQL: "SELECT * FROM table_not_in_schema",
   197  			Err: "unknown error: unable to resolve table name table_not_in_schema",
   198  		},
   199  	}
   200  
   201  	for _, test := range tests {
   202  		t.Run(test.SQL, func(t *testing.T) {
   203  			_, err := vte.Run(test.SQL)
   204  			require.Error(t, err)
   205  			require.Contains(t, err.Error(), test.Err)
   206  		})
   207  	}
   208  }
   209  
   210  func TestJSONOutput(t *testing.T) {
   211  	vte := initTest(ModeMulti, defaultTestOpts(), &testopts{}, t)
   212  	sql := "select 1 from user where id = 1"
   213  	explains, err := vte.Run(sql)
   214  	require.NoError(t, err, "vtexplain error")
   215  	require.NotNil(t, explains, "vtexplain error running %s: no explain", string(sql))
   216  
   217  	for _, e := range explains {
   218  		for i, action := range e.TabletActions {
   219  			var mysqlQueries []*MysqlQuery
   220  			for _, query := range action.MysqlQueries {
   221  				if !strings.Contains(strings.ToLower(query.SQL), "set collation_connection") {
   222  					mysqlQueries = append(mysqlQueries, query)
   223  				}
   224  			}
   225  			e.TabletActions[i].MysqlQueries = mysqlQueries
   226  		}
   227  	}
   228  	explainJSON := ExplainsAsJSON(explains)
   229  
   230  	var data any
   231  	err = json.Unmarshal([]byte(explainJSON), &data)
   232  	require.NoError(t, err, "error unmarshaling json")
   233  
   234  	array, ok := data.([]any)
   235  	if !ok || len(array) != 1 {
   236  		t.Errorf("expected single-element top-level array, got:\n%s", explainJSON)
   237  	}
   238  
   239  	explain, ok := array[0].(map[string]any)
   240  	if !ok {
   241  		t.Errorf("expected explain map, got:\n%s", explainJSON)
   242  	}
   243  
   244  	if explain["SQL"] != sql {
   245  		t.Errorf("expected SQL, got:\n%s", explainJSON)
   246  	}
   247  
   248  	plans, ok := explain["Plans"].([]any)
   249  	if !ok || len(plans) != 1 {
   250  		t.Errorf("expected single-element plans array, got:\n%s", explainJSON)
   251  	}
   252  
   253  	actions, ok := explain["TabletActions"].(map[string]any)
   254  	if !ok {
   255  		t.Errorf("expected TabletActions map, got:\n%s", explainJSON)
   256  	}
   257  
   258  	actionsJSON, err := json.MarshalIndent(actions, "", "    ")
   259  	require.NoError(t, err, "error in json marshal")
   260  	wantJSON := `{
   261      "ks_sharded/-40": {
   262          "MysqlQueries": [
   263              {
   264                  "SQL": "select 1 from ` + "`user`" + ` where id = 1 limit 10001",
   265                  "Time": 1
   266              }
   267          ],
   268          "TabletQueries": [
   269              {
   270                  "BindVars": {
   271                      "#maxLimit": "10001",
   272                      "vtg1": "1"
   273                  },
   274                  "SQL": "select :vtg1 from ` + "`user`" + ` where id = :vtg1",
   275                  "Time": 1
   276              }
   277          ]
   278      }
   279  }`
   280  	diff := cmp.Diff(wantJSON, string(actionsJSON))
   281  	if diff != "" {
   282  		t.Errorf(diff)
   283  	}
   284  }
   285  
   286  func testShardInfo(ks, start, end string, primaryServing bool, t *testing.T) *topo.ShardInfo {
   287  	kr, err := key.ParseKeyRangeParts(start, end)
   288  	require.NoError(t, err)
   289  
   290  	return topo.NewShardInfo(
   291  		ks,
   292  		fmt.Sprintf("%s-%s", start, end),
   293  		&topodata.Shard{KeyRange: kr, IsPrimaryServing: primaryServing},
   294  		&vtexplainTestTopoVersion{},
   295  	)
   296  }
   297  
   298  func TestUsingKeyspaceShardMap(t *testing.T) {
   299  	tests := []struct {
   300  		testcase      string
   301  		ShardRangeMap map[string]map[string]*topo.ShardInfo
   302  	}{
   303  		{
   304  			testcase: "select-sharded-8",
   305  			ShardRangeMap: map[string]map[string]*topo.ShardInfo{
   306  				"ks_sharded": {
   307  					"-20":   testShardInfo("ks_sharded", "", "20", true, t),
   308  					"20-40": testShardInfo("ks_sharded", "20", "40", true, t),
   309  					"40-60": testShardInfo("ks_sharded", "40", "60", true, t),
   310  					"60-80": testShardInfo("ks_sharded", "60", "80", true, t),
   311  					"80-a0": testShardInfo("ks_sharded", "80", "a0", true, t),
   312  					"a0-c0": testShardInfo("ks_sharded", "a0", "c0", true, t),
   313  					"c0-e0": testShardInfo("ks_sharded", "c0", "e0", true, t),
   314  					"e0-":   testShardInfo("ks_sharded", "e0", "", true, t),
   315  					// Some non-serving shards below - these should never be in the output of vtexplain
   316  					"-80": testShardInfo("ks_sharded", "", "80", false, t),
   317  					"80-": testShardInfo("ks_sharded", "80", "", false, t),
   318  				},
   319  			},
   320  		},
   321  		{
   322  			testcase: "uneven-keyspace",
   323  			ShardRangeMap: map[string]map[string]*topo.ShardInfo{
   324  				// Have mercy on the poor soul that has this keyspace sharding.
   325  				// But, hey, vtexplain still works so they have that going for them.
   326  				"ks_sharded": {
   327  					"-80":   testShardInfo("ks_sharded", "", "80", true, t),
   328  					"80-90": testShardInfo("ks_sharded", "80", "90", true, t),
   329  					"90-a0": testShardInfo("ks_sharded", "90", "a0", true, t),
   330  					"a0-e8": testShardInfo("ks_sharded", "a0", "e8", true, t),
   331  					"e8-":   testShardInfo("ks_sharded", "e8", "", true, t),
   332  					// Plus some un-even shards that are not serving and which should never be in the output of vtexplain
   333  					"80-a0": testShardInfo("ks_sharded", "80", "a0", false, t),
   334  					"a0-a5": testShardInfo("ks_sharded", "a0", "a5", false, t),
   335  					"a5-":   testShardInfo("ks_sharded", "a5", "", false, t),
   336  				},
   337  			},
   338  		},
   339  	}
   340  
   341  	for _, test := range tests {
   342  		runTestCase(test.testcase, ModeMulti, defaultTestOpts(), &testopts{test.ShardRangeMap}, t)
   343  	}
   344  }
   345  
   346  func TestInit(t *testing.T) {
   347  	vschema := `{
   348    "ks1": {
   349      "sharded": true,
   350      "tables": {
   351        "table_missing_primary_vindex": {}
   352      }
   353    }
   354  }`
   355  	schema := "create table table_missing_primary_vindex (id int primary key)"
   356  	_, err := Init(vschema, schema, "", defaultTestOpts())
   357  	require.Error(t, err)
   358  	require.Contains(t, err.Error(), "missing primary col vindex")
   359  }
   360  
   361  type vtexplainTestTopoVersion struct{}
   362  
   363  func (vtexplain *vtexplainTestTopoVersion) String() string { return "vtexplain-test-topo" }