vitess.io/vitess@v0.16.2/go/vt/vttablet/onlineddl/analysis.go (about)

     1  /*
     2  Copyright 2022 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 onlineddl
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"strings"
    23  
    24  	"vitess.io/vitess/go/mysql"
    25  	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
    26  	"vitess.io/vitess/go/vt/schema"
    27  	"vitess.io/vitess/go/vt/sqlparser"
    28  	"vitess.io/vitess/go/vt/vterrors"
    29  )
    30  
    31  type specialAlterOperation string
    32  
    33  const (
    34  	instantDDLSpecialOperation         specialAlterOperation = "instant-ddl"
    35  	dropRangePartitionSpecialOperation specialAlterOperation = "drop-range-partition"
    36  	addRangePartitionSpecialOperation  specialAlterOperation = "add-range-partition"
    37  )
    38  
    39  type SpecialAlterPlan struct {
    40  	operation   specialAlterOperation
    41  	details     map[string]string
    42  	alterTable  *sqlparser.AlterTable
    43  	createTable *sqlparser.CreateTable
    44  }
    45  
    46  func NewSpecialAlterOperation(operation specialAlterOperation, alterTable *sqlparser.AlterTable, createTable *sqlparser.CreateTable) *SpecialAlterPlan {
    47  	return &SpecialAlterPlan{
    48  		operation:   operation,
    49  		details:     map[string]string{"operation": string(operation)},
    50  		alterTable:  alterTable,
    51  		createTable: createTable,
    52  	}
    53  }
    54  
    55  func (p *SpecialAlterPlan) SetDetail(key string, val string) *SpecialAlterPlan {
    56  	p.details[key] = val
    57  	return p
    58  }
    59  
    60  func (p *SpecialAlterPlan) Detail(key string) string {
    61  	return p.details[key]
    62  }
    63  
    64  func (p *SpecialAlterPlan) String() string {
    65  	b, err := json.Marshal(p.details)
    66  	if err != nil {
    67  		return ""
    68  	}
    69  	return string(b)
    70  }
    71  
    72  // getCreateTableStatement gets a formal AlterTable representation of the given table
    73  func (e *Executor) getCreateTableStatement(ctx context.Context, tableName string) (*sqlparser.CreateTable, error) {
    74  	showCreateTable, err := e.showCreateTable(ctx, tableName)
    75  	if err != nil {
    76  		return nil, vterrors.Wrapf(err, "in Executor.getCreateTableStatement()")
    77  	}
    78  	stmt, err := sqlparser.ParseStrictDDL(showCreateTable)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	createTable, ok := stmt.(*sqlparser.CreateTable)
    83  	if !ok {
    84  		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "expected CREATE TABLE. Got %v", sqlparser.CanonicalString(stmt))
    85  	}
    86  	return createTable, nil
    87  }
    88  
    89  // analyzeDropRangePartition sees if the online DDL drops a single partition in a range partitioned table
    90  func analyzeDropRangePartition(alterTable *sqlparser.AlterTable, createTable *sqlparser.CreateTable) (*SpecialAlterPlan, error) {
    91  	// we are looking for a `ALTER TABLE <table> DROP PARTITION <name>` statement with nothing else
    92  	if len(alterTable.AlterOptions) > 0 {
    93  		return nil, nil
    94  	}
    95  	if alterTable.PartitionOption != nil {
    96  		return nil, nil
    97  	}
    98  	spec := alterTable.PartitionSpec
    99  	if spec == nil {
   100  		return nil, nil
   101  	}
   102  	if spec.Action != sqlparser.DropAction {
   103  		return nil, nil
   104  	}
   105  	if len(spec.Names) != 1 {
   106  		return nil, vterrors.Errorf(vtrpcpb.Code_FAILED_PRECONDITION, "vitess only supports dropping a single partition per query: %v", sqlparser.CanonicalString(alterTable))
   107  	}
   108  	partitionName := spec.Names[0].String()
   109  	// OK then!
   110  
   111  	// Now, is this query dropping the first partition in a RANGE partitioned table?
   112  	part := createTable.TableSpec.PartitionOption
   113  	if part.Type != sqlparser.RangeType {
   114  		return nil, nil
   115  	}
   116  	if len(part.Definitions) == 0 {
   117  		return nil, nil
   118  	}
   119  	var partitionDefinition *sqlparser.PartitionDefinition
   120  	var nextPartitionName string
   121  	for i, p := range part.Definitions {
   122  		if p.Name.String() == partitionName {
   123  			partitionDefinition = p
   124  			if i+1 < len(part.Definitions) {
   125  				nextPartitionName = part.Definitions[i+1].Name.String()
   126  			}
   127  			break
   128  		}
   129  	}
   130  	if partitionDefinition == nil {
   131  		// dropping a nonexistent partition. We'll let the "standard" migration execution flow deal with that.
   132  		return nil, nil
   133  	}
   134  	op := NewSpecialAlterOperation(dropRangePartitionSpecialOperation, alterTable, createTable)
   135  	op.SetDetail("partition_name", partitionName)
   136  	op.SetDetail("partition_definition", sqlparser.CanonicalString(partitionDefinition))
   137  	op.SetDetail("next_partition_name", nextPartitionName)
   138  	return op, nil
   139  }
   140  
   141  // analyzeAddRangePartition sees if the online DDL adds a partition in a range partitioned table
   142  func analyzeAddRangePartition(alterTable *sqlparser.AlterTable, createTable *sqlparser.CreateTable) *SpecialAlterPlan {
   143  	// we are looking for a `ALTER TABLE <table> ADD PARTITION (PARTITION ...)` statement with nothing else
   144  	if len(alterTable.AlterOptions) > 0 {
   145  		return nil
   146  	}
   147  	if alterTable.PartitionOption != nil {
   148  		return nil
   149  	}
   150  	spec := alterTable.PartitionSpec
   151  	if spec == nil {
   152  		return nil
   153  	}
   154  	if spec.Action != sqlparser.AddAction {
   155  		return nil
   156  	}
   157  	if len(spec.Definitions) != 1 {
   158  		return nil
   159  	}
   160  	partitionDefinition := spec.Definitions[0]
   161  	partitionName := partitionDefinition.Name.String()
   162  	// OK then!
   163  
   164  	// Now, is this query adding a partition in a RANGE partitioned table?
   165  	part := createTable.TableSpec.PartitionOption
   166  	if part.Type != sqlparser.RangeType {
   167  		return nil
   168  	}
   169  	if len(part.Definitions) == 0 {
   170  		return nil
   171  	}
   172  	op := NewSpecialAlterOperation(addRangePartitionSpecialOperation, alterTable, createTable)
   173  	op.SetDetail("partition_name", partitionName)
   174  	op.SetDetail("partition_definition", sqlparser.CanonicalString(partitionDefinition))
   175  	return op
   176  }
   177  
   178  // alterOptionAvailableViaInstantDDL chcks if the specific alter option is eligible to run via ALGORITHM=INSTANT
   179  // reference: https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html
   180  func alterOptionAvailableViaInstantDDL(alterOption sqlparser.AlterOption, createTable *sqlparser.CreateTable, capableOf mysql.CapableOf) (bool, error) {
   181  	findColumn := func(colName string) *sqlparser.ColumnDefinition {
   182  		if createTable == nil {
   183  			return nil
   184  		}
   185  		for _, col := range createTable.TableSpec.Columns {
   186  			if strings.EqualFold(colName, col.Name.String()) {
   187  				return col
   188  			}
   189  		}
   190  		return nil
   191  	}
   192  	findTableOption := func(optName string) *sqlparser.TableOption {
   193  		if createTable == nil {
   194  			return nil
   195  		}
   196  		for _, opt := range createTable.TableSpec.Options {
   197  			if strings.EqualFold(optName, opt.Name) {
   198  				return opt
   199  			}
   200  		}
   201  		return nil
   202  	}
   203  	isVirtualColumn := func(colName string) bool {
   204  		col := findColumn(colName)
   205  		if col == nil {
   206  			return false
   207  		}
   208  		if col.Type.Options == nil {
   209  			return false
   210  		}
   211  		if col.Type.Options.As == nil {
   212  			return false
   213  		}
   214  		return col.Type.Options.Storage == sqlparser.VirtualStorage
   215  	}
   216  	colStringStrippedDown := func(col *sqlparser.ColumnDefinition, stripDefault bool, stripEnum bool) string {
   217  		strippedCol := sqlparser.CloneRefOfColumnDefinition(col)
   218  		if stripDefault {
   219  			strippedCol.Type.Options.Default = nil
   220  		}
   221  		if stripEnum {
   222  			strippedCol.Type.EnumValues = nil
   223  		}
   224  		return sqlparser.CanonicalString(strippedCol)
   225  	}
   226  	hasPrefix := func(vals []string, prefix []string) bool {
   227  		if len(vals) < len(prefix) {
   228  			return false
   229  		}
   230  		for i := range prefix {
   231  			if vals[i] != prefix[i] {
   232  				return false
   233  			}
   234  		}
   235  		return true
   236  	}
   237  	// Up to 8.0.26 we could only ADD COLUMN as last column
   238  	switch opt := alterOption.(type) {
   239  	case *sqlparser.ChangeColumn:
   240  		// We do not support INSTANT for renaming a column (ALTER TABLE ...CHANGE) because:
   241  		// 1. We discourage column rename
   242  		// 2. We do not produce CHANGE statements in declarative diff
   243  		// 3. The success of the operation depends on whether the column is referenced by a foreign key
   244  		//    in another table. Which is a bit too much to compute here.
   245  		return false, nil
   246  	case *sqlparser.AddColumns:
   247  		if opt.First || opt.After != nil {
   248  			// not a "last" column. Only supported as of 8.0.29
   249  			return capableOf(mysql.InstantAddDropColumnFlavorCapability)
   250  		}
   251  		// Adding a *last* column is supported in 8.0
   252  		return capableOf(mysql.InstantAddLastColumnFlavorCapability)
   253  	case *sqlparser.DropColumn:
   254  		// not supported in COMPRESSED tables
   255  		if opt := findTableOption("ROW_FORMAT"); opt != nil {
   256  			if strings.EqualFold(opt.String, "COMPRESSED") {
   257  				return false, nil
   258  			}
   259  		}
   260  		if isVirtualColumn(opt.Name.Name.String()) {
   261  			// supported by all 8.0 versions
   262  			return capableOf(mysql.InstantAddDropVirtualColumnFlavorCapability)
   263  		}
   264  		return capableOf(mysql.InstantAddDropColumnFlavorCapability)
   265  	case *sqlparser.ModifyColumn:
   266  		if col := findColumn(opt.NewColDefinition.Name.String()); col != nil {
   267  			// Check if only diff is change of default
   268  			// we temporarily remove the DEFAULT expression (if any) from both
   269  			// table and ALTER statement, and compare the columns: if they're otherwise equal,
   270  			// then the only change can be an addition/change/removal of DEFAULT, which
   271  			// is instant-table.
   272  			tableColDefinition := colStringStrippedDown(col, true, false)
   273  			newColDefinition := colStringStrippedDown(opt.NewColDefinition, true, false)
   274  			if tableColDefinition == newColDefinition {
   275  				return capableOf(mysql.InstantChangeColumnDefaultFlavorCapability)
   276  			}
   277  			// Check if:
   278  			// 1. this an ENUM/SET
   279  			// 2. and the change is to append values to the end of the list
   280  			// 3. and the number of added values does not increase the storage size for the enum/set
   281  			// 4. while still not caring about a change in the default value
   282  			if len(col.Type.EnumValues) > 0 && len(opt.NewColDefinition.Type.EnumValues) > 0 {
   283  				// both are enum or set
   284  				if !hasPrefix(opt.NewColDefinition.Type.EnumValues, col.Type.EnumValues) {
   285  					return false, nil
   286  				}
   287  				// we know the new column definition is identical to, or extends, the old definition.
   288  				// Now validate storage:
   289  				if strings.EqualFold(col.Type.Type, "enum") {
   290  					if len(col.Type.EnumValues) <= 255 && len(opt.NewColDefinition.Type.EnumValues) > 255 {
   291  						// this increases the SET storage size (1 byte for up to 8 values, 2 bytes beyond)
   292  						return false, nil
   293  					}
   294  				}
   295  				if strings.EqualFold(col.Type.Type, "set") {
   296  					if (len(col.Type.EnumValues)+7)/8 != (len(opt.NewColDefinition.Type.EnumValues)+7)/8 {
   297  						// this increases the SET storage size (1 byte for up to 8 values, 2 bytes for 8-15, etc.)
   298  						return false, nil
   299  					}
   300  				}
   301  				// Now don't care about change of default:
   302  				tableColDefinition := colStringStrippedDown(col, true, true)
   303  				newColDefinition := colStringStrippedDown(opt.NewColDefinition, true, true)
   304  				if tableColDefinition == newColDefinition {
   305  					return capableOf(mysql.InstantExpandEnumCapability)
   306  				}
   307  			}
   308  		}
   309  		return false, nil
   310  	default:
   311  		return false, nil
   312  	}
   313  }
   314  
   315  // AnalyzeInstantDDL takes declarative CreateTable and AlterTable, as well as a server version, and checks whether it is possible to run the ALTER
   316  // using ALGORITM=INSTANT for that version.
   317  // This function is INTENTIONALLY public, even though we do not guarantee that it will remain so.
   318  func AnalyzeInstantDDL(alterTable *sqlparser.AlterTable, createTable *sqlparser.CreateTable, capableOf mysql.CapableOf) (*SpecialAlterPlan, error) {
   319  	capable, err := capableOf(mysql.InstantDDLFlavorCapability)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  	if !capable {
   324  		return nil, nil
   325  	}
   326  	if alterTable.PartitionOption != nil {
   327  		// no INSTANT for partitions
   328  		return nil, nil
   329  	}
   330  	if alterTable.PartitionSpec != nil {
   331  		// no INSTANT for partitions
   332  		return nil, nil
   333  	}
   334  	// For the ALTER statement to qualify for ALGORITHM=INSTANT, all alter options must each qualify.
   335  	for _, alterOption := range alterTable.AlterOptions {
   336  		instantOK, err := alterOptionAvailableViaInstantDDL(alterOption, createTable, capableOf)
   337  		if err != nil {
   338  			return nil, err
   339  		}
   340  		if !instantOK {
   341  			return nil, nil
   342  		}
   343  	}
   344  	op := NewSpecialAlterOperation(instantDDLSpecialOperation, alterTable, createTable)
   345  	return op, nil
   346  }
   347  
   348  // analyzeSpecialAlterPlan checks if the given ALTER onlineDDL, and for the current state of affected table,
   349  // can be executed in a special way. If so, it returns with a "special plan"
   350  func (e *Executor) analyzeSpecialAlterPlan(ctx context.Context, onlineDDL *schema.OnlineDDL, capableOf mysql.CapableOf) (*SpecialAlterPlan, error) {
   351  	ddlStmt, _, err := schema.ParseOnlineDDLStatement(onlineDDL.SQL)
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  	alterTable, ok := ddlStmt.(*sqlparser.AlterTable)
   356  	if !ok {
   357  		// We only deal here with ALTER TABLE
   358  		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "expected ALTER TABLE. Got %v", sqlparser.CanonicalString(ddlStmt))
   359  	}
   360  
   361  	createTable, err := e.getCreateTableStatement(ctx, onlineDDL.Table)
   362  	if err != nil {
   363  		return nil, vterrors.Wrapf(err, "in Executor.analyzeSpecialAlterPlan(), uuid=%v, table=%v", onlineDDL.UUID, onlineDDL.Table)
   364  	}
   365  
   366  	// special plans which support reverts are trivially desired:
   367  	// special plans which do not support reverts are flag protected:
   368  	if onlineDDL.StrategySetting().IsFastRangeRotationFlag() {
   369  		op, err := analyzeDropRangePartition(alterTable, createTable)
   370  		if err != nil {
   371  			return nil, err
   372  		}
   373  		if op != nil {
   374  			return op, nil
   375  		}
   376  		if op := analyzeAddRangePartition(alterTable, createTable); op != nil {
   377  			return op, nil
   378  		}
   379  	}
   380  	if onlineDDL.StrategySetting().IsPreferInstantDDL() {
   381  		op, err := AnalyzeInstantDDL(alterTable, createTable, capableOf)
   382  		if err != nil {
   383  			return nil, err
   384  		}
   385  		if op != nil {
   386  			return op, nil
   387  		}
   388  	}
   389  	return nil, nil
   390  }