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");