Skip to content

Commit

Permalink
Delete orphaned FK rows via GC
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonkelly committed Dec 5, 2024
1 parent d92f873 commit 80d5c39
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
- The Queue Manager utility now shows jobs’ class names. ([#16228](https://github.com/craftcms/cms/pull/16228))

## System
- Database rows with foreign keys referencing nonexistent rows are now deleted via garbage collection.
- Updated Twig to 3.15. ([#16207](https://github.com/craftcms/cms/discussions/16207))
53 changes: 53 additions & 0 deletions src/services/Gc.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use craft\db\Connection;
use craft\db\Query;
use craft\db\Table;
use craft\db\TableSchema;
use craft\elements\Asset;
use craft\elements\Category;
use craft\elements\Entry;
Expand Down Expand Up @@ -151,6 +152,7 @@ public function run(bool $force = false): void
$this->_deleteOrphanedSearchIndexes();
$this->_deleteOrphanedRelations();
$this->_deleteOrphanedStructureElements();
$this->_deleteOrphanedFkRows();

$this->_hardDeleteStructures();

Expand Down Expand Up @@ -525,6 +527,57 @@ private function _deleteOrphanedStructureElements(): void
$this->_stdout("done\n", Console::FG_GREEN);
}

private function _deleteOrphanedFkRows(): void
{
$this->_stdout(' > deleting orphaned foreign key rows ... ');

// Disable FK checks
$qb = $this->db->getSchema()->getQueryBuilder();
$this->db->createCommand($qb->checkIntegrity(false))->execute();

$isMysql = $this->db->getIsMysql();
foreach ($this->db->getSchema()->getTableSchemas() as $table) {
/** @var TableSchema $table */
$extendedFkInfo = $table->getExtendedForeignKeys();
$counter = 0;
foreach ($table->foreignKeys as $fk) {
if ($extendedFkInfo[$counter]['deleteType'] === 'CASCADE') {
$fk = array_merge($fk);
$refTable = array_shift($fk);

foreach ($fk as $fkColumn => $pkColumn) {
if ($isMysql) {
$sql = <<<SQL
DELETE t.* FROM $table->name t
LEFT JOIN $refTable t2 ON t2.$pkColumn = t.$fkColumn
WHERE t.$fkColumn IS NOT NULL
AND t2.$pkColumn IS NULL
SQL;
} else {
$sql = <<<SQL
DELETE FROM $table->name t
WHERE t2.$pkColumn IS NULL
AND NOT EXISTS (
SELECT * FROM $refTable
WHERE "$pkColumn" = t."$fkColumn"
)
SQL;
}

$this->db->createCommand($sql)->execute();
}
}

$counter++;
}
}

// Re-enable FK checks
$this->db->createCommand($qb->checkIntegrity())->execute();

$this->_stdout("done\n", Console::FG_GREEN);
}

/**
* Deletes field layouts that are no longer used.
*
Expand Down

1 comment on commit 80d5c39

@boboldehampsink
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brandonkelly this is breaking on Heroku again, see #15063 (comment) where you fixed this before

Please sign in to comment.