Skip to content

Commit 8d5b099

Browse files
committed
Move FK constraint handling to adapter layer
- Add disableForeignKeyConstraints/enableForeignKeyConstraints to AdapterInterface - Implement FK methods in MysqlAdapter (SET FOREIGN_KEY_CHECKS) - Implement FK methods in SqliteAdapter (PRAGMA foreign_keys) - Implement FK methods in SqlserverAdapter (drop all FK constraints) - PostgresAdapter uses no-op methods with CASCADE in dropTable - Add FK methods to AdapterWrapper for delegation - Refactor ResetCommand to use adapter methods instead of dialect-specific code
1 parent ceaf927 commit 8d5b099

7 files changed

Lines changed: 143 additions & 89 deletions

File tree

src/Command/ResetCommand.php

Lines changed: 22 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@
1818
use Cake\Console\ConsoleIo;
1919
use Cake\Console\ConsoleOptionParser;
2020
use Cake\Database\Connection;
21-
use Cake\Database\Driver\Mysql;
22-
use Cake\Database\Driver\Postgres;
23-
use Cake\Database\Driver\Sqlite;
24-
use Cake\Database\Driver\Sqlserver;
2521
use Cake\Datasource\ConnectionManager;
2622
use Cake\Event\EventDispatcherTrait;
2723
use Migrations\Config\ConfigInterface;
24+
use Migrations\Db\Adapter\AdapterInterface;
25+
use Migrations\Db\Adapter\DirectActionInterface;
2826
use Migrations\Migration\ManagerFactory;
27+
use RuntimeException;
2928
use Throwable;
3029

