Skip to content

Commit e7d04a6

Browse files
beberleialcaeus
authored andcommitted
[GH-1204] Add full support for foreign key constraints in SQLite Platform and Schema.
1 parent f334025 commit e7d04a6

File tree

14 files changed

+344
-25
lines changed

14 files changed

+344
-25
lines changed

lib/Doctrine/DBAL/Driver/AbstractSQLiteDriver.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ public function convertException($message, DriverException $exception)
6767
return new Exception\ConnectionException($message, $exception);
6868
}
6969

70+
if (strpos($exception->getMessage(), 'FOREIGN KEY constraint failed') !== false) {
71+
return new Exception\ForeignKeyConstraintViolationException($message, $exception);
72+
}
73+
7074
return new Exception\DriverException($message, $exception);
7175
}
7276

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
namespace Doctrine\DBAL\Internal;
4+
5+
use function array_reverse;
6+
7+
/**
8+
* DependencyOrderCalculator implements topological sorting, which is an ordering
9+
* algorithm for directed graphs (DG) and/or directed acyclic graphs (DAG) by
10+
* using a depth-first searching (DFS) to traverse the graph built in memory.
11+
* This algorithm have a linear running time based on nodes (V) and dependency
12+
* between the nodes (E), resulting in a computational complexity of O(V + E).
13+
*/
14+
final class DependencyOrderCalculator
15+
{
16+
public const NOT_VISITED = 0;
17+
public const IN_PROGRESS = 1;
18+
public const VISITED = 2;
19+
20+
/**
21+
* Matrix of nodes (aka. vertex).
22+
* Keys are provided hashes and values are the node definition objects.
23+
*
24+
* @var array<string,DependencyOrderNode>
25+
*/
26+
private $nodeList = [];
27+
28+
/**
29+
* Volatile variable holding calculated nodes during sorting process.
30+
*
31+
* @var array<object>
32+
*/
33+
private $sortedNodeList = [];
34+
35+
/**
36+
* Checks for node (vertex) existence in graph.
37+
*/
38+
public function hasNode(string $hash) : bool
39+
{
40+
return isset($this->nodeList[$hash]);
41+
}
42+
43+
/**
44+
* Adds a new node (vertex) to the graph, assigning its hash and value.
45+
*
46+
* @param object $node
47+
*/
48+
public function addNode(string $hash, $node) : void
49+
{
50+
$vertex = new DependencyOrderNode();
51+
52+
$vertex->hash = $hash;
53+
$vertex->state = self::NOT_VISITED;
54+
$vertex->value = $node;
55+
56+
$this->nodeList[$hash] = $vertex;
57+
}
58+
59+
/**
60+
* Adds a new dependency (edge) to the graph using their hashes.
61+
*/
62+
public function addDependency(string $fromHash, string $toHash) : void
63+
{
64+
$vertex = $this->nodeList[$fromHash];
65+
$edge = new DependencyOrderEdge();
66+
67+
$edge->from = $fromHash;
68+
$edge->to = $toHash;
69+
70+
$vertex->dependencyList[$toHash] = $edge;
71+
}
72+
73+
/**
74+
* Return a valid order list of all current nodes.
75+
* The desired topological sorting is the reverse post order of these searches.
76+
*
77+
* {@internal Highly performance-sensitive method.}
78+
*
79+
* @return array<object>
80+
*/
81+
public function sort() : array
82+
{
83+
foreach ($this->nodeList as $vertex) {
84+
if ($vertex->state !== self::NOT_VISITED) {
85+
continue;
86+
}
87+
88+
$this->visit($vertex);
89+
}
90+
91+
$sortedList = $this->sortedNodeList;
92+
93+
$this->nodeList = [];
94+
$this->sortedNodeList = [];
95+
96+
return array_reverse($sortedList);
97+
}
98+
99+
/**
100+
* Visit a given node definition for reordering.
101+
*
102+
* {@internal Highly performance-sensitive method.}
103+
*/
104+
private function visit(DependencyOrderNode $vertex)
105+
{
106+
$vertex->state = self::IN_PROGRESS;
107+
108+
foreach ($vertex->dependencyList as $edge) {
109+
$adjacentVertex = $this->nodeList[$edge->to];
110+
111+
switch ($adjacentVertex->state) {
112+
case self::VISITED:
113+
case self::IN_PROGRESS:
114+
// Do nothing, since node was already visited or is
115+
// currently visited
116+
break;
117+
118+
case self::NOT_VISITED:
119+
$this->visit($adjacentVertex);
120+
}
121+
}
122+
123+
if ($vertex->state === self::VISITED) {
124+
return;
125+
}
126+
127+
$vertex->state = self::VISITED;
128+
129+
$this->sortedNodeList[] = $vertex->value;
130+
}
131+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Doctrine\DBAL\Internal;
4+
5+
class DependencyOrderEdge
6+
{
7+
/** @var string */
8+
public $from;
9+
10+
/** @var string */
11+
public $to;
12+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Doctrine\DBAL\Internal;
4+
5+
class DependencyOrderNode
6+
{
7+
/** @var string */
8+
public $hash;
9+
10+
/** @var int */
11+
public $state;
12+
13+
/** @var object */
14+
public $value;
15+
16+
/** @var DependencyOrderEdge[] */
17+
public $dependencyList = [];
18+
}

lib/Doctrine/DBAL/Platforms/AbstractPlatform.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3184,6 +3184,16 @@ public function supportsForeignKeyConstraints()
31843184
return true;
31853185
}
31863186

3187+
/**
3188+
* Whether foreign key constraints can be dropped.
3189+
*
3190+
* If false, then getDropForeignKeySQL() throws exception.
3191+
*/
3192+
public function supportsCreateDropForeignKeyConstraints() : bool
3193+
{
3194+
return true;
3195+
}
3196+
31873197
/**
31883198
* Whether this platform supports onUpdate in foreign key constraints.
31893199
*

lib/Doctrine/DBAL/Platforms/SqlitePlatform.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,14 @@ public function canEmulateSchemas()
769769
* {@inheritDoc}
770770
*/
771771
public function supportsForeignKeyConstraints()
772+
{
773+
return true;
774+
}
775+
776+
/**
777+
* {@inheritDoc}
778+
*/
779+
public function supportsCreateDropForeignKeyConstraints() : bool
772780
{
773781
return false;
774782
}
@@ -786,15 +794,15 @@ public function getCreatePrimaryKeySQL(Index $index, $table)
786794
*/
787795
public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table)
788796
{
789-
throw new DBALException('Sqlite platform does not support alter foreign key.');
797+
throw new DBALException('Sqlite platform does not support alter foreign key, the table must be fully recreated using getAlterTableSQL.');
790798
}
791799

