From ac5437f1f9a1c811c20834b28fd82deaaa5c6fb2 Mon Sep 17 00:00:00 2001 From: Yves P Date: Wed, 23 Sep 2015 19:16:18 +0200 Subject: [PATCH] Implements a way of dealing with constraints in the fixtures Fixtures are first created without the foreign keys. Once all tables have been created, constraints are added. This prevents errors that can happend when referencing non existant columns. Before being truncated, tables have their constraints dropped to prevent constraint violation. --- src/Database/Schema/MysqlSchema.php | 30 +++++++ src/Database/Schema/Table.php | 19 +++++ src/TestSuite/Fixture/FixtureManager.php | 22 ++++- src/TestSuite/Fixture/TestFixture.php | 80 ++++++++++++++++++- .../TestCase/TestSuite/FixtureManagerTest.php | 58 ++++++++++++++ 5 files changed, 206 insertions(+), 3 deletions(-) diff --git a/src/Database/Schema/MysqlSchema.php b/src/Database/Schema/MysqlSchema.php index 53225f246b5..d32a1054823 100644 --- a/src/Database/Schema/MysqlSchema.php +++ b/src/Database/Schema/MysqlSchema.php @@ -389,6 +389,36 @@ public function constraintSql(Table $table, $name) return $this->_keySql($out, $data); } + public function addConstraintSql(Table $table) + { + $sqlPattern = 'ALTER TABLE %s ADD %s'; + $sql = []; + + foreach ($table->constraints() as $name) { + $constraint = $table->constraint($name); + if ($constraint['type'] === Table::CONSTRAINT_FOREIGN) { + $sql[] = sprintf($sqlPattern, $table->name(), $this->constraintSql($table, $name)); + } + } + + return $sql; + } + + public function dropConstraintSql(Table $table) + { + $sqlPattern = 'ALTER TABLE %s DROP FOREIGN KEY %s'; + $sql = []; + + foreach ($table->constraints() as $name) { + $constraint = $table->constraint($name); + if ($constraint['type'] === Table::CONSTRAINT_FOREIGN) { + $sql[] = sprintf($sqlPattern, $table->name(), $name); + } + } + + return $sql; + } + /** * {@inheritDoc} */ diff --git a/src/Database/Schema/Table.php b/src/Database/Schema/Table.php index 3f44d6e83fd..d6443164e51 100644 --- a/src/Database/Schema/Table.php +++ b/src/Database/Schema/Table.php @@ -565,6 +565,13 @@ public function addConstraint($name, $attrs) return $this; } + public function dropConstraint($name) + { + if (isset($this->_constraints[$name])) { + unset($this->_constraints[$name]); + } + } + /** * Check whether or not a table has an autoIncrement column defined. * @@ -710,4 +717,16 @@ public function truncateSql(ConnectionInterface $connection) $dialect = $connection->driver()->schemaDialect(); return $dialect->truncateTableSql($this); } + + public function addConstraintSql(Connection $connection) + { + $dialect = $connection->driver()->schemaDialect(); + return $dialect->addConstraintSql($this); + } + + public function dropConstraintSql(Connection $connection) + { + $dialect = $connection->driver()->schemaDialect(); + return $dialect->dropConstraintSql($this); + } } diff --git a/src/TestSuite/Fixture/FixtureManager.php b/src/TestSuite/Fixture/FixtureManager.php index 531e7f8cc56..768a1f4d83e 100644 --- a/src/TestSuite/Fixture/FixtureManager.php +++ b/src/TestSuite/Fixture/FixtureManager.php @@ -16,7 +16,6 @@ use Cake\Core\Configure; use Cake\Core\Exception\Exception; -use Cake\Database\Connection; use Cake\Datasource\ConnectionManager; use Cake\Utility\Inflector; use PDOException; @@ -57,6 +56,13 @@ class FixtureManager */ protected $_insertionMap = []; + /** + * List of TestCase class name that have been processed + * + * @var array + */ + protected $_processed = []; + /** * Inspects the test to look for unloaded fixtures and loads them * @@ -194,7 +200,7 @@ protected function _loadFixtures($test) * Runs the drop and create commands on the fixtures if necessary. * * @param \Cake\TestSuite\Fixture\TestFixture $fixture the fixture object to create - * @param Connection $db the datasource instance to use + * @param \Cake\Database\Connection $db The Connection object instance to use * @param array $sources The existing tables in the datasource. * @param bool $drop whether drop the fixture if it is already created or not * @return void @@ -240,11 +246,19 @@ public function load($test) try { $createTables = function ($db, $fixtures) use ($test) { + $db->enableForeignKeys(); $tables = $db->schemaCollection()->listTables(); $configName = $db->configName(); if (!isset($this->_insertionMap[$configName])) { $this->_insertionMap[$configName] = []; } + + foreach ($fixtures as $name => $fixture) { + if (in_array($fixture->table, $tables)) { + $fixture->dropConstraints($db); + } + } + foreach ($fixtures as $fixture) { if (!in_array($fixture, $this->_insertionMap[$configName])) { $this->_setupTable($fixture, $db, $tables, $test->dropTables); @@ -252,6 +266,10 @@ public function load($test) $fixture->truncate($db); } } + + foreach ($fixtures as $name => $fixture) { + $fixture->createConstraints($db); + } }; $this->_runOperation($fixtures, $createTables); diff --git a/src/TestSuite/Fixture/TestFixture.php b/src/TestSuite/Fixture/TestFixture.php index 4d127f3e1bc..48eed3cdfba 100644 --- a/src/TestSuite/Fixture/TestFixture.php +++ b/src/TestSuite/Fixture/TestFixture.php @@ -14,6 +14,7 @@ namespace Cake\TestSuite\Fixture; use Cake\Core\Exception\Exception as CakeException; +use Cake\Database\Connection; use Cake\Database\Schema\Table; use Cake\Datasource\ConnectionInterface; use Cake\Datasource\ConnectionManager; @@ -79,6 +80,13 @@ class TestFixture implements FixtureInterface */ protected $_schema; + /** + * Fixture constraints to be created. + * + * @var array + */ + public $constraints = []; + /** * Instantiate the fixture. * @@ -159,7 +167,11 @@ protected function _schemaFromFields() } if (!empty($this->fields['_constraints'])) { foreach ($this->fields['_constraints'] as $name => $data) { - $this->_schema->addConstraint($name, $data); + if ($data['type'] !== 'foreign') { + $this->_schema->addConstraint($name, $data); + } else { + $this->constraints[$name] = $data; + } } } if (!empty($this->fields['_indexes'])) { @@ -284,6 +296,72 @@ public function insert(ConnectionInterface $db) return true; } + /** + * Build and execute SQL queries necessary to create the constraints for the + * fixture + * + * @param \Cake\Database\Connection $db An instance of the database into which the constraints will be created + * @return bool on success or if there are no constraints to create, or false on failure + */ + public function createConstraints(Connection $db) + { + if (empty($this->constraints)) { + return true; + } + + foreach ($this->constraints as $name => $data) { + $this->_schema->addConstraint($name, $data); + } + + $sql = $this->_schema->addConstraintSql($db); + + if (empty($sql)) { + return true; + } + + try { + foreach ($sql as $stmt) { + $db->execute($stmt)->closeCursor(); + } + } catch (\Exception $e) { + return false; + } + return true; + } + + /** + * Build and execute SQL queries necessary to drop the constraints for the + * fixture + * + * @param \Cake\Database\Connection $db An instance of the database into which the constraints will be dropped + * @return bool on success or if there are no constraints to drop, or false on failure + */ + public function dropConstraints(Connection $db) + { + if (empty($this->constraints)) { + return true; + } + + $sql = $this->_schema->dropConstraintSql($db); + + if (empty($sql)) { + return true; + } + + try { + foreach ($sql as $stmt) { + $db->execute($stmt)->closeCursor(); + } + } catch (\Exception $e) { + return false; + } + + foreach ($this->constraints as $name => $data) { + $this->_schema->dropConstraint($name); + } + return true; + } + /** * Converts the internal records into data used to generate a query. * diff --git a/tests/TestCase/TestSuite/FixtureManagerTest.php b/tests/TestCase/TestSuite/FixtureManagerTest.php index 5138ab7aef8..7440ec0ef36 100644 --- a/tests/TestCase/TestSuite/FixtureManagerTest.php +++ b/tests/TestCase/TestSuite/FixtureManagerTest.php @@ -16,6 +16,7 @@ use Cake\Core\Plugin; use Cake\Database\ConnectionManager; +use Cake\ORM\TableRegistry; use Cake\TestSuite\Fixture\FixtureManager; use Cake\TestSuite\TestCase; @@ -52,6 +53,63 @@ public function testFixturizeCore() $this->assertInstanceOf('Cake\Test\Fixture\ArticlesFixture', $fixtures['core.articles']); } + /** + * Test loading fixtures with constraints. + * + * @return void + */ + public function testFixturizeCoreConstraint() + { + $test = $this->getMock('Cake\TestSuite\TestCase'); + $test->fixtures = ['core.articles', 'core.articles_tags', 'core.tags']; + $this->manager->fixturize($test); + $this->manager->load($test); + + $table = TableRegistry::get('ArticlesTags'); + $schema = $table->schema(); + + $this->assertEquals(['primary', 'tag_id_fk'], $schema->constraints()); + + $expectedConstraint = [ + 'type' => 'foreign', + 'columns' => [ + 'tag_id' + ], + 'references' => [ + 'tags', + 'id' + ], + 'update' => 'cascade', + 'delete' => 'cascade', + 'length' => [] + ]; + $this->assertEquals($expectedConstraint, $schema->constraint('tag_id_fk')); + $this->manager->unload($test); + + $this->manager->load($test); + $table = TableRegistry::get('ArticlesTags'); + $schema = $table->schema(); + + $this->assertEquals(['primary', 'tag_id_fk'], $schema->constraints()); + $expectedConstraint = [ + 'type' => 'foreign', + 'columns' => [ + 'tag_id' + ], + 'references' => [ + 'tags', + 'id' + ], + 'update' => 'cascade', + 'delete' => 'cascade', + 'length' => [] + ]; + $this->assertEquals($expectedConstraint, $schema->constraint('tag_id_fk')); + + $this->manager->unload($test); + $this->manager->shutDown($test); + } + /** * Test loading app fixtures. *