Skip to content

Commit 5c561a6

Browse files
authored
Merge pull request #172 from simon-mundy/171-rfc-split-adapter-query
Split Adapter::query() into prepareQuery() and executeQuery()
2 parents e037464 + 6b1b072 commit 5c561a6

8 files changed

Lines changed: 344 additions & 70 deletions

File tree

‎docs/book/adapter.md‎

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,33 @@ class Adapter implements AdapterInterface, Profiler\ProfilerAwareInterface, Sche
7878
public function getQueryResultSetPrototype(): ResultSet\ResultSetInterface;
7979
public function getCurrentSchema(): string|false;
8080

81+
/** @deprecated Use prepareQuery() and executeQuery() instead. */
8182
public function query(
8283
string $sql,
8384
ParameterContainer|array|string $parametersOrQueryMode = self::QUERY_MODE_PREPARE,
8485
?ResultSet\ResultSetInterface $resultPrototype = null
8586
): Driver\StatementInterface|ResultSet\ResultSet|Driver\ResultInterface;
8687

88+
public function prepareQuery(
89+
string $sql,
90+
ParameterContainer|array $parameters = []
91+
): Driver\StatementInterface;
92+
93+
public function executeQuery(
94+
string|Driver\StatementInterface $sql
95+
): Driver\ResultInterface;
96+
8797
public function createStatement(
8898
?string $initialSql = null,
8999
ParameterContainer|array|null $initialParameters = null
90100
): Driver\StatementInterface;
91101
}
92102
```
93103

104+
> **Note:** `prepareQuery()` and `executeQuery()` are currently declared on
105+
> the `Adapter` class only; `AdapterInterface` still declares `query()` alone
106+
> to avoid breaking existing implementors during the 0.x series.
107+
94108
### Constructor Parameters
95109

96110
- **`$driver`**: A `DriverInterface` implementation from a driver package
@@ -104,51 +118,85 @@ class Adapter implements AdapterInterface, Profiler\ProfilerAwareInterface, Sche
104118

105119
## Query Preparation
106120

107-
By default, `PhpDb\Adapter\Adapter::query()` prefers that you use
108-
"preparation" as a means for processing SQL statements. This generally means
109-
that you will supply a SQL statement containing placeholders for the values, and
110-
separately provide substitutions for those placeholders:
121+
`PhpDb\Adapter\Adapter::prepareQuery()` prepares a SQL statement, optionally
122+
binding parameters, and always returns the prepared `Statement` without
123+
executing it:
111124

112-
```php title="Query with Prepared Statement"
113-
$adapter->query('SELECT * FROM `artist` WHERE `id` = ?', [5]);
125+
```php title="Preparing a Statement"
126+
$statement = $adapter->prepareQuery('SELECT * FROM `artist` WHERE `id` = ?', [5]);
114127
```
115128

116129
The above example will go through the following steps:
117130

