Skip to content

Commit

Permalink
[GH-1204] Add full support for foreign key constraints in SQLite Plat…
Browse files Browse the repository at this point in the history
…form and Schema.
  • Loading branch information
beberlei committed Dec 7, 2019
1 parent 70553da commit 5a6bedc
Show file tree
Hide file tree
Showing 13 changed files with 425 additions and 25 deletions.
4 changes: 4 additions & 0 deletions lib/Doctrine/DBAL/Driver/AbstractSQLiteDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ public function convertException($message, DriverException $exception)
return new Exception\ConnectionException($message, $exception);
}

if (strpos($exception->getMessage(), 'FOREIGN KEY constraint failed') !== false) {
return new Exception\ForeignKeyConstraintViolationException($message, $exception);
}

return new Exception\DriverException($message, $exception);
}

Expand Down
164 changes: 164 additions & 0 deletions lib/Doctrine/DBAL/Internal/CommitOrderCalculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

namespace Doctrine\DBAL\Internal;

use function array_reverse;

/**
* CommitOrderCalculator implements topological sorting, which is an ordering
* algorithm for directed graphs (DG) and/or directed acyclic graphs (DAG) by
* using a depth-first searching (DFS) to traverse the graph built in memory.
* This algorithm have a linear running time based on nodes (V) and dependency
* between the nodes (E), resulting in a computational complexity of O(V + E).
*/
class CommitOrderCalculator
{
public const NOT_VISITED = 0;
public const IN_PROGRESS = 1;
public const VISITED = 2;

/**
* Matrix of nodes (aka. vertex).
* Keys are provided hashes and values are the node definition objects.
*
* @var array<string,CommitOrderNode>
*/
private $nodeList = [];

/**
* Volatile variable holding calculated nodes during sorting process.
*
* @var array<CommitOrderNode>
*/
private $sortedNodeList = [];

/**
* Checks for node (vertex) existence in graph.
*
* @param string $hash
*
* @return bool
*/
public function hasNode($hash)
{
return isset($this->nodeList[$hash]);
}

/**
* Adds a new node (vertex) to the graph, assigning its hash and value.
*
* @param string $hash
* @param object $node
*
* @return void
*/
public function addNode($hash, $node)
{
$vertex = new CommitOrderNode();

$vertex->hash = $hash;
$vertex->state = self::NOT_VISITED;
$vertex->value = $node;

$this->nodeList[$hash] = $vertex;
}

/**
* Adds a new dependency (edge) to the graph using their hashes.
*
* @param string $fromHash
* @param string $toHash
* @param int $weight
*
* @return void
*/
public function addDependency($fromHash, $toHash, $weight)
{
$vertex = $this->nodeList[$fromHash];
$edge = new CommitOrderEdge();

$edge->from = $fromHash;
$edge->to = $toHash;
$edge->weight = $weight;

$vertex->dependencyList[$toHash] = $edge;
}

/**
* Return a valid order list of all current nodes.
* The desired topological sorting is the reverse post order of these searches.
*
* {@internal Highly performance-sensitive method.}
*
* @return array<object>
*/
public function sort()
{
foreach ($this->nodeList as $vertex) {
if ($vertex->state !== self::NOT_VISITED) {
continue;
}

$this->visit($vertex);
}

$sortedList = $this->sortedNodeList;

$this->nodeList = [];
$this->sortedNodeList = [];

return array_reverse($sortedList);
}

/**
* Visit a given node definition for reordering.
*
* {@internal Highly performance-sensitive method.}
*/
private function visit(CommitOrderNode $vertex)
{
$vertex->state = self::IN_PROGRESS;

foreach ($vertex->dependencyList as $edge) {
$adjacentVertex = $this->nodeList[$edge->to];

switch ($adjacentVertex->state) {
case self::VISITED:
// Do nothing, since node was already visited
break;

case self::IN_PROGRESS:
if (isset($adjacentVertex->dependencyList[$vertex->hash]) &&
$adjacentVertex->dependencyList[$vertex->hash]->weight < $edge->weight) {
// If we have some non-visited dependencies in the in-progress dependency, we
// need to visit them before adding the node.
foreach ($adjacentVertex->dependencyList as $adjacentEdge) {
$adjacentEdgeVertex = $this->nodeList[$adjacentEdge->to];

if ($adjacentEdgeVertex->state !== self::NOT_VISITED) {
continue;
}

$this->visit($adjacentEdgeVertex);
}

$adjacentVertex->state = self::VISITED;

$this->sortedNodeList[] = $adjacentVertex->value;
}
break;

case self::NOT_VISITED:
$this->visit($adjacentVertex);
}
}

if ($vertex->state === self::VISITED) {
return;
}

$vertex->state = self::VISITED;

$this->sortedNodeList[] = $vertex->value;
}
}
15 changes: 15 additions & 0 deletions lib/Doctrine/DBAL/Internal/CommitOrderEdge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Doctrine\DBAL\Internal;

