Repository: vitessio/vitess (Go)

Issue:
Bug Report: Online DDL with foreign keys: MySQL fails altering shadow table when modifying column `NULL` -> `NOT NULL`
### Overview of the Issue

Consider this simple foreign key scenario:
```sql
create table parent (
  id int auto_increment,
  primary key(id)
);
create table child (
  id int auto_increment,
  parent_id int null,
  primary key(id),
  constraint test_fk foreign key (parent_id) references parent (id) on delete no action
);
```
If we run this Online DDL with `--unsafe-allow-foreign-keys`:

```sql
alter table child modify parent_id int not null
```

Then we get this error:
```
Cannot change column 'parent_id': used in a foreign key constraint 'test_fk_2waaktwt2x1i9xg3g55nrytg9' (errno 1832) (sqlstate HY000) during query: ALTER TABLE `_vt_vrp_eb0ec194d60d11ee97b70a43f95f28a3_20240228074940_` MODIFY COLUMN `parent_id` int NOT NULL, ALGORITHM = copy
```

The SQL statement `alter table child modify parent_id int not null` is valid. But vitess then adds `, ALGORITHM = copy` and that causes the statement to fail. It's a MySQL issue, because the statement shouldn't fail with `ALGORITHM = copy`. For example, modifying a `NOT NULL` column to `NULL` does work with `ALGORITHM = copy`.

We added `ALGORITHM = copy` in https://github.com/vitessio/vitess/pull/12436 and in https://github.com/vitessio/vitess/pull/12521 due to a change in the table format in MySQL `8.0.29` that caused `Xtrabackup` to fail backing up tables that were modified with `ALGORITHM=INSTANT`. See for example [this blog post by Percona](https://www.percona.com/blog/percona-xtrabackup-8-0-29-and-instant-add-drop-columns/). So we made that workaround to overcome a MySQL vs. Xtrabackup issue.

The `INSTANT`-`Xtrabackup` problem is known to be resolved starting MySQL `8.0.32`.


### Reproduction Steps

See above.

### Binary Version

```sh
`release-19`.
```


### Operating System and Environment details

```sh
Any
```


### Log Fragments

_No response_



Fix (patch):
diff --git a/go/mysql/capabilities/capability.go b/go/mysql/capabilities/capability.go
index d4b66821096..6364f950022 100644
--- a/go/mysql/capabilities/capability.go
+++ b/go/mysql/capabilities/capability.go
@@ -45,7 +45,8 @@ const (
 	DynamicRedoLogCapacityFlavorCapability                              // supported in MySQL 8.0.30 and above: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-30.html
 	DisableRedoLogFlavorCapability                                      // supported in MySQL 8.0.21 and above: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-21.html
 	CheckConstraintsCapability                                          // supported in MySQL 8.0.16 and above: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-16.html
-	PerformanceSchemaDataLocksTableCapability
+	PerformanceSchemaDataLocksTableCapability                           // supported in MySQL 8.0.1 and above: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-1.html
+	InstantDDLXtrabackupCapability                                      // Supported in 8.0.32 and above, solving a MySQL-vs-Xtrabackup bug starting 8.0.29
 )
 
 type CapableOf func(capability FlavorCapability) (bool, error)
@@ -109,6 +110,8 @@ func MySQLVersionHasCapability(serverVersion string, capability FlavorCapability
 		return atLeast(8, 0, 29)
 	case DynamicRedoLogCapacityFlavorCapability:
 		return atLeast(8, 0, 30)
+	case InstantDDLXtrabackupCapability:
+		return atLeast(8, 0, 32)
 	default:
 		return false, nil
 	}