118131
1. Create a new `Statement` object
119132
2. Prepare the array `[5]` into a `ParameterContainer` if necessary
120133
3. Inject the `ParameterContainer` into the `Statement` object
121-
4. Execute the `Statement` object, producing a `Result` object
122-
5. Check the `Result` object to check if the supplied SQL was a result set
123-
producing statement. If the query produced a result set, clone the
124-
`ResultSet` prototype, inject the `Result` as its datasource, and return
125-
the new `ResultSet` instance. Otherwise, return the `Result`.
134+
4. Prepare the `Statement` object and return it
135+
136+
To actually run the statement, pass it to `executeQuery()`:
137+
138+
```php title="Executing a Prepared Statement"
139+
$result = $adapter->executeQuery($statement);
140+
```
141+
142+
`executeQuery()` always returns the raw `Driver\ResultInterface` — it never
143+
wraps it. If you want the result wrapped in a `ResultSet`, check
144+
`isQueryResult()` and call `getQueryResult()` yourself:
145+
146+
```php title="Wrapping a Query Result"
147+
if ($result->isQueryResult()) {
148+
$resultSet = $result->getQueryResult();
149+
}
150+
```
151+
152+
`getQueryResult()` clones the `ResultSet` prototype you pass it (or a
153+
default prototype if you pass none), injects the `Result` as its data
154+
source, and returns the new `ResultSet` instance. See
155+
[`getQueryResult()`](#using-the-driver-object) below for details.
126156

127157
## Query Execution
128158

129-
In some cases, you have to execute statements directly without preparation. One
159+
In some cases, you have to execute SQL directly without preparation. One
130160
possible reason for doing so would be to execute a DDL statement, as most
131161
extensions and RDBMS systems are incapable of preparing such statements.
132162

133-
To execute a query without the preparation step, pass a flag as
134-
the second argument indicating execution is required:
163+
Pass the raw SQL string to `executeQuery()` to execute it without a
164+
preparation step:
135165

136166
```php title="Executing DDL Statement Without Preparation"
167+
$adapter->executeQuery(
168+
'ALTER TABLE ADD INDEX(`foo_index`) ON (`foo_column`)'
169+
);
170+
```
171+
172+
## The Deprecated query() Method
173+
174+
`Adapter::query()` predates the `prepareQuery()`/`executeQuery()` split and
175+
combines both concerns behind a single method and a stringly-typed second
176+
argument. It is deprecated in favour of the methods above but remains
177+
available, proxying to them internally, for backwards compatibility:
178+
179+
```php title="Query with Prepared Statement (deprecated)"
180+
$adapter->query('SELECT * FROM `artist` WHERE `id` = ?', [5]);
181+
```
182+
183+
```php title="Executing DDL Statement Without Preparation (deprecated)"
137184
$adapter->query(
138185
'ALTER TABLE ADD INDEX(`foo_index`) ON (`foo_column`)',
139186
Adapter::QUERY_MODE_EXECUTE
140187
);
141188
```
142189

143-
The primary difference to notice is that you must provide the
144-
`Adapter::QUERY_MODE_EXECUTE` (execute) flag as the second parameter.
190+
The primary difference to notice in the second example is that you must
191+
provide the `Adapter::QUERY_MODE_EXECUTE` (execute) flag as the second
192+
parameter.
145193

146194
## Creating Statements
147195

148-
While `query()` is highly useful for one-off and quick querying of a database
149-
via the `Adapter`, it generally makes more sense to create a statement and
150-
interact with it directly, so that you have greater control over the
151-
prepare-then-execute workflow:
196+
While `prepareQuery()` and `executeQuery()` are highly useful for one-off and
197+
quick querying of a database via the `Adapter`, it generally makes more sense
198+
to create a statement and interact with it directly, so that you have
199+
greater control over the prepare-then-execute workflow:
152200

153201
```php title="Creating and Executing a Statement"
154202
$statement = $adapter->createStatement($sql, $optionalParameters);
@@ -229,13 +277,22 @@ interface ResultInterface extends Countable, Iterator
229277
{
230278
public function buffer(): void;
231279
public function isQueryResult(): bool;
280+
public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface;
232281
public function getAffectedRows(): int;
233282
public function getGeneratedValue(): mixed;
234283
public function getResource(): mixed;
235284
public function getFieldCount(): int;
236285
}
237286
```
238287

288+
`getQueryResult()` clones the given `$resultPrototype` (or a default
289+
`ResultSet` prototype if none is given), initializes the clone from the
290+
result, and returns it. It throws `Exception\RuntimeException` if
291+
`isQueryResult()` is false. `Adapter::query()` (the deprecated BC method)
292+
delegates to this method rather than duplicating the clone-and-initialize
293+
logic itself; call it yourself when you want a `ResultSet` from
294+
`executeQuery()`'s raw `Driver\ResultInterface`.
295+
239296
## Using The Platform Object
240297

241298
The `Platform` object provides an API to assist in crafting queries in a way
@@ -442,7 +499,7 @@ $sql = 'UPDATE ' . $qi('artist')
442499
. ' SET ' . $qi('name') . ' = ' . $fp('name')
443500
. ' WHERE ' . $qi('id') . ' = ' . $fp('id');
444501

445-
$statement = $adapter->query($sql);
502+
$statement = $adapter->prepareQuery($sql);
446503

447504
$parameters = [
448505
'name' => 'Updated Artist',
@@ -452,7 +509,7 @@ $parameters = [
452509
$statement->execute($parameters);
453510

454511
// DATA UPDATED, NOW CHECK
455-
$statement = $adapter->query(
512+
$statement = $adapter->prepareQuery(
456513
'SELECT * FROM '
457514
. $qi('artist')
458515
. ' WHERE id = ' . $fp('id')

‎src/Adapter/Adapter.php‎

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
use PhpDb\ResultSet;
1010

1111
use function func_get_args;
12-
use function in_array;
1312
use function is_array;
14-
use function is_string;
1513
use function strtolower;
1614

1715
class Adapter implements AdapterInterface, Profiler\ProfilerAwareInterface, SchemaAwareInterface
@@ -73,7 +71,10 @@ public function getCurrentSchema(): string|false
7371
/**
7472
* query() is a convenience function
7573
*
74+
* @deprecated Use prepareQuery() and executeQuery() instead. query() will be removed in a future version.
75+
*
7676
* @throws Exception\InvalidArgumentException
77+
* @throws Exception\RuntimeException When execution did not produce a result.
7778
* @throws PhpException
7879
*/
7980
#[Override]
@@ -82,45 +83,69 @@ public function query(
8283
ParameterContainer|array|string $parametersOrQueryMode = self::QUERY_MODE_PREPARE,
8384
?ResultSet\ResultSetInterface $resultPrototype = null
8485
): Driver\StatementInterface|ResultSet\ResultSetInterface|Driver\ResultInterface {
85-
if (
86-
is_string($parametersOrQueryMode)
87-
&& in_array($parametersOrQueryMode, [self::QUERY_MODE_PREPARE, self::QUERY_MODE_EXECUTE])
88-
) {
89-
$mode = $parametersOrQueryMode;
90-
$parameters = null;
91-
} elseif (is_array($parametersOrQueryMode) || $parametersOrQueryMode instanceof ParameterContainer) {
92-
$mode = self::QUERY_MODE_PREPARE;
93-
$parameters = $parametersOrQueryMode;
94-
} else {
95-
throw new Exception\InvalidArgumentException(
96-
'Parameter 2 to this method must be a flag, an array, or ParameterContainer'
97-
);
86+
if ($parametersOrQueryMode === self::QUERY_MODE_PREPARE) {
87+
return $this->prepareQuery($sql);
9888
}
9989

100-
if ($mode === self::QUERY_MODE_PREPARE) {
101-
$lastPreparedStatement = $this->driver->createStatement($sql);
102-
$lastPreparedStatement->prepare();
103-
if (is_array($parameters) || $parameters instanceof ParameterContainer) {
104-
if (is_array($parameters)) {
105-
$lastPreparedStatement->setParameterContainer(new ParameterContainer($parameters));
106-
} else {
107-
$lastPreparedStatement->setParameterContainer($parameters);
108-
}
109-
$result = $lastPreparedStatement->execute();
110-
} else {
111-
return $lastPreparedStatement;
112-
}
113-
} else {
114-
$result = $this->driver->getConnection()->execute($sql);
90+
$sql = match (true) {
91+
$parametersOrQueryMode === self::QUERY_MODE_EXECUTE
92+
=> $sql,
93+
$parametersOrQueryMode instanceof ParameterContainer,
94+
is_array($parametersOrQueryMode)
95+
=> $this->prepareQuery($sql, $parametersOrQueryMode),
96+
default => throw new Exception\InvalidArgumentException(
97+
'Flag incorrectly set'
98+
),
99+
};
100+
101+
$result = $this->executeQuery($sql);
102+
103+
return $result->isQueryResult()
104+
? $result->getQueryResult($resultPrototype ?? $this->queryResultSetPrototype)
105+
: $result;
106+
}
107+
108+
/**
109+
* Prepare a statement for the given SQL, optionally binding parameters.
110+
*
111+
* Always prepares the statement; never executes it. Use executeQuery()
112+
* to run the returned statement.
113+
*/
114+
#[Override]
115+
public function prepareQuery(
116+
string $sql,
117+
ParameterContainer|array $parameters = []
118+
): Driver\StatementInterface {
119+
$statement = $this->driver->createStatement($sql);
120+
121+
if (is_array($parameters)) {
122+
$parameters = new ParameterContainer($parameters);
115123
}
116124

117-
if ($result instanceof Driver\ResultInterface && $result->isQueryResult()) {
118-
$resultSet = $resultPrototype ?? $this->queryResultSetPrototype;
119-
$resultSetCopy = clone $resultSet;
125+
$statement->setParameterContainer($parameters);
126+
$statement->prepare();
127+
128+
return $statement;
129+
}
120130

121-
$resultSetCopy->initialize($result);
131+
/**
132+
* Execute raw SQL or a prepared statement.
133+
*
134+
* Narrows the driver's execution result to a Driver\ResultInterface,
135+
* never a wrapped ResultSet. Callers can check isQueryResult() and use
136+
* getQueryResult() themselves if they want the result wrapped.
137+
*
138+
* @throws Exception\RuntimeException When execution did not produce a result.
139+
*/
140+
#[Override]
141+
public function executeQuery(Driver\StatementInterface|string $sql): Driver\ResultInterface
142+
{
143+
$result = $sql instanceof Driver\StatementInterface
144+
? $sql->execute()
145+
: $this->driver->getConnection()->execute($sql);
122146

123-
return $resultSetCopy;
147+
if (! $result instanceof Driver\ResultInterface) {
148+
throw new Exception\RuntimeException('Query execution did not produce a result');
124149
}
125150

126151
return $result;

‎src/Adapter/AdapterInterface.php‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ public function query(
4949
?ResultSet\ResultSetInterface $resultPrototype = null
5050
): Driver\StatementInterface|ResultSet\ResultSetInterface|Driver\ResultInterface;
5151

52+
/**
53+
* Prepares a statement for the given SQL without executing it.
54+
*/
55+
public function prepareQuery(
56+
string $sql,
57+
ParameterContainer|array $parameters = []
58+
): Driver\StatementInterface;
59+
60+
/**
61+
* Executes raw SQL or a prepared statement.
62+
*
63+
* @throws Exception\RuntimeException When execution did not produce a result.
64+
*/
65+
public function executeQuery(Driver\StatementInterface|string $sql): Driver\ResultInterface;
66+
5267
/**
5368
* @todo 0.3.x track down this usage!!!
5469
* @return array

‎src/Adapter/Driver/Pdo/Result.php‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use PDOStatement;
1212
use PhpDb\Adapter\Driver\ResultInterface;
1313
use PhpDb\Adapter\Exception;
14+
use PhpDb\ResultSet\ResultSet;
15+
use PhpDb\ResultSet\ResultSetInterface;
1416
// phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse
1517
use ReturnTypeWillChange;
1618

@@ -276,6 +278,28 @@ public function isQueryResult(): bool
276278
return $this->resource->columnCount() > 0;
277279
}
278280

281+
/**
282+
* {@inheritdoc}
283+
*
284+
* @throws Exception\RuntimeException
285+
*/
286+
#[Override]
287+
public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface
288+
{
289+
if (! $this->isQueryResult()) {
290+
throw new Exception\RuntimeException(
291+
'Cannot produce a query result set from a result that is not a query result;'
292+
. ' check isQueryResult() first'
293+
);
294+
}
295+
296+
$resultPrototype ??= new ResultSet();
297+
$resultSet = clone $resultPrototype;
298+
$resultSet->initialize($this);
299+
300+
return $resultSet;
301+
}
302+
279303
/**
280304
* {@inheritdoc}
281305
*/

‎src/Adapter/Driver/ResultInterface.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Countable;
88
use Iterator;
9+
use PhpDb\Adapter\Exception;
10+
use PhpDb\ResultSet\ResultSetInterface;
911

1012
interface ResultInterface extends
1113
Countable,
@@ -26,6 +28,14 @@ public function isBuffered(): ?bool;
2628
*/
2729
public function isQueryResult(): bool;
2830

31+
/**
32+
* Get the seeded query result set, cloned from $resultPrototype (or a
33+
* default prototype if none is given) and initialized from this result.
34+
*
35+
* @throws Exception\RuntimeException When isQueryResult() is false.
36+
*/
37+
public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface;
38+
2939
/**
3040
* Get affected rows
3141
*/

‎src/Sql/Select.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -762,8 +762,8 @@ public function __clone()
762762
}
763763

764764
/**
765-
* @return array{0: string, 1: string}
766-
* @phpstan-return array{0: string, 1: string}
765+
* @return array{0: string|null, 1: string}
766+
* @phpstan-return array{0: string|null, 1: string}
767767
*/
768768
protected function resolveTable(
769769
Select|string|array|TableIdentifier|null $table,

0 commit comments

Comments
 (0)