class CommitOrderEdge
{
/** @var string */
public $from;

/** @var string */
public $to;

/** @var int */
public $weight;
}
18 changes: 18 additions & 0 deletions lib/Doctrine/DBAL/Internal/CommitOrderNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Doctrine\DBAL\Internal;

class CommitOrderNode
{
/** @var string */
public $hash;

/** @var int */
public $state;

/** @var object */
public $value;

/** @var CommitOrderEdge[] */
public $dependencyList = [];
}
12 changes: 12 additions & 0 deletions lib/Doctrine/DBAL/Platforms/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -3180,6 +3180,18 @@ public function supportsForeignKeyConstraints()
return true;
}

/**
* Whether foreign key constraints can be dropped.
*
* If false, then getDropForeignKeySQL() throws exception.
*
* @return bool
*/
public function supportsCreateDropForeignKeyConstraints()
{
return true;
}

/**
* Whether this platform supports onUpdate in foreign key constraints.
*
Expand Down
12 changes: 10 additions & 2 deletions lib/Doctrine/DBAL/Platforms/SqlitePlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,14 @@ public function canEmulateSchemas()
* {@inheritDoc}
*/
public function supportsForeignKeyConstraints()
{
return true;
}

/**
* {@inheritDoc}
*/
public function supportsCreateDropForeignKeyConstraints()
{
return false;
}
Expand All @@ -780,15 +788,15 @@ public function getCreatePrimaryKeySQL(Index $index, $table)
*/
public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table)
{
throw new DBALException('Sqlite platform does not support alter foreign key.');
throw new DBALException('Sqlite platform does not support alter foreign key, the table must be fully recreated using getAlterTableSQL.');
}

/**
* {@inheritdoc}
*/
public function getDropForeignKeySQL($foreignKey, $table)
{
throw new DBALException('Sqlite platform does not support alter foreign key.');
throw new DBALException('Sqlite platform does not support alter foreign key, the table must be fully recreated using getAlterTableSQL.');
}

/**
Expand Down
49 changes: 43 additions & 6 deletions lib/Doctrine/DBAL/Schema/SchemaDiff.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Internal\CommitOrderCalculator;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use function array_merge;

Expand Down Expand Up @@ -137,13 +138,16 @@ protected function _toSql(AbstractPlatform $platform, $saveMode = false)
}

$foreignKeySql = [];
foreach ($this->newTables as $table) {
$sql = array_merge(
$sql,
$platform->getCreateTableSQL($table, AbstractPlatform::CREATE_INDEXES)
);
$createFlags = AbstractPlatform::CREATE_INDEXES;

if (! $platform->supportsCreateDropForeignKeyConstraints()) {
$createFlags |= AbstractPlatform::CREATE_FOREIGNKEYS;
}

if (! $platform->supportsForeignKeyConstraints()) {
foreach ($this->getNewTablesSortedByDependencies() as $table) {
$sql = array_merge($sql, $platform->getCreateTableSQL($table, $createFlags));

if (! $platform->supportsCreateDropForeignKeyConstraints()) {
continue;
}

Expand All @@ -165,4 +169,37 @@ protected function _toSql(AbstractPlatform $platform, $saveMode = false)

return $sql;
}

/**
* Sorts tables by dependencies so that they are created in the right order.
*
* This is ncessary when one table depends on another while creating foreign key
* constraints directly during CREATE TABLE.
*
* @return array<\Doctrine\DBAL\Schema\Table>
*/
private function getNewTablesSortedByDependencies()
{
$commitOrderCalculator = new CommitOrderCalculator();
$newTables = [];

foreach ($this->newTables as $table) {
$newTables[$table->getName()] = true;
$commitOrderCalculator->addNode($table->getName(), $table);
}

foreach ($this->newTables as $table) {
foreach ($table->getForeignKeys() as $foreignKey) {
$foreignTableName = $foreignKey->getForeignTableName();

if (! isset($newTables[$foreignTableName])) {
continue;
}

$commitOrderCalculator->addDependency($foreignTableName, $table->getName(), 1);
}
}

return $commitOrderCalculator->sort();
}
}
4 changes: 4 additions & 0 deletions lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public function acceptTable(Table $table)
*/
public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
{
if (! $this->platform->supportsCreateDropForeignKeyConstraints()) {
return;
}

if (strlen($fkConstraint->getName()) === 0) {
throw SchemaException::namedForeignKeyRequired($localTable, $fkConstraint);
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Doctrine/Tests/DBAL/Functional/ExceptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ public function testTableExistsException() : void

public function testForeignKeyConstraintViolationExceptionOnInsert() : void
{
if (! $this->connection->getDatabasePlatform()->supportsForeignKeyConstraints()) {
$this->markTestSkipped('Only fails on platforms with foreign key constraints.');
if ($this->connection->getDatabasePlatform()->getName() === 'sqlite') {
$this->connection->exec('PRAGMA foreign_keys=ON');
}

$this->setUpForeignKeyConstraintViolationExceptionTest();
Expand Down
Loading

0 comments on commit 5a6bedc

Please sign in to comment.