Query.php 38.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
<?php
/*
 *  $Id$
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.phpdoctrine.com>.
 */
21
Doctrine::autoload('Doctrine_Query_Abstract');
22 23 24 25 26 27 28 29 30 31 32
/**
 * Doctrine_Query
 *
 * @package     Doctrine
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @category    Object Relational Mapping
 * @link        www.phpdoctrine.com
 * @since       1.0
 * @version     $Revision$
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 */
33
class Doctrine_Query extends Doctrine_Query_Abstract implements Countable
zYne's avatar
zYne committed
34
{
35 36 37
    /**
     * @param array $subqueryAliases        the table aliases needed in some LIMIT subqueries
     */
zYne's avatar
zYne committed
38
    protected $subqueryAliases   = array();
39 40 41
    /**
     * @param boolean $needsSubquery
     */
zYne's avatar
zYne committed
42
    protected $needsSubquery     = false;
zYne's avatar
zYne committed
43

zYne's avatar
zYne committed
44
    protected $_status           = array('needsSubquery' => true);
45 46 47 48
    /**
     * @param boolean $isSubquery           whether or not this query object is a subquery of another 
     *                                      query object
     */
zYne's avatar
zYne committed
49
    protected $isSubquery;
50

zYne's avatar
zYne committed
51
    protected $neededTables      = array();
52 53 54
    /**
     * @var array $pendingFields
     */
zYne's avatar
zYne committed
55
    protected $pendingFields     = array();
zYne's avatar
zYne committed
56 57 58 59
    /**
     * @var array $pendingSubqueries        SELECT part subqueries, these are called pending subqueries since
     *                                      they cannot be parsed directly (some queries might be correlated)
     */
zYne's avatar
zYne committed
60
    protected $pendingSubqueries = array();
zYne's avatar
zYne committed
61
    /**
zYne's avatar
zYne committed
62 63
     * @var array $_parsers                 an array of parser objects
     */
zYne's avatar
zYne committed
64
    protected $_parsers    = array();
zYne's avatar
zYne committed
65 66 67 68
    /**
     * @var array $_enumParams              an array containing the keys of the parameters that should be enumerated
     */
    protected $_enumParams = array();
zYne's avatar
zYne committed
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
    /**
     * @var array $_dqlParts                an array containing all DQL query parts
     */
    protected $_dqlParts   = array(
                            'select'    => array(),
                            'distinct'  => false,
                            'forUpdate' => false,
                            'from'      => array(),
                            'set'       => array(),
                            'join'      => array(),
                            'where'     => array(),
                            'groupby'   => array(),
                            'having'    => array(),
                            'orderby'   => array(),
                            'limit'     => false,
                            'offset'    => false,
                            );
86 87 88 89 90 91 92 93 94 95 96

    /**
     * create
     * returns a new Doctrine_Query object
     *
     * @return Doctrine_Query
     */
    public static function create()
    {
        return new Doctrine_Query();
    }
zYne's avatar
zYne committed
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
    /** 
     * addEnumParam
     * sets input parameter as an enumerated parameter
     *
     * @param string $key   the key of the input parameter
     * @return Doctrine_Query
     */
    public function addEnumParam($key, $table = null, $column = null)
    {
    	$array = (isset($table) || isset($column)) ? array($table, $column) : array();

    	if ($key === '?') {
    	    $this->_enumParams[] = $array;
        } else {
            $this->_enumParams[$key] = $array;
        }
    }
    /**
     * getEnumParams
     * get all enumerated parameters
     *
     * @return array    all enumerated parameters
     */
    public function getEnumParams()
    {
        return $this->_enumParams;
    }
    /**
     * convertEnums
     * convert enum parameters to their integer equivalents
     *
     * @return array    converted parameter array
     */
    public function convertEnums($params) 
    {
        foreach ($this->_enumParams as $key => $values) {
            if (isset($params[$key])) {
                if ( ! empty($values)) {
                    $params[$key] = $values[0]->enumIndex($values[1], $params[$key]);
                }
            }
        }
        return $params;
    }
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
    /**
     * isSubquery
     * if $bool parameter is set this method sets the value of
     * Doctrine_Query::$isSubquery. If this value is set to true
     * the query object will not load the primary key fields of the selected
     * components.
     *
     * If null is given as the first parameter this method retrieves the current
     * value of Doctrine_Query::$isSubquery.
     *
     * @param boolean $bool     whether or not this query acts as a subquery
     * @return Doctrine_Query|bool
     */
    public function isSubquery($bool = null)
    {
        if ($bool === null) {
            return $this->isSubquery;
        }

        $this->isSubquery = (bool) $bool;
        return $this;
    }

    /**
     * getAggregateAlias
     * 
     * @return string
     */
    public function getAggregateAlias($dqlAlias)
    {
        if(isset($this->aggregateMap[$dqlAlias])) {
            return $this->aggregateMap[$dqlAlias];
        }
        
        return null;
    }
zYne's avatar
zYne committed
177 178 179 180 181 182 183 184
    /**
     * getParser
     * parser lazy-loader
     *
     * @throws Doctrine_Query_Exception     if unknown parser name given
     * @return Doctrine_Query_Part
     */
    public function getParser($name)
185
    {
zYne's avatar
zYne committed
186 187
        if ( ! isset($this->_parsers[$name])) {
            $class = 'Doctrine_Query_' . ucwords(strtolower($name));
188

zYne's avatar
zYne committed
189 190 191 192 193
            Doctrine::autoload($class);
            
            if ( ! class_exists($class)) {
                throw new Doctrine_Query_Exception('Unknown parser ' . $name);
            }
194

zYne's avatar
zYne committed
195 196 197 198 199
            $this->_parsers[$name] = new $class($this);
        }
        
        return $this->_parsers[$name];
    }
200 201
    /**
     * parseQueryPart
zYne's avatar
zYne committed
202
     * parses given DQL query part
203 204 205 206 207 208 209 210 211 212
     *
     * @param string $queryPartName     the name of the query part
     * @param string $queryPart         query part to be parsed
     * @param boolean $append           whether or not to append the query part to its stack
     *                                  if false is given, this method will overwrite 
     *                                  the given query part stack with $queryPart
     * @return Doctrine_Query           this object
     */
    public function parseQueryPart($queryPartName, $queryPart, $append = false) 
    {
zYne's avatar
zYne committed
213 214 215 216 217
    	if ($append) {
    	    $this->_dqlParts[$queryPartName][] = $queryPart;
    	} else {
            $this->_dqlParts[$queryPartName] = $queryPart;
    	}
218 219
    	return $this->getParser($queryPartName)->parse($queryPart);
    }
zYne's avatar
zYne committed
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
    /**
     * getDql
     * returns the DQL query associated with this object
     *
     * the query is built from $_dqlParts
     *
     * @return string   the DQL query
     */
    public function getDql()
    {
    	$q = '';
    	$q .= ( ! empty($this->parts['select']))?  'SELECT '    . implode(', ', $this->parts['select']) : '';
        $q .= ( ! empty($this->parts['from']))?    ' FROM '     . implode(' ', $this->parts['from']) : '';
        $q .= ( ! empty($this->parts['where']))?   ' WHERE '    . implode(' AND ', $this->parts['where']) : '';
        $q .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']) : '';
        $q .= ( ! empty($this->parts['having']))?  ' HAVING '   . implode(' AND ', $this->parts['having']) : '';
        $q .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby']) : '';
        $q .= ( ! empty($this->parts['limit']))?   ' LIMIT ' . implode(' ', $this->parts['limit']) : '';
        $q .= ( ! empty($this->parts['offset']))?  ' OFFSET ' . implode(' ', $this->parts['offset']) : '';
        
        return $q;
    }
zYne's avatar
zYne committed
242 243 244 245 246 247 248 249 250 251 252 253 254 255
    /**
     * processPendingFields
     * the fields in SELECT clause cannot be parsed until the components
     * in FROM clause are parsed, hence this method is called everytime a 
     * specific component is being parsed.
     *
     * @throws Doctrine_Query_Exception     if unknown component alias has been given
     * @param string $componentAlias        the alias of the component
     * @return void
     */
    public function processPendingFields($componentAlias)
    {
        $tableAlias = $this->getTableAlias($componentAlias);
        $table      = $this->_aliasMap[$componentAlias]['table'];
256 257 258 259

        if (isset($this->pendingFields[$componentAlias])) {
            $fields = $this->pendingFields[$componentAlias];

zYne's avatar
zYne committed
260
            // check for wildcards
zYne's avatar
zYne committed
261
            if (in_array('*', $fields)) {
262 263 264 265 266 267 268 269 270 271 272 273
                $fields = $table->getColumnNames();
            } else {
                // only auto-add the primary key fields if this query object is not 
                // a subquery of another query object
                if ( ! $this->isSubquery) {
                    $fields = array_unique(array_merge($table->getPrimaryKeys(), $fields));
                }
            }
        }
        foreach ($fields as $name) {
            $name = $table->getColumnName($name);

zYne's avatar
zYne committed
274
            $this->parts['select'][] = $tableAlias . '.' .$name . ' AS ' . $tableAlias . '__' . $name;
275 276 277 278 279 280 281 282 283 284 285 286 287 288
        }
        
        $this->neededTables[] = $tableAlias;

    }
    /**
     * parseSelect
     * parses the query select part and
     * adds selected fields to pendingFields array
     *
     * @param string $dql
     */
    public function parseSelect($dql)
    {
zYne's avatar
zYne committed
289
        $refs = Doctrine_Tokenizer::bracketExplode($dql, ',');
290

zYne's avatar
zYne committed
291 292 293 294 295 296 297 298
        foreach ($refs as $reference) {
            if (strpos($reference, '(') !== false) {
                if (substr($reference, 0, 1) === '(') {
                    // subselect found in SELECT part
                    $this->parseSubselect($reference);
                } else {
                    $this->parseAggregateFunction2($reference);
                }
299 300 301 302 303 304 305 306 307 308 309
            } else {

                $e = explode('.', $reference);
                if (count($e) > 2) {
                    $this->pendingFields[] = $reference;
                } else {
                    $this->pendingFields[$e[0]][] = $e[1];
                }
            }
        }
    }
zYne's avatar
zYne committed
310 311 312 313 314 315 316 317 318 319 320
    /** 
     * parseSubselect
     *
     * parses the subquery found in DQL SELECT part and adds the
     * parsed form into $pendingSubqueries stack
     *
     * @param string $reference
     * @return void
     */
    public function parseSubselect($reference) 
    {
zYne's avatar
zYne committed
321
        $e     = Doctrine_Tokenizer::bracketExplode($reference, ' ');
zYne's avatar
zYne committed
322 323 324 325 326 327 328 329 330 331 332 333 334
        $alias = $e[1];

        if (count($e) > 2) {
            if (strtoupper($e[1]) !== 'AS') {
                throw new Doctrine_Query_Exception('Syntax error near: ' . $reference);
            }
            $alias = $e[2];
        }
        
        $subquery = substr($e[0], 1, -1);
        
        $this->pendingSubqueries[] = array($subquery, $alias);
    }
335 336
    public function parseAggregateFunction2($func)
    {
zYne's avatar
zYne committed
337
        $e    = Doctrine_Tokenizer::bracketExplode($func, ' ');
338 339 340 341 342
        $func = $e[0];

        $pos  = strpos($func, '(');
        $name = substr($func, 0, $pos);

zYne's avatar
zYne committed
343
        try {
344 345
            $argStr = substr($func, ($pos + 1), -1);
            $args   = explode(',', $argStr);
zYne's avatar
zYne committed
346
    
347
            $func   = call_user_func_array(array($this->conn->expression, $name), $args);
zYne's avatar
zYne committed
348
    
349 350 351 352 353 354
            if(substr($func, 0, 1) !== '(') {
                $pos  = strpos($func, '(');
                $name = substr($func, 0, $pos);
            } else {
                $name = $func;
            }
zYne's avatar
zYne committed
355
    
356
            $e2     = explode(' ', $args[0]);
zYne's avatar
zYne committed
357
    
358
            $distinct = '';
zYne's avatar
zYne committed
359 360
            if (count($e2) > 1) {
                if (strtoupper($e2[0]) == 'DISTINCT') {
361
                    $distinct  = 'DISTINCT ';
zYne's avatar
zYne committed
362
                }
zYne's avatar
zYne committed
363
    
364 365
                $args[0] = $e2[1];
            }
zYne's avatar
zYne committed
366 367 368
    
    
    
369 370 371
            $parts = explode('.', $args[0]);
            $owner = $parts[0];
            $alias = (isset($e[1])) ? $e[1] : $name;
zYne's avatar
zYne committed
372
    
373
            $e3    = explode('.', $alias);
zYne's avatar
zYne committed
374
    
zYne's avatar
zYne committed
375
            if (count($e3) > 1) {
376 377 378
                $alias = $e3[1];
                $owner = $e3[0];
            }
zYne's avatar
zYne committed
379
    
380 381 382 383
            // a function without parameters eg. RANDOM()
            if ($owner === '') {
                $owner = 0;
            }
zYne's avatar
zYne committed
384
    
385
            $this->pendingAggregates[$owner][] = array($name, $args, $distinct, $alias);
zYne's avatar
zYne committed
386 387
        } catch(Doctrine_Expression_Exception $e) {
            throw new Doctrine_Query_Exception('Unknown function ' . $func . '.');
388 389
        }
    }
zYne's avatar
zYne committed
390 391 392 393 394 395 396
    public function processPendingSubqueries() 
    {
    	foreach ($this->pendingSubqueries as $value) {
            list($dql, $alias) = $value;

            $sql = $this->createSubquery()->parseQuery($dql, false)->getQuery();

zYne's avatar
zYne committed
397 398 399 400 401


            reset($this->_aliasMap);
            $componentAlias = key($this->_aliasMap);
            $tableAlias = $this->getTableAlias($componentAlias);
zYne's avatar
zYne committed
402 403

            $sqlAlias = $tableAlias . '__' . count($this->aggregateMap);
zYne's avatar
zYne committed
404

zYne's avatar
zYne committed
405
            $this->parts['select'][] = '(' . $sql . ') AS ' . $sqlAlias;
zYne's avatar
zYne committed
406

zYne's avatar
zYne committed
407 408 409
            $this->aggregateMap[$alias] = $sqlAlias;
            $this->subqueryAggregates[$componentAlias][] = $alias;
        }
zYne's avatar
zYne committed
410
        $this->pendingSubqueries = array();
zYne's avatar
zYne committed
411
    }
412 413
    public function processPendingAggregates($componentAlias)
    {
zYne's avatar
zYne committed
414 415 416 417 418
        $tableAlias = $this->getTableAlias($componentAlias);     

        $map   = reset($this->_aliasMap);
        $root  = $map['table'];
        $table = $this->_aliasMap[$componentAlias]['table'];
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439

        $aggregates = array();

        if(isset($this->pendingAggregates[$componentAlias])) {
            $aggregates = $this->pendingAggregates[$componentAlias];
        }
        
        if ($root === $table) {
            if (isset($this->pendingAggregates[0])) {
                $aggregates += $this->pendingAggregates[0];
            }
        }

        foreach($aggregates as $parts) {
            list($name, $args, $distinct, $alias) = $parts;

            $arglist = array();
            foreach($args as $arg) {
                $e = explode('.', $arg);


zYne's avatar
zYne committed
440 441 442
                if (is_numeric($arg)) {
                    $arglist[]  = $arg;
                } elseif (count($e) > 1) {
zYne's avatar
zYne committed
443 444
                    $map    = $this->_aliasMap[$e[0]];
                    $table  = $map['table'];
445 446 447

                    $e[1]       = $table->getColumnName($e[1]);

zYne's avatar
zYne committed
448
                    if ( ! $table->hasColumn($e[1])) {
449 450 451 452 453 454 455 456 457 458 459
                        throw new Doctrine_Query_Exception('Unknown column ' . $e[1]);
                    }

                    $arglist[]  = $tableAlias . '.' . $e[1];
                } else {
                    $arglist[]  = $e[0];
                }
            }

            $sqlAlias = $tableAlias . '__' . count($this->aggregateMap);

zYne's avatar
zYne committed
460
            if (substr($name, 0, 1) !== '(') {
461 462 463 464 465 466 467 468 469
                $this->parts['select'][] = $name . '(' . $distinct . implode(', ', $arglist) . ') AS ' . $sqlAlias;
            } else {
                $this->parts['select'][] = $name . ' AS ' . $sqlAlias;
            }
            $this->aggregateMap[$alias] = $sqlAlias;
            $this->neededTables[] = $tableAlias;
        }
    }
    /**
zYne's avatar
zYne committed
470 471 472
     * getQueryBase
     * returns the base of the generated sql query
     * On mysql driver special strategy has to be used for DELETE statements
473
     *
zYne's avatar
zYne committed
474
     * @return string       the base of the generated sql query
475
     */
zYne's avatar
zYne committed
476
    public function getQueryBase()
477
    {
zYne's avatar
zYne committed
478 479 480
        switch ($this->type) {
            case self::DELETE:
                $q = 'DELETE FROM ';
481
            break;
zYne's avatar
zYne committed
482 483
            case self::UPDATE:
                $q = 'UPDATE ';
484
            break;
zYne's avatar
zYne committed
485 486
            case self::SELECT:
                $distinct = ($this->isDistinct()) ? 'DISTINCT ' : '';
487

zYne's avatar
zYne committed
488 489
                $q = 'SELECT ' . $distinct . implode(', ', $this->parts['select']) . ' FROM ';
            break;
490
        }
zYne's avatar
zYne committed
491
        return $q;
492 493
    }
    /**
zYne's avatar
zYne committed
494
     * buildFromPart
495
     *
zYne's avatar
zYne committed
496
     * @return string
497
     */
zYne's avatar
zYne committed
498
    public function buildFromPart()
499
    {
zYne's avatar
zYne committed
500 501 502 503 504 505 506
    	$q = '';
        foreach ($this->parts['from'] as $k => $part) {
            if ($k === 0) {
                $q .= $part;
                continue;
            }
            // preserve LEFT JOINs only if needed
507

zYne's avatar
zYne committed
508 509
            if (substr($part, 0, 9) === 'LEFT JOIN') {
                $e = explode(' ', $part);
510

zYne's avatar
zYne committed
511 512
                $aliases = array_merge($this->subqueryAliases,
                            array_keys($this->neededTables));
513

zYne's avatar
zYne committed
514 515
                if( ! in_array($e[3], $aliases) &&
                    ! in_array($e[2], $aliases) &&
516

zYne's avatar
zYne committed
517 518
                    ! empty($this->pendingFields)) {
                    continue;
519
                }
520

zYne's avatar
zYne committed
521
            }
522

zYne's avatar
zYne committed
523 524 525 526
            $e = explode(' ON ', $part);
            
            // we can always be sure that the first join condition exists
            $e2 = explode(' AND ', $e[1]);
527

zYne's avatar
zYne committed
528
            $part = $e[0] . ' ON ' . array_shift($e2);
529

zYne's avatar
zYne committed
530 531 532 533
            if ( ! empty($e2)) {
                $parser = new Doctrine_Query_JoinCondition($this);
                $part  .= ' AND ' . $parser->_parse(implode(' AND ', $e2));
            }
534

zYne's avatar
zYne committed
535
            $q .= ' ' . $part;
536 537 538 539 540 541 542 543 544 545 546 547 548
        }
        return $q;
    }
    /**
     * builds the sql query from the given parameters and applies things such as
     * column aggregation inheritance and limit subqueries if needed
     *
     * @param array $params             an array of prepared statement params (needed only in mysql driver
     *                                  when limit subquery algorithm is used)
     * @return string                   the built sql query
     */
    public function getQuery($params = array())
    {
zYne's avatar
zYne committed
549
        if (empty($this->parts['select']) || empty($this->parts['from'])) {
550
            return false;
zYne's avatar
zYne committed
551
        }
552 553 554

        $needsSubQuery = false;
        $subquery = '';
zYne's avatar
zYne committed
555 556 557
        $map   = reset($this->_aliasMap);
        $table = $map['table'];
        $rootAlias = key($this->_aliasMap);
558

zYne's avatar
zYne committed
559
        if ( ! empty($this->parts['limit']) && $this->needsSubquery && $table->getAttribute(Doctrine::ATTR_QUERY_LIMIT) == Doctrine::LIMIT_RECORDS) {
560 561 562
            $needsSubQuery = true;
        }

zYne's avatar
zYne committed
563 564 565
        // process all pending SELECT part subqueries
        $this->processPendingSubqueries();

566 567 568
        // build the basic query

        $str = '';
zYne's avatar
zYne committed
569
        if ($this->isDistinct()) {
570 571 572
            $str = 'DISTINCT ';
        }

zYne's avatar
zYne committed
573 574 575 576
        $q  = $this->getQueryBase();
        $q .= $this->buildFromPart();

        if ( ! empty($this->parts['set'])) {
577 578 579 580 581
            $q .= ' SET ' . implode(', ', $this->parts['set']);
        }

        $string = $this->applyInheritance();

zYne's avatar
zYne committed
582 583 584
        if ( ! empty($string)) {
            $this->parts['where'][] = '(' . $string . ')';
        }
585 586 587


        $modifyLimit = true;
zYne's avatar
zYne committed
588
        if ( ! empty($this->parts["limit"]) || ! empty($this->parts["offset"])) {
589

zYne's avatar
zYne committed
590
            if ($needsSubQuery) {
591 592 593
                $subquery = $this->getLimitSubquery();


zYne's avatar
zYne committed
594
                switch (strtolower($this->conn->getName())) {
595 596 597 598
                    case 'mysql':
                        // mysql doesn't support LIMIT in subqueries
                        $list     = $this->conn->execute($subquery, $params)->fetchAll(PDO::FETCH_COLUMN);
                        $subquery = implode(', ', $list);
zYne's avatar
zYne committed
599
                        break;
600 601 602
                    case 'pgsql':
                        // pgsql needs special nested LIMIT subquery
                        $subquery = 'SELECT doctrine_subquery_alias.' . $table->getIdentifier(). ' FROM (' . $subquery . ') AS doctrine_subquery_alias';
zYne's avatar
zYne committed
603
                        break;
604 605
                }

zYne's avatar
zYne committed
606
                $field = $this->aliasHandler->getShortAlias($rootAlias) . '.' . $table->getIdentifier();
607 608

                // only append the subquery if it actually contains something
zYne's avatar
zYne committed
609
                if ($subquery !== '') {
610
                    array_unshift($this->parts['where'], $field. ' IN (' . $subquery . ')');
zYne's avatar
zYne committed
611
                }
612 613 614 615 616

                $modifyLimit = false;
            }
        }

zYne's avatar
zYne committed
617 618 619 620
        $q .= ( ! empty($this->parts['where']))?   ' WHERE '    . implode(' AND ', $this->parts['where']) : '';
        $q .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby'])  : '';
        $q .= ( ! empty($this->parts['having']))?  ' HAVING '   . implode(' AND ', $this->parts['having']): '';
        $q .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby'])  : '';
621

zYne's avatar
zYne committed
622
        if ($modifyLimit) {
623
            $q = $this->conn->modifyLimitQuery($q, $this->parts['limit'], $this->parts['offset']);
zYne's avatar
zYne committed
624
        }
625 626

        // return to the previous state
zYne's avatar
zYne committed
627
        if ( ! empty($string)) {
628
            array_pop($this->parts['where']);
zYne's avatar
zYne committed
629 630
        }
        if ($needsSubQuery) {
631
            array_shift($this->parts['where']);
zYne's avatar
zYne committed
632
        }
633 634 635
        return $q;
    }
    /**
zYne's avatar
zYne committed
636
     * getLimitSubquery
637 638 639 640 641 642 643 644 645 646
     * this is method is used by the record limit algorithm
     *
     * when fetching one-to-many, many-to-many associated data with LIMIT clause
     * an additional subquery is needed for limiting the number of returned records instead
     * of limiting the number of sql result set rows
     *
     * @return string       the limit subquery
     */
    public function getLimitSubquery()
    {
zYne's avatar
zYne committed
647 648 649
        $map    = reset($this->_aliasMap);
        $table  = $map['table'];
        $componentAlias = key($this->_aliasMap);
650 651

        // get short alias
zYne's avatar
zYne committed
652
        $alias      = $this->aliasHandler->getShortAlias($componentAlias);
653 654 655 656 657
        $primaryKey = $alias . '.' . $table->getIdentifier();

        // initialize the base of the subquery
        $subquery   = 'SELECT DISTINCT ' . $primaryKey;

zYne's avatar
zYne committed
658
        if ($this->conn->getDBH()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') {
659 660
            // pgsql needs the order by fields to be preserved in select clause

zYne's avatar
zYne committed
661
            foreach ($this->parts['orderby'] as $part) {
662 663 664
                $e = explode(' ', $part);

                // don't add primarykey column (its already in the select clause)
zYne's avatar
zYne committed
665
                if ($e[0] !== $primaryKey) {
666
                    $subquery .= ', ' . $e[0];
zYne's avatar
zYne committed
667
                }
668 669 670
            }
        }

zYne's avatar
zYne committed
671
        $subquery .= ' FROM';
672

zYne's avatar
zYne committed
673 674 675 676
        foreach ($this->parts['from'] as $part) {
            // preserve LEFT JOINs only if needed
            if (substr($part,0,9) === 'LEFT JOIN') {
                $e = explode(' ', $part);
677

zYne's avatar
zYne committed
678 679 680
                if ( ! in_array($e[3], $this->subqueryAliases) &&
                     ! in_array($e[2], $this->subqueryAliases)) {
                    continue;
681 682
                }
            }
zYne's avatar
zYne committed
683 684

            $subquery .= ' ' . $part;
685 686 687 688 689 690 691 692 693 694 695
        }

        // all conditions must be preserved in subquery
        $subquery .= ( ! empty($this->parts['where']))?   ' WHERE '    . implode(' AND ', $this->parts['where'])  : '';
        $subquery .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby'])   : '';
        $subquery .= ( ! empty($this->parts['having']))?  ' HAVING '   . implode(' AND ', $this->parts['having']) : '';
        $subquery .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby'])   : '';

        // add driver specific limit clause
        $subquery = $this->conn->modifyLimitQuery($subquery, $this->parts['limit'], $this->parts['offset']);

zYne's avatar
zYne committed
696
        $parts = Doctrine_Tokenizer::quoteExplode($subquery, ' ', "'", "'");
697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721

        foreach($parts as $k => $part) {
            if(strpos($part, "'") !== false) {
                continue;
            }

            if($this->aliasHandler->hasAliasFor($part)) {
                $parts[$k] = $this->aliasHandler->generateNewAlias($part);
            }

            if(strpos($part, '.') !== false) {
                $e = explode('.', $part);

                $trimmed = ltrim($e[0], '( ');
                $pos     = strpos($e[0], $trimmed);

                $e[0] = substr($e[0], 0, $pos) . $this->aliasHandler->generateNewAlias($trimmed);
                $parts[$k] = implode('.', $e);
            }
        }
        $subquery = implode(' ', $parts);

        return $subquery;
    }
    /**
zYne's avatar
zYne committed
722
     * tokenizeQuery
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
     * splits the given dql query into an array where keys
     * represent different query part names and values are
     * arrays splitted using sqlExplode method
     *
     * example:
     *
     * parameter:
     *      $query = "SELECT u.* FROM User u WHERE u.name LIKE ?"
     * returns:
     *      array('select' => array('u.*'),
     *            'from'   => array('User', 'u'),
     *            'where'  => array('u.name', 'LIKE', '?'))
     *
     * @param string $query                 DQL query
     * @throws Doctrine_Query_Exception     if some generic parsing error occurs
     * @return array                        an array containing the query string parts
     */
zYne's avatar
zYne committed
740
    public function tokenizeQuery($query)
741
    {
zYne's avatar
zYne committed
742
        $e = Doctrine_Tokenizer::sqlExplode($query, ' ');
743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791

        foreach($e as $k=>$part) {
            $part = trim($part);
            switch(strtolower($part)) {
                case 'delete':
                case 'update':
                case 'select':
                case 'set':
                case 'from':
                case 'where':
                case 'limit':
                case 'offset':
                case 'having':
                    $p = $part;
                    $parts[$part] = array();
                break;
                case 'order':
                case 'group':
                    $i = ($k + 1);
                    if(isset($e[$i]) && strtolower($e[$i]) === "by") {
                        $p = $part;
                        $parts[$part] = array();
                    } else
                        $parts[$p][] = $part;
                break;
                case "by":
                    continue;
                default:
                    if( ! isset($p))
                        throw new Doctrine_Query_Exception("Couldn't parse query.");

                    $parts[$p][] = $part;
            }
        }
        return $parts;
    }
    /**
     * DQL PARSER
     * parses a DQL query
     * first splits the query in parts and then uses individual
     * parsers for each part
     *
     * @param string $query                 DQL query
     * @param boolean $clear                whether or not to clear the aliases
     * @throws Doctrine_Query_Exception     if some generic parsing error occurs
     * @return Doctrine_Query
     */
    public function parseQuery($query, $clear = true)
    {
zYne's avatar
zYne committed
792
        if ($clear) {
793
            $this->clear();
zYne's avatar
zYne committed
794
        }
795 796 797 798 799

        $query = trim($query);
        $query = str_replace("\n", ' ', $query);
        $query = str_replace("\r", ' ', $query);

zYne's avatar
zYne committed
800
        $parts = $this->tokenizeQuery($query);
801 802 803

        foreach($parts as $k => $part) {
            $part = implode(' ', $part);
zYne's avatar
zYne committed
804 805
            switch(strtolower($k)) {
                case 'create':
806 807
                    $this->type = self::CREATE;
                break;
zYne's avatar
zYne committed
808
                case 'insert':
809 810
                    $this->type = self::INSERT;
                break;
zYne's avatar
zYne committed
811
                case 'delete':
812 813
                    $this->type = self::DELETE;
                break;
zYne's avatar
zYne committed
814
                case 'select':
815 816 817
                    $this->type = self::SELECT;
                    $this->parseSelect($part);
                break;
zYne's avatar
zYne committed
818
                case 'update':
819 820 821
                    $this->type = self::UPDATE;
                    $k = 'FROM';

zYne's avatar
zYne committed
822
                case 'from':
823 824 825 826
                    $class  = 'Doctrine_Query_' . ucwords(strtolower($k));
                    $parser = new $class($this);
                    $parser->parse($part);
                break;
zYne's avatar
zYne committed
827
                case 'set':
828 829
                    $class  = 'Doctrine_Query_' . ucwords(strtolower($k));
                    $parser = new $class($this);
zYne's avatar
zYne committed
830
                    $parser->parse($part);
831
                break;
zYne's avatar
zYne committed
832 833
                case 'group':
                case 'order':
834
                    $k .= 'by';
zYne's avatar
zYne committed
835 836
                case 'where':
                case 'having':
837 838 839 840
                    $class  = 'Doctrine_Query_' . ucwords(strtolower($k));
                    $parser = new $class($this);

                    $name = strtolower($k);
zYne's avatar
zYne committed
841
                    $parser->parse($part);
842
                break;
zYne's avatar
zYne committed
843
                case 'limit':
844 845
                    $this->parts['limit'] = trim($part);
                break;
zYne's avatar
zYne committed
846
                case 'offset':
847 848 849 850 851 852 853
                    $this->parts['offset'] = trim($part);
                break;
            }
        }

        return $this;
    }
zYne's avatar
zYne committed
854
    public function load($path, $loadFields = true) 
855 856 857 858 859
    {
        // parse custom join conditions
        $e = explode(' ON ', $path);
        
        $joinCondition = '';
zYne's avatar
zYne committed
860 861

        if (count($e) > 1) {
862 863 864 865
            $joinCondition = ' AND ' . $e[1];
            $path = $e[0];
        }

zYne's avatar
zYne committed
866 867
        $tmp           = explode(' ', $path);
        $originalAlias = (count($tmp) > 1) ? end($tmp) : null;
868 869 870

        $e = preg_split("/[.:]/", $tmp[0], -1);

zYne's avatar
zYne committed
871 872 873
        $fullPath = $tmp[0];
        $prevPath = '';
        $fullLength = strlen($fullPath);
874

zYne's avatar
zYne committed
875 876
        if (isset($this->_aliasMap[$e[0]])) {
            $table = $this->_aliasMap[$e[0]]['table'];
877

zYne's avatar
zYne committed
878 879
            $prevPath = $parent = array_shift($e);
        }
880

zYne's avatar
zYne committed
881 882 883
        foreach ($e as $key => $name) {
            // get length of the previous path
            $length = strlen($prevPath);
884

zYne's avatar
zYne committed
885 886
            // build the current component path
            $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name;
887

zYne's avatar
zYne committed
888
            $delimeter = substr($fullPath, $length, 1);
889

zYne's avatar
zYne committed
890 891 892 893 894 895
            // if an alias is not given use the current path as an alias identifier
            if (strlen($prevPath) === $fullLength && isset($originalAlias)) {
                $componentAlias = $originalAlias;
            } else {
                $componentAlias = $prevPath;
            }
896

zYne's avatar
zYne committed
897 898
            if ( ! isset($table)) {
                // process the root of the path
899

zYne's avatar
zYne committed
900 901 902
                $table = $this->loadRoot($name, $componentAlias);
            } else {
                $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN ';
903

zYne's avatar
zYne committed
904
                $relation = $table->getRelation($name);
905

zYne's avatar
zYne committed
906 907 908 909 910 911
                $this->_aliasMap[$componentAlias] = array('table'    => $relation->getTable(),
                                                          'parent'   => $parent,
                                                          'relation' => $relation);
                if ( ! $relation->isOneToOne()) {
                   $this->needsSubquery = true;
                }
912

zYne's avatar
zYne committed
913 914 915 916
                $localAlias   = $this->getShortAlias($parent, $table->getTableName());
                $foreignAlias = $this->getShortAlias($componentAlias, $relation->getTable()->getTableName());
                $localSql     = $this->conn->quoteIdentifier($table->getTableName()) . ' ' . $localAlias;
                $foreignSql   = $this->conn->quoteIdentifier($relation->getTable()->getTableName()) . ' ' . $foreignAlias;
917

zYne's avatar
zYne committed
918 919 920 921 922
                $map = $relation->getTable()->inheritanceMap;
  
                if ( ! $loadFields || ! empty($map) || $joinCondition) {
                    $this->subqueryAliases[] = $foreignAlias;
                }
923

zYne's avatar
zYne committed
924 925 926 927 928 929 930
                if ($relation instanceof Doctrine_Relation_Association) {
                    $asf = $relation->getAssociationFactory();
  
                    $assocTableName = $asf->getTableName();
  
                    if( ! $loadFields || ! empty($map) || $joinCondition) {
                        $this->subqueryAliases[] = $assocTableName;
931 932
                    }

zYne's avatar
zYne committed
933 934 935
                    $assocPath = $prevPath . '.' . $asf->getComponentName();
  
                    $assocAlias = $this->getShortAlias($assocPath, $asf->getTableName());
936

zYne's avatar
zYne committed
937 938 939
                    $queryPart = $join . $assocTableName . ' ' . $assocAlias . ' ON ' . $localAlias  . '.'
                                                                  . $table->getIdentifier() . ' = '
                                                                  . $assocAlias . '.' . $relation->getLocal();
940

zYne's avatar
zYne committed
941 942 943
                    if ($relation instanceof Doctrine_Relation_Association_Self) {
                        $queryPart .= ' OR ' . $localAlias  . '.' . $table->getIdentifier() . ' = '
                                                                  . $assocAlias . '.' . $relation->getForeign();
944 945
                    }

zYne's avatar
zYne committed
946
                    $this->parts['from'][] = $queryPart;
947

zYne's avatar
zYne committed
948 949 950 951
                    $queryPart = $join . $foreignSql . ' ON ' . $foreignAlias . '.'
                                               . $relation->getTable()->getIdentifier() . ' = '
                                               . $assocAlias . '.' . $relation->getForeign()
                                               . $joinCondition;
952

zYne's avatar
zYne committed
953 954 955
                    if ($relation instanceof Doctrine_Relation_Association_Self) {
                        $queryPart .= ' OR ' . $foreignAlias  . '.' . $table->getIdentifier() . ' = '
                                             . $assocAlias . '.' . $relation->getLocal();
956 957
                    }

zYne's avatar
zYne committed
958
                } else {
959

zYne's avatar
zYne committed
960 961 962 963
                    $queryPart = $join . $foreignSql
                                       . ' ON ' . $localAlias .  '.'
                                       . $relation->getLocal() . ' = ' . $foreignAlias . '.' . $relation->getForeign()
                                       . $joinCondition;
964
                }
zYne's avatar
zYne committed
965 966 967 968 969 970 971 972 973
                $this->parts['from'][] = $queryPart;
            }
            if ($loadFields) {
                             	
                $restoreState = false;
                // load fields if necessary
                if ($loadFields && empty($this->pendingFields) 
                    && empty($this->pendingAggregates)
                    && empty($this->pendingSubqueries)) {
974

zYne's avatar
zYne committed
975
                    $this->pendingFields[$componentAlias] = array('*');
976

zYne's avatar
zYne committed
977 978
                    $restoreState = true;
                }
979

zYne's avatar
zYne committed
980 981
                if(isset($this->pendingFields[$componentAlias])) {
                    $this->processPendingFields($componentAlias);
982 983
                }

zYne's avatar
zYne committed
984 985 986
                if(isset($this->pendingAggregates[$componentAlias]) || isset($this->pendingAggregates[0])) {
                    $this->processPendingAggregates($componentAlias);
                }
987

zYne's avatar
zYne committed
988 989 990 991
                if ($restoreState) {
                    $this->pendingFields = array();
                    $this->pendingAggregates = array();
                }
992
            }
zYne's avatar
zYne committed
993
            $parent = $prevPath;
994
        }
zYne's avatar
zYne committed
995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010
        return end($this->_aliasMap);
    }
    /**
     * loadRoot
     *
     * @param string $name
     * @param string $componentAlias
     */
    public function loadRoot($name, $componentAlias)
    {
    	// get the connection for the component
        $this->conn = Doctrine_Manager::getInstance()
                      ->getConnectionForComponent($name);

        $table = $this->conn->getTable($name);
        $tableName = $table->getTableName();
1011

zYne's avatar
zYne committed
1012 1013 1014 1015 1016 1017 1018
        // get the short alias for this table
        $tableAlias = $this->aliasHandler->getShortAlias($componentAlias, $tableName);
        // quote table name
        $queryPart = $this->conn->quoteIdentifier($tableName);

        if ($this->type === self::SELECT) {
            $queryPart .= ' ' . $tableAlias;
1019 1020
        }

zYne's avatar
zYne committed
1021 1022 1023 1024
        $this->parts['from'][] = $queryPart;
        $this->tableAliases[$tableAlias]  = $componentAlias;
        $this->_aliasMap[$componentAlias] = array('table' => $table);
        
1025 1026
        return $table;
    }
zYne's avatar
zYne committed
1027 1028 1029 1030 1031 1032
	/**
 	 * count
 	 * fetches the count of the query
 	 *
 	 * This method executes the main query without all the
     * selected fields, ORDER BY part, LIMIT part and OFFSET part.
1033
     *
zYne's avatar
zYne committed
1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046
     * Example:
     * Main query: 
     *      SELECT u.*, p.phonenumber FROM User u
     *          LEFT JOIN u.Phonenumber p 
     *          WHERE p.phonenumber = '123 123' LIMIT 10
     *
     * The query this method executes: 
     *      SELECT COUNT(DISTINCT u.id) FROM User u
     *          LEFT JOIN u.Phonenumber p
     *          WHERE p.phonenumber = '123 123'
     *
     * @param array $params        an array of prepared statement parameters
	 * @return integer             the count of this query
1047
     */
zYne's avatar
zYne committed
1048
	public function count($params = array())
1049
    {
zYne's avatar
zYne committed
1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060
    	// initialize temporary variables
		$where  = $this->parts['where'];
		$having = $this->parts['having'];
		$map    = reset($this->_aliasMap);
		$componentAlias = key($this->_aliasMap);
		$table = $map['table'];

        // build the query base
		$q  = 'SELECT COUNT(DISTINCT ' . $this->aliasHandler->getShortAlias($table->getTableName())
            . '.' . $table->getIdentifier()
            . ') FROM ' . $this->buildFromPart();
1061

zYne's avatar
zYne committed
1062 1063
        // append column aggregation inheritance (if needed)
        $string = $this->applyInheritance();
1064

zYne's avatar
zYne committed
1065 1066 1067 1068 1069 1070
        if ( ! empty($string)) {
            $where[] = $string;
        }
        // append conditions
        $q .= ( ! empty($where)) ?  ' WHERE '  . implode(' AND ', $where) : '';
		$q .= ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having): '';
1071

zYne's avatar
zYne committed
1072 1073 1074 1075 1076
        if ( ! is_array($params)) {
            $params = array($params);
        }
        // append parameters
        $params = array_merge($this->params, $params);
1077

zYne's avatar
zYne committed
1078 1079
		return (int) $this->getConnection()->fetchOne($q, $params);
	}
1080

zYne's avatar
zYne committed
1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092
    /**
     * query
     * query the database with DQL (Doctrine Query Language)
     *
     * @param string $query     DQL query
     * @param array $params     prepared statement parameters
     * @see Doctrine::FETCH_* constants
     * @return mixed
     */
    public function query($query, $params = array())
    {
        $this->parseQuery($query);
1093

zYne's avatar
zYne committed
1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111
        return $this->execute($params);
    }
    /**
     * getShortAlias
     * some database such as Oracle need the identifier lengths to be < ~30 chars
     * hence Doctrine creates as short identifier aliases as possible
     *
     * this method is used for the creation of short table aliases, its also
     * smart enough to check if an alias already exists for given component (componentAlias)
     *
     * @param string $componentAlias    the alias for the query component to search table alias for
     * @param string $tableName         the table name from which the table alias is being created
     * @return string                   the generated / fetched short alias
     */
    public function getShortAlias($componentAlias, $tableName)
    {
        return $this->aliasHandler->getShortAlias($componentAlias, $tableName);
    }
1112
}