Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #20239: fix yii\data\ActiveDataProvider to avoid unexpected pagination results with UNION queries #20311

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Yii Framework 2 Change Log
- Bug #20300: Clear stat cache in `FileCache::setValue()` (rob006)
- Enh #20306: Add new `yii\helpers\ArrayHelper::flatten()` method (xcopy)
- Bug #20308: Allow CompositeAuth auth methods to use their own user if defined (mtangoo)
- Bug #20239: Fix `yii\data\ActiveDataProvider` to avoid unexpected pagination results with UNION queries (Izumi-kun)
- Bug #20313: Allow CompositeAuth auth methods to use their own request and response if defined (mtangoo)

2.0.51 July 18, 2024
Expand Down
50 changes: 42 additions & 8 deletions framework/data/ActiveDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use yii\base\Model;
use yii\db\ActiveQueryInterface;
use yii\db\Connection;
use yii\db\Query;
use yii\db\QueryInterface;
use yii\di\Instance;

Expand Down Expand Up @@ -93,14 +94,51 @@ public function init()
}

/**
* {@inheritdoc}
* Creates a wrapper of [[query]] that allows adding limit and order.
* @return QueryInterface
* @throws InvalidConfigException
*/
protected function prepareModels()
protected function createQueryWrapper(): QueryInterface
{
if (!$this->query instanceof QueryInterface) {
throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.');
}
$query = clone $this->query;
if (!$this->query instanceof Query || empty($this->query->union)) {
return clone $this->query;
}

$wrapper = new class extends Query {
/**
* @var Query
*/
public $wrappedQuery;
/**
* @inheritDoc
*/
public function all($db = null)
{
return $this->wrappedQuery->populate(parent::all($db));
}
public function createCommand($db = null)
{
$command = $this->wrappedQuery->createCommand($db);
$this->from(['q' => "({$command->getSql()})"])->params($command->params);
return parent::createCommand($command->db);
}
};
$wrapper->select('*');
$wrapper->wrappedQuery = $this->query;
$wrapper->emulateExecution = $this->query->emulateExecution;

return $wrapper;
}

/**
* {@inheritdoc}
*/
protected function prepareModels()
{
$query = $this->createQueryWrapper();
if (($pagination = $this->getPagination()) !== false) {
$pagination->totalCount = $this->getTotalCount();
if ($pagination->totalCount === 0) {
Expand Down Expand Up @@ -161,11 +199,7 @@ protected function prepareKeys($models)
*/
protected function prepareTotalCount()
{
if (!$this->query instanceof QueryInterface) {
throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.');
}
$query = clone $this->query;
return (int) $query->limit(-1)->offset(-1)->orderBy([])->count('*', $this->db);
return (int) $this->createQueryWrapper()->limit(-1)->offset(-1)->orderBy([])->count('*', $this->db);
}

/**
Expand Down
26 changes: 26 additions & 0 deletions tests/framework/data/ActiveDataProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,30 @@ public function testDoesNotPerformQueryWhenHasNoModels()

$this->assertEquals(0, $pagination->getPageCount());
}

public function testPaginationWithUnionQuery()
{
$q1 = Item::find()->where(['category_id' => 2])->with('category');
$q2 = Item::find()->where(['id' => [2, 4]]);
$provider = new ActiveDataProvider([
'query' => $q1->union($q2)->indexBy('id'),
]);
$pagination = $provider->getPagination();
$pagination->pageSize = 2;
$provider->prepare();
$this->assertEquals(2, $pagination->getPageCount());
$this->assertEquals(4, $provider->getTotalCount());
$this->assertCount(2, $provider->getModels());

$pagination->pageSize = 10;
$provider->prepare(true);
/** @var Item[] $models */
$models = $provider->getModels();
$this->assertCount(4, $models);
$this->assertContainsOnlyInstancesOf(Item::class, $models);
$this->assertEquals('Yii 1.1 Application Development Cookbook', $models[2]->name);
$this->assertEquals('Toy Story', $models[4]->name);
$this->assertTrue($models[2]->isRelationPopulated('category'));
$this->assertTrue($models[4]->isRelationPopulated('category'));
}
}
Loading