Skip to content

Commit cce9fe3

Browse files
committed
Mixed orders in cursor paginate
1 parent 952f84a commit cce9fe3

File tree

5 files changed

+112
-83
lines changed

5 files changed

+112
-83
lines changed

src/Illuminate/Database/Concerns/BuildsQueries.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,52 @@ public function tap($callback)
292292
return $this->when(true, $callback);
293293
}
294294

295+
/**
296+
* Paginate the given query into a cursor paginator.
297+
*
298+
* @param int $perPage
299+
* @param array $columns
300+
* @param string $cursorName
301+
* @param string|null $cursor
302+
* @return \Illuminate\Contracts\Pagination\CursorPaginator
303+
*/
304+
protected function runCursorPaginate($perPage, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
305+
{
306+
$cursor = $cursor ?: CursorPaginator::resolveCurrentCursor($cursorName);
307+
308+
$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
309+
310+
if ($cursor !== null) {
311+
$addCursorConditions = function (self $builder, $prev, $i) use (&$addCursorConditions, $orders, $cursor) {
312+
if ($prev !== null) {
313+
$builder->where($prev, '=', $cursor->parameter($prev));
314+
}
315+
316+
$builder->where(function (self $builder) use ($orders, $cursor, $i, $addCursorConditions) {
317+
['column' => $column, 'direction' => $direction] = $orders[$i];
318+
$operator = $direction === 'asc' ? '>' : '<';
319+
320+
$builder->where($column, $operator, $cursor->parameter($column));
321+
322+
if ($i < $orders->count() - 1) {
323+
$builder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) {
324+
$addCursorConditions($builder, $column, $i + 1);
325+
});
326+
}
327+
});
328+
};
329+
$addCursorConditions($this, null, 0);
330+
}
331+
332+
$this->limit($perPage + 1);
333+
334+
return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
335+
'path' => Paginator::resolveCurrentPath(),
336+
'cursorName' => $cursorName,
337+
'parameters' => $orders->pluck('column')->toArray(),
338+
]);
339+
}
340+
295341
/**
296342
* Create a new length-aware paginator instance.
297343
*

src/Illuminate/Database/Eloquent/Builder.php

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
use Illuminate\Database\Eloquent\Relations\Relation;
1313
use Illuminate\Database\Query\Builder as QueryBuilder;
1414
use Illuminate\Database\RecordsNotFoundException;
15-
use Illuminate\Pagination\CursorPaginationException;
16-
use Illuminate\Pagination\CursorPaginator;
1715
use Illuminate\Pagination\Paginator;
1816
use Illuminate\Support\Arr;
1917
use Illuminate\Support\Str;
@@ -828,56 +826,25 @@ public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'p
828826
* @param string $cursorName
829827
* @param string|null $cursor
830828
* @return \Illuminate\Contracts\Pagination\CursorPaginator
831-
* @throws \Illuminate\Pagination\CursorPaginationException
832829
*/
833830
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
834831
{
835-
$cursor = $cursor ?: CursorPaginator::resolveCurrentCursor($cursorName);
836-
837832
$perPage = $perPage ?: $this->model->getPerPage();
838833

839-
$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
840-
841-
$orderDirection = $orders->first()['direction'] ?? 'asc';
842-
843-
$comparisonOperator = $orderDirection === 'asc' ? '>' : '<';
844-
845-
$parameters = $orders->pluck('column')->toArray();
846-
847-
if (! is_null($cursor)) {
848-
if (count($parameters) === 1) {
849-
$this->where($column = $parameters[0], $comparisonOperator, $cursor->parameter($column));
850-
} elseif (count($parameters) > 1) {
851-
$this->whereRowValues($parameters, $comparisonOperator, $cursor->parameters($parameters));
852-
}
853-
}
854-
855-
$this->take($perPage + 1);
856-
857-
return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
858-
'path' => Paginator::resolveCurrentPath(),
859-
'cursorName' => $cursorName,
860-
'parameters' => $parameters,
861-
]);
834+
return $this->runCursorPaginate($perPage, $columns, $cursorName, $cursor);
862835
}
863836