3130
/**
@@ -149,7 +148,15 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
149148
// Drop tables
150149
$io->out('');
151150
if (!$dryRun) {
152-
$this->dropTables($connection, $tablesToDrop, $io);
151+
$factory = new ManagerFactory([
152+
'plugin' => $args->getOption('plugin'),
153+
'source' => $args->getOption('source'),
154+
'connection' => $args->getOption('connection'),
155+
]);
156+
$manager = $factory->createManager($io);
157+
$adapter = $manager->getEnvironment()->getAdapter();
158+
159+
$this->dropTables($adapter, $tablesToDrop, $io);
153160
} else {
154161
$io->info('DRY-RUN: Would drop ' . count($tablesToDrop) . ' table(s).');
155162
}
@@ -182,104 +189,31 @@ protected function getTablesToDrop(Connection $connection): array
182189
/**
183190
* Drop tables with foreign key handling.
184191
*
185-
* @param \Cake\Database\Connection $connection Database connection
192+
* @param \Migrations\Db\Adapter\AdapterInterface $adapter The adapter
186193
* @param array<string> $tables Tables to drop
187194
* @param \Cake\Console\ConsoleIo $io Console IO
188195
* @return void
189196
*/
190-
protected function dropTables(Connection $connection, array $tables, ConsoleIo $io): void
197+
protected function dropTables(AdapterInterface $adapter, array $tables, ConsoleIo $io): void
191198
{
192-
$driver = $connection->getDriver();
199+
if (!$adapter instanceof DirectActionInterface) {
200+
throw new RuntimeException('The adapter must implement DirectActionInterface');
201+
}
193202

194-
// For PostgreSQL and SQL Server, we need to drop foreign keys first
195-
// or use CASCADE in the drop statement
196-
if ($driver instanceof Postgres) {
197-
foreach ($tables as $table) {
198-
$quotedTable = $driver->quoteIdentifier($table);
199-
$io->verbose("Dropping table: {$table}");
200-
$connection->execute("DROP TABLE IF EXISTS {$quotedTable} CASCADE");
201-
}
202-
} elseif ($driver instanceof Sqlserver) {
203-
// Drop all foreign key constraints first
204-
$this->dropForeignKeyConstraints($connection, $tables, $io);
203+
$adapter->disableForeignKeyConstraints();
205204

206-
// Then drop tables
205+
try {
207206
foreach ($tables as $table) {
208-
$quotedTable = $driver->quoteIdentifier($table);
209207
$io->verbose("Dropping table: {$table}");
210-
$connection->execute("DROP TABLE IF EXISTS {$quotedTable}");
211-
}
212-
} else {
213-
// MySQL and SQLite support disabling foreign key checks
214-
$this->setForeignKeyChecks($connection, false);
215-
216-
try {
217-
foreach ($tables as $table) {
218-
$quotedTable = $driver->quoteIdentifier($table);
219-
$io->verbose("Dropping table: {$table}");
220-
$connection->execute("DROP TABLE IF EXISTS {$quotedTable}");
221-
}
222-
} finally {
223-
$this->setForeignKeyChecks($connection, true);
208+
$adapter->dropTable($table);
224209
}
210+
} finally {
211+
$adapter->enableForeignKeyConstraints();
225212
}
226213

227214
$io->success('Dropped ' . count($tables) . ' table(s).');
228215
}
229216

230-
/**
231-
* Drop all foreign key constraints from the given tables.
232-
*
233-
* @param \Cake\Database\Connection $connection Database connection
234-
* @param array<string> $tables Tables to process
235-
* @param \Cake\Console\ConsoleIo $io Console IO
236-
* @return void
237-
*/
238-
protected function dropForeignKeyConstraints(Connection $connection, array $tables, ConsoleIo $io): void
239-
{
240-
$driver = $connection->getDriver();
241-
242-
if (!$driver instanceof Sqlserver) {
243-
return;
244-
}
245-
246-
// Query to find all foreign key constraints on the specified tables
247-
$tableList = implode("','", array_map(fn($t) => addslashes($t), $tables));
248-
249-
$sql = "SELECT
250-
fk.name AS constraint_name,
251-
OBJECT_NAME(fk.parent_object_id) AS table_name
252-
FROM sys.foreign_keys fk
253-
WHERE OBJECT_NAME(fk.parent_object_id) IN ('{$tableList}')";
254-
255-
$result = $connection->execute($sql)->fetchAll('assoc');
256-
257-
foreach ($result as $row) {
258-
$constraintName = $driver->quoteIdentifier($row['constraint_name']);
259-
$tableName = $driver->quoteIdentifier($row['table_name']);
260-
$io->verbose("Dropping foreign key: {$row['constraint_name']} on {$row['table_name']}");
261-
$connection->execute("ALTER TABLE {$tableName} DROP CONSTRAINT {$constraintName}");
262-
}
263-
}
264-
265-
/**
266-
* Enable or disable foreign key checks.
267-
*
268-
* @param \Cake\Database\Connection $connection Database connection
269-
* @param bool $enable Whether to enable or disable
270-
* @return void
271-
*/
272-
protected function setForeignKeyChecks(Connection $connection, bool $enable): void
273-
{
274-
$driver = $connection->getDriver();
275-
276-
if ($driver instanceof Mysql) {
277-
$connection->execute('SET FOREIGN_KEY_CHECKS = ' . ($enable ? '1' : '0'));
278-
} elseif ($driver instanceof Sqlite) {
279-
$connection->execute('PRAGMA foreign_keys = ' . ($enable ? 'ON' : 'OFF'));
280-
}
281-
}
282-
283217
/**
284218
* Run migrations and dispatch afterReset event.
285219
*

src/Db/Adapter/AdapterInterface.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,26 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
553553
*/
554554
public function truncateTable(string $tableName): void;
555555

556+
/**
557+
* Disable foreign key constraint checking.
558+
*
559+
* This is useful when dropping tables or performing bulk operations
560+
* that would otherwise fail due to foreign key constraints.
561+
*
562+
* @return void
563+
*/
564+
public function disableForeignKeyConstraints(): void;
565+
566+
/**
567+
* Enable foreign key constraint checking.
568+
*
569+
* This should be called after disableForeignKeyConstraints() to
570+
* restore normal constraint checking behavior.
571+
*
572+
* @return void
573+
*/
574+
public function enableForeignKeyConstraints(): void;
575+
556576
/**
557577
* Returns table columns
558578
*

src/Db/Adapter/AdapterWrapper.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,4 +590,20 @@ public function getSchemaTableName(): string
590590
{
591591
return $this->getAdapter()->getSchemaTableName();
592592
}
593+
594+
/**
595+
* @inheritDoc
596+
*/
597+
public function disableForeignKeyConstraints(): void
598+
{
599+
$this->getAdapter()->disableForeignKeyConstraints();
600+
}
601+
602+
/**
603+
* @inheritDoc
604+
*/
605+
public function enableForeignKeyConstraints(): void
606+
{
607+
$this->getAdapter()->enableForeignKeyConstraints();
608+
}
593609
}

