Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 230 lines (198 sloc) 8.266 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
<?php
/**
* Doctrine ORM
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to kontakt@beberlei.de so I can send you a copy immediately.
*/

namespace Doctrine\ORM\Tools\Pagination;

use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;

/**
* Wraps the query in order to select root entity IDs for pagination.
*
* Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
* SELECT DISTINCT <id> FROM (<original SQL>) LIMIT x OFFSET y
*
* Works with composite keys but cannot deal with queries that have multiple
* root entities (e.g. `SELECT f, b from Foo, Bar`)
*
* @author Sander Marechal <s.marechal@jejik.com>
*/
class LimitSubqueryOutputWalker extends SqlWalker
{
    /**
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
    private $platform;

    /**
* @var \Doctrine\ORM\Query\ResultSetMapping
*/
    private $rsm;

    /**
* @var array
*/
    private $queryComponents;

    /**
* @var int
*/
    private $firstResult;

    /**
* @var int
*/
    private $maxResults;

    /**
* Constructor.
*
* Stores various parameters that are otherwise unavailable
* because Doctrine\ORM\Query\SqlWalker keeps everything private without
* accessors.
*
* @param \Doctrine\ORM\Query $query
* @param \Doctrine\ORM\Query\ParserResult $parserResult
* @param array $queryComponents
*/
    public function __construct($query, $parserResult, array $queryComponents)
    {
        $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
        $this->rsm = $parserResult->getResultSetMapping();
        $this->queryComponents = $queryComponents;

        // Reset limit and offset
        $this->firstResult = $query->getFirstResult();
        $this->maxResults = $query->getMaxResults();
        $query->setFirstResult(null)->setMaxResults(null);

        parent::__construct($query, $parserResult, $queryComponents);
    }

    /**
* Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
*
* @param SelectStatement $AST
*
* @return string
*
* @throws \RuntimeException
*/
    public function walkSelectStatement(SelectStatement $AST)
    {
        if ($this->platform instanceof PostgreSqlPlatform) {
            // Set every select expression as visible(hidden = false) to
            // make $AST to have scalar mappings properly
            $hiddens = array();
            foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
                $hiddens[$idx] = $expr->hiddenAliasResultVariable;
                $expr->hiddenAliasResultVariable = false;
            }

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

            // Restore hiddens
            foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
                $expr->hiddenAliasResultVariable = $hiddens[$idx];
            }
        } else {
            $innerSql = parent::walkSelectStatement($AST);
        }


        // Find out the SQL alias of the identifier column of the root entity.
        // It may be possible to make this work with multiple root entities but that
        // would probably require issuing multiple queries or doing a UNION SELECT.
        // So for now, it's not supported.

        // Get the root entity and alias from the AST fromClause.
        $from = $AST->fromClause->identificationVariableDeclarations;
        if (count($from) !== 1) {
            throw new \RuntimeException("Cannot count query which selects two FROM components, cannot make distinction");
        }

        $rootAlias = $from[0]->rangeVariableDeclaration->aliasIdentificationVariable;
        $rootClass = $this->queryComponents[$rootAlias]['metadata'];
        $rootIdentifier = $rootClass->identifier;

        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
        $sqlIdentifier = array();
        foreach ($rootIdentifier as $property) {
            if (isset($rootClass->fieldMappings[$property])) {
                foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) {
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
                        $sqlIdentifier[$property] = $alias;
                    }
                }
            }

            if (isset($rootClass->associationMappings[$property])) {
                $joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];

                foreach (array_keys($this->rsm->metaMappings, $joinColumn) as $alias) {
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
                        $sqlIdentifier[$property] = $alias;
                    }
                }
            }
        }

        if (count($sqlIdentifier) === 0) {
            throw new \RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
        }

        if (count($rootIdentifier) != count($sqlIdentifier)) {
            throw new \RuntimeException(sprintf(
                'Not all identifier properties can be found in the ResultSetMapping: %s',
                implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
            ));
        }

        // Build the counter query
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result',
            implode(', ', $sqlIdentifier), $innerSql);

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

        // Apply the limit and offset.
        $sql = $this->platform->modifyLimitQuery(
            $sql, $this->maxResults, $this->firstResult
        );

        // Add the columns to the ResultSetMapping. It's not really nice but
        // it works. Preferably I'd clear the RSM or simply create a new one
        // but that is not possible from inside the output walker, so we dirty
        // up the one we have.
        foreach ($sqlIdentifier as $property => $alias) {
            $this->rsm->addScalarResult($alias, $property);
        }

        return $sql;
    }

    /**
* Generates new SQL for Postgresql or Oracle if necessary.
*
* @param SelectStatement $AST
* @param array $sqlIdentifier
* @param string $innerSql
* @param string $sql
*
* @return void
*/
    public function preserveSqlOrdering(SelectStatement $AST, array $sqlIdentifier, $innerSql, $sql)
    {
        // For every order by, find out the SQL alias by inspecting the ResultSetMapping.
        $sqlOrderColumns = array();
        $orderBy = array();
        if (isset($AST->orderByClause)) {
            foreach ($AST->orderByClause->orderByItems as $item) {
                $possibleAliases = (is_object($item->expression))
                    ? array_keys($this->rsm->fieldMappings, $item->expression->field)
                    : array_keys($this->rsm->scalarMappings, $item->expression);

                foreach ($possibleAliases as $alias) {
                    if (!is_object($item->expression) || $this->rsm->columnOwnerMap[$alias] == $item->expression->identificationVariable) {
                        $sqlOrderColumns[] = $alias;
                        $orderBy[] = $alias . ' ' . $item->type;
                        break;
                    }
                }
            }
            // remove identifier aliases
            $sqlOrderColumns = array_diff($sqlOrderColumns, $sqlIdentifier);
        }

        if (count($orderBy)) {
            $sql = sprintf(
                'SELECT DISTINCT %s FROM (%s) dctrn_result ORDER BY %s',
                implode(', ', array_merge($sqlIdentifier, $sqlOrderColumns)),
                $innerSql,
                implode(', ', $orderBy)
            );
        }

        return $sql;
    }
}
Something went wrong with that request. Please try again.