From cf05e8c4a256ccf27622e9435d15cf8af53c6917 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Sun, 8 Nov 2020 12:35:57 +0100 Subject: [PATCH 1/2] result: move IResultAdapter to Result namespace (BC break!) --- src/Drivers/Mysqli/MysqliEmptyResultAdapter.php | 2 +- src/Drivers/Mysqli/MysqliResultAdapter.php | 2 +- src/Drivers/Pdo/PdoDriver.php | 2 +- src/Drivers/PdoMysql/PdoMysqlDriver.php | 2 +- src/Drivers/PdoMysql/PdoMysqlResultAdapter.php | 3 +-- src/Drivers/PdoPgsql/PdoPgsqlDriver.php | 2 +- src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php | 3 +-- src/Drivers/Pgsql/PgsqlResultAdapter.php | 2 +- src/Drivers/Sqlsrv/SqlsrvResultAdapter.php | 2 +- src/{Drivers => Result}/IResultAdapter.php | 2 +- src/Result/Result.php | 1 - tests/cases/unit/ResultTest.phpt | 2 +- 12 files changed, 11 insertions(+), 14 deletions(-) rename src/{Drivers => Result}/IResultAdapter.php (95%) diff --git a/src/Drivers/Mysqli/MysqliEmptyResultAdapter.php b/src/Drivers/Mysqli/MysqliEmptyResultAdapter.php index e4a38f90..ef869c1d 100644 --- a/src/Drivers/Mysqli/MysqliEmptyResultAdapter.php +++ b/src/Drivers/Mysqli/MysqliEmptyResultAdapter.php @@ -3,8 +3,8 @@ namespace Nextras\Dbal\Drivers\Mysqli; -use Nextras\Dbal\Drivers\IResultAdapter; use Nextras\Dbal\Exception\InvalidArgumentException; +use Nextras\Dbal\Result\IResultAdapter; use Nextras\Dbal\Utils\StrictObjectTrait; diff --git a/src/Drivers/Mysqli/MysqliResultAdapter.php b/src/Drivers/Mysqli/MysqliResultAdapter.php index 58b09744..966db742 100644 --- a/src/Drivers/Mysqli/MysqliResultAdapter.php +++ b/src/Drivers/Mysqli/MysqliResultAdapter.php @@ -4,8 +4,8 @@ use mysqli_result; -use Nextras\Dbal\Drivers\IResultAdapter; use Nextras\Dbal\Exception\InvalidArgumentException; +use Nextras\Dbal\Result\IResultAdapter; use Nextras\Dbal\Utils\StrictObjectTrait; diff --git a/src/Drivers/Pdo/PdoDriver.php b/src/Drivers/Pdo/PdoDriver.php index 7c7122ff..676ff00b 100644 --- a/src/Drivers/Pdo/PdoDriver.php +++ b/src/Drivers/Pdo/PdoDriver.php @@ -8,10 +8,10 @@ use Nextras\Dbal\Drivers\Exception\ConnectionException; use Nextras\Dbal\Drivers\Exception\DriverException; use Nextras\Dbal\Drivers\IDriver; -use Nextras\Dbal\Drivers\IResultAdapter; use Nextras\Dbal\Exception\InvalidStateException; use Nextras\Dbal\Exception\NotSupportedException; use Nextras\Dbal\ILogger; +use Nextras\Dbal\Result\IResultAdapter; use Nextras\Dbal\Result\Result; use Nextras\Dbal\Utils\LoggerHelper; use Nextras\Dbal\Utils\StrictObjectTrait; diff --git a/src/Drivers/PdoMysql/PdoMysqlDriver.php b/src/Drivers/PdoMysql/PdoMysqlDriver.php index 62e344b2..3d7279e4 100644 --- a/src/Drivers/PdoMysql/PdoMysqlDriver.php +++ b/src/Drivers/PdoMysql/PdoMysqlDriver.php @@ -14,13 +14,13 @@ use Nextras\Dbal\Drivers\Exception\QueryException; use Nextras\Dbal\Drivers\Exception\UniqueConstraintViolationException; use Nextras\Dbal\Drivers\IDriver; -use Nextras\Dbal\Drivers\IResultAdapter; use Nextras\Dbal\Drivers\Pdo\PdoDriver; use Nextras\Dbal\Exception\InvalidArgumentException; use Nextras\Dbal\Exception\NotSupportedException; use Nextras\Dbal\ILogger; use Nextras\Dbal\Platforms\IPlatform; use Nextras\Dbal\Platforms\MySqlPlatform; +use Nextras\Dbal\Result\IResultAdapter; use PDO; use PDOStatement; use function array_key_exists; diff --git a/src/Drivers/PdoMysql/PdoMysqlResultAdapter.php b/src/Drivers/PdoMysql/PdoMysqlResultAdapter.php index 4e527d4b..ca2947b2 100644 --- a/src/Drivers/PdoMysql/PdoMysqlResultAdapter.php +++ b/src/Drivers/PdoMysql/PdoMysqlResultAdapter.php @@ -3,13 +3,12 @@ namespace Nextras\Dbal\Drivers\PdoMysql; -use Nextras\Dbal\Drivers\IResultAdapter; use Nextras\Dbal\Exception\InvalidStateException; use Nextras\Dbal\Exception\NotSupportedException; +use Nextras\Dbal\Result\IResultAdapter; use Nextras\Dbal\Utils\StrictObjectTrait; use PDO; use PDOStatement; -use function assert; class PdoMysqlResultAdapter implements IResultAdapter diff --git a/src/Drivers/PdoPgsql/PdoPgsqlDriver.php b/src/Drivers/PdoPgsql/PdoPgsqlDriver.php index b6409867..4b64a0a9 100644 --- a/src/Drivers/PdoPgsql/PdoPgsqlDriver.php +++ b/src/Drivers/PdoPgsql/PdoPgsqlDriver.php @@ -14,7 +14,6 @@ use Nextras\Dbal\Drivers\Exception\QueryException; use Nextras\Dbal\Drivers\Exception\UniqueConstraintViolationException; use Nextras\Dbal\Drivers\IDriver; -use Nextras\Dbal\Drivers\IResultAdapter; use Nextras\Dbal\Drivers\Pdo\PdoDriver; use Nextras\Dbal\Exception\InvalidArgumentException; use Nextras\Dbal\Exception\InvalidStateException; @@ -22,6 +21,7 @@ use Nextras\Dbal\ILogger; use Nextras\Dbal\Platforms\IPlatform; use Nextras\Dbal\Platforms\PostgreSqlPlatform; +use Nextras\Dbal\Result\IResultAdapter; use PDOStatement; use function array_map; use function date; diff --git a/src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php b/src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php index f2a9051a..a5ac0b81 100644 --- a/src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php +++ b/src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php @@ -3,13 +3,12 @@ namespace Nextras\Dbal\Drivers\PdoPgsql; -use Nextras\Dbal\Drivers\IResultAdapter; use Nextras\Dbal\Exception\InvalidStateException; use Nextras\Dbal\Exception\NotSupportedException; +use Nextras\Dbal\Result\IResultAdapter; use Nextras\Dbal\Utils\StrictObjectTrait; use PDO; use PDOStatement; -use function assert; class PdoPgsqlResultAdapter implements IResultAdapter diff --git a/src/Drivers/Pgsql/PgsqlResultAdapter.php b/src/Drivers/Pgsql/PgsqlResultAdapter.php index defcdda9..b958c09c 100644 --- a/src/Drivers/Pgsql/PgsqlResultAdapter.php +++ b/src/Drivers/Pgsql/PgsqlResultAdapter.php @@ -3,8 +3,8 @@ namespace Nextras\Dbal\Drivers\Pgsql; -use Nextras\Dbal\Drivers\IResultAdapter; use Nextras\Dbal\Exception\InvalidArgumentException; +use Nextras\Dbal\Result\IResultAdapter; use Nextras\Dbal\Utils\StrictObjectTrait; use function pg_fetch_array; use function pg_field_name; diff --git a/src/Drivers/Sqlsrv/SqlsrvResultAdapter.php b/src/Drivers/Sqlsrv/SqlsrvResultAdapter.php index 163afe8f..97a67369 100644 --- a/src/Drivers/Sqlsrv/SqlsrvResultAdapter.php +++ b/src/Drivers/Sqlsrv/SqlsrvResultAdapter.php @@ -3,8 +3,8 @@ namespace Nextras\Dbal\Drivers\Sqlsrv; -use Nextras\Dbal\Drivers\IResultAdapter; use Nextras\Dbal\Exception\InvalidArgumentException; +use Nextras\Dbal\Result\IResultAdapter; use Nextras\Dbal\Utils\StrictObjectTrait; use function sqlsrv_fetch; use function sqlsrv_fetch_array; diff --git a/src/Drivers/IResultAdapter.php b/src/Result/IResultAdapter.php similarity index 95% rename from src/Drivers/IResultAdapter.php rename to src/Result/IResultAdapter.php index 4310ee09..91564b90 100644 --- a/src/Drivers/IResultAdapter.php +++ b/src/Result/IResultAdapter.php @@ -1,6 +1,6 @@ Date: Sun, 8 Nov 2020 12:54:37 +0100 Subject: [PATCH 2/2] result: implement buffering --- doc/result.texy | 13 +++ .../Mysqli/MysqliEmptyResultAdapter.php | 12 +++ src/Drivers/Mysqli/MysqliResultAdapter.php | 12 +++ src/Drivers/PdoMysql/PdoMysqlDriver.php | 2 +- .../PdoMysql/PdoMysqlResultAdapter.php | 20 +++- src/Drivers/PdoPgsql/PdoPgsqlDriver.php | 2 +- .../PdoPgsql/PdoPgsqlResultAdapter.php | 20 +++- src/Drivers/Pgsql/PgsqlResultAdapter.php | 12 +++ src/Drivers/Sqlsrv/SqlsrvResultAdapter.php | 12 +++ src/Result/BufferedResultAdapter.php | 101 ++++++++++++++++++ src/Result/IResultAdapter.php | 15 +++ src/Result/Result.php | 28 ++++- tests/cases/integration/result.buffering.phpt | 51 +++++++++ tests/cases/integration/result.phpt | 9 -- 14 files changed, 293 insertions(+), 16 deletions(-) create mode 100644 src/Result/BufferedResultAdapter.php create mode 100644 tests/cases/integration/result.buffering.phpt diff --git a/doc/result.texy b/doc/result.texy index 8c13a586..a012de9f 100644 --- a/doc/result.texy +++ b/doc/result.texy @@ -50,3 +50,16 @@ echo $row->age; echo $row->getNthField(0); // prints name \-- + + +Buffering +========= + +Some database drivers do not support rewinding or seeking the result. I.e. you cannot iterate over the result multiple times. Similarly, you cannot use `seek()` method to skip some rows. Dbal's emulated buffering comes to solve this for you. The relevant drivers automatically enable emulated buffering. You can disable or enable it for particular `Result` instances. + +/--php +$result = $connection->query('...')->buffered(); // enable emulated buffering +$result->unbuffered(); // disable the emulated buffering +\-- + +If the unbuffered Result was already partially consumed, enabling buffering does nothing and Result will potentially throw an exception when rewinded or seeked. If the buffered Result was already partially consumed, disabling buffering does nothing and Result will still use the buffer. diff --git a/src/Drivers/Mysqli/MysqliEmptyResultAdapter.php b/src/Drivers/Mysqli/MysqliEmptyResultAdapter.php index ef869c1d..bfcdd7dc 100644 --- a/src/Drivers/Mysqli/MysqliEmptyResultAdapter.php +++ b/src/Drivers/Mysqli/MysqliEmptyResultAdapter.php @@ -13,6 +13,18 @@ class MysqliEmptyResultAdapter implements IResultAdapter use StrictObjectTrait; + public function toBuffered(): IResultAdapter + { + return $this; + } + + + public function toUnbuffered(): IResultAdapter + { + return $this; + } + + public function seek(int $index): void { throw new InvalidArgumentException("Unable to seek in row set to {$index} index."); diff --git a/src/Drivers/Mysqli/MysqliResultAdapter.php b/src/Drivers/Mysqli/MysqliResultAdapter.php index 966db742..b19eee72 100644 --- a/src/Drivers/Mysqli/MysqliResultAdapter.php +++ b/src/Drivers/Mysqli/MysqliResultAdapter.php @@ -63,6 +63,18 @@ public function __destruct() } + public function toBuffered(): IResultAdapter + { + return $this; + } + + + public function toUnbuffered(): IResultAdapter + { + return $this; + } + + public function seek(int $index): void { if ($this->result->num_rows !== 0 && !$this->result->data_seek($index)) { diff --git a/src/Drivers/PdoMysql/PdoMysqlDriver.php b/src/Drivers/PdoMysql/PdoMysqlDriver.php index 3d7279e4..a452f559 100644 --- a/src/Drivers/PdoMysql/PdoMysqlDriver.php +++ b/src/Drivers/PdoMysql/PdoMysqlDriver.php @@ -135,7 +135,7 @@ public function convertToPhp($value, $nativeType) protected function createResultAdapter(PDOStatement $statement): IResultAdapter { - return new PdoMysqlResultAdapter($statement); + return (new PdoMysqlResultAdapter($statement))->toBuffered(); } diff --git a/src/Drivers/PdoMysql/PdoMysqlResultAdapter.php b/src/Drivers/PdoMysql/PdoMysqlResultAdapter.php index ca2947b2..c8cb29a6 100644 --- a/src/Drivers/PdoMysql/PdoMysqlResultAdapter.php +++ b/src/Drivers/PdoMysql/PdoMysqlResultAdapter.php @@ -5,6 +5,7 @@ use Nextras\Dbal\Exception\InvalidStateException; use Nextras\Dbal\Exception\NotSupportedException; +use Nextras\Dbal\Result\BufferedResultAdapter; use Nextras\Dbal\Result\IResultAdapter; use Nextras\Dbal\Utils\StrictObjectTrait; use PDO; @@ -58,10 +59,25 @@ public function __construct(PDOStatement $statement) } + public function toBuffered(): IResultAdapter + { + return new BufferedResultAdapter($this); + } + + + public function toUnbuffered(): IResultAdapter + { + return $this; + } + + public function seek(int $index): void { - if ($index === 0 && $this->beforeFirstFetch) return; - throw new NotSupportedException("PDO does not support seek & replay. Use Result::fetchAll() to and result its result."); + if ($index === 0 && $this->beforeFirstFetch) { + return; + } + + throw new NotSupportedException("PDO does not support rewinding or seeking. Use Result::buffered() before first consume of the result."); } diff --git a/src/Drivers/PdoPgsql/PdoPgsqlDriver.php b/src/Drivers/PdoPgsql/PdoPgsqlDriver.php index 4b64a0a9..a040a35a 100644 --- a/src/Drivers/PdoPgsql/PdoPgsqlDriver.php +++ b/src/Drivers/PdoPgsql/PdoPgsqlDriver.php @@ -110,7 +110,7 @@ public function convertToPhp($value, $nativeType) protected function createResultAdapter(PDOStatement $statement): IResultAdapter { - return new PdoPgsqlResultAdapter($statement); + return (new PdoPgsqlResultAdapter($statement))->toBuffered(); } diff --git a/src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php b/src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php index a5ac0b81..a4def2b9 100644 --- a/src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php +++ b/src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php @@ -5,6 +5,7 @@ use Nextras\Dbal\Exception\InvalidStateException; use Nextras\Dbal\Exception\NotSupportedException; +use Nextras\Dbal\Result\BufferedResultAdapter; use Nextras\Dbal\Result\IResultAdapter; use Nextras\Dbal\Utils\StrictObjectTrait; use PDO; @@ -54,10 +55,25 @@ public function __construct(PDOStatement $statement) } + public function toBuffered(): IResultAdapter + { + return new BufferedResultAdapter($this); + } + + + public function toUnbuffered(): IResultAdapter + { + return $this; + } + + public function seek(int $index): void { - if ($index === 0 && $this->beforeFirstFetch) return; - throw new NotSupportedException("PDO does not support seek & replay. Use Result::fetchAll() to and result its result."); + if ($index === 0 && $this->beforeFirstFetch) { + return; + } + + throw new NotSupportedException("PDO does not support rewinding or seeking. Use Result::buffered() before first consume of the result."); } diff --git a/src/Drivers/Pgsql/PgsqlResultAdapter.php b/src/Drivers/Pgsql/PgsqlResultAdapter.php index b958c09c..0dc4df1c 100644 --- a/src/Drivers/Pgsql/PgsqlResultAdapter.php +++ b/src/Drivers/Pgsql/PgsqlResultAdapter.php @@ -70,6 +70,18 @@ public function __destruct() } + public function toBuffered(): IResultAdapter + { + return $this; + } + + + public function toUnbuffered(): IResultAdapter + { + return $this; + } + + public function seek(int $index): void { if (pg_num_rows($this->result) !== 0 && !pg_result_seek($this->result, $index)) { diff --git a/src/Drivers/Sqlsrv/SqlsrvResultAdapter.php b/src/Drivers/Sqlsrv/SqlsrvResultAdapter.php index 97a67369..e1ac68ef 100644 --- a/src/Drivers/Sqlsrv/SqlsrvResultAdapter.php +++ b/src/Drivers/Sqlsrv/SqlsrvResultAdapter.php @@ -55,6 +55,18 @@ public function __destruct() } + public function toBuffered(): IResultAdapter + { + return $this; + } + + + public function toUnbuffered(): IResultAdapter + { + return $this; + } + + public function seek(int $index): void { if ($index !== 0 && sqlsrv_num_rows($this->statement) !== 0 && sqlsrv_fetch($this->statement, SQLSRV_SCROLL_ABSOLUTE, $index) !== true) { diff --git a/src/Result/BufferedResultAdapter.php b/src/Result/BufferedResultAdapter.php new file mode 100644 index 00000000..a82aa53f --- /dev/null +++ b/src/Result/BufferedResultAdapter.php @@ -0,0 +1,101 @@ +|null */ + private $data; + + + public function __construct(IResultAdapter $adapter) + { + $this->adapter = $adapter; + } + + + public function toBuffered(): IResultAdapter + { + return $this; + } + + + public function toUnbuffered(): IResultAdapter + { + if ($this->data === null) { + return $this->adapter->toUnbuffered(); + } else { + return $this; + } + } + + + public function seek(int $index): void + { + if ($this->data === null) { + $this->init(); + } + assert($this->data !== null); + + if ($index === 0) { + $this->data->rewind(); + return; + } + + try { + $this->data->seek($index); + } catch (OutOfBoundsException $e) { + throw new InvalidArgumentException("Unable to seek in row set to {$index} index.", 0, $e); + } + } + + + public function fetch(): ?array + { + if ($this->data === null) { + $this->init(); + } + assert($this->data !== null); + + $fetched = $this->data->valid() ? $this->data->current() : null; + $this->data->next(); + return $fetched; + } + + + public function getTypes(): array + { + return $this->adapter->getTypes(); + } + + + public function getRowsCount(): int + { + if ($this->data === null) { + $this->init(); + } + assert($this->data !== null); + + return $this->data->count(); + } + + + private function init(): void + { + $rows = []; + while (($row = $this->adapter->fetch()) !== null) { + $rows[] = $row; + } + $this->data = new ArrayIterator($rows); + } +} diff --git a/src/Result/IResultAdapter.php b/src/Result/IResultAdapter.php index 91564b90..05367cfe 100644 --- a/src/Result/IResultAdapter.php +++ b/src/Result/IResultAdapter.php @@ -17,6 +17,21 @@ interface IResultAdapter public const TYPE_AS_IS = 64; + /** + * Converts result adapter to buffered version. + * @internal + */ + public function toBuffered(): IResultAdapter; + + + /** + * Converts result adapter to not explicitly buffered version. + * The resulting adapter may be naturally buffered by PHP's extension implementation. + * @internal + */ + public function toUnbuffered(): IResultAdapter; + + /** * @throws InvalidArgumentException */ diff --git a/src/Result/Result.php b/src/Result/Result.php index 01054c73..a6ed838a 100644 --- a/src/Result/Result.php +++ b/src/Result/Result.php @@ -71,6 +71,30 @@ public function __construct(IResultAdapter $adapter, IDriver $driver) } + /** + * Enables emulated buffering mode to allow rewinding the result multiple times or seeking to specific position. + * This will enable emulated buffering for drivers that do not support buffering & scrolling the result. + * @return static + */ + public function buffered(): Result + { + $this->adapter = $this->adapter->toBuffered(); + return $this; + } + + + /** + * Disables emulated buffering mode. + * Emulated buffering may not be disabled when the result was already (partially) consumed. + * @return static + */ + public function unbuffered(): Result + { + $this->adapter = $this->adapter->toUnbuffered(); + return $this; + } + + public function getAdapter(): IResultAdapter { return $this->adapter; @@ -163,7 +187,9 @@ public function fetchPairs(?string $key = null, ?string $value = null): array public function getColumns(): array { return array_map( - function($name): string { return (string) $name; }, // @phpstan-ignore-line + function ($name): string { + return (string) $name; // @phpstan-ignore-line + }, array_keys($this->adapter->getTypes()) ); } diff --git a/tests/cases/integration/result.buffering.phpt b/tests/cases/integration/result.buffering.phpt new file mode 100644 index 00000000..21ae2323 --- /dev/null +++ b/tests/cases/integration/result.buffering.phpt @@ -0,0 +1,51 @@ +connection->getDriver() instanceof PdoDriver) { + Environment::skip('Explicit buffering is needed only in PDO drivers.'); + } + + $this->initData($this->connection); + $this->lockConnection($this->connection); + + $buffered = $this->connection->query('SELECT * FROM books ORDER BY id')->buffered(); + Assert::same([1, 2, 3, 4], $buffered->fetchPairs(null, 'id')); + Assert::same([1, 2, 3, 4], $buffered->fetchPairs(null, 'id')); // repeated + Assert::same(4, $buffered->count()); + + $unbuffered = $this->connection->query('SELECT * FROM books ORDER BY id')->buffered()->unbuffered(); + Assert::same([1, 2, 3, 4], $unbuffered->fetchPairs(null, 'id')); + Assert::throws(function () use ($unbuffered): void { + $unbuffered->fetchPairs(null, 'id'); + }, NotSupportedException::class); + + $lateChanged = $this->connection->query('SELECT * FROM books ORDER BY id')->buffered(); + Assert::same([1, 2, 3, 4], $buffered->fetchPairs(null, 'id')); + $lateChanged->unbuffered(); + Assert::same([1, 2, 3, 4], $buffered->fetchPairs(null, 'id')); + } +} + + +$test = new ResultBufferingIntegrationTest(); +$test->run(); diff --git a/tests/cases/integration/result.phpt b/tests/cases/integration/result.phpt index 160fbcc3..86938b67 100644 --- a/tests/cases/integration/result.phpt +++ b/tests/cases/integration/result.phpt @@ -8,10 +8,8 @@ namespace NextrasTests\Dbal; -use Nextras\Dbal\Drivers\Pdo\PdoDriver; use Nextras\Dbal\Drivers\PdoPgsql\PdoPgsqlDriver; use Nextras\Dbal\Exception\InvalidArgumentException; -use Nextras\Dbal\Exception\NotSupportedException; use Nextras\Dbal\Platforms\SqlServerPlatform; use Nextras\Dbal\Utils\DateTimeImmutable; use Tester\Assert; @@ -70,13 +68,6 @@ class ResultIntegrationTest extends IntegrationTestCase $books = $result->fetchPairs(null, 'id'); Assert::same([1, 2, 3, 4], $books); - if ($this->connection->getDriver() instanceof PdoDriver) { - Assert::throws(function () use ($result): void { - $result->fetchPairs(null, 'id'); - }, NotSupportedException::class); - return; - } - $books = $result->fetchPairs(null, 'id'); Assert::same([1, 2, 3, 4], $books);