Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d00aa33

Browse files
committed
Merge branch 'hotfix/#1337-fix-oracle-postgresql-paginator-sorting-issues' into hotfix/#1342-paginator-functional-test-integration
2 parents 870f596 + 4c84f54 commit d00aa33

3 files changed

Lines changed: 329 additions & 54 deletions

File tree

lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php

Lines changed: 209 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@
1313

1414
namespace Doctrine\ORM\Tools\Pagination;
1515

16+
use Doctrine\ORM\Query\AST\ArithmeticExpression;
17+
use Doctrine\ORM\Query\AST\ArithmeticTerm;
18+
use Doctrine\ORM\Query\AST\OrderByClause;
19+
use Doctrine\ORM\Query\AST\PartialObjectExpression;
1620
use Doctrine\ORM\Query\AST\PathExpression;
21+
use Doctrine\ORM\Query\AST\SelectExpression;
22+
use Doctrine\ORM\Query\Expr\OrderBy;
1723
use Doctrine\ORM\Query\SqlWalker;
1824
use Doctrine\ORM\Query\AST\SelectStatement;
19-
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
2025

2126
/**
2227
* Wraps the query in order to select root entity IDs for pagination.
@@ -56,6 +61,23 @@ class LimitSubqueryOutputWalker extends SqlWalker
5661
*/
5762
private $maxResults;
5863

64+
/**
65+
* @var \Doctrine\ORM\EntityManager
66+
*/
67+
private $em;
68+
69+
/**
70+
* @var array
71+
*/
72+
private $orderByPathExpressions = [];
73+
74+
/**
75+
* The quote strategy.
76+
*
77+
* @var \Doctrine\ORM\Mapping\QuoteStrategy
78+
*/
79+
private $quoteStrategy;
80+
5981
/**
6082
* Constructor.
6183
*
@@ -78,20 +100,37 @@ public function __construct($query, $parserResult, array $queryComponents)
78100
$this->maxResults = $query->getMaxResults();
79101
$query->setFirstResult(null)->setMaxResults(null);
80102

103+
$this->em = $query->getEntityManager();
104+
$this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy();
105+
81106
parent::__construct($query, $parserResult, $queryComponents);
82107
}
83108

84109
/**
85110
* Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
86111
*
87112
* @param SelectStatement $AST
113+
* @param bool $addMissingItemsFromOrderByToSelect
88114
*
89115
* @return string
90116
*
91117
* @throws \RuntimeException
92118
*/
93-
public function walkSelectStatement(SelectStatement $AST)
119+
public function walkSelectStatement(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
94120
{
121+
// We don't want to call this recursively!
122+
if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
123+
// In the case of ordering a query by columns from joined tables, we
124+
// must add those columns to the select clause of the query BEFORE
125+
// the SQL is generated.
126+
$this->addMissingItemsFromOrderByToSelect($AST);
127+
}
128+
129+
// Remove order by clause from the inner query
130+
// It will be re-appended in the outer select generated by this method
131+
$orderByClause = $AST->orderByClause;
132+
$AST->orderByClause = null;
133+
95134
// Set every select expression as visible(hidden = false) to
96135
// make $AST have scalar mappings properly - this is relevant for referencing selected
97136
// fields from outside the subquery, for example in the ORDER BY segment
@@ -104,6 +143,9 @@ public function walkSelectStatement(SelectStatement $AST)
104143

105144
$innerSql = parent::walkSelectStatement($AST);
106145

146+
// Restore orderByClause
147+
$AST->orderByClause = $orderByClause;
148+
107149
// Restore hiddens
108150
foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
109151
$expr->hiddenAliasResultVariable = $hiddens[$idx];
@@ -163,7 +205,7 @@ public function walkSelectStatement(SelectStatement $AST)
163205
implode(', ', $sqlIdentifier), $innerSql);
164206

165207
// http://www.doctrine-project.org/jira/browse/DDC-1958
166-
$sql = $this->preserveSqlOrdering($AST, $sqlIdentifier, $innerSql, $sql);
208+
$sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
167209

168210
// Apply the limit and offset.
169211
$sql = $this->platform->modifyLimitQuery(
@@ -182,49 +224,182 @@ public function walkSelectStatement(SelectStatement $AST)
182224
}
183225

