Coverage for Doctrine_Query

Back to coverage report

1 <?php
2 /*
3  *  $Id: Query.php 3042 2007-10-30 19:19:53Z zYne $
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.com>.
20  */
21 Doctrine::autoload('Doctrine_Query_Abstract');
22 /**
23  * Doctrine_Query
24  *
25  * @package     Doctrine
26  * @subpackage  Query
27  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
28  * @link        www.phpdoctrine.com
29  * @since       1.0
30  * @version     $Revision: 3042 $
31  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
32  */
33 class Doctrine_Query extends Doctrine_Query_Abstract implements Countable
34 {
35     const STATE_CLEAN  = 1;
36
37     const STATE_DIRTY  = 2;
38
39     const STATE_DIRECT = 3;
40
41     const STATE_LOCKED = 4;
42
43     protected static $_keywords  = array('ALL', 
44                                          'AND', 
45                                          'ANY', 
46                                          'AS', 
47                                          'ASC', 
48                                          'AVG', 
49                                          'BETWEEN', 
50                                          'BIT_LENGTH', 
51                                          'BY', 
52                                          'CHARACTER_LENGTH', 
53                                          'CHAR_LENGTH', 
54                                          'CURRENT_DATE',
55                                          'CURRENT_TIME', 
56                                          'CURRENT_TIMESTAMP', 
57                                          'DELETE', 
58                                          'DESC', 
59                                          'DISTINCT', 
60                                          'EMPTY', 
61                                          'EXISTS', 
62                                          'FALSE', 
63                                          'FETCH', 
64                                          'FROM', 
65                                          'GROUP', 
66                                          'HAVING', 
67                                          'IN', 
68                                          'INDEXBY', 
69                                          'INNER', 
70                                          'IS', 
71                                          'JOIN',
72                                          'LEFT', 
73                                          'LIKE', 
74                                          'LOWER',
75                                          'MEMBER',
76                                          'MOD',
77                                          'NEW', 
78                                          'NOT', 
79                                          'NULL', 
80                                          'OBJECT', 
81                                          'OF', 
82                                          'OR', 
83                                          'ORDER', 
84                                          'OUTER', 
85                                          'POSITION', 
86                                          'SELECT', 
87                                          'SOME',
88                                          'TRIM', 
89                                          'TRUE', 
90                                          'UNKNOWN', 
91                                          'UPDATE', 
92                                          'WHERE');
93
94
95     protected $subqueryAliases   = array();
96
97     /**
98      * @param boolean $needsSubquery
99      */
100     protected $needsSubquery     = false;
101
102     /**
103      * @param boolean $isSubquery           whether or not this query object is a subquery of another 
104      *                                      query object
105      */
106     protected $isSubquery;
107
108     protected $isLimitSubqueryUsed = false;
109
110     /**
111      * @var array $_neededTables            an array containing the needed table aliases
112      */
113     protected $_neededTables     = array();
114
115     /**
116      * @var array $pendingSubqueries        SELECT part subqueries, these are called pending subqueries since
117      *                                      they cannot be parsed directly (some queries might be correlated)
118      */
119     protected $_pendingSubqueries = array();
120
121     /**
122      * @var array $_pendingFields           an array of pending fields (fields waiting to be parsed)
123      */
124     protected $_pendingFields     = array();
125
126     /**
127      * @var array $_parsers                 an array of parser objects, each DQL query part has its own parser
128      */
129     protected $_parsers    = array();
130
131     /**
132      * @var array $_enumParams              an array containing the keys of the parameters that should be enumerated
133      */
134     protected $_enumParams = array();
135
136     /**
137      * @var array $_dqlParts                an array containing all DQL query parts
138      */
139     protected $_dqlParts   = array(
140                             'from'      => array(),
141                             'select'    => array(),
142                             'forUpdate' => false,
143                             'set'       => array(),
144                             'join'      => array(),
145                             'where'     => array(),
146                             'groupby'   => array(),
147                             'having'    => array(),
148                             'orderby'   => array(),
149                             'limit'     => array(),
150                             'offset'    => array(),
151                             );
152
153     /**
154      * @var array $_pendingJoinConditions    an array containing pending joins
155      */
156     protected $_pendingJoinConditions = array();
157
158     protected $_expressionMap = array();
159
160     protected $_state = Doctrine_Query::STATE_CLEAN;
161
162     /**
163      * create
164      * returns a new Doctrine_Query object
165      *
166      * @param Doctrine_Connection $conn     optional connection parameter
167      * @return Doctrine_Query
168      */
169     public static function create($conn = null)
170     {
171         return new Doctrine_Query($conn);
172     }
173
174     public function reset()
175     {
176         $this->_pendingJoinConditions = array();
177         $this->_pendingSubqueries = array();
178         $this->_pendingFields = array();
179         $this->_neededTables = array();
180         $this->_expressionMap = array();
181         $this->subqueryAliases = array();
182         $this->needsSubquery = false;
183         $this->isLimitSubqueryUsed = false;
184     }
185
186     /**
187      * setOption
188      *
189      * @param string $name      option name
190      * @param string $value     option value
191      * @return Doctrine_Query   this object
192      */
193     public function setOption($name, $value)
194     {
195         if ( ! isset($this->_options[$name])) {
196             throw new Doctrine_Query_Exception('Unknown option ' . $name);
197         }
198         $this->_options[$name] = $value;
199     }
200
201     /**
202      * addPendingJoinCondition
203      *
204      * @param string $componentAlias    component alias
205      * @param string $joinCondition     dql join condition
206      * @return Doctrine_Query           this object
207      */
208     public function addPendingJoinCondition($componentAlias, $joinCondition)
209     {
210         $this->_pendingJoins[$componentAlias] = $joinCondition;
211     }
212
213     /**
214      * addEnumParam
215      * sets input parameter as an enumerated parameter
216      *
217      * @param string $key   the key of the input parameter
218      * @return Doctrine_Query
219      */
220     public function addEnumParam($key, $table = null, $column = null)
221     {
222         $array = (isset($table) || isset($column)) ? array($table, $column) : array();
223
224         if ($key === '?') {
225             $this->_enumParams[] = $array;
226         } else {
227             $this->_enumParams[$key] = $array;
228         }
229     }
230
231     /**
232      * getEnumParams
233      * get all enumerated parameters
234      *
235      * @return array    all enumerated parameters
236      */
237     public function getEnumParams()
238     {
239         return $this->_enumParams;
240     }
241
242     /**
243      * limitSubqueryUsed
244      *
245      * @return boolean
246      */
247     public function isLimitSubqueryUsed()
248     {
249         return $this->isLimitSubqueryUsed;
250     }
251
252     /**
253      * convertEnums
254      * convert enum parameters to their integer equivalents
255      *
256      * @return array    converted parameter array
257      */
258     public function convertEnums($params)
259     {
260         foreach ($this->_enumParams as $key => $values) {
261             if (isset($params[$key])) {
262                 if ( ! empty($values)) {
263                     $params[$key] = $values[0]->enumIndex($values[1], $params[$key]);
264                 }
265             }
266         }
267         return $params;
268     }
269
270     /**
271      * isSubquery
272      * if $bool parameter is set this method sets the value of
273      * Doctrine_Query::$isSubquery. If this value is set to true
274      * the query object will not load the primary key fields of the selected
275      * components.
276      *
277      * If null is given as the first parameter this method retrieves the current
278      * value of Doctrine_Query::$isSubquery.
279      *
280      * @param boolean $bool     whether or not this query acts as a subquery
281      * @return Doctrine_Query|bool
282      */
283     public function isSubquery($bool = null)
284     {
285         if ($bool === null) {
286             return $this->isSubquery;
287         }
288
289         $this->isSubquery = (bool) $bool;
290         return $this;
291     }
292
293     /**
294      * getAggregateAlias
295      *
296      * @param string $dqlAlias      the dql alias of an aggregate value
297      * @return string
298      */
299     public function getAggregateAlias($dqlAlias)
300     {
301         if (isset($this->aggregateMap[$dqlAlias])) {
302             // mark the expression as used
303             $this->_expressionMap[$dqlAlias][1] = true;
304
305             return $this->aggregateMap[$dqlAlias];
306         }
307         if ( ! empty($this->pendingAggregates)) {
308             $this->processPendingAggregates();
309
310             return $this->getAggregateAlias($dqlAlias);
311         }
312         throw new Doctrine_Query_Exception('Unknown aggregate alias ' . $dqlAlias);
313     }
314
315     /**
316      * getParser
317      * parser lazy-loader
318      *
319      * @throws Doctrine_Query_Exception     if unknown parser name given
320      * @return Doctrine_Query_Part
321      */
322     public function getParser($name)
323     {
324         if ( ! isset($this->_parsers[$name])) {
325             $class = 'Doctrine_Query_' . ucwords(strtolower($name));
326
327             Doctrine::autoload($class);
328
329             if ( ! class_exists($class)) {
330                 throw new Doctrine_Query_Exception('Unknown parser ' . $name);
331             }
332
333             $this->_parsers[$name] = new $class($this);
334         }
335
336         return $this->_parsers[$name];
337     }
338
339     /**
340      * parseQueryPart
341      * parses given DQL query part
342      *
343      * @param string $queryPartName     the name of the query part
344      * @param string $queryPart         query part to be parsed
345      * @param boolean $append           whether or not to append the query part to its stack
346      *                                  if false is given, this method will overwrite 
347      *                                  the given query part stack with $queryPart
348      * @return Doctrine_Query           this object
349      */
350     public function parseQueryPart($queryPartName, $queryPart, $append = false) 
351     {
352         if ($this->_state === self::STATE_LOCKED) {
353             throw new Doctrine_Query_Exception('This query object is locked. No query parts can be manipulated.');
354         }
355
356         // sanity check
357         if ($queryPart === '' || $queryPart === null) {
358             throw new Doctrine_Query_Exception('Empty ' . $queryPartName . ' part given.');
359         }
360
361         // add query part to the dql part array
362         if ($append) {
363             $this->_dqlParts[$queryPartName][] = $queryPart;
364         } else {
365             $this->_dqlParts[$queryPartName] = array($queryPart);
366         }
367
368         if ($this->_state === self::STATE_DIRECT) {
369             $parser = $this->getParser($queryPartName);
370
371             $sql = $parser->parse($queryPart);
372
373             if (isset($sql)) {
374                 if ($append) {
375                     $this->addQueryPart($queryPartName, $sql);
376                 } else {
377                     $this->setQueryPart($queryPartName, $sql);
378                 }
379             }
380         }
381
382         $this->_state = Doctrine_Query::STATE_DIRTY;
383
384         return $this;
385     }
386
387     /**
388      * getDqlPart
389      * returns the given DQL query part 
390      *
391      * @param string $queryPart     the name of the query part
392      * @return string   the DQL query part
393      */
394     public function getDqlPart($queryPart)
395     {
396         if ( ! isset($this->_dqlParts[$queryPart])) {
397            throw new Doctrine_Query_Exception('Unknown query part ' . $queryPart);
398         }
399
400         return $this->_dqlParts[$queryPart];
401     }
402
403     /**
404      * getDql
405      * returns the DQL query associated with this object
406      *
407      * the query is built from $_dqlParts
408      *
409      * @return string   the DQL query
410      */
411     public function getDql()
412     {
413         $q = '';
414         $q .= ( ! empty($this->_dqlParts['select']))?  'SELECT '    . implode(', ', $this->_dqlParts['select']) : '';
415         $q .= ( ! empty($this->_dqlParts['from']))?    ' FROM '     . implode(' ', $this->_dqlParts['from']) : '';
416         $q .= ( ! empty($this->_dqlParts['where']))?   ' WHERE '    . implode(' AND ', $this->_dqlParts['where']) : '';
417         $q .= ( ! empty($this->_dqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_dqlParts['groupby']) : '';
418         $q .= ( ! empty($this->_dqlParts['having']))?  ' HAVING '   . implode(' AND ', $this->_dqlParts['having']) : '';
419         $q .= ( ! empty($this->_dqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_dqlParts['orderby']) : '';
420         $q .= ( ! empty($this->_dqlParts['limit']))?   ' LIMIT '    . implode(' ', $this->_dqlParts['limit']) : '';
421         $q .= ( ! empty($this->_dqlParts['offset']))?  ' OFFSET '   . implode(' ', $this->_dqlParts['offset']) : '';
422
423         return $q;
424     }
425
426     /**
427      * processPendingFields
428      * the fields in SELECT clause cannot be parsed until the components
429      * in FROM clause are parsed, hence this method is called everytime a 
430      * specific component is being parsed.
431      *
432      * @throws Doctrine_Query_Exception     if unknown component alias has been given
433      * @param string $componentAlias        the alias of the component
434      * @return void
435      */
436     public function processPendingFields($componentAlias)
437     {
438         $tableAlias = $this->getTableAlias($componentAlias);
439         $table      = $this->_aliasMap[$componentAlias]['table'];
440
441         if (isset($this->_pendingFields[$componentAlias])) {
442             $fields = $this->_pendingFields[$componentAlias];
443
444             // check for wildcards
445             if (in_array('*', $fields)) {
446                 $fields = $table->getColumnNames();
447             } else {
448                 // only auto-add the primary key fields if this query object is not
449                 // a subquery of another query object
450                 if ( ! $this->isSubquery) {
451                     $fields = array_unique(array_merge((array) $table->getIdentifier(), $fields));
452                 }
453             }
454             $sql = array();
455             foreach ($fields as $name) {
456                 $name = $table->getColumnName($name);
457
458                 $sql[] = $this->_conn->quoteIdentifier($tableAlias . '.' . $name)
459                        . ' AS '
460                        . $this->_conn->quoteIdentifier($tableAlias . '__' . $name);
461             }
462
463             $this->_neededTables[] = $tableAlias;
464
465             return implode(', ', $sql);
466         }
467     }
468
469     /**
470      * parseSelectField
471      *
472      * @throws Doctrine_Query_Exception     if unknown component alias has been given
473      * @return void
474      */
475     public function parseSelectField($field)
476     {
477         $terms = explode('.', $field);
478
479         if (isset($terms[1])) {
480             $componentAlias = $terms[0];
481             $field = $terms[1];
482         } else {
483             reset($this->_aliasMap);
484             $componentAlias = key($this->_aliasMap);
485             $fields = $terms[0];
486         }
487
488         $tableAlias = $this->getTableAlias($componentAlias);
489         $table      = $this->_aliasMap[$componentAlias]['table'];
490
491
492         // check for wildcards
493         if ($field === '*') {
494             $sql = array();
495
496             foreach ($table->getColumnNames() as $field) {
497                 $sql[] = $this->parseSelectField($componentAlias . '.' . $field);
498             }
499
500             return implode(', ', $sql);
501         } else {
502
503         }
504
505         $name = $table->getColumnName($field);
506
507         $this->_neededTables[] = $tableAlias;
508
509         return $this->_conn->quoteIdentifier($tableAlias . '.' . $name)
510                . ' AS '
511                . $this->_conn->quoteIdentifier($tableAlias . '__' . $name);
512     }
513
514     /**
515      * getExpressionOwner
516      * returns the component alias for owner of given expression
517      *
518      * @param string $expr      expression from which to get to owner from
519      * @return string           the component alias
520      */
521     public function getExpressionOwner($expr)
522     {
523         if (strtoupper(substr(trim($expr, '( '), 0, 6)) !== 'SELECT') {
524             preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $expr, $matches);
525
526             $match = current($matches);
527
528             if (isset($match[0])) {
529                 $terms = explode('.', $match[0]);
530
531                 return $terms[0];
532             }
533         }
534         return $this->getRootAlias();
535
536     }
537
538     /**
539      * parseSelect
540      * parses the query select part and
541      * adds selected fields to pendingFields array
542      *
543      * @param string $dql
544      */
545     public function parseSelect($dql)
546     {
547         $refs = Doctrine_Tokenizer::sqlExplode($dql, ',');
548
549         $pos   = strpos(trim($refs[0]), ' ');
550         $first = substr($refs[0], 0, $pos);
551
552         // check for DISTINCT keyword
553         if ($first === 'DISTINCT') {
554             $this->parts['distinct'] = true;
555
556             $refs[0] = substr($refs[0], ++$pos);
557         }
558
559         $parsedComponents = array();
560
561         foreach ($refs as $reference) {
562             $reference = trim($reference);
563
564             if (empty($reference)) {
565                 continue;
566             }
567
568             $terms = Doctrine_Tokenizer::sqlExplode($reference, ' ');
569
570             $pos   = strpos($terms[0], '(');
571
572             if (count($terms) > 1 || $pos !== false) {
573                 $expression = array_shift($terms);
574                 $alias = array_pop($terms);
575
576                 if ( ! $alias) {
577                     $alias = substr($expression, 0, $pos);
578                 }
579
580                 $componentAlias = $this->getExpressionOwner($expression);
581                 $expression = $this->parseClause($expression);
582
583                 $tableAlias = $this->getTableAlias($componentAlias);
584
585                 $index    = count($this->aggregateMap);
586
587                 $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index);
588
589                 $this->parts['select'][] = $expression . ' AS ' . $sqlAlias;
590
591                 $this->aggregateMap[$alias] = $sqlAlias;
592                 $this->_expressionMap[$alias][0] = $expression;
593
594                 $this->_aliasMap[$componentAlias]['agg'][$index] = $alias;
595
596                 $this->_neededTables[] = $tableAlias;
597             } else {
598                 $e = explode('.', $terms[0]);
599
600                 if (isset($e[1])) {
601                     $componentAlias = $e[0];
602                     $field = $e[1];
603                 } else {
604                     reset($this->_aliasMap);
605                     $componentAlias = key($this->_aliasMap);
606                     $field = $e[0];
607                 }
608
609                 $this->_pendingFields[$componentAlias][] = $field;
610             }
611         }
612     }
613
614     /**
615      * parseClause
616      * parses given DQL clause
617      *
618      * this method handles five tasks:
619      *
620      * 1. Converts all DQL functions to their native SQL equivalents
621      * 2. Converts all component references to their table alias equivalents
622      * 3. Converts all column aliases to actual column names
623      * 4. Quotes all identifiers
624      * 5. Parses nested clauses and subqueries recursively
625      *
626      * @return string   SQL string
627      */
628     public function parseClause($clause) 
629     {
630      $clause = trim($clause);
631
632      if (is_numeric($clause)) {
633         return $clause;
634      }
635
636         $terms = Doctrine_Tokenizer::clauseExplode($clause, array(' ', '+', '-', '*', '/'));
637
638         $str = '';
639         foreach ($terms as $term) {
640             $pos = strpos($term[0], '(');
641             
642             if ($pos !== false) {
643                 $name = substr($term[0], 0, $pos);
644                 if ($name !== '') {
645                     $argStr = substr($term[0], ($pos + 1), -1);
646
647                     $args   = array();
648                     // parse args
649
650                     foreach (Doctrine_Tokenizer::sqlExplode($argStr, ',') as $expr) {
651                        $args[] = $this->parseClause($expr);
652                     }
653
654                     // convert DQL function to its RDBMS specific equivalent
655                     try {
656                         $expr = call_user_func_array(array($this->_conn->expression, $name), $args);
657                     } catch(Doctrine_Expression_Exception $e) {
658                         throw new Doctrine_Query_Exception('Unknown function ' . $expr . '.');
659                     }
660                     $term[0] = $expr;
661                 } else {
662                     $trimmed = trim(Doctrine_Tokenizer::bracketTrim($term[0]));
663
664                     // check for possible subqueries
665                     if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') {
666                         // parse subquery
667                         $trimmed = $this->createSubquery()->parseQuery($trimmed)->getQuery();
668                     } else {
669                         // parse normal clause
670                         $trimmed = $this->parseClause($trimmed);
671                     }
672
673                     $term[0] = '(' . $trimmed . ')';
674                 }
675             } else {
676                 if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'") {
677                     
678                     if (strpos($term[0], '.') !== false) {
679                         if ( ! is_numeric($term[0])) {
680                             $e = explode('.', $term[0]);
681
682                             $field = array_pop($e);
683
684                             if ($this->getType() === Doctrine_Query::SELECT) {
685                                 $componentAlias = implode('.', $e);
686     
687                                 if (empty($componentAlias)) {
688                                     $componentAlias = $this->getRootAlias();
689                                 }
690     
691                                 $this->load($componentAlias);
692
693                                 // check the existence of the component alias
694                                 if ( ! isset($this->_aliasMap[$componentAlias])) {
695                                     throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias);
696                                 }
697
698                                 $table = $this->_aliasMap[$componentAlias]['table'];
699     
700                                 // get the actual field name from alias
701                                 $field = $table->getColumnName($field);
702     
703                                 // check column existence
704                                 if ( ! $table->hasColumn($field)) {
705                                     throw new Doctrine_Query_Exception('Unknown column ' . $field);
706                                 }
707     
708                                 $tableAlias = $this->getTableAlias($componentAlias);
709
710                                 // build sql expression
711                                 $term[0] = $this->_conn->quoteIdentifier($tableAlias)
712                                          . '.'
713                                          . $this->_conn->quoteIdentifier($field);
714                             } else {
715                                 // build sql expression
716                                 $term[0] = $this->_conn->quoteIdentifier($field);
717                             }
718                         }
719                     } else {
720
721                         if ( ! empty($term[0]) &&
722                              ! in_array(strtoupper($term[0]), self::$_keywords) &&
723                              ! is_numeric($term[0])) {
724
725                             $componentAlias = $this->getRootAlias();
726
727                             $found = false;
728
729                             if ($componentAlias !== false && 
730                                 $componentAlias !== null) {
731                                 $table = $this->_aliasMap[$componentAlias]['table'];
732
733                                 // check column existence
734                                 if ($table->hasColumn($term[0])) {
735                                     $found = true;
736     
737                                     // get the actual field name from alias
738                                     $term[0] = $table->getColumnName($term[0]);
739
740                                     $tableAlias = $this->getTableAlias($componentAlias);
741
742                                     if ($this->getType() === Doctrine_Query::SELECT) {
743                                         // build sql expression
744                                         $term[0] = $this->_conn->quoteIdentifier($tableAlias)
745                                                  . '.'
746                                                  . $this->_conn->quoteIdentifier($term[0]);
747                                     } else {
748                                         // build sql expression
749                                         $term[0] = $this->_conn->quoteIdentifier($term[0]);
750                                     }
751                                 } else {
752                                     $found = false;
753                                 }
754                             }
755
756                             if ( ! $found) {
757                                 $term[0] = $this->getAggregateAlias($term[0]);
758                             }
759                         }
760                     }
761                 }
762             }
763
764             $str .= $term[0] . $term[1];
765         }
766         return $str;
767     }
768
769     /**
770      * parseAggregateFunction
771      * parses an aggregate function and returns the parsed form
772      *
773      * @see Doctrine_Expression
774      * @param string $expr                  DQL aggregate function
775      * @throws Doctrine_Query_Exception     if unknown aggregate function given
776      * @return array                        parsed form of given function
777      */
778     public function parseAggregateFunction($expr, $nestedCall = false)
779     {
780         $e    = Doctrine_Tokenizer::bracketExplode($expr, ' ');
781         $func = $e[0];
782
783         $pos  = strpos($func, '(');
784         if ($pos === false) {
785             return $expr;
786         }
787
788         // get the name of the function
789         $name   = substr($func, 0, $pos);
790         $argStr = substr($func, ($pos + 1), -1);
791
792         $args   = array();
793         // parse args
794         foreach (Doctrine_Tokenizer::bracketExplode($argStr, ',') as $expr) {
795            $args[] = $this->parseAggregateFunction($expr, true);
796         }
797
798         // convert DQL function to its RDBMS specific equivalent
799         try {
800             $expr = call_user_func_array(array($this->_conn->expression, $name), $args);
801         } catch(Doctrine_Expression_Exception $e) {
802             throw new Doctrine_Query_Exception('Unknown function ' . $func . '.');
803         }
804
805         if ( ! $nestedCall) {
806             // try to find all component references
807             preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $argStr, $m);
808
809             if (isset($e[1])) {
810                 if (strtoupper($e[1]) === 'AS') {
811                     if ( ! isset($e[2])) {
812                         throw new Doctrine_Query_Exception('Missing aggregate function alias.');
813                     }
814                     $alias = $e[2];
815                 } else {
816                     $alias = $e[1];
817                 }
818             } else {
819                 $alias = substr($expr, 0, strpos($expr, '('));
820             }
821
822             $this->pendingAggregates[] = array($expr, $m[0], $alias);
823         }
824
825         return $expr;
826     }
827
828     /**
829      * processPendingSubqueries
830      * processes pending subqueries
831      *
832      * subqueries can only be processed when the query is fully constructed
833      * since some subqueries may be correlated
834      *
835      * @return void
836      */
837     public function processPendingSubqueries()
838     {
839         foreach ($this->_pendingSubqueries as $value) {
840             list($dql, $alias) = $value;
841
842             $subquery = $this->createSubquery();
843
844             $sql = $subquery->parseQuery($dql, false)->getQuery();
845
846             reset($this->_aliasMap);
847             $componentAlias = key($this->_aliasMap);
848             $tableAlias = $this->getTableAlias($componentAlias);
849
850             $sqlAlias = $tableAlias . '__' . count($this->aggregateMap);
851
852             $this->parts['select'][] = '(' . $sql . ') AS ' . $this->_conn->quoteIdentifier($sqlAlias);
853
854             $this->aggregateMap[$alias] = $sqlAlias;
855             $this->_aliasMap[$componentAlias]['agg'][] = $alias;
856         }
857         $this->_pendingSubqueries = array();
858     }
859
860     /**
861      * processPendingAggregates
862      * processes pending aggregate values for given component alias
863      *
864      * @return void
865      */
866     public function processPendingAggregates()
867     {
868         // iterate trhough all aggregates
869         foreach ($this->pendingAggregates as $aggregate) {
870             list ($expression, $components, $alias) = $aggregate;
871
872             $tableAliases = array();
873
874             // iterate through the component references within the aggregate function
875             if ( ! empty ($components)) {
876                 foreach ($components as $component) {
877
878                     if (is_numeric($component)) {
879                         continue;
880                     }
881
882                     $e = explode('.', $component);
883
884                     $field = array_pop($e);
885                     $componentAlias = implode('.', $e);
886
887                     // check the existence of the component alias
888                     if ( ! isset($this->_aliasMap[$componentAlias])) {
889                         throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias);
890                     }
891
892                     $table = $this->_aliasMap[$componentAlias]['table'];
893
894                     $field = $table->getColumnName($field);
895
896                     // check column existence
897                     if ( ! $table->hasColumn($field)) {
898                         throw new Doctrine_Query_Exception('Unknown column ' . $field);
899                     }
900
901                     $tableAlias = $this->getTableAlias($componentAlias);
902
903                     $tableAliases[$tableAlias] = true;
904
905                     // build sql expression
906
907                     $identifier = $this->_conn->quoteIdentifier($tableAlias . '.' . $field);
908                     $expression = str_replace($component, $identifier, $expression);
909                 }
910             }
911
912             if (count($tableAliases) !== 1) {
913                 $componentAlias = reset($this->tableAliases);
914                 $tableAlias = key($this->tableAliases);
915             }
916
917             $index    = count($this->aggregateMap);
918             $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index);
919
920             $this->parts['select'][] = $expression . ' AS ' . $sqlAlias;
921
922             $this->aggregateMap[$alias] = $sqlAlias;
923             $this->_expressionMap[$alias][0] = $expression;
924
925             $this->_aliasMap[$componentAlias]['agg'][$index] = $alias;
926
927             $this->_neededTables[] = $tableAlias;
928         }
929         // reset the state
930         $this->pendingAggregates = array();
931     }
932
933     /**
934      * getQueryBase
935      * returns the base of the generated sql query
936      * On mysql driver special strategy has to be used for DELETE statements
937      *
938      * @return string       the base of the generated sql query
939      */
940     public function getQueryBase()
941     {
942         switch ($this->type) {
943             case self::DELETE:
944                 $q = 'DELETE FROM ';
945             break;
946             case self::UPDATE:
947                 $q = 'UPDATE ';
948             break;
949             case self::SELECT:
950                 $distinct = ($this->parts['distinct']) ? 'DISTINCT ' : '';
951
952                 $q = 'SELECT ' . $distinct . implode(', ', $this->parts['select']) . ' FROM ';
953             break;
954         }
955         return $q;
956     }
957
958     /**
959      * buildFromPart
960      * builds the from part of the query and returns it
961      *
962      * @return string   the query sql from part
963      */
964     public function buildFromPart()
965     {
966         $q = '';
967         foreach ($this->parts['from'] as $k => $part) {
968             if ($k === 0) {
969                 $q .= $part;
970                 continue;
971             }
972             // preserve LEFT JOINs only if needed
973
974             if (substr($part, 0, 9) === 'LEFT JOIN') {
975                 $e = explode(' ', $part);
976
977                 $aliases = array_merge($this->subqueryAliases,
978                             array_keys($this->_neededTables));
979
980                 if ( ! in_array($e[3], $aliases) &&
981                     ! in_array($e[2], $aliases) &&
982
983                     ! empty($this->_pendingFields)) {
984                     continue;
985                 }
986
987             }
988
989             if (isset($this->_pendingJoinConditions[$k])) {
990                 $parser = new Doctrine_Query_JoinCondition($this);
991
992                 if (strpos($part, ' ON ') !== false) {
993                     $part .= ' AND ';
994                 } else {
995                     $part .= ' ON ';
996                 }
997                 $part .= $parser->parse($this->_pendingJoinConditions[$k]);
998
999                 unset($this->_pendingJoinConditions[$k]);
1000             }
1001
1002             $q .= ' ' . $part;
1003
1004             $this->parts['from'][$k] = $part;
1005         }
1006         return $q;
1007     }
1008
1009     /**
1010      * preQuery
1011      *
1012      * Empty template method to provide Query subclasses with the possibility
1013      * to hook into the query building procedure, doing any custom / specialized
1014      * query building procedures that are neccessary.
1015      *
1016      * @return void
1017      */
1018     public function preQuery()
1019     {
1020
1021     }
1022
1023     /**
1024      * postQuery
1025      *
1026      * Empty template method to provide Query subclasses with the possibility
1027      * to hook into the query building procedure, doing any custom / specialized
1028      * post query procedures (for example logging) that are neccessary.
1029      *
1030      * @return void
1031      */
1032     public function postQuery()
1033     {
1034
1035     }
1036
1037     /**
1038      * processQueryPart
1039      * parses given query part
1040      *
1041      * @param string $queryPartName     the name of the query part
1042      * @param array $queryParts         an array containing the query part data
1043      * @return Doctrine_Query           this object
1044      */
1045     public function processQueryPart($queryPartName, $queryParts)
1046     {
1047         $this->removeQueryPart($queryPartName);
1048
1049         if (is_array($queryParts) && ! empty($queryParts)) {
1050
1051             foreach ($queryParts as $queryPart) {
1052                 $parser = $this->getParser($queryPartName);
1053
1054                 $sql = $parser->parse($queryPart);
1055
1056                 if (isset($sql)) {
1057                     if ($queryPartName == 'limit' ||
1058                         $queryPartName == 'offset') {
1059
1060                         $this->setQueryPart($queryPartName, $sql);
1061                     } else {
1062                         $this->addQueryPart($queryPartName, $sql);
1063                     }
1064                 }
1065             }
1066         }
1067     }
1068
1069     /**
1070      * builds the sql query from the given parameters and applies things such as
1071      * column aggregation inheritance and limit subqueries if needed
1072      *
1073      * @param array $params             an array of prepared statement params (needed only in mysql driver
1074      *                                  when limit subquery algorithm is used)
1075      * @return string                   the built sql query
1076      */
1077     public function getQuery($params = array())
1078     {
1079         if ($this->_state !== self::STATE_DIRTY) {
1080            return $this->_sql;
1081         }
1082
1083         $parts = $this->_dqlParts;
1084
1085         // reset the state
1086         if ( ! $this->isSubquery()) {
1087             $this->_aliasMap = array();
1088             $this->pendingAggregates = array();
1089             $this->aggregateMap = array();
1090         }
1091         $this->reset();   
1092
1093         // parse the DQL parts
1094         foreach ($this->_dqlParts as $queryPartName => $queryParts) {
1095             $this->processQueryPart($queryPartName, $queryParts);
1096         }
1097         $params = $this->convertEnums($params);
1098
1099         $this->_state = self::STATE_DIRECT;
1100
1101         // invoke the preQuery hook
1102         $this->preQuery();        
1103         $this->_state = self::STATE_CLEAN;
1104
1105         $this->_dqlParts = $parts;
1106
1107         if (empty($this->parts['from'])) {
1108             return false;
1109         }
1110
1111         $needsSubQuery = false;
1112         $subquery = '';
1113         $map   = reset($this->_aliasMap);
1114         $table = $map['table'];
1115         $rootAlias = key($this->_aliasMap);
1116
1117         if ( ! empty($this->parts['limit']) && $this->needsSubquery && $table->getAttribute(Doctrine::ATTR_QUERY_LIMIT) == Doctrine::LIMIT_RECORDS) {
1118             $this->isLimitSubqueryUsed = true;
1119             $needsSubQuery = true;
1120         }
1121
1122         $sql = array();
1123         foreach ($this->_aliasMap as $alias => $map) {
1124             $fieldSql = $this->processPendingFields($alias);
1125             if ( ! empty($fieldSql)) {
1126                 $sql[] = $fieldSql;
1127             }
1128         }
1129         if ( ! empty($sql)) {
1130             array_unshift($this->parts['select'], implode(', ', $sql));
1131         }
1132
1133         $this->_pendingFields = array();
1134
1135         // build the basic query
1136         $q  = $this->getQueryBase();
1137         $q .= $this->buildFromPart();
1138
1139         if ( ! empty($this->parts['set'])) {
1140             $q .= ' SET ' . implode(', ', $this->parts['set']);
1141         }
1142
1143
1144         $string = $this->applyInheritance();
1145
1146         // apply inheritance to WHERE part
1147         if ( ! empty($string)) {
1148             if (substr($string, 0, 1) === '(' && substr($string, -1) === ')') {
1149                 $this->parts['where'][] = $string;
1150             } else {
1151                 $this->parts['where'][] = '(' . $string . ')';
1152             }
1153         }
1154
1155
1156         $modifyLimit = true;
1157         if ( ! empty($this->parts['limit']) || ! empty($this->parts['offset'])) {
1158
1159             if ($needsSubQuery) {
1160                 $subquery = $this->getLimitSubquery();
1161
1162
1163                 switch (strtolower($this->_conn->getName())) {
1164                     case 'mysql':
1165                         // mysql doesn't support LIMIT in subqueries
1166                         $list     = $this->_conn->execute($subquery, $params)->fetchAll(Doctrine::FETCH_COLUMN);
1167                         $subquery = implode(', ', array_map(array($this->_conn, 'quote'), $list));
1168                         break;
1169                     case 'pgsql':
1170                         // pgsql needs special nested LIMIT subquery
1171                         $subquery = 'SELECT doctrine_subquery_alias.' . $table->getIdentifier(). ' FROM (' . $subquery . ') AS doctrine_subquery_alias';
1172                         break;
1173                 }
1174
1175                 $field = $this->getTableAlias($rootAlias) . '.' . $table->getIdentifier();
1176
1177                 // only append the subquery if it actually contains something
1178                 if ($subquery !== '') {
1179                     array_unshift($this->parts['where'], $this->_conn->quoteIdentifier($field) . ' IN (' . $subquery . ')');
1180                 }
1181
1182                 $modifyLimit = false;
1183             }
1184         }
1185
1186         $q .= ( ! empty($this->parts['where']))?   ' WHERE '    . implode(' AND ', $this->parts['where']) : '';
1187         $q .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby'])  : '';
1188         $q .= ( ! empty($this->parts['having']))?  ' HAVING '   . implode(' AND ', $this->parts['having']): '';
1189         $q .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby'])  : '';
1190
1191         if ($modifyLimit) {
1192
1193             $q = $this->_conn->modifyLimitQuery($q, $this->parts['limit'], $this->parts['offset']);
1194         }
1195
1196         // return to the previous state
1197         if ( ! empty($string)) {
1198             array_pop($this->parts['where']);
1199         }
1200         if ($needsSubQuery) {
1201             array_shift($this->parts['where']);
1202         }
1203         $this->_sql = $q;
1204
1205         return $q;
1206     }
1207
1208     /**
1209      * getLimitSubquery
1210      * this is method is used by the record limit algorithm
1211      *
1212      * when fetching one-to-many, many-to-many associated data with LIMIT clause
1213      * an additional subquery is needed for limiting the number of returned records instead
1214      * of limiting the number of sql result set rows
1215      *
1216      * @return string       the limit subquery
1217      */
1218     public function getLimitSubquery()
1219     {
1220         $map    = reset($this->_aliasMap);
1221         $table  = $map['table'];
1222         $componentAlias = key($this->_aliasMap);
1223
1224         // get short alias
1225         $alias      = $this->getTableAlias($componentAlias);
1226         $primaryKey = $alias . '.' . $table->getIdentifier();
1227
1228         // initialize the base of the subquery
1229         $subquery   = 'SELECT DISTINCT ' . $this->_conn->quoteIdentifier($primaryKey);
1230
1231         $driverName = $this->_conn->getAttribute(Doctrine::ATTR_DRIVER_NAME);
1232
1233
1234         // pgsql needs the order by fields to be preserved in select clause
1235         if ($driverName == 'pgsql') {
1236             foreach ($this->parts['orderby'] as $part) {
1237                 $part = trim($part);
1238                 $e = Doctrine_Tokenizer::bracketExplode($part, ' ');
1239                 $part = trim($e[0]);
1240
1241                 if (strpos($part, '.') === false) {
1242                     continue;
1243                 }
1244
1245                 // don't add functions
1246                 if (strpos($part, '(') !== false) {
1247                     continue;
1248                 }
1249
1250                 // don't add primarykey column (its already in the select clause)
1251                 if ($part !== $primaryKey) {
1252                     $subquery .= ', ' . $part;
1253                 }
1254             }
1255         }
1256
1257         if ($driverName == 'mysql' || $driverName == 'pgsql') {
1258             foreach ($this->_expressionMap as $dqlAlias => $expr) {
1259                 if (isset($expr[1])) {
1260                     $subquery .= ', ' . $expr[0] . ' AS ' . $this->aggregateMap[$dqlAlias];
1261                 }
1262             }
1263         }
1264
1265
1266         $subquery .= ' FROM';
1267
1268
1269         foreach ($this->parts['from'] as $part) {
1270             // preserve LEFT JOINs only if needed
1271             if (substr($part, 0, 9) === 'LEFT JOIN') {
1272                 $e = explode(' ', $part);
1273
1274                 if (empty($this->parts['orderby']) && empty($this->parts['where'])) {
1275                     continue;
1276                 }
1277             }
1278
1279             $subquery .= ' ' . $part;
1280         }
1281
1282         // all conditions must be preserved in subquery
1283         $subquery .= ( ! empty($this->parts['where']))?   ' WHERE '    . implode(' AND ', $this->parts['where'])  : '';
1284         $subquery .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby'])   : '';
1285         $subquery .= ( ! empty($this->parts['having']))?  ' HAVING '   . implode(' AND ', $this->parts['having']) : '';
1286
1287         $subquery .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby'])   : '';
1288
1289         // add driver specific limit clause
1290         $subquery = $this->_conn->modifyLimitQuery($subquery, $this->parts['limit'], $this->parts['offset']);
1291
1292         $parts = Doctrine_Tokenizer::quoteExplode($subquery, ' ', "'", "'");
1293
1294         foreach ($parts as $k => $part) {
1295             if (strpos($part, ' ') !== false) {
1296                 continue;
1297             }
1298
1299             $part = trim($part, "\"'`");
1300
1301             if ($this->hasTableAlias($part)) {
1302                 $parts[$k] = $this->_conn->quoteIdentifier($this->generateNewTableAlias($part));
1303                 continue;
1304             }
1305
1306             if (strpos($part, '.') === false) {
1307                 continue;
1308             }
1309             preg_match_all("/[a-zA-Z0-9_]+\.[a-z0-9_]+/i", $part, $m);
1310
1311             foreach ($m[0] as $match) {
1312                 $e = explode('.', $match);
1313                 $e[0] = $this->generateNewTableAlias($e[0]);
1314
1315                 $parts[$k] = str_replace($match, implode('.', $e), $parts[$k]);
1316             }
1317         }
1318
1319         if ($driverName == 'mysql' || $driverName == 'pgsql') {
1320             foreach ($parts as $k => $part) {
1321                 if (strpos($part, "'") !== false) {
1322                     continue;
1323                 }
1324                 if (strpos($part, '__') == false) {
1325                     continue;
1326                 }
1327
1328                 preg_match_all("/[a-zA-Z0-9_]+\_\_[a-z0-9_]+/i", $part, $m);
1329
1330                 foreach ($m[0] as $match) {
1331                     $e = explode('__', $match);
1332                     $e[0] = $this->generateNewTableAlias($e[0]);
1333
1334                     $parts[$k] = str_replace($match, implode('__', $e), $parts[$k]);
1335                 }
1336             }
1337         }
1338
1339         $subquery = implode(' ', $parts);
1340         return $subquery;
1341     }
1342
1343     /**
1344      * tokenizeQuery
1345      * splits the given dql query into an array where keys
1346      * represent different query part names and values are
1347      * arrays splitted using sqlExplode method
1348      *
1349      * example:
1350      *
1351      * parameter:
1352      *      $query = "SELECT u.* FROM User u WHERE u.name LIKE ?"
1353      * returns:
1354      *      array('select' => array('u.*'),
1355      *            'from'   => array('User', 'u'),
1356      *            'where'  => array('u.name', 'LIKE', '?'))
1357      *
1358      * @param string $query                 DQL query
1359      * @throws Doctrine_Query_Exception     if some generic parsing error occurs
1360      * @return array                        an array containing the query string parts
1361      */
1362     public function tokenizeQuery($query)
1363     {
1364         $e = Doctrine_Tokenizer::sqlExplode($query, ' ');
1365
1366         foreach ($e as $k=>$part) {
1367             $part = trim($part);
1368             switch (strtolower($part)) {
1369                 case 'delete':
1370                 case 'update':
1371                 case 'select':
1372                 case 'set':
1373                 case 'from':
1374                 case 'where':
1375                 case 'limit':
1376                 case 'offset':
1377                 case 'having':
1378                     $p = $part;
1379                     $parts[$part] = array();
1380                 break;
1381                 case 'order':
1382                 case 'group':
1383                     $i = ($k + 1);
1384                     if (isset($e[$i]) && strtolower($e[$i]) === 'by') {
1385                         $p = $part;
1386                         $parts[$part] = array();
1387                     } else {
1388                         $parts[$p][] = $part;
1389                     }
1390                 break;
1391                 case 'by':
1392                     continue;
1393                 default:
1394                     if ( ! isset($p)) {
1395                         throw new Doctrine_Query_Exception("Couldn't parse query.");
1396                     }
1397
1398                     $parts[$p][] = $part;
1399             }
1400         }
1401         return $parts;
1402     }
1403
1404     /**
1405      * DQL PARSER
1406      * parses a DQL query
1407      * first splits the query in parts and then uses individual
1408      * parsers for each part
1409      *
1410      * @param string $query                 DQL query
1411      * @param boolean $clear                whether or not to clear the aliases
1412      * @throws Doctrine_Query_Exception     if some generic parsing error occurs
1413      * @return Doctrine_Query
1414      */
1415     public function parseQuery($query, $clear = true)
1416     {
1417         if ($clear) {
1418             $this->clear();
1419         }
1420
1421         $query = trim($query);
1422         $query = str_replace("\n", ' ', $query);
1423         $query = str_replace("\r", ' ', $query);
1424
1425         $parts = $this->tokenizeQuery($query);
1426
1427         foreach($parts as $k => $part) {
1428             $part = implode(' ', $part);
1429             $k = strtolower($k);
1430             switch ($k) {
1431                 case 'create':
1432                     $this->type = self::CREATE;
1433                 break;
1434                 case 'insert':
1435                     $this->type = self::INSERT;
1436                 break;
1437                 case 'delete':
1438                     $this->type = self::DELETE;
1439                 break;
1440                 case 'select':
1441                     $this->type = self::SELECT;
1442                     $this->parseQueryPart($k, $part);
1443                 break;
1444                 case 'update':
1445                     $this->type = self::UPDATE;
1446                     $k = 'from';
1447                 case 'from':
1448                     $this->parseQueryPart($k, $part);
1449                 break;
1450                 case 'set':
1451                     $this->parseQueryPart($k, $part, true);
1452                 break;
1453                 case 'group':
1454                 case 'order':
1455                     $k .= 'by';
1456                 case 'where':
1457                 case 'having':
1458                 case 'limit':
1459                 case 'offset':
1460                     $this->parseQueryPart($k, $part);
1461                 break;
1462             }
1463         }
1464
1465         return $this;
1466     }
1467
1468     public function load($path, $loadFields = true)
1469     {
1470      if (isset($this->_aliasMap[$path])) {
1471         return $this->_aliasMap[$path];
1472      }
1473         $e = Doctrine_Tokenizer::quoteExplode($path, ' INDEXBY ');
1474
1475         $mapWith = null;
1476         if (count($e) > 1) {
1477             $mapWith = trim($e[1]);
1478
1479             $path = $e[0];
1480         }
1481
1482         // parse custom join conditions
1483         $e = explode(' ON ', $path);
1484
1485         $joinCondition = '';
1486
1487         if (count($e) > 1) {
1488             $joinCondition = $e[1];
1489             $overrideJoin = true;
1490             $path = $e[0];
1491         } else {
1492             $e = explode(' WITH ', $path);
1493
1494             if (count($e) > 1) {
1495                 $joinCondition = $e[1];
1496                 $path = $e[0];
1497             }
1498             $overrideJoin = false;
1499         }
1500
1501         $tmp            = explode(' ', $path);
1502         $componentAlias = $originalAlias = (count($tmp) > 1) ? end($tmp) : null;
1503
1504         $e = preg_split("/[.:]/", $tmp[0], -1);
1505
1506         $fullPath = $tmp[0];
1507         $prevPath = '';
1508         $fullLength = strlen($fullPath);
1509
1510         if (isset($this->_aliasMap[$e[0]])) {
1511             $table = $this->_aliasMap[$e[0]]['table'];
1512             $componentAlias = $e[0];
1513
1514             $prevPath = $parent = array_shift($e);
1515         }
1516
1517         foreach ($e as $key => $name) {
1518             // get length of the previous path
1519             $length = strlen($prevPath);
1520
1521             // build the current component path
1522             $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name;
1523
1524             $delimeter = substr($fullPath, $length, 1);
1525
1526             // if an alias is not given use the current path as an alias identifier
1527             if (strlen($prevPath) === $fullLength && isset($originalAlias)) {
1528                 $componentAlias = $originalAlias;
1529             } else {
1530                 $componentAlias = $prevPath;
1531             }
1532
1533             // if the current alias already exists, skip it
1534             if (isset($this->_aliasMap[$componentAlias])) {
1535                 continue;
1536             }
1537
1538             if ( ! isset($table)) {
1539                 // process the root of the path
1540
1541                 $table = $this->loadRoot($name, $componentAlias);
1542             } else {
1543                 $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN ';
1544
1545                 $relation = $table->getRelation($name);
1546                 $localTable = $table;
1547
1548                 $table    = $relation->getTable();
1549                 $this->_aliasMap[$componentAlias] = array('table'    => $table,
1550                                                           'parent'   => $parent,
1551                                                           'relation' => $relation,
1552                                                           'map'      => null);
1553                 if ( ! $relation->isOneToOne()) {
1554                    $this->needsSubquery = true;
1555                 }
1556
1557                 $localAlias   = $this->getTableAlias($parent, $table->getTableName());
1558                 $foreignAlias = $this->getTableAlias($componentAlias, $relation->getTable()->getTableName());
1559                 $localSql     = $this->_conn->quoteIdentifier($table->getTableName())
1560                               . ' '
1561                               . $this->_conn->quoteIdentifier($localAlias);
1562
1563                 $foreignSql   = $this->_conn->quoteIdentifier($relation->getTable()->getTableName())
1564                               . ' '
1565                               . $this->_conn->quoteIdentifier($foreignAlias);
1566
1567                 $map = $relation->getTable()->inheritanceMap;
1568
1569                 if ( ! $loadFields || ! empty($map) || $joinCondition) {
1570                     $this->subqueryAliases[] = $foreignAlias;
1571                 }
1572
1573                 if ($relation instanceof Doctrine_Relation_Association) {
1574                     $asf = $relation->getAssociationTable();
1575
1576                     $assocTableName = $asf->getTableName();
1577
1578                     if ( ! $loadFields || ! empty($map) || $joinCondition) {
1579                         $this->subqueryAliases[] = $assocTableName;
1580                     }
1581
1582                     $assocPath = $prevPath . '.' . $asf->getComponentName();
1583
1584                     $this->_aliasMap[$assocPath] = array('parent' => $prevPath, 'relation' => $relation, 'table' => $asf);
1585
1586                     $assocAlias = $this->getTableAlias($assocPath, $asf->getTableName());
1587
1588                     $queryPart = $join . $assocTableName . ' ' . $assocAlias;
1589
1590                     $queryPart .= ' ON ' . $localAlias
1591                                 . '.'
1592                                 . $localTable->getIdentifier()
1593                                 . ' = '
1594                                 . $assocAlias . '.' . $relation->getLocal();
1595
1596                     if ($relation->isEqual()) {
1597                         // equal nest relation needs additional condition
1598                         $queryPart .= ' OR ' . $localAlias
1599                                     . '.'
1600                                     . $table->getColumnName($table->getIdentifier())
1601                                     . ' = '
1602                                     . $assocAlias . '.' . $relation->getForeign();
1603                     }
1604
1605                     $this->parts['from'][] = $queryPart;
1606
1607                     $queryPart = $join . $foreignSql;
1608
1609                     if ( ! $overrideJoin) {
1610                         $queryPart .= ' ON ';
1611
1612                         if ($relation->isEqual()) {
1613                             $queryPart .= '(';
1614                         }
1615
1616                         $queryPart .= $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getTable()->getIdentifier())
1617                                     . ' = '
1618                                     . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeign());
1619
1620                         if ($relation->isEqual()) {
1621                             $queryPart .= ' OR '
1622                                         . $this->_conn->quoteIdentifier($foreignAlias . '.' . $table->getColumnName($table->getIdentifier()))
1623                                         . ' = ' 
1624                                         . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocal())
1625                                         . ') AND ' 
1626                                         . $this->_conn->quoteIdentifier($foreignAlias . '.' . $table->getIdentifier())
1627                                         . ' != '  
1628                                         . $this->_conn->quoteIdentifier($localAlias . '.' . $table->getIdentifier());
1629                         }
1630                     }
1631                 } else {
1632
1633                     $queryPart = $join . $foreignSql;
1634
1635                     if ( ! $overrideJoin) {
1636                         $queryPart .= ' ON '
1637                                    . $this->_conn->quoteIdentifier($localAlias . '.' . $relation->getLocal())
1638                                    . ' = ' 
1639                                    . $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getForeign());
1640                     }
1641
1642                 }
1643                 $this->parts['from'][$componentAlias] = $queryPart;
1644                 if ( ! empty($joinCondition)) {
1645                     $this->_pendingJoinConditions[$componentAlias] = $joinCondition;
1646                 }
1647             }
1648             if ($loadFields) {
1649
1650                 $restoreState = false;
1651                 // load fields if necessary
1652                 if ($loadFields && empty($this->_dqlParts['select'])) {
1653                     $this->_pendingFields[$componentAlias] = array('*');
1654                 }
1655             }
1656             $parent = $prevPath;
1657         }
1658         
1659         $table = $this->_aliasMap[$componentAlias]['table'];
1660
1661         $indexBy = null;
1662
1663         if (isset($mapWith)) {
1664             $e = explode('.', $mapWith);
1665
1666             if (isset($e[1])) {
1667                 $indexBy = $e[1];
1668             }
1669         } elseif ($table->getBoundQueryPart('indexBy') !== null) {
1670             $indexBy = $table->getBoundQueryPart('indexBy');
1671         }
1672
1673         if ($indexBy !== null) {
1674             if ( ! $table->hasColumn($indexBy)) {
1675                 throw new Doctrine_Query_Exception("Couldn't use key mapping. Column " . $indexBy . " does not exist.");
1676             }
1677     
1678             $this->_aliasMap[$componentAlias]['map'] = $table->getColumnName($indexBy);
1679         }
1680         return $this->_aliasMap[$componentAlias];
1681     }
1682
1683     /**
1684      * loadRoot
1685      *
1686      * @param string $name
1687      * @param string $componentAlias
1688      */
1689     public function loadRoot($name, $componentAlias)
1690     {
1691         // get the connection for the component
1692         $this->_conn = Doctrine_Manager::getInstance()
1693                       ->getConnectionForComponent($name);
1694
1695         $table = $this->_conn->getTable($name);
1696         $tableName = $table->getTableName();
1697
1698         // get the short alias for this table
1699         $tableAlias = $this->getTableAlias($componentAlias, $tableName);
1700         // quote table name
1701         $queryPart = $this->_conn->quoteIdentifier($tableName);
1702
1703         if ($this->type === self::SELECT) {
1704             $queryPart .= ' ' . $this->_conn->quoteIdentifier($tableAlias);
1705         }
1706
1707         $this->parts['from'][] = $queryPart;
1708         $this->tableAliases[$tableAlias]  = $componentAlias;
1709         $this->_aliasMap[$componentAlias] = array('table' => $table, 'map' => null);
1710
1711         return $table;
1712     }
1713
1714     /**
1715      * count
1716      * fetches the count of the query
1717      *
1718      * This method executes the main query without all the
1719      * selected fields, ORDER BY part, LIMIT part and OFFSET part.
1720      *
1721      * Example:
1722      * Main query: 
1723      *      SELECT u.*, p.phonenumber FROM User u
1724      *          LEFT JOIN u.Phonenumber p 
1725      *          WHERE p.phonenumber = '123 123' LIMIT 10
1726      *
1727      * The modified DQL query:
1728      *      SELECT COUNT(DISTINCT u.id) FROM User u
1729      *          LEFT JOIN u.Phonenumber p
1730      *          WHERE p.phonenumber = '123 123'
1731      *
1732      * @param array $params        an array of prepared statement parameters
1733      * @return integer             the count of this query
1734      */
1735     public function count($params = array())
1736     {
1737         $this->getQuery();
1738
1739         // initialize temporary variables
1740         $where  = $this->parts['where'];
1741         $having = $this->parts['having'];
1742         $groupby = $this->parts['groupby'];
1743         $map    = reset($this->_aliasMap);
1744         $componentAlias = key($this->_aliasMap);
1745         $table = $map['table'];
1746
1747         // build the query base
1748         $q  = 'SELECT COUNT(DISTINCT ' . $this->getTableAlias($componentAlias)
1749               . '.' . implode(',', (array) $table->getIdentifier())
1750               . ') AS num_results';
1751
1752         foreach ($this->parts['select'] as $field) {
1753             if (strpos($field, '(') !== false) {
1754                 $q .= ', ' . $field;
1755             }
1756         }
1757
1758         $q .= ' FROM ' . $this->buildFromPart();
1759
1760         // append column aggregation inheritance (if needed)
1761         $string = $this->applyInheritance();
1762
1763         if ( ! empty($string)) {
1764             $where[] = $string;
1765         }
1766         // append conditions
1767         $q .= ( ! empty($where)) ?  ' WHERE '  . implode(' AND ', $where) : '';
1768         $q .= ( ! empty($groupby)) ?  ' GROUP BY '  . implode(', ', $groupby) : '';
1769         $q .= ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having): '';
1770
1771         if ( ! is_array($params)) {
1772             $params = array($params);
1773         }
1774         // append parameters
1775         $params = array_merge($this->_params['where'], $this->_params['having'], $params);
1776
1777         $results = $this->getConnection()->fetchAll($q, $params);
1778
1779         if (count($results) > 1) {
1780             $count = 0;
1781             foreach ($results as $result) {
1782                 $count += $result['num_results'];
1783             }
1784         } else {
1785             $count = isset($results[0]) ? $results[0]['num_results']:0;
1786         }
1787
1788         return (int) $count;
1789     }
1790
1791     /**
1792      * query
1793      * query the database with DQL (Doctrine Query Language)
1794      *
1795      * @param string $query      DQL query
1796      * @param array $params      prepared statement parameters
1797      * @param int $hydrationMode Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD
1798      * @see Doctrine::FETCH_* constants
1799      * @return mixed
1800      */
1801     public function query($query, $params = array(), $hydrationMode = null)
1802     {
1803         $this->parseQuery($query);
1804
1805         return $this->execute($params, $hydrationMode);
1806     }
1807
1808     /**
1809      * Copies a Doctrine_Query object.
1810      *
1811      * @param Doctrine_Query   Doctrine query instance.
1812      *                         If ommited the instance itself will be used as source.
1813      * @return Doctrine_Query  Copy of the Doctrine_Query instance.
1814      */
1815     public function copy(Doctrine_Query $query = null)
1816     {
1817         if ( ! $query) {
1818             $query = $this;
1819         }
1820
1821         $new = new Doctrine_Query();
1822         $new->_dqlParts = $query->_dqlParts;
1823         $new->_params = $query->_params;
1824         $new->_hydrationMode = $query->_hydrationMode;
1825
1826         return $new;
1827     }
1828
1829     /**
1830      * Frees the resources used by the query object. It especially breaks a 
1831      * cyclic reference between the query object and it's parsers. This enables
1832      * PHP's current GC to reclaim the memory.
1833      * This method can therefore be used to reduce memory usage when creating a lot
1834      * of query objects during a request.
1835      *
1836      * @return Doctrine_Query   this object
1837      */
1838     public function free()
1839     {
1840         $this->reset();
1841         $this->_parsers = array();
1842         $this->_dqlParts = array();
1843         $this->_enumParams = array();
1844     }
1845 }