From cb5d832e4e9a58da300ce0a737613950114bfa34 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Tue, 7 Nov 2023 16:18:53 +0100 Subject: [PATCH] [Cache][Lock] Fix PDO store not creating table + add tests --- .../Cache/Adapter/DoctrineDbalAdapter.php | 3 +- .../Component/Cache/Adapter/PdoAdapter.php | 21 ++++++++- .../Tests/Adapter/DoctrineDbalAdapterTest.php | 43 +++++++++++++------ .../Cache/Tests/Adapter/PdoAdapterTest.php | 43 ++++++++++++++----- .../Storage/Handler/SessionHandlerFactory.php | 1 + src/Symfony/Component/Lock/Store/PdoStore.php | 33 ++++++++++++-- .../Tests/Store/DoctrineDbalStoreTest.php | 36 +++++++++++++--- .../Lock/Tests/Store/PdoStoreTest.php | 38 +++++++++++++--- .../Transport/PostgreSqlConnection.php | 1 + 9 files changed, 177 insertions(+), 42 deletions(-) diff --git a/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php b/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php index eacf8eb9bcc88..0e061d26ea1d8 100644 --- a/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php @@ -420,7 +420,8 @@ private function getServerVersion(): string return $this->serverVersion; } - $conn = $this->conn->getWrappedConnection(); + // The condition should be removed once support for DBAL <3.3 is dropped + $conn = method_exists($this->conn, 'getNativeConnection') ? $this->conn->getNativeConnection() : $this->conn->getWrappedConnection(); if ($conn instanceof ServerInfoAwareConnection) { return $this->serverVersion = $conn->getServerVersion(); } diff --git a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php index b339defeb30fd..ba0aaa15853bf 100644 --- a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php @@ -507,7 +507,7 @@ protected function doSave(array $values, int $lifetime) try { $stmt = $conn->prepare($sql); } catch (\PDOException $e) { - if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { $this->createTable(); } $stmt = $conn->prepare($sql); @@ -542,7 +542,7 @@ protected function doSave(array $values, int $lifetime) try { $stmt->execute(); } catch (\PDOException $e) { - if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { $this->createTable(); } $stmt->execute(); @@ -596,4 +596,21 @@ private function getServerVersion(): string return $this->serverVersion; } + + private function isTableMissing(\PDOException $exception): bool + { + $driver = $this->driver; + $code = $exception->getCode(); + + switch (true) { + case 'pgsql' === $driver && '42P01' === $code: + case 'sqlite' === $driver && str_contains($exception->getMessage(), 'no such table:'): + case 'oci' === $driver && 942 === $code: + case 'sqlsrv' === $driver && 208 === $code: + case 'mysql' === $driver && 1146 === $code: + return true; + default: + return false; + } + } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php index 79299ecd61506..63a567a069e08 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php @@ -18,12 +18,13 @@ use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; use Doctrine\DBAL\Schema\Schema; -use PHPUnit\Framework\SkippedTestSuiteError; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; use Symfony\Component\Cache\Tests\Fixtures\DriverWrapper; /** + * @requires extension pdo_sqlite + * * @group time-sensitive */ class DoctrineDbalAdapterTest extends AdapterTestCase @@ -32,10 +33,6 @@ class DoctrineDbalAdapterTest extends AdapterTestCase public static function setUpBeforeClass(): void { - if (!\extension_loaded('pdo_sqlite')) { - throw new SkippedTestSuiteError('Extension pdo_sqlite required.'); - } - self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); } @@ -107,13 +104,12 @@ public function testConfigureSchemaTableExists() } /** - * @dataProvider provideDsn + * @dataProvider provideDsnWithSQLite */ - public function testDsn(string $dsn, string $file = null) + public function testDsnWithSQLite(string $dsn, string $file = null) { try { $pool = new DoctrineDbalAdapter($dsn); - $pool->createTable(); $item = $pool->getItem('key'); $item->set('value'); @@ -125,12 +121,35 @@ public function testDsn(string $dsn, string $file = null) } } - public static function provideDsn() + public static function provideDsnWithSQLite() { $dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); - yield ['sqlite://localhost/'.$dbFile.'1', $dbFile.'1']; - yield ['sqlite3:///'.$dbFile.'3', $dbFile.'3']; - yield ['sqlite://localhost/:memory:']; + yield 'SQLite file' => ['sqlite://localhost/'.$dbFile.'1', $dbFile.'1']; + yield 'SQLite3 file' => ['sqlite3:///'.$dbFile.'3', $dbFile.'3']; + yield 'SQLite in memory' => ['sqlite://localhost/:memory:']; + } + + /** + * @requires extension pdo_pgsql + * + * @group integration + */ + public function testDsnWithPostgreSQL() + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + try { + $pool = new DoctrineDbalAdapter('pgsql://postgres:password@'.$host); + + $item = $pool->getItem('key'); + $item->set('value'); + $this->assertTrue($pool->save($item)); + } finally { + $pdo = new \PDO('pgsql:host='.$host.';user=postgres;password=password'); + $pdo->exec('DROP TABLE IF EXISTS cache_items'); + } } protected function isPruned(DoctrineDbalAdapter $cache, string $name): bool diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php index 6bed9285c59ac..b630e9eebea3a 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php @@ -11,11 +11,12 @@ namespace Symfony\Component\Cache\Tests\Adapter; -use PHPUnit\Framework\SkippedTestSuiteError; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\PdoAdapter; /** + * @requires extension pdo_sqlite + * * @group time-sensitive */ class PdoAdapterTest extends AdapterTestCase @@ -24,10 +25,6 @@ class PdoAdapterTest extends AdapterTestCase public static function setUpBeforeClass(): void { - if (!\extension_loaded('pdo_sqlite')) { - throw new SkippedTestSuiteError('Extension pdo_sqlite required.'); - } - self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); $pool = new PdoAdapter('sqlite:'.self::$dbFile); @@ -71,13 +68,12 @@ public function testCleanupExpiredItems() } /** - * @dataProvider provideDsn + * @dataProvider provideDsnSQLite */ - public function testDsn(string $dsn, string $file = null) + public function testDsnWithSQLite(string $dsn, string $file = null) { try { $pool = new PdoAdapter($dsn); - $pool->createTable(); $item = $pool->getItem('key'); $item->set('value'); @@ -89,11 +85,36 @@ public function testDsn(string $dsn, string $file = null) } } - public static function provideDsn() + public static function provideDsnSQLite() { $dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); - yield ['sqlite:'.$dbFile.'2', $dbFile.'2']; - yield ['sqlite::memory:']; + yield 'SQLite file' => ['sqlite:'.$dbFile.'2', $dbFile.'2']; + yield 'SQLite in memory' => ['sqlite::memory:']; + } + + /** + * @requires extension pdo_pgsql + * + * @group integration + */ + public function testDsnWithPostgreSQL() + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + $dsn = 'pgsql:host='.$host.';user=postgres;password=password'; + + try { + $pool = new PdoAdapter($dsn); + + $item = $pool->getItem('key'); + $item->set('value'); + $this->assertTrue($pool->save($item)); + } finally { + $pdo = new \PDO($dsn); + $pdo->exec('DROP TABLE IF EXISTS cache_items'); + } } protected function isPruned(PdoAdapter $cache, string $name): bool diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php index 14454d0b80b47..76e4373f83809 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -82,6 +82,7 @@ public static function createHandler($connection): AbstractSessionHandler } $connection = DriverManager::getConnection($params, $config); + // The condition should be removed once support for DBAL <3.3 is dropped $connection = method_exists($connection, 'getNativeConnection') ? $connection->getNativeConnection() : $connection->getWrappedConnection(); // no break; diff --git a/src/Symfony/Component/Lock/Store/PdoStore.php b/src/Symfony/Component/Lock/Store/PdoStore.php index 3eeb83b572e9c..159b9287d6852 100644 --- a/src/Symfony/Component/Lock/Store/PdoStore.php +++ b/src/Symfony/Component/Lock/Store/PdoStore.php @@ -115,7 +115,7 @@ public function save(Key $key) try { $stmt = $conn->prepare($sql); } catch (\PDOException $e) { - if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { $this->createTable(); } $stmt = $conn->prepare($sql); @@ -127,8 +127,18 @@ public function save(Key $key) try { $stmt->execute(); } catch (\PDOException $e) { - // the lock is already acquired. It could be us. Let's try to put off. - $this->putOffExpiration($key, $this->initialTtl); + if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) { + $this->createTable(); + + try { + $stmt->execute(); + } catch (\PDOException $e) { + $this->putOffExpiration($key, $this->initialTtl); + } + } else { + // the lock is already acquired. It could be us. Let's try to put off. + $this->putOffExpiration($key, $this->initialTtl); + } } $this->randomlyPrune(); @@ -316,4 +326,21 @@ private function getCurrentTimestampStatement(): string return (string) time(); } } + + private function isTableMissing(\PDOException $exception): bool + { + $driver = $this->getDriver(); + $code = $exception->getCode(); + + switch (true) { + case 'pgsql' === $driver && '42P01' === $code: + case 'sqlite' === $driver && str_contains($exception->getMessage(), 'no such table:'): + case 'oci' === $driver && 942 === $code: + case 'sqlsrv' === $driver && 208 === $code: + case 'mysql' === $driver && 1146 === $code: + return true; + default: + return false; + } + } } diff --git a/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php index 9f8c2aac6be3b..e037341e5f05f 100644 --- a/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php @@ -79,9 +79,9 @@ public function testAbortAfterExpiration() } /** - * @dataProvider provideDsn + * @dataProvider provideDsnWithSQLite */ - public function testDsn(string $dsn, string $file = null) + public function testDsnWithSQLite(string $dsn, string $file = null) { $key = new Key(uniqid(__METHOD__, true)); @@ -97,12 +97,36 @@ public function testDsn(string $dsn, string $file = null) } } - public static function provideDsn() + public static function provideDsnWithSQLite() { $dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); - yield ['sqlite://localhost/'.$dbFile.'1', $dbFile.'1']; - yield ['sqlite3:///'.$dbFile.'3', $dbFile.'3']; - yield ['sqlite://localhost/:memory:']; + yield 'SQLite file' => ['sqlite://localhost/'.$dbFile.'1', $dbFile.'1']; + yield 'SQLite3 file' => ['sqlite3:///'.$dbFile.'3', $dbFile.'3']; + yield 'SQLite in memory' => ['sqlite://localhost/:memory:']; + } + + /** + * @requires extension pdo_pgsql + * + * @group integration + */ + public function testDsnWithPostgreSQL() + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + $key = new Key(uniqid(__METHOD__, true)); + + try { + $store = new DoctrineDbalStore('pgsql://postgres:password@'.$host); + + $store->save($key); + $this->assertTrue($store->exists($key)); + } finally { + $pdo = new \PDO('pgsql:host='.$host.';user=postgres;password=password'); + $pdo->exec('DROP TABLE IF EXISTS lock_keys'); + } } /** diff --git a/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php index 0dc4eb015bafd..d2960d08bf274 100644 --- a/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php @@ -20,8 +20,6 @@ * @author Jérémy Derussé * * @requires extension pdo_sqlite - * - * @group integration */ class PdoStoreTest extends AbstractStoreTestCase { @@ -78,9 +76,9 @@ public function testInvalidTtlConstruct() } /** - * @dataProvider provideDsn + * @dataProvider provideDsnWithSQLite */ - public function testDsn(string $dsn, string $file = null) + public function testDsnWithSQLite(string $dsn, string $file = null) { $key = new Key(uniqid(__METHOD__, true)); @@ -96,10 +94,36 @@ public function testDsn(string $dsn, string $file = null) } } - public static function provideDsn() + public static function provideDsnWithSQLite() { $dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); - yield ['sqlite:'.$dbFile.'2', $dbFile.'2']; - yield ['sqlite::memory:']; + yield 'SQLite file' => ['sqlite:'.$dbFile.'2', $dbFile.'2']; + yield 'SQLite in memory' => ['sqlite::memory:']; + } + + /** + * @requires extension pdo_pgsql + * + * @group integration + */ + public function testDsnWithPostgreSQL() + { + if (!$host = getenv('POSTGRES_HOST')) { + $this->markTestSkipped('Missing POSTGRES_HOST env variable'); + } + + $key = new Key(uniqid(__METHOD__, true)); + + $dsn = 'pgsql:host='.$host.';user=postgres;password=password'; + + try { + $store = new PdoStore($dsn); + + $store->save($key); + $this->assertTrue($store->exists($key)); + } finally { + $pdo = new \PDO($dsn); + $pdo->exec('DROP TABLE IF EXISTS lock_keys'); + } } } diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php index 3691a9383f293..4d0c3f422971d 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php @@ -64,6 +64,7 @@ public function get(): ?array // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS $this->executeStatement(sprintf('LISTEN "%s"', $this->configuration['table_name'])); + // The condition should be removed once support for DBAL <3.3 is dropped if (method_exists($this->driverConnection, 'getNativeConnection')) { $wrappedConnection = $this->driverConnection->getNativeConnection(); } else { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy