From eaea971fe4fbb43458dc6280a4f056bcc06d9b32 Mon Sep 17 00:00:00 2001 From: zYne Date: Sat, 14 Apr 2007 16:28:09 +0000 Subject: [PATCH] fixes #307 --- lib/Doctrine/Db/Statement.php | 2 +- lib/Doctrine/Hydrate.php | 33 ++++++++---- lib/Doctrine/Query.php | 87 +++++++++++++++++++++++++++++--- lib/Doctrine/Query/Where.php | 2 +- tests/Query/SubqueryTestCase.php | 19 ++++--- 5 files changed, 117 insertions(+), 26 deletions(-) diff --git a/lib/Doctrine/Db/Statement.php b/lib/Doctrine/Db/Statement.php index 20c03e77a..3b61543da 100644 --- a/lib/Doctrine/Db/Statement.php +++ b/lib/Doctrine/Db/Statement.php @@ -197,7 +197,7 @@ class Doctrine_Db_Statement implements Doctrine_Adapter_Statement_Interface public function execute($params = null) { $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::EXECUTE, $this->stmt->queryString, $params); - + //print $this->stmt->queryString . print_r($params, true) . "
"; $skip = $this->adapter->getListener()->onPreExecute($event); if ( ! $skip) { diff --git a/lib/Doctrine/Hydrate.php b/lib/Doctrine/Hydrate.php index f1619cbd6..ad04886cf 100644 --- a/lib/Doctrine/Hydrate.php +++ b/lib/Doctrine/Hydrate.php @@ -109,6 +109,8 @@ abstract class Doctrine_Hydrate extends Doctrine_Access protected $tableIndexes = array(); protected $pendingAggregates = array(); + + protected $subqueryAggregates = array(); /** * @var array $aggregateMap an array containing all aggregate aliases, keys as dql aliases * and values as sql aliases @@ -403,9 +405,9 @@ abstract class Doctrine_Hydrate extends Doctrine_Access $query = $this->view->getSelectSql(); } - if ($this->isLimitSubqueryUsed() - && $this->conn->getDBH()->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'mysql' - ) { + if ($this->isLimitSubqueryUsed() && + $this->conn->getDBH()->getAttribute(Doctrine::ATTR_DRIVER_NAME) !== 'mysql') { + $params = array_merge($params, $params); } $stmt = $this->conn->execute($query, $params); @@ -415,8 +417,9 @@ abstract class Doctrine_Hydrate extends Doctrine_Access } if (count($this->tables) == 0) { - throw new Doctrine_Query_Exception("No components selected"); + throw new Doctrine_Query_Exception('No components selected'); } + $keys = array_keys($this->tables); $root = $keys[0]; @@ -425,13 +428,15 @@ abstract class Doctrine_Hydrate extends Doctrine_Access $coll = $this->getCollection($root); $prev[$root] = $coll; - if ($this->aggregate) + if ($this->aggregate) { $return = Doctrine::FETCH_ARRAY; + } $array = $this->parseData($stmt); - if ($return == Doctrine::FETCH_ARRAY) + if ($return == Doctrine::FETCH_ARRAY) { return $array; + } foreach ($array as $data) { /** @@ -482,6 +487,8 @@ abstract class Doctrine_Hydrate extends Doctrine_Access if (isset($this->pendingAggregates[$alias][$index])) { $agg = $this->pendingAggregates[$alias][$index][3]; + } elseif (isset($this->subqueryAggregates[$alias][$index])) { + $agg = $this->subqueryAggregates[$alias][$index]; } $record->mapValue($agg, $value); @@ -494,13 +501,14 @@ abstract class Doctrine_Hydrate extends Doctrine_Access if ( ! isset($previd[$name])) { $previd[$name] = array(); - } + } if ($previd[$name] !== $row) { // set internal data $this->tables[$name]->setData($row); // initialize a new record + $record = $this->tables[$name]->getRecord(); // aggregate values have numeric keys @@ -514,6 +522,8 @@ abstract class Doctrine_Hydrate extends Doctrine_Access if (isset($this->pendingAggregates[$alias][$index])) { $agg = $this->pendingAggregates[$alias][$index][3]; + } elseif (isset($this->subqueryAggregates[$alias][$index])) { + $agg = $this->subqueryAggregates[$alias][$index]; } $record->mapValue($agg, $value); } @@ -521,20 +531,21 @@ abstract class Doctrine_Hydrate extends Doctrine_Access if ($name == $root) { // add record into root collection + $coll->add($record); unset($previd); } else { - - $prev = $this->addRelated($prev, $name, $record); + $prev = $this->addRelated($prev, $name, $record); } // following statement is needed to ensure that mappings // are being done properly when the result set doesn't // contain the rows in 'right order' - if ($prev[$name] !== $record) + if ($prev[$name] !== $record) { $prev[$name] = $record; + } } $previd[$name] = $row; @@ -682,7 +693,7 @@ abstract class Doctrine_Hydrate extends Doctrine_Access } else { $tableAlias .= '.'; } - + foreach ($maps as $map) { $b = array(); foreach ($map as $field => $value) { diff --git a/lib/Doctrine/Query.php b/lib/Doctrine/Query.php index aa7c13b7f..6e99257e7 100644 --- a/lib/Doctrine/Query.php +++ b/lib/Doctrine/Query.php @@ -62,6 +62,17 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { * @var array $pendingFields */ private $pendingFields = array(); + /** + * @var array $pendingSubqueries SELECT part subqueries, these are called pending subqueries since + * they cannot be parsed directly (some queries might be correlated) + */ + private $pendingSubqueries = array(); + /** + * @var boolean $subqueriesProcessed Whether or not pending subqueries have already been processed. + * Consequent calls to getQuery would result badly constructed queries + * without this variable + */ + private $subqueriesProcessed = false; @@ -142,7 +153,7 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { if (isset($this->pendingFields[$componentAlias])) { $fields = $this->pendingFields[$componentAlias]; - if(in_array('*', $fields)) { + if (in_array('*', $fields)) { $fields = $table->getColumnNames(); } else { // only auto-add the primary key fields if this query object is not @@ -155,7 +166,7 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { foreach ($fields as $name) { $name = $table->getColumnName($name); - $this->parts["select"][] = $tableAlias . '.' .$name . ' AS ' . $tableAlias . '__' . $name; + $this->parts['select'][] = $tableAlias . '.' .$name . ' AS ' . $tableAlias . '__' . $name; } $this->neededTables[] = $tableAlias; @@ -172,9 +183,14 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { { $refs = Doctrine_Query::bracketExplode($dql, ','); - foreach($refs as $reference) { - if(strpos($reference, '(') !== false) { - $this->parseAggregateFunction2($reference); + 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); @@ -186,6 +202,31 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { } } } + /** + * 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, ' '); @@ -242,9 +283,39 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { throw new Doctrine_Query_Exception('Unknown function '.$name); } } + 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); + $tableAlias = $this->getTableAlias($componentAlias); if ( ! isset($this->tables[$tableAlias])) { throw new Doctrine_Query_Exception('Unknown component path ' . $componentAlias); @@ -670,6 +741,9 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { $this->limitSubqueryUsed = true; } + // process all pending SELECT part subqueries + $this->processPendingSubqueries(); + // build the basic query $str = ''; @@ -1333,7 +1407,6 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { $this->tableAliases[$currPath] = $tname; $tableName = $tname; - } else { $index += strlen($e[($key - 1)]) + 1; diff --git a/lib/Doctrine/Query/Where.php b/lib/Doctrine/Query/Where.php index 370301726..735a2326d 100644 --- a/lib/Doctrine/Query/Where.php +++ b/lib/Doctrine/Query/Where.php @@ -203,7 +203,7 @@ class Doctrine_Query_Where extends Doctrine_Query_Condition $sub = Doctrine_Query::bracketTrim(substr($where, $pos)); - return $operator . ' ('.$this->query->createSubquery()->parseQuery($sub, false)->getQuery() . ')'; + return $operator . ' (' . $this->query->createSubquery()->parseQuery($sub, false)->getQuery() . ')'; } /** * getOperator diff --git a/tests/Query/SubqueryTestCase.php b/tests/Query/SubqueryTestCase.php index 0e9f5e490..262e695b7 100644 --- a/tests/Query/SubqueryTestCase.php +++ b/tests/Query/SubqueryTestCase.php @@ -33,7 +33,8 @@ */ class Doctrine_Query_Subquery_TestCase extends Doctrine_UnitTestCase { - public function testSubqueryWithWherePartAndInExpression() + + public function testSubqueryWithWherePartAndInExpression() { $q = new Doctrine_Query(); $q->from('User')->where("User.id NOT IN (FROM User(id) WHERE User.name = 'zYne')"); @@ -46,11 +47,11 @@ class Doctrine_Query_Subquery_TestCase extends Doctrine_UnitTestCase $this->assertEqual($users->count(), 7); $this->assertEqual($users[0]->name, 'Arnold Schwarzenegger'); } - public function testSubqueryAllowsSelectingOfAnyField() + public function testSubqueryAllowsSelectingOfAnyField() { $q = new Doctrine_Query(); $q->from('User u')->where('u.id NOT IN (SELECT g.user_id FROM Groupuser g)'); - + $this->assertEqual($q->getQuery(), "SELECT e.id AS e__id, e.name AS e__name, e.loginname AS e__loginname, e.password AS e__password, e.type AS e__type, e.created AS e__created, e.updated AS e__updated, e.email_id AS e__email_id FROM entity e WHERE e.id NOT IN (SELECT g.user_id AS g__user_id FROM groupuser g) AND (e.type = 0)"); } @@ -58,13 +59,19 @@ class Doctrine_Query_Subquery_TestCase extends Doctrine_UnitTestCase { // ticket #307 $q = new Doctrine_Query(); - /*$q->query("SELECT u.*, (SELECT p.name FROM User p WHERE p.name = u.name) name2 FROM User u WHERE u.name = 'zYne' LIMIT 1"); + + $q->parseQuery("SELECT u.name, (SELECT COUNT(p.id) FROM Phonenumber p WHERE p.entity_id = u.id) pcount FROM User u WHERE u.name = 'zYne' LIMIT 1"); + + $this->assertEqual($q->getQuery(), "SELECT e.id AS e__id, e.name AS e__name, (SELECT COUNT(p.id) AS p__0 FROM phonenumber p WHERE p.entity_id = e.id) AS e__0 FROM entity e WHERE e.name = 'zYne' AND (e.type = 0) LIMIT 1"); + // test consequent call + $this->assertEqual($q->getQuery(), "SELECT e.id AS e__id, e.name AS e__name, (SELECT COUNT(p.id) AS p__0 FROM phonenumber p WHERE p.entity_id = e.id) AS e__0 FROM entity e WHERE e.name = 'zYne' AND (e.type = 0) LIMIT 1"); $users = $q->execute(); $this->assertEqual($users->count(), 1); - */ - $this->fail("Subquery support in select part. Ticket #307."); + + $this->assertEqual($users[0]->name, 'zYne'); + $this->assertEqual($users[0]->pcount, 1); } } ?>