-
Notifications
You must be signed in to change notification settings - Fork 71
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
[POC] Migrating data with foreign key constraints #288
base: main
Are you sure you want to change the base?
Changes from all commits
28191c0
91b2cc9
7a36ba7
b4b44e2
7f794cb
5a65912
4ea968d
988e6b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
.. _copydbforeignkey: | ||
|
||
=================================================================================== | ||
Running ``ghostferry-copydb`` in production for tables with Foreign Key Constraints | ||
=================================================================================== | ||
|
||
Migrating tables with foreign keys constraints is an experimental feature in copydb and should be used at your own risk in production. | ||
|
||
|
||
Prerequisites | ||
------------- | ||
|
||
Before migrating tables with foreign key constraints via copydb there are a couple of things to take care of | ||
|
||
- Ghostferry needs to be ran with ``SkipForeignKeyConstraintsCheck = true``, which will disable ghostferry to check foreign key | ||
constraints during initialization. | ||
|
||
- Source DB should be ``read_only``. | ||
|
||
- Need to disable foreign key constraint checks on target DB by passing the following config to target db | ||
|
||
.. code-block:: json | ||
|
||
"Params": { | ||
"foreign_key_checks": "0" | ||
} | ||
|
||
- Even though foreign key constraint checks are disabled on target db, table and db creation must happen in a specific order (eg parent should be created | ||
before child table). This creation order can be specified by passing ``TablesToBeCreatedFirst`` in the config, or else the table creation order will be | ||
figured out by copydb. | ||
|
||
Limitations | ||
------------- | ||
|
||
- Currently migrating tables with foreign key constraints is only possible if the source database is in read_only mode. Since tables with foreign key constraints can have referential actions for a foreign key such as ``ON DELETE CASCADE``, ``ON UPDATE CASCADE``. Cascading deletes and updates in child tablees caused by foreign key constraints don't show up in binlogs because these referential actions are dealt internally by InnoDB. This issue descibes briefly why the source database should be read_only during the migration - https://github.com/Shopify/ghostferry/issues/289. | ||
|
||
- ``Interrupt-Resume`` functionality can be used as long as source database is read_only also during the interrupt period | ||
|
||
- ``Inline Verifier`` can be used as long as it is ensured that the source database is read_only (even during the interrupt period) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -342,6 +342,75 @@ func (c TableSchemaCache) GetTableListWithPriority(priorityList []string) (prior | |||||
return | ||||||
} | ||||||
|
||||||
func (c TableSchemaCache) GetTableCreationOrder(db *sql.DB) ([]string, error) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe change the name so it doesn't seem like a generic order that people should follow?
Suggested change
|
||||||
visitedTables := map[string]bool{} | ||||||
tableMap := map[string][]string{} | ||||||
tableOrder := []string{} | ||||||
|
||||||
for table := range c { | ||||||
t := strings.Split(table, ".") | ||||||
referencedTables, err := getForeignKeyTablesOfTable(db, t[0], t[1]) | ||||||
if err != nil { | ||||||
logrus.WithField("table", table).Error("cannot fetch foreign keys") | ||||||
} | ||||||
tableMap[table] = referencedTables | ||||||
} | ||||||
|
||||||
for table := range c { | ||||||
if _, found := visitedTables[table]; !found { | ||||||
visitedTables, tableOrder = getTableCreationOrderUtil(visitedTables, tableOrder, tableMap, table) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you document how this algorithm is supposed to work (in either comments attached to a function here, or via a separate document)? It looks feasible to trace through myself, but it's better for a non-trivial algorithm to have its intent documented, so reviewers can focus on if the implementation is right or wrong, not reverse engineering what you're trying to do. |
||||||
} | ||||||
} | ||||||
|
||||||
return tableOrder, nil | ||||||
} | ||||||
|
||||||
func getTableCreationOrderUtil(visitedTables map[string]bool, tableOrder []string, tableMap map[string][]string, currTable string) (map[string]bool, []string) { | ||||||
visitedTables[currTable] = true | ||||||
for _, referencedTable := range tableMap[currTable] { | ||||||
if _, found := visitedTables[referencedTable]; !found { | ||||||
visitedTables, tableOrder = getTableCreationOrderUtil(visitedTables, tableOrder, tableMap, referencedTable) | ||||||
} | ||||||
} | ||||||
tableOrder = append(tableOrder, currTable) | ||||||
return visitedTables, tableOrder | ||||||
} | ||||||
|
||||||
func getForeignKeyTablesOfTable(db *sql.DB, schema string, table string) ([]string, error) { | ||||||
fkTables := []string{} | ||||||
visitedTables := map[string]bool{} | ||||||
|
||||||
query := fmt.Sprintf(` | ||||||
SELECT CONCAT(REFERENCED_TABLE_SCHEMA, '.', REFERENCED_TABLE_NAME) | ||||||
FROM information_schema.key_column_usage | ||||||
WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' AND REFERENCED_TABLE_NAME IS NOT NULL | ||||||
`, | ||||||
schema, | ||||||
table, | ||||||
) | ||||||
|
||||||
rows, err := db.Query(query) | ||||||
if err != nil { | ||||||
return fkTables, err | ||||||
} | ||||||
|
||||||
var tableName string | ||||||
for rows.Next() { | ||||||
err = rows.Scan(&tableName) | ||||||
if err != nil { | ||||||
return fkTables, err | ||||||
} | ||||||
|
||||||
if _, found := visitedTables[tableName]; !found && tableName != table { | ||||||
fkTables = append(fkTables, tableName) | ||||||
visitedTables[tableName] = true | ||||||
} | ||||||
|
||||||
} | ||||||
|
||||||
return fkTables, nil | ||||||
} | ||||||
|
||||||
func showDatabases(c *sql.DB) ([]string, error) { | ||||||
rows, err := c.Query("show databases") | ||||||
if err != nil { | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.