184226
/**
185-
* Generates new SQL for Postgresql or Oracle if necessary.
227+
* Finds all PathExpressions in an AST's OrderByClause, and ensures that
228+
* the referenced fields are present in the SelectClause of the passed AST.
186229
*
187230
* @param SelectStatement $AST
231+
*/
232+
private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
233+
{
234+
$this->orderByPathExpressions = [];
235+
236+
// We need to do this in another walker because otherwise we'll end up
237+
// polluting the state of this one.
238+
$walker = clone $this;
239+
240+
// This will populate $orderByPathExpressions via
241+
// LimitSubqueryOutputWalker::walkPathExpression, which will be called
242+
// as the select statement is walked. We'll end up with an array of all
243+
// path expressions referenced in the query.
244+
$walker->walkSelectStatement($AST, false);
245+
$orderByPathExpressions = $walker->getOrderByPathExpressions();
246+
247+
// Get a map of referenced identifiers to field names.
248+
$selects = [];
249+
foreach ($orderByPathExpressions as $pathExpression) {
250+
$idVar = $pathExpression->identificationVariable;
251+
$field = $pathExpression->field;
252+
if (!isset($selects[$idVar])) {
253+
$selects[$idVar] = [];
254+
}
255+
$selects[$idVar][$field] = true;
256+
}
257+
258+
// Loop the select clause of the AST and exclude items from $select
259+
// that are already being selected in the query.
260+
foreach ($AST->selectClause->selectExpressions as $selectExpression) {
261+
if ($selectExpression instanceof SelectExpression) {
262+
$idVar = $selectExpression->expression;
263+
if (!is_string($idVar)) {
264+
continue;
265+
}
266+
$field = $selectExpression->fieldIdentificationVariable;
267+
if ($field === null) {
268+
// No need to add this select, as we're already fetching the whole object.
269+
unset($selects[$idVar]);
270+
} else {
271+
unset($selects[$idVar][$field]);
272+
}
273+
}
274+
}
275+
276+
// Add select items which were not excluded to the AST's select clause.
277+
foreach ($selects as $idVar => $fields) {
278+
$AST->selectClause->selectExpressions[] = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
279+
}
280+
}
281+
282+
/**
283+
* Generates new SQL for statements with an order by clause
284+
*
188285
* @param array $sqlIdentifier
189286
* @param string $innerSql
190287
* @param string $sql
288+
* @param OrderByClause $orderByClause
191289
*
192-
* @return void
290+
* @return string
193291
*/
194-
public function preserveSqlOrdering(SelectStatement $AST, array $sqlIdentifier, $innerSql, $sql)
292+
public function preserveSqlOrdering(array $sqlIdentifier, $innerSql, $sql, $orderByClause)
195293
{
196-
// For every order by, find out the SQL alias by inspecting the ResultSetMapping.
197-
$sqlOrderColumns = array();
198-
$orderBy = array();
199-
if (isset($AST->orderByClause)) {
200-
foreach ($AST->orderByClause->orderByItems as $item) {
201-
$expression = $item->expression;
202-
203-
$possibleAliases = $expression instanceof PathExpression
204-
? array_keys($this->rsm->fieldMappings, $expression->field)
205-
: array_keys($this->rsm->scalarMappings, $expression);
206-
207-
foreach ($possibleAliases as $alias) {
208-
if (!is_object($expression) || $this->rsm->columnOwnerMap[$alias] == $expression->identificationVariable) {
209-
$sqlOrderColumns[] = $alias;
210-
$orderBy[] = $alias . ' ' . $item->type;
211-
break;
212-
}
213-
}
214-
}
215-
// remove identifier aliases
216-
$sqlOrderColumns = array_diff($sqlOrderColumns, $sqlIdentifier);
294+
// If the sql statement has an order by clause, we need to wrap it in a new select distinct
295+
// statement
296+
if (!$orderByClause instanceof OrderByClause) {
297+
return $sql;
217298
}
218299

219-
if (count($orderBy)) {
220-
$sql = sprintf(
221-
'SELECT DISTINCT %s FROM (%s) dctrn_result ORDER BY %s',
222-
implode(', ', array_merge($sqlIdentifier, $sqlOrderColumns)),
223-
$innerSql,
224-
implode(', ', $orderBy)
300+
// Rebuild the order by clause to work in the scope of the new select statement
301+
/* @var array $sqlOrderColumns an array of items that need to be included in the select list */
302+
/* @var array $orderBy an array of rebuilt order by items */
303+
list($sqlOrderColumns, $orderBy) = $this->rebuildOrderByClauseForOuterScope($orderByClause);
304+
305+
// Identifiers are always included in the select list, so there's no need to include them twice
306+
$sqlOrderColumns = array_diff($sqlOrderColumns, $sqlIdentifier);
307+
308+
// Build the select distinct statement
309+
$sql = sprintf(
310+
'SELECT DISTINCT %s FROM (%s) dctrn_result ORDER BY %s',
311+
implode(', ', array_merge($sqlIdentifier, $sqlOrderColumns)),
312+
$innerSql,
313+
implode(', ', $orderBy)
314+
);
315+
316+
return $sql;
317+
}
318+
319+
/**
320+
* Generates a new order by clause that works in the scope of a select query wrapping the original
321+
*
322+
* @param OrderByClause $orderByClause
323+
* @return array
324+
*/
325+
private function rebuildOrderByClauseForOuterScope(OrderByClause $orderByClause) {
326+
$dqlAliasToSqlTableAliasMap
327+
= $searchPatterns
328+
= $replacements
329+
= $dqlAliasToClassMap
330+
= $selectListAdditions
331+
= $orderByItems
332+
= [];
333+
334+
// Generate DQL alias -> SQL table alias mapping
335+
foreach(array_keys($this->rsm->aliasMap) as $dqlAlias) {
336+
$dqlAliasToClassMap[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata'];
337+
$dqlAliasToSqlTableAliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
338+
}
339+
340+
// Pattern to find table path expressions in the order by clause
341+
$fieldSearchPattern = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
342+
343+
// Generate search patterns for each field's path expression in the order by clause
344+
foreach($this->rsm->fieldMappings as $fieldAlias => $columnName) {
345+
$dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
346+
$columnName = $this->quoteStrategy->getColumnName(
347+
$columnName,
348+
$dqlAliasToClassMap[$dqlAliasForFieldAlias],
349+
$this->em->getConnection()->getDatabasePlatform()
225350
);
351+
352+
$sqlTableAliasForFieldAlias = $dqlAliasToSqlTableAliasMap[$dqlAliasForFieldAlias];
353+
354+
$searchPatterns[] = sprintf($fieldSearchPattern, $sqlTableAliasForFieldAlias, $columnName);
355+
$replacements[] = $fieldAlias;
226356
}
227357

228-
return $sql;
358+
$complexAddedOrderByAliases = 0;
359+
foreach($orderByClause->orderByItems as $orderByItem) {
360+
// Walk order by item to get string representation of it
361+
$orderByItemString = $this->walkOrderByItem($orderByItem);
362+
363+
// Replace path expressions in the order by clause with their column alias
364+
$orderByItemString = preg_replace($searchPatterns, $replacements, $orderByItemString);
365+
366+
// The order by items are not required to be in the select list on Oracle and PostgreSQL, but
367+
// for the sake of simplicity, order by items will be included in the select list on all platforms.
368+
// This doesn't impact functionality.
369+
$selectListAddition = trim(preg_replace('/([^ ]+) (?:asc|desc)/i', '$1', $orderByItemString));
370+
371+
// If the expression is an arithmetic expression, we need to create an alias for it.
372+
if ($orderByItem->expression instanceof ArithmeticTerm) {
373+
$orderByAlias = "ordr_" . $complexAddedOrderByAliases++;
374+
$orderByItemString = $orderByAlias . " " . $orderByItem->type;
375+
$selectListAddition .= " AS $orderByAlias";
376+
}
377+
$selectListAdditions[] = $selectListAddition;
378+
$orderByItems[] = $orderByItemString;
379+
}
380+
381+
return array($selectListAdditions, $orderByItems);
382+
}
383+
384+
/**
385+
* {@inheritdoc}
386+
*/
387+
public function walkPathExpression($pathExpr)
388+
{
389+
if (!in_array($pathExpr, $this->orderByPathExpressions)) {
390+
$this->orderByPathExpressions[] = $pathExpr;
391+
}
392+
393+
return parent::walkPathExpression($pathExpr);
394+
}
395+
396+
/**
397+
* getter for $orderByPathExpressions
398+
*
399+
* @return array
400+
*/
401+
public function getOrderByPathExpressions()
402+
{
403+
return $this->orderByPathExpressions;
229404
}
230405
}

0 commit comments

Comments
 (0)