Coverage for Doctrine_Query

Back to coverage report

1 <?php
2 /*
3  *  $Id: Query.php 3095 2007-11-08 21:02:56Z phuson $
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: 3095 $
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             
973             // preserve LEFT JOINs only if needed
974 // Check if it's JOIN, if not add a comma separator instead of space
975             if (!preg_match('/\bJOIN\b/i', $part) && !isset($this->_pendingJoinConditions[$k])) {
976                 $q .= ', ' . $part;
977             } else {
978                 if (substr($part, 0, 9) === 'LEFT JOIN') {
979                     $e = explode(' ', $part);
980
981                     $aliases = array_merge($this->subqueryAliases,
982                                 array_keys($this->_neededTables));
983
984                     if ( ! in_array($e[3], $aliases) &&
985                         ! in_array($e[2], $aliases) &&
986
987                         ! empty($this->_pendingFields)) {
988                         continue;
989                     }
990
991                 }
992
993                 if (isset($this->_pendingJoinConditions[$k])) {
994                     $parser = new Doctrine_Query_JoinCondition($this);
995
996                     if (strpos($part, ' ON ') !== false) {
997                         $part .= ' AND ';
998                     } else {
999                         $part .= ' ON ';
1000                     }
1001                     $part .= $parser->parse($this->_pendingJoinConditions[$k]);
1002
1003                     unset($this->_pendingJoinConditions[$k]);
1004                 }
1005
1006                 $q .= ' ' . $part;
1007             }
1008
1009             $this->parts['from'][$k] = $part;
1010         }
1011         return $q;
1012     }
1013
1014     /**
1015      * preQuery
1016      *
1017      * Empty template method to provide Query subclasses with the possibility
1018      * to hook into the query building procedure, doing any custom / specialized
1019      * query building procedures that are neccessary.
1020      *
1021      * @return void
1022      */
1023     public function preQuery()
1024     {
1025
1026     }
1027
1028     /**
1029      * postQuery
1030      *
1031      * Empty template method to provide Query subclasses with the possibility
1032      * to hook into the query building procedure, doing any custom / specialized
1033      * post query procedures (for example logging) that are neccessary.
1034      *
1035      * @return void
1036      */
1037     public function postQuery()
1038     {
1039
1040     }
1041
1042     /**
1043      * processQueryPart
1044      * parses given query part
1045      *
1046      * @param string $queryPartName     the name of the query part
1047      * @param array $queryParts         an array containing the query part data
1048      * @return Doctrine_Query           this object
1049      */
1050     public function processQueryPart($queryPartName, $queryParts)
1051     {
1052         $this->removeQueryPart($queryPartName);
1053
1054         if (is_array($queryParts) && ! empty($queryParts)) {
1055
1056             foreach ($queryParts as $queryPart) {
1057                 $parser = $this->getParser($queryPartName);
1058
1059                 $sql = $parser->parse($queryPart);
1060
1061                 if (isset($sql)) {
1062                     if ($queryPartName == 'limit' ||
1063                         $queryPartName == 'offset') {
1064
1065                         $this->setQueryPart($queryPartName, $sql);
1066                     } else {
1067                         $this->addQueryPart($queryPartName, $sql);
1068                     }
1069                 }
1070             }
1071         }
1072     }
1073
1074     /**
1075      * builds the sql query from the given parameters and applies things such as
1076      * column aggregation inheritance and limit subqueries if needed
1077      *
1078      * @param array $params             an array of prepared statement params (needed only in mysql driver
1079      *                                  when limit subquery algorithm is used)
1080      * @return string                   the built sql query
1081      */
1082     public function getQuery($params = array())
1083     {
1084         if ($this->_state !== self::STATE_DIRTY) {
1085            return $this->_sql;
1086         }
1087
1088         $parts = $this->_dqlParts;
1089
1090         // reset the state
1091         if ( ! $this->isSubquery()) {
1092             $this->_aliasMap = array();
1093             $this->pendingAggregates = array();
1094             $this->aggregateMap = array();
1095         }
1096         $this->reset();   
1097
1098         // parse the DQL parts
1099         foreach ($this->_dqlParts as $queryPartName => $queryParts) {
1100             $this->processQueryPart($queryPartName, $queryParts);
1101         }
1102         $params = $this->convertEnums($params);
1103
1104         $this->_state = self::STATE_DIRECT;
1105
1106         // invoke the preQuery hook
1107         $this->preQuery();        
1108         $this->_state = self::STATE_CLEAN;
1109
1110         $this->_dqlParts = $parts;
1111
1112         if (empty($this->parts['from'])) {
1113             return false;
1114         }
1115
1116         $needsSubQuery = false;
1117         $subquery = '';
1118         $map   = reset($this->_aliasMap);
1119         $table = $map['table'];
1120         $rootAlias = key($this->_aliasMap);
1121
1122         if ( ! empty($this->parts['limit']) && $this->needsSubquery && $table->getAttribute(Doctrine::ATTR_QUERY_LIMIT) == Doctrine::LIMIT_RECORDS) {
1123             $this->isLimitSubqueryUsed = true;
1124             $needsSubQuery = true;
1125         }
1126
1127         $sql = array();
1128         foreach ($this->_aliasMap as $alias => $map) {
1129             $fieldSql = $this->processPendingFields($alias);
1130             if ( ! empty($fieldSql)) {
1131                 $sql[] = $fieldSql;
1132             }
1133         }
1134         if ( ! empty($sql)) {
1135             array_unshift($this->parts['select'], implode(', ', $sql));
1136         }
1137
1138         $this->_pendingFields = array();
1139
1140         // build the basic query
1141         $q  = $this->getQueryBase();
1142         $q .= $this->buildFromPart();
1143
1144         if ( ! empty($this->parts['set'])) {
1145             $q .= ' SET ' . implode(', ', $this->parts['set']);
1146         }
1147
1148
1149         $string = $this->applyInheritance();
1150
1151         // apply inheritance to WHERE part
1152         if ( ! empty($string)) {
1153             if (substr($string, 0, 1) === '(' && substr($string, -1) === ')') {
1154                 $this->parts['where'][] = $string;
1155             } else {
1156                 $this->parts['where'][] = '(' . $string . ')';
1157             }
1158         }
1159
1160
1161         $modifyLimit = true;
1162         if ( ! empty($this->parts['limit']) || ! empty($this->parts['offset'])) {
1163
1164             if ($needsSubQuery) {
1165                 $subquery = $this->getLimitSubquery();
1166
1167
1168                 switch (strtolower($this->_conn->getName())) {
1169                     case 'mysql':
1170                         // mysql doesn't support LIMIT in subqueries
1171                         $list     = $this->_conn->execute($subquery, $params)->fetchAll(Doctrine::FETCH_COLUMN);
1172                         $subquery = implode(', ', array_map(array($this->_conn, 'quote'), $list));
1173                         break;
1174                     case 'pgsql':
1175                         // pgsql needs special nested LIMIT subquery
1176                         $subquery = 'SELECT doctrine_subquery_alias.' . $table->getIdentifier(). ' FROM (' . $subquery . ') AS doctrine_subquery_alias';
1177                         break;
1178                 }
1179
1180                 $field = $this->getTableAlias($rootAlias) . '.' . $table->getIdentifier();
1181
1182                 // only append the subquery if it actually contains something
1183                 if ($subquery !== '') {
1184                     array_unshift($this->parts['where'], $this->_conn->quoteIdentifier($field) . ' IN (' . $subquery . ')');
1185                 }
1186
1187                 $modifyLimit = false;
1188             }
1189         }
1190
1191         $q .= ( ! empty($this->parts['where']))?   ' WHERE '    . implode(' AND ', $this->parts['where']) : '';
1192         $q .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby'])  : '';
1193         $q .= ( ! empty($this->parts['having']))?  ' HAVING '   . implode(' AND ', $this->parts['having']): '';
1194         $q .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby'])  : '';
1195
1196         if ($modifyLimit) {
1197
1198             $q = $this->_conn->modifyLimitQuery($q, $this->parts['limit'], $this->parts['offset']);
1199         }
1200
1201         // return to the previous state
1202         if ( ! empty($string)) {
1203             array_pop($this->parts['where']);
1204         }
1205         if ($needsSubQuery) {
1206             array_shift($this->parts['where']);
1207         }
1208         $this->_sql = $q;
1209
1210         return $q;
1211     }
1212
1213     /**
1214      * getLimitSubquery
1215      * this is method is used by the record limit algorithm
1216      *
1217      * when fetching one-to-many, many-to-many associated data with LIMIT clause
1218      * an additional subquery is needed for limiting the number of returned records instead
1219      * of limiting the number of sql result set rows
1220      *
1221      * @return string       the limit subquery
1222      */
1223     public function getLimitSubquery()
1224     {
1225         $map    = reset($this->_aliasMap);
1226         $table  = $map['table'];
1227         $componentAlias = key($this->_aliasMap);
1228
1229         // get short alias
1230         $alias      = $this->getTableAlias($componentAlias);
1231         $primaryKey = $alias . '.' . $table->getIdentifier();
1232
1233         // initialize the base of the subquery
1234         $subquery   = 'SELECT DISTINCT ' . $this->_conn->quoteIdentifier($primaryKey);
1235
1236         $driverName = $this->_conn->getAttribute(Doctrine::ATTR_DRIVER_NAME);
1237
1238
1239         // pgsql needs the order by fields to be preserved in select clause
1240         if ($driverName == 'pgsql') {
1241             foreach ($this->parts['orderby'] as $part) {
1242                 $part = trim($part);
1243                 $e = Doctrine_Tokenizer::bracketExplode($part, ' ');
1244                 $part = trim($e[0]);
1245
1246                 if (strpos($part, '.') === false) {
1247                     continue;
1248                 }
1249
1250                 // don't add functions
1251                 if (strpos($part, '(') !== false) {
1252                     continue;
1253                 }
1254
1255                 // don't add primarykey column (its already in the select clause)
1256                 if ($part !== $primaryKey) {
1257                     $subquery .= ', ' . $part;
1258                 }
1259             }
1260         }
1261
1262         if ($driverName == 'mysql' || $driverName == 'pgsql') {
1263             foreach ($this->_expressionMap as $dqlAlias => $expr) {
1264                 if (isset($expr[1])) {
1265                     $subquery .= ', ' . $expr[0] . ' AS ' . $this->aggregateMap[$dqlAlias];
1266                 }
1267             }
1268         }
1269
1270
1271         $subquery .= ' FROM';
1272
1273
1274         foreach ($this->parts['from'] as $part) {
1275             // preserve LEFT JOINs only if needed
1276             if (substr($part, 0, 9) === 'LEFT JOIN') {
1277                 $e = explode(' ', $part);
1278
1279                 if (empty($this->parts['orderby']) && empty($this->parts['where'])) {
1280                     continue;
1281                 }
1282             }
1283
1284             $subquery .= ' ' . $part;
1285         }
1286
1287         // all conditions must be preserved in subquery
1288         $subquery .= ( ! empty($this->parts['where']))?   ' WHERE '    . implode(' AND ', $this->parts['where'])  : '';
1289         $subquery .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby'])   : '';
1290         $subquery .= ( ! empty($this->parts['having']))?  ' HAVING '   . implode(' AND ', $this->parts['having']) : '';
1291
1292         $subquery .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby'])   : '';
1293
1294         // add driver specific limit clause
1295         $subquery = $this->_conn->modifyLimitQuery($subquery, $this->parts['limit'], $this->parts['offset']);
1296
1297         $parts = Doctrine_Tokenizer::quoteExplode($subquery, ' ', "'", "'");
1298
1299         foreach ($parts as $k => $part) {
1300             if (strpos($part, ' ') !== false) {
1301                 continue;
1302             }
1303
1304             $part = trim($part, "\"'`");
1305
1306             if ($this->hasTableAlias($part)) {
1307                 $parts[$k] = $this->_conn->quoteIdentifier($this->generateNewTableAlias($part));
1308                 continue;
1309             }
1310
1311             if (strpos($part, '.') === false) {
1312                 continue;
1313             }
1314             preg_match_all("/[a-zA-Z0-9_]+\.[a-z0-9_]+/i", $part, $m);
1315
1316             foreach ($m[0] as $match) {
1317                 $e = explode('.', $match);
1318                 $e[0] = $this->generateNewTableAlias($e[0]);
1319
1320                 $parts[$k] = str_replace($match, implode('.', $e), $parts[$k]);
1321             }
1322         }
1323
1324         if ($driverName == 'mysql' || $driverName == 'pgsql') {
1325             foreach ($parts as $k => $part) {
1326                 if (strpos($part, "'") !== false) {
1327                     continue;
1328                 }
1329                 if (strpos($part, '__') == false) {
1330                     continue;
1331                 }
1332
1333                 preg_match_all("/[a-zA-Z0-9_]+\_\_[a-z0-9_]+/i", $part, $m);
1334
1335                 foreach ($m[0] as $match) {
1336                     $e = explode('__', $match);
1337                     $e[0] = $this->generateNewTableAlias($e[0]);
1338
1339                     $parts[$k] = str_replace($match, implode('__', $e), $parts[$k]);
1340                 }
1341             }
1342         }
1343
1344         $subquery = implode(' ', $parts);
1345         return $subquery;
1346     }
1347
1348     /**
1349      * tokenizeQuery
1350      * splits the given dql query into an array where keys
1351      * represent different query part names and values are
1352      * arrays splitted using sqlExplode method
1353      *
1354      * example:
1355      *
1356      * parameter:
1357      *      $query = "SELECT u.* FROM User u WHERE u.name LIKE ?"
1358      * returns:
1359      *      array('select' => array('u.*'),
1360      *            'from'   => array('User', 'u'),
1361      *            'where'  => array('u.name', 'LIKE', '?'))
1362      *
1363      * @param string $query                 DQL query
1364      * @throws Doctrine_Query_Exception     if some generic parsing error occurs
1365      * @return array                        an array containing the query string parts
1366      */
1367     public function tokenizeQuery($query)
1368     {
1369         $e = Doctrine_Tokenizer::sqlExplode($query, ' ');
1370
1371         foreach ($e as $k=>$part) {
1372             $part = trim($part);
1373             switch (strtolower($part)) {
1374                 case 'delete':
1375                 case 'update':
1376                 case 'select':
1377                 case 'set':
1378                 case 'from':
1379                 case 'where':
1380                 case 'limit':
1381                 case 'offset':
1382                 case 'having':
1383                     $p = $part;
1384                     $parts[$part] = array();
1385                 break;
1386                 case 'order':
1387                 case 'group':
1388                     $i = ($k + 1);
1389                     if (isset($e[$i]) && strtolower($e[$i]) === 'by') {
1390                         $p = $part;
1391                         $parts[$part] = array();
1392                     } else {
1393                         $parts[$p][] = $part;
1394                     }
1395                 break;
1396                 case 'by':
1397                     continue;
1398                 default:
1399                     if ( ! isset($p)) {
1400                         throw new Doctrine_Query_Exception("Couldn't parse query.");
1401                     }
1402
1403                     $parts[$p][] = $part;
1404             }
1405         }
1406         return $parts;
1407     }
1408
1409     /**
1410      * DQL PARSER
1411      * parses a DQL query
1412      * first splits the query in parts and then uses individual
1413      * parsers for each part
1414      *
1415      * @param string $query                 DQL query
1416      * @param boolean $clear                whether or not to clear the aliases
1417      * @throws Doctrine_Query_Exception     if some generic parsing error occurs
1418      * @return Doctrine_Query
1419      */
1420     public function parseQuery($query, $clear = true)
1421     {
1422         if ($clear) {
1423             $this->clear();
1424         }
1425
1426         $query = trim($query);
1427         $query = str_replace("\n", ' ', $query);
1428         $query = str_replace("\r", ' ', $query);
1429
1430         $parts = $this->tokenizeQuery($query);
1431
1432         foreach($parts as $k => $part) {
1433             $part = implode(' ', $part);
1434             $k = strtolower($k);
1435             switch ($k) {
1436                 case 'create':
1437                     $this->type = self::CREATE;
1438                 break;
1439                 case 'insert':
1440                     $this->type = self::INSERT;
1441                 break;
1442                 case 'delete':
1443                     $this->type = self::DELETE;
1444                 break;
1445                 case 'select':
1446                     $this->type = self::SELECT;
1447                     $this->parseQueryPart($k, $part);
1448                 break;
1449                 case 'update':
1450                     $this->type = self::UPDATE;
1451                     $k = 'from';
1452                 case 'from':
1453                     $this->parseQueryPart($k, $part);
1454                 break;
1455                 case 'set':
1456                     $this->parseQueryPart($k, $part, true);
1457                 break;
1458                 case 'group':
1459                 case 'order':
1460                     $k .= 'by';
1461                 case 'where':
1462                 case 'having':
1463                 case 'limit':
1464                 case 'offset':
1465                     $this->parseQueryPart($k, $part);
1466                 break;
1467             }
1468         }
1469
1470         return $this;
1471     }
1472
1473     public function load($path, $loadFields = true)
1474     {
1475      if (isset($this->_aliasMap[$path])) {
1476         return $this->_aliasMap[$path];
1477      }
1478         $e = Doctrine_Tokenizer::quoteExplode($path, ' INDEXBY ');
1479
1480         $mapWith = null;
1481         if (count($e) > 1) {
1482             $mapWith = trim($e[1]);
1483
1484             $path = $e[0];
1485         }
1486
1487         // parse custom join conditions
1488         $e = explode(' ON ', $path);
1489
1490         $joinCondition = '';
1491
1492         if (count($e) > 1) {
1493             $joinCondition = $e[1];
1494             $overrideJoin = true;
1495             $path = $e[0];
1496         } else {
1497             $e = explode(' WITH ', $path);
1498
1499             if (count($e) > 1) {
1500                 $joinCondition = $e[1];
1501                 $path = $e[0];
1502             }
1503             $overrideJoin = false;
1504         }
1505
1506         $tmp            = explode(' ', $path);
1507         $componentAlias = $originalAlias = (count($tmp) > 1) ? end($tmp) : null;
1508
1509         $e = preg_split("/[.:]/", $tmp[0], -1);
1510
1511         $fullPath = $tmp[0];
1512         $prevPath = '';
1513         $fullLength = strlen($fullPath);
1514
1515         if (isset($this->_aliasMap[$e[0]])) {
1516             $table = $this->_aliasMap[$e[0]]['table'];
1517             $componentAlias = $e[0];
1518
1519             $prevPath = $parent = array_shift($e);
1520         }
1521
1522         foreach ($e as $key => $name) {
1523             // get length of the previous path
1524             $length = strlen($prevPath);
1525
1526             // build the current component path
1527             $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name;
1528
1529             $delimeter = substr($fullPath, $length, 1);
1530
1531             // if an alias is not given use the current path as an alias identifier
1532             if (strlen($prevPath) === $fullLength && isset($originalAlias)) {
1533                 $componentAlias = $originalAlias;
1534             } else {
1535                 $componentAlias = $prevPath;
1536             }
1537
1538             // if the current alias already exists, skip it
1539             if (isset($this->_aliasMap[$componentAlias])) {
1540                 continue;
1541             }
1542
1543             if ( ! isset($table)) {
1544                 // process the root of the path
1545
1546                 $table = $this->loadRoot($name, $componentAlias);
1547             } else {
1548                 $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN ';
1549
1550                 $relation = $table->getRelation($name);
1551                 $localTable = $table;
1552
1553                 $table    = $relation->getTable();
1554                 $this->_aliasMap[$componentAlias] = array('table'    => $table,
1555                                                           'parent'   => $parent,
1556                                                           'relation' => $relation,
1557                                                           'map'      => null);
1558                 if ( ! $relation->isOneToOne()) {
1559                    $this->needsSubquery = true;
1560                 }
1561
1562                 $localAlias   = $this->getTableAlias($parent, $table->getTableName());
1563                 $foreignAlias = $this->getTableAlias($componentAlias, $relation->getTable()->getTableName());
1564                 $localSql     = $this->_conn->quoteIdentifier($table->getTableName())
1565                               . ' '
1566                               . $this->_conn->quoteIdentifier($localAlias);
1567
1568                 $foreignSql   = $this->_conn->quoteIdentifier($relation->getTable()->getTableName())
1569                               . ' '
1570                               . $this->_conn->quoteIdentifier($foreignAlias);
1571
1572                 $map = $relation->getTable()->inheritanceMap;
1573
1574                 if ( ! $loadFields || ! empty($map) || $joinCondition) {
1575                     $this->subqueryAliases[] = $foreignAlias;
1576                 }
1577
1578                 if ($relation instanceof Doctrine_Relation_Association) {
1579                     $asf = $relation->getAssociationTable();
1580
1581                     $assocTableName = $asf->getTableName();
1582
1583                     if ( ! $loadFields || ! empty($map) || $joinCondition) {
1584                         $this->subqueryAliases[] = $assocTableName;
1585                     }
1586
1587                     $assocPath = $prevPath . '.' . $asf->getComponentName();
1588
1589                     $this->_aliasMap[$assocPath] = array('parent' => $prevPath, 'relation' => $relation, 'table' => $asf);
1590
1591                     $assocAlias = $this->getTableAlias($assocPath, $asf->getTableName());
1592
1593                     $queryPart = $join . $assocTableName . ' ' . $assocAlias;
1594
1595                     $queryPart .= ' ON ' . $localAlias
1596                                 . '.'
1597                                 . $localTable->getIdentifier()
1598                                 . ' = '
1599                                 . $assocAlias . '.' . $relation->getLocal();
1600
1601                     if ($relation->isEqual()) {
1602                         // equal nest relation needs additional condition
1603                         $queryPart .= ' OR ' . $localAlias
1604                                     . '.'
1605                                     . $table->getColumnName($table->getIdentifier())
1606                                     . ' = '
1607                                     . $assocAlias . '.' . $relation->getForeign();
1608                     }
1609
1610                     $this->parts['from'][] = $queryPart;
1611
1612                     $queryPart = $join . $foreignSql;
1613
1614                     if ( ! $overrideJoin) {
1615                         $queryPart .= ' ON ';
1616
1617                         if ($relation->isEqual()) {
1618                             $queryPart .= '(';
1619                         }
1620
1621                         $queryPart .= $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getTable()->getIdentifier())
1622                                     . ' = '
1623                                     . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeign());
1624
1625                         if ($relation->isEqual()) {
1626                             $queryPart .= ' OR '
1627                                         . $this->_conn->quoteIdentifier($foreignAlias . '.' . $table->getColumnName($table->getIdentifier()))
1628                                         . ' = ' 
1629                                         . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocal())
1630                                         . ') AND ' 
1631                                         . $this->_conn->quoteIdentifier($foreignAlias . '.' . $table->getIdentifier())
1632                                         . ' != '  
1633                                         . $this->_conn->quoteIdentifier($localAlias . '.' . $table->getIdentifier());
1634                         }
1635                     }
1636                 } else {
1637
1638                     $queryPart = $join . $foreignSql;
1639
1640                     if ( ! $overrideJoin) {
1641                         $queryPart .= ' ON '
1642                                    . $this->_conn->quoteIdentifier($localAlias . '.' . $relation->getLocal())
1643                                    . ' = ' 
1644                                    . $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getForeign());
1645                     }
1646
1647                 }
1648                 $this->parts['from'][$componentAlias] = $queryPart;
1649                 if ( ! empty($joinCondition)) {
1650                     $this->_pendingJoinConditions[$componentAlias] = $joinCondition;
1651                 }
1652             }
1653             if ($loadFields) {
1654
1655                 $restoreState = false;
1656                 // load fields if necessary
1657                 if ($loadFields && empty($this->_dqlParts['select'])) {
1658                     $this->_pendingFields[$componentAlias] = array('*');
1659                 }
1660             }
1661             $parent = $prevPath;
1662         }
1663         
1664         $table = $this->_aliasMap[$componentAlias]['table'];
1665
1666         $indexBy = null;
1667
1668         if (isset($mapWith)) {
1669             $e = explode('.', $mapWith);
1670
1671             if (isset($e[1])) {
1672                 $indexBy = $e[1];
1673             }
1674         } elseif ($table->getBoundQueryPart('indexBy') !== null) {
1675             $indexBy = $table->getBoundQueryPart('indexBy');
1676         }
1677
1678         if ($indexBy !== null) {
1679             if ( ! $table->hasColumn($indexBy)) {
1680                 throw new Doctrine_Query_Exception("Couldn't use key mapping. Column " . $indexBy . " does not exist.");
1681             }
1682     
1683             $this->_aliasMap[$componentAlias]['map'] = $table->getColumnName($indexBy);
1684         }
1685         return $this->_aliasMap[$componentAlias];
1686     }
1687
1688     /**
1689      * loadRoot
1690      *
1691      * @param string $name
1692      * @param string $componentAlias
1693      */
1694     public function loadRoot($name, $componentAlias)
1695     {
1696         // get the connection for the component
1697         $this->_conn = Doctrine_Manager::getInstance()
1698                       ->getConnectionForComponent($name);
1699
1700         $table = $this->_conn->getTable($name);
1701         $tableName = $table->getTableName();
1702
1703         // get the short alias for this table
1704         $tableAlias = $this->getTableAlias($componentAlias, $tableName);
1705         // quote table name
1706         $queryPart = $this->_conn->quoteIdentifier($tableName);
1707
1708         if ($this->type === self::SELECT) {
1709             $queryPart .= ' ' . $this->_conn->quoteIdentifier($tableAlias);
1710         }
1711
1712         $this->parts['from'][] = $queryPart;
1713         $this->tableAliases[$tableAlias]  = $componentAlias;
1714         $this->_aliasMap[$componentAlias] = array('table' => $table, 'map' => null);
1715
1716         return $table;
1717     }
1718
1719     /**
1720      * count
1721      * fetches the count of the query
1722      *
1723      * This method executes the main query without all the
1724      * selected fields, ORDER BY part, LIMIT part and OFFSET part.
1725      *
1726      * Example:
1727      * Main query: 
1728      *      SELECT u.*, p.phonenumber FROM User u
1729      *          LEFT JOIN u.Phonenumber p 
1730      *          WHERE p.phonenumber = '123 123' LIMIT 10
1731      *
1732      * The modified DQL query:
1733      *      SELECT COUNT(DISTINCT u.id) FROM User u
1734      *          LEFT JOIN u.Phonenumber p
1735      *          WHERE p.phonenumber = '123 123'
1736      *
1737      * @param array $params        an array of prepared statement parameters
1738      * @return integer             the count of this query
1739      */
1740     public function count($params = array())
1741     {
1742         $this->getQuery();
1743
1744         // initialize temporary variables
1745         $where  = $this->parts['where'];
1746         $having = $this->parts['having'];
1747         $groupby = $this->parts['groupby'];
1748         $map    = reset($this->_aliasMap);
1749         $componentAlias = key($this->_aliasMap);
1750         $table = $map['table'];
1751
1752         // build the query base
1753         $q  = 'SELECT COUNT(DISTINCT ' . $this->getTableAlias($componentAlias)
1754               . '.' . implode(',', (array) $table->getIdentifier())
1755               . ') AS num_results';
1756
1757         foreach ($this->parts['select'] as $field) {
1758             if (strpos($field, '(') !== false) {
1759                 $q .= ', ' . $field;
1760             }
1761         }
1762
1763         $q .= ' FROM ' . $this->buildFromPart();
1764
1765         // append column aggregation inheritance (if needed)
1766         $string = $this->applyInheritance();
1767
1768         if ( ! empty($string)) {
1769             $where[] = $string;
1770         }
1771         // append conditions
1772         $q .= ( ! empty($where)) ?  ' WHERE '  . implode(' AND ', $where) : '';
1773         $q .= ( ! empty($groupby)) ?  ' GROUP BY '  . implode(', ', $groupby) : '';
1774         $q .= ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having): '';
1775
1776         if ( ! is_array($params)) {
1777             $params = array($params);
1778         }
1779         // append parameters
1780         $params = array_merge($this->_params['where'], $this->_params['having'], $params);
1781
1782         $results = $this->getConnection()->fetchAll($q, $params);
1783
1784         if (count($results) > 1) {
1785             $count = 0;
1786             foreach ($results as $result) {
1787                 $count += $result['num_results'];
1788             }
1789         } else {
1790             $count = isset($results[0]) ? $results[0]['num_results']:0;
1791         }
1792
1793         return (int) $count;
1794     }
1795
1796     /**
1797      * query
1798      * query the database with DQL (Doctrine Query Language)
1799      *
1800      * @param string $query      DQL query
1801      * @param array $params      prepared statement parameters
1802      * @param int $hydrationMode Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD
1803      * @see Doctrine::FETCH_* constants
1804      * @return mixed
1805      */
1806     public function query($query, $params = array(), $hydrationMode = null)
1807     {
1808         $this->parseQuery($query);
1809
1810         return $this->execute($params, $hydrationMode);
1811     }
1812
1813     /**
1814      * Copies a Doctrine_Query object.
1815      *
1816      * @param Doctrine_Query   Doctrine query instance.
1817      *                         If ommited the instance itself will be used as source.
1818      * @return Doctrine_Query  Copy of the Doctrine_Query instance.
1819      */
1820     public function copy(Doctrine_Query $query = null)
1821     {
1822         if ( ! $query) {
1823             $query = $this;
1824         }
1825
1826         $new = new Doctrine_Query();
1827         $new->_dqlParts = $query->_dqlParts;
1828         $new->_params = $query->_params;
1829         $new->_hydrationMode = $query->_hydrationMode;
1830
1831         return $new;
1832     }
1833
1834     /**
1835      * Frees the resources used by the query object. It especially breaks a 
1836      * cyclic reference between the query object and it's parsers. This enables
1837      * PHP's current GC to reclaim the memory.
1838      * This method can therefore be used to reduce memory usage when creating a lot
1839      * of query objects during a request.
1840      *
1841      * @return Doctrine_Query   this object
1842      */
1843     public function free()
1844     {
1845         $this->reset();
1846         $this->_parsers = array();
1847         $this->_dqlParts = array();
1848         $this->_enumParams = array();
1849     }
1850 }