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 8 commits into
base: master
Choose a base branch
from

Conversation

Izumi-kun
Copy link
Contributor

Q A
Is bugfix? ✔️
New feature?
Breaks BC?
Fixed issues #20239

Related PR: #20246

…ted pagination results with UNION queries
Copy link

codecov bot commented Jan 15, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 64.86%. Comparing base (d146307) to head (30bc262).

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #20311      +/-   ##
============================================
+ Coverage     64.84%   64.86%   +0.01%     
- Complexity    11433    11435       +2     
============================================
  Files           431      431              
  Lines         37187    37197      +10     
============================================
+ Hits          24115    24126      +11     
+ Misses        13072    13071       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@samdark samdark requested a review from a team January 15, 2025 18:06
@samdark samdark added this to the 2.0.52 milestone Jan 15, 2025
@samdark
Copy link
Member

samdark commented Jan 15, 2025

Let's exclude composer.lock changes.

@samdark samdark requested a review from bizley January 15, 2025 18:09
@samdark
Copy link
Member

samdark commented Jan 15, 2025

@rob006 would you please take a look?

{
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;
$wrapper = clone $this->query;
if ($wrapper instanceof Query && !empty($wrapper->union)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we return $this->query if the condition isn't met right away?

if (!$wrapper instanceof Query || empty($wrapper->union)) {
    return $this->query;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather return $wrapper;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to keep the original logic (not reusing the original query):

protected function prepareModels()
{
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 (($pagination = $this->getPagination()) !== false) {

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);

Should I change it?

tests/framework/data/ActiveDataProviderTest.php Outdated Show resolved Hide resolved
Comment on lines 108 to 120
$wrapper->where = [];
$wrapper->limit = null;
$wrapper->offset = null;
$wrapper->orderBy = [];
$wrapper->selectOption = null;
$wrapper->distinct = false;
$wrapper->groupBy = [];
$wrapper->join = [];
$wrapper->having = [];
$wrapper->union = [];
$wrapper->params = [];
$wrapper->withQueries = [];
$wrapper->select('*')->from(['q' => $this->query]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be more properties that needs to be reset. For example ActiveQuery has on and joinWith (and sql?) properties, that may leak to wrapper. Developer may extend these classes and create own properties with the same problem. Query needs some kind of reset() method, that would be responsible for resting query state.

Or we cold use new Query(), similar to ActiveQuery::queryScalar():

$command = (new Query())->select([$selectExpression])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a new Query() will break prepareModels() itself if query is an ActiveQuery and the expected return value of ActiveQuery::all() is array of objects rather then an array of arrays. I'll think about the solution a little more.

@Izumi-kun
Copy link
Contributor Author

I don't really like the implementation, it looks like a workaround. The design of the query build doesn't allow for a more proper solution without breaking BC. Or am I missing something?

@Izumi-kun Izumi-kun requested review from rob006 and samdark January 17, 2025 05:19
{
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a clone here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests pass successfully without clone. I think it can be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this kind of test will fail without clone.

$provider = new ActiveDataProvider([
    'query' => Item::find(),
]);
$pagination = $provider->getPagination();
$pagination->pageSize = 2;
$provider->prepare();
$this->assertCount(2, $provider->getModels());

$provider->setPagination(false);
$provider->prepare(true);
$this->assertCount(5, $provider->getModels());

Copy link
Member

@samdark samdark Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants