Skip to content

Disable FK checks during Migrator::dropTables#1074

Closed
dereuromark wants to merge 1 commit into5.xfrom
fix/migrator-disable-constraints-on-drop
Closed

Disable FK checks during Migrator::dropTables#1074
dereuromark wants to merge 1 commit into5.xfrom
fix/migrator-disable-constraints-on-drop

Conversation

@dereuromark
Copy link
Copy Markdown
Member

Problem

On MySQL, Migrator::dropTables() (called during test-suite rebuilds when shouldDropTables() returns true) can fail with error 3730:

SQLSTATE[HY000]: General error: 3730 Cannot drop table 'X' referenced by
a foreign key constraint 'fk_name' on table 'Y'.
Query: DROP TABLE \`X\`

ConnectionHelper::dropTables() drops each table's outgoing FKs first and then drops the tables. With complex schemas (many cross-referencing FKs across plugins, unified-mode ledgers, and migrations that add/drop FKs in non-trivial patterns) the drop pass can still hit an ordering MySQL refuses.

This showed up in a project with ~80 migrations and ~15 FK-linked tables: after clearing the ledger to force a rebuild, Migrator::dropTables bombed on DROP TABLE bank_transactions because a FK on rent_payments pointing at it hadn't been fully resolved by the drop-constraints pass.

Projects work around this by dropping tables by hand inside $connection->disableConstraints(...) in their tests/bootstrap.php — exactly what this PR moves upstream.

Fix

Wrap $this->helper->dropTables(...) in $connection->disableConstraints(...) so MySQL doesn't enforce FK checks during the bulk drop. Drop order becomes irrelevant.

No behaviour change on SQLite or Postgres — they handle the drop order without help.

Test

Adds a MySQL-only regression test (testDropTablesHandlesCrossTableForeignKeys) that creates three tables with cascading FK references and calls dropTables directly. Skipped on SQLite/Postgres because they don't block on this pattern.

Notes

Tagged as draft while folks sanity-check the approach and the minimal test. Happy to iterate on naming / add more coverage if wanted.

The test migrator calls ConnectionHelper::dropTables() which drops each
table's outgoing FK constraints and then the tables themselves. With
complex schemas that have many cross-referencing FKs, this can still
fail on MySQL with error 3730 ("Cannot drop table X referenced by FK on
Y") because the drop order doesn't always satisfy MySQL's checks.

Wrap the drop call in $connection->disableConstraints() so FK checks are
suspended for the duration of the bulk drop, matching what projects
routinely do by hand in their bootstrap. No behaviour change on SQLite or
Postgres; MySQL gets robust regardless of FK topology.

Adds a MySQL-only regression test that sets up three cross-referenced
tables and calls dropTables directly.
@dereuromark dereuromark requested a review from markstory April 19, 2026 12:25
@LordSimal
Copy link
Copy Markdown
Contributor

I usually just drop the whole database as i dont need to keep other non related tables.

@dereuromark
Copy link
Copy Markdown
Member Author

Thats usually much slower I imagine.
But yes, that project was not using Transaction strategy yet. With it, this problem would be void anyway.

Current workaround is quite ugly:

$migrationSets = [
    [],
    ['plugin' => 'Queue'],
    ['plugin' => 'DatabaseLog'],
    ['plugin' => 'Tools'],
];

// The upstream test migrator still only treats legacy `phinxlog` tables as
// migration metadata when it decides to rebuild. In unified-mode setups like
// this app, partial runs can leave real tables and the `cake_migrations` ledger
// out of sync in either direction. Reusing that state makes bootstrap flaky.
//
// Reset the shared `db_test` schema unconditionally and replay migrations from
// scratch. This is slower, but deterministic, and the repo already documents
// that PHPUnit here must run serially through ddev.
$connection = ConnectionManager::get('test');
$tables = $connection->getSchemaCollection()->listTables();
$dropTables = array_values(array_filter($tables, static function (string $table): bool {
    return $table !== 'cake_migrations' && !str_contains($table, 'phinxlog');
}));

if ($dropTables !== []) {
    $connection->disableConstraints(function ($connection) use ($dropTables): void {
        foreach ($dropTables as $table) {
            $quotedTable = '`' . str_replace('`', '``', $table) . '`';
            $connection->execute("DROP TABLE IF EXISTS {$quotedTable}");
        }
    });
}
if (in_array('cake_migrations', $tables, true)) {
    ConnectionHelper::truncateTables('test', ['cake_migrations']);
}

(new Migrator())->runMany($migrationSets);

@dereuromark
Copy link
Copy Markdown
Member Author

Switching to transaction :)

@dereuromark dereuromark deleted the fix/migrator-disable-constraints-on-drop branch April 19, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants