1313
1414namespace 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 ;
1620use Doctrine \ORM \Query \AST \PathExpression ;
21+ use Doctrine \ORM \Query \AST \SelectExpression ;
22+ use Doctrine \ORM \Query \Expr \OrderBy ;
1723use Doctrine \ORM \Query \SqlWalker ;
1824use 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