From ec16c82f20be1a7224e65ac67144a29199f87959 Mon Sep 17 00:00:00 2001 From: Peter Mein Date: Thu, 16 Jan 2025 09:28:55 +0100 Subject: [PATCH] bugfix: deallocate mysqli prepared statement (#6681) Long running processes might hit the `max_prepared_stmt_count` due not deallocating the statement correctly. | Q | A |------------- | ----------- | Type | bug | Fixed issues | - #### Summary For postgres there was an implementation to deallocate prepared statements to prevent hitting a any limits. This was not the case for the `mysqli` drivers. Which can suffer from the same problem. In this PR a `mysqli_stmt_close` is added to properly cleanup any open prepared statement. A similar approach was implemented for PostgreSQL in this PR https://github.com/doctrine/dbal/pull/5893 --------- Co-authored-by: Alexander M. Turek --- src/Driver/Mysqli/Result.php | 18 ++++- src/Driver/Mysqli/Statement.php | 7 +- .../Driver/Mysqli/StatementTest.php | 68 +++++++++++++++++++ 3 files changed, 89 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..e327afefdd4 --- /dev/null +++ b/tests/Functional/Driver/Mysqli/StatementTest.php @@ -0,0 +1,68 @@ +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->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'); + } + + 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(); + } + } +}