Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove ALGORITHM=COPY from Online DDL in MySQL >= 8.0.32 #15376

Merged
merged 8 commits into from
Mar 6, 2024
5 changes: 4 additions & 1 deletion go/mysql/capabilities/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
10 changes: 10 additions & 0 deletions go/mysql/capabilities/capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ func TestMySQLVersionCapableOf(t *testing.T) {
capability: PerformanceSchemaDataLocksTableCapability,
isCapable: true,
},
{
version: "8.0.29",
capability: InstantDDLXtrabackupCapability,
isCapable: false,
},
{
version: "8.0.32",
capability: InstantDDLXtrabackupCapability,
isCapable: true,
},
{
// What happens if server version is unspecified
version: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,27 @@ func TestSchemaChange(t *testing.T) {

throttler.EnableLagThrottlerAndWaitForStatus(t, clusterInstance, time.Second)

fkOnlineDDLPossible := false
t.Run("check 'rename_table_preserve_foreign_key' variable", func(t *testing.T) {
// Online DDL is not possible on vanilla MySQL 8.0 for reasons described in https://vitess.io/blog/2021-06-15-online-ddl-why-no-fk/.
// However, Online DDL is made possible in via these changes:
// - https://github.com/planetscale/mysql-server/commit/bb777e3e86387571c044fb4a2beb4f8c60462ced
// - https://github.com/planetscale/mysql-server/commit/c2f1344a6863518d749f2eb01a4c74ca08a5b889
// as part of https://github.com/planetscale/mysql-server/releases/tag/8.0.34-ps3.
// Said changes introduce a new global/session boolean variable named 'rename_table_preserve_foreign_key'. It defaults 'false'/0 for backwards compatibility.
// When enabled, a `RENAME TABLE` to a FK parent "pins" the children's foreign keys to the table name rather than the table pointer. Which means after the RENAME,
// the children will point to the newly instated table rather than the original, renamed table.
// (Note: this applies to a particular type of RENAME where we swap tables, see the above blog post).
// For FK children, the MySQL changes simply ignore any Vitess-internal table.
//
// In this stress test, we enable Online DDL if the variable 'rename_table_preserve_foreign_key' is present. The Online DDL mechanism will in turn
// query for this variable, and manipulate it, when starting the migration and when cutting over.
rs, err := shards[0].Vttablets[0].VttabletProcess.QueryTablet("show global variables like 'rename_table_preserve_foreign_key'", keyspaceName, false)
require.NoError(t, err)
fkOnlineDDLPossible = len(rs.Rows) > 0
t.Logf("MySQL support for 'rename_table_preserve_foreign_key': %v", fkOnlineDDLPossible)
})

files, err := os.ReadDir(testDataPath)
require.NoError(t, err)
for _, f := range files {
Expand All @@ -142,7 +163,7 @@ func TestSchemaChange(t *testing.T) {
}
// this is a test!
t.Run(f.Name(), func(t *testing.T) {
testSingle(t, f.Name())
testSingle(t, f.Name(), fkOnlineDDLPossible)
})
}
}
Expand All @@ -161,7 +182,14 @@ func readTestFile(t *testing.T, testName string, fileName string) (content strin

// testSingle is the main testing function for a single test in the suite.
// It prepares the grounds, creates the test data, runs a migration, expects results/error, cleans up.
func testSingle(t *testing.T, testName string) {
func testSingle(t *testing.T, testName string, fkOnlineDDLPossible bool) {
if _, exists := readTestFile(t, testName, "require_rename_table_preserve_foreign_key"); exists {
if !fkOnlineDDLPossible {
t.Skipf("Skipping test due to require_rename_table_preserve_foreign_key")
return
}
}
Comment on lines +186 to +191
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead use mysqlctl.GetVersionString() here and check for the PS fork indicator in the string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what https://github.com/vitessio/vitess/pull/15376/files#diff-5b2f7e26e6a031b4098461528dca98e5ff630e70e1986f35efaab59f3fa5bd76R137-R156 does (not using version() but using variables). The intent of the flag file is to point out that this specific test should only run where preserve_foreign_key is supported. We can't automatically associate a specific test with this specific restriction.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run it


if ignoreVersions, exists := readTestFile(t, testName, "ignore_versions"); exists {
// ignoreVersions is a regexp
re, err := regexp.Compile(ignoreVersions)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
modify parent_id int not null
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
set session foreign_key_checks=0;
drop table if exists onlineddl_test_child;
drop table if exists onlineddl_test;
drop table if exists onlineddl_test_parent;
set session foreign_key_checks=1;
create table onlineddl_test_parent (
id int auto_increment,
primary key(id)
);
create table onlineddl_test (
id int auto_increment,
parent_id int null,
primary key(id),
constraint test_fk foreign key (parent_id) references onlineddl_test_parent (id) on delete no action
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--unsafe-allow-foreign-keys
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We introduce this flag file because vanilla MySQL, used by this test, cannot run foreign-key Online DDLs.

Empty file.
12 changes: 12 additions & 0 deletions go/vt/schemadiff/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,18 @@ func TestInvalidSchema(t *testing.T) {
schema: "create table t10(id bigint primary key); create table t11 (id int primary key, i varchar(100), key ix(i), constraint f10 foreign key (i) references t10(id) on delete restrict)",
expectErr: &ForeignKeyColumnTypeMismatchError{Table: "t11", Constraint: "f10", Column: "i", ReferencedTable: "t10", ReferencedColumn: "id"},
},
{
schema: "create table t10(id int primary key, pid int null, key (pid)); create table t11 (id int primary key, pid int unsigned, key ix(pid), constraint f10 foreign key (pid) references t10(pid))",
expectErr: &ForeignKeyColumnTypeMismatchError{Table: "t11", Constraint: "f10", Column: "pid", ReferencedTable: "t10", ReferencedColumn: "pid"},
},
{
// NULL vs NOT NULL should be fine
schema: "create table t10(id int primary key, pid int null, key (pid)); create table t11 (id int primary key, pid int not null, key ix(pid), constraint f10 foreign key (pid) references t10(pid))",
},
{
// NOT NULL vs NULL should be fine
schema: "create table t10(id int primary key, pid int not null, key (pid)); create table t11 (id int primary key, pid int null, key ix(pid), constraint f10 foreign key (pid) references t10(pid))",
},
Copy link
Contributor Author

@shlomi-noach shlomi-noach Feb 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes here merely validate that as far as schemadiff is concerned, modifying NULL to NOT NULL and vice versa is OK. It's related is essence to the bugfix of this PR, but isn't in any way affected by changes in this PR. These tests would have passed even before this PR. They're here for completeness.

{
// InnoDB allows different string length
schema: "create table t10(id varchar(50) primary key); create table t11 (id int primary key, i varchar(100), key ix(i), constraint f10 foreign key (i) references t10(id) on delete restrict)",
Expand Down
26 changes: 20 additions & 6 deletions go/vt/vttablet/onlineddl/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1315,7 +1315,12 @@
// 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

Check warning on line 1321 in go/vt/vttablet/onlineddl/executor.go

View check run for this annotation

Codecov / codecov/patch

go/vt/vttablet/onlineddl/executor.go#L1321

Added line #L1321 was not covered by tests
}

hashExists := map[string]bool{}
validateWalk := func(node sqlparser.SQLNode) (kontinue bool, err error) {
switch node := node.(type) {
Expand Down Expand Up @@ -1347,8 +1352,10 @@
opt := alterTable.AlterOptions[i]
switch opt := opt.(type) {
case sqlparser.AlgorithmValue:
// we do not pass ALGORITHM. We choose our own ALGORITHM.
continue
if !capableOfInstantDDLXtrabackup {

Check warning on line 1355 in go/vt/vttablet/onlineddl/executor.go

View check run for this annotation

Codecov / codecov/patch

go/vt/vttablet/onlineddl/executor.go#L1355

Added line #L1355 was not covered by tests
// we do not pass ALGORITHM. We choose our own ALGORITHM.
continue

Check warning on line 1357 in go/vt/vttablet/onlineddl/executor.go

View check run for this annotation

Codecov / codecov/patch

go/vt/vttablet/onlineddl/executor.go#L1357

Added line #L1357 was not covered by tests
}
case *sqlparser.AddIndexDefinition:
if opt.IndexDefinition.Info.Type == sqlparser.IndexTypeFullText {
countAddFullTextStatements++
Expand All @@ -1357,7 +1364,10 @@
// 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)

Check warning on line 1370 in go/vt/vttablet/onlineddl/executor.go

View check run for this annotation

Codecov / codecov/patch

go/vt/vttablet/onlineddl/executor.go#L1370

Added line #L1370 was not covered by tests
}
alters = append(alters, extraAlterTable)
continue
Expand All @@ -1367,7 +1377,9 @@
redactedOptions = append(redactedOptions, opt)
}
alterTable.AlterOptions = redactedOptions
alterTable.AlterOptions = append(alterTable.AlterOptions, copyAlgorithm)
if !capableOfInstantDDLXtrabackup {
alterTable.AlterOptions = append(alterTable.AlterOptions, copyAlgorithm)
}
return alters, nil
}

Expand Down Expand Up @@ -1461,7 +1473,9 @@
// 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)

Check warning on line 1478 in go/vt/vttablet/onlineddl/executor.go

View check run for this annotation

Codecov / codecov/patch

go/vt/vttablet/onlineddl/executor.go#L1477-L1478

Added lines #L1477 - L1478 were not covered by tests
if err != nil {
return v, err
}
Expand Down
55 changes: 36 additions & 19 deletions go/vt/vttablet/onlineddl/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"vitess.io/vitess/go/mysql"
"vitess.io/vitess/go/vt/vtenv"
"vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv"

"vitess.io/vitess/go/vt/schema"
"vitess.io/vitess/go/vt/sqlparser"
)

var (
testMySQLVersion = "8.0.34"
)

func TestGetConstraintType(t *testing.T) {
{
typ := GetConstraintType(&sqlparser.CheckConstraintDefinition{})
Expand Down Expand Up @@ -195,72 +200,80 @@ func TestValidateAndEditAlterTableStatement(t *testing.T) {
env: tabletenv.NewEnv(vtenv.NewTestEnv(), nil, "TestValidateAndEditAlterTableStatementTest"),
}
tt := []struct {
alter string
m map[string]string
expect []string
alter string
mySQLVersion string
m map[string]string
expect []string
}{
{
alter: "alter table t add column i int",
expect: []string{"alter table t add column i int, algorithm = copy"},
alter: "alter table t add column i int",
mySQLVersion: "8.0.29",
expect: []string{"alter table t add column i int, algorithm = copy"},
},
{
alter: "alter table t add column i int",
mySQLVersion: "8.0.32",
expect: []string{"alter table t add column i int"},
},
{
alter: "alter table t add column i int, add fulltext key name1_ft (name1)",
expect: []string{"alter table t add column i int, add fulltext key name1_ft (name1), algorithm = copy"},
expect: []string{"alter table t add column i int, add fulltext key name1_ft (name1)"},
},
{
alter: "alter table t add column i int, add fulltext key name1_ft (name1), add fulltext key name2_ft (name2)",
expect: []string{"alter table t add column i int, add fulltext key name1_ft (name1), algorithm = copy", "alter table t add fulltext key name2_ft (name2), algorithm = copy"},
expect: []string{"alter table t add column i int, add fulltext key name1_ft (name1)", "alter table t add fulltext key name2_ft (name2)"},
},
{
alter: "alter table t add fulltext key name0_ft (name0), add column i int, add fulltext key name1_ft (name1), add fulltext key name2_ft (name2)",
expect: []string{"alter table t add fulltext key name0_ft (name0), add column i int, algorithm = copy", "alter table t add fulltext key name1_ft (name1), algorithm = copy", "alter table t add fulltext key name2_ft (name2), algorithm = copy"},
expect: []string{"alter table t add fulltext key name0_ft (name0), add column i int", "alter table t add fulltext key name1_ft (name1)", "alter table t add fulltext key name2_ft (name2)"},
},
{
alter: "alter table t add constraint check (id != 1)",
expect: []string{"alter table t add constraint chk_aulpn7bjeortljhguy86phdn9 check (id != 1), algorithm = copy"},
expect: []string{"alter table t add constraint chk_aulpn7bjeortljhguy86phdn9 check (id != 1)"},
},
{
alter: "alter table t add constraint t_chk_1 check (id != 1)",
expect: []string{"alter table t add constraint chk_1_aulpn7bjeortljhguy86phdn9 check (id != 1), algorithm = copy"},
expect: []string{"alter table t add constraint chk_1_aulpn7bjeortljhguy86phdn9 check (id != 1)"},
},
{
alter: "alter table t add constraint some_check check (id != 1)",
expect: []string{"alter table t add constraint some_check_aulpn7bjeortljhguy86phdn9 check (id != 1), algorithm = copy"},
expect: []string{"alter table t add constraint some_check_aulpn7bjeortljhguy86phdn9 check (id != 1)"},
},
{
alter: "alter table t add constraint some_check check (id != 1), add constraint another_check check (id != 2)",
expect: []string{"alter table t add constraint some_check_aulpn7bjeortljhguy86phdn9 check (id != 1), add constraint another_check_4fa197273p3w96267pzm3gfi3 check (id != 2), algorithm = copy"},
expect: []string{"alter table t add constraint some_check_aulpn7bjeortljhguy86phdn9 check (id != 1), add constraint another_check_4fa197273p3w96267pzm3gfi3 check (id != 2)"},
},
{
alter: "alter table t add foreign key (parent_id) references onlineddl_test_parent (id) on delete no action",
expect: []string{"alter table t add constraint fk_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action, algorithm = copy"},
expect: []string{"alter table t add constraint fk_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action"},
},
{
alter: "alter table t add constraint myfk foreign key (parent_id) references onlineddl_test_parent (id) on delete no action",
expect: []string{"alter table t add constraint myfk_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action, algorithm = copy"},
expect: []string{"alter table t add constraint myfk_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action"},
},
{
// strip out table name
alter: "alter table t add constraint t_ibfk_1 foreign key (parent_id) references onlineddl_test_parent (id) on delete no action",
expect: []string{"alter table t add constraint ibfk_1_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action, algorithm = copy"},
expect: []string{"alter table t add constraint ibfk_1_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action"},
},
{
// stript out table name
alter: "alter table t add constraint t_ibfk_1 foreign key (parent_id) references onlineddl_test_parent (id) on delete no action",
expect: []string{"alter table t add constraint ibfk_1_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action, algorithm = copy"},
expect: []string{"alter table t add constraint ibfk_1_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action"},
},
{
alter: "alter table t add constraint t_ibfk_1 foreign key (parent_id) references onlineddl_test_parent (id) on delete no action, add constraint some_check check (id != 1)",
expect: []string{"alter table t add constraint ibfk_1_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action, add constraint some_check_aulpn7bjeortljhguy86phdn9 check (id != 1), algorithm = copy"},
expect: []string{"alter table t add constraint ibfk_1_6fmhzdlya89128u5j3xapq34i foreign key (parent_id) references onlineddl_test_parent (id) on delete no action, add constraint some_check_aulpn7bjeortljhguy86phdn9 check (id != 1)"},
},
{
alter: "alter table t drop foreign key t_ibfk_1",
m: map[string]string{
"t_ibfk_1": "ibfk_1_aaaaaaaaaaaaaa",
},
expect: []string{"alter table t drop foreign key ibfk_1_aaaaaaaaaaaaaa, algorithm = copy"},
expect: []string{"alter table t drop foreign key ibfk_1_aaaaaaaaaaaaaa"},
},
}

for _, tc := range tt {
t.Run(tc.alter, func(t *testing.T) {
stmt, err := e.env.Environment().Parser().ParseStrictDDL(tc.alter)
Expand All @@ -272,8 +285,12 @@ func TestValidateAndEditAlterTableStatement(t *testing.T) {
for k, v := range tc.m {
m[k] = v
}
if tc.mySQLVersion == "" {
tc.mySQLVersion = testMySQLVersion
}
capableOf := mysql.ServerVersionCapableOf(tc.mySQLVersion)
onlineDDL := &schema.OnlineDDL{UUID: "a5a563da_dc1a_11ec_a416_0a43f95f28a3", Table: "t", Options: "--unsafe-allow-foreign-keys"}
alters, err := e.validateAndEditAlterTableStatement(context.Background(), onlineDDL, alterTable, m)
alters, err := e.validateAndEditAlterTableStatement(context.Background(), capableOf, onlineDDL, alterTable, m)
assert.NoError(t, err)
var altersStrings []string
for _, alter := range alters {
Expand Down
Loading