Skip to content

Commit 300802f

Browse files
stefanheimannStefan Heimann
andauthored
[8.x] apply where's from union query builder in cursor pagination (#42651)
* [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 <[email protected]>
1 parent 5fcb8fb commit 300802f

File tree

3 files changed

+221
-7
lines changed

3 files changed

+221
-7
lines changed

src/Illuminate/Database/Concerns/BuildsQueries.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,15 +339,27 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName =
339339

340340
if (! is_null($cursor)) {
341341
$addCursorConditions = function (self $builder, $previousColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
342+
$unionBuilders = isset($builder->unions) ? collect($builder->unions)->pluck('query') : collect();
343+
342344
if (! is_null($previousColumn)) {
343345
$builder->where(
344346
$this->getOriginalColumnNameForCursorPagination($this, $previousColumn),
345347
'=',
346348
$cursor->parameter($previousColumn)
347349
);
350+
351+
$unionBuilders->each(function ($unionBuilder) use($previousColumn, $cursor) {
352+
$unionBuilder->where(
353+
$this->getOriginalColumnNameForCursorPagination($this, $previousColumn),
354+
'=',
355+
$cursor->parameter($previousColumn)
356+
);
357+
358+
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
359+
});
348360
}
349361

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

353365
$builder->where(
@@ -361,6 +373,24 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName =
361373
$addCursorConditions($builder, $column, $i + 1);
362374
});
363375
}
376+
377+
$unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions){
378+
$unionBuilder->where(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
379+
$unionBuilder->where(
380+
$this->getOriginalColumnNameForCursorPagination($this, $column),
381+
$direction === 'asc' ? '>' : '<',
382+
$cursor->parameter($column)
383+
);
384+
385+
if ($i < $orders->count() - 1) {
386+
$unionBuilder->orWhere(function (self $builder) use ($addCursorConditions, $column, $i) {
387+
$addCursorConditions($builder, $column, $i + 1);
388+
});
389+
}
390+
391+
$this->addBinding($unionBuilder->getRawBindings()['where'], 'union');
392+
});
393+
});
364394
});
365395
};
366396

src/Illuminate/Database/Query/Builder.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2487,15 +2487,15 @@ protected function ensureOrderForCursorPagination($shouldReverse = false)
24872487
{
24882488
$this->enforceOrderBy();
24892489

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

24942496
return $order;
2495-
})->toArray();
2496-
}
2497-
2498-
return collect($this->orders);
2497+
});
2498+
})->values();
24992499
}
25002500

