From 0224a3a7a80d8b1d502965f4aaf7c03f6fdc8d23 Mon Sep 17 00:00:00 2001 From: Peter Mein Date: Thu, 2 Jan 2025 16:43:01 +0100 Subject: [PATCH 1/2] bugfix: deallocate mysqli prepared statement Long running processes might hit the `max_prepared_stmt_count` due not deallocating the statement correctly. --- src/Driver/Mysqli/Result.php | 18 +++++- src/Driver/Mysqli/Statement.php | 7 ++- .../Driver/Mysqli/StatementTest.php | 57 +++++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/Functional/Driver/Mysqli/StatementTest.php diff --git a/src/Driver/Mysqli/Result.php b/src/Driver/Mysqli/Result.php index c7dc65d1db5..bfd558b4fbc 100644 --- a/src/Driver/Mysqli/Result.php +++ b/src/Driver/Mysqli/Result.php @@ -20,6 +20,15 @@ final class Result implements ResultInterface { private mysqli_stmt $statement; + /** + * Maintains a reference to the Statement that generated this result. This ensures that the lifetime of the + * Statement is managed in conjunction with its associated results, so they are destroyed together + * at the appropriate time {@see Statement::__destruct()}. + * + * @phpstan-ignore property.onlyWritten + */ + private ?Statement $statementReference = null; + /** * Whether the statement result has columns. The property should be used only after the result metadata * has been fetched ({@see $metadataFetched}). Otherwise, the property value is undetermined. @@ -42,9 +51,12 @@ final class Result implements ResultInterface * * @throws Exception */ - public function __construct(mysqli_stmt $statement) - { - $this->statement = $statement; + public function __construct( + mysqli_stmt $statement, + ?Statement $statementReference = null + ) { + $this->statement = $statement; + $this->statementReference = $statementReference; $meta = $statement->result_metadata(); diff --git a/src/Driver/Mysqli/Statement.php b/src/Driver/Mysqli/Statement.php index a72a4f58918..be555e3dd09 100644 --- a/src/Driver/Mysqli/Statement.php +++ b/src/Driver/Mysqli/Statement.php @@ -61,6 +61,11 @@ public function __construct(mysqli_stmt $stmt) $this->boundValues = array_fill(1, $paramCount, null); } + public function __destruct() + { + @$this->stmt->close(); + } + /** * @deprecated Use {@see bindValue()} instead. * @@ -159,7 +164,7 @@ public function execute($params = null): ResultInterface throw StatementError::new($this->stmt); } - return new Result($this->stmt); + return new Result($this->stmt, $this); } /** diff --git a/tests/Functional/Driver/Mysqli/StatementTest.php b/tests/Functional/Driver/Mysqli/StatementTest.php new file mode 100644 index 00000000000..03c99694d27 --- /dev/null +++ b/tests/Functional/Driver/Mysqli/StatementTest.php @@ -0,0 +1,57 @@ +connection->prepare('SELECT 1'); + + $property = new ReflectionProperty(WrapperStatement::class, 'stmt'); + $property->setAccessible(true); + + $driverStatement = $property->getValue($statement); + + $mysqliProperty = new ReflectionProperty(Statement::class, 'stmt'); + $mysqliProperty->setAccessible(true); + + $mysqliStatement = $mysqliProperty->getValue($driverStatement); + + unset($statement, $driverStatement); + + + if (PHP_VERSION_ID < 80000) { + $this->expectError(); + $this->expectErrorMessage('mysqli_stmt::execute(): Couldn\'t fetch mysqli_stmt'); + } else { + $this->expectException(Error::class); + $this->expectExceptionMessage('mysqli_stmt object is already closed'); + } + + $mysqliStatement->execute(); + } +} From 845ca168b77856fd2f3fb29285d2214d92fd27cb Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 16 Jan 2025 09:19:51 +0100 Subject: [PATCH 2/2] Fix test --- .../Driver/Mysqli/StatementTest.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/Functional/Driver/Mysqli/StatementTest.php b/tests/Functional/Driver/Mysqli/StatementTest.php index 03c99694d27..e327afefdd4 100644 --- a/tests/Functional/Driver/Mysqli/StatementTest.php +++ b/tests/Functional/Driver/Mysqli/StatementTest.php @@ -9,8 +9,12 @@ use Doctrine\DBAL\Tests\FunctionalTestCase; use Doctrine\DBAL\Tests\TestUtil; use Error; +use ErrorException; use ReflectionProperty; +use function restore_error_handler; +use function set_error_handler; + use const PHP_VERSION_ID; /** @requires extension mysqli */ @@ -43,15 +47,22 @@ public function testStatementsAreDeallocatedProperly(): void unset($statement, $driverStatement); - if (PHP_VERSION_ID < 80000) { - $this->expectError(); - $this->expectErrorMessage('mysqli_stmt::execute(): Couldn\'t fetch mysqli_stmt'); + $this->expectException(ErrorException::class); + $this->expectExceptionMessage('mysqli_stmt::execute(): Couldn\'t fetch mysqli_stmt'); } else { $this->expectException(Error::class); $this->expectExceptionMessage('mysqli_stmt object is already closed'); } - $mysqliStatement->execute(); + set_error_handler(static function (int $errorNumber, string $error, string $file, int $line): void { + throw new ErrorException($error, 0, $errorNumber, $file, $line); + }); + + try { + $mysqliStatement->execute(); + } finally { + restore_error_handler(); + } } }