Skip to content

Commit

Permalink
[8.x] apply where's from union query builder in cursor pagination (#4…
Browse files Browse the repository at this point in the history
…2651)

* [8.x] apply where's from union query builder in cursor pagination

* fixed Property [unions] does not exist on the Eloquent builder instance.

* apply StyleCI fixes

Co-authored-by: Stefan Heimann <s.heimann@sportradar.com>
  • Loading branch information
stefanheimann and Stefan Heimann authored Jun 9, 2022
1 parent 5fcb8fb commit 300802f
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 7 deletions.
32 changes: 31 additions & 1 deletion src/Illuminate/Database/Concerns/BuildsQueries.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,15 +339,27 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName =

if (! is_null($cursor)) {
$addCursorConditions = function (self $builder, $previousColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
$unionBuilders = isset($builder->unions) ? collect($builder->unions)->pluck('query') : collect();

if (! is_null($previousColumn)) {
$builder->where(
$this->getOriginalColumnNameForCursorPagination($this, $previousColumn),
'=',
$cursor->parameter($previousColumn)
);

$unionBuilders->each(function ($unionBuilder) use($previousColumn, $cursor) {
$unionBuilder->where(
$this->getOriginalColumnNameForCursorPagination($this, $previousColumn),
'=',
$cursor->parameter($previousColumn)
);

$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
}

$builder->where(function (self $builder) use ($addCursorConditions, $cursor, $orders, $i) {
$builder->where(function (self $builder) use ($addCursorConditions, $cursor, $orders, $i, $unionBuilders) {
['column' => $column, 'direction' => $direction] = $orders[$i];

$builder->where(
Expand All @@ -361,6 +373,24 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName =
$addCursorConditions($builder, $column, $i + 1);
});
}

$unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions){
$unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
$unionBuilder->where(
$this->getOriginalColumnNameForCursorPagination($this, $column),
$direction === 'asc' ? '>' : '<',
$cursor->parameter($column)
);

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

$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
});
});
});
};

Expand Down
12 changes: 6 additions & 6 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2487,15 +2487,15 @@ protected function ensureOrderForCursorPagination($shouldReverse = false)
{
$this->enforceOrderBy();

if ($shouldReverse) {
$this->orders = collect($this->orders)->map(function ($order) {
return collect($this->orders ?? $this->unionOrders ?? [])->filter(function ($order) {
return Arr::has($order, 'direction');
})->when($shouldReverse, function (Collection $orders) {
return $orders->map(function ($order) {
$order['direction'] = $order['direction'] === 'asc' ? 'desc' : 'asc';

return $order;
})->toArray();
}

return collect($this->orders);
});
})->values();
}

