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" }