Coverage for Doctrine_Query

Back to coverage report

1 <?php
2 /*
3  *  $Id: Query.php 3221 2007-11-25 15:57:08Z romanb $
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: 3221 $
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['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      * processPendingFields
471      * the fields in SELECT clause cannot be parsed until the components
472      * in FROM clause are parsed, hence this method is called everytime a 
473      * specific component is being parsed.
474      *
475      * @throws Doctrine_Query_Exception     if unknown component alias has been given
476      * @param string $componentAlias        the alias of the component
477      * @return void
478      * @todo Description: What is a 'pending field' (and are there non-pending fields, too)?
479      *       What is 'processed'? (Meaning: What information is gathered & stored away)
480      */
481     public function processPendingFields($componentAlias)
482     {
483         $tableAlias = $this->getTableAlias($componentAlias);
484         $table      = $this->_queryComponents[$componentAlias]['table'];
485
486         if ( ! isset($this->_pendingFields[$componentAlias])) {
487             return;
488         }
489
490         $fields = $this->_pendingFields[$componentAlias];
491
492
493         // check for wildcards
494         if (in_array('*', $fields)) {
495             //echo "<br />";Doctrine::dump($table->getColumnNames()); echo "<br />";
496             $fields = $table->getFieldNames();
497         } else {
498             // only auto-add the primary key fields if this query object is not
499             // a subquery of another query object
500             if ( ! $this->_isSubquery) {
501                 $fields = array_unique(array_merge((array) $table->getIdentifier(), $fields));
502             }
503         }
504         
505         $sql = array();
506         foreach ($fields as $fieldName) {
507             $columnName = $table->getColumnName($fieldName);
508             if (($owner = $table->getColumnOwner($columnName)) !== null && 
509                     $owner !== $table->getComponentName()) {
510
511                 $parent = $this->_conn->getTable($owner);
512                 $columnName = $parent->getColumnName($fieldName);
513                 $parentAlias = $this->getTableAlias($componentAlias . '.' . $parent->getComponentName());
514                 $sql[] = $this->_conn->quoteIdentifier($parentAlias . '.' . $columnName)
515                        . ' AS '
516                        . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName);
517             } else {
518                 $columnName = $table->getColumnName($fieldName);
519                 $sql[] = $this->_conn->quoteIdentifier($tableAlias . '.' . $columnName)
520                        . ' AS '
521                        . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName);
522             }
523         }
524
525         $this->_neededTables[] = $tableAlias;
526         //Doctrine::dump(implode(', ', $sql));
527         //echo "<br /><br />";
528         return implode(', ', $sql);
529     }
530
531     /**
532      * parseSelectField
533      *
534      * @throws Doctrine_Query_Exception     if unknown component alias has been given
535      * @return void
536      * @todo Description: Explain what this method does. Is there a relation to parseSelect()?
537      *       (It doesnt seem to get called from there...?). In what circumstances is this method
538      *       used?
539      */
540     public function parseSelectField($field)
541     {
542         $terms = explode('.', $field);
543
544         if (isset($terms[1])) {
545             $componentAlias = $terms[0];
546             $field = $terms[1];
547         } else {
548             reset($this->_queryComponents);
549             $componentAlias = key($this->_queryComponents);
550             $fields = $terms[0];
551         }
552
553         $tableAlias = $this->getTableAlias($componentAlias);
554         $table      = $this->_queryComponents[$componentAlias]['table'];
555
556
557         // check for wildcards
558         if ($field === '*') {
559             $sql = array();
560
561             foreach ($table->getColumnNames() as $field) {
562                 $sql[] = $this->parseSelectField($componentAlias . '.' . $field);
563             }
564
565             return implode(', ', $sql);
566         } else {
567             $name = $table->getColumnName($field);
568     
569             $this->_neededTables[] = $tableAlias;
570     
571             return $this->_conn->quoteIdentifier($tableAlias . '.' . $name)
572                    . ' AS '
573                    . $this->_conn->quoteIdentifier($tableAlias . '__' . $name);
574         }
575     }
576
577     /**
578      * getExpressionOwner
579      * returns the component alias for owner of given expression
580      *
581      * @param string $expr      expression from which to get to owner from
582      * @return string           the component alias
583      * @todo Description: What does it mean if a component is an 'owner' of an expression?
584      *       What kind of 'expression' are we talking about here?
585      */
586     public function getExpressionOwner($expr)
587     {
588         if (strtoupper(substr(trim($expr, '( '), 0, 6)) !== 'SELECT') {
589             preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $expr, $matches);
590
591             $match = current($matches);
592
593             if (isset($match[0])) {
594                 $terms = explode('.', $match[0]);
595
596                 return $terms[0];
597             }
598         }
599         return $this->getRootAlias();
600
601     }
602
603     /**
604      * parseSelect
605      * parses the query select part and
606      * adds selected fields to pendingFields array
607      *
608      * @param string $dql
609      * @todo Description: What information is extracted (and then stored)?
610      */
611     public function parseSelect($dql)
612     {
613         $refs = $this->_tokenizer->sqlExplode($dql, ',');
614
615         $pos   = strpos(trim($refs[0]), ' ');
616         $first = substr($refs[0], 0, $pos);
617
618         // check for DISTINCT keyword
619         if ($first === 'DISTINCT') {
620             $this->_sqlParts['distinct'] = true;
621
622             $refs[0] = substr($refs[0], ++$pos);
623         }
624
625         $parsedComponents = array();
626
627         foreach ($refs as $reference) {
628             $reference = trim($reference);
629
630             if (empty($reference)) {
631                 continue;
632             }
633
634             $terms = $this->_tokenizer->sqlExplode($reference, ' ');
635
636             $pos   = strpos($terms[0], '(');
637
638             if (count($terms) > 1 || $pos !== false) {
639                 $expression = array_shift($terms);
640                 $alias = array_pop($terms);
641
642                 if ( ! $alias) {
643                     $alias = substr($expression, 0, $pos);
644                 }
645
646                 $componentAlias = $this->getExpressionOwner($expression);
647                 $expression = $this->parseClause($expression);
648
649                 $tableAlias = $this->getTableAlias($componentAlias);
650
651                 $index    = count($this->_aggregateAliasMap);
652
653                 $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index);
654
655                 $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias;
656
657                 $this->_aggregateAliasMap[$alias] = $sqlAlias;
658                 $this->_expressionMap[$alias][0] = $expression;
659
660                 $this->_queryComponents[$componentAlias]['agg'][$index] = $alias;
661
662                 $this->_neededTables[] = $tableAlias;
663             } else {
664                 $e = explode('.', $terms[0]);
665
666                 if (isset($e[1])) {
667                     $componentAlias = $e[0];
668                     $field = $e[1];
669                 } else {
670                     reset($this->_queryComponents);
671                     $componentAlias = key($this->_queryComponents);
672                     $field = $e[0];
673                 }
674
675                 $this->_pendingFields[$componentAlias][] = $field;
676             }
677         }
678     }
679
680     /**
681      * parseClause
682      * parses given DQL clause
683      *
684      * this method handles five tasks:
685      *
686      * 1. Converts all DQL functions to their native SQL equivalents
687      * 2. Converts all component references to their table alias equivalents
688      * 3. Converts all field names to actual column names
689      * 4. Quotes all identifiers
690      * 5. Parses nested clauses and subqueries recursively
691      *
692      * @return string   SQL string
693      * @todo Description: What is a 'dql clause' (and what not)?
694      *       Refactor: Too long & nesting level
695      */
696     public function parseClause($clause)
697     {
698      $clause = trim($clause);
699
700      if (is_numeric($clause)) {
701         return $clause;
702      }
703
704         $terms = $this->_tokenizer->clauseExplode($clause, array(' ', '+', '-', '*', '/'));
705
706         $str = '';
707         foreach ($terms as $term) {
708             $pos = strpos($term[0], '(');
709             
710             if ($pos !== false) {
711                 $name = substr($term[0], 0, $pos);
712                 if ($name !== '') {
713                     $argStr = substr($term[0], ($pos + 1), -1);
714
715                     $args   = array();
716                     // parse args
717
718                     foreach ($this->_tokenizer->sqlExplode($argStr, ',') as $expr) {
719                        $args[] = $this->parseClause($expr);
720                     }
721
722                     // convert DQL function to its RDBMS specific equivalent
723                     try {
724                         $expr = call_user_func_array(array($this->_conn->expression, $name), $args);
725                     } catch (Doctrine_Expression_Exception $e) {
726                         throw new Doctrine_Query_Exception('Unknown function ' . $expr . '.');
727                     }
728                     $term[0] = $expr;
729                 } else {
730                     $trimmed = trim($this->_tokenizer->bracketTrim($term[0]));
731
732                     // check for possible subqueries
733                     if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') {
734                         // parse subquery
735                         $trimmed = $this->createSubquery()->parseDqlQuery($trimmed)->getQuery();
736                     } else {
737                         // parse normal clause
738                         $trimmed = $this->parseClause($trimmed);
739                     }
740
741                     $term[0] = '(' . $trimmed . ')';
742                 }
743             } else {
744                 if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'") {
745                     
746                     if (strpos($term[0], '.') !== false) {
747                         if ( ! is_numeric($term[0])) {
748                             $e = explode('.', $term[0]);
749
750                             $field = array_pop($e);
751
752                             if ($this->getType() === Doctrine_Query::SELECT) {
753                                 $componentAlias = implode('.', $e);
754     
755                                 if (empty($componentAlias)) {
756                                     $componentAlias = $this->getRootAlias();
757                                 }
758     
759                                 $this->load($componentAlias);
760
761                                 // check the existence of the component alias
762                                 if ( ! isset($this->_queryComponents[$componentAlias])) {
763                                     throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias);
764                                 }
765
766                                 $table = $this->_queryComponents[$componentAlias]['table'];
767
768                                 $def = $table->getDefinitionOf($field);
769
770                                 // get the actual field name from alias
771                                 $field = $table->getColumnName($field);
772
773                                 // check column existence
774                                 if ( ! $def) {
775                                     throw new Doctrine_Query_Exception('Unknown column ' . $field);
776                                 }
777                                 
778                                 if (isset($def['owner'])) {
779                                     $componentAlias = $componentAlias . '.' . $def['owner'];
780                                 }
781
782                                 $tableAlias = $this->getTableAlias($componentAlias);
783
784                                 // build sql expression
785                                 $term[0] = $this->_conn->quoteIdentifier($tableAlias)
786                                          . '.'
787                                          . $this->_conn->quoteIdentifier($field);
788                             } else {
789                                 // build sql expression
790                                 $term[0] = $this->_conn->quoteIdentifier($field);
791                             }
792                         }
793                     } else {
794
795                         if ( ! empty($term[0]) &&
796                              ! in_array(strtoupper($term[0]), self::$_keywords) &&
797                              ! is_numeric($term[0])) {
798
799                             $componentAlias = $this->getRootAlias();
800
801                             $found = false;
802
803                             if ($componentAlias !== false && 
804                                 $componentAlias !== null) {
805                                 $table = $this->_queryComponents[$componentAlias]['table'];
806
807                                 // check column existence
808                                 if ($table->hasColumn($term[0])) {
809                                     $found = true;
810
811
812                                     $def = $table->getDefinitionOf($term[0]);
813
814                                     // get the actual column name from alias
815                                     $term[0] = $table->getColumnName($term[0]);
816
817
818                                     if (isset($def['owner'])) {
819                                         $componentAlias = $componentAlias . '.' . $def['owner'];
820                                     }
821
822                                     $tableAlias = $this->getTableAlias($componentAlias);
823
824                                     $tableAlias = $this->getTableAlias($componentAlias);
825
826                                     if ($this->getType() === Doctrine_Query::SELECT) {
827                                         // build sql expression
828                                         $term[0] = $this->_conn->quoteIdentifier($tableAlias)
829                                                  . '.'
830                                                  . $this->_conn->quoteIdentifier($term[0]);
831                                     } else {
832                                         // build sql expression
833                                         $term[0] = $this->_conn->quoteIdentifier($term[0]);
834                                     }
835                                 } else {
836                                     $found = false;
837                                 }
838                             }
839
840                             if ( ! $found) {
841                                 $term[0] = $this->getSqlAggregateAlias($term[0]);
842                             }
843                         }
844                     }
845                 }
846             }
847
848             $str .= $term[0] . $term[1];
849         }
850         return $str;
851     }
852
853     /**
854      * parseAggregateFunction
855      * parses an aggregate function and returns the parsed form
856      *
857      * @see Doctrine_Expression
858      * @param string $expr                  DQL aggregate function
859      * @throws Doctrine_Query_Exception     if unknown aggregate function given
860      * @return array                        parsed form of given function
861      */
862     public function parseAggregateFunction($expr, $nestedCall = false)
863     {
864         $e    = $this->_tokenizer->bracketExplode($expr, ' ');
865         $func = $e[0];
866
867         $pos  = strpos($func, '(');
868         if ($pos === false) {
869             return $expr;
870         }
871
872         // get the name of the function
873         $name   = substr($func, 0, $pos);
874         $argStr = substr($func, ($pos + 1), -1);
875
876         $args   = array();
877         // parse args
878         foreach ($this->_tokenizer->bracketExplode($argStr, ',') as $expr) {
879            $args[] = $this->parseAggregateFunction($expr, true);
880         }
881
882         // convert DQL function to its RDBMS specific equivalent
883         try {
884             $expr = call_user_func_array(array($this->_conn->expression, $name), $args);
885         } catch (Doctrine_Expression_Exception $e) {
886             throw new Doctrine_Query_Exception('Unknown function ' . $func . '.');
887         }
888
889         if ( ! $nestedCall) {
890             // try to find all component references
891             preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $argStr, $m);
892
893             if (isset($e[1])) {
894                 if (strtoupper($e[1]) === 'AS') {
895                     if ( ! isset($e[2])) {
896                         throw new Doctrine_Query_Exception('Missing aggregate function alias.');
897                     }
898                     $alias = $e[2];
899                 } else {
900                     $alias = $e[1];
901                 }
902             } else {
903                 $alias = substr($expr, 0, strpos($expr, '('));
904             }
905
906             $this->_pendingAggregates[] = array($expr, $m[0], $alias);
907         }
908
909         return $expr;
910     }
911
912     /**
913      * processPendingSubqueries
914      * processes pending subqueries
915      *
916      * subqueries can only be processed when the query is fully constructed
917      * since some subqueries may be correlated
918      *
919      * @return void
920      * @todo Better description. i.e. What is a 'pending subquery'? What does 'processed' mean?
921      *       (parsed? sql is constructed? some information is gathered?)
922      */
923     public function processPendingSubqueries()
924     {
925         foreach ($this->_pendingSubqueries as $value) {
926             list($dql, $alias) = $value;
927
928             $subquery = $this->createSubquery();
929
930             $sql = $subquery->parseDqlQuery($dql, false)->getQuery();
931
932             reset($this->_queryComponents);
933             $componentAlias = key($this->_queryComponents);
934             $tableAlias = $this->getTableAlias($componentAlias);
935
936             $sqlAlias = $tableAlias . '__' . count($this->_aggregateAliasMap);
937
938             $this->_sqlParts['select'][] = '(' . $sql . ') AS ' . $this->_conn->quoteIdentifier($sqlAlias);
939
940             $this->_aggregateAliasMap[$alias] = $sqlAlias;
941             $this->_queryComponents[$componentAlias]['agg'][] = $alias;
942         }
943         $this->_pendingSubqueries = array();
944     }
945
946     /**
947      * processPendingAggregates
948      * processes pending aggregate values for given component alias
949      *
950      * @return void
951      * @todo Better description. i.e. What is a 'pending aggregate'? What does 'processed' mean?
952      */
953     public function processPendingAggregates()
954     {
955         // iterate trhough all aggregates
956         foreach ($this->_pendingAggregates as $aggregate) {
957             list ($expression, $components, $alias) = $aggregate;
958
959             $tableAliases = array();
960
961             // iterate through the component references within the aggregate function
962             if ( ! empty ($components)) {
963                 foreach ($components as $component) {
964
965                     if (is_numeric($component)) {
966                         continue;
967                     }
968
969                     $e = explode('.', $component);
970
971                     $field = array_pop($e);
972                     $componentAlias = implode('.', $e);
973
974                     // check the existence of the component alias
975                     if ( ! isset($this->_queryComponents[$componentAlias])) {
976                         throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias);
977                     }
978
979                     $table = $this->_queryComponents[$componentAlias]['table'];
980
981                     $field = $table->getColumnName($field);
982
983                     // check column existence
984                     if ( ! $table->hasColumn($field)) {
985                         throw new Doctrine_Query_Exception('Unknown column ' . $field);
986                     }
987
988                     $sqlTableAlias = $this->getSqlTableAlias($componentAlias);
989
990                     $tableAliases[$sqlTableAlias] = true;
991
992                     // build sql expression
993
994                     $identifier = $this->_conn->quoteIdentifier($sqlTableAlias . '.' . $field);
995                     $expression = str_replace($component, $identifier, $expression);
996                 }
997             }
998
999             if (count($tableAliases) !== 1) {
1000                 $componentAlias = reset($this->_tableAliasMap);
1001                 $tableAlias = key($this->_tableAliasMap);
1002             }
1003
1004             $index    = count($this->_aggregateAliasMap);
1005             $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index);
1006
1007             $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias;
1008
1009             $this->_aggregateAliasMap[$alias] = $sqlAlias;
1010             $this->_expressionMap[$alias][0] = $expression;
1011
1012             $this->_queryComponents[$componentAlias]['agg'][$index] = $alias;
1013
1014             $this->_neededTables[] = $tableAlias;
1015         }
1016         // reset the state
1017         $this->_pendingAggregates = array();
1018     }
1019
1020     /**
1021      * _buildSqlQueryBase
1022      * returns the base of the generated sql query
1023      * On mysql driver special strategy has to be used for DELETE statements
1024      * (where is this special strategy??)
1025      *
1026      * @return string       the base of the generated sql query
1027      */
1028     protected function _buildSqlQueryBase()
1029     {
1030         switch ($this->_type) {
1031             case self::DELETE:
1032                 $q = 'DELETE FROM ';
1033             break;
1034             case self::UPDATE:
1035                 $q = 'UPDATE ';
1036             break;
1037             case self::SELECT:
1038                 $distinct = ($this->_sqlParts['distinct']) ? 'DISTINCT ' : '';
1039                 $q = 'SELECT ' . $distinct . implode(', ', $this->_sqlParts['select']) . ' FROM ';
1040             break;
1041         }
1042         return $q;
1043     }
1044
1045     /**
1046      * _buildSqlFromPart
1047      * builds the from part of the query and returns it
1048      *
1049      * @return string   the query sql from part
1050      */
1051     protected function _buildSqlFromPart()
1052     {
1053         $q = '';
1054         foreach ($this->_sqlParts['from'] as $k => $part) {
1055             if ($k === 0) {
1056                 $q .= $part;
1057                 continue;
1058             }
1059             
1060             // preserve LEFT JOINs only if needed
1061 // Check if it's JOIN, if not add a comma separator instead of space
1062             if (!preg_match('/\bJOIN\b/i', $part) && !isset($this->_pendingJoinConditions[$k])) {
1063                 $q .= ', ' . $part;
1064             } else {
1065                 if (substr($part, 0, 9) === 'LEFT JOIN') {
1066                     $e = explode(' ', $part);
1067
1068                     $aliases = array_merge($this->_subqueryAliases,
1069                                 array_keys($this->_neededTables));
1070
1071                     if ( ! in_array($e[3], $aliases) &&
1072                         ! in_array($e[2], $aliases) &&
1073
1074                         ! empty($this->_pendingFields)) {
1075                         continue;
1076                     }
1077
1078                 }
1079
1080                 if (isset($this->_pendingJoinConditions[$k])) {
1081                     $parser = new Doctrine_Query_JoinCondition($this, $this->_tokenizer);
1082
1083                     if (strpos($part, ' ON ') !== false) {
1084                         $part .= ' AND ';
1085                     } else {
1086                         $part .= ' ON ';
1087                     }
1088                     $part .= $parser->parse($this->_pendingJoinConditions[$k]);
1089
1090                     unset($this->_pendingJoinConditions[$k]);
1091                 }
1092
1093                 $q .= ' ' . $part;
1094             }
1095
1096             $this->_sqlParts['from'][$k] = $part;
1097         }
1098         return $q;
1099     }
1100
1101     /**
1102      * preQuery
1103      *
1104      * Empty template method to provide Query subclasses with the possibility
1105      * to hook into the query building procedure, doing any custom / specialized
1106      * query building procedures that are neccessary.
1107      *
1108      * @return void
1109      */
1110     public function preQuery()
1111     {
1112
1113     }
1114
1115     /**
1116      * postQuery
1117      *
1118      * Empty template method to provide Query subclasses with the possibility
1119      * to hook into the query building procedure, doing any custom / specialized
1120      * post query procedures (for example logging) that are neccessary.
1121      *
1122      * @return void
1123      */
1124     public function postQuery()
1125     {
1126
1127     }
1128
1129     /**
1130      * builds the sql query from the given parameters and applies things such as
1131      * column aggregation inheritance and limit subqueries if needed
1132      *
1133      * @param array $params             an array of prepared statement params (needed only in mysql driver
1134      *                                  when limit subquery algorithm is used)
1135      * @return string                   the built sql query
1136      */
1137     public function getSqlQuery($params = array())
1138     {
1139         if ($this->_state !== self::STATE_DIRTY) {
1140            return $this->_sql;
1141         }
1142
1143         // reset the state
1144         if ( ! $this->isSubquery()) {
1145             $this->_queryComponents = array();
1146             $this->_pendingAggregates = array();
1147             $this->_aggregateAliasMap = array();
1148         }
1149         $this->reset();
1150
1151         // invoke the preQuery hook
1152         $this->preQuery();
1153
1154         // process the DQL parts => generate the SQL parts.
1155         // this will also populate the $_queryComponents.
1156         foreach ($this->_dqlParts as $queryPartName => $queryParts) {
1157             $this->_processDqlQueryPart($queryPartName, $queryParts);
1158         }
1159         $this->_state = self::STATE_CLEAN;
1160         
1161         $params = $this->convertEnums($params);        
1162
1163
1164         // Proceed with the generated SQL
1165         
1166         if (empty($this->_sqlParts['from'])) {
1167             return false;
1168         }
1169
1170         $needsSubQuery = false;
1171         $subquery = '';
1172         $map = reset($this->_queryComponents);
1173         $table = $map['table'];
1174         $rootAlias = key($this->_queryComponents);
1175
1176         if ( ! empty($this->_sqlParts['limit']) && $this->_needsSubquery &&
1177                 $table->getAttribute(Doctrine::ATTR_QUERY_LIMIT) == Doctrine::LIMIT_RECORDS) {
1178             $this->_isLimitSubqueryUsed = true;
1179             $needsSubQuery = true;
1180         }
1181
1182         $sql = array();
1183         foreach ($this->_queryComponents as $alias => $map) {
1184             $fieldSql = $this->processPendingFields($alias);
1185             if ( ! empty($fieldSql)) {
1186                 $sql[] = $fieldSql;
1187             }
1188         }
1189         if ( ! empty($sql)) {
1190             array_unshift($this->_sqlParts['select'], implode(', ', $sql));
1191         }
1192
1193         $this->_pendingFields = array();
1194
1195         // build the basic query
1196         $q  = $this->_buildSqlQueryBase();
1197         $q .= $this->_buildSqlFromPart();
1198
1199         if ( ! empty($this->_sqlParts['set'])) {
1200             $q .= ' SET ' . implode(', ', $this->_sqlParts['set']);
1201         }
1202
1203
1204         $string = $this->applyInheritance();
1205
1206         // apply inheritance to WHERE part
1207         if ( ! empty($string)) {
1208             if (substr($string, 0, 1) === '(' && substr($string, -1) === ')') {
1209                 $this->_sqlParts['where'][] = $string;
1210             } else {
1211                 $this->_sqlParts['where'][] = '(' . $string . ')';
1212             }
1213         }
1214
1215         $modifyLimit = true;
1216         if ( ! empty($this->_sqlParts['limit']) || ! empty($this->_sqlParts['offset'])) {
1217
1218             if ($needsSubQuery) {
1219                 $subquery = $this->getLimitSubquery();
1220                 // what about composite keys?
1221                 $idColumnName = $table->getColumnName($table->getIdentifier());
1222                 switch (strtolower($this->_conn->getName())) {
1223                     case 'mysql':
1224                         // mysql doesn't support LIMIT in subqueries
1225                         $list     = $this->_conn->execute($subquery, $params)->fetchAll(Doctrine::FETCH_COLUMN);
1226                         $subquery = implode(', ', array_map(array($this->_conn, 'quote'), $list));
1227                         break;
1228                     case 'pgsql':
1229                         // pgsql needs special nested LIMIT subquery
1230                         $subquery = 'SELECT doctrine_subquery_alias.' . $idColumnName . ' FROM (' . $subquery . ') AS doctrine_subquery_alias';
1231                         break;
1232                 }
1233
1234                 $field = $this->getSqlTableAlias($rootAlias) . '.' . $idColumnName;
1235
1236                 // only append the subquery if it actually contains something
1237                 if ($subquery !== '') {
1238                     array_unshift($this->_sqlParts['where'], $this->_conn->quoteIdentifier($field) . ' IN (' . $subquery . ')');
1239                 }
1240
1241                 $modifyLimit = false;
1242             }
1243         }
1244
1245         $q .= ( ! empty($this->_sqlParts['where']))?   ' WHERE '    . implode(' AND ', $this->_sqlParts['where']) : '';
1246         $q .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby'])  : '';
1247         $q .= ( ! empty($this->_sqlParts['having']))?  ' HAVING '   . implode(' AND ', $this->_sqlParts['having']): '';
1248         $q .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby'])  : '';
1249
1250         if ($modifyLimit) {
1251             $q = $this->_conn->modifyLimitQuery($q, $this->_sqlParts['limit'], $this->_sqlParts['offset']);
1252         }
1253
1254         // return to the previous state
1255         if ( ! empty($string)) {
1256             array_pop($this->_sqlParts['where']);
1257         }
1258         if ($needsSubQuery) {
1259             array_shift($this->_sqlParts['where']);
1260         }
1261         $this->_sql = $q;
1262
1263         return $q;
1264     }
1265
1266     /**
1267      * getLimitSubquery
1268      * this is method is used by the record limit algorithm
1269      *
1270      * when fetching one-to-many, many-to-many associated data with LIMIT clause
1271      * an additional subquery is needed for limiting the number of returned records instead
1272      * of limiting the number of sql result set rows
1273      *
1274      * @return string       the limit subquery
1275      * @todo A little refactor to make the method easier to understand & maybe shorter?
1276      */
1277     public function getLimitSubquery()
1278     {
1279         $map    = reset($this->_queryComponents);
1280         $table  = $map['table'];
1281         $componentAlias = key($this->_queryComponents);
1282
1283         // get short alias
1284         $alias      = $this->getTableAlias($componentAlias);
1285         // what about composite keys?
1286         $primaryKey = $alias . '.' . $table->getColumnName($table->getIdentifier());
1287
1288         // initialize the base of the subquery
1289         $subquery   = 'SELECT DISTINCT ' . $this->_conn->quoteIdentifier($primaryKey);
1290
1291         $driverName = $this->_conn->getAttribute(Doctrine::ATTR_DRIVER_NAME);
1292
1293         // pgsql needs the order by fields to be preserved in select clause
1294         if ($driverName == 'pgsql') {
1295             foreach ($this->_sqlParts['orderby'] as $part) {
1296                 $part = trim($part);
1297                 $e = $this->_tokenizer->bracketExplode($part, ' ');
1298                 $part = trim($e[0]);
1299
1300                 if (strpos($part, '.') === false) {
1301                     continue;
1302                 }
1303
1304                 // don't add functions
1305                 if (strpos($part, '(') !== false) {
1306                     continue;
1307                 }
1308
1309                 // don't add primarykey column (its already in the select clause)
1310                 if ($part !== $primaryKey) {
1311                     $subquery .= ', ' . $part;
1312                 }
1313             }
1314         }
1315
1316         if ($driverName == 'mysql' || $driverName == 'pgsql') {
1317             foreach ($this->_expressionMap as $dqlAlias => $expr) {
1318                 if (isset($expr[1])) {
1319                     $subquery .= ', ' . $expr[0] . ' AS ' . $this->_aggregateAliasMap[$dqlAlias];
1320                 }
1321             }
1322         }
1323
1324         $subquery .= ' FROM';
1325
1326         foreach ($this->_sqlParts['from'] as $part) {
1327             // preserve LEFT JOINs only if needed
1328             if (substr($part, 0, 9) === 'LEFT JOIN') {
1329                 $e = explode(' ', $part);
1330
1331                 if (empty($this->_sqlParts['orderby']) && empty($this->_sqlParts['where'])) {
1332                     continue;
1333                 }
1334             }
1335
1336             $subquery .= ' ' . $part;
1337         }
1338
1339         // all conditions must be preserved in subquery
1340         $subquery .= ( ! empty($this->_sqlParts['where']))?   ' WHERE '    . implode(' AND ', $this->_sqlParts['where'])  : '';
1341         $subquery .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby'])   : '';
1342         $subquery .= ( ! empty($this->_sqlParts['having']))?  ' HAVING '   . implode(' AND ', $this->_sqlParts['having']) : '';
1343
1344         $subquery .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby'])   : '';
1345
1346         // add driver specific limit clause
1347         $subquery = $this->_conn->modifyLimitQuery($subquery, $this->_sqlParts['limit'], $this->_sqlParts['offset']);
1348
1349         $parts = $this->_tokenizer->quoteExplode($subquery, ' ', "'", "'");
1350
1351         foreach ($parts as $k => $part) {
1352             if (strpos($part, ' ') !== false) {
1353                 continue;
1354             }
1355
1356             $part = trim($part, "\"'`");
1357
1358             if ($this->hasSqlTableAlias($part)) {
1359                 $parts[$k] = $this->_conn->quoteIdentifier($this->generateNewSqlTableAlias($part));
1360                 continue;
1361             }
1362
1363             if (strpos($part, '.') === false) {
1364                 continue;
1365             }
1366             preg_match_all("/[a-zA-Z0-9_]+\.[a-z0-9_]+/i", $part, $m);
1367
1368             foreach ($m[0] as $match) {
1369                 $e = explode('.', $match);
1370                 $e[0] = $this->generateNewSqlTableAlias($e[0]);
1371
1372                 $parts[$k] = str_replace($match, implode('.', $e), $parts[$k]);
1373             }
1374         }
1375
1376         if ($driverName == 'mysql' || $driverName == 'pgsql') {
1377             foreach ($parts as $k => $part) {
1378                 if (strpos($part, "'") !== false) {
1379                     continue;
1380                 }
1381                 if (strpos($part, '__') == false) {
1382                     continue;
1383                 }
1384
1385                 preg_match_all("/[a-zA-Z0-9_]+\_\_[a-z0-9_]+/i", $part, $m);
1386
1387                 foreach ($m[0] as $match) {
1388                     $e = explode('__', $match);
1389                     $e[0] = $this->generateNewTableAlias($e[0]);
1390
1391                     $parts[$k] = str_replace($match, implode('__', $e), $parts[$k]);
1392                 }
1393             }
1394         }
1395
1396         $subquery = implode(' ', $parts);
1397         return $subquery;
1398     }
1399
1400     /**
1401      * DQL PARSER
1402      * parses a DQL query
1403      * first splits the query in parts and then uses individual
1404      * parsers for each part
1405      *
1406      * @param string $query                 DQL query
1407      * @param boolean $clear                whether or not to clear the aliases
1408      * @throws Doctrine_Query_Exception     if some generic parsing error occurs
1409      * @return Doctrine_Query
1410      */
1411     public function parseDqlQuery($query, $clear = true)
1412     {
1413         if ($clear) {
1414             $this->clear();
1415         }
1416
1417         $query = trim($query);
1418         $query = str_replace("\n", ' ', $query);
1419         $query = str_replace("\r", ' ', $query);
1420
1421         $parts = $this->_tokenizer->tokenizeQuery($query);
1422
1423         foreach ($parts as $partName => $subParts) {
1424             $subParts = trim($subParts);
1425             $partName = strtolower($partName);
1426             switch ($partName) {
1427                 case 'create':
1428                     $this->_type = self::CREATE;
1429                 break;
1430                 case 'insert':
1431                     $this->_type = self::INSERT;
1432                 break;
1433                 case 'delete':
1434                     $this->_type = self::DELETE;
1435                 break;
1436                 case 'select':
1437                     $this->_type = self::SELECT;
1438                     $this->_addDqlQueryPart($partName, $subParts);
1439                 break;
1440                 case 'update':
1441                     $this->_type = self::UPDATE;
1442                     $partName = 'from';
1443                 case 'from':
1444                     $this->_addDqlQueryPart($partName, $subParts);
1445                 break;
1446                 case 'set':
1447                     $this->_addDqlQueryPart($partName, $subParts, true);
1448                 break;
1449                 case 'group':
1450                 case 'order':
1451                     $partName .= 'by';
1452                 case 'where':
1453                 case 'having':
1454                 case 'limit':
1455                 case 'offset':
1456                     $this->_addDqlQueryPart($partName, $subParts);
1457                 break;
1458             }
1459         }
1460
1461         return $this;
1462     }
1463     
1464     /**
1465      * @todo Describe & refactor... too long and nested.
1466      */
1467     public function load($path, $loadFields = true)
1468     {
1469      if (isset($this->_queryComponents[$path])) {
1470         return $this->_queryComponents[$path];
1471      }
1472         $e = $this->_tokenizer->quoteExplode($path, ' INDEXBY ');
1473
1474         $mapWith = null;
1475         if (count($e) > 1) {
1476             $mapWith = trim($e[1]);
1477
1478             $path = $e[0];
1479         }
1480
1481         // parse custom join conditions
1482         $e = explode(' ON ', $path);
1483
1484         $joinCondition = '';
1485
1486         if (count($e) > 1) {
1487             $joinCondition = $e[1];
1488             $overrideJoin = true;
1489             $path = $e[0];
1490         } else {
1491             $e = explode(' WITH ', $path);
1492
1493             if (count($e) > 1) {
1494                 $joinCondition = $e[1];
1495                 $path = $e[0];
1496             }
1497             $overrideJoin = false;
1498         }
1499
1500         $tmp            = explode(' ', $path);
1501         $componentAlias = $originalAlias = (count($tmp) > 1) ? end($tmp) : null;
1502
1503         $e = preg_split("/[.:]/", $tmp[0], -1);
1504
1505         $fullPath = $tmp[0];
1506         $prevPath = '';
1507         $fullLength = strlen($fullPath);
1508
1509         if (isset($this->_queryComponents[$e[0]])) {
1510             $table = $this->_queryComponents[$e[0]]['table'];
1511             $componentAlias = $e[0];
1512
1513             $prevPath = $parent = array_shift($e);
1514         }
1515
1516         foreach ($e as $key => $name) {
1517             // get length of the previous path
1518             $length = strlen($prevPath);
1519
1520             // build the current component path
1521             $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name;
1522
1523             $delimeter = substr($fullPath, $length, 1);
1524
1525             // if an alias is not given use the current path as an alias identifier
1526             if (strlen($prevPath) === $fullLength && isset($originalAlias)) {
1527                 $componentAlias = $originalAlias;
1528             } else {
1529                 $componentAlias = $prevPath;
1530             }
1531
1532             // if the current alias already exists, skip it
1533             if (isset($this->_queryComponents[$componentAlias])) {
1534                 continue;
1535             }
1536
1537             if ( ! isset($table)) {
1538                 // process the root of the path
1539
1540                 $table = $this->loadRoot($name, $componentAlias);
1541             } else {
1542                 $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN ';
1543
1544                 $relation = $table->getRelation($name);
1545                 $localTable = $table;
1546
1547                 $table    = $relation->getTable();
1548                 $this->_queryComponents[$componentAlias] = array('table'    => $table,
1549                                                           'parent'   => $parent,
1550                                                           'relation' => $relation,
1551                                                           'map'      => null);
1552                 if ( ! $relation->isOneToOne()) {
1553                    $this->_needsSubquery = true;
1554                 }
1555
1556                 $localAlias   = $this->getTableAlias($parent, $table->getTableName());
1557                 $foreignAlias = $this->getTableAlias($componentAlias, $relation->getTable()->getTableName());
1558                 $localSql     = $this->_conn->quoteIdentifier($table->getTableName())
1559                               . ' '
1560                               . $this->_conn->quoteIdentifier($localAlias);
1561
1562                 $foreignSql   = $this->_conn->quoteIdentifier($relation->getTable()->getTableName())
1563                               . ' '
1564                               . $this->_conn->quoteIdentifier($foreignAlias);
1565
1566                 $map = $relation->getTable()->inheritanceMap;
1567
1568                 if ( ! $loadFields || ! empty($map) || $joinCondition) {
1569                     $this->_subqueryAliases[] = $foreignAlias;
1570                 }
1571
1572                 if ($relation instanceof Doctrine_Relation_Association) {
1573                     $asf = $relation->getAssociationTable();
1574
1575                     $assocTableName = $asf->getTableName();
1576
1577                     if ( ! $loadFields || ! empty($map) || $joinCondition) {
1578                         $this->_subqueryAliases[] = $assocTableName;
1579                     }
1580
1581                     $assocPath = $prevPath . '.' . $asf->getComponentName();
1582
1583                     $this->_queryComponents[$assocPath] = array('parent' => $prevPath, 'relation' => $relation, 'table' => $asf);
1584
1585                     $assocAlias = $this->getTableAlias($assocPath, $asf->getTableName());
1586
1587                     $queryPart = $join . $assocTableName . ' ' . $assocAlias;
1588
1589                     $queryPart .= ' ON ' . $localAlias
1590                                 . '.'
1591                                 . $localTable->getIdentifier() // what about composite keys?
1592                                 . ' = '
1593                                 . $assocAlias . '.' . $relation->getLocal();
1594
1595                     if ($relation->isEqual()) {
1596                         // equal nest relation needs additional condition
1597                         $queryPart .= ' OR ' . $localAlias
1598                                     . '.'
1599                                     . $table->getColumnName($table->getIdentifier())
1600                                     . ' = '
1601                                     . $assocAlias . '.' . $relation->getForeign();
1602                     }
1603
1604                     $this->_sqlParts['from'][] = $queryPart;
1605
1606                     $queryPart = $join . $foreignSql;
1607
1608                     if ( ! $overrideJoin) {
1609                         $queryPart .= ' ON ';
1610
1611                         if ($relation->isEqual()) {
1612                             $queryPart .= '(';
1613                         }
1614
1615                         $queryPart .= $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getTable()->getIdentifier())
1616                                     . ' = '
1617                                     . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeign());
1618
1619                         if ($relation->isEqual()) {
1620                             $queryPart .= ' OR '
1621                                         . $this->_conn->quoteIdentifier($foreignAlias . '.' . $table->getColumnName($table->getIdentifier()))
1622                                         . ' = ' 
1623                                         . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocal())
1624                                         . ') AND ' 
1625                                         . $this->_conn->quoteIdentifier($foreignAlias . '.' . $table->getIdentifier())
1626                                         . ' != '  
1627                                         . $this->_conn->quoteIdentifier($localAlias . '.' . $table->getIdentifier());
1628                         }
1629                     }
1630                 } else {
1631
1632                     $queryPart = $join . $foreignSql;
1633
1634                     if ( ! $overrideJoin) {
1635                         $queryPart .= ' ON '
1636                                    . $this->_conn->quoteIdentifier($localAlias . '.' . $relation->getLocal())
1637                                    . ' = ' 
1638                                    . $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getForeign());
1639                     }
1640                 }
1641
1642                 $queryPart .= $this->buildInheritanceJoinSql($table->getComponentName(), $componentAlias);
1643
1644                 $this->_sqlParts['from'][$componentAlias] = $queryPart;
1645                 if ( ! empty($joinCondition)) {
1646                     $this->_pendingJoinConditions[$componentAlias] = $joinCondition;
1647                 }
1648             }
1649             if ($loadFields) {
1650
1651                 $restoreState = false;
1652                 // load fields if necessary
1653                 if ($loadFields && empty($this->_dqlParts['select'])) {
1654                     $this->_pendingFields[$componentAlias] = array('*');
1655                 }
1656             }
1657             $parent = $prevPath;
1658         }
1659         
1660         $table = $this->_queryComponents[$componentAlias]['table'];
1661
1662         $indexBy = null;
1663
1664         if (isset($mapWith)) {
1665             $e = explode('.', $mapWith);
1666
1667             if (isset($e[1])) {
1668                 $indexBy = $e[1];
1669             }
1670         } elseif ($table->getBoundQueryPart('indexBy') !== null) {
1671             $indexBy = $table->getBoundQueryPart('indexBy');
1672         }
1673
1674         if ($indexBy !== null) {
1675             if ( ! $table->hasColumn($indexBy)) {
1676                 throw new Doctrine_Query_Exception("Couldn't use key mapping. Column " . $indexBy . " does not exist.");
1677             }
1678     
1679             $this->_queryComponents[$componentAlias]['map'] = $table->getColumnName($indexBy);
1680         }
1681         return $this->_queryComponents[$componentAlias];
1682     }
1683
1684     /**
1685      * loadRoot
1686      *
1687      * @param string $name
1688      * @param string $componentAlias
1689      * @todo DESCRIBE ME!
1690      */
1691     public function loadRoot($name, $componentAlias)
1692     {
1693         // get the connection for the component
1694         $this->_conn = Doctrine_Manager::getInstance()
1695                       ->getConnectionForComponent($name);
1696
1697         $table = $this->_conn->getTable($name);
1698         $tableName = $table->getTableName();
1699
1700         // get the short alias for this table
1701         $tableAlias = $this->getTableAlias($componentAlias, $tableName);
1702         // quote table name
1703         $queryPart = $this->_conn->quoteIdentifier($tableName);
1704
1705         if ($this->_type === self::SELECT) {
1706             $queryPart .= ' ' . $this->_conn->quoteIdentifier($tableAlias);
1707         }
1708         
1709         $this->_tableAliasMap[$tableAlias] = $componentAlias;
1710
1711         $queryPart .= $this->buildInheritanceJoinSql($name, $componentAlias);
1712
1713         $this->_sqlParts['from'][] = $queryPart;
1714
1715         $this->_queryComponents[$componentAlias] = array('table' => $table, 'map' => null);
1716
1717         return $table;
1718     }
1719     
1720     /**
1721      * @todo DESCRIBE ME!
1722      */
1723     public function buildInheritanceJoinSql($name, $componentAlias)
1724     {
1725         // get the connection for the component
1726         $this->_conn = Doctrine_Manager::getInstance()->getConnectionForComponent($name);
1727
1728         $table = $this->_conn->getTable($name);
1729         $tableName = $table->getTableName();
1730
1731         // get the short alias for this table
1732         $tableAlias = $this->getTableAlias($componentAlias, $tableName);
1733         
1734         $queryPart = '';
1735
1736         foreach ($table->getOption('joinedParents') as $parent) {
1737          $parentTable = $this->_conn->getTable($parent);
1738
1739             $parentAlias = $componentAlias . '.' . $parent;
1740
1741             // get the short alias for the parent table
1742             $parentTableAlias = $this->getTableAlias($parentAlias, $parentTable->getTableName());
1743
1744             $queryPart .= ' LEFT JOIN ' . $this->_conn->quoteIdentifier($parentTable->getTableName())
1745                         . ' ' . $this->_conn->quoteIdentifier($parentTableAlias) . ' ON ';
1746             
1747             //Doctrine::dump($table->getIdentifier());
1748             foreach ((array) $table->getIdentifier() as $identifier) {
1749                 $column = $table->getColumnName($identifier);
1750
1751                 $queryPart .= $this->_conn->quoteIdentifier($tableAlias) 
1752                             . '.' . $this->_conn->quoteIdentifier($column)
1753                             . ' = ' . $this->_conn->quoteIdentifier($parentTableAlias)
1754                             . '.' . $this->_conn->quoteIdentifier($column);
1755             }
1756         }
1757         
1758         return $queryPart;
1759     }
1760
1761     /**
1762      * count
1763      * fetches the count of the query
1764      *
1765      * This method executes the main query without all the
1766      * selected fields, ORDER BY part, LIMIT part and OFFSET part.
1767      *
1768      * Example:
1769      * Main query: 
1770      *      SELECT u.*, p.phonenumber FROM User u
1771      *          LEFT JOIN u.Phonenumber p 
1772      *          WHERE p.phonenumber = '123 123' LIMIT 10
1773      *
1774      * The modified DQL query:
1775      *      SELECT COUNT(DISTINCT u.id) FROM User u
1776      *          LEFT JOIN u.Phonenumber p
1777      *          WHERE p.phonenumber = '123 123'
1778      *
1779      * @param array $params        an array of prepared statement parameters
1780      * @return integer             the count of this query
1781      */
1782     public function count($params = array())
1783     {
1784         // triggers dql parsing/processing
1785         $this->getQuery(); // this is ugly
1786
1787         // initialize temporary variables
1788         $where  = $this->_sqlParts['where'];
1789         $having = $this->_sqlParts['having'];
1790         $groupby = $this->_sqlParts['groupby'];
1791         $map = reset($this->_queryComponents);
1792         $componentAlias = key($this->_queryComponents);
1793         $table = $map['table'];
1794
1795         // build the query base
1796         $q  = 'SELECT COUNT(DISTINCT ' . $this->getTableAlias($componentAlias)
1797               . '.' . implode(',', (array) $table->getIdentifier())
1798               . ') AS num_results';
1799
1800         foreach ($this->_sqlParts['select'] as $field) {
1801             if (strpos($field, '(') !== false) {
1802                 $q .= ', ' . $field;
1803             }
1804         }
1805
1806         $q .= ' FROM ' . $this->_buildSqlFromPart();
1807
1808         // append column aggregation inheritance (if needed)
1809         $string = $this->applyInheritance();
1810
1811         if ( ! empty($string)) {
1812             $where[] = $string;
1813         }
1814         // append conditions
1815         $q .= ( ! empty($where)) ?  ' WHERE '  . implode(' AND ', $where) : '';
1816         $q .= ( ! empty($groupby)) ?  ' GROUP BY '  . implode(', ', $groupby) : '';
1817         $q .= ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having): '';
1818
1819         if ( ! is_array($params)) {
1820             $params = array($params);
1821         }
1822         // append parameters
1823         $params = array_merge($this->_params['where'], $this->_params['having'], $params);
1824
1825         $params = $this->convertEnums($params);
1826
1827         $results = $this->getConnection()->fetchAll($q, $params);
1828
1829         if (count($results) > 1) {
1830             $count = 0;
1831             foreach ($results as $result) {
1832                 $count += $result['num_results'];
1833             }
1834         } else {
1835             $count = isset($results[0]) ? $results[0]['num_results']:0;
1836         }
1837
1838         return (int) $count;
1839     }
1840
1841     /**
1842      * query
1843      * query the database with DQL (Doctrine Query Language)
1844      *
1845      * @param string $query      DQL query
1846      * @param array $params      prepared statement parameters
1847      * @param int $hydrationMode Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD
1848      * @see Doctrine::FETCH_* constants
1849      * @return mixed
1850      */
1851     public function query($query, $params = array(), $hydrationMode = null)
1852     {
1853         $this->parseDqlQuery($query);
1854         return $this->execute($params, $hydrationMode);
1855     }
1856
1857     /**
1858      * Copies a Doctrine_Query object.
1859      *
1860      * @param Doctrine_Query   Doctrine query instance.
1861      *                         If ommited the instance itself will be used as source.
1862      * @return Doctrine_Query  Copy of the Doctrine_Query instance.
1863      */
1864     public function copy(Doctrine_Query $query = null)
1865     {
1866         if ( ! $query) {
1867             $query = $this;
1868         }
1869
1870         $new = new Doctrine_Query();
1871         $new->_dqlParts = $query->_dqlParts;
1872         $new->_params = $query->_params;
1873         $new->_hydrator = $query->_hydrator;
1874
1875         return $new;
1876     }
1877
1878     /**
1879      * Frees the resources used by the query object. It especially breaks a 
1880      * cyclic reference between the query object and it's parsers. This enables
1881      * PHP's current GC to reclaim the memory.
1882      * This method can therefore be used to reduce memory usage when creating a lot
1883      * of query objects during a request.
1884      *
1885      * @return Doctrine_Query   this object
1886      */
1887     public function free()
1888     {
1889         $this->reset();
1890         $this->_parsers = array();
1891         $this->_dqlParts = array();
1892         $this->_enumParams = array();
1893     }
1894     
1895     /**
1896      * serialize
1897      * this method is automatically called when this Doctrine_Hydrate is serialized
1898      *
1899      * @return array    an array of serialized properties
1900      */
1901     public function serialize()
1902     {
1903         $vars = get_object_vars($this);
1904     }
1905
1906     /**
1907      * unseralize
1908      * this method is automatically called everytime a Doctrine_Hydrate object is unserialized
1909      *
1910      * @param string $serialized                Doctrine_Record as serialized string
1911      * @return void
1912      */
1913     public function unserialize($serialized)
1914     {
1915
1916     }
1917 }