25012501
/**

tests/Database/DatabaseQueryBuilderTest.php

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4023,6 +4023,190 @@ public function testCursorPaginateWithMixedOrders()
40234023
]), $result);
40244024
}
40254025

4026+
public function testCursorPaginateWithUnionWheres()
4027+
{
4028+
$ts = now()->toDateTimeString();
4029+
4030+
$perPage = 16;
4031+
$columns = ['test'];
4032+
$cursorName = 'cursor-name';
4033+
$cursor = new Cursor(['created_at' => $ts]);
4034+
$builder = $this->getMockQueryBuilder();
4035+
$builder->select('id', 'start_time as created_at')->selectRaw("'video' as type")->from('videos');
4036+
$builder->union($this->getBuilder()->select('id', 'created_at')->selectRaw("'news' as type")->from('news'));
4037+
$builder->orderBy('created_at');
4038+
4039+
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
4040+
return new Builder($builder->connection, $builder->grammar, $builder->processor);
4041+
});
4042+
4043+
$path = 'http://foo.bar?cursor='.$cursor->encode();
4044+
4045+
$results = collect([
4046+
['id' => 1, 'created_at' => now(), 'type' => 'video'],
4047+
['id' => 2, 'created_at' => now(), 'type' => 'news'],
4048+
]);
4049+
4050+
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) {
4051+
$this->assertEquals(
4052+
'(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',
4053+
$builder->toSql());
4054+
$this->assertEquals([$ts], $builder->bindings['where']);
4055+
$this->assertEquals([$ts], $builder->bindings['union']);
4056+
return $results;
4057+
});
4058+
4059+
Paginator::currentPathResolver(function () use ($path) {
4060+
return $path;
4061+
});
4062+
4063+
$result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor);
4064+
4065+
$this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [
4066+
'path' => $path,
4067+
'cursorName' => $cursorName,
4068+
'parameters' => ['created_at'],
4069+
]), $result);
4070+
}
4071+
4072+
public function testCursorPaginateWithUnionWheresWithRawOrderExpression()
4073+
{
4074+
$ts = now()->toDateTimeString();
4075+
4076+
$perPage = 16;
4077+
$columns = ['test'];
4078+
$cursorName = 'cursor-name';
4079+
$cursor = new Cursor(['created_at' => $ts]);
4080+
$builder = $this->getMockQueryBuilder();
4081+
$builder->select('id', 'is_published', 'start_time as created_at')->selectRaw("'video' as type")->where('is_published', true)->from('videos');
4082+
$builder->union($this->getBuilder()->select('id', 'is_published', 'created_at')->selectRaw("'news' as type")->where('is_published', true)->from('news'));
4083+
$builder->orderByRaw('case when (id = 3 and type="news" then 0 else 1 end)')->orderBy('created_at');
4084+
4085+
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
4086+
return new Builder($builder->connection, $builder->grammar, $builder->processor);
4087+
});
4088+
4089+
$path = 'http://foo.bar?cursor='.$cursor->encode();
4090+
4091+
$results = collect([
4092+
['id' => 1, 'created_at' => now(), 'type' => 'video', 'is_published' => true],
4093+
['id' => 2, 'created_at' => now(), 'type' => 'news', 'is_published' => true],
4094+
]);
4095+
4096+
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) {
4097+
$this->assertEquals(
4098+
'(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',
4099+
$builder->toSql());
4100+
$this->assertEquals([true, $ts], $builder->bindings['where']);
4101+
$this->assertEquals([true, $ts], $builder->bindings['union']);
4102+
return $results;
4103+
});
4104+
4105+
Paginator::currentPathResolver(function () use ($path) {
4106+
return $path;
4107+
});
4108+
4109+
$result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor);
4110+
4111+
$this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [
4112+
'path' => $path,
4113+
'cursorName' => $cursorName,
4114+
'parameters' => ['created_at'],
4115+
]), $result);
4116+
}
4117+
4118+
public function testCursorPaginateWithUnionWheresReverseOrder()
4119+
{
4120+
$ts = now()->toDateTimeString();
4121+
4122+
$perPage = 16;
4123+
$columns = ['test'];
4124+
$cursorName = 'cursor-name';
4125+
$cursor = new Cursor(['created_at' => $ts], false);
4126+
$builder = $this->getMockQueryBuilder();
4127+
$builder->select('id', 'start_time as created_at')->selectRaw("'video' as type")->from('videos');
4128+
$builder->union($this->getBuilder()->select('id', 'created_at')->selectRaw("'news' as type")->from('news'));
4129+
$builder->orderBy('created_at');
4130+
4131+
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
4132+
return new Builder($builder->connection, $builder->grammar, $builder->processor);
4133+
});
4134+
4135+
$path = 'http://foo.bar?cursor='.$cursor->encode();
4136+
4137+
$results = collect([
4138+
['id' => 1, 'created_at' => now(), 'type' => 'video'],
4139+
['id' => 2, 'created_at' => now(), 'type' => 'news'],
4140+
]);
4141+
4142+
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) {
4143+
$this->assertEquals(
4144+
'(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',
4145+
$builder->toSql());
4146+
$this->assertEquals([$ts], $builder->bindings['where']);
4147+
$this->assertEquals([$ts], $builder->bindings['union']);
4148+
return $results;
4149+
});
4150+
4151+
Paginator::currentPathResolver(function () use ($path) {
4152+
return $path;
4153+
});
4154+
4155+
$result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor);
4156+
4157+
$this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [
4158+
'path' => $path,
4159+
'cursorName' => $cursorName,
4160+
'parameters' => ['created_at'],
4161+
]), $result);
4162+
}
4163+
4164+
public function testCursorPaginateWithUnionWheresMultipleOrders()
4165+
{
4166+
$ts = now()->toDateTimeString();
4167+
4168+
$perPage = 16;
4169+
$columns = ['test'];
4170+
$cursorName = 'cursor-name';
4171+
$cursor = new Cursor(['created_at' => $ts, 'id' => 1]);
4172+
$builder = $this->getMockQueryBuilder();
4173+
$builder->select('id', 'start_time as created_at')->selectRaw("'video' as type")->from('videos');
4174+
$builder->union($this->getBuilder()->select('id', 'created_at')->selectRaw("'news' as type")->from('news'));
4175+
$builder->orderByDesc('created_at')->orderBy('id');
4176+
4177+
$builder->shouldReceive('newQuery')->andReturnUsing(function () use ($builder) {
4178+
return new Builder($builder->connection, $builder->grammar, $builder->processor);
4179+
});
4180+
4181+
$path = 'http://foo.bar?cursor='.$cursor->encode();
4182+
4183+
$results = collect([
4184+
['id' => 1, 'created_at' => now(), 'type' => 'video'],
4185+
['id' => 2, 'created_at' => now(), 'type' => 'news'],
4186+
]);
4187+
4188+
$builder->shouldReceive('get')->once()->andReturnUsing(function () use ($builder, $results, $ts) {
4189+
$this->assertEquals(
4190+
'(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',
4191+
$builder->toSql());
4192+
$this->assertEquals([$ts, $ts, 1], $builder->bindings['where']);
4193+
$this->assertEquals([$ts, $ts, 1], $builder->bindings['union']);
4194+
return $results;
4195+
});
4196+
4197+
Paginator::currentPathResolver(function () use ($path) {
4198+
return $path;
4199+
});
4200+
4201+
$result = $builder->cursorPaginate($perPage, $columns, $cursorName, $cursor);
4202+
4203+
$this->assertEquals(new CursorPaginator($results, $perPage, $cursor, [
4204+
'path' => $path,
4205+
'cursorName' => $cursorName,
4206+
'parameters' => ['created_at', 'id'],
4207+
]), $result);
4208+
}
4209+
40264210
public function testWhereRowValues()
40274211
{
40284212
$builder = $this->getBuilder();

0 commit comments

Comments
 (0)