diff --git a/src/PostgresDriver/ArrayCursorDriver.php b/src/PostgresDriver/ArrayCursorDriver.php new file mode 100644 index 0000000000000000000000000000000000000000..41ad9ead48b9bf1030ae5e7c247aed389776d7a1 --- /dev/null +++ b/src/PostgresDriver/ArrayCursorDriver.php @@ -0,0 +1,174 @@ +<?php declare(strict_types = 1); +/** + * This file is part of mappi/store. + */ + +namespace Grifart\Mappi\Store\PostgresDriver; + +use Dibi\Connection; +use Dibi\Row; + +/** + * Array Cursor simulates scrolling cursor as implemented in PostgreSQL 9.5. + * + * For more information see ICursor. + * @see ICursor + * @package Grifart\Mappi\Store\PostgresDriver + */ +class ArrayCursorDriver implements ICursorDriver +{ + private $cursors = []; + private $indexes = []; + + public function createTestCursor(string $name, int $length) + { + if(isset($this->cursors[$name])) { + throw new CursorException("Cursor with name $name already exists"); + } + + $this->cursors[$name] = range(0, $length+1); + $this->cursors[$name][0] = null; + $this->cursors[$name][$length+1] = null; + + $this->indexes[$name] = 0; + } + + public function close(string $name) : bool + { + unset($this->cursors[$name]); + return TRUE; + } + + private function getMaxIndex(string $name) { + return count($this->cursors[$name])-1; + } + + private function normalizeIndex(string $name, int &$index) + { + if($index < 0) { + $index = 0; + } + $max = $this->getMaxIndex($name); + if($index > $max) { + $index = $max; + } + } + + private function pointerMoveTo(string $name, int $index) + { + $this->normalizeIndex($name, $index); + $this->indexes[$name] = $index; + } + + private function getCurrentIndex(string $name) : int + { + return $this->indexes[$name]; + } + + private function getCurrentValue(string $name) { + return $this->cursors[$name][$this->getCurrentIndex($name)]; + } + + public function moveTo(string $name, int $index) : bool + { + if ($index < 0) { + throw new \InvalidArgumentException("Negative index not supported. Use moveFromEndTo() instead."); + } + $this->pointerMoveTo($name, $index); + return !!$this->getCurrentValue($name); + } + + private function cursorArrayMoveFromEndTo(string $name, int $index) + { + $max = $this->getMaxIndex($name); + + $indexFromLeft = $max - $index; + $this->normalizeIndex($name, $indexFromLeft); + + $this->moveTo($name, $indexFromLeft); + } + + public function moveFromEndTo(string $name, int $index) : bool + { + if ($index < 0) { + throw new \InvalidArgumentException("Negative index not supported. Use moveTo() instead."); + } + $this->cursorArrayMoveFromEndTo($name, $index); + return !!$this->getCurrentValue($name); + } + + public function moveBy(string $name, int $rows) : bool + { + $i = $this->getCurrentIndex($name); + $indexFromLeft = $i + $rows; + $this->normalizeIndex($name, $indexFromLeft); + $this->moveTo($name, $indexFromLeft); + return !!$this->getCurrentValue($name); + } + + // ----------------- FETCH --------------------- + + public function fetchRange(string $name, int $rows) : array + { + $currentKey = $this->getCurrentIndex($name); + if($rows === self::FETCH_FOREGOING) { + $finalKey = PHP_INT_MIN; + } elseif ($rows === self::FETCH_REMAINING) { + $finalKey = PHP_INT_MAX; + } else { + $finalKey = $currentKey + $rows; + } + $this->normalizeIndex($name, $finalKey); + + $data = []; + if($rows === 0) { + $v = $this->getCurrentValue($name); + if($v !== NULL) { + $data[] = ["n" => $this->getCurrentValue($name)]; + } + return $data; + } + + if($finalKey < $currentKey) { // backwards + for($i = $currentKey-1; $i >= $finalKey; $i--) { + $this->moveTo($name, $i); + $v = $this->getCurrentValue($name); + if($v === NULL) {break;} + $data[] = ["n" => $this->getCurrentValue($name)]; + } + } else { + for($i = $currentKey+1; $i <= $finalKey; $i++) { + $this->moveTo($name, $i); + $v = $this->getCurrentValue($name); + if($v === NULL) {break;} + $data[] = ["n" => $this->getCurrentValue($name)]; + } + } + return $data; + } + + public function fetchOneAt(string $name, int $index) + { + if($index < 0) { + $this->moveFromEndTo($name, abs($index)); + } else { + $this->moveTo($name, $index); + } + $data = $this->fetchRange($name, 0); + if(count($data) === 0) { + return NULL; + } + return $data[0]; + } + + public function fetchOneBy(string $name, int $rows) + { + $this->moveBy($name, $rows); + $data = $this->fetchRange($name, 0); + if(count($data) === 0) { + return NULL; + } + return $data[0]; + } + +} \ No newline at end of file diff --git a/src/PostgresDriver/Cursor.php b/src/PostgresDriver/Cursor.php index 0685104f21dece412842a86db8753a1983d18a53..8de0357c7e4dba2c24793ab2e5f1e0050c681cad 100644 --- a/src/PostgresDriver/Cursor.php +++ b/src/PostgresDriver/Cursor.php @@ -20,8 +20,8 @@ use Dibi\Row; */ final class Cursor implements ICursor { - /** @var Connection */ - private $connection; + /** @var ICursorDriver */ + private $driver; /** @var string */ private $name; @@ -33,23 +33,16 @@ final class Cursor implements ICursor * Warning: this class itself does not check if cursor is really valid in * constructor. * - * @param Connection $connection Connection with defined cursor with - * name given in next parameter - * @param string $name Name of cursor defined in - * transaction (if not valid all later - * call wil fail) + * @param ICursorDriver $driver + * @param string $name Name of cursor (if not valid all later call will fail) */ - public function __construct(Connection $connection, string $name) + public function __construct(ICursorDriver $driver, string $name) { - $this->connection = $connection; + $this->driver = $driver; $this->name = $name; // todo: close; for now automatically closed on transaction end; maybe in destructor? Ignore errors? } - public function getConnection() : Connection - { - return $this->connection; - } public function getName() : string { @@ -66,11 +59,7 @@ final class Cursor implements ICursor if ($index < 0) { throw new \InvalidArgumentException("Negative index not supported. Use moveFromEndTo() instead."); } - $this->headOnRecord = !!$this->getConnection()->query( - "MOVE ABSOLUTE %i IN %n", - $index, - $this->getName() - ); + $this->headOnRecord = $this->driver->moveTo($this->name, $index); } public function moveFromEndTo(int $index) @@ -78,102 +67,42 @@ final class Cursor implements ICursor if ($index < 0) { throw new \InvalidArgumentException("Negative index not supported. Use moveTo() instead."); } - if ($index === 0) { // END position - $this->headOnRecord = !!$this->getConnection()->query( - "MOVE ABSOLUTE -1 IN %n; MOVE NEXT IN %n;", - $this->getName(), - $this->getName() - ); - return; - } - $negativeIndex = -$index; - $this->headOnRecord = !!$this->getConnection()->query( - "MOVE ABSOLUTE %i IN %n", - $negativeIndex, - $this->getName() - ); + $this->headOnRecord = $this->driver->moveFromEndTo($this->name, $index); } public function moveBy(int $rows) { - $this->headOnRecord = !!$this->getConnection()->query( - "MOVE RELATIVE %i IN %n", - $rows, - $this->getName() - ); + $this->headOnRecord = $this->driver->moveBy($this->name, $rows); } // ----------------- FETCH --------------------- public function fetchRange(int $rows) : array { - $forward = $rows >= 0; - $numberOfRowsSQLClause = ($rows === self::FETCH_FOREGOING || $rows === self::FETCH_REMAINING) ? - "ALL" : abs($rows); - - $recs = $this->connection->query( - $forward ? "FETCH FORWARD %sql FROM %n" : "FETCH BACKWARD %sql FROM %n", - $numberOfRowsSQLClause, - $this->getName() - )->fetchAll(); + $recs = $this->driver->fetchRange($this->name, $rows); - if ($rows === 0) { + if ($rows === 0) { // current row $this->headOnRecord = count($recs) === 1; } else { // todo: test this properly!! $this->headOnRecord = count($recs) === abs($rows); } - // todo: DibiRecord to array - return $this->convertRows($recs); + return $recs; } public function fetchOneAt(int $index) { - $row = $this->connection->query( - "FETCH ABSOLUTE %i FROM %n", - $index, - $this->getName() - )->fetch(); - $this->headOnRecord = $row !== FALSE; - if ($row === FALSE) { - return NULL; - } - return $this->convertRow($row); + $row = $this->driver->fetchOneAt($this->name, $index); + $this->headOnRecord = $row !== NULL; + return $row; } public function fetchOneBy(int $rows) { - $row = $this->connection->query( - "FETCH RELATIVE %i FROM %n", - $rows, - $this->getName() - )->fetch(); - $this->headOnRecord = $row !== FALSE; - if ($row === FALSE) { - return NULL; - } - return $this->convertRow($row); + $row = $this->driver->fetchOneBy($this->name, $rows); + $this->headOnRecord = $row !== NULL; + return $row; } - // ----------- DATA CONVERSION ----------- - - private function convertRow(Row $row) : array - { - return $row->toArray(); - } - - /** - * @param Row[] $rows - * @return array - */ - private function convertRows(array $rows) : array - { - // TODO: TrackedCursor should return correct index values in array indexes - $new = []; - foreach ($rows as $row) { - $new[] = $this->convertRow($row); - } - return $new; - } } \ No newline at end of file diff --git a/src/PostgresDriver/CursorFactory.php b/src/PostgresDriver/CursorFactory.php index b317d2eaf2c728e5eb8979804a5e14b2f112445b..63a50f2209628f044f1d8bdd6c4561621ed6538a 100644 --- a/src/PostgresDriver/CursorFactory.php +++ b/src/PostgresDriver/CursorFactory.php @@ -41,7 +41,7 @@ final class CursorFactory ); return new Cursor( - $this->connection, + new PostgresCursorDriver($this->connection), $name ); } diff --git a/src/PostgresDriver/ICursorDriver.php b/src/PostgresDriver/ICursorDriver.php new file mode 100644 index 0000000000000000000000000000000000000000..6d3ab8464d0e2abc10de1d4555b67f8feaa81048 --- /dev/null +++ b/src/PostgresDriver/ICursorDriver.php @@ -0,0 +1,53 @@ +<?php +/** + * This file is part of mappi/store. + */ +namespace Grifart\Mappi\Store\PostgresDriver; + +interface ICursorDriver +{ + const FETCH_REMAINING = PHP_INT_MAX; + const FETCH_FOREGOING = PHP_INT_MIN; + + /** + * @param string $name Cursor name + * @param int $index + * @return bool Head on record? + */ + public function moveTo(string $name, int $index) : bool; + + /** + * @param string $name Cursor name + * @param int $index + * @return bool Head on record? + */ + public function moveFromEndTo(string $name, int $index) : bool; + + /** + * @param string $name Cursor name + * @param int $rows + * @return bool Head on record? + */ + public function moveBy(string $name, int $rows) : bool; + + /** + * @param string $name Cursor name + * @param int $rows + * @return array + */ + public function fetchRange(string $name, int $rows) : array; + + /** + * @param string $name + * @param int $index + * @return array|null + */ + public function fetchOneAt(string $name, int $index); + + /** + * @param string $name + * @param int $rows + * @return array|null + */ + public function fetchOneBy(string $name, int $rows); +} \ No newline at end of file diff --git a/src/PostgresDriver/PostgresCursorDriver.php b/src/PostgresDriver/PostgresCursorDriver.php new file mode 100644 index 0000000000000000000000000000000000000000..76fea60afca536b00f4430dd12fbf53fe900418f --- /dev/null +++ b/src/PostgresDriver/PostgresCursorDriver.php @@ -0,0 +1,139 @@ +<?php declare(strict_types = 1); +/** + * This file is part of mappi/store. + */ + +namespace Grifart\Mappi\Store\PostgresDriver; + +use Dibi\Connection; +use Dibi\Row; + +class PostgresCursorDriver implements ICursorDriver +{ + + /** @var Connection */ + private $connection; + + /** + * PostgresCursorDriver constructor. + * @param Connection $connection + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * @return Connection + */ + private function getConnection() + { + return $this->connection; + } + + public function moveTo(string $name, int $index) : bool + { + if ($index < 0) { + throw new \InvalidArgumentException("Negative index not supported. Use moveFromEndTo() instead."); + } + return !!$this->getConnection()->query( + "MOVE ABSOLUTE %i IN %n", + $index, + $name + ); + } + + public function moveFromEndTo(string $name, int $index) : bool + { + if ($index < 0) { + throw new \InvalidArgumentException("Negative index not supported. Use moveTo() instead."); + } + if ($index === 0) { // END position + return !!$this->getConnection()->query( + "MOVE ABSOLUTE -1 IN %n; MOVE NEXT IN %n;", + $name, + $name + ); + } + + $negativeIndex = -$index; + return !!$this->getConnection()->query( + "MOVE ABSOLUTE %i IN %n", + $negativeIndex, + $name + ); + } + + public function moveBy(string $name, int $rows) : bool + { + return !!$this->getConnection()->query( + "MOVE RELATIVE %i IN %n", + $rows, + $name + ); + } + + // ----------------- FETCH --------------------- + + public function fetchRange(string $name, int $rows) : array + { + $forward = $rows >= 0; + $numberOfRowsSQLClause = ($rows === self::FETCH_FOREGOING || $rows === self::FETCH_REMAINING) ? + "ALL" : abs($rows); + + $recs = $this->connection->query( + $forward ? "FETCH FORWARD %sql FROM %n" : "FETCH BACKWARD %sql FROM %n", + $numberOfRowsSQLClause, + $name + )->fetchAll(); + + return $this->convertRows($recs); + } + + public function fetchOneAt(string $name, int $index) + { + $row = $this->connection->query( + "FETCH ABSOLUTE %i FROM %n", + $index, + $name + )->fetch(); + if($row === FALSE) { + return NULL; + } + return $this->convertRow($row); + } + + public function fetchOneBy(string $name, int $rows) + { + $row = $this->connection->query( + "FETCH RELATIVE %i FROM %n", + $rows, + $name + )->fetch(); + if($row === FALSE) { + return NULL; + } + return $this->convertRow($row); + } + + // ----------- DATA CONVERSION ----------- + + private function convertRow(Row $row) : array + { + return $row->toArray(); + } + + /** + * @param Row[] $rows + * @return array + */ + private function convertRows(array $rows) : array + { + $new = []; + foreach ($rows as $row) { + $new[] = $this->convertRow($row); + } + return $new; + } + +} \ No newline at end of file diff --git a/src/PostgresDriver/SemanticCursor.php b/src/PostgresDriver/SemanticCursor.php index 5d3ce73037435a2517141c982425fa5affd6b871..695c6782349063bcd13c04979b10e5767d8650cb 100644 --- a/src/PostgresDriver/SemanticCursor.php +++ b/src/PostgresDriver/SemanticCursor.php @@ -6,9 +6,9 @@ namespace Grifart\Mappi\Store\PostgresDriver; /** - * Adds more semantics into cursor API. + * Adds semantic API into ICursor. * - * Use this terminology: + * Terminology: * - BEGINNING: the position before first row (initial) * - FIRST: the first row position * - LAST: the last row position @@ -18,7 +18,7 @@ namespace Grifart\Mappi\Store\PostgresDriver; */ class SemanticCursor implements ICursor { - /** @var ICursor */ + /** @var ICursor */ private $cursor; /** @@ -29,7 +29,7 @@ class SemanticCursor implements ICursor $this->cursor = $cursor; } - // Classic driver part: + // ICursor proxy: public function getName() : string { @@ -71,7 +71,7 @@ class SemanticCursor implements ICursor return $this->cursor->fetchOneBy($rows); } - // The extension: + // The semantic extension: /** * Move cursor on the last row diff --git a/src/PostgresDriver/TrackedCursor.php b/src/PostgresDriver/TrackedCursor.php index d4b820aed15d4d76a6dfcb7a8e9d9823fcf87067..5e160a6c97da8052a70da8da9124456033a5e87d 100644 --- a/src/PostgresDriver/TrackedCursor.php +++ b/src/PostgresDriver/TrackedCursor.php @@ -15,20 +15,15 @@ class TrackedCursor implements ICursor /** @var ICursor */ private $cursor; - /** @var Connection */ - private $connection; - /** * Tip: if you are not sure that cursor will be in initial state, call * ->moveToBeginning() after initialization. - * @param Connection $connection * @param ICursor $cursor Cursor in initial state (index=0) * @param CursorPosition $initialPosition */ - public function __construct(Connection $connection, ICursor $cursor, CursorPosition $initialPosition) + public function __construct(ICursor $cursor, CursorPosition $initialPosition) { $this->cursor = $cursor; - $this->connection = $connection; $this->position = $initialPosition; } @@ -40,11 +35,6 @@ class TrackedCursor implements ICursor return clone $this->position; } - public function getConnection() : Connection - { - return $this->connection; - } - public function getName() : string { return $this->cursor->getName(); diff --git a/tests/Store/PostgresDriver/CursorTest.phpt b/tests/Store/PostgresDriver/CursorTest.phpt index bb0884687ae31c082c0ac608a6ac6e94dc934628..5ab1fb8a4abce83957cf3128eda3cd401a594626 100644 --- a/tests/Store/PostgresDriver/CursorTest.phpt +++ b/tests/Store/PostgresDriver/CursorTest.phpt @@ -5,6 +5,8 @@ namespace Grifart\Mappi\Tests\Store\Store\PostgresDriver; +use Grifart\Mappi\Store\PostgresDriver\ArrayCursorDriver; +use Grifart\Mappi\Store\PostgresDriver\Cursor; use Grifart\Mappi\Store\PostgresDriver\CursorFactory; use Grifart\Mappi\Store\PostgresDriver\ICursor; @@ -18,19 +20,25 @@ class CursorTest extends ICursorTest protected function setUp() { - global $connection, $SQL_thousandRowsAscending; - $connection->begin(); - - $factory = new CursorFactory($connection); - $this->uut = $factory->create($SQL_thousandRowsAscending, true); + //global $connection, $SQL_thousandRowsAscending; + //$connection->begin(); + + //$factory = new CursorFactory($connection); + //$this->uut = $factory->create($SQL_thousandRowsAscending, true); + $driver = new ArrayCursorDriver(); + $driver->createTestCursor("test", 1000); + $this->uut = new Cursor( + $driver, + "test" + ); parent::setUp(); } public function tearDown() { - global $connection; - $connection->rollback(); +// global $connection; +// $connection->rollback(); parent::tearDown(); } diff --git a/tests/Store/PostgresDriver/SemanticCursorIntegration.phpt b/tests/Store/PostgresDriver/SemanticCursorIntegration.phpt index 89b1fea074503883da19173e603104a20b79e5cc..137534facb93987e0df9399f66edcc7b6dcfba78 100644 --- a/tests/Store/PostgresDriver/SemanticCursorIntegration.phpt +++ b/tests/Store/PostgresDriver/SemanticCursorIntegration.phpt @@ -5,6 +5,8 @@ namespace Grifart\Mappi\Tests\Store\Store\PostgresDriver; +use Grifart\Mappi\Store\PostgresDriver\ArrayCursorDriver; +use Grifart\Mappi\Store\PostgresDriver\Cursor; use Grifart\Mappi\Store\PostgresDriver\CursorFactory; use Grifart\Mappi\Store\PostgresDriver\SemanticCursor; use Mockery; @@ -26,12 +28,18 @@ class SemanticCursorIntegrationTest extends ICursorTest protected function setUp() { - global $connection, $SQL_thousandRowsAscending; - $connection->begin(); +// global $connection, $SQL_thousandRowsAscending; +// $connection->begin(); + + $driver = new ArrayCursorDriver(); + $driver->createTestCursor("test", 1000); + $cursor = new Cursor( + $driver, + "test" + ); $this->uut = new SemanticCursor( - (new CursorFactory($connection)) - ->create($SQL_thousandRowsAscending, true) + $cursor ); parent::setUp(); @@ -39,8 +47,8 @@ class SemanticCursorIntegrationTest extends ICursorTest public function tearDown() { - global $connection; - $connection->rollback(); +// global $connection; +// $connection->rollback(); parent::tearDown(); } diff --git a/tests/Store/PostgresDriver/TrackedCursorTest.phpt b/tests/Store/PostgresDriver/TrackedCursorTest.phpt index b7183d02d64ed2b905b361469535647f913201c9..0574378e85d4d029e2f0dbeefae32394bd1985be 100644 --- a/tests/Store/PostgresDriver/TrackedCursorTest.phpt +++ b/tests/Store/PostgresDriver/TrackedCursorTest.phpt @@ -8,6 +8,8 @@ namespace Grifart\Mappi\Tests\Store\Store\PostgresDriver; +use Grifart\Mappi\Store\PostgresDriver\ArrayCursorDriver; +use Grifart\Mappi\Store\PostgresDriver\Cursor; use Grifart\Mappi\Store\PostgresDriver\CursorFactory; use Grifart\Mappi\Store\PostgresDriver\CursorPosition; use Grifart\Mappi\Store\PostgresDriver\ICursor; @@ -28,13 +30,19 @@ class TrackedCursorTest extends ICursorTest protected function setUp() { - global $connection, $SQL_thousandRowsAscending; - $connection->begin(); +// global $connection, $SQL_thousandRowsAscending; +// $connection->begin(); + +// $factory = new CursorFactory($connection); + $driver = new ArrayCursorDriver(); + $driver->createTestCursor("test", 1000); + $cursor = new Cursor( + $driver, + "test" + ); - $factory = new CursorFactory($connection); $this->uut = new TrackedCursor( - $connection, - $factory->create($SQL_thousandRowsAscending, TRUE), + $cursor, CursorPosition::fromLeft(0) ); parent::setUp(); @@ -42,8 +50,8 @@ class TrackedCursorTest extends ICursorTest public function tearDown() { - global $connection; - $connection->rollback(); +// global $connection; +// $connection->rollback(); parent::tearDown(); }