Coverage for Doctrine_Query

Back to coverage report

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