Skip to content

Commit

Permalink
Mixed orders in cursor paginate
Browse files Browse the repository at this point in the history
  • Loading branch information
halaei committed Jun 30, 2021
1 parent 165ba51 commit 8d656a8
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 82 deletions.
46 changes: 46 additions & 0 deletions src/Illuminate/Database/Concerns/BuildsQueries.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,52 @@ public function tap($callback)
return $this->when(true, $callback);
}

/**
* Paginate the given query into a cursor paginator.
*
* @param int $perPage
* @param array $columns
* @param string $cursorName
* @param string|null $cursor
* @return \Illuminate\Contracts\Pagination\CursorPaginator
*/
protected function runCursorPaginate($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
{
$cursor = $cursor ?: CursorPaginator::resolveCurrentCursor($cursorName);

$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());

if ($cursor !== null) {
$addCursorConditions = function (self $builder, $prev, $i) use (&$addCursorConditions, $orders, $cursor) {
if ($prev !== null) {
$builder->where($prev, '=', $cursor->parameter($prev));
}

$builder->where(function (self $builder) use ($orders, $cursor, $i, $addCursorConditions) {
['column' => $column, 'direction' => $direction] = $orders[$i];
$operator = $direction === 'asc' ? '>' : '<';

$builder->where($column, $operator, $cursor->parameter($column));

if ($i < $orders->count() - 1) {
$builder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) {
$addCursorConditions($builder, $column, $i + 1);
});
}
});
};
$addCursorConditions($this, null, 0);
}

$this->limit($perPage + 1);

return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
'path' => Paginator::resolveCurrentPath(),
'cursorName' => $cursorName,
'parameters' => $orders->pluck('column')->toArray(),
]);
}

/**
* Create a new length-aware paginator instance.
*
Expand Down
39 changes: 3 additions & 36 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\RecordsNotFoundException;
use Illuminate\Pagination\CursorPaginationException;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
Expand Down Expand Up @@ -828,56 +826,25 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p
* @param string $cursorName
* @param string|null $cursor
* @return \Illuminate\Contracts\Pagination\CursorPaginator
* @throws \Illuminate\Pagination\CursorPaginationException
*/
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
{
$cursor = $cursor ?: CursorPaginator::resolveCurrentCursor($cursorName);

$perPage = $perPage ?: $this->model->getPerPage();

$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());

$orderDirection = $orders->first()['direction'] ?? 'asc';

$comparisonOperator = $orderDirection === 'asc' ? '>' : '<';

$parameters = $orders->pluck('column')->toArray();

if (! is_null($cursor)) {
if (count($parameters) === 1) {
$this->where($column = $parameters[0], $comparisonOperator, $cursor->parameter($column));
} elseif (count($parameters) > 1) {
$this->whereRowValues($parameters, $comparisonOperator, $cursor->parameters($parameters));
}
}

$this->take($perPage + 1);

return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
'path' => Paginator::resolveCurrentPath(),
'cursorName' => $cursorName,
'parameters' => $parameters,
]);
return $this->runCursorPaginate($perPage, $columns, $cursorName, $cursor);
}

/**
* Ensure the proper order by required for cursor pagination.
*
* @param bool $shouldReverse
* @return \Illuminate\Support\Collection
*
* @throws \Illuminate\Pagination\CursorPaginationException
*/
protected function ensureOrderForCursorPagination($shouldReverse = false)
{
$orderDirections = collect($this->query->orders)->pluck('direction')->unique();

if ($orderDirections->count() > 1) {
throw new CursorPaginationException('Only a single order by direction is supported when using cursor pagination.');
}
$orders = collect($this->query->orders);

if ($orderDirections->count() === 0) {
if ($orders->count() === 0) {
$this->enforceOrderBy();
}

Expand Down
36 changes: 1 addition & 35 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Database\Query\Processors\Processor;
use Illuminate\Pagination\CursorPaginationException;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
Expand Down Expand Up @@ -2408,54 +2406,22 @@ public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'pag
* @param string $cursorName
* @param string|null $cursor
* @return \Illuminate\Contracts\Pagination\CursorPaginator
* @throws \Illuminate\Pagination\CursorPaginationException
*/
public function cursorPaginate($perPage = 15, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
{
$cursor = $cursor ?: CursorPaginator::resolveCurrentCursor($cursorName);

$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());

$orderDirection = $orders->first()['direction'] ?? 'asc';

$comparisonOperator = $orderDirection === 'asc' ? '>' : '<';

$parameters = $orders->pluck('column')->toArray();

if (! is_null($cursor)) {
if (count($parameters) === 1) {
$this->where($column = $parameters[0], $comparisonOperator, $cursor->parameter($column));
} elseif (count($parameters) > 1) {
$this->whereRowValues($parameters, $comparisonOperator, $cursor->parameters($parameters));
}
}

$this->limit($perPage + 1);

return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
'path' => Paginator::resolveCurrentPath(),
'cursorName' => $cursorName,
'parameters' => $parameters,
]);
return $this->runCursorPaginate($perPage, $columns, $cursorName, $cursor);
}

