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  }