Coverage for Doctrine_Query

Back to coverage report

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