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  }