Browse Source

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.
Yves P 10 years ago
parent
commit
ac5437f1f9

+ 30 - 0
src/Database/Schema/MysqlSchema.php

@@ -389,6 +389,36 @@ class MysqlSchema extends BaseSchema
         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}
      */

+ 19 - 0
src/Database/Schema/Table.php

@@ -565,6 +565,13 @@ class Table
         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 @@ class Table
         $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);
+    }
 }

+ 20 - 2
src/TestSuite/Fixture/FixtureManager.php

@@ -16,7 +16,6 @@ namespace Cake\TestSuite\Fixture;
 
 use Cake\Core\Configure;
 use Cake\Core\Exception\Exception;
-use Cake\Database\Connection;
 use Cake\Datasource\ConnectionManager;
 use Cake\Utility\Inflector;
 use PDOException;
@@ -58,6 +57,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
      *
      * @param \Cake\TestSuite\TestCase $test The test case to inspect.
@@ -194,7 +200,7 @@ class FixtureManager
      * 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 @@ class FixtureManager
 
         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 @@ class FixtureManager
                         $fixture->truncate($db);
                     }
                 }
+
+                foreach ($fixtures as $name => $fixture) {
+                    $fixture->createConstraints($db);
+                }
             };
             $this->_runOperation($fixtures, $createTables);
 

+ 79 - 1
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;
@@ -80,6 +81,13 @@ class TestFixture implements FixtureInterface
     protected $_schema;
 
     /**
+     * Fixture constraints to be created.
+     *
+     * @var array
+     */
+    public $constraints = [];
+
+    /**
      * Instantiate the fixture.
      *
      * @throws \Cake\Core\Exception\Exception on invalid datasource usage.
@@ -159,7 +167,11 @@ class TestFixture implements FixtureInterface
         }
         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'])) {
@@ -285,6 +297,72 @@ class TestFixture implements FixtureInterface
     }
 
     /**
+     * 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.
      *
      * @return array

+ 58 - 0
tests/TestCase/TestSuite/FixtureManagerTest.php

@@ -16,6 +16,7 @@ namespace Cake\Test\TestSuite;
 
 use Cake\Core\Plugin;
 use Cake\Database\ConnectionManager;
+use Cake\ORM\TableRegistry;
 use Cake\TestSuite\Fixture\FixtureManager;
 use Cake\TestSuite\TestCase;
 
@@ -53,6 +54,63 @@ class FixtureManagerTest extends TestCase
     }
 
     /**
+     * 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.
      *
      * @return void