diff --git a/src/Cursor.php b/src/Cursor.php index d2ef5baa66e62adcbcc20af448c03e4b8227207b..ca66d34b14c50021fedc049053b0f6e01508cc93 100644 --- a/src/Cursor.php +++ b/src/Cursor.php @@ -74,6 +74,13 @@ final class Cursor implements ICursor $this->headOnRecord = $this->driver->moveBy($this->name, $offset); } + public function scroll(int $offset) : int + { + $numberOfRows = $this->driver->scroll($this->name, $offset); + $this->headOnRecord = $numberOfRows === abs($offset); + return $numberOfRows; + } + // ----------------- FETCH --------------------- public function fetchRange(int $offset) : array diff --git a/src/Driver/ICursorDriver.php b/src/Driver/ICursorDriver.php index 5ce13f23ab19bb869717fd0c57a2be4faaed986c..aa076dccfb1b1d5537e67aa681f4a6338f32dab8 100644 --- a/src/Driver/ICursorDriver.php +++ b/src/Driver/ICursorDriver.php @@ -51,6 +51,14 @@ interface ICursorDriver */ public function moveBy(string $name, int $offset) : bool; + /** + * Scroll from current position by given offset + * @param string $name The cursor + * @param int $offset + * @return int Number of records scrolled + */ + public function scroll(string $name, int $offset) : int; + /** * Fetch all rows from current position to given offset * @param string $name The cursor name diff --git a/src/Driver/PostgresCursorDriver.php b/src/Driver/PostgresCursorDriver.php index 656337dd452da04e38e009abba6ad8a9949de3f2..8dca1338ceb14a1e1f40dad37993ef9a5f3c39ad 100644 --- a/src/Driver/PostgresCursorDriver.php +++ b/src/Driver/PostgresCursorDriver.php @@ -73,6 +73,33 @@ class PostgresCursorDriver implements ICursorDriver ); } + public function scroll(string $name, int $offset) : int + { + if($offset === 0) return 0; + $forward = $offset > 0; + $offsetSql = abs($offset); + + if($forward > 0) { + if($offset === self::FETCH_REMAINING) { + $offsetSql = "ALL"; + } + return $this->getConnection()->query( + "MOVE FORWARD %sql IN %n", + $offsetSql, + $name + ); + } else { + if($offset === self::FETCH_FOREGOING) { + $offsetSql = "ALL"; + } + return $this->getConnection()->query( + "MOVE BACKWARD %sql IN %n", + $offsetSql, + $name + ); + } + } + // ----------------- FETCH --------------------- public function fetchRange(string $name, int $offset) : array diff --git a/src/ICursor.php b/src/ICursor.php index 67ae8f55b8d35686df7404ab3e97e06c238454a6..730382c1fa681a6d7340f98df6f7d33fb1a03bb2 100644 --- a/src/ICursor.php +++ b/src/ICursor.php @@ -57,6 +57,13 @@ interface ICursor */ public function moveFromEndTo(int $index); + /** + * Move cursor by given offset and return number of scrolled rows + * @param int $offset + * @return int The number of scrolled rows + */ + public function scroll(int $offset) : int; + /** * Fetch the next/previous count of rows. If zero given current row is fetched. * Cursor position will be on the last fetched row. diff --git a/src/SemanticCursor.php b/src/SemanticCursor.php index 2c618e1f5ed97802709f8ebf9641ea9577804dfd..7e3244b0ff7210ad878caa774eca370f0f07cec7 100644 --- a/src/SemanticCursor.php +++ b/src/SemanticCursor.php @@ -227,6 +227,12 @@ class SemanticCursor implements ICursor $this->cursor->moveBy($offset); } + /** {@inheritdoc} */ + public function scroll(int $offset) : int + { + return $this->cursor->scroll($offset); + } + /** {@inheritdoc} */ public function fetchRange(int $offset) : array { diff --git a/src/TrackedCursor.php b/src/TrackedCursor.php index cf92d013e5b24f808e10fe4cb883e359e5eebc04..4352aca0c5f20eac9a72c0de126661135bd919af 100644 --- a/src/TrackedCursor.php +++ b/src/TrackedCursor.php @@ -83,7 +83,6 @@ class TrackedCursor implements ICursor public function moveBy(int $offset) { - // todo: use MOVE FORWARD n IN ... which returns number of rows read $this->cursor->moveBy($offset); if ($this->cursor->isOnRecord()) { $this->position = Position::relativeTo($this->position, $offset); @@ -103,6 +102,26 @@ class TrackedCursor implements ICursor throw CursorException::cursorPosition_unknownError(); } + public function scroll(int $offset) : int + { + $wasOnRecord = $this->isOnRecord(); + $rowsRead = $cursorDelta = $this->cursor->scroll($offset); + $forward = $offset >= 0; + + // scroll over the whole data-set + $scrolledOverWholeSet = $rowsRead > 0 && $wasOnRecord === FALSE && !$this->isOnRecord(); + $wasOnRecordAndHitEnd = $wasOnRecord === TRUE && !$this->isOnRecord(); + if($scrolledOverWholeSet || $wasOnRecordAndHitEnd) { + $cursorDelta++; + } + + $this->position = Position::relativeTo( + $this->position, + ($forward ? 1 : -1) * $cursorDelta + ); + return $rowsRead; + } + public function fetchRange(int $offset): array { $result = $this->cursor->fetchRange($offset); diff --git a/tests/Cursor/Driver/ArrayCursorDriver.php b/tests/Cursor/Driver/ArrayCursorDriver.php index 663988e0380dc900c96d62e26bf7e1a7ea42b4c5..8c29678094f03c149b3d737798a074e0f22d6fcd 100644 --- a/tests/Cursor/Driver/ArrayCursorDriver.php +++ b/tests/Cursor/Driver/ArrayCursorDriver.php @@ -148,6 +148,12 @@ class ArrayCursorDriver implements ICursorDriver return !!$this->retrieveCurrentRow($name); } + public function scroll(string $name, int $offset) : int + { + if($offset === 0) return 0; + return count($this->fetchRange($name, $offset)); + } + // ----------------- FETCH --------------------- public function fetchRange(string $name, int $offset) : array diff --git a/tests/Cursor/ICursorTest.php b/tests/Cursor/ICursorTest.php index 9749ae564de34a8929a73d7ce5ae72bd39219fad..ec82cb1d5a029efd9882bee30ec7dbe1a759c71a 100644 --- a/tests/Cursor/ICursorTest.php +++ b/tests/Cursor/ICursorTest.php @@ -183,6 +183,49 @@ abstract class ICursorTest extends BaseTest $this->uut->moveBy(-1); Assert::null($this->helper_fetchCurrentSingle()); } + + // scroll() + public function test_givenInitialPosition_whenMoveToEnd_thenGetTotalCount() + { + Assert::same(1000, $this->uut->scroll(ICursor::FETCH_REMAINING)); + Assert::null($this->helper_fetchCurrentSingle()); + } + + public function test_givenFifthPosition_whenMoveToEndByIncrements_thenGetRestOfCount() + { + $this->uut->moveTo(5); + Assert::same(5, $this->uut->scroll(5)); + Assert::same(10, $this->helper_fetchCurrentSingle()); + + Assert::same(989, $this->uut->scroll(989)); + Assert::same(999, $this->helper_fetchCurrentSingle()); + + Assert::same(1, $this->uut->scroll(1)); + Assert::same(1000, $this->helper_fetchCurrentSingle()); + + Assert::same(0, $this->uut->scroll(1)); + Assert::null($this->helper_fetchCurrentSingle()); + + Assert::same(0, $this->uut->scroll(1)); + } + + public function test_givenFifthPosition_whenScrollZero_thenGetOne() + { + $this->uut->moveTo(5); + Assert::same(0, $this->uut->scroll(0)); + } + + public function test_givenFifthPosition_whenMoveToBeginning_thenGetForegoingNumberOfRows() + { + $this->uut->moveTo(5); + + Assert::same(4, $this->uut->scroll(ICursor::FETCH_FOREGOING)); + } + + public function test_givenBeginningPosition_whenMoveToBeginning_thenGetZero() + { + Assert::same(0, $this->uut->scroll(ICursor::FETCH_FOREGOING)); + } // fetchOneAt() diff --git a/tests/Cursor/SemanticCursorIntegration.phpt b/tests/Cursor/SemanticCursorIntegration.phpt index 5cf8cb2715fbc11f5c7d9c1ed0ceecaf3d7fbdc5..eef253da3fd04e483a8c4b0de3f3d6ec446bd929 100644 --- a/tests/Cursor/SemanticCursorIntegration.phpt +++ b/tests/Cursor/SemanticCursorIntegration.phpt @@ -6,7 +6,6 @@ namespace Grifart\Mappi\Tests\Cursor; use Grifart\Mappi\Cursor\Cursor; -use Grifart\Mappi\Tests\Cursor\Driver\ArrayCursorDriver; use Grifart\Mappi\Cursor\SemanticCursor; use Mockery; diff --git a/tests/Cursor/TrackedCursorTest.phpt b/tests/Cursor/TrackedCursorTest.phpt index dedd795ac33e7d42403f39b8fe25968705e6c00b..1e3e21bd644a8d077f8aa10388d814de92f13a33 100644 --- a/tests/Cursor/TrackedCursorTest.phpt +++ b/tests/Cursor/TrackedCursorTest.phpt @@ -3,14 +3,13 @@ * @testCase */ -// TODO: moveBy() ->isOnRecord() ->moveBy() ->isOnRecord() (now I see state before and after command) // TODO: Maybe construct Cursor class in TrackedCursor constructor? namespace Grifart\Mappi\Tests\Cursor; use Grifart\Mappi\Cursor\Cursor; +use Grifart\Mappi\Cursor\ICursor; use Grifart\Mappi\Cursor\Position; -use Grifart\Mappi\Tests\Cursor\Driver\ArrayCursorDriver; use Grifart\Mappi\Cursor\TrackedCursor; use Tester\Assert; @@ -19,14 +18,37 @@ require_once __DIR__ . "/ICursorTest.php"; class TrackedCursorTest extends ICursorTest { - /** @link https://en.wikipedia.org/wiki/42_(number)#Hitchhiker.27s_Guide_to_the_Galaxy */ - const THE_MAGIC_NUMBER = 42; - /** @var TrackedCursor */ protected $uut; + // test with underlying postgres driver +// protected function setUp() +// { +// global $connection, $SQL_thousandRowsAscending; +// try{ +// $connection->begin(); +// +// $factory = new PostgresCursorFactory($connection); +// $cursor = $factory->create($SQL_thousandRowsAscending, true); +// $this->uut = new TrackedCursor($cursor, Position::fromLeft(0)); +// } catch (DriverException $e) { +// Environment::skip("It looks like you haven't properly set-up dibi connection to PostgreSQL. Check tests/bootstrap.php"); +// } +// +// parent::setUp(); +// } +// +// public function tearDown() +// { +// global $connection; +// $connection->rollback(); +// +// parent::tearDown(); +// } + protected function setUp() { + $driver = new Driver\ArrayCursorDriver(); $driver->createTestCursor("test", 1000); $cursor = new Cursor( @@ -149,6 +171,81 @@ class TrackedCursorTest extends ICursorTest $this->assertPosition("left:0"); } + public function test_getPosition_left_scroll() + { + Assert::same(0, $this->uut->scroll(-5)); + $this->assertPosition("left:0"); + + Assert::same(0, $this->uut->scroll(0)); + $this->assertPosition("left:0"); + + Assert::same(1, $this->uut->scroll(1)); + $this->assertPosition("left:1"); + + Assert::same(0, $this->uut->scroll(0)); + $this->assertPosition("left:1"); + + Assert::same(0, $this->uut->scroll(-1)); + $this->assertPosition("left:0"); + + Assert::same(1, $this->uut->scroll(1)); + $this->assertPosition("left:1"); + + Assert::same(999, $this->uut->scroll(999)); + Assert::same(1000, $this->helper_fetchCurrentSingle()); // regression + $this->assertPosition("left:1000"); + + Assert::same(0, $this->uut->scroll(0)); + $this->assertPosition("left:1000"); + + Assert::same(0, $this->uut->scroll(1)); + $this->assertPosition("left:1001"); + + Assert::same(0, $this->uut->scroll(1)); + $this->assertPosition("left:1001"); + + Assert::same(1, $this->uut->scroll(-1)); + $this->assertPosition("left:1000"); + + Assert::same(0, $this->uut->scroll(1)); + $this->assertPosition("left:1001"); + + Assert::same(1000, $this->uut->scroll(ICursor::FETCH_FOREGOING)); + $this->assertPosition("left:0"); + + Assert::same(1000, $this->uut->scroll(ICursor::FETCH_REMAINING)); + $this->assertPosition("left:1001"); + + Assert::same(1000, $this->uut->scroll(ICursor::FETCH_FOREGOING)); + $this->assertPosition("left:0"); + } + + public function test_getPosition_right_scroll() + { + $this->uut->moveFromEndTo(0); + $this->assertPosition("right:0"); + + $this->uut->scroll(1); + $this->assertPosition("right:0"); + + $this->uut->scroll(0); + $this->assertPosition("right:0"); + + $this->uut->scroll(-5); + $this->assertPosition("right:5"); + + $this->uut->scroll(-995); + $this->assertPosition("right:1000"); + Assert::equal(1, $this->helper_fetchCurrentSingle()); // regression + + $this->uut->scroll(-1); + $this->assertPosition("right:1001"); + Assert::null($this->helper_fetchCurrentSingle()); // regression + + $this->uut->scroll(-1); + $this->assertPosition("right:1001"); + } + public function test_getPosition_left_fetchRange() { $this->assertPosition("left:0");