From 641ae591937307cc2ead1c4ce663a6d7a4d49040 Mon Sep 17 00:00:00 2001 From: Ere Maijala <ere.maijala@helsinki.fi> Date: Thu, 1 Feb 2018 18:28:34 +0200 Subject: [PATCH] Add support for modification of foreign key constraints to FixDatabase action. (#1108) --- .../VuFind/Controller/Plugin/DbUpgrade.php | 169 +++++++++++++++++- .../VuFind/Controller/UpgradeController.php | 19 ++ 2 files changed, 186 insertions(+), 2 deletions(-) diff --git a/module/VuFind/src/VuFind/Controller/Plugin/DbUpgrade.php b/module/VuFind/src/VuFind/Controller/Plugin/DbUpgrade.php index a0b3e109d0e..6b33db2c954 100644 --- a/module/VuFind/src/VuFind/Controller/Plugin/DbUpgrade.php +++ b/module/VuFind/src/VuFind/Controller/Plugin/DbUpgrade.php @@ -22,6 +22,7 @@ * @category VuFind * @package Controller_Plugins * @author Demian Katz <demian.katz@villanova.edu> + * @author Ere Maijala <ere.maijala@helsinki.fi> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Page */ @@ -37,6 +38,7 @@ use Zend\Mvc\Controller\Plugin\AbstractPlugin; * @category VuFind * @package Controller_Plugins * @author Demian Katz <demian.katz@villanova.edu> + * @author Ere Maijala <ere.maijala@helsinki.fi> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Page */ @@ -400,7 +402,11 @@ class DbUpgrade extends AbstractPlugin $constraints = isset($info[$table]) ? $info[$table]->getConstraints() : []; $retVal = []; foreach ($constraints as $current) { - $fields = ['fields' => $current->getColumns()]; + $fields = [ + 'fields' => $current->getColumns(), + 'deleteRule' => $current->getDeleteRule(), + 'updateRule' => $current->getUpdateRule() + ]; switch ($current->getType()) { case 'FOREIGN KEY': $retVal['foreign'][$current->getName()] = $fields; @@ -621,6 +627,132 @@ class DbUpgrade extends AbstractPlugin return $missing; } + /** + * Compare expected vs. actual constraint actions and return an array of SQL + * clauses required to create the modified constraints. + * + * @param array $expected Expected constraints (based on mysql.sql) + * @param array $actual Actual constraints (pulled from database metadata) + * + * @return array + */ + protected function compareConstraintActions($expected, $actual) + { + $modified = []; + foreach ($expected as $type => $constraints) { + foreach ($constraints as $name => $constraint) { + if (!isset($actual[$type][$name])) { + throw new \Exception( + "Could not find constraint '$name' in actual constraints" + ); + } + $actualConstr = $actual[$type][$name]; + if ($constraint['deleteRule'] !== $actualConstr['deleteRule'] + || $constraint['updateRule'] !== $actualConstr['updateRule'] + ) { + $modified[$name] = $constraint; + } + } + } + return $modified; + } + + /** + * Support method for getModifiedConstraints() -- check if the current constraint + * is in the missing constraint list so we can avoid modifying something that + * does not exist. + * + * @param string $constraint Column to check + * @param array $missing Missing constraint list for constraint's table. + * + * @return bool + */ + public function constraintIsMissing($constraint, $missing) + { + foreach ($missing as $current) { + preg_match('/^\s*CONSTRAINT\s*`([^`]*)`.*$/', $current, $matches); + if ($constraint == $matches[1]) { + return true; + } + } + return false; + } + + /** + * Get a list of modified constraints in the database tables (associative array, + * key = table name, value = array of modified constraint definitions). + * + * @param array $missingTables List of missing tables + * @param array $missingConstraints List of missing constraints + * + * @throws \Exception + * @return array + */ + public function getModifiedConstraints($missingTables = [], + $missingConstraints = [] + ) { + $modified = []; + foreach ($this->dbCommands as $table => $sql) { + // Skip missing tables if we're logging + if (in_array($table, $missingTables)) { + continue; + } + + $expectedConstraints = []; + + // Parse column names out of the CREATE TABLE SQL, which will always be + // the first entry in the array; we assume the standard mysqldump + // formatting is used here. + preg_match_all( + '/^\s*CONSTRAINT `([^`]+)` FOREIGN KEY \(`([^)]*)`\)(.*)$/m', + $sql[0], $foreignKeyMatches + ); + foreach ($foreignKeyMatches[0] as $i => $sql) { + $fkName = $foreignKeyMatches[1][$i]; + // Skip constraint if we're logging and it's missing + if (isset($missingConstraints[$table]) + && $this->constraintIsMissing( + $fkName, $missingConstraints[$table] + ) + ) { + continue; + } + + $deleteRule = 'RESTRICT'; + $updateRule = 'RESTRICT'; + $options = 'RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT'; + $actions = isset($foreignKeyMatches[3][$i]) + ? $foreignKeyMatches[3][$i] : ''; + if (preg_match("/ON DELETE ($options)/", $actions, $matches)) { + $deleteRule = $matches[1]; + } + if (preg_match("/ON UPDATE ($options)/", $actions, $matches)) { + $updateRule = $matches[1]; + } + + // Fix trailing whitespace/punctuation: + $sql = trim(trim($sql), ',;'); + + $expectedConstraints['foreign'][$fkName] = [ + 'sql' => $sql, + 'fields' => $this->explodeFields($foreignKeyMatches[2][$i]), + 'deleteRule' => $deleteRule, + 'updateRule' => $updateRule + ]; + } + + // Now check for missing columns and build our return array: + $actualConstraints = $this->getTableConstraints($table); + + $mismatches = $this + ->compareConstraintActions($expectedConstraints, $actualConstraints); + if (!empty($mismatches)) { + $modified[$table]['foreign'] = $mismatches; + } + } + return $modified; + } + /** * Given a current row default, return true if the current default matches the * one found in the SQL provided as the $sql parameter. Return false if there @@ -686,7 +818,7 @@ class DbUpgrade extends AbstractPlugin * not exist. * * @param string $column Column to check - * @param string $missing Missing column list for column's table. + * @param array $missing Missing column list for column's table. * * @return bool */ @@ -836,4 +968,37 @@ class DbUpgrade extends AbstractPlugin } return $sqlcommands; } + + /** + * Modify constraints based on the output of getModifiedConstraints(). + * + * @param array $constraints Output of getModifiedConstraints() + * @param bool $logsql Should we return the SQL as a string rather than + * execute it? + * + * @throws \Exception + * @return string SQL if $logsql is true, empty string otherwise + */ + public function updateModifiedConstraints($constraints, $logsql = false) + { + $sqlcommands = ''; + foreach ($constraints as $table => $constraintTypeList) { + foreach ($constraintTypeList as $type => $constraintList) { + if ('foreign' !== $type) { + throw new \Exception( + "Unable to handle modification of constraint type '$type'" + ); + } + foreach ($constraintList as $name => $constraint) { + $sqlcommands .= $this->query( + "ALTER TABLE `{$table}` DROP FOREIGN KEY `{$name}`", $logsql + ); + $sqlcommands .= $this->query( + "ALTER TABLE $table ADD {$constraint['sql']}", $logsql + ); + } + } + } + return $sqlcommands; + } } diff --git a/module/VuFind/src/VuFind/Controller/UpgradeController.php b/module/VuFind/src/VuFind/Controller/UpgradeController.php index 609b53a596d..0b8c48bc32b 100644 --- a/module/VuFind/src/VuFind/Controller/UpgradeController.php +++ b/module/VuFind/src/VuFind/Controller/UpgradeController.php @@ -414,6 +414,25 @@ class UpgradeController extends AbstractBase ->createMissingConstraints($missingConstraints, $this->logsql); } + // Check for modified constraints. + $mC = $this->logsql ? $missingConstraints : []; + $modifiedConstraints = $this->dbUpgrade()->getModifiedConstraints($mT, $mC); + if (!empty($modifiedConstraints)) { + // Only manipulate DB if we're not in logging mode: + if (!$this->logsql) { + if (!$this->hasDatabaseRootCredentials()) { + return $this->forwardTo('Upgrade', 'GetDbCredentials'); + } + $this->dbUpgrade()->setAdapter($this->getRootDbAdapter()); + $this->session->warnings->append( + "Modified constraint(s) in table(s): " + . implode(', ', array_keys($modifiedConstraints)) + ); + } + $sql .= $this->dbUpgrade() + ->updateModifiedConstraints($modifiedConstraints, $this->logsql); + } + // Check for encoding problems. $encProblems = $this->dbUpgrade()->getEncodingProblems(); if (!empty($encProblems)) { -- GitLab