Skip to content

Commit

Permalink
[10.x] Fix cursor paginate with union and column alias (#50882)
Browse files Browse the repository at this point in the history
* Cursor paginate uses incorrect column name for where on union

* Set the right column names for the cursor on the unions

* Add dedicated test to test correct column alias on union cursor paginate

* Add eloquent test for cursor pagination with union and multiple aliases

* Use correct column name in cursor where clause
  • Loading branch information
thijsvdanker authored Apr 16, 2024
1 parent 67d4d0d commit 877ebca
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 15 deletions.
21 changes: 11 additions & 10 deletions src/Illuminate/Database/Concerns/BuildsQueries.php
Original file line number Diff line number Diff line change
Expand Up @@ -382,11 +382,11 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName =
// Reset the union bindings so we can add the cursor where in the correct position...
$this->setBindings([], 'union');

$addCursorConditions = function (self $builder, $previousColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
$addCursorConditions = function (self $builder, $previousColumn, $originalColumn, $i) use (&$addCursorConditions, $cursor, $orders) {
$unionBuilders = $builder->getUnionBuilders();

if (! is_null($previousColumn)) {
$originalColumn = $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);
$originalColumn ??= $this->getOriginalColumnNameForCursorPagination($this, $previousColumn);

$builder->where(
Str::contains($originalColumn, ['(', ')']) ? new Expression($originalColumn) : $originalColumn,
Expand All @@ -396,7 +396,7 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName =

$unionBuilders->each(function ($unionBuilder) use ($previousColumn, $cursor) {
$unionBuilder->where(
$this->getOriginalColumnNameForCursorPagination($this, $previousColumn),
$this->getOriginalColumnNameForCursorPagination($unionBuilder, $previousColumn),
'=',
$cursor->parameter($previousColumn)
);
Expand All @@ -417,24 +417,25 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName =
);

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

$unionBuilders->each(function ($unionBuilder) use ($column, $direction, $cursor, $i, $orders, $addCursorConditions) {
$unionWheres = $unionBuilder->getRawBindings()['where'];

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

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

Expand All @@ -445,7 +446,7 @@ protected function paginateUsingCursor($perPage, $columns = ['*'], $cursorName =
});
};

$addCursorConditions($this, null, 0);
$addCursorConditions($this, null, null, 0);
}

$this->limit($perPage + 1);
Expand Down
59 changes: 54 additions & 5 deletions tests/Database/DatabaseQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5276,7 +5276,7 @@ public function testCursorPaginateWithUnionWheres()

$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',
'(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 ("created_at" > ?)) order by "created_at" asc limit 17',
$builder->toSql());
$this->assertEquals([$ts], $builder->bindings['where']);
$this->assertEquals([$ts], $builder->bindings['union']);
Expand Down Expand Up @@ -5325,7 +5325,7 @@ public function testCursorPaginateWithMultipleUnionsAndMultipleWheres()

$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 "extra" = ? and ("start_time" > ?)) union (select "id", "created_at", \'podcast\' as type from "podcasts" where "extra" = ? and ("start_time" > ?)) order by "created_at" asc limit 17',
'(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 "extra" = ? and ("created_at" > ?)) union (select "id", "created_at", \'podcast\' as type from "podcasts" where "extra" = ? and ("created_at" > ?)) order by "created_at" asc limit 17',
$builder->toSql());
$this->assertEquals([$ts], $builder->bindings['where']);
$this->assertEquals(['first', $ts, 'second', $ts], $builder->bindings['union']);
Expand Down Expand Up @@ -5422,7 +5422,7 @@ public function testCursorPaginateWithUnionWheresWithRawOrderExpression()

$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',
'(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 ("created_at" > ?)) 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']);
Expand Down Expand Up @@ -5469,7 +5469,7 @@ public function testCursorPaginateWithUnionWheresReverseOrder()

$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" desc limit 17',
'(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 ("created_at" < ?)) order by "created_at" desc limit 17',
$builder->toSql());
$this->assertEquals([$ts], $builder->bindings['where']);
$this->assertEquals([$ts], $builder->bindings['union']);
Expand Down Expand Up @@ -5516,7 +5516,7 @@ public function testCursorPaginateWithUnionWheresMultipleOrders()

$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',
'(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 ("created_at" < ? or ("created_at" = ? 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']);
Expand All @@ -5537,6 +5537,55 @@ public function testCursorPaginateWithUnionWheresMultipleOrders()
]), $result);
}

public function testCursorPaginateWithUnionWheresAndAliassedOrderColumns()
{
$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->union($this->getBuilder()->select('id', 'init_at as created_at')->selectRaw("'podcast' as type")->from('podcasts'));
$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'],
['id' => 3, 'created_at' => now(), 'type' => 'podcast'],
]);

$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 ("created_at" > ?)) union (select "id", "init_at" as "created_at", \'podcast\' as type from "podcasts" where ("init_at" > ?)) order by "created_at" asc limit 17',
$builder->toSql());
$this->assertEquals([$ts], $builder->bindings['where']);
$this->assertEquals([$ts, $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 testWhereExpression()
{
$builder = $this->getBuilder();
Expand Down
27 changes: 27 additions & 0 deletions tests/Integration/Database/EloquentCursorPaginateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ protected function afterRefreshingDatabase()

Schema::create('test_users', function ($table) {
$table->increments('id');
$table->string('name')->nullable();
$table->timestamps();
});
}
Expand Down Expand Up @@ -195,6 +196,32 @@ public function testPaginationWithMultipleUnionAndMultipleWhereClauses()
$this->assertEquals('Post B', current($result->items())->title, 'Expect the paginated query would return `Post B`');
}

public function testPaginationWithMultipleAliases()
{
TestUser::create(['name' => 'A (user)']);
TestUser::create(['name' => 'C (user)']);

TestPost::create(['title' => 'B (post)']);
TestPost::create(['title' => 'D (post)']);

$table1 = TestPost::select(['title as alias']);
$table2 = TestUser::select(['name as alias']);

$columns = ['alias'];
$cursorName = 'cursor-name';
$cursor = new Cursor(['alias' => 'A (user)']);

$result = $table1->toBase()
->union($table2->toBase())
->orderBy('alias', 'asc')
->cursorPaginate(1, $columns, $cursorName, $cursor);

$this->assertSame(['alias'], $result->getOptions()['parameters']);

$this->assertCount(1, $result->items(), 'Expect cursor paginated query should have 1 result');
$this->assertEquals('B (post)', current($result->items())->alias, 'Expect the paginated query would return `B (post)`');
}

public function testPaginationWithAliasedOrderBy()
{
for ($i = 1; $i <= 6; $i++) {
Expand Down

0 comments on commit 877ebca

Please sign in to comment.