diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 4869591ce6f..bbe95f48fac 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -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 diff --git a/framework/data/ActiveDataProvider.php b/framework/data/ActiveDataProvider.php index 3a129aa7293..b79b6fcb54a 100644 --- a/framework/data/ActiveDataProvider.php +++ b/framework/data/ActiveDataProvider.php @@ -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; @@ -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) { @@ -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); } /** diff --git a/tests/framework/data/ActiveDataProviderTest.php b/tests/framework/data/ActiveDataProviderTest.php index af96dfef267..c3dd6688f87 100644 --- a/tests/framework/data/ActiveDataProviderTest.php +++ b/tests/framework/data/ActiveDataProviderTest.php @@ -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')); + } }