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 }