vitess.io/vitess@v0.16.2/go/vt/schema/online_ddl.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 schema 18 19 import ( 20 "encoding/hex" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "regexp" 25 "strconv" 26 "strings" 27 "time" 28 29 vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" 30 "vitess.io/vitess/go/vt/sqlparser" 31 "vitess.io/vitess/go/vt/vterrors" 32 ) 33 34 var ( 35 onlineDdlUUIDRegexp = regexp.MustCompile(`^[0-f]{8}_[0-f]{4}_[0-f]{4}_[0-f]{4}_[0-f]{12}$`) 36 onlineDDLGeneratedTableNameRegexp = regexp.MustCompile(`^_[0-f]{8}_[0-f]{4}_[0-f]{4}_[0-f]{4}_[0-f]{12}_([0-9]{14})_(gho|ghc|del|new|vrepl)$`) 37 ptOSCGeneratedTableNameRegexp = regexp.MustCompile(`^_.*_old$`) 38 ) 39 40 var ( 41 // ErrDirectDDLDisabled is returned when direct DDL is disabled, and a user attempts to run a DDL statement 42 ErrDirectDDLDisabled = errors.New("direct DDL is disabled") 43 // ErrOnlineDDLDisabled is returned when online DDL is disabled, and a user attempts to run an online DDL operation (submit, review, control) 44 ErrOnlineDDLDisabled = errors.New("online DDL is disabled") 45 // ErrForeignKeyFound indicates any finding of FOREIGN KEY clause in a DDL statement 46 ErrForeignKeyFound = errors.New("Foreign key found") 47 // ErrRenameTableFound indicates finding of ALTER TABLE...RENAME in ddl statement 48 ErrRenameTableFound = errors.New("RENAME clause found") 49 ) 50 51 const ( 52 SchemaMigrationsTableName = "schema_migrations" 53 RevertActionStr = "revert" 54 ) 55 56 // when validateWalk returns true, then the child nodes are also visited 57 func validateWalk(node sqlparser.SQLNode, allowForeignKeys bool) (kontinue bool, err error) { 58 switch node.(type) { 59 case *sqlparser.CreateTable, *sqlparser.AlterTable, 60 *sqlparser.TableSpec, *sqlparser.AddConstraintDefinition, *sqlparser.ConstraintDefinition: 61 return true, nil 62 case *sqlparser.ForeignKeyDefinition: 63 if !allowForeignKeys { 64 return false, ErrForeignKeyFound 65 } 66 case *sqlparser.RenameTableName: 67 return false, ErrRenameTableFound 68 } 69 return false, nil 70 } 71 72 // OnlineDDLStatus is an indicator to a online DDL status 73 type OnlineDDLStatus string 74 75 const ( 76 OnlineDDLStatusRequested OnlineDDLStatus = "requested" 77 OnlineDDLStatusCancelled OnlineDDLStatus = "cancelled" 78 OnlineDDLStatusQueued OnlineDDLStatus = "queued" 79 OnlineDDLStatusReady OnlineDDLStatus = "ready" 80 OnlineDDLStatusRunning OnlineDDLStatus = "running" 81 OnlineDDLStatusComplete OnlineDDLStatus = "complete" 82 OnlineDDLStatusFailed OnlineDDLStatus = "failed" 83 ) 84 85 // OnlineDDL encapsulates the relevant information in an online schema change request 86 type OnlineDDL struct { 87 Keyspace string `json:"keyspace,omitempty"` 88 Table string `json:"table,omitempty"` 89 Schema string `json:"schema,omitempty"` 90 SQL string `json:"sql,omitempty"` 91 UUID string `json:"uuid,omitempty"` 92 Strategy DDLStrategy `json:"strategy,omitempty"` 93 Options string `json:"options,omitempty"` 94 RequestTime int64 `json:"time_created,omitempty"` 95 MigrationContext string `json:"context,omitempty"` 96 Status OnlineDDLStatus `json:"status,omitempty"` 97 TabletAlias string `json:"tablet,omitempty"` 98 Retries int64 `json:"retries,omitempty"` 99 ReadyToComplete int64 `json:"ready_to_complete,omitempty"` 100 } 101 102 // FromJSON creates an OnlineDDL from json 103 func FromJSON(bytes []byte) (*OnlineDDL, error) { 104 onlineDDL := &OnlineDDL{} 105 err := json.Unmarshal(bytes, onlineDDL) 106 return onlineDDL, err 107 } 108 109 // ParseOnlineDDLStatement parses the given SQL into a statement and returns the action type of the DDL statement, or error 110 // if the statement is not a DDL 111 func ParseOnlineDDLStatement(sql string) (ddlStmt sqlparser.DDLStatement, action sqlparser.DDLAction, err error) { 112 stmt, err := sqlparser.Parse(sql) 113 if err != nil { 114 return nil, 0, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "error parsing statement: SQL=%s, error=%+v", sql, err) 115 } 116 switch ddlStmt := stmt.(type) { 117 case sqlparser.DDLStatement: 118 return ddlStmt, ddlStmt.GetAction(), nil 119 } 120 return ddlStmt, action, vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "unsupported query type: %s", sql) 121 } 122 123 func onlineDDLStatementSanity(sql string, ddlStmt sqlparser.DDLStatement, ddlStrategySetting *DDLStrategySetting) error { 124 // SQL statement sanity checks: 125 if !ddlStmt.IsFullyParsed() { 126 if _, err := sqlparser.ParseStrictDDL(sql); err != nil { 127 // More information about the reason why the statement is not fully parsed: 128 return vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.SyntaxError, "%v", err) 129 } 130 return vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.SyntaxError, "cannot parse statement: %v", sql) 131 } 132 133 walkFunc := func(node sqlparser.SQLNode) (kontinue bool, err error) { 134 return validateWalk(node, ddlStrategySetting.IsAllowForeignKeysFlag()) 135 } 136 if err := sqlparser.Walk(walkFunc, ddlStmt); err != nil { 137 switch err { 138 case ErrForeignKeyFound: 139 return vterrors.Errorf(vtrpcpb.Code_ABORTED, "foreign key constraints are not supported in online DDL, see https://vitess.io/blog/2021-06-15-online-ddl-why-no-fk/") 140 case ErrRenameTableFound: 141 return vterrors.Errorf(vtrpcpb.Code_ABORTED, "ALTER TABLE ... RENAME is not supported in online DDL") 142 } 143 } 144 return nil 145 } 146 147 // NewOnlineDDLs takes a single DDL statement, normalizes it (potentially break down into multiple statements), and generates one or more OnlineDDL instances, one for each normalized statement 148 func NewOnlineDDLs(keyspace string, sql string, ddlStmt sqlparser.DDLStatement, ddlStrategySetting *DDLStrategySetting, migrationContext string, providedUUID string) (onlineDDLs [](*OnlineDDL), err error) { 149 appendOnlineDDL := func(tableName string, ddlStmt sqlparser.DDLStatement) error { 150 if err := onlineDDLStatementSanity(sql, ddlStmt, ddlStrategySetting); err != nil { 151 return err 152 } 153 onlineDDL, err := NewOnlineDDL(keyspace, tableName, sqlparser.String(ddlStmt), ddlStrategySetting, migrationContext, providedUUID) 154 if err != nil { 155 return err 156 } 157 if len(onlineDDLs) > 0 && providedUUID != "" { 158 return vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "UUID %s provided but multiple DDLs generated", providedUUID) 159 } 160 onlineDDLs = append(onlineDDLs, onlineDDL) 161 return nil 162 } 163 switch ddlStmt := ddlStmt.(type) { 164 case *sqlparser.CreateTable, *sqlparser.AlterTable, *sqlparser.CreateView, *sqlparser.AlterView: 165 if err := appendOnlineDDL(ddlStmt.GetTable().Name.String(), ddlStmt); err != nil { 166 return nil, err 167 } 168 case *sqlparser.DropTable, *sqlparser.DropView: 169 tables := ddlStmt.GetFromTables() 170 for _, table := range tables { 171 ddlStmt.SetFromTables([]sqlparser.TableName{table}) 172 if err := appendOnlineDDL(table.Name.String(), ddlStmt); err != nil { 173 return nil, err 174 } 175 } 176 default: 177 return nil, vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "unsupported statement for Online DDL: %v", sqlparser.String(ddlStmt)) 178 } 179 180 return onlineDDLs, nil 181 } 182 183 // NewOnlineDDL creates a schema change request with self generated UUID and RequestTime 184 func NewOnlineDDL(keyspace string, table string, sql string, ddlStrategySetting *DDLStrategySetting, migrationContext string, providedUUID string) (onlineDDL *OnlineDDL, err error) { 185 if ddlStrategySetting == nil { 186 return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "NewOnlineDDL: found nil DDLStrategySetting") 187 } 188 var onlineDDLUUID string 189 if providedUUID != "" { 190 if !IsOnlineDDLUUID(providedUUID) { 191 return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "NewOnlineDDL: not a valid UUID: %s", providedUUID) 192 } 193 onlineDDLUUID = providedUUID 194 } else { 195 // No explicit UUID provided. We generate our own 196 onlineDDLUUID, err = CreateOnlineDDLUUID() 197 if err != nil { 198 return nil, err 199 } 200 } 201 202 { 203 encodeDirective := func(directive string) string { 204 return strconv.Quote(hex.EncodeToString([]byte(directive))) 205 } 206 comments := sqlparser.Comments{ 207 fmt.Sprintf(`/*vt+ uuid=%s context=%s table=%s strategy=%s options=%s */`, 208 encodeDirective(onlineDDLUUID), 209 encodeDirective(migrationContext), 210 encodeDirective(table), 211 encodeDirective(string(ddlStrategySetting.Strategy)), 212 encodeDirective(ddlStrategySetting.Options), 213 )} 214 if uuid, err := legacyParseRevertUUID(sql); err == nil { 215 sql = fmt.Sprintf("revert vitess_migration '%s'", uuid) 216 } 217 218 stmt, err := sqlparser.Parse(sql) 219 if err != nil { 220 isLegacyRevertStatement := false 221 // query validation and rebuilding 222 if _, err := legacyParseRevertUUID(sql); err == nil { 223 // This is a revert statement of the form "revert <uuid>". We allow this for now. Future work will 224 // make sure the statement is a valid, parseable "revert vitess_migration '<uuid>'", but we must 225 // be backwards compatible for now. 226 isLegacyRevertStatement = true 227 } 228 if !isLegacyRevertStatement { 229 // otherwise the statement should have been parseable! 230 return nil, err 231 } 232 } else { 233 switch stmt := stmt.(type) { 234 case sqlparser.DDLStatement: 235 stmt.SetComments(comments) 236 case *sqlparser.RevertMigration: 237 stmt.SetComments(comments) 238 default: 239 return nil, vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "Unsupported statement for Online DDL: %v", sqlparser.String(stmt)) 240 } 241 sql = sqlparser.String(stmt) 242 } 243 } 244 245 return &OnlineDDL{ 246 Keyspace: keyspace, 247 Table: table, 248 SQL: sql, 249 UUID: onlineDDLUUID, 250 Strategy: ddlStrategySetting.Strategy, 251 Options: ddlStrategySetting.Options, 252 RequestTime: time.Now().UnixNano(), 253 MigrationContext: migrationContext, 254 Status: OnlineDDLStatusRequested, 255 }, nil 256 } 257 258 func formatWithoutComments(buf *sqlparser.TrackedBuffer, node sqlparser.SQLNode) { 259 if _, ok := node.(*sqlparser.ParsedComments); ok { 260 return 261 } 262 node.Format(buf) 263 } 264 265 // OnlineDDLFromCommentedStatement creates a schema instance based on a commented query. The query is expected 266 // to be commented as e.g. `CREATE /*vt+ uuid=... context=... table=... strategy=... options=... */ TABLE ...` 267 func OnlineDDLFromCommentedStatement(stmt sqlparser.Statement) (onlineDDL *OnlineDDL, err error) { 268 var comments *sqlparser.ParsedComments 269 switch stmt := stmt.(type) { 270 case sqlparser.DDLStatement: 271 comments = stmt.GetParsedComments() 272 case *sqlparser.RevertMigration: 273 comments = stmt.Comments 274 default: 275 return nil, vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "unsupported statement for Online DDL: %v", sqlparser.String(stmt)) 276 } 277 278 if comments.Length() == 0 { 279 return nil, vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "no comments found in statement: %v", sqlparser.String(stmt)) 280 } 281 282 directives := comments.Directives() 283 decodeDirective := func(name string) (string, error) { 284 value, ok := directives.GetString(name, "") 285 if !ok { 286 return "", vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "no value found for comment directive %s", name) 287 } 288 b, err := hex.DecodeString(value) 289 if err != nil { 290 return "", err 291 } 292 return string(b), nil 293 } 294 295 buf := sqlparser.NewTrackedBuffer(formatWithoutComments) 296 stmt.Format(buf) 297 298 onlineDDL = &OnlineDDL{ 299 SQL: buf.String(), 300 } 301 if onlineDDL.UUID, err = decodeDirective("uuid"); err != nil { 302 return nil, err 303 } 304 if !IsOnlineDDLUUID(onlineDDL.UUID) { 305 return nil, vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "invalid UUID read from statement %s", sqlparser.String(stmt)) 306 } 307 if onlineDDL.Table, err = decodeDirective("table"); err != nil { 308 return nil, err 309 } 310 if strategy, err := decodeDirective("strategy"); err == nil { 311 onlineDDL.Strategy = DDLStrategy(strategy) 312 } else { 313 return nil, err 314 } 315 if options, err := decodeDirective("options"); err == nil { 316 onlineDDL.Options = options 317 } else { 318 return nil, err 319 } 320 if onlineDDL.MigrationContext, err = decodeDirective("context"); err != nil { 321 return nil, err 322 } 323 return onlineDDL, nil 324 } 325 326 // StrategySetting returns the ddl strategy setting associated with this online DDL 327 func (onlineDDL *OnlineDDL) StrategySetting() *DDLStrategySetting { 328 return NewDDLStrategySetting(onlineDDL.Strategy, onlineDDL.Options) 329 } 330 331 // RequestTimeSeconds converts request time to seconds (losing nano precision) 332 func (onlineDDL *OnlineDDL) RequestTimeSeconds() int64 { 333 return onlineDDL.RequestTime / int64(time.Second) 334 } 335 336 // ToJSON exports this onlineDDL to JSON 337 func (onlineDDL *OnlineDDL) ToJSON() ([]byte, error) { 338 return json.Marshal(onlineDDL) 339 } 340 341 // sqlWithoutComments returns the SQL statement without comment directives. Useful for tests 342 func (onlineDDL *OnlineDDL) sqlWithoutComments() (sql string, err error) { 343 sql = onlineDDL.SQL 344 stmt, err := sqlparser.Parse(sql) 345 if err != nil { 346 // query validation and rebuilding 347 if _, err := legacyParseRevertUUID(sql); err == nil { 348 // This is a revert statement of the form "revert <uuid>". We allow this for now. Future work will 349 // make sure the statement is a valid, parseable "revert vitess_migration '<uuid>'", but we must 350 // be backwards compatible for now. 351 return sql, nil 352 } 353 // otherwise the statement should have been parseable! 354 return "", err 355 } 356 357 switch stmt := stmt.(type) { 358 case sqlparser.DDLStatement: 359 stmt.SetComments(nil) 360 case *sqlparser.RevertMigration: 361 stmt.SetComments(nil) 362 } 363 sql = sqlparser.String(stmt) 364 return sql, nil 365 } 366 367 // GetAction extracts the DDL action type from the online DDL statement 368 func (onlineDDL *OnlineDDL) GetAction() (action sqlparser.DDLAction, err error) { 369 if _, err := onlineDDL.GetRevertUUID(); err == nil { 370 return sqlparser.RevertDDLAction, nil 371 } 372 373 _, action, err = ParseOnlineDDLStatement(onlineDDL.SQL) 374 return action, err 375 } 376 377 // IsView returns 'true' when the statement affects a VIEW 378 func (onlineDDL *OnlineDDL) IsView() bool { 379 stmt, _, err := ParseOnlineDDLStatement(onlineDDL.SQL) 380 if err != nil { 381 return false 382 } 383 switch stmt.(type) { 384 case *sqlparser.CreateView, *sqlparser.DropView, *sqlparser.AlterView: 385 return true 386 } 387 return false 388 } 389 390 // GetActionStr returns a string representation of the DDL action 391 func (onlineDDL *OnlineDDL) GetActionStr() (action sqlparser.DDLAction, actionStr string, err error) { 392 action, err = onlineDDL.GetAction() 393 if err != nil { 394 return action, actionStr, err 395 } 396 switch action { 397 case sqlparser.RevertDDLAction: 398 return action, RevertActionStr, nil 399 case sqlparser.CreateDDLAction: 400 return action, sqlparser.CreateStr, nil 401 case sqlparser.AlterDDLAction: 402 return action, sqlparser.AlterStr, nil 403 case sqlparser.DropDDLAction: 404 return action, sqlparser.DropStr, nil 405 } 406 return action, "", vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "unsupported online DDL action. SQL=%s", onlineDDL.SQL) 407 } 408 409 // GetRevertUUID works when this migration is a revert for another migration. It returns the UUID 410 // fo the reverted migration. 411 // The function returns error when this is not a revert migration. 412 func (onlineDDL *OnlineDDL) GetRevertUUID() (uuid string, err error) { 413 if uuid, err := legacyParseRevertUUID(onlineDDL.SQL); err == nil { 414 return uuid, nil 415 } 416 if stmt, err := sqlparser.Parse(onlineDDL.SQL); err == nil { 417 if revert, ok := stmt.(*sqlparser.RevertMigration); ok { 418 return revert.UUID, nil 419 } 420 } 421 return "", vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "not a Revert DDL: '%s'", onlineDDL.SQL) 422 } 423 424 // ToString returns a simple string representation of this instance 425 func (onlineDDL *OnlineDDL) ToString() string { 426 return fmt.Sprintf("OnlineDDL: keyspace=%s, table=%s, sql=%s", onlineDDL.Keyspace, onlineDDL.Table, onlineDDL.SQL) 427 } 428 429 // GetGCUUID gets this OnlineDDL UUID in GC UUID format 430 func (onlineDDL *OnlineDDL) GetGCUUID() string { 431 return OnlineDDLToGCUUID(onlineDDL.UUID) 432 } 433 434 // CreateOnlineDDLUUID creates a UUID in OnlineDDL format, e.g.: 435 // a0638f6b_ec7b_11ea_9bf8_000d3a9b8a9a 436 func CreateOnlineDDLUUID() (string, error) { 437 return CreateUUIDWithDelimiter("_") 438 } 439 440 // IsOnlineDDLUUID answers 'true' when the given string is an online-ddl UUID, e.g.: 441 // a0638f6b_ec7b_11ea_9bf8_000d3a9b8a9a 442 func IsOnlineDDLUUID(uuid string) bool { 443 return onlineDdlUUIDRegexp.MatchString(uuid) 444 } 445 446 // OnlineDDLToGCUUID converts a UUID in online-ddl format to GC-table format 447 func OnlineDDLToGCUUID(uuid string) string { 448 return strings.Replace(uuid, "_", "", -1) 449 } 450 451 // IsOnlineDDLTableName answers 'true' when the given table name _appears to be_ a name 452 // generated by an online DDL operation; either the name determined by the online DDL Executor, or 453 // by pt-online-schema-change. 454 // There is no guarantee that the tables _was indeed_ generated by an online DDL flow. 455 func IsOnlineDDLTableName(tableName string) bool { 456 if onlineDDLGeneratedTableNameRegexp.MatchString(tableName) { 457 return true 458 } 459 if ptOSCGeneratedTableNameRegexp.MatchString(tableName) { 460 return true 461 } 462 return false 463 }