diff --git a/.doctrine-project.json b/.doctrine-project.json index 1bf091b733e..ccc71f8187c 100644 --- a/.doctrine-project.json +++ b/.doctrine-project.json @@ -35,6 +35,12 @@ "slug": "4.0", "maintained": false }, + { + "name": "3.10", + "branchName": "3.10.x", + "slug": "3.10", + "upcoming": true + }, { "name": "3.9", "branchName": "3.9.x", diff --git a/README.md b/README.md index 8d68192eb78..849d8f41106 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Doctrine DBAL -| [5.0-dev][5.0] | [4.3-dev][4.3] | [4.2][4.2] | [3.9][3.9] | -|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:| -| [![GitHub Actions][GA 5.0 image]][GA 5.0] | [![GitHub Actions][GA 4.3 image]][GA 4.3] | [![GitHub Actions][GA 4.2 image]][GA 4.2] | [![GitHub Actions][GA 3.9 image]][GA 3.9] | -| [![AppVeyor][AppVeyor 5.0 image]][AppVeyor 5.0] | [![AppVeyor][AppVeyor 4.3 image]][AppVeyor 4.3] | [![AppVeyor][AppVeyor 4.2 image]][AppVeyor 4.2] | [![AppVeyor][AppVeyor 3.9 image]][AppVeyor 3.9] | -| [![Code Coverage][Coverage 5.0 image]][CodeCov 5.0] | [![Code Coverage][Coverage 4.3 image]][CodeCov 4.3] | [![Code Coverage][Coverage 4.2 image]][CodeCov 4.2] | [![Code Coverage][Coverage 3.9 image]][CodeCov 3.9] | -| N/A | N/A | [![Type Coverage][TypeCov image]][TypeCov] | N/A | +| [5.0-dev][5.0] | [4.3-dev][4.3] | [4.2][4.2] | [3.10][3.10] | [3.9][3.9] | +|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:|:-----------------------------------------------------:|:---------------------------------------------------:| +| [![GitHub Actions][GA 5.0 image]][GA 5.0] | [![GitHub Actions][GA 4.3 image]][GA 4.3] | [![GitHub Actions][GA 4.2 image]][GA 4.2] | [![GitHub Actions][GA 3.10 image]][GA 3.10] | [![GitHub Actions][GA 3.9 image]][GA 3.9] | +| [![AppVeyor][AppVeyor 5.0 image]][AppVeyor 5.0] | [![AppVeyor][AppVeyor 4.3 image]][AppVeyor 4.3] | [![AppVeyor][AppVeyor 4.2 image]][AppVeyor 4.2] | [![AppVeyor][AppVeyor 3.10 image]][AppVeyor 3.10] | [![AppVeyor][AppVeyor 3.9 image]][AppVeyor 3.9] | +| [![Code Coverage][Coverage 5.0 image]][CodeCov 5.0] | [![Code Coverage][Coverage 4.3 image]][CodeCov 4.3] | [![Code Coverage][Coverage 4.2 image]][CodeCov 4.2] | [![Code Coverage][Coverage 3.10 image]][CodeCov 3.10] | [![Code Coverage][Coverage 3.9 image]][CodeCov 3.9] | +| N/A | N/A | [![Type Coverage][TypeCov image]][TypeCov] | N/A | N/A | Powerful ***D***ata***B***ase ***A***bstraction ***L***ayer with many features for database schema introspection and schema management. @@ -21,7 +21,7 @@ Powerful ***D***ata***B***ase ***A***bstraction ***L***ayer with many features f [AppVeyor 5.0]: https://ci.appveyor.com/project/doctrine/dbal/branch/5.0.x [AppVeyor 5.0 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/5.0.x?svg=true [GA 5.0]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A5.0.x - [GA 5.0 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=5.0.x + [GA 5.0 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=5.0.x [Coverage 4.3 image]: https://codecov.io/gh/doctrine/dbal/branch/4.3.x/graph/badge.svg [4.3]: https://github.com/doctrine/dbal/tree/4.3.x @@ -29,7 +29,7 @@ Powerful ***D***ata***B***ase ***A***bstraction ***L***ayer with many features f [AppVeyor 4.3]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.3.x [AppVeyor 4.3 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.3.x?svg=true [GA 4.3]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.3.x - [GA 4.3 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=4.3.x + [GA 4.3 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=4.3.x [Coverage 4.2 image]: https://codecov.io/gh/doctrine/dbal/branch/4.2.x/graph/badge.svg [4.2]: https://github.com/doctrine/dbal/tree/4.2.x @@ -37,14 +37,22 @@ Powerful ***D***ata***B***ase ***A***bstraction ***L***ayer with many features f [AppVeyor 4.2]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.2.x [AppVeyor 4.2 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.2.x?svg=true [GA 4.2]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.2.x - [GA 4.2 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=4.2.x + [GA 4.2 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=4.2.x [TypeCov]: https://shepherd.dev/github/doctrine/dbal [TypeCov image]: https://shepherd.dev/github/doctrine/dbal/coverage.svg + [Coverage 3.10 image]: https://codecov.io/gh/doctrine/dbal/branch/3.10.x/graph/badge.svg + [3.10]: https://github.com/doctrine/dbal/tree/3.10.x + [CodeCov 3.10]: https://codecov.io/gh/doctrine/dbal/branch/3.10.x + [AppVeyor 3.10]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.10.x + [AppVeyor 3.10 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.10.x?svg=true + [GA 3.10]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.10.x + [GA 3.10 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=3.10.x + [Coverage 3.9 image]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x/graph/badge.svg [3.9]: https://github.com/doctrine/dbal/tree/3.9.x [CodeCov 3.9]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x [AppVeyor 3.9]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.9.x [AppVeyor 3.9 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.9.x?svg=true [GA 3.9]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.9.x - [GA 3.9 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=3.9.x + [GA 3.9 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=3.9.x diff --git a/docs/en/reference/schema-representation.rst b/docs/en/reference/schema-representation.rst index f5aa7f4e0ac..72d4198915c 100644 --- a/docs/en/reference/schema-representation.rst +++ b/docs/en/reference/schema-representation.rst @@ -93,7 +93,7 @@ and absolutely not portable. - **engine** (string): The DB engine used for the table. Currently only supported on MySQL. - **unlogged** (boolean): Set a PostgreSQL table type as - `unlogged `_ + `unlogged `_ Column ~~~~~~ diff --git a/src/Connection.php b/src/Connection.php index 5a66e1ad21c..a81e57e2c7d 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -811,7 +811,7 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer } if (isset($value[$realKey]) && $value[$realKey] instanceof ArrayResult) { - return new Result($value[$realKey], $this); + return new Result(clone $value[$realKey], $this); } } else { $value = []; @@ -837,7 +837,7 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer $resultCache->save($item); - return new Result($value[$realKey], $this); + return new Result(clone $value[$realKey], $this); } /** diff --git a/tests/Connection/CachedQueryTest.php b/tests/Connection/CachedQueryTest.php index 3d9c825a1bd..7f2fe62ad44 100644 --- a/tests/Connection/CachedQueryTest.php +++ b/tests/Connection/CachedQueryTest.php @@ -8,27 +8,37 @@ use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; class CachedQueryTest extends TestCase { - public function testCachedQuery(): void + #[DataProvider('providePsrCacheImplementations')] + public function testCachedQuery(callable $psrCacheProvider): void { - $cache = new ArrayAdapter(); + $cache = $psrCacheProvider(); $connection = $this->createConnection(1, ['foo'], [['bar']]); $qcp = new QueryCacheProfile(0, __FUNCTION__, $cache); - self::assertSame([['foo' => 'bar']], $connection->executeCacheQuery('SELECT 1', [], [], $qcp) + $firstResult = $connection->executeCacheQuery('SELECT 1', [], [], $qcp); + self::assertSame([['foo' => 'bar']], $firstResult + ->fetchAllAssociative()); + $firstResult->free(); + $secondResult = $connection->executeCacheQuery('SELECT 1', [], [], $qcp); + self::assertSame([['foo' => 'bar']], $secondResult ->fetchAllAssociative()); + $secondResult->free(); self::assertSame([['foo' => 'bar']], $connection->executeCacheQuery('SELECT 1', [], [], $qcp) ->fetchAllAssociative()); self::assertCount(1, $cache->getItem(__FUNCTION__)->get()); } - public function testCachedQueryWithChangedImplementationIsExecutedTwice(): void + #[DataProvider('providePsrCacheImplementations')] + public function testCachedQueryWithChangedImplementationIsExecutedTwice(callable $psrCacheProvider): void { $connection = $this->createConnection(2, ['baz'], [['qux']]); @@ -36,21 +46,22 @@ public function testCachedQueryWithChangedImplementationIsExecutedTwice(): void 'SELECT 1', [], [], - new QueryCacheProfile(0, __FUNCTION__, new ArrayAdapter()), + new QueryCacheProfile(0, __FUNCTION__, $psrCacheProvider()), )->fetchAllAssociative()); self::assertSame([['baz' => 'qux']], $connection->executeCacheQuery( 'SELECT 1', [], [], - new QueryCacheProfile(0, __FUNCTION__, new ArrayAdapter()), + new QueryCacheProfile(0, __FUNCTION__, $psrCacheProvider()), )->fetchAllAssociative()); } - public function testOldCacheFormat(): void + #[DataProvider('providePsrCacheImplementations')] + public function testOldCacheFormat(callable $psrCacheProvider): void { $connection = $this->createConnection(1, ['foo'], [['bar']]); - $cache = new ArrayAdapter(); + $cache = $psrCacheProvider(); $qcp = new QueryCacheProfile(0, __FUNCTION__, $cache); [$cacheKey, $realKey] = $qcp->generateCacheKeys('SELECT 1', [], [], []); @@ -83,4 +94,13 @@ private function createConnection(int $expectedQueryCount, array $columnNames, a return new Connection([], $driver); } + + /** @return array> */ + public static function providePsrCacheImplementations(): array + { + return [ + 'serialized' => [static fn () => new ArrayAdapter(0, true)], + 'by-reference' => [static fn () => new ArrayAdapter(0, false)], + ]; + } } diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index cac8873a755..3d085ca9314 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -47,11 +47,11 @@ protected function setUp(): void private function getExecuteStatementMockConnection(): Connection&MockObject { - $driverMock = $this->createMock(Driver::class); + $driver = self::createStub(Driver::class); return $this->getMockBuilder(Connection::class) ->onlyMethods(['executeStatement']) - ->setConstructorArgs([[], $driverMock]) + ->setConstructorArgs([[], $driver]) ->getMock(); } @@ -146,9 +146,9 @@ public function testSetAutoCommit(): void public function testConnectStartsTransactionInNoAutoCommitMode(): void { - $driverMock = $this->createMock(Driver::class); + $driver = self::createStub(Driver::class); - $conn = new Connection([], $driverMock); + $conn = new Connection([], $driver); $conn->setAutoCommit(false); @@ -161,9 +161,9 @@ public function testConnectStartsTransactionInNoAutoCommitMode(): void public function testCommitStartsTransactionInNoAutoCommitMode(): void { - $driverMock = $this->createMock(Driver::class); + $driver = self::createStub(Driver::class); - $conn = new Connection([], $driverMock); + $conn = new Connection([], $driver); $conn->setAutoCommit(false); $conn->executeQuery('SELECT 1'); @@ -180,9 +180,9 @@ public static function resultProvider(): array public function testRollBackStartsTransactionInNoAutoCommitMode(): void { - $driverMock = $this->createMock(Driver::class); + $driver = self::createStub(Driver::class); - $conn = new Connection([], $driverMock); + $conn = new Connection([], $driver); $conn->setAutoCommit(false); $conn->executeQuery('SELECT 1'); @@ -198,12 +198,12 @@ public function testSwitchingAutoCommitModeCommitsAllCurrentTransactions(): void ->method('supportsSavepoints') ->willReturn(true); - $driverMock = $this->createMock(Driver::class); - $driverMock + $driver = self::createStub(Driver::class); + $driver ->method('getDatabasePlatform') ->willReturn($platform); - $conn = new Connection([], $driverMock); + $conn = new Connection([], $driver); $conn->beginTransaction(); $conn->beginTransaction(); diff --git a/tests/Functional/TransactionTest.php b/tests/Functional/TransactionTest.php index 693271f83ee..d21c2e6d2c1 100644 --- a/tests/Functional/TransactionTest.php +++ b/tests/Functional/TransactionTest.php @@ -7,7 +7,9 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception\ConnectionLost; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Tests\FunctionalTestCase; +use Doctrine\DBAL\Types\Types; use function func_get_args; use function restore_error_handler; @@ -18,15 +20,6 @@ class TransactionTest extends FunctionalTestCase { - protected function setUp(): void - { - if ($this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { - return; - } - - self::markTestSkipped('Restricted to MySQL.'); - } - public function testCommitFailure(): void { $this->expectConnectionLoss(static function (Connection $connection): void { @@ -43,6 +36,10 @@ public function testRollbackFailure(): void private function expectConnectionLoss(callable $scenario): void { + if (! $this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + self::markTestSkipped('Restricted to MySQL.'); + } + $this->connection->executeStatement('SET SESSION wait_timeout=1'); $this->connection->beginTransaction(); @@ -67,4 +64,34 @@ private function expectConnectionLoss(callable $scenario): void restore_error_handler(); } } + + public function testNestedTransactionWalkthrough(): void + { + if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) { + self::markTestIncomplete('Broken when savepoints are not supported.'); + } + + $table = new Table('storage'); + $table->addColumn('test_int', Types::INTEGER); + $table->setPrimaryKey(['test_int']); + + $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)); + } } diff --git a/tests/Query/Expression/ExpressionBuilderTest.php b/tests/Query/Expression/ExpressionBuilderTest.php index 06cc0a7dbd6..4ec5d5badca 100644 --- a/tests/Query/Expression/ExpressionBuilderTest.php +++ b/tests/Query/Expression/ExpressionBuilderTest.php @@ -16,11 +16,11 @@ class ExpressionBuilderTest extends TestCase protected function setUp(): void { - $conn = $this->createMock(Connection::class); + $conn = self::createStub(Connection::class); $this->expr = new ExpressionBuilder($conn); - $conn->expects(self::any()) + $conn ->method('createExpressionBuilder') ->willReturn($this->expr); } diff --git a/tests/Types/DateImmutableTypeTest.php b/tests/Types/DateImmutableTypeTest.php index 31f5cdce1fe..3a3ca8a15cc 100644 --- a/tests/Types/DateImmutableTypeTest.php +++ b/tests/Types/DateImmutableTypeTest.php @@ -90,7 +90,7 @@ public function testConvertsDateStringToPHPValue(): void public function testResetTimeFractionsWhenConvertingToPHPValue(): void { - $this->platform->expects(self::any()) + $this->platform ->method('getDateFormatString') ->willReturn('Y-m-d'); diff --git a/tests/Types/DateTimeImmutableTypeTest.php b/tests/Types/DateTimeImmutableTypeTest.php index cce9557636b..358267895e2 100644 --- a/tests/Types/DateTimeImmutableTypeTest.php +++ b/tests/Types/DateTimeImmutableTypeTest.php @@ -90,7 +90,7 @@ public function testConvertsDateTimeStringToPHPValue(): void public function testConvertsDateTimeStringWithMicrosecondsToPHPValue(): void { - $this->platform->expects(self::any()) + $this->platform ->method('getDateTimeFormatString') ->willReturn('Y-m-d H:i:s'); diff --git a/tests/Types/TimeImmutableTypeTest.php b/tests/Types/TimeImmutableTypeTest.php index f1738546868..103328b0aae 100644 --- a/tests/Types/TimeImmutableTypeTest.php +++ b/tests/Types/TimeImmutableTypeTest.php @@ -90,7 +90,7 @@ public function testConvertsTimeStringToPHPValue(): void public function testResetDateFractionsWhenConvertingToPHPValue(): void { - $this->platform->expects(self::any()) + $this->platform ->method('getTimeFormatString') ->willReturn('H:i:s');