Coverage for Doctrine_Query

Back to coverage report

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