864837
/**
865838
* Ensure the proper order by required for cursor pagination.
866839
*
867840
* @param bool $shouldReverse
868841
* @return \Illuminate\Support\Collection
869-
*
870-
* @throws \Illuminate\Pagination\CursorPaginationException
871842
*/
872843
protected function ensureOrderForCursorPagination($shouldReverse = false)
873844
{
874-
$orderDirections = collect($this->query->orders)->pluck('direction')->unique();
875-
876-
if ($orderDirections->count() > 1) {
877-
throw new CursorPaginationException('Only a single order by direction is supported when using cursor pagination.');
878-
}
845+
$orders = collect($this->query->orders);
879846

880-
if ($orderDirections->count() === 0) {
847+
if ($orders->count() === 0) {
881848
$this->enforceOrderBy();
882849
}
883850

src/Illuminate/Database/Query/Builder.php

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
use Illuminate\Database\Eloquent\Relations\Relation;
1313
use Illuminate\Database\Query\Grammars\Grammar;
1414
use Illuminate\Database\Query\Processors\Processor;
15-
use Illuminate\Pagination\CursorPaginationException;
16-
use Illuminate\Pagination\CursorPaginator;
1715
use Illuminate\Pagination\Paginator;
1816
use Illuminate\Support\Arr;
1917
use Illuminate\Support\Collection;
@@ -2407,55 +2405,23 @@ public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'pag
24072405
* @param array $columns
24082406
* @param string $cursorName
24092407
* @param string|null $cursor
2410-
* @return \Illuminate\Contracts\Pagination\Paginator
2411-
* @throws \Illuminate\Pagination\CursorPaginationException
2408+
* @return \Illuminate\Contracts\Pagination\CursorPaginator
24122409
*/
24132410
public function cursorPaginate($perPage = 15, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
24142411
{
2415-
$cursor = $cursor ?: CursorPaginator::resolveCurrentCursor($cursorName);
2416-
2417-
$orders = $this->ensureOrderForCursorPagination(! is_null($cursor) && $cursor->pointsToPreviousItems());
2418-
2419-
$orderDirection = $orders->first()['direction'] ?? 'asc';
2420-
2421-
$comparisonOperator = $orderDirection === 'asc' ? '>' : '<';
2422-
2423-
$parameters = $orders->pluck('column')->toArray();
2424-
2425-
if (! is_null($cursor)) {
2426-
if (count($parameters) === 1) {
2427-
$this->where($column = $parameters[0], $comparisonOperator, $cursor->parameter($column));
2428-
} elseif (count($parameters) > 1) {
2429-
$this->whereRowValues($parameters, $comparisonOperator, $cursor->parameters($parameters));
2430-
}
2431-
}
2432-
2433-
$this->limit($perPage + 1);
2434-
2435-
return $this->cursorPaginator($this->get($columns), $perPage, $cursor, [
2436-
'path' => Paginator::resolveCurrentPath(),
2437-
'cursorName' => $cursorName,
2438-
'parameters' => $parameters,
2439-
]);
2412+
return $this->runCursorPaginate($perPage, $columns, $cursorName, $cursor);
24402413
}
24412414

24422415
/**
24432416
* Ensure the proper order by required for cursor pagination.
24442417
*
24452418
* @param bool $shouldReverse
24462419
* @return \Illuminate\Support\Collection
2447-
* @throws \Illuminate\Pagination\CursorPaginationException
24482420
*/
24492421
protected function ensureOrderForCursorPagination($shouldReverse = false)
24502422
{
24512423
$this->enforceOrderBy();
24522424

2453-
$orderDirections = collect($this->orders)->pluck('direction')->unique();
2454-
2455-
if ($orderDirections->count() > 1) {
2456-
throw new CursorPaginationException('Only a single order by direction is supported when using cursor pagination.');
2457-
}
2458-
24592425
if ($shouldReverse) {
24602426
$this->orders = collect($this->orders)->map(function ($order) {
24612427
$order['direction'] = $order['direction'] === 'asc' ? 'desc' : 'asc';

src/Illuminate/Pagination/CursorPaginationException.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
use RuntimeException;
66

7+
/**
8+
* @deprecated Will be removed in a future Laravel version.
9+
*/
710
class CursorPaginationException extends RuntimeException
811
{
912
//

tests/Database/DatabaseQueryBuilderTest.php

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3619,13 +3619,24 @@ public function testCursorPaginate()
36193619
$columns = ['test'];
36203620
$cursorName = 'cursor-name';
36213621
$cursor = new Cursor(['test' => 'bar']);
3622-
$builder = $this->getMockQueryBuilder()->orderBy('test');
3622+
$builder = $this->getMockQueryBuilder();
3623+
$builder->from('foobar')->orderBy('test');
3624+
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
3625+
return new Builder($builder->connection, $builder->grammar, $builder->processor);
3626+
});
3627+
36233628
$path = 'http://foo.bar?cursor='.$cursor->encode();
36243629

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

3627-
$builder->shouldReceive('where')->with('test', '>', 'bar')->once()->andReturnSelf();
3628-
$builder->shouldReceive('get')->once()->andReturn($results);
3632+
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) {
3633+
$this->assertEquals(
3634+
'select * from "foobar" where ("test" > ?) order by "test" asc limit 17',
3635+
$builder->toSql());
3636+
$this->assertEquals(['bar'], $builder->bindings['where']);
3637+
3638+
return $results;
3639+
});
36293640

36303641
Paginator::currentPathResolver(function () use ($path) {
36313642
return $path;
@@ -3646,13 +3657,25 @@ public function testCursorPaginateMultipleOrderColumns()
36463657
$columns = ['test'];
36473658
$cursorName = 'cursor-name';
36483659
$cursor = new Cursor(['test' => 'bar', 'another' => 'foo']);
3649-
$builder = $this->getMockQueryBuilder()->orderBy('test')->orderBy('another');
3660+
$builder = $this->getMockQueryBuilder();
3661+
$builder->from('foobar')->orderBy('test')->orderBy('another');
3662+
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
3663+
return new Builder($builder->connection, $builder->grammar, $builder->processor);
3664+
});
3665+
36503666
$path = 'http://foo.bar?cursor='.$cursor->encode();
36513667

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

3654-
$builder->shouldReceive('whereRowValues')->with(['test', 'another'], '>', ['bar', 'foo'])->once()->andReturnSelf();
3655-
$builder->shouldReceive('get')->once()->andReturn($results);
3670+
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) {
3671+
$this->assertEquals(
3672+
'select * from "foobar" where ("test" > ? or ("test" = ? and ("another" > ?))) order by "test" asc, "another" asc limit 17',
3673+
$builder->toSql()
3674+
);
3675+
$this->assertEquals(['bar', 'bar', 'foo'], $builder->bindings['where']);
3676+
3677+
return $results;
3678+
});
36563679

36573680
Paginator::currentPathResolver(function () use ($path) {
36583681
return $path;
@@ -3672,12 +3695,24 @@ public function testCursorPaginateWithDefaultArguments()
36723695
$perPage = 15;
36733696
$cursorName = 'cursor';
36743697
$cursor = new Cursor(['test' => 'bar']);
3675-
$builder = $this->getMockQueryBuilder()->orderBy('test');
3698+
$builder = $this->getMockQueryBuilder();
3699+
$builder->from('foobar')->orderBy('test');
3700+
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
3701+
return new Builder($builder->connection, $builder->grammar, $builder->processor);
3702+
});
3703+
36763704
$path = 'http://foo.bar?cursor='.$cursor->encode();
36773705

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

3680-
$builder->shouldReceive('get')->once()->andReturn($results);
3708+
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) {
3709+
$this->assertEquals(
3710+
'select * from "foobar" where ("test" > ?) order by "test" asc limit 16',
3711+
$builder->toSql());
3712+
$this->assertEquals(['bar'], $builder->bindings['where']);
3713+
3714+
return $results;
3715+
});
36813716