/**
Expand Down
184 changes: 184 additions & 0 deletions tests/Database/DatabaseQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4023,6 +4023,190 @@ public function testCursorPaginateWithMixedOrders()
]), $result);
}

public function testCursorPaginateWithUnionWheres()
{
$ts = now()->toDateTimeString();

$perPage = 16;
$columns = ['test'];
$cursorName = 'cursor-name';
$cursor = new Cursor(['created_at' => $ts]);
$builder = $this->getMockQueryBuilder();
$builder->select('id', 'start_time as created_at')->selectRaw("'video' as type")->from('videos');
$builder->union($this->getBuilder()->select('id', 'created_at')->selectRaw("'news' as type")->from('news'));
$builder->orderBy('created_at');

$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([
['id' => 1, 'created_at' => now(), 'type' => 'video'],
['id' => 2, 'created_at' => now(), 'type' => 'news'],
]);

$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) {
$this->assertEquals(
'(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" > ?)) union (select "id", "created_at", \'news\' as type from "news" where ("start_time" > ?)) order by "created_at" asc limit 17',
$builder->toSql());
$this->assertEquals([$ts], $builder->bindings['where']);
$this->assertEquals([$ts], $builder->bindings['union']);
return $results;
});

Paginator::currentPathResolver(function () use ($path) {
return $path;
});

$result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor);

$this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [
'path' => $path,
'cursorName' => $cursorName,
'parameters' => ['created_at'],
]), $result);
}

public function testCursorPaginateWithUnionWheresWithRawOrderExpression()
{
$ts = now()->toDateTimeString();

$perPage = 16;
$columns = ['test'];
$cursorName = 'cursor-name';
$cursor = new Cursor(['created_at' => $ts]);
$builder = $this->getMockQueryBuilder();
$builder->select('id', 'is_published', 'start_time as created_at')->selectRaw("'video' as type")->where('is_published', true)->from('videos');
$builder->union($this->getBuilder()->select('id', 'is_published', 'created_at')->selectRaw("'news' as type")->where('is_published', true)->from('news'));
$builder->orderByRaw('case when (id = 3 and type="news" then 0 else 1 end)')->orderBy('created_at');

$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([
['id' => 1, 'created_at' => now(), 'type' => 'video', 'is_published' => true],
['id' => 2, 'created_at' => now(), 'type' => 'news', 'is_published' => true],
]);

$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) {
$this->assertEquals(
'(select "id", "is_published", "start_time" as "created_at", \'video\' as type from "videos" where "is_published" = ? and ("start_time" > ?)) union (select "id", "is_published", "created_at", \'news\' as type from "news" where "is_published" = ? and ("start_time" > ?)) order by case when (id = 3 and type="news" then 0 else 1 end), "created_at" asc limit 17',
$builder->toSql());
$this->assertEquals([true, $ts], $builder->bindings['where']);
$this->assertEquals([true, $ts], $builder->bindings['union']);
return $results;
});

Paginator::currentPathResolver(function () use ($path) {
return $path;
});

$result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor);

$this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [
'path' => $path,
'cursorName' => $cursorName,
'parameters' => ['created_at'],
]), $result);
}

public function testCursorPaginateWithUnionWheresReverseOrder()
{
$ts = now()->toDateTimeString();

$perPage = 16;
$columns = ['test'];
$cursorName = 'cursor-name';
$cursor = new Cursor(['created_at' => $ts], false);
$builder = $this->getMockQueryBuilder();
$builder->select('id', 'start_time as created_at')->selectRaw("'video' as type")->from('videos');
$builder->union($this->getBuilder()->select('id', 'created_at')->selectRaw("'news' as type")->from('news'));
$builder->orderBy('created_at');

$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([
['id' => 1, 'created_at' => now(), 'type' => 'video'],
['id' => 2, 'created_at' => now(), 'type' => 'news'],
]);

$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) {
$this->assertEquals(
'(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" < ?)) union (select "id", "created_at", \'news\' as type from "news" where ("start_time" < ?)) order by "created_at" asc limit 17',
$builder->toSql());
$this->assertEquals([$ts], $builder->bindings['where']);
$this->assertEquals([$ts], $builder->bindings['union']);
return $results;
});

Paginator::currentPathResolver(function () use ($path) {
return $path;
});

$result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor);

$this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [
'path' => $path,
'cursorName' => $cursorName,
'parameters' => ['created_at'],
]), $result);
}

public function testCursorPaginateWithUnionWheresMultipleOrders()
{
$ts = now()->toDateTimeString();

$perPage = 16;
$columns = ['test'];
$cursorName = 'cursor-name';
$cursor = new Cursor(['created_at' => $ts, 'id' => 1]);
$builder = $this->getMockQueryBuilder();
$builder->select('id', 'start_time as created_at')->selectRaw("'video' as type")->from('videos');
$builder->union($this->getBuilder()->select('id', 'created_at')->selectRaw("'news' as type")->from('news'));
$builder->orderByDesc('created_at')->orderBy('id');

$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([
['id' => 1, 'created_at' => now(), 'type' => 'video'],
['id' => 2, 'created_at' => now(), 'type' => 'news'],
]);

$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) {
$this->assertEquals(
'(select "id", "start_time" as "created_at", \'video\' as type from "videos" where ("start_time" < ? or ("start_time" = ? and ("id" > ?)))) union (select "id", "created_at", \'news\' as type from "news" where ("start_time" < ? or ("start_time" = ? and ("id" > ?)))) order by "created_at" desc, "id" asc limit 17',
$builder->toSql());
$this->assertEquals([$ts, $ts, 1], $builder->bindings['where']);
$this->assertEquals([$ts, $ts, 1], $builder->bindings['union']);
return $results;
});

Paginator::currentPathResolver(function () use ($path) {
return $path;
});

$result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor);

$this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [
'path' => $path,
'cursorName' => $cursorName,
'parameters' => ['created_at', 'id'],
]), $result);
}

public function testWhereRowValues()
{
$builder = $this->getBuilder();
Expand Down

0 comments on commit 300802f

Please sign in to comment.