792800
/**
793801
* {@inheritdoc}
794802
*/
795803
public function getDropForeignKeySQL($foreignKey, $table)
796804
{
797-
throw new DBALException('Sqlite platform does not support alter foreign key.');
805+
throw new DBALException('Sqlite platform does not support alter foreign key, the table must be fully recreated using getAlterTableSQL.');
798806
}
799807

800808
/**

lib/Doctrine/DBAL/Schema/SchemaDiff.php

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Doctrine\DBAL\Schema;
44

5+
use Doctrine\DBAL\Internal\DependencyOrderCalculator;
56
use Doctrine\DBAL\Platforms\AbstractPlatform;
67
use function array_merge;
78

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

139140
$foreignKeySql = [];
140-
foreach ($this->newTables as $table) {
141-
$sql = array_merge(
142-
$sql,
143-
$platform->getCreateTableSQL($table, AbstractPlatform::CREATE_INDEXES)
144-
);
141+
$createFlags = AbstractPlatform::CREATE_INDEXES;
142+
143+
if (! $platform->supportsCreateDropForeignKeyConstraints()) {
144+
$createFlags |= AbstractPlatform::CREATE_FOREIGNKEYS;
145+
}
145146

146-
if (! $platform->supportsForeignKeyConstraints()) {
147+
foreach ($this->getNewTablesSortedByDependencies() as $table) {
148+
$sql = array_merge($sql, $platform->getCreateTableSQL($table, $createFlags));
149+
150+
if (! $platform->supportsCreateDropForeignKeyConstraints()) {
147151
continue;
148152
}
149153

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

166170
return $sql;
167171
}
172+
173+
/**
174+
* Sorts tables by dependencies so that they are created in the right order.
175+
*
176+
* This is necessary when one table depends on another while creating foreign key
177+
* constraints directly during CREATE TABLE.
178+
*
179+
* @return array<Table>
180+
*/
181+
private function getNewTablesSortedByDependencies()
182+
{
183+
$calculator = new DependencyOrderCalculator();
184+
$newTables = [];
185+
186+
foreach ($this->newTables as $table) {
187+
$newTables[$table->getName()] = true;
188+
$calculator->addNode($table->getName(), $table);
189+
}
190+
191+
foreach ($this->newTables as $table) {
192+
foreach ($table->getForeignKeys() as $foreignKey) {
193+
$foreignTableName = $foreignKey->getForeignTableName();
194+
195+
if (! isset($newTables[$foreignTableName])) {
196+
continue;
197+
}
198+
199+
$calculator->addDependency($foreignTableName, $table->getName());
200+
}
201+
}
202+
203+
return $calculator->sort();
204+
}
168205
}

lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public function acceptTable(Table $table)
4646
*/
4747
public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
4848
{
49+
if (! $this->platform->supportsCreateDropForeignKeyConstraints()) {
50+
return;
51+
}
52+
4953
if (strlen($fkConstraint->getName()) === 0) {
5054
throw SchemaException::namedForeignKeyRequired($localTable, $fkConstraint);
5155
}

phpstan.neon.dist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,9 @@ parameters:
7777
-
7878
message: '~^Access to undefined constant PDO::PGSQL_ATTR_DISABLE_PREPARES\.$~'
7979
path: %currentWorkingDirectory%/lib/Doctrine/DBAL/Driver/PDOPgSql/Driver.php
80+
81+
# False Positive
82+
- '~Strict comparison using === between 1 and 2 will always evaluate to false~'
83+
84+
# Needs Generics
85+
- '~Method Doctrine\\DBAL\\Schema\\SchemaDiff::getNewTablesSortedByDependencies\(\) should return array<Doctrine\\DBAL\\Schema\\Table> but returns array<object>.~'

tests/Doctrine/Tests/DBAL/Functional/ExceptionTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ public function testTableExistsException() : void
7777

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

8484
$this->setUpForeignKeyConstraintViolationExceptionTest();

0 commit comments

Comments
 (0)