/**
* Ensure the proper order by required for cursor pagination.
*
* @param bool $shouldReverse
* @return \Illuminate\Support\Collection
* @throws \Illuminate\Pagination\CursorPaginationException
*/
protected function ensureOrderForCursorPagination($shouldReverse = false)
{
$this->enforceOrderBy();

$orderDirections = collect($this->orders)->pluck('direction')->unique();

if ($orderDirections->count() > 1) {
throw new CursorPaginationException('Only a single order by direction is supported when using cursor pagination.');
}

if ($shouldReverse) {
$this->orders = collect($this->orders)->map(function ($order) {
$order['direction'] = $order['direction'] === 'asc' ? 'desc' : 'asc';
Expand Down
3 changes: 3 additions & 0 deletions src/Illuminate/Pagination/CursorPaginationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

use RuntimeException;

/**
* @deprecated Will be removed in a future Laravel version.
*/
class CursorPaginationException extends RuntimeException
{
//
Expand Down
69 changes: 58 additions & 11 deletions tests/Database/DatabaseQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3662,13 +3662,24 @@ public function testCursorPaginate()
$columns = ['test'];
$cursorName = 'cursor-name';
$cursor = new Cursor(['test' => 'bar']);
$builder = $this->getMockQueryBuilder()->orderBy('test');
$builder = $this->getMockQueryBuilder();
$builder->from('foobar')->orderBy('test');
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
return new Builder($builder->connection, $builder->grammar, $builder->processor);
});

$path = 'http://foo.bar?cursor='.$cursor->encode();

$results = collect([['test' => 'foo'], ['test' => 'bar']]);

$builder->shouldReceive('where')->with('test', '>', 'bar')->once()->andReturnSelf();
$builder->shouldReceive('get')->once()->andReturn($results);
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) {
$this->assertEquals(
'select * from "foobar" where ("test" > ?) order by "test" asc limit 17',
$builder->toSql());
$this->assertEquals(['bar'], $builder->bindings['where']);

return $results;
});

Paginator::currentPathResolver(function () use ($path) {
return $path;
Expand All @@ -3689,13 +3700,25 @@ public function testCursorPaginateMultipleOrderColumns()
$columns = ['test'];
$cursorName = 'cursor-name';
$cursor = new Cursor(['test' => 'bar', 'another' => 'foo']);
$builder = $this->getMockQueryBuilder()->orderBy('test')->orderBy('another');
$builder = $this->getMockQueryBuilder();
$builder->from('foobar')->orderBy('test')->orderBy('another');
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
return new Builder($builder->connection, $builder->grammar, $builder->processor);
});

$path = 'http://foo.bar?cursor='.$cursor->encode();

$results = collect([['test' => 'foo'], ['test' => 'bar']]);

$builder->shouldReceive('whereRowValues')->with(['test', 'another'], '>', ['bar', 'foo'])->once()->andReturnSelf();
$builder->shouldReceive('get')->once()->andReturn($results);
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) {
$this->assertEquals(
'select * from "foobar" where ("test" > ? or ("test" = ? and ("another" > ?))) order by "test" asc, "another" asc limit 17',
$builder->toSql()
);
$this->assertEquals(['bar', 'bar', 'foo'], $builder->bindings['where']);

return $results;
});

Paginator::currentPathResolver(function () use ($path) {
return $path;
Expand All @@ -3715,12 +3738,24 @@ public function testCursorPaginateWithDefaultArguments()
$perPage = 15;
$cursorName = 'cursor';
$cursor = new Cursor(['test' => 'bar']);
$builder = $this->getMockQueryBuilder()->orderBy('test');
$builder = $this->getMockQueryBuilder();
$builder->from('foobar')->orderBy('test');
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
return new Builder($builder->connection, $builder->grammar, $builder->processor);
});

$path = 'http://foo.bar?cursor='.$cursor->encode();

$results = collect([['test' => 'foo'], ['test' => 'bar']]);

$builder->shouldReceive('get')->once()->andReturn($results);
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) {
$this->assertEquals(
'select * from "foobar" where ("test" > ?) order by "test" asc limit 16',
$builder->toSql());
$this->assertEquals(['bar'], $builder->bindings['where']);

return $results;
});

CursorPaginator::currentCursorResolver(function () use ($cursor) {
return $cursor;
Expand Down Expand Up @@ -3773,12 +3808,24 @@ public function testCursorPaginateWithSpecificColumns()
$columns = ['id', 'name'];
$cursorName = 'cursor-name';
$cursor = new Cursor(['id' => 2]);
$builder = $this->getMockQueryBuilder()->orderBy('id');
$builder = $this->getMockQueryBuilder();
$builder->from('foobar')->orderBy('id');
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
return new Builder($builder->connection, $builder->grammar, $builder->processor);
});

$path = 'http://foo.bar?cursor=3';

$results = collect([['id' => 3, 'name' => 'Taylor'], ['id' => 5, 'name' => 'Mohamed']]);

$builder->shouldReceive('get')->once()->andReturn($results);
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) {
$this->assertEquals(
'select * from "foobar" where ("id" > ?) order by "id" asc limit 17',
$builder->toSql());
$this->assertEquals([2], $builder->bindings['where']);

return $results;
});

Paginator::currentPathResolver(function () use ($path) {
return $path;
Expand Down Expand Up @@ -4185,7 +4232,7 @@ protected function getMySqlBuilderWithProcessor()
}

/**
* @return m\MockInterface
* @return m\MockInterface|Builder
*/
protected function getMockQueryBuilder()
{
Expand Down

0 comments on commit 8d656a8

Please sign in to comment.