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

shcemadiff: support INSTANT DDL for changing column visibility #16503

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go/mysql/capabilities/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const (
InstantAddDropColumnFlavorCapability // Adding/dropping column in any position/ordinal.
InstantChangeColumnDefaultFlavorCapability //
InstantExpandEnumCapability //
InstantChangeColumnVisibilityCapability //
MySQLUpgradeInServerFlavorCapability //
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
Expand Down Expand Up @@ -106,6 +107,8 @@ func MySQLVersionHasCapability(serverVersion string, capability FlavorCapability
return atLeast(8, 0, 21)
case FastDropTableFlavorCapability:
return atLeast(8, 0, 23)
case InstantChangeColumnVisibilityCapability:
return atLeast(8, 0, 23)
case InstantAddDropColumnFlavorCapability:
return atLeast(8, 0, 29)
case DynamicRedoLogCapacityFlavorCapability:
Expand Down
126 changes: 73 additions & 53 deletions go/vt/schemadiff/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ func alterOptionCapableOfInstantDDL(alterOption sqlparser.AlterOption, createTab
}
return true, col.Type.Options.Storage
}
colStringStrippedDown := func(col *sqlparser.ColumnDefinition, stripDefault bool, stripEnum bool) string {
colStringStrippedDown := func(col *sqlparser.ColumnDefinition, stripEnum bool) string {
strippedCol := sqlparser.Clone(col)
if stripDefault {
strippedCol.Type.Options.Default = nil
strippedCol.Type.Options.DefaultLiteral = false
}
// strip `default`
strippedCol.Type.Options.Default = nil
strippedCol.Type.Options.DefaultLiteral = false
// strip `visibility`
strippedCol.Type.Options.Invisible = nil
if stripEnum {
strippedCol.Type.EnumValues = nil
}
Expand All @@ -95,15 +96,53 @@ func alterOptionCapableOfInstantDDL(alterOption sqlparser.AlterOption, createTab
}
return true
}
changeModifyColumnCapableOfInstantDDL := func(col *sqlparser.ColumnDefinition, newCol *sqlparser.ColumnDefinition) (bool, error) {
// Check if only diff is change of default.
// We temporarily remove the DEFAULT expression (if any) from both
// table and ALTER statement, and compare the columns: if they're otherwise equal,
// then the only change can be an addition/change/removal of DEFAULT, which
// is instant-table.
tableColDefinition := colStringStrippedDown(col, false)
newColDefinition := colStringStrippedDown(newCol, false)
if tableColDefinition == newColDefinition {
return capableOf(capabilities.InstantChangeColumnDefaultFlavorCapability)
}
// Check if:
// 1. this an ENUM/SET
// 2. and the change is to append values to the end of the list
// 3. and the number of added values does not increase the storage size for the enum/set
// 4. while still not caring about a change in the default value
if len(col.Type.EnumValues) > 0 && len(newCol.Type.EnumValues) > 0 {
// both are enum or set
if !hasPrefix(newCol.Type.EnumValues, col.Type.EnumValues) {
return false, nil
}
// we know the new column definition is identical to, or extends, the old definition.
// Now validate storage:
if strings.EqualFold(col.Type.Type, "enum") {
if len(col.Type.EnumValues) <= 255 && len(newCol.Type.EnumValues) > 255 {
// this increases the SET storage size (1 byte for up to 8 values, 2 bytes beyond)
return false, nil
}
}
if strings.EqualFold(col.Type.Type, "set") {
if (len(col.Type.EnumValues)+7)/8 != (len(newCol.Type.EnumValues)+7)/8 {
// this increases the SET storage size (1 byte for up to 8 values, 2 bytes for 8-15, etc.)
return false, nil
}
}
// Now don't care about change of default:
tableColDefinition := colStringStrippedDown(col, true)
newColDefinition := colStringStrippedDown(newCol, true)
if tableColDefinition == newColDefinition {
return capableOf(capabilities.InstantExpandEnumCapability)
}
}
return false, nil
}

// Up to 8.0.26 we could only ADD COLUMN as last column
switch opt := alterOption.(type) {
case *sqlparser.ChangeColumn:
// We do not support INSTANT for renaming a column (ALTER TABLE ...CHANGE) because:
// 1. We discourage column rename
// 2. We do not produce CHANGE statements in declarative diff
// 3. The success of the operation depends on whether the column is referenced by a foreign key
// in another table. Which is a bit too much to compute here.
return false, nil
case *sqlparser.AddColumns:
if tableHasFulltextIndex {
// not supported if the table has a FULLTEXT index
Expand Down Expand Up @@ -157,49 +196,30 @@ func alterOptionCapableOfInstantDDL(alterOption sqlparser.AlterOption, createTab
return capableOf(capabilities.InstantAddDropVirtualColumnFlavorCapability)
}
return capableOf(capabilities.InstantAddDropColumnFlavorCapability)
case *sqlparser.ChangeColumn:
// We do not support INSTANT for renaming a column (ALTER TABLE ...CHANGE) because:
// 1. We discourage column rename
// 2. We do not produce CHANGE statements in declarative diff
// 3. The success of the operation depends on whether the column is referenced by a foreign key
// in another table. Which is a bit too much to compute here.
if opt.OldColumn.Name.String() != opt.NewColDefinition.Name.String() {
return false, nil
}
if col := findColumn(opt.OldColumn.Name.String()); col != nil {
return changeModifyColumnCapableOfInstantDDL(col, opt.NewColDefinition)
}
return false, nil
case *sqlparser.ModifyColumn:
if col := findColumn(opt.NewColDefinition.Name.String()); col != nil {
// Check if only diff is change of default.
// We temporarily remove the DEFAULT expression (if any) from both
// table and ALTER statement, and compare the columns: if they're otherwise equal,
// then the only change can be an addition/change/removal of DEFAULT, which
// is instant-table.
tableColDefinition := colStringStrippedDown(col, true, false)
newColDefinition := colStringStrippedDown(opt.NewColDefinition, true, false)
if tableColDefinition == newColDefinition {
return capableOf(capabilities.InstantChangeColumnDefaultFlavorCapability)
}
// Check if:
// 1. this an ENUM/SET
// 2. and the change is to append values to the end of the list
// 3. and the number of added values does not increase the storage size for the enum/set
// 4. while still not caring about a change in the default value
if len(col.Type.EnumValues) > 0 && len(opt.NewColDefinition.Type.EnumValues) > 0 {
// both are enum or set
if !hasPrefix(opt.NewColDefinition.Type.EnumValues, col.Type.EnumValues) {
return false, nil
}
// we know the new column definition is identical to, or extends, the old definition.
// Now validate storage:
if strings.EqualFold(col.Type.Type, "enum") {
if len(col.Type.EnumValues) <= 255 && len(opt.NewColDefinition.Type.EnumValues) > 255 {
// this increases the SET storage size (1 byte for up to 8 values, 2 bytes beyond)
return false, nil
}
}
if strings.EqualFold(col.Type.Type, "set") {
if (len(col.Type.EnumValues)+7)/8 != (len(opt.NewColDefinition.Type.EnumValues)+7)/8 {
// this increases the SET storage size (1 byte for up to 8 values, 2 bytes for 8-15, etc.)
return false, nil
}
}
// Now don't care about change of default:
tableColDefinition := colStringStrippedDown(col, true, true)
newColDefinition := colStringStrippedDown(opt.NewColDefinition, true, true)
if tableColDefinition == newColDefinition {
return capableOf(capabilities.InstantExpandEnumCapability)
}
}
return changeModifyColumnCapableOfInstantDDL(col, opt.NewColDefinition)
}
return false, nil
case *sqlparser.AlterColumn:
if opt.DropDefault || opt.DefaultLiteral || opt.DefaultVal != nil {
return capableOf(capabilities.InstantChangeColumnDefaultFlavorCapability)
}
if opt.Invisible != nil {
return capableOf(capabilities.InstantChangeColumnVisibilityCapability)
}
return false, nil
default:
Expand Down
31 changes: 31 additions & 0 deletions go/vt/schemadiff/capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func TestAlterTableCapableOfInstantDDL(t *testing.T) {
capabilities.InstantAddDropVirtualColumnFlavorCapability,
capabilities.InstantAddDropColumnFlavorCapability,
capabilities.InstantChangeColumnDefaultFlavorCapability,
capabilities.InstantChangeColumnVisibilityCapability,
capabilities.InstantExpandEnumCapability:
return true, nil
}
Expand Down Expand Up @@ -272,6 +273,36 @@ func TestAlterTableCapableOfInstantDDL(t *testing.T) {
alter: "alter table t modify column c1 set('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i')",
expectCapableOfInstantDDL: false,
},
{
name: "make a column invisible",
create: "create table t1 (id int, i1 int)",
alter: "alter table t1 modify column i1 int invisible",
expectCapableOfInstantDDL: true,
},
{
name: "make a column visible",
create: "create table t1 (id int, i1 int)",
alter: "alter table t1 change column i1 i1 int visible",
expectCapableOfInstantDDL: true,
},
{
name: "make a column visible with rename",
create: "create table t1 (id int, i1 int)",
alter: "alter table t1 change column i1 i2 int visible",
expectCapableOfInstantDDL: false,
},
{
name: "make a column invisible via SET",
create: "create table t1 (id int, i1 int)",
alter: "alter table t1 alter column i1 set invisible",
expectCapableOfInstantDDL: true,
},
{
name: "drop column default",
create: "create table t1 (id int, i1 int)",
alter: "alter table t1 alter column i1 drop default",
expectCapableOfInstantDDL: true,
},
}
for _, tcase := range tcases {
t.Run(tcase.name, func(t *testing.T) {
Expand Down
Loading