Skip to content

Commit 675d1ac

Browse files
committed
Support deferring FKs
1 parent b1b6ab2 commit 675d1ac

File tree

2 files changed

+290
-0
lines changed

2 files changed

+290
-0
lines changed

src/Connection.php

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Doctrine\DBAL\Exception\ConnectionLost;
1919
use Doctrine\DBAL\Exception\DeadlockException;
2020
use Doctrine\DBAL\Exception\DriverException;
21+
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
2122
use Doctrine\DBAL\Exception\InvalidArgumentException;
2223
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
2324
use Doctrine\DBAL\Platforms\AbstractPlatform;
@@ -1303,6 +1304,7 @@ public function transactional(Closure $func)
13031304
$convertedException = $this->handleDriverException($t, null);
13041305
$shouldRollback = ! (
13051306
$convertedException instanceof UniqueConstraintViolationException
1307+
|| $convertedException instanceof ForeignKeyConstraintViolationException
13061308
|| $convertedException instanceof DeadlockException
13071309
);
13081310

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Functional;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
9+
use Doctrine\DBAL\Driver\PDO\PDOException;
10+
use Doctrine\DBAL\Driver\PDO\PgSQL\Driver as PDOPgSQLDriver;
11+
use Doctrine\DBAL\Driver\PgSQL\Driver as PgSQLDriver;
12+
use Doctrine\DBAL\Driver\PgSQL\Exception as PgSQLException;
13+
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
14+
use Doctrine\DBAL\Platforms\OraclePlatform;
15+
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
16+
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
17+
use Doctrine\DBAL\Schema\Table;
18+
use Doctrine\DBAL\Tests\FunctionalTestCase;
19+
use PHPUnit\Framework\Assert;
20+
use Throwable;
21+
22+
use function sprintf;
23+
24+
final class ForeignKeyConstraintViolationsTest extends FunctionalTestCase
25+
{
26+
private string $constraintName = '';
27+
28+
protected function setUp(): void
29+
{
30+
parent::setUp();
31+
32+
$platform = $this->connection->getDatabasePlatform();
33+
34+
if ($platform instanceof OraclePlatform) {
35+
$constraintName = 'FK1';
36+
} else {
37+
$constraintName = 'fk1';
38+
}
39+
40+
$this->constraintName = $constraintName;
41+
42+
$schemaManager = $this->connection->createSchemaManager();
43+
44+
$table = new Table('test_t1');
45+
$table->addColumn('ref_id', 'integer', ['notnull' => true]);
46+
$schemaManager->createTable($table);
47+
48+
$table2 = new Table('test_t2');
49+
$table2->addColumn('id', 'integer', ['notnull' => true]);
50+
$table2->setPrimaryKey(['id']);
51+
$schemaManager->createTable($table2);
52+
53+
if ($platform instanceof OraclePlatform) {
54+
$this->connection->executeStatement(
55+
<<<SQL
56+
ALTER TABLE test_t1 ADD CONSTRAINT ' . $constraintName . '
57+
FOREIGN KEY (ref_id) REFERENCES test_t2 (id)
58+
DEFERRABLE INITIALLY DEFERRED
59+
SQL,
60+
);
61+
} else {
62+
$createConstraint = new ForeignKeyConstraint(['ref_id'], 'test_t2', ['id'], $constraintName);
63+
64+
$schemaManager->createForeignKey($createConstraint, 'test_t1');
65+
if (! $this->supportsDeferrableConstraints()) {
66+
return;
67+
}
68+
69+
$this->connection->executeStatement(
70+
sprintf('ALTER TABLE test_t1 ALTER CONSTRAINT %s DEFERRABLE', $constraintName),
71+
);
72+
}
73+
}
74+
75+
public function testTransactionalViolatesDeferredConstraint(): void
76+
{
77+
$this->skipIfDeferrableIsNotSupported();
78+
79+
$this->connection->transactional(function (Connection $connection): void {
80+
$connection->executeStatement(sprintf('SET CONSTRAINTS "%s" DEFERRED', $this->constraintName));
81+
82+
$connection->executeStatement('INSERT INTO test_t1 VALUES (1)');
83+
84+
$this->expectConstraintViolation(true);
85+
});
86+
}
87+
88+
public function testTransactionalViolatesConstraint(): void
89+
{
90+
$this->connection->transactional(function (Connection $connection): void {
91+
$this->expectConstraintViolation(false);
92+
$connection->executeStatement('INSERT INTO test_t1 VALUES (1)');
93+
});
94+
}
95+
96+
public function testTransactionalViolatesDeferredConstraintWhileUsingTransactionNesting(): void
97+
{
98+
if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) {
99+
self::markTestSkipped('This test requires the platform to support savepoints.');
100+
}
101+
102+
$this->skipIfDeferrableIsNotSupported();
103+
104+
$this->connection->setNestTransactionsWithSavepoints(true);
105+
106+
$this->connection->transactional(function (Connection $connection): void {
107+
$connection->executeStatement(sprintf('SET CONSTRAINTS "%s" DEFERRED', $this->constraintName));
108+
$connection->beginTransaction();
109+
$connection->executeStatement('INSERT INTO test_t1 VALUES (1)');
110+
$connection->commit();
111+
112+
$this->expectConstraintViolation(true);
113+
});
114+
}
115+
116+
public function testTransactionalViolatesConstraintWhileUsingTransactionNesting(): void
117+
{
118+
if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) {
119+
self::markTestSkipped('This test requires the platform to support savepoints.');
120+
}
121+
122+
$this->connection->setNestTransactionsWithSavepoints(true);
123+
124+
$this->connection->transactional(function (Connection $connection): void {
125+
$connection->beginTransaction();
126+
127+
try {
128+
$this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)');
129+
} catch (Throwable $t) {
130+
$this->connection->rollBack();
131+
132+
$this->expectConstraintViolation(false);
133+
134+
throw $t;
135+
}
136+
});
137+
}
138+
139+
public function testCommitViolatesDeferredConstraint(): void
140+
{
141+
$this->skipIfDeferrableIsNotSupported();
142+
143+
$this->connection->beginTransaction();
144+
$this->connection->executeStatement(sprintf('SET CONSTRAINTS "%s" DEFERRED', $this->constraintName));
145+
$this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)');
146+
147+
$this->expectConstraintViolation(true);
148+
$this->connection->commit();
149+
}
150+
151+
public function testInsertViolatesConstraint(): void
152+
{
153+
$this->connection->beginTransaction();
154+
155+
try {
156+
$this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)');
157+
} catch (Throwable $t) {
158+
$this->connection->rollBack();
159+
160+
$this->expectConstraintViolation(false);
161+
162+
throw $t;
163+
}
164+
}
165+
166+
public function testCommitViolatesDeferredConstraintWhileUsingTransactionNesting(): void
167+
{
168+
if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) {
169+
self::markTestSkipped('This test requires the platform to support savepoints.');
170+
}
171+
172+
$this->skipIfDeferrableIsNotSupported();
173+
174+
$this->connection->setNestTransactionsWithSavepoints(true);
175+
176+
$this->connection->beginTransaction();
177+
$this->connection->executeStatement(sprintf('SET CONSTRAINTS "%s" DEFERRED', $this->constraintName));
178+
$this->connection->beginTransaction();
179+
$this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)');
180+
$this->connection->commit();
181+
182+
$this->expectConstraintViolation(true);
183+
184+
$this->connection->commit();
185+
}
186+
187+
public function testCommitViolatesConstraintWhileUsingTransactionNesting(): void
188+
{
189+
if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) {
190+
self::markTestSkipped('This test requires the platform to support savepoints.');
191+
}
192+
193+
$this->skipIfDeferrableIsNotSupported();
194+
195+
$this->connection->setNestTransactionsWithSavepoints(true);
196+
197+
$this->connection->beginTransaction();
198+
$this->connection->beginTransaction();
199+
200+
try {
201+
$this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)');
202+
} catch (Throwable $t) {
203+
$this->connection->rollBack();
204+
205+
$this->expectConstraintViolation(false);
206+
207+
throw $t;
208+
}
209+
}
210+
211+
private function supportsDeferrableConstraints(): bool
212+
{
213+
$platform = $this->connection->getDatabasePlatform();
214+
215+
return $platform instanceof OraclePlatform || $platform instanceof PostgreSQLPlatform;
216+
}
217+
218+
private function skipIfDeferrableIsNotSupported(): void
219+
{
220+
if ($this->supportsDeferrableConstraints()) {
221+
return;
222+
}
223+
224+
self::markTestSkipped('Only databases supporting deferrable constraints are eligible for this test.');
225+
}
226+
227+
private function expectConstraintViolation(bool $deferred): void
228+
{
229+
// if ($this->connection->getDatabasePlatform() instanceof SQLServerPlatform) {
230+
// $this->expectExceptionMessage(sprintf("Violation of UNIQUE KEY constraint '%s'", $this->constraintName));
231+
//
232+
// return;
233+
// }
234+
//
235+
// if ($this->connection->getDatabasePlatform() instanceof DB2Platform) {
236+
// // No concrete message is provided
237+
// $this->expectException(DriverException::class);
238+
//
239+
// return;
240+
// }
241+
242+
if ($deferred) {
243+
// if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
244+
// $this->expectExceptionMessageMatches(
245+
// sprintf('~unique constraint \(.+\.%s\) violated~', $this->constraintName),
246+
// );
247+
//
248+
// return;
249+
// }
250+
251+
$driver = $this->connection->getDriver();
252+
if ($driver instanceof AbstractPostgreSQLDriver) {
253+
$this->expectExceptionMessageMatches(
254+
sprintf('~violates foreign key constraint "%s"~', $this->constraintName),
255+
);
256+
257+
if ($driver instanceof PDOPgSQLDriver) {
258+
$this->expectException(PDOException::class);
259+
260+
return;
261+
}
262+
263+
if ($driver instanceof PgSQLDriver) {
264+
$this->expectException(PgSQLException::class);
265+
266+
return;
267+
}
268+
269+
Assert::fail('Unsupported PG driver');
270+
}
271+
272+
Assert::fail('Unsupported platform');
273+
} else {
274+
$this->expectException(ForeignKeyConstraintViolationException::class);
275+
}
276+
}
277+
278+
protected function tearDown(): void
279+
{
280+
$schemaManager = $this->connection->createSchemaManager();
281+
$schemaManager->dropTable('test_t1');
282+
$schemaManager->dropTable('test_t2');
283+
284+
$this->markConnectionNotReusable();
285+
286+
parent::tearDown();
287+
}
288+
}

0 commit comments

Comments
 (0)