Coverage for Doctrine_Query

Back to coverage report

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