Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 79 additions & 22 deletions docs/book/adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,33 @@ class Adapter implements AdapterInterface, Profiler\ProfilerAwareInterface, Sche
public function getQueryResultSetPrototype(): ResultSet\ResultSetInterface;
public function getCurrentSchema(): string|false;

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

public function prepareQuery(
string $sql,
ParameterContainer|array $parameters = []
): Driver\StatementInterface;

public function executeQuery(
string|Driver\StatementInterface $sql
): Driver\ResultInterface;

public function createStatement(
?string $initialSql = null,
ParameterContainer|array|null $initialParameters = null
): Driver\StatementInterface;
}
```

> **Note:** `prepareQuery()` and `executeQuery()` are currently declared on
> the `Adapter` class only; `AdapterInterface` still declares `query()` alone
> to avoid breaking existing implementors during the 0.x series.

### Constructor Parameters

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

## Query Preparation

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

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

The above example will go through the following steps:

1. Create a new `Statement` object
2. Prepare the array `[5]` into a `ParameterContainer` if necessary
3. Inject the `ParameterContainer` into the `Statement` object
4. Execute the `Statement` object, producing a `Result` object
5. Check the `Result` object to check if the supplied SQL was a result set
producing statement. If the query produced a result set, clone the
`ResultSet` prototype, inject the `Result` as its datasource, and return
the new `ResultSet` instance. Otherwise, return the `Result`.
4. Prepare the `Statement` object and return it

To actually run the statement, pass it to `executeQuery()`:

```php title="Executing a Prepared Statement"
$result = $adapter->executeQuery($statement);
```

`executeQuery()` always returns the raw `Driver\ResultInterface` — it never
wraps it. If you want the result wrapped in a `ResultSet`, check
`isQueryResult()` and call `getQueryResult()` yourself:

```php title="Wrapping a Query Result"
if ($result->isQueryResult()) {
$resultSet = $result->getQueryResult();
}
```

`getQueryResult()` clones the `ResultSet` prototype you pass it (or a
default prototype if you pass none), injects the `Result` as its data
source, and returns the new `ResultSet` instance. See
[`getQueryResult()`](#using-the-driver-object) below for details.

## Query Execution

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

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

```php title="Executing DDL Statement Without Preparation"
$adapter->executeQuery(
'ALTER TABLE ADD INDEX(`foo_index`) ON (`foo_column`)'
);
```

## The Deprecated query() Method

`Adapter::query()` predates the `prepareQuery()`/`executeQuery()` split and
combines both concerns behind a single method and a stringly-typed second
argument. It is deprecated in favour of the methods above but remains
available, proxying to them internally, for backwards compatibility:

```php title="Query with Prepared Statement (deprecated)"
$adapter->query('SELECT * FROM `artist` WHERE `id` = ?', [5]);
```

```php title="Executing DDL Statement Without Preparation (deprecated)"
$adapter->query(
'ALTER TABLE ADD INDEX(`foo_index`) ON (`foo_column`)',
Adapter::QUERY_MODE_EXECUTE
);
```

The primary difference to notice is that you must provide the
`Adapter::QUERY_MODE_EXECUTE` (execute) flag as the second parameter.
The primary difference to notice in the second example is that you must
provide the `Adapter::QUERY_MODE_EXECUTE` (execute) flag as the second
parameter.

## Creating Statements

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

```php title="Creating and Executing a Statement"
$statement = $adapter->createStatement($sql, $optionalParameters);
Expand Down Expand Up @@ -229,13 +277,22 @@ interface ResultInterface extends Countable, Iterator
{
public function buffer(): void;
public function isQueryResult(): bool;
public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface;
public function getAffectedRows(): int;
public function getGeneratedValue(): mixed;
public function getResource(): mixed;
public function getFieldCount(): int;
}
```

`getQueryResult()` clones the given `$resultPrototype` (or a default
`ResultSet` prototype if none is given), initializes the clone from the
result, and returns it. It throws `Exception\RuntimeException` if
`isQueryResult()` is false. `Adapter::query()` (the deprecated BC method)
delegates to this method rather than duplicating the clone-and-initialize
logic itself; call it yourself when you want a `ResultSet` from
`executeQuery()`'s raw `Driver\ResultInterface`.

## Using The Platform Object

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

$statement = $adapter->query($sql);
$statement = $adapter->prepareQuery($sql);

$parameters = [
'name' => 'Updated Artist',
Expand All @@ -452,7 +509,7 @@ $parameters = [
$statement->execute($parameters);

// DATA UPDATED, NOW CHECK
$statement = $adapter->query(
$statement = $adapter->prepareQuery(
'SELECT * FROM '
. $qi('artist')
. ' WHERE id = ' . $fp('id')
Expand Down
95 changes: 60 additions & 35 deletions src/Adapter/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
use PhpDb\ResultSet;

use function func_get_args;
use function in_array;
use function is_array;
use function is_string;
use function strtolower;

class Adapter implements AdapterInterface, Profiler\ProfilerAwareInterface, SchemaAwareInterface
Expand Down Expand Up @@ -73,7 +71,10 @@ public function getCurrentSchema(): string|false
/**
* query() is a convenience function
*
* @deprecated Use prepareQuery() and executeQuery() instead. query() will be removed in a future version.
*
* @throws Exception\InvalidArgumentException
* @throws Exception\RuntimeException When execution did not produce a result.
* @throws PhpException
*/
#[Override]
Expand All @@ -82,45 +83,69 @@ public function query(
ParameterContainer|array|string $parametersOrQueryMode = self::QUERY_MODE_PREPARE,
?ResultSet\ResultSetInterface $resultPrototype = null
): Driver\StatementInterface|ResultSet\ResultSetInterface|Driver\ResultInterface {
if (
is_string($parametersOrQueryMode)
&& in_array($parametersOrQueryMode, [self::QUERY_MODE_PREPARE, self::QUERY_MODE_EXECUTE])
) {
$mode = $parametersOrQueryMode;
$parameters = null;
} elseif (is_array($parametersOrQueryMode) || $parametersOrQueryMode instanceof ParameterContainer) {
$mode = self::QUERY_MODE_PREPARE;
$parameters = $parametersOrQueryMode;
} else {
throw new Exception\InvalidArgumentException(
'Parameter 2 to this method must be a flag, an array, or ParameterContainer'
);
if ($parametersOrQueryMode === self::QUERY_MODE_PREPARE) {
return $this->prepareQuery($sql);
}

if ($mode === self::QUERY_MODE_PREPARE) {
$lastPreparedStatement = $this->driver->createStatement($sql);
$lastPreparedStatement->prepare();
if (is_array($parameters) || $parameters instanceof ParameterContainer) {
if (is_array($parameters)) {
$lastPreparedStatement->setParameterContainer(new ParameterContainer($parameters));
} else {
$lastPreparedStatement->setParameterContainer($parameters);
}
$result = $lastPreparedStatement->execute();
} else {
return $lastPreparedStatement;
}
} else {
$result = $this->driver->getConnection()->execute($sql);
$sql = match (true) {
$parametersOrQueryMode === self::QUERY_MODE_EXECUTE
=> $sql,
$parametersOrQueryMode instanceof ParameterContainer,
is_array($parametersOrQueryMode)
=> $this->prepareQuery($sql, $parametersOrQueryMode),
default => throw new Exception\InvalidArgumentException(
'Flag incorrectly set'
),
};

$result = $this->executeQuery($sql);

return $result->isQueryResult()
? $result->getQueryResult($resultPrototype ?? $this->queryResultSetPrototype)
: $result;
}

/**
* Prepare a statement for the given SQL, optionally binding parameters.
*
* Always prepares the statement; never executes it. Use executeQuery()
* to run the returned statement.
*/
#[Override]
public function prepareQuery(
string $sql,
ParameterContainer|array $parameters = []
): Driver\StatementInterface {
$statement = $this->driver->createStatement($sql);

if (is_array($parameters)) {
$parameters = new ParameterContainer($parameters);
}

if ($result instanceof Driver\ResultInterface && $result->isQueryResult()) {
$resultSet = $resultPrototype ?? $this->queryResultSetPrototype;
$resultSetCopy = clone $resultSet;
$statement->setParameterContainer($parameters);
$statement->prepare();

return $statement;
}

$resultSetCopy->initialize($result);
/**
* Execute raw SQL or a prepared statement.
*
* Narrows the driver's execution result to a Driver\ResultInterface,
* never a wrapped ResultSet. Callers can check isQueryResult() and use
* getQueryResult() themselves if they want the result wrapped.
*
* @throws Exception\RuntimeException When execution did not produce a result.
*/
#[Override]
public function executeQuery(Driver\StatementInterface|string $sql): Driver\ResultInterface
{
$result = $sql instanceof Driver\StatementInterface
? $sql->execute()
: $this->driver->getConnection()->execute($sql);

return $resultSetCopy;
if (! $result instanceof Driver\ResultInterface) {
throw new Exception\RuntimeException('Query execution did not produce a result');
}

return $result;
Expand Down
15 changes: 15 additions & 0 deletions src/Adapter/AdapterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ public function query(
?ResultSet\ResultSetInterface $resultPrototype = null
): Driver\StatementInterface|ResultSet\ResultSetInterface|Driver\ResultInterface;

/**
* Prepares a statement for the given SQL without executing it.
*/
public function prepareQuery(
string $sql,
ParameterContainer|array $parameters = []
): Driver\StatementInterface;

/**
* Executes raw SQL or a prepared statement.
*
* @throws Exception\RuntimeException When execution did not produce a result.
*/
public function executeQuery(Driver\StatementInterface|string $sql): Driver\ResultInterface;

/**
* @todo 0.3.x track down this usage!!!
* @return array
Expand Down
24 changes: 24 additions & 0 deletions src/Adapter/Driver/Pdo/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use PDOStatement;
use PhpDb\Adapter\Driver\ResultInterface;
use PhpDb\Adapter\Exception;
use PhpDb\ResultSet\ResultSet;
use PhpDb\ResultSet\ResultSetInterface;
// phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse
use ReturnTypeWillChange;

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

/**
* {@inheritdoc}
*
* @throws Exception\RuntimeException
*/
#[Override]
public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface
{
if (! $this->isQueryResult()) {
throw new Exception\RuntimeException(
'Cannot produce a query result set from a result that is not a query result;'
. ' check isQueryResult() first'
);
}

$resultPrototype ??= new ResultSet();
$resultSet = clone $resultPrototype;
$resultSet->initialize($this);

return $resultSet;
}

/**
* {@inheritdoc}
*/
Expand Down
10 changes: 10 additions & 0 deletions src/Adapter/Driver/ResultInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Countable;
use Iterator;
use PhpDb\Adapter\Exception;
use PhpDb\ResultSet\ResultSetInterface;

interface ResultInterface extends
Countable,
Expand All @@ -26,6 +28,14 @@ public function isBuffered(): ?bool;
*/
public function isQueryResult(): bool;

/**
* Get the seeded query result set, cloned from $resultPrototype (or a
* default prototype if none is given) and initialized from this result.
*
* @throws Exception\RuntimeException When isQueryResult() is false.
*/
public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface;

/**
* Get affected rows
*/
Expand Down
4 changes: 2 additions & 2 deletions src/Sql/Select.php
Original file line number Diff line number Diff line change
Expand Up @@ -762,8 +762,8 @@ public function __clone()
}

/**
* @return array{0: string, 1: string}
* @phpstan-return array{0: string, 1: string}
* @return array{0: string|null, 1: string}
* @phpstan-return array{0: string|null, 1: string}
*/
protected function resolveTable(
Select|string|array|TableIdentifier|null $table,
Expand Down
Loading