Skip to content

Commit 1a82717

Browse files
authored
Merge pull request #6665 from fpoy/FEAT_deallocate_prepared_statements
Deallocate prepared statements
2 parents 258bc3e + 1a72ba8 commit 1a82717

File tree

11 files changed

+109
-23
lines changed

11 files changed

+109
-23
lines changed

phpstan-baseline.neon.dist

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ parameters:
216216
path: system/Database/MySQLi/PreparedQuery.php
217217

218218
-
219-
message: "#^Property CodeIgniter\\\\Database\\\\BasePreparedQuery\\:\\:\\$statement \\(object\\|resource\\) in isset\\(\\) is not nullable\\.$#"
219+
message: "#^Cannot call method close\\(\\) on object\\|resource\\.$#"
220220
count: 1
221221
path: system/Database/MySQLi/PreparedQuery.php
222222

@@ -265,11 +265,6 @@ parameters:
265265
count: 1
266266
path: system/Database/Postgre/Connection.php
267267

268-
-
269-
message: "#^Property CodeIgniter\\\\Database\\\\BasePreparedQuery\\:\\:\\$statement \\(object\\|resource\\) in isset\\(\\) is not nullable\\.$#"
270-
count: 1
271-
path: system/Database/Postgre/PreparedQuery.php
272-
273268
-
274269
message: "#^Access to an undefined property CodeIgniter\\\\Database\\\\BaseConnection\\:\\:\\$schema\\.$#"
275270
count: 2
@@ -280,11 +275,6 @@ parameters:
280275
count: 13
281276
path: system/Database/SQLSRV/Forge.php
282277

283-
-
284-
message: "#^Property CodeIgniter\\\\Database\\\\BasePreparedQuery\\:\\:\\$statement \\(object\\|resource\\) in isset\\(\\) is not nullable\\.$#"
285-
count: 1
286-
path: system/Database/SQLSRV/PreparedQuery.php
287-
288278
-
289279
message: "#^Cannot call method changes\\(\\) on bool\\|object\\|resource\\.$#"
290280
count: 1
@@ -351,7 +341,7 @@ parameters:
351341
path: system/Database/SQLite3/PreparedQuery.php
352342

353343
-
354-
message: "#^Property CodeIgniter\\\\Database\\\\BasePreparedQuery\\:\\:\\$statement \\(object\\|resource\\) in isset\\(\\) is not nullable\\.$#"
344+
message: "#^Cannot call method close\\(\\) on object\\|resource\\.$#"
355345
count: 1
356346
path: system/Database/SQLite3/PreparedQuery.php
357347

system/Database/BasePreparedQuery.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ abstract class BasePreparedQuery implements PreparedQueryInterface
2525
/**
2626
* The prepared statement itself.
2727
*
28-
* @var object|resource
28+
* @var object|resource|null
2929
*/
3030
protected $statement;
3131

@@ -147,17 +147,28 @@ abstract public function _execute(array $data): bool;
147147
abstract public function _getResult();
148148

149149
/**
150-
* Explicitly closes the statement.
150+
* Explicitly closes the prepared statement.
151+
*
152+
* @throws BadMethodCallException
151153
*/
152-
public function close()
154+
public function close(): bool
153155
{
154-
if (! is_object($this->statement) || ! method_exists($this->statement, 'close')) {
155-
return;
156+
if (! isset($this->statement)) {
157+
throw new BadMethodCallException('Cannot call close on a non-existing prepared statement.');
156158
}
157159

158-
$this->statement->close();
160+
try {
161+
return $this->_close();
162+
} finally {
163+
$this->statement = null;
164+
}
159165
}
160166

167+
/**
168+
* The database-dependent version of the close method.
169+
*/
170+
abstract protected function _close(): bool;
171+
161172
/**
162173
* Returns the SQL that has been prepared.
163174
*/

system/Database/MySQLi/PreparedQuery.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,12 @@ public function _getResult()
8989
{
9090
return $this->statement->get_result();
9191
}
92+
93+
/**
94+
* Deallocate prepared statements
95+
*/
96+
protected function _close(): bool
97+
{
98+
return $this->statement->close();
99+
}
92100
}

