From 47a066a78357f8b5b684d1e4e8d09cc486a4bca1 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 14 Oct 2024 14:06:38 +0200 Subject: [PATCH] Support deferring FKs --- src/Connection.php | 2 + .../ForeignKeyConstraintViolationsTest.php | 293 ++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 tests/Functional/ForeignKeyConstraintViolationsTest.php diff --git a/src/Connection.php b/src/Connection.php index 3816a18689..7409f77a6d 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -18,6 +18,7 @@ use Doctrine\DBAL\Exception\ConnectionLost; use Doctrine\DBAL\Exception\DeadlockException; use Doctrine\DBAL\Exception\DriverException; +use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; use Doctrine\DBAL\Exception\InvalidArgumentException; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\Platforms\AbstractPlatform; @@ -1303,6 +1304,7 @@ public function transactional(Closure $func) $convertedException = $this->handleDriverException($t, null); $shouldRollback = ! ( $convertedException instanceof UniqueConstraintViolationException + || $convertedException instanceof ForeignKeyConstraintViolationException || $convertedException instanceof DeadlockException ); diff --git a/tests/Functional/ForeignKeyConstraintViolationsTest.php b/tests/Functional/ForeignKeyConstraintViolationsTest.php new file mode 100644 index 0000000000..0b8acbf89e --- /dev/null +++ b/tests/Functional/ForeignKeyConstraintViolationsTest.php @@ -0,0 +1,293 @@ +connection->getDatabasePlatform(); + + if ($platform instanceof OraclePlatform) { + $constraintName = 'FK1'; + } else { + $constraintName = 'fk1'; + } + + $this->constraintName = $constraintName; + + $schemaManager = $this->connection->createSchemaManager(); + + $table = new Table('test_t1'); + $table->addColumn('ref_id', 'integer', ['notnull' => true]); + $schemaManager->createTable($table); + + $table2 = new Table('test_t2'); + $table2->addColumn('id', 'integer', ['notnull' => true]); + $table2->setPrimaryKey(['id']); + $schemaManager->createTable($table2); + + if ($platform instanceof OraclePlatform) { + $this->connection->executeStatement( + <<createForeignKey($createConstraint, 'test_t1'); + if (! $this->supportsDeferrableConstraints()) { + return; + } + + $this->connection->executeStatement( + sprintf('ALTER TABLE test_t1 ALTER CONSTRAINT %s DEFERRABLE', $constraintName), + ); + } + } + + public function testTransactionalViolatesDeferredConstraint(): void + { + $this->skipIfDeferrableIsNotSupported(); + + $this->connection->transactional(function (Connection $connection): void { + $connection->executeStatement(sprintf('SET CONSTRAINTS "%s" DEFERRED', $this->constraintName)); + + $connection->executeStatement('INSERT INTO test_t1 VALUES (1)'); + + $this->expectConstraintViolation(true); + }); + } + + public function testTransactionalViolatesConstraint(): void + { + $this->connection->transactional(function (Connection $connection): void { + $this->expectConstraintViolation(false); + $connection->executeStatement('INSERT INTO test_t1 VALUES (1)'); + }); + } + + public function testTransactionalViolatesDeferredConstraintWhileUsingTransactionNesting(): void + { + if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) { + self::markTestSkipped('This test requires the platform to support savepoints.'); + } + + $this->skipIfDeferrableIsNotSupported(); + + $this->connection->setNestTransactionsWithSavepoints(true); + + $this->connection->transactional(function (Connection $connection): void { + $connection->executeStatement(sprintf('SET CONSTRAINTS "%s" DEFERRED', $this->constraintName)); + $connection->beginTransaction(); + $connection->executeStatement('INSERT INTO test_t1 VALUES (1)'); + $connection->commit(); + + $this->expectConstraintViolation(true); + }); + } + + public function testTransactionalViolatesConstraintWhileUsingTransactionNesting(): void + { + if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) { + self::markTestSkipped('This test requires the platform to support savepoints.'); + } + + $this->connection->setNestTransactionsWithSavepoints(true); + + $this->connection->transactional(function (Connection $connection): void { + $connection->beginTransaction(); + + try { + $this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)'); + } catch (Throwable $t) { + $this->connection->rollBack(); + + if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) { + var_dump($t); + } + + $this->expectConstraintViolation(false); + + throw $t; + } + }); + } + + public function testCommitViolatesDeferredConstraint(): void + { + $this->skipIfDeferrableIsNotSupported(); + + $this->connection->beginTransaction(); + $this->connection->executeStatement(sprintf('SET CONSTRAINTS "%s" DEFERRED', $this->constraintName)); + $this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)'); + + $this->expectConstraintViolation(true); + $this->connection->commit(); + } + + public function testInsertViolatesConstraint(): void + { + $this->connection->beginTransaction(); + + try { + $this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)'); + } catch (Throwable $t) { + $this->connection->rollBack(); + + $this->expectConstraintViolation(false); + + throw $t; + } + } + + public function testCommitViolatesDeferredConstraintWhileUsingTransactionNesting(): void + { + if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) { + self::markTestSkipped('This test requires the platform to support savepoints.'); + } + + $this->skipIfDeferrableIsNotSupported(); + + $this->connection->setNestTransactionsWithSavepoints(true); + + $this->connection->beginTransaction(); + $this->connection->executeStatement(sprintf('SET CONSTRAINTS "%s" DEFERRED', $this->constraintName)); + $this->connection->beginTransaction(); + $this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)'); + $this->connection->commit(); + + $this->expectConstraintViolation(true); + + $this->connection->commit(); + } + + public function testCommitViolatesConstraintWhileUsingTransactionNesting(): void + { + if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) { + self::markTestSkipped('This test requires the platform to support savepoints.'); + } + + $this->skipIfDeferrableIsNotSupported(); + + $this->connection->setNestTransactionsWithSavepoints(true); + + $this->connection->beginTransaction(); + $this->connection->beginTransaction(); + + try { + $this->connection->executeStatement('INSERT INTO test_t1 VALUES (1)'); + } catch (Throwable $t) { + $this->connection->rollBack(); + + $this->expectConstraintViolation(false); + + throw $t; + } + } + + private function supportsDeferrableConstraints(): bool + { + $platform = $this->connection->getDatabasePlatform(); + + return $platform instanceof OraclePlatform || $platform instanceof PostgreSQLPlatform; + } + + private function skipIfDeferrableIsNotSupported(): void + { + if ($this->supportsDeferrableConstraints()) { + return; + } + + self::markTestSkipped('Only databases supporting deferrable constraints are eligible for this test.'); + } + + private function expectConstraintViolation(bool $deferred): void + { +// if ($this->connection->getDatabasePlatform() instanceof SQLServerPlatform) { +// $this->expectExceptionMessage(sprintf("Violation of UNIQUE KEY constraint '%s'", $this->constraintName)); +// +// return; +// } +// +// if ($this->connection->getDatabasePlatform() instanceof DB2Platform) { +// // No concrete message is provided +// $this->expectException(DriverException::class); +// +// return; +// } + + if ($deferred) { + if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) { + $this->expectExceptionMessageMatches( + sprintf('~integrity constraint \(.+\.%s\) violated~', $this->constraintName), + ); + + return; + } + + $driver = $this->connection->getDriver(); + if ($driver instanceof AbstractPostgreSQLDriver) { + $this->expectExceptionMessageMatches( + sprintf('~violates foreign key constraint "%s"~', $this->constraintName), + ); + + if ($driver instanceof PDOPgSQLDriver) { + $this->expectException(PDOException::class); + + return; + } + + if ($driver instanceof PgSQLDriver) { + $this->expectException(PgSQLException::class); + + return; + } + + Assert::fail('Unsupported PG driver'); + } + + Assert::fail('Unsupported platform'); + } else { + $this->expectException(ForeignKeyConstraintViolationException::class); + } + } + + protected function tearDown(): void + { + $schemaManager = $this->connection->createSchemaManager(); + $schemaManager->dropTable('test_t1'); + $schemaManager->dropTable('test_t2'); + + $this->markConnectionNotReusable(); + + parent::tearDown(); + } +}