1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
|
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Tests\Functional;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Tests\TestUtil;
use Doctrine\DBAL\Types\Types;
use function func_get_args;
use function restore_error_handler;
use function set_error_handler;
use const E_WARNING;
class TransactionTest extends FunctionalTestCase
{
public function testBeginTransactionFailure(): void
{
$this->expectConnectionLoss(static function (Connection $connection): void {
$connection->beginTransaction();
});
}
public function testCommitFailure(): void
{
$this->connection->beginTransaction();
$this->expectConnectionLoss(static function (Connection $connection): void {
$connection->commit();
});
}
public function testRollbackFailure(): void
{
$this->connection->beginTransaction();
$this->expectConnectionLoss(static function (Connection $connection): void {
$connection->rollBack();
});
}
private function expectConnectionLoss(callable $scenario): void
{
$this->killCurrentSession();
$this->expectException(ConnectionLost::class);
// prevent the PHPUnit error handler from handling the "MySQL server has gone away" warning
/** @var callable|null $previous */
$previous = null;
$previous = set_error_handler(static function (int $errno) use (&$previous): bool {
if (($errno & ~E_WARNING) === 0) {
return true;
}
return $previous !== null && $previous(...func_get_args());
});
try {
$scenario($this->connection);
} finally {
restore_error_handler();
}
}
private function killCurrentSession(): void
{
$this->markConnectionNotReusable();
$databasePlatform = $this->connection->getDatabasePlatform();
[$currentProcessQuery, $killProcessStatement] = match (true) {
$databasePlatform instanceof AbstractMySqlPlatform => [
'SELECT CONNECTION_ID()',
'KILL ?',
],
$databasePlatform instanceof PostgreSQLPlatform => [
'SELECT pg_backend_pid()',
'SELECT pg_terminate_backend(?)',
],
default => self::markTestSkipped('Unsupported test platform.'),
};
$privilegedConnection = TestUtil::getPrivilegedConnection();
$privilegedConnection->executeStatement(
$killProcessStatement,
[$this->connection->executeQuery($currentProcessQuery)->fetchOne()],
);
}
public function testNestedTransactionWalkthrough(): void
{
if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) {
self::markTestIncomplete('Broken when savepoints are not supported.');
}
$table = Table::editor()
->setUnquotedName('storage')
->setColumns(
Column::editor()
->setUnquotedName('test_int')
->setTypeName(Types::INTEGER)
->create(),
)
->setPrimaryKeyConstraint(
PrimaryKeyConstraint::editor()
->setUnquotedColumnNames('test_int')
->create(),
)
->create();
$this->dropAndCreateTable($table);
$query = 'SELECT count(test_int) FROM storage';
self::assertSame('0', (string) $this->connection->fetchOne($query));
$result = $this->connection->transactional(
static fn (Connection $connection) => $connection->transactional(
static function (Connection $connection) use ($query) {
$connection->insert('storage', ['test_int' => 1]);
return $connection->fetchOne($query);
},
),
);
self::assertSame('1', (string) $result);
self::assertSame('1', (string) $this->connection->fetchOne($query));
}
}
|