Skip to content

Commit

Permalink
Implements a way of dealing with constraints in the fixtures
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
HavokInspiration committed Oct 15, 2015
1 parent d2993dd commit ac5437f
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 3 deletions.
30 changes: 30 additions & 0 deletions src/Database/Schema/MysqlSchema.php
Expand Up @@ -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}
*/
Expand Down
19 changes: 19 additions & 0 deletions src/Database/Schema/Table.php
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
}
}
22 changes: 20 additions & 2 deletions src/TestSuite/Fixture/FixtureManager.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -240,18 +246,30 @@ 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);
} else {
$fixture->truncate($db);
}
}

foreach ($fixtures as $name => $fixture) {
$fixture->createConstraints($db);
}
};
$this->_runOperation($fixtures, $createTables);

Expand Down
80 changes: 79 additions & 1 deletion src/TestSuite/Fixture/TestFixture.php
Expand Up @@ -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;
Expand Down Expand Up @@ -79,6 +80,13 @@ class TestFixture implements FixtureInterface
*/
protected $_schema;

/**
* Fixture constraints to be created.
*
* @var array
*/
public $constraints = [];

/**
* Instantiate the fixture.
*
Expand Down Expand Up @@ -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'])) {
Expand Down Expand Up @@ -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.
*
Expand Down
58 changes: 58 additions & 0 deletions tests/TestCase/TestSuite/FixtureManagerTest.php
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
Expand Down

0 comments on commit ac5437f

Please sign in to comment.