Ticket #1272: Query.php

File Query.php, 69.0 KB (added by mm, 6 months ago)

Patched file

Line 
1<?php
2/*
3 *  $Id: $
4 *
5 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16 *
17 * This software consists of voluntary contributions made by many individuals
18 * and is licensed under the LGPL. For more information, see
19 * <http://www.phpdoctrine.org>.
20 */
21
22/**
23 * Doctrine_Query
24 * A Doctrine_Query object represents a DQL query. It is used to query databases for
25 * data in an object-oriented fashion. A DQL query understands relations and inheritance
26 * and is dbms independant.
27 *
28 * @package     Doctrine
29 * @subpackage  Query
30 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
31 * @link        www.phpdoctrine.org
32 * @since       1.0
33 * @version     $Revision: 4645 $
34 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
35 * @todo        Proposal: This class does far too much. It should have only 1 task: Collecting
36 *              the DQL query parts and the query parameters (the query state and caching options/methods
37 *              can remain here, too).
38 *              The actual SQL construction could be done by a separate object (Doctrine_Query_SqlBuilder?)
39 *              whose task it is to convert DQL into SQL.
40 *              Furthermore the SqlBuilder? can then use other objects (Doctrine_Query_Tokenizer?),
41 *              (Doctrine_Query_Parser(s)?) to accomplish his work. Doctrine_Query does not need
42 *              to know the tokenizer/parsers. There could be extending
43 *              implementations of SqlBuilder? that cover the specific SQL dialects.
44 *              This would release Doctrine_Connection and the Doctrine_Connection_xxx classes
45 *              from this tedious task.
46 *              This would also largely reduce the currently huge interface of Doctrine_Query(_Abstract)
47 *              and better hide all these transformation internals from the public Query API.
48 *
49 * @internal    The lifecycle of a Query object is the following:
50 *              After construction the query object is empty. Through using the fluent
51 *              query interface the user fills the query object with DQL parts and query parameters.
52 *              These get collected in {@link $_dqlParts} and {@link $_params}, respectively.
53 *              When the query is executed the first time, or when {@link getSqlQuery()}
54 *              is called the first time, the collected DQL parts get parsed and the resulting
55 *              connection-driver specific SQL is generated. The generated SQL parts are
56 *              stored in {@link $_sqlParts} and the final resulting SQL query is stored in
57 *              {@link $_sql}.
58 */
59class Doctrine_Query extends Doctrine_Query_Abstract implements Countable, Serializable
60{
61    /**
62     * @var array  The DQL keywords.
63     */
64    protected static $_keywords  = array('ALL',
65                                         'AND',
66                                         'ANY',
67                                         'AS',
68                                         'ASC',
69                                         'AVG',
70                                         'BETWEEN',
71                                         'BIT_LENGTH',
72                                         'BY',
73                                         'CHARACTER_LENGTH',
74                                         'CHAR_LENGTH',
75                                         'CURRENT_DATE',
76                                         'CURRENT_TIME',
77                                         'CURRENT_TIMESTAMP',
78                                         'DELETE',
79                                         'DESC',
80                                         'DISTINCT',
81                                         'EMPTY',
82                                         'EXISTS',
83                                         'FALSE',
84                                         'FETCH',
85                                         'FROM',
86                                         'GROUP',
87                                         'HAVING',
88                                         'IN',
89                                         'INDEXBY',
90                                         'INNER',
91                                         'IS',
92                                         'JOIN',
93                                         'LEFT',
94                                         'LIKE',
95                                         'LOWER',
96                                         'MEMBER',
97                                         'MOD',
98                                         'NEW',
99                                         'NOT',
100                                         'NULL',
101                                         'OBJECT',
102                                         'OF',
103                                         'OR',
104                                         'ORDER',
105                                         'OUTER',
106                                         'POSITION',
107                                         'SELECT',
108                                         'SOME',
109                                         'TRIM',
110                                         'TRUE',
111                                         'UNKNOWN',
112                                         'UPDATE',
113                                         'WHERE');
114
115    /**
116     * @var array
117     */
118    protected $_subqueryAliases = array();
119
120    /**
121     * @var array $_aggregateAliasMap       an array containing all aggregate aliases, keys as dql aliases
122     *                                      and values as sql aliases
123     */
124    protected $_aggregateAliasMap      = array();
125
126    /**
127     * @var array
128     */
129    protected $_pendingAggregates = array();
130
131    /**
132     * @param boolean $needsSubquery
133     */
134    protected $_needsSubquery = false;
135
136    /**
137     * @param boolean $isSubquery           whether or not this query object is a subquery of another
138     *                                      query object
139     */
140    protected $_isSubquery;
141
142    /**
143     * @var array $_neededTables            an array containing the needed table aliases
144     */
145    protected $_neededTables = array();
146
147    /**
148     * @var array $pendingSubqueries        SELECT part subqueries, these are called pending subqueries since
149     *                                      they cannot be parsed directly (some queries might be correlated)
150     */
151    protected $_pendingSubqueries = array();
152
153    /**
154     * @var array $_pendingFields           an array of pending fields (fields waiting to be parsed)
155     */
156    protected $_pendingFields = array();
157
158    /**
159     * @var array $_parsers                 an array of parser objects, each DQL query part has its own parser
160     */
161    protected $_parsers = array();
162
163    /**
164     * @var array $_pendingJoinConditions    an array containing pending joins
165     */
166    protected $_pendingJoinConditions = array();
167
168    /**
169     * @var array
170     */
171    protected $_expressionMap = array();
172
173    /**
174     * @var string $_sql            cached SQL query
175     */
176    protected $_sql;
177
178
179    /**
180     * create
181     * returns a new Doctrine_Query object
182     *
183     * @param Doctrine_Connection $conn     optional connection parameter
184     * @return Doctrine_Query
185     */
186    public static function create($conn = null)
187    {
188        return new Doctrine_Query($conn);
189    }
190
191    /**
192     * Resets the query to the state just after it has been instantiated.
193     */
194    public function reset()
195    {
196        $this->_pendingJoinConditions = array();
197        $this->_pendingSubqueries = array();
198        $this->_pendingFields = array();
199        $this->_neededTables = array();
200        $this->_expressionMap = array();
201        $this->_subqueryAliases = array();
202        $this->_needsSubquery = false;
203        $this->_isLimitSubqueryUsed = false;
204    }
205
206    /**
207     * createSubquery
208     * creates a subquery
209     *
210     * @return Doctrine_Hydrate
211     */
212    public function createSubquery()
213    {
214        $class = get_class($this);
215        $obj   = new $class();
216
217        // copy the aliases to the subquery
218        $obj->copyAliases($this);
219
220        // this prevents the 'id' being selected, re ticket #307
221        $obj->isSubquery(true);
222
223        return $obj;
224    }
225
226    /**
227     * _addPendingJoinCondition
228     *
229     * @param string $componentAlias    component alias
230     * @param string $joinCondition     dql join condition
231     * @return Doctrine_Query           this object
232     */
233    protected function _addPendingJoinCondition($componentAlias, $joinCondition)
234    {
235        $this->_pendingJoins[$componentAlias] = $joinCondition;
236    }
237
238    /**
239     * addEnumParam
240     * sets input parameter as an enumerated parameter
241     *
242     * @param string $key   the key of the input parameter
243     * @return Doctrine_Query
244     */
245    public function addEnumParam($key, $table = null, $column = null)
246    {
247        $array = (isset($table) || isset($column)) ? array($table, $column) : array();
248
249        if ($key === '?') {
250            $this->_enumParams[] = $array;
251        } else {
252            $this->_enumParams[$key] = $array;
253        }
254    }
255
256    /**
257     * getEnumParams
258     * get all enumerated parameters
259     *
260     * @return array    all enumerated parameters
261     */
262    public function getEnumParams()
263    {
264        return $this->_enumParams;
265    }
266
267    /**
268     * getDql
269     * returns the DQL query that is represented by this query object.
270     *
271     * the query is built from $_dqlParts
272     *
273     * @return string   the DQL query
274     */
275    public function getDql()
276    {
277        $q = '';
278        $q .= ( ! empty($this->_dqlParts['select']))?  'SELECT '    . implode(', ', $this->_dqlParts['select']) : '';
279        $q .= ( ! empty($this->_dqlParts['from']))?    ' FROM '     . implode(' ', $this->_dqlParts['from']) : '';
280        $q .= ( ! empty($this->_dqlParts['where']))?   ' WHERE '    . implode(' AND ', $this->_dqlParts['where']) : '';
281        $q .= ( ! empty($this->_dqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_dqlParts['groupby']) : '';
282        $q .= ( ! empty($this->_dqlParts['having']))?  ' HAVING '   . implode(' AND ', $this->_dqlParts['having']) : '';
283        $q .= ( ! empty($this->_dqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_dqlParts['orderby']) : '';
284        $q .= ( ! empty($this->_dqlParts['limit']))?   ' LIMIT '    . implode(' ', $this->_dqlParts['limit']) : '';
285        $q .= ( ! empty($this->_dqlParts['offset']))?  ' OFFSET '   . implode(' ', $this->_dqlParts['offset']) : '';
286
287        return $q;
288    }
289
290
291    /**
292     * fetchArray
293     * Convenience method to execute using array fetching as hydration mode.
294     *
295     * @param string $params
296     * @return array
297     */
298    public function fetchArray($params = array()) {
299        return $this->execute($params, Doctrine::HYDRATE_ARRAY);
300    }
301
302    /**
303     * fetchOne
304     * Convenience method to execute the query and return the first item
305     * of the collection.
306     *
307     * @param string $params Parameters
308     * @param int $hydrationMode Hydration mode
309     * @return mixed Array or Doctrine_Collection or false if no result.
310     */
311    public function fetchOne($params = array(), $hydrationMode = null)
312    {
313        $collection = $this->execute($params, $hydrationMode);
314
315        if (count($collection) === 0) {
316            return false;
317        }
318
319        if ($collection instanceof Doctrine_Collection) {
320            return $collection->getFirst();
321        } else if (is_array($collection)) {
322            return array_shift($collection);
323        }
324
325        return false;
326    }
327
328    /**
329     * isSubquery
330     * if $bool parameter is set this method sets the value of
331     * Doctrine_Query::$isSubquery. If this value is set to true
332     * the query object will not load the primary key fields of the selected
333     * components.
334     *
335     * If null is given as the first parameter this method retrieves the current
336     * value of Doctrine_Query::$isSubquery.
337     *
338     * @param boolean $bool     whether or not this query acts as a subquery
339     * @return Doctrine_Query|bool
340     */
341    public function isSubquery($bool = null)
342    {
343        if ($bool === null) {
344            return $this->_isSubquery;
345        }
346
347        $this->_isSubquery = (bool) $bool;
348        return $this;
349    }
350
351    /**
352     * getAggregateAlias
353     *
354     * @param string $dqlAlias      the dql alias of an aggregate value
355     * @return string
356     * @deprecated
357     */
358    public function getAggregateAlias($dqlAlias)
359    {
360        return $this->getSqlAggregateAlias($dqlAlias);
361    }
362
363    /**
364     * getSqlAggregateAlias
365     *
366     * @param string $dqlAlias      the dql alias of an aggregate value
367     * @return string
368     */
369    public function getSqlAggregateAlias($dqlAlias)
370    {
371        if (isset($this->_aggregateAliasMap[$dqlAlias])) {
372            // mark the expression as used
373            $this->_expressionMap[$dqlAlias][1] = true;
374
375            return $this->_aggregateAliasMap[$dqlAlias];
376        } else if ( ! empty($this->_pendingAggregates)) {
377            $this->processPendingAggregates();
378
379            return $this->getSqlAggregateAlias($dqlAlias);
380        } else {
381            throw new Doctrine_Query_Exception('Unknown aggregate alias: ' . $dqlAlias);
382        }
383    }
384
385    /**
386     * parseQueryPart
387     * parses given DQL query part
388     *
389     * @param string $queryPartName     the name of the query part
390     * @param string $queryPart         query part to be parsed
391     * @param boolean $append           whether or not to append the query part to its stack
392     *                                  if false is given, this method will overwrite
393     *                                  the given query part stack with $queryPart
394     * @return Doctrine_Query           this object
395     */
396    /*protected function parseQueryPart($queryPartName, $queryPart, $append = false)
397    {
398        if ($this->_state === self::STATE_LOCKED) {
399            throw new Doctrine_Query_Exception('This query object is locked. No query parts can be manipulated.');
400        }
401
402        // sanity check
403        if ($queryPart === '' || $queryPart === null) {
404            throw new Doctrine_Query_Exception('Empty ' . $queryPartName . ' part given.');
405        }
406
407        // add query part to the dql part array
408        if ($append) {
409            $this->_dqlParts[$queryPartName][] = $queryPart;
410        } else {
411            $this->_dqlParts[$queryPartName] = array($queryPart);
412        }
413
414        if ($this->_state === self::STATE_DIRECT) {
415            $parser = $this->_getParser($queryPartName);
416
417            $sql = $parser->parse($queryPart);
418
419            if (isset($sql)) {
420                if ($append) {
421                    $this->addSqlQueryPart($queryPartName, $sql);
422                } else {
423                    $this->setSqlQueryPart($queryPartName, $sql);
424                }
425            }
426        }
427
428        $this->_state = Doctrine_Query::STATE_DIRTY;
429
430        return $this;
431    }*/
432
433    /**
434     * getDqlPart
435     * returns a specific DQL query part.
436     *
437     * @param string $queryPart     the name of the query part
438     * @return string   the DQL query part
439     * @todo Description: List which query parts exist or point to the method/property
440     *       where they are listed.
441     */
442    public function getDqlPart($queryPart)
443    {
444        if ( ! isset($this->_dqlParts[$queryPart])) {
445           throw new Doctrine_Query_Exception('Unknown query part ' . $queryPart);
446        }
447
448        return $this->_dqlParts[$queryPart];
449    }
450
451    /**
452     * contains
453     *
454     * Method to check if a arbitrary piece of dql exists
455     *
456     * @param string $dql Arbitrary piece of dql to check for
457     * @return boolean
458     */
459    public function contains($dql)
460    {
461      return stripos($this->getDql(), $dql) === false ? false : true;
462    }
463
464    /**
465     * processPendingFields
466     * the fields in SELECT clause cannot be parsed until the components
467     * in FROM clause are parsed, hence this method is called everytime a
468     * specific component is being parsed.
469     *
470     * @throws Doctrine_Query_Exception     if unknown component alias has been given
471     * @param string $componentAlias        the alias of the component
472     * @return void
473     * @todo Description: What is a 'pending field' (and are there non-pending fields, too)?
474     *       What is 'processed'? (Meaning: What information is gathered & stored away)
475     */
476    public function processPendingFields($componentAlias)
477    {
478        $tableAlias = $this->getTableAlias($componentAlias);
479        $table      = $this->_queryComponents[$componentAlias]['table'];
480
481        if ( ! isset($this->_pendingFields[$componentAlias])) {
482            if ($this->_hydrator->getHydrationMode() != Doctrine::HYDRATE_NONE) {
483                if ( ! $this->_isSubquery && $componentAlias == $this->getRootAlias()) {
484                    throw new Doctrine_Query_Exception("The root class of the query (alias $componentAlias) "
485                            . " must have at least one field selected.");
486                }
487            }
488            return;
489        }
490
491        // At this point we know the component is FETCHED (either it's the base class of
492        // the query (FROM xyz) or its a "fetch join").
493
494        // Check that the parent join (if there is one), is a "fetch join", too.
495        if (isset($this->_queryComponents[$componentAlias]['parent'])) {
496            $parentAlias = $this->_queryComponents[$componentAlias]['parent'];
497            if (is_string($parentAlias) && ! isset($this->_pendingFields[$parentAlias])
498                    && $this->_hydrator->getHydrationMode() != Doctrine::HYDRATE_NONE) {
499                throw new Doctrine_Query_Exception("The left side of the join between "
500                        . "the aliases '$parentAlias' and '$componentAlias' must have at least"
501                        . " the primary key field(s) selected.");
502            }
503        }
504
505        $fields = $this->_pendingFields[$componentAlias];
506
507        // check for wildcards
508        if (in_array('*', $fields)) {
509            $fields = $table->getFieldNames();
510        } else {
511            // only auto-add the primary key fields if this query object is not
512            // a subquery of another query object
513            if ( ! $this->_isSubquery || $this->_hydrator->getHydrationMode() === Doctrine::HYDRATE_NONE) {
514                $fields = array_unique(array_merge((array) $table->getIdentifier(), $fields));
515            }
516        }
517
518        $sql = array();
519        foreach ($fields as $fieldName) {
520            $columnName = $table->getColumnName($fieldName);
521            if (($owner = $table->getColumnOwner($columnName)) !== null &&
522                    $owner !== $table->getComponentName()) {
523
524                $parent = $this->_conn->getTable($owner);
525                $columnName = $parent->getColumnName($fieldName);
526                $parentAlias = $this->getTableAlias($componentAlias . '.' . $parent->getComponentName());
527                $sql[] = $this->_conn->quoteIdentifier($parentAlias . '.' . $columnName)
528                       . ' AS '
529                       . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName);
530            } else {
531                $columnName = $table->getColumnName($fieldName);
532                $sql[] = $this->_conn->quoteIdentifier($tableAlias . '.' . $columnName)
533                       . ' AS '
534                       . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName);
535            }
536        }
537
538        $this->_neededTables[] = $tableAlias;
539
540        return implode(', ', $sql);
541    }
542
543    /**
544     * parseSelectField
545     *
546     * @throws Doctrine_Query_Exception     if unknown component alias has been given
547     * @return void
548     * @todo Description: Explain what this method does. Is there a relation to parseSelect()?
549     *       (It doesnt seem to get called from there...?). In what circumstances is this method
550     *       used?
551     */
552    public function parseSelectField($field)
553    {
554        $terms = explode('.', $field);
555
556        if (isset($terms[1])) {
557            $componentAlias = $terms[0];
558            $field = $terms[1];
559        } else {
560            reset($this->_queryComponents);
561            $componentAlias = key($this->_queryComponents);
562            $fields = $terms[0];
563        }
564
565        $tableAlias = $this->getTableAlias($componentAlias);
566        $table      = $this->_queryComponents[$componentAlias]['table'];
567
568
569        // check for wildcards
570        if ($field === '*') {
571            $sql = array();
572
573            foreach ($table->getColumnNames() as $field) {
574                $sql[] = $this->parseSelectField($componentAlias . '.' . $field);
575            }
576
577            return implode(', ', $sql);
578        } else {
579            $name = $table->getColumnName($field);
580
581            $this->_neededTables[] = $tableAlias;
582
583            return $this->_conn->quoteIdentifier($tableAlias . '.' . $name)
584                   . ' AS '
585                   . $this->_conn->quoteIdentifier($tableAlias . '__' . $name);
586        }
587    }
588
589    /**
590     * getExpressionOwner
591     * returns the component alias for owner of given expression
592     *
593     * @param string $expr      expression from which to get to owner from
594     * @return string           the component alias
595     * @todo Description: What does it mean if a component is an 'owner' of an expression?
596     *       What kind of 'expression' are we talking about here?
597     */
598    public function getExpressionOwner($expr)
599    {
600        if (strtoupper(substr(trim($expr, '( '), 0, 6)) !== 'SELECT') {
601            preg_match_all("/[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*[\.[a-z0-9]+]*/i", $expr, $matches);
602
603            $match = current($matches);
604
605            if (isset($match[0])) {
606                $terms = explode('.', $match[0]);
607
608                return $terms[0];
609            }
610        }
611        return $this->getRootAlias();
612
613    }
614
615    /**
616     * parseSelect
617     * parses the query select part and
618     * adds selected fields to pendingFields array
619     *
620     * @param string $dql
621     * @todo Description: What information is extracted (and then stored)?
622     */
623    public function parseSelect($dql)
624    {
625        $refs = $this->_tokenizer->sqlExplode($dql, ',');
626
627        $pos   = strpos(trim($refs[0]), ' ');
628        $first = substr($refs[0], 0, $pos);
629
630        // check for DISTINCT keyword
631        if ($first === 'DISTINCT') {
632            $this->_sqlParts['distinct'] = true;
633
634            $refs[0] = substr($refs[0], ++$pos);
635        }
636
637        $parsedComponents = array();
638
639        foreach ($refs as $reference) {
640            $reference = trim($reference);
641
642            if (empty($reference)) {
643                continue;
644            }
645
646            $terms = $this->_tokenizer->sqlExplode($reference, ' ');
647
648            $pos   = strpos($terms[0], '(');
649
650            if (count($terms) > 1 || $pos !== false) {
651                $expression = array_shift($terms);
652                $alias = array_pop($terms);
653
654                if ( ! $alias) {
655                    $alias = substr($expression, 0, $pos);
656                }
657
658                $componentAlias = $this->getExpressionOwner($expression);
659                $expression = $this->parseClause($expression);
660
661                $tableAlias = $this->getTableAlias($componentAlias);
662
663                $index    = count($this->_aggregateAliasMap);
664
665                $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index);
666
667                $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias;
668
669                $this->_aggregateAliasMap[$alias] = $sqlAlias;
670                $this->_expressionMap[$alias][0] = $expression;
671
672                $this->_queryComponents[$componentAlias]['agg'][$index] = $alias;
673
674                $this->_neededTables[] = $tableAlias;
675            } else {
676                $e = explode('.', $terms[0]);
677
678                if (isset($e[1])) {
679                    $componentAlias = $e[0];
680                    $field = $e[1];
681                } else {
682                    reset($this->_queryComponents);
683                    $componentAlias = key($this->_queryComponents);
684                    $field = $e[0];
685                }
686
687                $this->_pendingFields[$componentAlias][] = $field;
688            }
689        }
690    }
691
692    /**
693     * parseClause
694     * parses given DQL clause
695     *
696     * this method handles five tasks:
697     *
698     * 1. Converts all DQL functions to their native SQL equivalents
699     * 2. Converts all component references to their table alias equivalents
700     * 3. Converts all field names to actual column names
701     * 4. Quotes all identifiers
702     * 5. Parses nested clauses and subqueries recursively
703     *
704     * @return string   SQL string
705     * @todo Description: What is a 'dql clause' (and what not)?
706     *       Refactor: Too long & nesting level
707     */
708    public function parseClause($clause)
709    {
710        $clause = trim($clause);
711
712        if (is_numeric($clause)) {
713           return $clause;
714        }
715
716        $terms = $this->_tokenizer->clauseExplode($clause, array(' ', '+', '-', '*', '/', '<', '>', '=', '>=', '<='));
717
718        $str = '';
719        foreach ($terms as $term) {
720            $pos = strpos($term[0], '(');
721
722            if ($pos !== false) {
723                $name = substr($term[0], 0, $pos);
724                $term[0] = $this->parseFunctionExpression($term[0]);
725            } else {
726                if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'")