From 2662b46e9a5808e45c7e8f462271a057f9c09151 Mon Sep 17 00:00:00 2001 From: zYne Date: Fri, 1 Jun 2007 10:17:50 +0000 Subject: [PATCH] DQL aggregate value model rewrite --- lib/Doctrine/Connection/Sqlite.php | 17 ++- lib/Doctrine/Expression.php | 2 +- lib/Doctrine/Hydrate.php | 2 + lib/Doctrine/Query.php | 193 ++++++++++++------------- tests/Query/AggregateValueTestCase.php | 46 ++++++ tests/Query/OrderbyTestCase.php | 20 +-- tests/Query/SelectTestCase.php | 16 +- tests/Test.php | 5 + tests/run.php | 98 ++++--------- 9 files changed, 210 insertions(+), 189 deletions(-) diff --git a/lib/Doctrine/Connection/Sqlite.php b/lib/Doctrine/Connection/Sqlite.php index 14813156b..f70b10abf 100644 --- a/lib/Doctrine/Connection/Sqlite.php +++ b/lib/Doctrine/Connection/Sqlite.php @@ -74,7 +74,7 @@ class Doctrine_Connection_Sqlite extends Doctrine_Connection_Common $this->options['server_version'] = ''; */ parent::__construct($manager, $adapter); - //$this->initFunctions(); + $this->initFunctions(); } /** * initializes database functions missing in sqlite @@ -84,9 +84,16 @@ class Doctrine_Connection_Sqlite extends Doctrine_Connection_Common */ public function initFunctions() { - $this->dbh->sqliteCreateFunction('md5', array('Doctrine_Expression_Sqlite', 'md5Impl'), 1); - $this->dbh->sqliteCreateFunction('mod', array('Doctrine_Expression_Sqlite', 'modImpl'), 2); - $this->dbh->sqliteCreateFunction('concat', array('Doctrine_Expression_Sqlite', 'concatImpl')); - $this->dbh->sqliteCreateFunction('now', 'time', 0); + if ($this->dbh instanceof Doctrine_Db) { + $this->dbh->connect(); + $adapter = $this->dbh->getDbh(); + } else { + $adapter = $this->dbh; + } + + $adapter->sqliteCreateFunction('md5', array('Doctrine_Expression_Sqlite', 'md5Impl'), 1); + $adapter->sqliteCreateFunction('mod', array('Doctrine_Expression_Sqlite', 'modImpl'), 2); + $adapter->sqliteCreateFunction('concat', array('Doctrine_Expression_Sqlite', 'concatImpl')); + $adapter->sqliteCreateFunction('now', 'time', 0); } } diff --git a/lib/Doctrine/Expression.php b/lib/Doctrine/Expression.php index f42b8e3d9..647f1420e 100644 --- a/lib/Doctrine/Expression.php +++ b/lib/Doctrine/Expression.php @@ -269,7 +269,7 @@ class Doctrine_Expression extends Doctrine_Connection_Module * * @param string|array(string) strings that will be concatinated. */ - public function concat($args) + public function concat() { $args = func_get_args(); diff --git a/lib/Doctrine/Hydrate.php b/lib/Doctrine/Hydrate.php index 9bc68de68..c512448bc 100644 --- a/lib/Doctrine/Hydrate.php +++ b/lib/Doctrine/Hydrate.php @@ -578,9 +578,11 @@ class Doctrine_Hydrate implements Serializable foreach ($row as $index => $value) { $agg = false; + if (isset($this->_aliasMap[$alias]['agg'][$index])) { $agg = $this->_aliasMap[$alias]['agg'][$index]; } + if (is_array($record)) { $record[$agg] = $value; } else { diff --git a/lib/Doctrine/Query.php b/lib/Doctrine/Query.php index 3e373384b..af37f2147 100644 --- a/lib/Doctrine/Query.php +++ b/lib/Doctrine/Query.php @@ -192,11 +192,15 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable */ public function getAggregateAlias($dqlAlias) { - if(isset($this->aggregateMap[$dqlAlias])) { + if (isset($this->aggregateMap[$dqlAlias])) { return $this->aggregateMap[$dqlAlias]; } - - return null; + if ( ! empty($this->pendingAggregates)) { + $this->processPendingAggregates(); + + return $this->getAggregateAlias($dqlAlias); + } + throw new Doctrine_Query_Exception('Unknown aggregate alias ' . $dqlAlias); } /** * getParser @@ -336,6 +340,7 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable $refs = Doctrine_Tokenizer::bracketExplode($dql, ','); foreach ($refs as $reference) { + $reference = trim($reference); if (strpos($reference, '(') !== false) { if (substr($reference, 0, 1) === '(') { // subselect found in SELECT part @@ -384,64 +389,58 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable * parses an aggregate function and returns the parsed form * * @see Doctrine_Expression - * @param string $func DQL aggregate function + * @param string $expr DQL aggregate function * @throws Doctrine_Query_Exception if unknown aggregate function given * @return array parsed form of given function */ - public function parseAggregateFunction($func) + public function parseAggregateFunction($expr, $nestedCall = false) { - $e = Doctrine_Tokenizer::bracketExplode($func, ' '); + $e = Doctrine_Tokenizer::bracketExplode($expr, ' '); $func = $e[0]; $pos = strpos($func, '('); - $name = substr($func, 0, $pos); + if ($pos === false) { + return $expr; + } + // get the name of the function + $name = substr($func, 0, $pos); + $argStr = substr($func, ($pos + 1), -1); + + $args = array(); + // parse args + foreach (Doctrine_Tokenizer::bracketExplode($argStr, ',') as $expr) { + $args[] = $this->parseAggregateFunction($expr, true); + } + + // convert DQL function to its RDBMS specific equivalent 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); + $expr = call_user_func_array(array($this->_conn->expression, $name), $args); } catch(Doctrine_Expression_Exception $e) { throw new Doctrine_Query_Exception('Unknown function ' . $func . '.'); } + + if ( ! $nestedCall) { + // try to find all component references + preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $argStr, $m); + + if (isset($e[1])) { + if (strtoupper($e[1]) === 'AS') { + if ( ! isset($e[2])) { + throw new Doctrine_Query_Exception('Missing aggregate function alias.'); + } + $alias = $e[2]; + } else { + $alias = $e[1]; + } + } else { + $alias = substr($expr, 0, strpos($expr, '(')); + } + + $this->pendingAggregates[] = array($expr, $m[0], $alias); + } + + return $expr; } /** * processPendingSubqueries @@ -476,67 +475,65 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable * processPendingAggregates * processes pending aggregate values for given component alias * - * @param string $componentAlias dql component alias * @return void */ - public function processPendingAggregates($componentAlias) + public function processPendingAggregates() { - $tableAlias = $this->getTableAlias($componentAlias); + // iterate trhough all aggregates + foreach ($this->pendingAggregates as $aggregate) { + list ($expression, $components, $alias) = $aggregate; - $map = reset($this->_aliasMap); - $root = $map['table']; - $table = $this->_aliasMap[$componentAlias]['table']; + $tableAliases = array(); - $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) { - $map = $this->_aliasMap[$e[0]]; - $table = $map['table']; - - $e[1] = $table->getColumnName($e[1]); - - if ( ! $table->hasColumn($e[1])) { - throw new Doctrine_Query_Exception('Unknown column ' . $e[1]); + // iterate through the component references within the aggregate function + if ( ! empty ($components)) { + foreach ($components as $component) { + $e = explode('.', $component); + + $field = array_pop($e); + $componentAlias = implode('.', $e); + + // check the existence of the component alias + if ( ! isset($this->_aliasMap[$componentAlias])) { + throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); } - - $arglist[] = $tableAlias . '.' . $e[1]; - } else { - $arglist[] = $e[0]; + + $table = $this->_aliasMap[$componentAlias]['table']; + + $field = $table->getColumnName($field); + + // check column existence + if ( ! $table->hasColumn($field)) { + throw new Doctrine_Query_Exception('Unknown column ' . $field); + } + + $tableAlias = $this->getTableAlias($componentAlias); + + $tableAliases[$tableAlias] = true; + + // build sql expression + $expression = str_replace($component, $tableAlias . '.' . $field, $expression); } } + + if (count($tableAliases) !== 1) { + $componentAlias = reset($this->tableAliases); + $tableAlias = key($this->tableAliases); + } + $index = count($this->aggregateMap); $sqlAlias = $tableAlias . '__' . $index; - if (substr($name, 0, 1) !== '(') { - $this->parts['select'][] = $name . '(' . $distinct . implode(', ', $arglist) . ') AS ' . $sqlAlias; - } else { - $this->parts['select'][] = $name . ' AS ' . $sqlAlias; - } + $this->parts['select'][] = $expression . ' AS ' . $sqlAlias; + $this->aggregateMap[$alias] = $sqlAlias; + $this->_aliasMap[$componentAlias]['agg'][$index] = $alias; $this->neededTables[] = $tableAlias; } + // reset the state + $this->pendingAggregates = array(); } /** * getQueryBase @@ -630,7 +627,7 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable } } } - if (empty($this->parts['select']) || empty($this->parts['from'])) { + if (empty($this->parts['from'])) { return false; } @@ -647,6 +644,7 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable // process all pending SELECT part subqueries $this->processPendingSubqueries(); + $this->processPendingAggregates(); // build the basic query @@ -758,7 +756,7 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable $e = explode(' ', $part); if (empty($this->parts['orderby']) && empty($this->parts['where'])) { - continue; + continue; } } @@ -1055,10 +1053,11 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable if(isset($this->pendingFields[$componentAlias])) { $this->processPendingFields($componentAlias); } - + /** if(isset($this->pendingAggregates[$componentAlias]) || isset($this->pendingAggregates[0])) { $this->processPendingAggregates($componentAlias); } + */ if ($restoreState) { $this->pendingFields = array(); diff --git a/tests/Query/AggregateValueTestCase.php b/tests/Query/AggregateValueTestCase.php index cad5dc5c0..01989c366 100644 --- a/tests/Query/AggregateValueTestCase.php +++ b/tests/Query/AggregateValueTestCase.php @@ -160,5 +160,51 @@ class Doctrine_Query_AggregateValue_TestCase extends Doctrine_UnitTestCase $this->assertEqual($users[1]->Phonenumber[0]->count, 2); $this->assertEqual($users[2]->Phonenumber[0]->count, 1); } + public function testAggregateFunctionParser() + { + $q = new Doctrine_Query(); + $func = $q->parseAggregateFunction('SUM(i.price)'); + + $this->assertEqual($func, 'SUM(i.price)'); + } + public function testAggregateFunctionParser2() + { + $q = new Doctrine_Query(); + $func = $q->parseAggregateFunction('SUM(i.price * i.quantity)'); + + $this->assertEqual($func, 'SUM(i.price * i.quantity)'); + } + public function testAggregateFunctionParser3() + { + $q = new Doctrine_Query(); + $func = $q->parseAggregateFunction('MOD(i.price, i.quantity)'); + + $this->assertEqual($func, 'MOD(i.price, i.quantity)'); + } + public function testAggregateFunctionParser4() + { + $q = new Doctrine_Query(); + $func = $q->parseAggregateFunction('CONCAT(i.price, i.quantity)'); + + $this->assertEqual($func, 'CONCAT(i.price, i.quantity)'); + } + public function testAggregateFunctionParsingSupportsMultipleComponentReferences() + { + $q = new Doctrine_Query(); + $q->select('SUM(i.price * i.quantity)') + ->from('QueryTest_Item i'); + + $this->assertEqual($q->getQuery(), "SELECT SUM(q.price * q.quantity) AS q__0 FROM query_test__item q"); + } + + +} +class QueryTest_Item extends Doctrine_Record +{ + public function setTableDefinition() + { + $this->hasColumn('price', 'decimal'); + $this->hasColumn('quantity', 'integer'); + } } ?> diff --git a/tests/Query/OrderbyTestCase.php b/tests/Query/OrderbyTestCase.php index 468b96913..4c2f54c78 100644 --- a/tests/Query/OrderbyTestCase.php +++ b/tests/Query/OrderbyTestCase.php @@ -32,6 +32,16 @@ */ class Doctrine_Query_Orderby_TestCase extends Doctrine_UnitTestCase { + public function testOrderByRandomIsSupported() + { + $q = new Doctrine_Query(); + + $q->select('u.name, RANDOM() rand') + ->from('User u') + ->orderby('rand DESC'); + + $this->assertEqual($q->getQuery(), 'SELECT e.id AS e__id, e.name AS e__name, ((RANDOM() + 2147483648) / 4294967296) AS e__0 FROM entity e WHERE (e.type = 0) ORDER BY e__0 DESC'); + } public function testOrderByAggregateValueIsSupported() { $q = new Doctrine_Query(); @@ -43,14 +53,4 @@ class Doctrine_Query_Orderby_TestCase extends Doctrine_UnitTestCase $this->assertEqual($q->getQuery(), 'SELECT e.id AS e__id, e.name AS e__name, COUNT(p.phonenumber) AS p__0 FROM entity e LEFT JOIN phonenumber p ON e.id = p.entity_id WHERE (e.type = 0) ORDER BY p__0 DESC'); } - public function testOrderByRandomIsSupported() - { - $q = new Doctrine_Query(); - - $q->select('u.name, RANDOM() rand') - ->from('User u') - ->orderby('rand DESC'); - - $this->assertEqual($q->getQuery(), 'SELECT e.id AS e__id, e.name AS e__name, ((RANDOM() + 2147483648) / 4294967296) AS e__0 FROM entity e WHERE (e.type = 0) ORDER BY e__0 DESC'); - } } diff --git a/tests/Query/SelectTestCase.php b/tests/Query/SelectTestCase.php index ce64268a2..c728e1f29 100644 --- a/tests/Query/SelectTestCase.php +++ b/tests/Query/SelectTestCase.php @@ -32,6 +32,18 @@ */ class Doctrine_Query_Select_TestCase extends Doctrine_UnitTestCase { + public function testAggregateFunctionParsingSupportsMultipleComponentReferences() + { + $q = new Doctrine_Query(); + $q->select("CONCAT(u.name, ' ', e.address) value") + ->from('User u')->innerJoin('u.Email e'); + + $this->assertEqual($q->getQuery(), "SELECT CONCAT(e.name, ' ', e2.address) AS e__0 FROM entity e INNER JOIN email e2 ON e.email_id = e2.id WHERE (e.type = 0)"); + + $users = $q->execute(); + $this->assertEqual($users[0]->value, 'zYne zYne@example.com'); + } + public function testAggregateFunctionWithDistinctKeyword() { $q = new Doctrine_Query(); @@ -40,7 +52,6 @@ class Doctrine_Query_Select_TestCase extends Doctrine_UnitTestCase $this->assertEqual($q->getQuery(), 'SELECT COUNT(DISTINCT e.name) AS e__0 FROM entity e WHERE (e.type = 0)'); } - public function testAggregateFunction() { $q = new Doctrine_Query(); @@ -90,8 +101,7 @@ class Doctrine_Query_Select_TestCase extends Doctrine_UnitTestCase } } - - public function testAggregateFunctionValueHydration() + public function testAggregateFunctionValueHydration() { $q = new Doctrine_Query(); diff --git a/tests/Test.php b/tests/Test.php index d3981cec5..b5ffc42df 100644 --- a/tests/Test.php +++ b/tests/Test.php @@ -106,9 +106,14 @@ class UnitTestCase foreach ($trace as $stack) { + if (substr($stack['function'], 0, 4) === 'test') { $class = new ReflectionClass($stack['class']); + if ( ! isset($line)) { + $line = $stack['line']; + } + $this->_messages[] = $class->getName() . ' : method ' . $stack['function'] . ' failed on line ' . $line; break; } diff --git a/tests/run.php b/tests/run.php index 61e79564e..5fa276670 100644 --- a/tests/run.php +++ b/tests/run.php @@ -9,7 +9,11 @@ function autoload($class) { $e = explode('_', $class); $count = count($e); - array_shift($e); + $prefix = array_shift($e); + + if ($prefix !== 'Doctrine') { + return false; + } $dir = array_shift($e); @@ -23,7 +27,7 @@ function autoload($class) { // create a test case file if it doesn't exist - if( ! file_exists($file)) { + if ( ! file_exists($file)) { $contents = file_get_contents('template.tpl'); $contents = sprintf($contents, $class, $class); @@ -47,11 +51,12 @@ spl_autoload_register('autoload'); require_once dirname(__FILE__) . '/../models/location.php'; require_once dirname(__FILE__) . '/classes.php'; +/** require_once dirname(__FILE__) . '/../vendor/simpletest/unit_tester.php'; require_once dirname(__FILE__) . '/../vendor/simpletest/reporter.php'; - +*/ +require_once dirname(__FILE__) . '/Test.php'; require_once dirname(__FILE__) . '/UnitTestCase.php'; -require_once dirname(__FILE__) . '/DriverTestCase.php'; error_reporting(E_ALL); @@ -138,7 +143,7 @@ $test->addTestCase(new Doctrine_Expression_Oracle_TestCase()); $test->addTestCase(new Doctrine_Expression_Sqlite_TestCase()); // Core - + */ $test->addTestCase(new Doctrine_Access_TestCase()); //$test->addTestCase(new Doctrine_Configurable_TestCase()); @@ -214,6 +219,7 @@ $test->addTestCase(new Doctrine_Query_ShortAliases_TestCase()); $test->addTestCase(new Doctrine_Query_Expression_TestCase()); $test->addTestCase(new Doctrine_ColumnAggregationInheritance_TestCase()); + $test->addTestCase(new Doctrine_ColumnAlias_TestCase()); @@ -223,6 +229,7 @@ $test->addTestCase(new Doctrine_Cache_Memcache_TestCase()); $test->addTestCase(new Doctrine_Cache_Sqlite_TestCase()); $test->addTestCase(new Doctrine_Query_Check_TestCase()); + $test->addTestCase(new Doctrine_Query_Limit_TestCase()); @@ -246,6 +253,7 @@ $test->addTestCase(new Doctrine_Query_Subquery_TestCase()); $test->addTestCase(new Doctrine_Query_AggregateValue_TestCase()); $test->addTestCase(new Doctrine_Query_Select_TestCase()); + $test->addTestCase(new Doctrine_Query_From_TestCase()); $test->addTestCase(new Doctrine_NewCore_TestCase()); @@ -256,7 +264,7 @@ $test->addTestCase(new Doctrine_Record_State_TestCase()); //$test->addTestCase(new Doctrine_Query_Cache_TestCase()); $test->addTestCase(new Doctrine_Tokenizer_TestCase()); -*/ + $test->addTestCase(new Doctrine_Collection_Snapshot_TestCase()); @@ -277,33 +285,27 @@ class MyReporter extends HtmlReporter { public function paintHeader() {} public function paintFooter() { - $colour = ($this->getFailCount() + $this->getExceptionCount() > 0 ? "red" : "green"); + print "
";
+    	foreach ($this->_test->getMessages() as $message) {
+    	   print $message . "\n";
+    	}
+    	print "
"; + $colour = ($this->_test->getFailCount() > 0 ? "red" : "green"); print "
"; - print $this->getTestCaseProgress() . "/" . $this->getTestCaseCount(); + print $this->_test->getTestCaseCount() . ' test cases'; print " test cases complete:\n"; - print "" . $this->getPassCount() . " passes, "; - print "" . $this->getFailCount() . " fails and "; - print "" . $this->getExceptionCount() . " exceptions."; + print "" . $this->_test->getPassCount() . " passes, "; + print "" . $this->_test->getFailCount() . " fails and "; print "
\n"; } } -if (TextReporter::inCli()) { - if ($argc == 4) - { - $dsn = $argv[1]; - $username = $argv[2]; - $password = $argv[3]; - } - exit ($test->run(new TextReporter()) ? 0 : 1); -} ?> - Doctrine Unit Tests