From 60276421e71a3ad218e614bcfc135fde995af4f7 Mon Sep 17 00:00:00 2001 From: zYne Date: Mon, 14 May 2007 23:04:32 +0000 Subject: [PATCH] --- lib/Doctrine/Query/From.php | 168 ++++++++++++- lib/Doctrine/Query/Groupby.php | 5 - lib/Doctrine/Query/Having.php | 11 +- lib/Doctrine/Query/Orderby.php | 4 - lib/Doctrine/Query/Parser.php | 420 +++++++++++++++++++++++++++++++++ lib/Doctrine/Query/Part.php | 41 +--- lib/Doctrine/Query/Select.php | 271 +++++++++++++++++++++ 7 files changed, 865 insertions(+), 55 deletions(-) create mode 100644 lib/Doctrine/Query/Parser.php create mode 100644 lib/Doctrine/Query/Select.php diff --git a/lib/Doctrine/Query/From.php b/lib/Doctrine/Query/From.php index 559e654de..a4a416145 100644 --- a/lib/Doctrine/Query/From.php +++ b/lib/Doctrine/Query/From.php @@ -79,15 +79,175 @@ class Doctrine_Query_From extends Doctrine_Query_Part $reference = array_shift($e) . $operator . implode('.', $e); } - $table = $this->query->load($reference); + $table = $this->query->load($reference); } $operator = ($last == 'INNER') ? ':' : '.'; } } - - public function __toString() + public function load($path, $loadFields = true) { - return ( ! empty($this->parts))?implode(", ", $this->parts):''; + // parse custom join conditions + $e = explode(' ON ', $path); + + $joinCondition = ''; + + if (count($e) > 1) { + $joinCondition = ' AND ' . $e[1]; + $path = $e[0]; + } + + $tmp = explode(' ', $path); + $componentAlias = (count($tmp) > 1) ? end($tmp) : false; + + $e = preg_split("/[.:]/", $tmp[0], -1); + + $fullPath = $tmp[0]; + $prevPath = ''; + $fullLength = strlen($fullPath); + + if (isset($this->_aliasMap[$e[0]])) { + $table = $this->_aliasMap[$e[0]]['table']; + + $prevPath = $parent = array_shift($e); + } + + foreach ($e as $key => $name) { + // get length of the previous path + $length = strlen($prevPath); + + // build the current component path + $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name; + + $delimeter = substr($fullPath, $length, 1); + + // if an alias is not given use the current path as an alias identifier + if (strlen($prevPath) !== $fullLength || ! $componentAlias) { + $componentAlias = $prevPath; + } + + if ( ! isset($table)) { + // process the root of the path + $table = $this->loadRoot($name, $componentAlias); + } else { + + + $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN '; + + $relation = $table->getRelation($name); + $this->_aliasMap[$componentAlias] = array('table' => $relation->getTable(), + 'parent' => $parent, + 'relation' => $relation); + if( ! $relation->isOneToOne()) { + $this->needsSubquery = true; + } + + $localAlias = $this->getShortAlias($parent, $table->getTableName()); + $foreignAlias = $this->getShortAlias($componentAlias, $relation->getTable()->getTableName()); + $localSql = $this->conn->quoteIdentifier($table->getTableName()) . ' ' . $localAlias; + $foreignSql = $this->conn->quoteIdentifier($relation->getTable()->getTableName()) . ' ' . $foreignAlias; + + $map = $relation->getTable()->inheritanceMap; + + if ( ! $loadFields || ! empty($map) || $joinCondition) { + $this->subqueryAliases[] = $foreignAlias; + } + + if ($relation instanceof Doctrine_Relation_Association) { + $asf = $relation->getAssociationFactory(); + + $assocTableName = $asf->getTableName(); + + if( ! $loadFields || ! empty($map) || $joinCondition) { + $this->subqueryAliases[] = $assocTableName; + } + + $assocPath = $prevPath . '.' . $asf->getComponentName(); + + $assocAlias = $this->getShortAlias($assocPath, $asf->getTableName()); + + $queryPart = $join . $assocTableName . ' ' . $assocAlias . ' ON ' . $localAlias . '.' + . $table->getIdentifier() . ' = ' + . $assocAlias . '.' . $relation->getLocal(); + + if ($relation instanceof Doctrine_Relation_Association_Self) { + $queryPart .= ' OR ' . $localAlias . '.' . $table->getIdentifier() . ' = ' + . $assocAlias . '.' . $relation->getForeign(); + } + + $this->parts['from'][] = $queryPart; + + $queryPart = $join . $foreignSql . ' ON ' . $foreignAlias . '.' + . $relation->getTable()->getIdentifier() . ' = ' + . $assocAlias . '.' . $relation->getForeign() + . $joinCondition; + + if ($relation instanceof Doctrine_Relation_Association_Self) { + $queryPart .= ' OR ' . $foreignTable . '.' . $table->getIdentifier() . ' = ' + . $assocAlias . '.' . $relation->getLocal(); + } + + } else { + $queryPart = $join . $foreignSql + . ' ON ' . $localAlias . '.' + . $relation->getLocal() . ' = ' . $foreignAlias . '.' . $relation->getForeign() + . $joinCondition; + } + $this->parts['from'][] = $queryPart; + } + if ($loadFields) { + + $restoreState = false; + // load fields if necessary + if ($loadFields && empty($this->pendingFields)) { + $this->pendingFields[$componentAlias] = array('*'); + + $restoreState = true; + } + + if(isset($this->pendingFields[$componentAlias])) { + $this->processPendingFields($componentAlias); + } + + if ($restoreState) { + $this->pendingFields = array(); + } + + + if(isset($this->pendingAggregates[$componentAlias]) || isset($this->pendingAggregates[0])) { + $this->processPendingAggregates($componentAlias); + } + } + } + } + /** + * loadRoot + * + * @param string $name + * @param string $componentAlias + */ + public function loadRoot($name, $componentAlias) + { + // get the connection for the component + $this->conn = Doctrine_Manager::getInstance() + ->getConnectionForComponent($name); + + $table = $this->conn->getTable($name); + $tableName = $table->getTableName(); + + // get the short alias for this table + $tableAlias = $this->aliasHandler->getShortAlias($componentAlias, $tableName); + // quote table name + $queryPart = $this->conn->quoteIdentifier($tableName); + + if ($this->type === self::SELECT) { + $queryPart .= ' ' . $tableAlias; + } + + $this->parts['from'][] = $queryPart; + $this->tableAliases[$tableAlias] = $componentAlias; + $this->_aliasMap[$componentAlias] = array('table' => $table); + + return $table; } } diff --git a/lib/Doctrine/Query/Groupby.php b/lib/Doctrine/Query/Groupby.php index 5ed7e77cb..5d130e19c 100644 --- a/lib/Doctrine/Query/Groupby.php +++ b/lib/Doctrine/Query/Groupby.php @@ -53,9 +53,4 @@ class Doctrine_Query_Groupby extends Doctrine_Query_Part } return implode(', ', $r); } - - public function __toString() - { - return ( ! empty($this->parts))?implode(", ", $this->parts):''; - } } diff --git a/lib/Doctrine/Query/Having.php b/lib/Doctrine/Query/Having.php index c60451168..d491d6d31 100644 --- a/lib/Doctrine/Query/Having.php +++ b/lib/Doctrine/Query/Having.php @@ -60,7 +60,7 @@ class Doctrine_Query_Having extends Doctrine_Query_Condition } else { if ( ! is_numeric($func)) { $a = explode('.', $func); - + if (count($a) > 1) { $field = array_pop($a); $reference = implode('.', $a); @@ -99,13 +99,4 @@ class Doctrine_Query_Having extends Doctrine_Query_Condition return $r; } - /** - * __toString - * - * @return string - */ - public function __toString() - { - return ( ! empty($this->parts))?implode(' AND ', $this->parts):''; - } } diff --git a/lib/Doctrine/Query/Orderby.php b/lib/Doctrine/Query/Orderby.php index 080bed861..fa363a874 100644 --- a/lib/Doctrine/Query/Orderby.php +++ b/lib/Doctrine/Query/Orderby.php @@ -74,8 +74,4 @@ class Doctrine_Query_Orderby extends Doctrine_Query_Part return implode(', ', $ret); } - public function __toString() - { - return ( ! empty($this->parts))?implode(', ', $this->parts):''; - } } diff --git a/lib/Doctrine/Query/Parser.php b/lib/Doctrine/Query/Parser.php new file mode 100644 index 000000000..5212496ad --- /dev/null +++ b/lib/Doctrine/Query/Parser.php @@ -0,0 +1,420 @@ +. + */ + +/** + * Doctrine_Query_Parser + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision: 1296 $ + * @author Konsta Vesterinen + */ +class Doctrine_Query_Parser +{ + /** + * getQueryBase + * returns the base of the generated sql query + * On mysql driver special strategy has to be used for DELETE statements + * + * @return string the base of the generated sql query + */ + public function getQueryBase() + { + switch ($this->type) { + case self::DELETE: + $q = 'DELETE FROM '; + break; + case self::UPDATE: + $q = 'UPDATE '; + break; + case self::SELECT: + $distinct = ($this->isDistinct()) ? 'DISTINCT ' : ''; + + $q = 'SELECT ' . $distinct . implode(', ', $this->parts['select']) . ' FROM '; + break; + } + return $q; + } + /** + * buildFromPart + * + * @return string + */ + public function buildFromPart() + { + foreach ($this->parts['from'] as $k => $part) { + if ($k === 0) { + $q .= $part; + continue; + } + // preserve LEFT JOINs only if needed + + if (substr($part, 0, 9) === 'LEFT JOIN') { + $e = explode(' ', $part); + + $aliases = array_merge($this->subqueryAliases, + array_keys($this->neededTables)); + + if( ! in_array($e[3], $aliases) && + ! in_array($e[2], $aliases) && + + ! empty($this->pendingFields)) { + continue; + } + + } + + $e = explode(' ON ', $part); + + // we can always be sure that the first join condition exists + $e2 = explode(' AND ', $e[1]); + + $part = $e[0] . ' ON ' . array_shift($e2); + + if ( ! empty($e2)) { + $parser = new Doctrine_Query_JoinCondition($this); + $part .= ' AND ' . $parser->parse(implode(' AND ', $e2)); + } + + $q .= ' ' . $part; + } + } + /** + * builds the sql query from the given parameters and applies things such as + * column aggregation inheritance and limit subqueries if needed + * + * @param array $params an array of prepared statement params (needed only in mysql driver + * when limit subquery algorithm is used) + * @return string the built sql query + */ + public function getQuery($params = array()) + { + if (empty($this->parts['select']) || empty($this->parts['from'])) { + return false; + } + + $needsSubQuery = false; + $subquery = ''; + $k = array_keys($this->_aliasMap); + $table = $this->_aliasMap[$k[0]]['table']; + + if( ! empty($this->parts['limit']) && $this->needsSubquery && $table->getAttribute(Doctrine::ATTR_QUERY_LIMIT) == Doctrine::LIMIT_RECORDS) { + $needsSubQuery = true; + $this->limitSubqueryUsed = true; + } + + // process all pending SELECT part subqueries + $this->processPendingSubqueries(); + + // build the basic query + + $str = ''; + if($this->isDistinct()) { + $str = 'DISTINCT '; + } + + $q = $this->getQueryBase(); + $q .= $this->buildFrom(); + + if ( ! empty($this->parts['set'])) { + $q .= ' SET ' . implode(', ', $this->parts['set']); + } + + $string = $this->applyInheritance(); + + if ( ! empty($string)) { + $this->parts['where'][] = '(' . $string . ')'; + } + + + $modifyLimit = true; + if ( ! empty($this->parts["limit"]) || ! empty($this->parts["offset"])) { + + if($needsSubQuery) { + $subquery = $this->getLimitSubquery(); + + + switch(strtolower($this->conn->getName())) { + case 'mysql': + // mysql doesn't support LIMIT in subqueries + $list = $this->conn->execute($subquery, $params)->fetchAll(PDO::FETCH_COLUMN); + $subquery = implode(', ', $list); + break; + case 'pgsql': + // pgsql needs special nested LIMIT subquery + $subquery = 'SELECT doctrine_subquery_alias.' . $table->getIdentifier(). ' FROM (' . $subquery . ') AS doctrine_subquery_alias'; + break; + } + + $field = $this->aliasHandler->getShortAlias($table->getTableName()) . '.' . $table->getIdentifier(); + + // only append the subquery if it actually contains something + if($subquery !== '') { + array_unshift($this->parts['where'], $field. ' IN (' . $subquery . ')'); + } + + $modifyLimit = false; + } + } + + $q .= ( ! empty($this->parts['where']))? ' WHERE ' . implode(' AND ', $this->parts['where']):''; + $q .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']):''; + $q .= ( ! empty($this->parts['having']))? ' HAVING ' . implode(' AND ', $this->parts['having']):''; + $q .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby']):''; + + if ($modifyLimit) { + $q = $this->conn->modifyLimitQuery($q, $this->parts['limit'], $this->parts['offset']); + } + + // return to the previous state + if ( ! empty($string)) { + array_pop($this->parts['where']); + } + if ($needsSubQuery) { + array_shift($this->parts['where']); + } + return $q; + } + /** + * getLimitSubquery + * this is method is used by the record limit algorithm + * + * when fetching one-to-many, many-to-many associated data with LIMIT clause + * an additional subquery is needed for limiting the number of returned records instead + * of limiting the number of sql result set rows + * + * @return string the limit subquery + */ + public function getLimitSubquery() + { + $k = array_keys($this->tables); + $table = $this->tables[$k[0]]; + + // get short alias + $alias = $this->aliasHandler->getShortAlias($table->getTableName()); + $primaryKey = $alias . '.' . $table->getIdentifier(); + + // initialize the base of the subquery + $subquery = 'SELECT DISTINCT ' . $primaryKey; + + if ($this->conn->getDBH()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { + // pgsql needs the order by fields to be preserved in select clause + + foreach ($this->parts['orderby'] as $part) { + $e = explode(' ', $part); + + // don't add primarykey column (its already in the select clause) + if ($e[0] !== $primaryKey) { + $subquery .= ', ' . $e[0]; + } + } + } + + $subquery .= ' FROM ' . $this->conn->quoteIdentifier($table->getTableName()) . ' ' . $alias; + + foreach ($this->parts['join'] as $parts) { + foreach ($parts as $part) { + // preserve LEFT JOINs only if needed + if (substr($part,0,9) === 'LEFT JOIN') { + $e = explode(' ', $part); + + if ( ! in_array($e[3], $this->subqueryAliases) && + ! in_array($e[2], $this->subqueryAliases)) { + continue; + } + + } + + $subquery .= ' '.$part; + } + } + + // all conditions must be preserved in subquery + $subquery .= ( ! empty($this->parts['where']))? ' WHERE ' . implode(' AND ', $this->parts['where']) : ''; + $subquery .= ( ! empty($this->parts['groupby']))? ' GROUP BY ' . implode(', ', $this->parts['groupby']) : ''; + $subquery .= ( ! empty($this->parts['having']))? ' HAVING ' . implode(' AND ', $this->parts['having']) : ''; + $subquery .= ( ! empty($this->parts['orderby']))? ' ORDER BY ' . implode(', ', $this->parts['orderby']) : ''; + + // add driver specific limit clause + $subquery = $this->conn->modifyLimitQuery($subquery, $this->parts['limit'], $this->parts['offset']); + + $parts = self::quoteExplode($subquery, ' ', "'", "'"); + + foreach($parts as $k => $part) { + if(strpos($part, "'") !== false) { + continue; + } + + if($this->aliasHandler->hasAliasFor($part)) { + $parts[$k] = $this->aliasHandler->generateNewAlias($part); + } + + if(strpos($part, '.') !== false) { + $e = explode('.', $part); + + $trimmed = ltrim($e[0], '( '); + $pos = strpos($e[0], $trimmed); + + $e[0] = substr($e[0], 0, $pos) . $this->aliasHandler->generateNewAlias($trimmed); + $parts[$k] = implode('.', $e); + } + } + $subquery = implode(' ', $parts); + + return $subquery; + } + /** + * tokenizeQuery + * splits the given dql query into an array where keys + * represent different query part names and values are + * arrays splitted using sqlExplode method + * + * example: + * + * parameter: + * $query = "SELECT u.* FROM User u WHERE u.name LIKE ?" + * returns: + * array('select' => array('u.*'), + * 'from' => array('User', 'u'), + * 'where' => array('u.name', 'LIKE', '?')) + * + * @param string $query DQL query + * @throws Doctrine_Query_Exception if some generic parsing error occurs + * @return array an array containing the query string parts + */ + public function tokenizeQuery($query) + { + $e = Doctrine_Tokenizer::sqlExplode($query, ' '); + + foreach($e as $k=>$part) { + $part = trim($part); + switch(strtolower($part)) { + case 'delete': + case 'update': + case 'select': + case 'set': + case 'from': + case 'where': + case 'limit': + case 'offset': + case 'having': + $p = $part; + $parts[$part] = array(); + break; + case 'order': + case 'group': + $i = ($k + 1); + if(isset($e[$i]) && strtolower($e[$i]) === "by") { + $p = $part; + $parts[$part] = array(); + } else + $parts[$p][] = $part; + break; + case "by": + continue; + default: + if( ! isset($p)) + throw new Doctrine_Query_Exception("Couldn't parse query."); + + $parts[$p][] = $part; + } + } + return $parts; + } + /** + * DQL PARSER + * parses a DQL query + * first splits the query in parts and then uses individual + * parsers for each part + * + * @param string $query DQL query + * @param boolean $clear whether or not to clear the aliases + * @throws Doctrine_Query_Exception if some generic parsing error occurs + * @return Doctrine_Query + */ + public function parseQuery($query, $clear = true) + { + if ($clear) { + $this->clear(); + } + + $query = trim($query); + $query = str_replace("\n", ' ', $query); + $query = str_replace("\r", ' ', $query); + + $parts = $this->tokenizeQuery($query); + + foreach($parts as $k => $part) { + $part = implode(' ', $part); + switch(strtolower($k)) { + case 'create': + $this->type = self::CREATE; + break; + case 'insert': + $this->type = self::INSERT; + break; + case 'delete': + $this->type = self::DELETE; + break; + case 'select': + $this->type = self::SELECT; + $this->parseSelect($part); + break; + case 'update': + $this->type = self::UPDATE; + $k = 'FROM'; + + case 'from': + $class = 'Doctrine_Query_' . ucwords(strtolower($k)); + $parser = new $class($this); + $parser->parse($part); + break; + case 'set': + $class = 'Doctrine_Query_' . ucwords(strtolower($k)); + $parser = new $class($this); + $this->parts['set'][] = $parser->parse($part); + break; + case 'group': + case 'order': + $k .= 'by'; + case 'where': + case 'having': + $class = 'Doctrine_Query_' . ucwords(strtolower($k)); + $parser = new $class($this); + + $name = strtolower($k); + $this->parts[$name][] = $parser->parse($part); + break; + case 'limit': + $this->parts['limit'] = trim($part); + break; + case 'offset': + $this->parts['offset'] = trim($part); + break; + } + } + + return $this; + } +} diff --git a/lib/Doctrine/Query/Part.php b/lib/Doctrine/Query/Part.php index 761cce571..e1b57c389 100644 --- a/lib/Doctrine/Query/Part.php +++ b/lib/Doctrine/Query/Part.php @@ -18,7 +18,7 @@ * and is licensed under the LGPL. For more information, see * . */ -Doctrine::autoload("Doctrine_Access"); + /** * Doctrine_Query_Part * @@ -30,34 +30,19 @@ Doctrine::autoload("Doctrine_Access"); * @version $Revision$ * @author Konsta Vesterinen */ -abstract class Doctrine_Query_Part extends Doctrine_Access +abstract class Doctrine_Query_Part { /** * @var Doctrine_Query $query the query object associated with this parser */ protected $query; - /** - * @var string $name the name of this parser - */ - protected $name; - /** - * @var array $parts - */ - protected $parts = array(); /** * @param Doctrine_Query $query the query object associated with this parser */ - public function __construct(Doctrine_Query $query) + public function __construct($query) { $this->query = $query; } - /** - * @return string $name the name of this parser - */ - public function getName() - { - return $this->name; - } /** * @return Doctrine_Query $query the query object associated with this parser */ @@ -65,20 +50,12 @@ abstract class Doctrine_Query_Part extends Doctrine_Access { return $this->query; } - /** - * add - * - * @param string $value - * @return void - */ - public function add($value) + public function parse($dql, $append = false) { - $method = "parse".$this->name; - $this->query->$method($value); - } + $e = explode(' ', __CLASS__); + $name = end($e); - public function get($name) - { } - public function set($name, $value) - { } + $this->query->addDqlPart($name, $dql); + $this->_parse($dql); + } } diff --git a/lib/Doctrine/Query/Select.php b/lib/Doctrine/Query/Select.php new file mode 100644 index 000000000..ff228f46a --- /dev/null +++ b/lib/Doctrine/Query/Select.php @@ -0,0 +1,271 @@ +. + */ +Doctrine::autoload("Doctrine_Query_Part"); +/** + * Doctrine_Query_Select + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision: 1080 $ + * @author Konsta Vesterinen + */ +class Doctrine_Query_Select extends Doctrine_Query_Part +{ + /** + * processPendingFields + * the fields in SELECT clause cannot be parsed until the components + * in FROM clause are parsed, hence this method is called everytime a + * specific component is being parsed. + * + * @throws Doctrine_Query_Exception if unknown component alias has been given + * @param string $componentAlias the alias of the component + * @return void + */ + public function processPendingFields($componentAlias) + { + $tableAlias = $this->getTableAlias($componentAlias); + $table = $this->_aliasMap[$componentAlias]['table']; + + if (isset($this->pendingFields[$componentAlias])) { + $fields = $this->pendingFields[$componentAlias]; + + // check for wildcards + if (in_array('*', $fields)) { + $fields = $table->getColumnNames(); + } else { + // only auto-add the primary key fields if this query object is not + // a subquery of another query object + if ( ! $this->isSubquery) { + $fields = array_unique(array_merge($table->getPrimaryKeys(), $fields)); + } + } + } + foreach ($fields as $name) { + $name = $table->getColumnName($name); + + $this->parts['select'][] = $tableAlias . '.' .$name . ' AS ' . $tableAlias . '__' . $name; + } + + $this->neededTables[] = $tableAlias; + + } + /** + * parseSelect + * parses the query select part and + * adds selected fields to pendingFields array + * + * @param string $dql + */ + public function parseSelect($dql) + { + $refs = Doctrine_Query::bracketExplode($dql, ','); + + foreach ($refs as $reference) { + if (strpos($reference, '(') !== false) { + if (substr($reference, 0, 1) === '(') { + // subselect found in SELECT part + $this->parseSubselect($reference); + } else { + $this->parseAggregateFunction2($reference); + } + } else { + + $e = explode('.', $reference); + if (count($e) > 2) { + $this->pendingFields[] = $reference; + } else { + $this->pendingFields[$e[0]][] = $e[1]; + } + } + } + } + /** + * parseSubselect + * + * parses the subquery found in DQL SELECT part and adds the + * parsed form into $pendingSubqueries stack + * + * @param string $reference + * @return void + */ + public function parseSubselect($reference) + { + $e = Doctrine_Query::bracketExplode($reference, ' '); + $alias = $e[1]; + + if (count($e) > 2) { + if (strtoupper($e[1]) !== 'AS') { + throw new Doctrine_Query_Exception('Syntax error near: ' . $reference); + } + $alias = $e[2]; + } + + $subquery = substr($e[0], 1, -1); + + $this->pendingSubqueries[] = array($subquery, $alias); + } + public function parseAggregateFunction2($func) + { + $e = Doctrine_Query::bracketExplode($func, ' '); + $func = $e[0]; + + $pos = strpos($func, '('); + $name = substr($func, 0, $pos); + + try { + $argStr = substr($func, ($pos + 1), -1); + $args = explode(',', $argStr); + + $func = call_user_func_array(array($this->conn->expression, $name), $args); + + if(substr($func, 0, 1) !== '(') { + $pos = strpos($func, '('); + $name = substr($func, 0, $pos); + } else { + $name = $func; + } + + $e2 = explode(' ', $args[0]); + + $distinct = ''; + if(count($e2) > 1) { + if(strtoupper($e2[0]) == 'DISTINCT') + $distinct = 'DISTINCT '; + + $args[0] = $e2[1]; + } + + + + $parts = explode('.', $args[0]); + $owner = $parts[0]; + $alias = (isset($e[1])) ? $e[1] : $name; + + $e3 = explode('.', $alias); + + if(count($e3) > 1) { + $alias = $e3[1]; + $owner = $e3[0]; + } + + // a function without parameters eg. RANDOM() + if ($owner === '') { + $owner = 0; + } + + $this->pendingAggregates[$owner][] = array($name, $args, $distinct, $alias); + } catch(Doctrine_Expression_Exception $e) { + throw new Doctrine_Query_Exception('Unknown function ' . $func . '.'); + } + } + public function processPendingSubqueries() + { + if ($this->subqueriesProcessed === true) { + return false; + } + + foreach ($this->pendingSubqueries as $value) { + list($dql, $alias) = $value; + + $sql = $this->createSubquery()->parseQuery($dql, false)->getQuery(); + + reset($this->tableAliases); + + $tableAlias = current($this->tableAliases); + + reset($this->compAliases); + + $componentAlias = key($this->compAliases); + + $sqlAlias = $tableAlias . '__' . count($this->aggregateMap); + + $this->parts['select'][] = '(' . $sql . ') AS ' . $sqlAlias; + + $this->aggregateMap[$alias] = $sqlAlias; + $this->subqueryAggregates[$componentAlias][] = $alias; + } + $this->subqueriesProcessed = true; + + return true; + } + public function processPendingAggregates($componentAlias) + { + $tableAlias = $this->getTableAlias($componentAlias); + + if ( ! isset($this->tables[$tableAlias])) { + throw new Doctrine_Query_Exception('Unknown component path ' . $componentAlias); + } + + $root = current($this->tables); + $table = $this->tables[$tableAlias]; + $aggregates = array(); + + if(isset($this->pendingAggregates[$componentAlias])) { + $aggregates = $this->pendingAggregates[$componentAlias]; + } + + if ($root === $table) { + if (isset($this->pendingAggregates[0])) { + $aggregates += $this->pendingAggregates[0]; + } + } + + foreach($aggregates as $parts) { + list($name, $args, $distinct, $alias) = $parts; + + $arglist = array(); + foreach($args as $arg) { + $e = explode('.', $arg); + + + if (is_numeric($arg)) { + $arglist[] = $arg; + } elseif (count($e) > 1) { + //$tableAlias = $this->getTableAlias($e[0]); + $table = $this->tables[$tableAlias]; + + $e[1] = $table->getColumnName($e[1]); + + if( ! $table->hasColumn($e[1])) { + throw new Doctrine_Query_Exception('Unknown column ' . $e[1]); + } + + $arglist[] = $tableAlias . '.' . $e[1]; + } else { + $arglist[] = $e[0]; + } + } + + $sqlAlias = $tableAlias . '__' . count($this->aggregateMap); + + if(substr($name, 0, 1) !== '(') { + $this->parts['select'][] = $name . '(' . $distinct . implode(', ', $arglist) . ') AS ' . $sqlAlias; + } else { + $this->parts['select'][] = $name . ' AS ' . $sqlAlias; + } + $this->aggregateMap[$alias] = $sqlAlias; + $this->neededTables[] = $tableAlias; + } + } +}