diff --git a/go/base/context.go b/go/base/context.go index 92f052a12..b28513240 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -158,6 +158,8 @@ type MigrationContext struct { PanicOnWarnings bool Checkpoint bool CheckpointIntervalSeconds int64 + AllowChildForeignKeys bool + ForeignKeyRenamePrefix string DropServeSocket bool ServeSocketFile string diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index 2c2bc6665..1e8c631a6 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -81,6 +81,8 @@ func main() { flag.BoolVar(&migrationContext.SkipRenamedColumns, "skip-renamed-columns", false, "in case your `ALTER` statement renames columns, gh-ost will note that and offer its interpretation of the rename. By default gh-ost does not proceed to execute. This flag tells gh-ost to skip the renamed columns, i.e. to treat what gh-ost thinks are renamed columns as unrelated columns. NOTE: you may lose column data") flag.BoolVar(&migrationContext.IsTungsten, "tungsten", false, "explicitly let gh-ost know that you are running on a tungsten-replication based topology (you are likely to also provide --assume-master-host)") flag.BoolVar(&migrationContext.DiscardForeignKeys, "discard-foreign-keys", false, "DANGER! This flag will migrate a table that has foreign keys and will NOT create foreign keys on the ghost table, thus your altered table will have NO foreign keys. This is useful for intentional dropping of foreign keys") + flag.BoolVar(&migrationContext.AllowChildForeignKeys, "allow-child-foreign-keys", false, "Allow gh-ost to create foreign keys on the ghost table when the child table has foreign keys") + flag.StringVar(&migrationContext.ForeignKeyRenamePrefix, "foreign-key-rename-prefix", "_", "Rename foreign keys in the ghost table by adding this prefix") flag.BoolVar(&migrationContext.SkipForeignKeyChecks, "skip-foreign-key-checks", false, "set to 'true' when you know for certain there are no foreign keys on your table, and wish to skip the time it takes for gh-ost to verify that") flag.BoolVar(&migrationContext.SkipStrictMode, "skip-strict-mode", false, "explicitly tell gh-ost binlog applier not to enforce strict sql mode") flag.BoolVar(&migrationContext.AllowZeroInDate, "allow-zero-in-date", false, "explicitly tell gh-ost binlog applier to ignore NO_ZERO_IN_DATE,NO_ZERO_DATE in sql_mode") @@ -235,6 +237,9 @@ func main() { if migrationContext.DiscardForeignKeys { log.Warning("--discard-foreign-keys was provided with --revert, it will be ignored") } + if migrationContext.AllowChildForeignKeys { + log.Warning("--allow-child-foreign-keys was provided with --revert, it will be ignored") + } } if migrationContext.DatabaseName == "" { diff --git a/go/logic/applier.go b/go/logic/applier.go index 34ea79afb..f83269c76 100644 --- a/go/logic/applier.go +++ b/go/logic/applier.go @@ -335,6 +335,67 @@ func (this *Applier) CreateGhostTable() error { return err } +func (this *Applier) getTableForeignKeyDefinitions(tableName string) (fkNames []string, fkBodies []string, err error) { + query := fmt.Sprintf("show create table %s.%s", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(tableName)) + var dummy string + var createStatement string + if err := this.db.QueryRow(query).Scan(&dummy, &createStatement); err != nil { + return nil, nil, err + } + + lines := strings.Split(createStatement, "\n") + re := regexp.MustCompile("CONSTRAINT\\s+['\"`]?([^'\"`]+)['\"`]?\\s+FOREIGN\\s+KEY\\s+(.*)") + for _, line := range lines { + line = strings.TrimSpace(line) + line = strings.TrimRight(line, ",") + if matches := re.FindStringSubmatch(line); len(matches) == 3 { + fkNames = append(fkNames, matches[1]) + fkBodies = append(fkBodies, "FOREIGN KEY "+matches[2]) + } + } + return fkNames, fkBodies, nil +} + +func (this *Applier) ApplyForeignKeys() error { + this.migrationContext.Log.Infof("Applying foreign keys with prefix '%s'", this.migrationContext.ForeignKeyRenamePrefix) + + ghostFKNames, _, err := this.getTableForeignKeyDefinitions(this.migrationContext.GetGhostTableName()) + if err != nil { + return err + } + for _, name := range ghostFKNames { + query := fmt.Sprintf("alter /* gh-ost */ table %s.%s drop foreign key %s", + sql.EscapeName(this.migrationContext.DatabaseName), + sql.EscapeName(this.migrationContext.GetGhostTableName()), + sql.EscapeName(name)) + this.migrationContext.Log.Infof("Dropping foreign key %s from ghost table", name) + if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil { + return err + } + } + + origFKNames, origFKBodies, err := this.getTableForeignKeyDefinitions(this.migrationContext.OriginalTableName) + if err != nil { + return err + } + for i, name := range origFKNames { + newName := fmt.Sprintf("%s%s", this.migrationContext.ForeignKeyRenamePrefix, name) + if len(newName) > 64 { + return fmt.Errorf("Generate foreign key name '%s' exceeds 64 characters", newName) + } + query := fmt.Sprintf("alter /* gh-ost */ table %s.%s add constraint %s %s", + sql.EscapeName(this.migrationContext.DatabaseName), + sql.EscapeName(this.migrationContext.GetGhostTableName()), + sql.EscapeName(newName), + origFKBodies[i]) + this.migrationContext.Log.Infof("Adding foreign key %s to ghost table", newName) + if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil { + return err + } + } + return nil +} + // AlterGhost applies `alter` statement on ghost table func (this *Applier) AlterGhost() error { query := fmt.Sprintf(`alter /* gh-ost */ table %s.%s %s`, diff --git a/go/logic/inspect.go b/go/logic/inspect.go index 044360153..12579f81a 100644 --- a/go/logic/inspect.go +++ b/go/logic/inspect.go @@ -89,7 +89,7 @@ func (this *Inspector) ValidateOriginalTable() (err error) { if err := this.validateTable(); err != nil { return err } - if err := this.validateTableForeignKeys(this.migrationContext.DiscardForeignKeys); err != nil { + if err := this.validateTableForeignKeys(this.migrationContext.DiscardForeignKeys || this.migrationContext.AllowChildForeignKeys); err != nil { return err } if err := this.validateTableTriggers(); err != nil { @@ -539,7 +539,7 @@ func (this *Inspector) validateTableForeignKeys(allowChildForeignKeys bool) erro } if numChildForeignKeys > 0 { if allowChildForeignKeys { - this.migrationContext.Log.Debugf("Foreign keys found and will be dropped, as per given --discard-foreign-keys flag") + this.migrationContext.Log.Debugf("Foreign keys are found and will be removed using the --discard-foreign-keys flag or copied using the --allow-child-foreign-keys flag") return nil } return this.migrationContext.Log.Errorf("Found %d child-side foreign keys on %s.%s. Child-side foreign keys are not supported. Bailing out", numChildForeignKeys, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName)) diff --git a/go/logic/migrator.go b/go/logic/migrator.go index 507a503fe..5ae5e827b 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -1349,6 +1349,10 @@ func (this *Migrator) initiateApplier() error { this.migrationContext.Log.Errorf("Unable to create ghost table, see further error details. Perhaps a previous migration failed without dropping the table? Bailing out") return err } + if err := this.applier.ApplyForeignKeys(); err != nil { + this.migrationContext.Log.Errorf("Unable to apply foreign keys, see further error details. Bailing out") + return err + } if err := this.applier.AlterGhost(); err != nil { this.migrationContext.Log.Errorf("Unable to ALTER ghost table, see further error details. Bailing out") return err