Coverage for Doctrine_Query

Back to coverage report

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