From bfcfa9f2f20a722edb31c3ad1e2c772661f9d745 Mon Sep 17 00:00:00 2001 From: zYne Date: Tue, 17 Oct 2006 17:21:21 +0000 Subject: [PATCH] Aggregate function support added --- lib/Doctrine/Collection.php | 5 +- lib/Doctrine/Hydrate.php | 42 ++++++++- lib/Doctrine/Query.php | 167 ++++++++++++++++++++++++++-------- lib/Doctrine/Table.php | 8 +- tests/QuerySelectTestCase.php | 69 +++++++++++++- tests/run.php | 5 +- 6 files changed, 241 insertions(+), 55 deletions(-) diff --git a/lib/Doctrine/Collection.php b/lib/Doctrine/Collection.php index 1c5f4eabe..efe0fa239 100644 --- a/lib/Doctrine/Collection.php +++ b/lib/Doctrine/Collection.php @@ -64,6 +64,9 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator * @var Doctrine_Null $null used for extremely fast null value testing */ protected static $null; + + + protected $aggregateValues = array(); /** * constructor @@ -117,7 +120,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator * @param string $name * @return mixed */ - public function getAggregateValue() { + public function getAggregateValue($name) { return $this->aggregateValues[$name]; } /** diff --git a/lib/Doctrine/Hydrate.php b/lib/Doctrine/Hydrate.php index 2481b7e6e..3fb7c85d1 100644 --- a/lib/Doctrine/Hydrate.php +++ b/lib/Doctrine/Hydrate.php @@ -82,6 +82,12 @@ abstract class Doctrine_Hydrate extends Doctrine_Access { * @var array $tableIndexes */ protected $tableIndexes = array(); + + protected $components = array(); + + protected $pendingAggregates = array(); + + protected $aggregateMap = array(); /** * @var array $parts SQL query string parts */ @@ -143,6 +149,14 @@ abstract class Doctrine_Hydrate extends Doctrine_Access { return $this; } + + public function getPathAlias($path) { + $s = array_search($path, $this->compAliases); + if($s === false) + return $path; + + return $s; + } /** * createSubquery * @@ -264,6 +278,9 @@ abstract class Doctrine_Hydrate extends Doctrine_Access { */ private function getCollection($name) { $table = $this->tables[$name]; + if( ! isset($this->fetchModes[$name])) + return new Doctrine_Collection($table); + switch($this->fetchModes[$name]): case Doctrine::FETCH_BATCH: $coll = new Doctrine_Collection_Batch($table); @@ -344,7 +361,6 @@ abstract class Doctrine_Hydrate extends Doctrine_Access { if($return == Doctrine::FETCH_ARRAY) return $array; - foreach($array as $data) { @@ -355,12 +371,30 @@ abstract class Doctrine_Hydrate extends Doctrine_Access { if(empty($row)) continue; + $ids = $this->tables[$key]->getIdentifier(); $name = $key; if($this->isIdentifiable($row, $ids)) { $prev = $this->initRelated($prev, $name); + // aggregate values have numeric keys + + if(isset($row[0])) { + $path = array_search($name, $this->tableAliases); + $alias = $this->getPathAlias($path); + + + foreach($row as $index => $value) { + $agg = false; + + if(isset($this->pendingAggregates[$alias][$index])) + $agg = $this->pendingAggregates[$alias][$index][0]; + + $prev[$name]->setAggregateValue($agg, $value); + } + + } continue; } @@ -384,7 +418,8 @@ abstract class Doctrine_Hydrate extends Doctrine_Access { unset($previd); } else { - $prev = $this->addRelated($prev, $name, $record); + + $prev = $this->addRelated($prev, $name, $record); } // following statement is needed to ensure that mappings @@ -476,7 +511,6 @@ abstract class Doctrine_Hydrate extends Doctrine_Access { * @return boolean */ public function isIdentifiable(array $row, $ids) { - $emptyID = false; if(is_array($ids)) { foreach($ids as $id) { if($row[$id] == null) @@ -544,10 +578,8 @@ abstract class Doctrine_Hydrate extends Doctrine_Access { $e = explode("__",$key); $field = strtolower( array_pop($e) ); - $component = strtolower( implode("__",$e) ); - $data[$component][$field] = $value; unset($data[$key]); diff --git a/lib/Doctrine/Query.php b/lib/Doctrine/Query.php index 131773f92..aa4afe73a 100644 --- a/lib/Doctrine/Query.php +++ b/lib/Doctrine/Query.php @@ -45,6 +45,9 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { private $relationStack = array(); private $isDistinct = false; + + private $pendingFields = array(); + /** * create * returns a new Doctrine_Query object @@ -69,21 +72,97 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { return $this->isDistinct; } - /** - * count - * - * @return integer - */ - public function count(Doctrine_Table $table, $params = array()) { - $this->remove('select'); - $join = $this->join; - $where = $this->where; - $having = $this->having; - $q = "SELECT COUNT(DISTINCT ".$table->getTableName().'.'.$table->getIdentifier().") FROM ".$table->getTableName()." "; - foreach($join as $j) { - $q .= " ".implode(" ",$j); + public function processPendingFields($componentAlias) { + $tableAlias = $this->getTableAlias($componentAlias); + + $componentPath = $this->compAliases[$componentAlias]; + + if( ! isset($this->components[$componentPath])) + throw new Doctrine_Query_Exception('Unknown component path '.$componentPath); + + $table = $this->components[$componentPath]; + + if(isset($this->pendingFields[$componentAlias])) { + $fields = $this->pendingFields[$componentAlias]; + + if(in_array('*', $fields)) + $fields = $table->getColumnNames(); + else + $fields = array_unique(array_merge($table->getPrimaryKeys(), $fields)); } + foreach($fields as $name) { + $this->parts["select"][] = $tableAlias . '.' .$name . ' AS ' . $tableAlias . '__' . $name; + } + + } + public function parseSelect($dql) { + $refs = Doctrine_Query::bracketExplode($dql, ','); + + foreach($refs as $reference) { + if(strpos($reference, '(') !== false) { + $this->parseAggregateFunction2($reference); + } else { + + $e = explode('.', $reference); + if(count($e) > 2) + $this->pendingFields[] = $reference; + else + $this->pendingFields[$e[0]][] = $e[1]; + } + } + } + public function parseAggregateFunction2($func) { + $pos = strpos($func, '('); + $name = substr($func, 0, $pos); + switch($name) { + case 'MAX': + case 'MIN': + case 'COUNT': + case 'AVG': + $reference = substr($func, ($pos + 1), -1); + + $e = explode('.', $reference); + + $this->pendingAggregates[$e[0]][] = array($name, $e[1]); + break; + default: + throw new Doctrine_Query_Exception('Unknown aggregate function '.$name); + } + } + public function processPendingAggregates($componentAlias) { + $tableAlias = $this->getTableAlias($componentAlias); + + $componentPath = $this->compAliases[$componentAlias]; + + if( ! isset($this->components[$componentPath])) + throw new Doctrine_Query_Exception('Unknown component path '.$componentPath); + + $table = $this->components[$componentPath]; + + foreach($this->pendingAggregates[$componentAlias] as $args) { + list($name, $arg) = $args; + + $this->parts["select"][] = $name . '(' . $tableAlias . '.' . $arg . ') AS ' . $tableAlias . '__' . count($this->aggregateMap); + + $this->aggregateMap[] = $table; + } + } + /** + * count + * + * @return integer + */ + public function count(Doctrine_Table $table, $params = array()) { + $this->remove('select'); + $join = $this->join; + $where = $this->where; + $having = $this->having; + + $q = "SELECT COUNT(DISTINCT ".$table->getTableName().'.'.$table->getIdentifier().") FROM ".$table->getTableName()." "; + foreach($join as $j) { + $q .= implode(" ",$j); + } $string = $this->applyInheritance(); if( ! empty($where)) { @@ -94,13 +173,13 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { if( ! empty($string)) $q .= " WHERE (".$string.")"; } + + if( ! empty($having)) + $q .= " HAVING ".implode(' AND ',$having); - if( ! empty($having)) - $q .= " HAVING ".implode(' AND ',$having); - - $a = $this->getConnection()->execute($q, $params)->fetch(PDO::FETCH_NUM); - return $a[0]; - } + $a = $this->getConnection()->execute($q, $params)->fetch(PDO::FETCH_NUM); + return $a[0]; + } /** * loadFields * loads fields for a given table and @@ -203,7 +282,7 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { $class = "Doctrine_Query_".ucwords($name); $parser = new $class($this); - + $parser->parse($args[0]); break; case "where": @@ -697,10 +776,6 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { foreach($e as $key => $fullname) { try { - $copy = $e; - - - $e2 = preg_split("/[-(]/",$fullname); $name = $e2[0]; @@ -751,7 +826,7 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { else $aliasString = $original; - switch($mark): + switch($mark) { case ":": $join = 'INNER JOIN '; break; @@ -760,23 +835,17 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { break; default: throw new Doctrine_Exception("Unknown operator '$mark'"); - endswitch; + } - if($fk->getType() == Doctrine_Relation::MANY_AGGREGATE || - $fk->getType() == Doctrine_Relation::MANY_COMPOSITE) { + if( ! $fk->isOneToOne()) { if( ! $loadFields) { $this->subqueryAliases[] = $tname2; } $this->needsSubquery = true; } - - if($fk instanceof Doctrine_Relation_ForeignKey || - $fk instanceof Doctrine_Relation_LocalKey) { - - $this->parts["join"][$tname][$tname2] = $join.$aliasString." ON ".$tname.".".$fk->getLocal()." = ".$tname2.".".$fk->getForeign(); - - } elseif($fk instanceof Doctrine_Relation_Association) { + + if($fk instanceof Doctrine_Relation_Association) { $asf = $fk->getAssociationFactory(); $assocTableName = $asf->getTableName(); @@ -784,8 +853,11 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { if( ! $loadFields) { $this->subqueryAliases[] = $assocTableName; } - $this->parts["join"][$tname][$assocTableName] = $join.$assocTableName." ON ".$tname.".".$table->getIdentifier()." = ".$assocTableName.".".$fk->getLocal(); - $this->parts["join"][$tname][$tname2] = $join.$aliasString." ON ".$tname2.".".$table->getIdentifier()." = ".$assocTableName.".".$fk->getForeign(); + $this->parts["join"][$tname][$assocTableName] = $join.$assocTableName .' ON '.$tname.".".$table->getIdentifier()." = ".$assocTableName.".".$fk->getLocal(); + $this->parts["join"][$tname][$tname2] = $join.$aliasString .' ON '.$tname2.".".$table->getIdentifier()." = ".$assocTableName.".".$fk->getForeign(); + + } else { + $this->parts["join"][$tname][$tname2] = $join.$aliasString .' ON '.$tname.".".$fk->getLocal()." = ".$tname2.".".$fk->getForeign(); } $this->joins[$tname2] = $prevTable; @@ -798,13 +870,31 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { $this->relationStack[] = $fk; } + + $this->components[$currPath] = $table; + $this->tableStack[] = $table; if( ! isset($this->tables[$tableName])) { $this->tables[$tableName] = $table; if($loadFields) { - $this->parseFields($fullname, $tableName, $e2, $currPath); + + $skip = false; + if($componentAlias) { + $this->compAliases[$componentAlias] = $currPath; + + if(isset($this->pendingFields[$componentAlias])) { + $this->processPendingFields($componentAlias); + $skip = true; + } + if(isset($this->pendingAggregates[$componentAlias])) { + $this->processPendingAggregates($componentAlias); + $skip = true; + } + } + if( ! $skip) + $this->parseFields($fullname, $tableName, $e2, $currPath); } } @@ -919,3 +1009,4 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { } } } + diff --git a/lib/Doctrine/Table.php b/lib/Doctrine/Table.php index 8b27bc457..b89856dad 100644 --- a/lib/Doctrine/Table.php +++ b/lib/Doctrine/Table.php @@ -86,11 +86,6 @@ class Doctrine_Table extends Doctrine_Configurable implements Countable { * @var Doctrine_Table_Repository $repository record repository */ private $repository; - - /** - * @var Doctrine_Cache $cache second level cache - */ - private $cache; /** * @var array $columns an array of column definitions */ @@ -733,8 +728,7 @@ class Doctrine_Table extends Doctrine_Configurable implements Countable { * finds a record by its identifier * * @param $id database row id - * @throws Doctrine_Find_Exception - * @return Doctrine_Record a record for given database identifier + * @return Doctrine_Record|false a record for given database identifier */ public function find($id) { if($id !== null) { diff --git a/tests/QuerySelectTestCase.php b/tests/QuerySelectTestCase.php index 439ee1f94..0e6f619b0 100644 --- a/tests/QuerySelectTestCase.php +++ b/tests/QuerySelectTestCase.php @@ -1,7 +1,72 @@ parseQuery('SELECT COUNT(u.id) FROM User u'); + + $this->assertEqual($q->getQuery(), 'SELECT COUNT(entity.id) AS entity__0 FROM entity WHERE (entity.type = 0)'); } + public function testMultipleAggregateFunctions() { + $q = new Doctrine_Query(); + + $q->parseQuery('SELECT MAX(u.id), MIN(u.name) FROM User u'); + + $this->assertEqual($q->getQuery(), 'SELECT MAX(entity.id) AS entity__0, MIN(entity.name) AS entity__1 FROM entity WHERE (entity.type = 0)'); + } + public function testMultipleAggregateFunctionsWithMultipleComponents() { + $q = new Doctrine_Query(); + + $q->parseQuery('SELECT MAX(u.id), MIN(u.name), COUNT(p.id) FROM User u, u.Phonenumber p'); + + $this->assertEqual($q->getQuery(), 'SELECT MAX(entity.id) AS entity__0, MIN(entity.name) AS entity__1, COUNT(phonenumber.id) AS phonenumber__2 FROM entity LEFT JOIN phonenumber ON entity.id = phonenumber.entity_id WHERE (entity.type = 0)'); + } + + public function testAggregateFunctionValueHydration() { + $q = new Doctrine_Query(); + + $q->parseQuery('SELECT u.id, COUNT(p.id) FROM User u, u.Phonenumber p GROUP BY u.id'); + + $users = $q->execute(); + + $this->assertEqual($users[0]->Phonenumber->getAggregateValue('COUNT'), 1); + $this->assertEqual($users[1]->Phonenumber->getAggregateValue('COUNT'), 3); + $this->assertEqual($users[2]->Phonenumber->getAggregateValue('COUNT'), 1); + $this->assertEqual($users[3]->Phonenumber->getAggregateValue('COUNT'), 1); + $this->assertEqual($users[4]->Phonenumber->getAggregateValue('COUNT'), 3); + + } + + public function testSingleComponentWithAsterisk() { + $q = new Doctrine_Query(); + + $q->parseQuery('SELECT u.* FROM User u'); + + $this->assertEqual($q->getQuery(),'SELECT entity.id AS entity__id, entity.name AS entity__name, entity.loginname AS entity__loginname, entity.password AS entity__password, entity.type AS entity__type, entity.created AS entity__created, entity.updated AS entity__updated, entity.email_id AS entity__email_id FROM entity WHERE (entity.type = 0)'); + } + public function testSingleComponentWithMultipleColumns() { + $q = new Doctrine_Query(); + + $q->parseQuery('SELECT u.name, u.type FROM User u'); + + $this->assertEqual($q->getQuery(),'SELECT entity.id AS entity__id, entity.name AS entity__name, entity.type AS entity__type FROM entity WHERE (entity.type = 0)'); + } + public function testMultipleComponentsWithAsterisk() { + $q = new Doctrine_Query(); + + $q->parseQuery('SELECT u.*, p.* FROM User u, u.Phonenumber p'); + + $this->assertEqual($q->getQuery(),'SELECT entity.id AS entity__id, entity.name AS entity__name, entity.loginname AS entity__loginname, entity.password AS entity__password, entity.type AS entity__type, entity.created AS entity__created, entity.updated AS entity__updated, entity.email_id AS entity__email_id, phonenumber.id AS phonenumber__id, phonenumber.phonenumber AS phonenumber__phonenumber, phonenumber.entity_id AS phonenumber__entity_id FROM entity LEFT JOIN phonenumber ON entity.id = phonenumber.entity_id WHERE (entity.type = 0)'); + } + public function testMultipleComponentsWithMultipleColumns() { + $q = new Doctrine_Query(); + + $q->parseQuery('SELECT u.id, u.name, p.id FROM User u, u.Phonenumber p'); + + $this->assertEqual($q->getQuery(),'SELECT entity.id AS entity__id, entity.name AS entity__name, phonenumber.id AS phonenumber__id FROM entity LEFT JOIN phonenumber ON entity.id = phonenumber.entity_id WHERE (entity.type = 0)'); + } + } ?> diff --git a/tests/run.php b/tests/run.php index 946d35f70..ff64f12ba 100644 --- a/tests/run.php +++ b/tests/run.php @@ -37,6 +37,7 @@ require_once("QueryFromTestCase.php"); require_once("QueryConditionTestCase.php"); require_once("QueryComponentAliasTestCase.php"); require_once("QuerySubqueryTestCase.php"); +require_once("QuerySelectTestCase.php"); require_once("DBTestCase.php"); require_once("SchemaTestCase.php"); @@ -52,12 +53,11 @@ error_reporting(E_ALL); print "
";
 
 $test = new GroupTest("Doctrine Framework Unit Tests");
- 
+
 $test->addTestCase(new Doctrine_RecordTestCase());
 
 $test->addTestCase(new Doctrine_ValidatorTestCase());
 
-
 $test->addTestCase(new Doctrine_Query_MultiJoin_TestCase());
 
 $test->addTestCase(new Doctrine_Relation_TestCase());
@@ -130,6 +130,7 @@ $test->addTestCase(new Doctrine_Query_Where_TestCase());
 
 $test->addTestCase(new Doctrine_Query_From_TestCase());
 
+$test->addTestCase(new Doctrine_Query_Select_TestCase());
 
 
 //$test->addTestCase(new Doctrine_Cache_FileTestCase());