Skip to content

Commit 3943c20

Browse files
committed
Reproduce
1 parent 220aec4 commit 3943c20

File tree

1 file changed

+295
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)