Skip to content

Commit

Permalink
Make LimitSubqueryOutputWalker a bit more readable
Browse files Browse the repository at this point in the history
Also simplifying the REGEX to remove the ORDER BY type (ASC/DESC) with a
substr() since OrderByItem#type is always defined.
  • Loading branch information
lcobucci committed Jul 22, 2017
1 parent 3d5acd6 commit 5389ad7
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 113 deletions.
142 changes: 73 additions & 69 deletions lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
*/
class LimitSubqueryOutputWalker extends SqlWalker
{
private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';

/**
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
Expand Down Expand Up @@ -354,85 +356,95 @@ private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
/**
* Generates new SQL for statements with an order by clause
*
* @param array $sqlIdentifier
* @param string $innerSql
* @param string $sql
* @param OrderByClause $orderByClause
* @param array $sqlIdentifier
* @param string $innerSql
* @param string $sql
* @param OrderByClause|null $orderByClause
*
* @return string
*/
private function preserveSqlOrdering(array $sqlIdentifier, $innerSql, $sql, $orderByClause)
{
// If the sql statement has an order by clause, we need to wrap it in a new select distinct
// statement
if (! $orderByClause instanceof OrderByClause) {
private function preserveSqlOrdering(
array $sqlIdentifier,
string $innerSql,
string $sql,
?OrderByClause $orderByClause
) : string {
// If the sql statement has an order by clause, we need to wrap it in a new select distinct statement
if (! $orderByClause) {
return $sql;
}

// Rebuild the order by clause to work in the scope of the new select statement
/* @var array $orderBy an array of rebuilt order by items */
$orderBy = $this->rebuildOrderByClauseForOuterScope($orderByClause);
// now only select distinct identifier
return \sprintf(
'SELECT DISTINCT %s FROM (%s) dctrn_result',
\implode(', ', $sqlIdentifier),
$this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql)
);
}

$innerSqlIdentifier = $sqlIdentifier;
/**
* Generates a new SQL statement for the inner query to keep the correct sorting
*
* @param OrderByClause $orderByClause
* @param array $identifiers
* @param string $innerSql
*
* @return string
*/
private function recreateInnerSql(
OrderByClause $orderByClause,
array $identifiers,
string $innerSql
) : string {
[$searchPatterns, $replacements] = $this->generateSqlAliasReplacements();

foreach ($orderBy as $field) {
$field = preg_replace('/((\S+)\s+(ASC|DESC)\s*,?)*/', '${2}', $field);
$orderByItems = [];

// skip fields that are selected by identifiers,
// if those are ordered by in the query
if (in_array($field, $sqlIdentifier, true)) {
continue;
foreach ($orderByClause->orderByItems as $orderByItem) {
// Walk order by item to get string representation of it and
// replace path expressions in the order by clause with their column alias
$orderByItemString = \preg_replace(
$searchPatterns,
$replacements,
$this->walkOrderByItem($orderByItem)
);

$orderByItems[] = $orderByItemString;
$identifier = \substr($orderByItemString, 0, \strrpos($orderByItemString, ' '));

if (! \in_array($identifier, $identifiers, true)) {
$identifiers[] = $identifier;
}
$innerSqlIdentifier[] = $field;
}

// Build the innner select statement
$sql = sprintf(
return $sql = \sprintf(
'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s',
implode(', ', $innerSqlIdentifier),
\implode(', ', $identifiers),
$innerSql,
implode(', ', $orderBy)
\implode(', ', $orderByItems)
);

// now only select distinct identifier
$sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result', implode(', ', $sqlIdentifier), $sql);

return $sql;
}

/**
* Generates a new order by clause that works in the scope of a select query wrapping the original
*
* @param OrderByClause $orderByClause
* @return array
* @return string[][]
*/
private function rebuildOrderByClauseForOuterScope(OrderByClause $orderByClause)
private function generateSqlAliasReplacements() : array
{
$dqlAliasToSqlTableAliasMap
= $searchPatterns
= $replacements
= $dqlAliasToClassMap
= $selectListAdditions
= $orderByItems
= [];
$aliasMap = $searchPatterns = $replacements = $metadataList = [];

// Generate DQL alias -> SQL table alias mapping
foreach(array_keys($this->rsm->aliasMap) as $dqlAlias) {
$dqlAliasToClassMap[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata'];
$dqlAliasToSqlTableAliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
foreach (\array_keys($this->rsm->aliasMap) as $dqlAlias) {
$metadataList[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata'];
$aliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
}

// Pattern to find table path expressions in the order by clause
$fieldSearchPattern = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';

// Generate search patterns for each field's path expression in the order by clause
foreach($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
$dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
$class = $dqlAliasToClassMap[$dqlAliasForFieldAlias];
$class = $metadataList[$dqlAliasForFieldAlias];

// If the field is from a joined child table, we won't be ordering
// on it.
if (!isset($class->fieldMappings[$fieldName])) {
// If the field is from a joined child table, we won't be ordering on it.
if (! isset($class->fieldMappings[$fieldName])) {
continue;
}

Expand All @@ -441,37 +453,29 @@ private function rebuildOrderByClauseForOuterScope(OrderByClause $orderByClause)
// Get the proper column name as will appear in the select list
$columnName = $this->quoteStrategy->getColumnName(
$fieldName,
$dqlAliasToClassMap[$dqlAliasForFieldAlias],
$metadataList[$dqlAliasForFieldAlias],
$this->em->getConnection()->getDatabasePlatform()
);

// Get the SQL table alias for the entity and field
$sqlTableAliasForFieldAlias = $dqlAliasToSqlTableAliasMap[$dqlAliasForFieldAlias];
$sqlTableAliasForFieldAlias = $aliasMap[$dqlAliasForFieldAlias];

if (isset($fieldMapping['declared']) && $fieldMapping['declared'] !== $class->name) {
// Field was declared in a parent class, so we need to get the proper SQL table alias
// for the joined parent table.
$otherClassMetadata = $this->em->getClassMetadata($fieldMapping['declared']);
if (!$otherClassMetadata->isMappedSuperclass) {

if (! $otherClassMetadata->isMappedSuperclass) {
$sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias);
}
}

// Compose search/replace patterns
$searchPatterns[] = sprintf($fieldSearchPattern, $sqlTableAliasForFieldAlias, $columnName);
$replacements[] = $fieldAlias;
}

foreach($orderByClause->orderByItems as $orderByItem) {
// Walk order by item to get string representation of it
$orderByItemString = $this->walkOrderByItem($orderByItem);

// Replace path expressions in the order by clause with their column alias
$orderByItemString = preg_replace($searchPatterns, $replacements, $orderByItemString);

$orderByItems[] = $orderByItemString;
// Compose search and replace patterns
$searchPatterns[] = \sprintf(self::ORDER_BY_PATH_EXPRESSION, $sqlTableAliasForFieldAlias, $columnName);
$replacements[] = $fieldAlias;
}

return $orderByItems;
return [$searchPatterns, $replacements];
}

/**
Expand Down
Loading

0 comments on commit 5389ad7

Please sign in to comment.