diff --git a/go/vt/vttablet/onlineddl/executor.go b/go/vt/vttablet/onlineddl/executor.go
index 0fff5d920eb..19f5c82ca5a 100644
--- a/go/vt/vttablet/onlineddl/executor.go
+++ b/go/vt/vttablet/onlineddl/executor.go
@@ -1315,7 +1315,12 @@ func (e *Executor) validateAndEditCreateTableStatement(ctx context.Context, onli
 // validateAndEditAlterTableStatement inspects the AlterTable statement and:
 // - modifies any CONSTRAINT name according to given name mapping
 // - explode ADD FULLTEXT KEY into multiple statements
-func (e *Executor) validateAndEditAlterTableStatement(ctx context.Context, onlineDDL *schema.OnlineDDL, alterTable *sqlparser.AlterTable, constraintMap map[string]string) (alters []*sqlparser.AlterTable, err error) {
+func (e *Executor) validateAndEditAlterTableStatement(ctx context.Context, capableOf capabilities.CapableOf, onlineDDL *schema.OnlineDDL, alterTable *sqlparser.AlterTable, constraintMap map[string]string) (alters []*sqlparser.AlterTable, err error) {
+	capableOfInstantDDLXtrabackup, err := capableOf(capabilities.InstantDDLXtrabackupCapability)
+	if err != nil {
+		return nil, err
+	}
+
 	hashExists := map[string]bool{}
 	validateWalk := func(node sqlparser.SQLNode) (kontinue bool, err error) {
 		switch node := node.(type) {
@@ -1347,8 +1352,10 @@ func (e *Executor) validateAndEditAlterTableStatement(ctx context.Context, onlin
 		opt := alterTable.AlterOptions[i]
 		switch opt := opt.(type) {
 		case sqlparser.AlgorithmValue:
-			// we do not pass ALGORITHM. We choose our own ALGORITHM.
-			continue
+			if !capableOfInstantDDLXtrabackup {
+				// we do not pass ALGORITHM. We choose our own ALGORITHM.
+				continue
+			}
 		case *sqlparser.AddIndexDefinition:
 			if opt.IndexDefinition.Info.Type == sqlparser.IndexTypeFullText {
 				countAddFullTextStatements++
@@ -1357,7 +1364,10 @@ func (e *Executor) validateAndEditAlterTableStatement(ctx context.Context, onlin
 					// in the same statement
 					extraAlterTable := &sqlparser.AlterTable{
 						Table:        alterTable.Table,
-						AlterOptions: []sqlparser.AlterOption{opt, copyAlgorithm},
+						AlterOptions: []sqlparser.AlterOption{opt},
+					}
+					if !capableOfInstantDDLXtrabackup {
+						extraAlterTable.AlterOptions = append(extraAlterTable.AlterOptions, copyAlgorithm)
 					}
 					alters = append(alters, extraAlterTable)
 					continue
@@ -1367,7 +1377,9 @@ func (e *Executor) validateAndEditAlterTableStatement(ctx context.Context, onlin
 		redactedOptions = append(redactedOptions, opt)
 	}
 	alterTable.AlterOptions = redactedOptions
-	alterTable.AlterOptions = append(alterTable.AlterOptions, copyAlgorithm)
+	if !capableOfInstantDDLXtrabackup {
+		alterTable.AlterOptions = append(alterTable.AlterOptions, copyAlgorithm)
+	}
 	return alters, nil
 }
 
@@ -1461,7 +1473,9 @@ func (e *Executor) initVreplicationOriginalMigration(ctx context.Context, online
 	// ALTER TABLE should apply to the vrepl table
 	alterTable.SetTable(alterTable.GetTable().Qualifier.CompliantName(), vreplTableName)
 	// Also, change any constraint names:
-	alters, err := e.validateAndEditAlterTableStatement(ctx, onlineDDL, alterTable, constraintMap)
+
+	capableOf := mysql.ServerVersionCapableOf(conn.ServerVersion)
+	alters, err := e.validateAndEditAlterTableStatement(ctx, capableOf, onlineDDL, alterTable, constraintMap)
 	if err != nil {
 		return v, err
 	}