system/Database/OCI8/PreparedQuery.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function _prepare(string $sql, array $options = [])
6868
*/
6969
public function _execute(array $data): bool
7070
{
71-
if (null === $this->statement) {
71+
if (! isset($this->statement)) {
7272
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
7373
}
7474

@@ -98,6 +98,14 @@ public function _getResult()
9898
return $this->statement;
9999
}
100100

101+
/**
102+
* Deallocate prepared statements
103+
*/
104+
protected function _close(): bool
105+
{
106+
return oci_free_statement($this->statement);
107+
}
108+
101109
/**
102110
* Replaces the ? placeholders with :0, :1, etc parameters for use
103111
* within the prepared query.

system/Database/Postgre/PreparedQuery.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ public function _getResult()
9898
return $this->result;
9999
}
100100

101+
/**
102+
* Deallocate prepared statements
103+
*/
104+
protected function _close(): bool
105+
{
106+
return pg_query($this->db->connID, 'DEALLOCATE "' . $this->db->escapeIdentifiers($this->name) . '"') !== false;
107+
}
108+
101109
/**
102110
* Replaces the ? placeholders with $1, $2, etc parameters for use
103111
* within the prepared query.

system/Database/SQLSRV/PreparedQuery.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ public function _getResult()
116116
return $this->result;
117117
}
118118

119+
/**
120+
* Deallocate prepared statements
121+
*/
122+
protected function _close(): bool
123+
{
124+
return sqlsrv_free_stmt($this->statement);
125+
}
126+
119127
/**
120128
* Handle parameters
121129
*/

system/Database/SQLite3/PreparedQuery.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ public function _prepare(string $sql, array $options = [])
5656
/**
5757
* Takes a new set of data and runs it against the currently
5858
* prepared query. Upon success, will return a Results object.
59-
*
60-
* @todo finalize()
6159
*/
6260
public function _execute(array $data): bool
6361
{
@@ -93,4 +91,12 @@ public function _getResult()
9391
{
9492
return $this->result;
9593
}
94+
95+
/**
96+
* Deallocate prepared statements
97+
*/
98+
protected function _close(): bool
99+
{
100+
return $this->statement->close();
101+
}
96102
}

tests/system/Database/Live/PreparedQueryTest.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace CodeIgniter\Database\Live;
1313

14+
use BadMethodCallException;
1415
use CodeIgniter\Database\BasePreparedQuery;
1516
use CodeIgniter\Database\Exceptions\DatabaseException;
1617
use CodeIgniter\Database\Query;
@@ -41,8 +42,10 @@ protected function tearDown(): void
4142
{
4243
parent::tearDown();
4344

44-
if ($this->query !== null) {
45+
try {
4546
$this->query->close();
47+
} catch (BadMethodCallException $e) {
48+
$this->query = null;
4649
}
4750
}
4851

@@ -148,4 +151,38 @@ public function testExecuteRunsInvalidQuery()
148151

149152
$this->query->execute('foo', '[email protected]', 'US');
150153
}
154+
155+
public function testDeallocatePreparedQueryThenTryToExecute()
156+
{
157+
$this->query = $this->db->prepare(static fn ($db) => $db->table('user')->insert([
158+
'name' => 'a',
159+
'email' => '[email protected]',
160+
'country' => 'x',
161+
]));
162+
163+
$this->query->close();
164+
165+
// Try to execute a non-existing prepared statement
166+
$this->expectException(BadMethodCallException::class);
167+
$this->expectExceptionMessage('You must call prepare before trying to execute a prepared statement.');
168+
169+
$this->query->execute('bar', '[email protected]', 'GB');
170+
}
171+
172+
public function testDeallocatePreparedQueryThenTryToClose()
173+
{
174+
$this->query = $this->db->prepare(static fn ($db) => $db->table('user')->insert([
175+
'name' => 'a',
176+
'email' => '[email protected]',
177+
'country' => 'x',
178+
]));
179+
180+
$this->query->close();
181+
182+
// Try to close a non-existing prepared statement
183+
$this->expectException(BadMethodCallException::class);
184+
$this->expectExceptionMessage('Cannot call close on a non-existing prepared statement.');
185+
186+
$this->query->close();
187+
}
151188
}

user_guide_src/source/changelogs/v4.3.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ The return value of ``Validation::loadRuleGroup()`` has been changed ``null`` t
9090
Others
9191
------
9292

93+
- The return type of ``CodeIgniter\Database\BasePreparedQuery::close()`` has been changed to ``bool``.
9394
- The return type of ``CodeIgniter\Database\Database::loadForge()`` has been changed to ``Forge``.
9495
- The return type of ``CodeIgniter\Database\Database::loadUtils()`` has been changed to ``BaseUtils``.
9596
- Parameter ``$column`` has changed in ``Table::dropForeignKey()`` to ``$foreignName``.
@@ -142,6 +143,7 @@ Database
142143
- Added the ability to manually set index names. These methods include: ``Forge::addKey()``, ``Forge::addPrimaryKey()``, and ``Forge::addUniqueKey()``
143144
- Fixed ``Forge::dropKey()`` to allow droping unique indexes. This required the ``DROP CONSTRAINT`` SQL command.
144145
- Added ``upsert()`` and ``upsertBatch()`` methods to QueryBuilder. See :ref:`upsert-data`.
146+
- ``BasePreparedQuery::close()`` now deallocates the prepared statement in all DBMS. Previously, they were not deallocated in Postgre, SQLSRV and OCI8. See :ref:`database-queries-stmt-close`.
145147

146148
Model
147149
=====

user_guide_src/source/database/queries.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ Other Methods
232232

233233
In addition to these two primary methods, the prepared query object also has the following methods:
234234

235+
.. _database-queries-stmt-close:
236+
235237
close()
236238
-------
237239

@@ -240,6 +242,8 @@ close out the prepared statement when you're done with it:
240242

241243
.. literalinclude:: queries/020.php
242244

245+
.. note:: Since v4.3.0, the ``close()`` method deallocates the prepared statement in all DBMS. Previously, they were not deallocated in Postgre, SQLSRV and OCI8.
246+
243247
getQueryString()
244248
----------------
245249

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
<?php
22

3-
$pQuery->close();
3+
if ($pQuery->close()) {
4+
echo 'Success!';
5+
} else {
6+
echo 'Deallocation of prepared statements failed!';
7+
}

0 commit comments

Comments
 (0)