vitess.io/vitess@v0.16.2/go/vt/wrangler/schema.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 wrangler 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "html/template" 24 "sync" 25 "time" 26 27 "vitess.io/vitess/go/vt/concurrency" 28 "vitess.io/vitess/go/vt/log" 29 "vitess.io/vitess/go/vt/logutil" 30 "vitess.io/vitess/go/vt/mysqlctl/tmutils" 31 "vitess.io/vitess/go/vt/schema" 32 "vitess.io/vitess/go/vt/topo" 33 "vitess.io/vitess/go/vt/topo/topoproto" 34 "vitess.io/vitess/go/vt/vtctl/schematools" 35 "vitess.io/vitess/go/vt/vttablet/tabletmanager/vreplication" 36 37 tabletmanagerdatapb "vitess.io/vitess/go/vt/proto/tabletmanagerdata" 38 topodatapb "vitess.io/vitess/go/vt/proto/topodata" 39 vtctldatapb "vitess.io/vitess/go/vt/proto/vtctldata" 40 ) 41 42 const ( 43 // DefaultWaitReplicasTimeout is the default value for waitReplicasTimeout, which is used when calling method CopySchemaShardFromShard. 44 DefaultWaitReplicasTimeout = 10 * time.Second 45 ) 46 47 // helper method to asynchronously diff a schema 48 func (wr *Wrangler) diffSchema(ctx context.Context, primarySchema *tabletmanagerdatapb.SchemaDefinition, primaryTabletAlias, alias *topodatapb.TabletAlias, excludeTables []string, includeViews bool, wg *sync.WaitGroup, er concurrency.ErrorRecorder) { 49 defer wg.Done() 50 log.Infof("Gathering schema for %v", topoproto.TabletAliasString(alias)) 51 req := &tabletmanagerdatapb.GetSchemaRequest{ExcludeTables: excludeTables, IncludeViews: includeViews} 52 replicaSchema, err := schematools.GetSchema(ctx, wr.ts, wr.tmc, alias, req) 53 if err != nil { 54 er.RecordError(fmt.Errorf("GetSchema(%v, nil, %v, %v) failed: %v", alias, excludeTables, includeViews, err)) 55 return 56 } 57 58 log.Infof("Diffing schema for %v", topoproto.TabletAliasString(alias)) 59 tmutils.DiffSchema(topoproto.TabletAliasString(primaryTabletAlias), primarySchema, topoproto.TabletAliasString(alias), replicaSchema, er) 60 } 61 62 // ValidateSchemaShard will diff the schema from all the tablets in the shard. 63 func (wr *Wrangler) ValidateSchemaShard(ctx context.Context, keyspace, shard string, excludeTables []string, includeViews bool, includeVSchema bool) error { 64 si, err := wr.ts.GetShard(ctx, keyspace, shard) 65 if err != nil { 66 return fmt.Errorf("GetShard(%v, %v) failed: %v", keyspace, shard, err) 67 } 68 69 // get schema from the primary, or error 70 if !si.HasPrimary() { 71 return fmt.Errorf("no primary in shard %v/%v", keyspace, shard) 72 } 73 log.Infof("Gathering schema for primary %v", topoproto.TabletAliasString(si.PrimaryAlias)) 74 req := &tabletmanagerdatapb.GetSchemaRequest{ExcludeTables: excludeTables, IncludeViews: includeViews} 75 primarySchema, err := schematools.GetSchema(ctx, wr.ts, wr.tmc, si.PrimaryAlias, req) 76 if err != nil { 77 return fmt.Errorf("GetSchema(%v, nil, %v, %v) failed: %v", si.PrimaryAlias, excludeTables, includeViews, err) 78 } 79 80 if includeVSchema { 81 err := wr.ValidateVSchema(ctx, keyspace, []string{shard}, excludeTables, includeViews) 82 if err != nil { 83 return err 84 } 85 } 86 87 // read all the aliases in the shard, that is all tablets that are 88 // replicating from the primary 89 aliases, err := wr.ts.FindAllTabletAliasesInShard(ctx, keyspace, shard) 90 if err != nil { 91 return fmt.Errorf("FindAllTabletAliasesInShard(%v, %v) failed: %v", keyspace, shard, err) 92 } 93 94 // then diff with all replicas 95 er := concurrency.AllErrorRecorder{} 96 wg := sync.WaitGroup{} 97 for _, alias := range aliases { 98 if topoproto.TabletAliasEqual(alias, si.PrimaryAlias) { 99 continue 100 } 101 102 wg.Add(1) 103 go wr.diffSchema(ctx, primarySchema, si.PrimaryAlias, alias, excludeTables, includeViews, &wg, &er) 104 } 105 wg.Wait() 106 if er.HasErrors() { 107 return fmt.Errorf("schema diffs: %v", er.Error().Error()) 108 } 109 return nil 110 } 111 112 // ValidateSchemaKeyspace will diff the schema from all the tablets in the keyspace. 113 func (wr *Wrangler) ValidateSchemaKeyspace(ctx context.Context, keyspace string, excludeTables []string, includeViews, skipNoPrimary bool, includeVSchema bool) error { 114 res, err := wr.VtctldServer().ValidateSchemaKeyspace(ctx, &vtctldatapb.ValidateSchemaKeyspaceRequest{ 115 Keyspace: keyspace, 116 ExcludeTables: excludeTables, 117 IncludeViews: includeViews, 118 IncludeVschema: includeVSchema, 119 SkipNoPrimary: skipNoPrimary, 120 }) 121 122 for _, result := range res.Results { 123 wr.Logger().Printf("%s\n", result) 124 } 125 126 if len(res.Results) > 0 { 127 return fmt.Errorf("schema diffs: %v", res.Results) 128 } 129 130 return err 131 } 132 133 // ValidateVSchema compares the schema of each primary tablet in "keyspace/shards..." to the vschema and errs if there are differences 134 func (wr *Wrangler) ValidateVSchema(ctx context.Context, keyspace string, shards []string, excludeTables []string, includeViews bool) error { 135 vschm, err := wr.ts.GetVSchema(ctx, keyspace) 136 if err != nil { 137 return fmt.Errorf("GetVSchema(%s) failed: %v", keyspace, err) 138 } 139 140 shardFailures := concurrency.AllErrorRecorder{} 141 var wg sync.WaitGroup 142 wg.Add(len(shards)) 143 144 for _, shard := range shards { 145 go func(shard string) { 146 defer wg.Done() 147 notFoundTables := []string{} 148 si, err := wr.ts.GetShard(ctx, keyspace, shard) 149 if err != nil { 150 shardFailures.RecordError(fmt.Errorf("GetShard(%v, %v) failed: %v", keyspace, shard, err)) 151 return 152 } 153 req := &tabletmanagerdatapb.GetSchemaRequest{ExcludeTables: excludeTables, IncludeViews: includeViews} 154 primarySchema, err := schematools.GetSchema(ctx, wr.ts, wr.tmc, si.PrimaryAlias, req) 155 if err != nil { 156 shardFailures.RecordError(fmt.Errorf("GetSchema(%s, nil, %v, %v) (%v/%v) failed: %v", si.PrimaryAlias.String(), 157 excludeTables, includeViews, keyspace, shard, err, 158 )) 159 return 160 } 161 for _, tableDef := range primarySchema.TableDefinitions { 162 if _, ok := vschm.Tables[tableDef.Name]; !ok { 163 if !schema.IsInternalOperationTableName(tableDef.Name) { 164 notFoundTables = append(notFoundTables, tableDef.Name) 165 } 166 } 167 } 168 if len(notFoundTables) > 0 { 169 shardFailure := fmt.Errorf("%v/%v has tables that are not in the vschema: %v", keyspace, shard, notFoundTables) 170 shardFailures.RecordError(shardFailure) 171 } 172 }(shard) 173 } 174 wg.Wait() 175 if shardFailures.HasErrors() { 176 return fmt.Errorf("ValidateVSchema(%v, %v, %v, %v) failed: %v", keyspace, shards, excludeTables, includeViews, shardFailures.Error().Error()) 177 } 178 return nil 179 } 180 181 // PreflightSchema will try a schema change on the remote tablet. 182 func (wr *Wrangler) PreflightSchema(ctx context.Context, tabletAlias *topodatapb.TabletAlias, changes []string) ([]*tabletmanagerdatapb.SchemaChangeResult, error) { 183 ti, err := wr.ts.GetTablet(ctx, tabletAlias) 184 if err != nil { 185 return nil, fmt.Errorf("GetTablet(%v) failed: %v", tabletAlias, err) 186 } 187 return wr.tmc.PreflightSchema(ctx, ti.Tablet, changes) 188 } 189 190 // CopySchemaShardFromShard copies the schema from a source shard to the specified destination shard. 191 // For both source and destination it picks the primary tablet. See also CopySchemaShard. 192 func (wr *Wrangler) CopySchemaShardFromShard(ctx context.Context, tables, excludeTables []string, includeViews bool, sourceKeyspace, sourceShard, destKeyspace, destShard string, waitReplicasTimeout time.Duration, skipVerify bool) error { 193 sourceShardInfo, err := wr.ts.GetShard(ctx, sourceKeyspace, sourceShard) 194 if err != nil { 195 return fmt.Errorf("GetShard(%v, %v) failed: %v", sourceKeyspace, sourceShard, err) 196 } 197 if sourceShardInfo.PrimaryAlias == nil { 198 return fmt.Errorf("no primary in shard record %v/%v. Consider running 'vtctl InitShardPrimary' in case of a new shard or reparenting the shard to fix the topology data, or providing a non-primary tablet alias", sourceKeyspace, sourceShard) 199 } 200 201 return wr.CopySchemaShard(ctx, sourceShardInfo.PrimaryAlias, tables, excludeTables, includeViews, destKeyspace, destShard, waitReplicasTimeout, skipVerify) 202 } 203 204 // CopySchemaShard copies the schema from a source tablet to the 205 // specified shard. The schema is applied directly on the primary of 206 // the destination shard, and is propagated to the replicas through 207 // binlogs. 208 func (wr *Wrangler) CopySchemaShard(ctx context.Context, sourceTabletAlias *topodatapb.TabletAlias, tables, excludeTables []string, includeViews bool, destKeyspace, destShard string, waitReplicasTimeout time.Duration, skipVerify bool) error { 209 destShardInfo, err := wr.ts.GetShard(ctx, destKeyspace, destShard) 210 if err != nil { 211 return fmt.Errorf("GetShard(%v, %v) failed: %v", destKeyspace, destShard, err) 212 } 213 214 if destShardInfo.PrimaryAlias == nil { 215 return fmt.Errorf("no primary in shard record %v/%v. Consider running 'vtctl InitShardPrimary' in case of a new shard or reparenting the shard to fix the topology data", destKeyspace, destShard) 216 } 217 218 diffs, err := schematools.CompareSchemas(ctx, wr.ts, wr.tmc, sourceTabletAlias, destShardInfo.PrimaryAlias, tables, excludeTables, includeViews) 219 if err != nil { 220 return fmt.Errorf("CopySchemaShard failed because schemas could not be compared initially: %v", err) 221 } 222 if diffs == nil { 223 // Return early because dest has already the same schema as source. 224 return nil 225 } 226 227 req := &tabletmanagerdatapb.GetSchemaRequest{Tables: tables, ExcludeTables: excludeTables, IncludeViews: includeViews} 228 sourceSd, err := schematools.GetSchema(ctx, wr.ts, wr.tmc, sourceTabletAlias, req) 229 if err != nil { 230 return fmt.Errorf("GetSchema(%v, %v, %v, %v) failed: %v", sourceTabletAlias, tables, excludeTables, includeViews, err) 231 } 232 233 createSQLstmts := tmutils.SchemaDefinitionToSQLStrings(sourceSd) 234 235 destTabletInfo, err := wr.ts.GetTablet(ctx, destShardInfo.PrimaryAlias) 236 if err != nil { 237 return fmt.Errorf("GetTablet(%v) failed: %v", destShardInfo.PrimaryAlias, err) 238 } 239 for _, createSQL := range createSQLstmts { 240 err = wr.applySQLShard(ctx, destTabletInfo, createSQL) 241 if err != nil { 242 return fmt.Errorf("creating a table failed."+ 243 " Most likely some tables already exist on the destination and differ from the source."+ 244 " Please remove all to be copied tables from the destination manually and run this command again."+ 245 " Full error: %v", err) 246 } 247 } 248 249 // Remember the replication position after all the above were applied. 250 destPrimaryPos, err := wr.tmc.PrimaryPosition(ctx, destTabletInfo.Tablet) 251 if err != nil { 252 return fmt.Errorf("CopySchemaShard: can't get replication position after schema applied: %v", err) 253 } 254 255 // Although the copy was successful, we have to verify it to catch the case 256 // where the database already existed on the destination, but with different 257 // options e.g. a different character set. 258 // In that case, MySQL would have skipped our CREATE DATABASE IF NOT EXISTS 259 // statement. 260 if !skipVerify { 261 diffs, err = schematools.CompareSchemas(ctx, wr.ts, wr.tmc, sourceTabletAlias, destShardInfo.PrimaryAlias, tables, excludeTables, includeViews) 262 if err != nil { 263 return fmt.Errorf("CopySchemaShard failed because schemas could not be compared finally: %v", err) 264 } 265 if diffs != nil { 266 return fmt.Errorf("CopySchemaShard was not successful because the schemas between the two tablets %v and %v differ: %v", sourceTabletAlias, destShardInfo.PrimaryAlias, diffs) 267 } 268 } 269 270 // Notify Replicass to reload schema. This is best-effort. 271 reloadCtx, cancel := context.WithTimeout(ctx, waitReplicasTimeout) 272 defer cancel() 273 resp, err := wr.VtctldServer().ReloadSchemaShard(reloadCtx, &vtctldatapb.ReloadSchemaShardRequest{ 274 Keyspace: destKeyspace, 275 Shard: destShard, 276 WaitPosition: destPrimaryPos, 277 Concurrency: 10, 278 IncludePrimary: true, 279 }) 280 if resp != nil { 281 for _, e := range resp.Events { 282 logutil.LogEvent(wr.Logger(), e) 283 } 284 } 285 return err 286 } 287 288 // applySQLShard applies a given SQL change on a given tablet alias. It allows executing arbitrary 289 // SQL statements, but doesn't return any results, so it's only useful for SQL statements 290 // that would be run for their effects (e.g., CREATE). 291 // It works by applying the SQL statement on the shard's primary tablet with replication turned on. 292 // Thus it should be used only for changes that can be applied on a live instance without causing issues; 293 // it shouldn't be used for anything that will require a pivot. 294 // The SQL statement string is expected to have {{.DatabaseName}} in place of the actual db name. 295 func (wr *Wrangler) applySQLShard(ctx context.Context, tabletInfo *topo.TabletInfo, change string) error { 296 filledChange, err := fillStringTemplate(change, map[string]string{"DatabaseName": tabletInfo.DbName()}) 297 if err != nil { 298 return fmt.Errorf("fillStringTemplate failed: %v", err) 299 } 300 ctx, cancel := context.WithTimeout(ctx, 30*time.Second) 301 defer cancel() 302 // Need to make sure that replication is enabled since we're only applying the statement on primaries 303 _, err = wr.tmc.ApplySchema(ctx, tabletInfo.Tablet, &tmutils.SchemaChange{ 304 SQL: filledChange, 305 Force: false, 306 AllowReplication: true, 307 SQLMode: vreplication.SQLMode, 308 }) 309 return err 310 } 311 312 // fillStringTemplate returns the string template filled 313 func fillStringTemplate(tmpl string, vars any) (string, error) { 314 myTemplate := template.Must(template.New("").Parse(tmpl)) 315 data := new(bytes.Buffer) 316 if err := myTemplate.Execute(data, vars); err != nil { 317 return "", err 318 } 319 return data.String(), nil 320 }