Skip to content

Commit

Permalink
result: implement buffering
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Nov 8, 2020
1 parent cf05e8c commit 3b5bc72
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 16 deletions.
13 changes: 13 additions & 0 deletions doc/result.texy
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 12 additions & 0 deletions src/Drivers/Mysqli/MysqliEmptyResultAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
12 changes: 12 additions & 0 deletions src/Drivers/Mysqli/MysqliResultAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion src/Drivers/PdoMysql/PdoMysqlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public function convertToPhp($value, $nativeType)

protected function createResultAdapter(PDOStatement $statement): IResultAdapter
{
return new PdoMysqlResultAdapter($statement);
return (new PdoMysqlResultAdapter($statement))->toBuffered();
}


Expand Down
20 changes: 18 additions & 2 deletions src/Drivers/PdoMysql/PdoMysqlResultAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.");
}


Expand Down
2 changes: 1 addition & 1 deletion src/Drivers/PdoPgsql/PdoPgsqlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public function convertToPhp($value, $nativeType)

protected function createResultAdapter(PDOStatement $statement): IResultAdapter
{
return new PdoPgsqlResultAdapter($statement);
return (new PdoPgsqlResultAdapter($statement))->toBuffered();
}


Expand Down
20 changes: 18 additions & 2 deletions src/Drivers/PdoPgsql/PdoPgsqlResultAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.");
}


Expand Down
12 changes: 12 additions & 0 deletions src/Drivers/Pgsql/PgsqlResultAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
12 changes: 12 additions & 0 deletions src/Drivers/Sqlsrv/SqlsrvResultAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
101 changes: 101 additions & 0 deletions src/Result/BufferedResultAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php declare(strict_types = 1);

namespace Nextras\Dbal\Result;


use ArrayIterator;
use Nextras\Dbal\Exception\InvalidArgumentException;
use OutOfBoundsException;
use function assert;


class BufferedResultAdapter implements IResultAdapter
{
/** @var IResultAdapter */
private $adapter;

/** @var ArrayIterator<mixed, mixed>|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);
}
}
15 changes: 15 additions & 0 deletions src/Result/IResultAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
28 changes: 27 additions & 1 deletion src/Result/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
);
}
Expand Down
Loading

0 comments on commit 3b5bc72

Please sign in to comment.