36823717
CursorPaginator::currentCursorResolver(function () use ($cursor) {
36833718
return $cursor;
@@ -3730,12 +3765,24 @@ public function testCursorPaginateWithSpecificColumns()
37303765
$columns = ['id', 'name'];
37313766
$cursorName = 'cursor-name';
37323767
$cursor = new Cursor(['id' => 2]);
3733-
$builder = $this->getMockQueryBuilder()->orderBy('id');
3768+
$builder = $this->getMockQueryBuilder();
3769+
$builder->from('foobar')->orderBy('id');
3770+
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
3771+
return new Builder($builder->connection, $builder->grammar, $builder->processor);
3772+
});
3773+
37343774
$path = 'http://foo.bar?cursor=3';
37353775

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

3738-
$builder->shouldReceive('get')->once()->andReturn($results);
3778+
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results) {
3779+
$this->assertEquals(
3780+
'select * from "foobar" where ("id" > ?) order by "id" asc limit 17',
3781+
$builder->toSql());
3782+
$this->assertEquals([2], $builder->bindings['where']);
3783+
3784+
return $results;
3785+
});
37393786

37403787
Paginator::currentPathResolver(function () use ($path) {
37413788
return $path;
@@ -4142,7 +4189,7 @@ protected function getMySqlBuilderWithProcessor()
41424189
}
41434190

41444191
/**
4145-
* @return m\MockInterface
4192+
* @return m\MockInterface|Builder
41464193
*/
41474194
protected function getMockQueryBuilder()
41484195
{

0 commit comments

Comments
 (0)