Skip to content
1 change: 1 addition & 0 deletions framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Yii Framework 2 Change Log
------------------------

- Enh #20309: Add custom attributes support to style tags (nzwz)
- Bug #20239: Fix `yii\data\ActiveDataProvider` to avoid unexpected pagination results with UNION queries (Izumi-kun)


2.0.52 February 13, 2025
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'));
}
}