src/Db/Adapter/MysqlAdapter.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,22 @@ public function truncateTable(string $tableName): void
560560
$this->execute($sql);
561561
}
562562

563+
/**
564+
* @inheritDoc
565+
*/
566+
public function disableForeignKeyConstraints(): void
567+
{
568+
$this->execute('SET FOREIGN_KEY_CHECKS = 0');
569+
}
570+
571+
/**
572+
* @inheritDoc
573+
*/
574+
public function enableForeignKeyConstraints(): void
575+
{
576+
$this->execute('SET FOREIGN_KEY_CHECKS = 1');
577+
}
578+
563579
/**
564580
* Convert from cakephp/database conventions to migrations\column
565581
*

src/Db/Adapter/PostgresAdapter.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ protected function getRenameTableInstructions(string $tableName, string $newTabl
333333
protected function getDropTableInstructions(string $tableName): AlterInstructions
334334
{
335335
$this->removeCreatedTable($tableName);
336-
$sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
336+
$sql = sprintf('DROP TABLE %s CASCADE', $this->quoteTableName($tableName));
337337

338338
return new AlterInstructions([], [$sql]);
339339
}
@@ -351,6 +351,24 @@ public function truncateTable(string $tableName): void
351351
$this->execute($sql);
352352
}
353353

354+
/**
355+
* @inheritDoc
356+
*/
357+
public function disableForeignKeyConstraints(): void
358+
{
359+
// PostgreSQL uses CASCADE on DROP TABLE instead of disabling FK checks.
360+
// This method is a no-op for PostgreSQL since dropTable already uses CASCADE.
361+
}
362+
363+
/**
364+
* @inheritDoc
365+
*/
366+
public function enableForeignKeyConstraints(): void
367+
{
368+
// PostgreSQL uses CASCADE on DROP TABLE instead of disabling FK checks.
369+
// This method is a no-op for PostgreSQL.
370+
}
371+
354372
/**
355373
* @inheritDoc
356374
*/

src/Db/Adapter/SqliteAdapter.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,22 @@ public function truncateTable(string $tableName): void
408408
}
409409
}
410410

411+
/**
412+
* @inheritDoc
413+
*/
414+
public function disableForeignKeyConstraints(): void
415+
{
416+
$this->execute('PRAGMA foreign_keys = OFF');
417+
}
418+
419+
/**
420+
* @inheritDoc
421+
*/
422+
public function enableForeignKeyConstraints(): void
423+
{
424+
$this->execute('PRAGMA foreign_keys = ON');
425+
}
426+
411427
/**
412428
* Parses a default-value expression to yield either a Literal representing
413429
* a string value, a string representing an expression, or some other scalar

src/Db/Adapter/SqlserverAdapter.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,40 @@ public function truncateTable(string $tableName): void
280280
$this->execute($sql);
281281
}
282282

283+
/**
284+
* @inheritDoc
285+
*/
286+
public function disableForeignKeyConstraints(): void
287+
{
288+
// SQL Server doesn't support disabling FK checks globally.
289+
// We drop all foreign key constraints instead.
290+
$sql = "SELECT
291+
fk.name AS constraint_name,
292+
SCHEMA_NAME(t.schema_id) AS schema_name,
293+
t.name AS table_name
294+
FROM sys.foreign_keys fk
295+
INNER JOIN sys.tables t ON fk.parent_object_id = t.object_id
296+
WHERE SCHEMA_NAME(t.schema_id) = ?";
297+
298+
$rows = $this->query($sql, [$this->schema])->fetchAll('assoc');
299+
300+
foreach ($rows as $row) {
301+
$constraintName = $this->quoteColumnName($row['constraint_name']);
302+
$tableName = $this->quoteTableName($row['table_name']);
303+
$this->execute("ALTER TABLE {$tableName} DROP CONSTRAINT {$constraintName}");
304+
}
305+
}
306+
307+
/**
308+
* @inheritDoc
309+
*/
310+
public function enableForeignKeyConstraints(): void
311+
{
312+
// SQL Server FK constraints were dropped, not disabled.
313+
// They would need to be recreated, but after a reset/drop operation
314+
// the tables will be recreated with their constraints by migrations.
315+
}
316+
283317
/**
284318
* @param string $tableName Table name
285319
* @param ?string $columnName Column name

0 commit comments

Comments
 (0)