diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 542883ec591..34db89b56b5 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -37,10 +37,8 @@ jobs: os: - "ubuntu-22.04" php-version: - - "8.1" - - "8.2" - - "8.3" - "8.4" + - "8.5" dependencies: - "highest" extension: @@ -101,15 +99,15 @@ jobs: strategy: matrix: php-version: - - "8.1" - - "8.2" - - "8.3" + - "8.4" oracle-version: - "18" - "21" - "23" include: - - php-version: "8.4" + - php-version: "8.1" + oracle-version: "23" + - php-version: "8.5" oracle-version: "23" services: @@ -161,15 +159,15 @@ jobs: strategy: matrix: php-version: - - "8.1" - - "8.2" - - "8.3" + - "8.4" oracle-version: - "18" - "21" - "23" include: - - php-version: "8.4" + - php-version: "8.1" + oracle-version: "23" + - php-version: "8.5" oracle-version: "23" services: @@ -221,7 +219,7 @@ jobs: strategy: matrix: php-version: - - "8.1" + - "8.4" postgres-version: - "12" - "16" @@ -230,16 +228,16 @@ jobs: - "pgsql" - "pdo_pgsql" include: - - php-version: "8.2" + - php-version: "8.1" postgres-version: "17" extension: "pgsql" - - php-version: "8.3" + - php-version: "8.1" postgres-version: "17" - extension: "pgsql" - - php-version: "8.4" + extension: "pdo_pgsql" + - php-version: "8.5" postgres-version: "17" extension: "pgsql" - - php-version: "8.4" + - php-version: "8.5" postgres-version: "17" extension: "pdo_pgsql" @@ -291,30 +289,27 @@ jobs: strategy: matrix: php-version: - - "8.1" + - "8.4" mariadb-version: # keep in sync with https://mariadb.org/about/#maintenance-policy - - "10.5" # Oldest version supported by DBAL, LTS (Jun 2025) - - "10.6" # LTS (Jul 2026) - - "10.11" # LTS (Feb 2028) - - "11.1" # STS (Aug 2024) - - "11.2" # STS (Nov 2024) - - "11.3" # STS (Feb 2025) + - "10.5" # Oldest version supported by DBAL, LTS (Jun 2025) We have code specific to 10.5.2-10.6.0 + - "10.6" # LTS (Jul 2026) We have code specific to 10.6.0-10.10.0 + - "10.11" # LTS (Feb 2028) We have code specific to ^10.10 - "11.4" # LTS (May 2029) extension: - "mysqli" - "pdo_mysql" include: - - php-version: "8.2" + - php-version: "8.1" mariadb-version: "11.4" extension: "mysqli" - - php-version: "8.3" + - php-version: "8.1" mariadb-version: "11.4" extension: "pdo_mysql" - - php-version: "8.4" + - php-version: "8.5" mariadb-version: "11.4" extension: "mysqli" - - php-version: "8.4" + - php-version: "8.5" mariadb-version: "11.4" extension: "pdo_mysql" @@ -367,7 +362,7 @@ jobs: strategy: matrix: php-version: - - "8.3" + - "8.4" mysql-version: - "8.0" - "9.1" @@ -379,29 +374,15 @@ jobs: include: - config-file-suffix: "-tls" php-version: "8.1" - mysql-version: "8.0" - extension: "mysqli" - - php-version: "8.1" - mysql-version: "8.0" + mysql-version: "9.1" extension: "mysqli" - php-version: "8.1" - mysql-version: "8.0" - extension: "pdo_mysql" - # Workaround for https://bugs.mysql.com/114876 - - php-version: "8.3" - mysql-version: "8.4" + mysql-version: "9.1" extension: "mysqli" - custom-entrypoint: >- - --entrypoint sh mysql:8.4 -c "exec docker-entrypoint.sh mysqld --mysql-native-password=ON" - - php-version: "8.3" - mysql-version: "8.4" - extension: "pdo_mysql" - custom-entrypoint: >- - --entrypoint sh mysql:8.4 -c "exec docker-entrypoint.sh mysqld --mysql-native-password=ON" - - php-version: "8.4" + - php-version: "8.5" mysql-version: "9.1" extension: "mysqli" - - php-version: "8.4" + - php-version: "8.5" mysql-version: "9.1" extension: "pdo_mysql" @@ -459,9 +440,8 @@ jobs: matrix: php-version: - "8.1" - - "8.2" - - "8.3" - "8.4" + - "8.5" extension: - "sqlsrv" - "pdo_sqlsrv" @@ -527,9 +507,8 @@ jobs: matrix: php-version: - "8.1" - - "8.2" - - "8.3" - "8.4" + - "8.5" services: ibm_db2: diff --git a/docs/en/reference/configuration.rst b/docs/en/reference/configuration.rst index 495a38a8aba..38cb6bdd76d 100644 --- a/docs/en/reference/configuration.rst +++ b/docs/en/reference/configuration.rst @@ -324,6 +324,7 @@ pdo_oci / oci8 - ``driverOptions`` (array): - ``exclusive`` (boolean): Once specified for an ``oci8`` connection, forces the driver to always establish a new connection instead of reusing an existing one from the connection pool. + - ``protocol`` (string): The protocol used to connect to Oracle (default TCP, Autonomous tends to be TCPS). See `docs.oracle.com/en/enterprise-manager/cloud-control/enterprise-manager-cloud-control/13.4/emsec/secured-communication-tcps-access-databases.html `_. pdo_sqlsrv / sqlsrv ^^^^^^^^^^^^^^^^^^^ diff --git a/src/Driver/AbstractOracleDriver/EasyConnectString.php b/src/Driver/AbstractOracleDriver/EasyConnectString.php index a7778176ce5..be85d5f63ad 100644 --- a/src/Driver/AbstractOracleDriver/EasyConnectString.php +++ b/src/Driver/AbstractOracleDriver/EasyConnectString.php @@ -74,7 +74,7 @@ public static function fromConnectionParameters(array $params): self return self::fromArray([ 'DESCRIPTION' => [ 'ADDRESS' => [ - 'PROTOCOL' => 'TCP', + 'PROTOCOL' => $params['driverOptions']['protocol'] ?? 'TCP', 'HOST' => $params['host'], 'PORT' => $params['port'] ?? 1521, ], diff --git a/src/Driver/OCI8/Connection.php b/src/Driver/OCI8/Connection.php index 8334cb94171..64c210c4989 100644 --- a/src/Driver/OCI8/Connection.php +++ b/src/Driver/OCI8/Connection.php @@ -47,15 +47,21 @@ public function getServerVersion(): string return $matches[1]; } - /** @throws Parser\Exception */ + /** + * @throws Parser\Exception + * @throws Error + */ public function prepare(string $sql): Statement { $visitor = new ConvertPositionalToNamedPlaceholders(); $this->parser->parse($sql, $visitor); - $statement = oci_parse($this->connection, $visitor->getSQL()); - assert(is_resource($statement)); + $statement = @oci_parse($this->connection, $visitor->getSQL()); + + if (! is_resource($statement)) { + throw Error::new($this->connection); + } return new Statement($this->connection, $statement, $visitor->getParameterMap(), $this->executionMode); } diff --git a/src/Driver/PDO/PDOConnect.php b/src/Driver/PDO/PDOConnect.php index f14a97c07aa..742298dd072 100644 --- a/src/Driver/PDO/PDOConnect.php +++ b/src/Driver/PDO/PDOConnect.php @@ -18,8 +18,7 @@ private function doConnect( string $password, array $options, ): PDO { - // see https://github.com/php/php-src/issues/16314 - if (PHP_VERSION_ID < 80400 || ($options[PDO::ATTR_PERSISTENT] ?? false) === true) { + if (PHP_VERSION_ID < 80400) { return new PDO($dsn, $username, $password, $options); } diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index 7555fe217e1..c80a4a6ac4b 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -50,6 +50,7 @@ use function is_float; use function is_int; use function is_string; +use function key; use function max; use function mb_strlen; use function preg_quote; @@ -195,7 +196,11 @@ public function getEnumDeclarationSQL(array $column): string throw ColumnValuesRequired::new($this, 'ENUM'); } - return $this->getStringTypeDeclarationSQL(['length' => max(...array_map(mb_strlen(...), $column['values']))]); + $length = count($column['values']) > 1 + ? max(...array_map(mb_strlen(...), $column['values'])) + : mb_strlen($column['values'][key($column['values'])]); + + return $this->getStringTypeDeclarationSQL(['length' => $length]); } /** diff --git a/tests/Driver/AbstractOracleDriver/EasyConnectStringTest.php b/tests/Driver/AbstractOracleDriver/EasyConnectStringTest.php index 2f7a067bbe0..e12d272313e 100644 --- a/tests/Driver/AbstractOracleDriver/EasyConnectStringTest.php +++ b/tests/Driver/AbstractOracleDriver/EasyConnectStringTest.php @@ -57,6 +57,18 @@ public static function connectionParametersProvider(): iterable '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=41521))' . '(CONNECT_DATA=(SID=XE)(INSTANCE_NAME=SALES)(SERVER=POOLED)))', ], + 'tcps-params' => [ + [ + 'host' => 'localhost', + 'port' => 41521, + 'dbname' => 'XE', + 'instancename' => 'SALES', + 'pooled' => true, + 'driverOptions' => ['protocol' => 'TCPS'], + ], + '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCPS)(HOST=localhost)(PORT=41521))' + . '(CONNECT_DATA=(SID=XE)(INSTANCE_NAME=SALES)(SERVER=POOLED)))', + ], ]; } } diff --git a/tests/Functional/Driver/OCI8/ConnectionTest.php b/tests/Functional/Driver/OCI8/ConnectionTest.php new file mode 100644 index 00000000000..11ba84e65dc --- /dev/null +++ b/tests/Functional/Driver/OCI8/ConnectionTest.php @@ -0,0 +1,68 @@ +markConnectionNotReusable(); + $this->killCurrentSession(); + + $this->expectException(DriverException::class); + + $this->connection->prepare('SELECT * FROM 1'); + } + + /** + * Kill the current session, by using another connection + * Oracle doesn't allow you to terminate the current session, so we use a second connection + */ + private function killCurrentSession(): void + { + $row = $this->connection->fetchNumeric( + <<<'SQL' +SELECT SID, SERIAL# +FROM V$SESSION +WHERE AUDSID = USERENV('SESSIONID') +SQL, + ); + + self::assertNotFalse($row); + /** @psalm-suppress PossiblyUndefinedArrayOffset */ + [$sid, $serialNumber] = $row; + + self::assertNotNull($sid, 'SID is missing.'); + self::assertNotNull($serialNumber, 'Serial number is missing.'); + + $params = TestUtil::getConnectionParams(); + $params['driverOptions']['exclusive'] = true; + $secondConnection = DriverManager::getConnection($params); + + $sessionParam = $this->connection->quote($sid . ', ' . $serialNumber); + $secondConnection->executeStatement('ALTER SYSTEM DISCONNECT SESSION ' . $sessionParam . ' IMMEDIATE'); + + // Ensure OCI driver is aware of connection state change by executing any statement + try { + $this->connection->executeStatement('INVALID SQL'); + } catch (Throwable) { + } + } +} diff --git a/tests/Platforms/AbstractMySQLPlatformTestCase.php b/tests/Platforms/AbstractMySQLPlatformTestCase.php index 8a7db6bc7e7..b2738b45740 100644 --- a/tests/Platforms/AbstractMySQLPlatformTestCase.php +++ b/tests/Platforms/AbstractMySQLPlatformTestCase.php @@ -701,4 +701,13 @@ protected function createComparator(): Comparator new ComparatorConfig(), ); } + + /** @return array, string}> */ + public static function getEnumDeclarationSQLProvider(): array + { + return [ + 'single value' => [['foo'], "ENUM('foo')"], + 'multiple values' => [['foo', 'bar1'], "ENUM('foo', 'bar1')"], + ]; + } } diff --git a/tests/Platforms/AbstractPlatformTestCase.php b/tests/Platforms/AbstractPlatformTestCase.php index 659c45d76c3..2ff1b704b93 100644 --- a/tests/Platforms/AbstractPlatformTestCase.php +++ b/tests/Platforms/AbstractPlatformTestCase.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\InvalidColumnDeclaration; +use Doctrine\DBAL\Exception\InvalidColumnType\ColumnValuesRequired; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\ColumnDiff; @@ -1019,4 +1020,38 @@ public static function asciiStringSqlDeclarationDataProvider(): array ['CHAR(12)', ['length' => 12, 'fixed' => true]], ]; } + + /** @param array $values */ + #[DataProvider('getEnumDeclarationSQLProvider')] + public function testGetEnumDeclarationSQL(array $values, string $expectedSQL): void + { + self::assertSame($expectedSQL, $this->platform->getEnumDeclarationSQL(['values' => $values])); + } + + /** @return array, string}> */ + public static function getEnumDeclarationSQLProvider(): array + { + return [ + 'single value' => [['foo'], 'VARCHAR(3)'], + 'multiple values' => [['foo', 'bar1'], 'VARCHAR(4)'], + ]; + } + + /** @param array $column */ + #[DataProvider('getEnumDeclarationSQLWithInvalidValuesProvider')] + public function testGetEnumDeclarationSQLWithInvalidValues(array $column): void + { + self::expectException(ColumnValuesRequired::class); + $this->platform->getEnumDeclarationSQL($column); + } + + /** @return array}> */ + public static function getEnumDeclarationSQLWithInvalidValuesProvider(): array + { + return [ + "field 'values' does not exist" => [[]], + "field 'values' is not an array" => [['values' => 'foo']], + "field 'values' is an empty array" => [['values' => []]], + ]; + } } diff --git a/tests/Platforms/OraclePlatformTest.php b/tests/Platforms/OraclePlatformTest.php index 70b63626f8f..66e1bb85c6e 100644 --- a/tests/Platforms/OraclePlatformTest.php +++ b/tests/Platforms/OraclePlatformTest.php @@ -596,4 +596,13 @@ public static function asciiStringSqlDeclarationDataProvider(): array ['CHAR(12)', ['length' => 12, 'fixed' => true]], ]; } + + /** @return array, string}> */ + public static function getEnumDeclarationSQLProvider(): array + { + return [ + 'single value' => [['foo'], 'VARCHAR2(3)'], + 'multiple values' => [['foo', 'bar1'], 'VARCHAR2(4)'], + ]; + } } diff --git a/tests/Platforms/SQLServerPlatformTest.php b/tests/Platforms/SQLServerPlatformTest.php index f6c6d90d005..f14ffba7550 100644 --- a/tests/Platforms/SQLServerPlatformTest.php +++ b/tests/Platforms/SQLServerPlatformTest.php @@ -1134,4 +1134,13 @@ public function testGeneratesTypeDeclarationForDateTimeTz(): void { self::assertEquals('DATETIMEOFFSET(6)', $this->platform->getDateTimeTzTypeDeclarationSQL([])); } + + /** @return array, string}> */ + public static function getEnumDeclarationSQLProvider(): array + { + return [ + 'single value' => [['foo'], 'NVARCHAR(3)'], + 'multiple values' => [['foo', 'bar1'], 'NVARCHAR(4)'], + ]; + } }