Commit 2662b46e authored by zYne's avatar zYne

DQL aggregate value model rewrite

parent d4405bb9
......@@ -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);
}
}
......@@ -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();
......
......@@ -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 {
......
......@@ -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];
}
if ( ! empty($this->pendingAggregates)) {
$this->processPendingAggregates();
return null;
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;
}
try {
// get the name of the function
$name = substr($func, 0, $pos);
$argStr = substr($func, ($pos + 1), -1);
$args = explode(',', $argStr);
$func = call_user_func_array(array($this->_conn->expression, $name), $args);
$args = array();
// parse args
foreach (Doctrine_Tokenizer::bracketExplode($argStr, ',') as $expr) {
$args[] = $this->parseAggregateFunction($expr, true);
}
if(substr($func, 0, 1) !== '(') {
$pos = strpos($func, '(');
$name = substr($func, 0, $pos);
} else {
$name = $func;
// convert DQL function to its RDBMS specific equivalent
try {
$expr = call_user_func_array(array($this->_conn->expression, $name), $args);
} catch(Doctrine_Expression_Exception $e) {
throw new Doctrine_Query_Exception('Unknown function ' . $func . '.');
}
$e2 = explode(' ', $args[0]);
if ( ! $nestedCall) {
// try to find all component references
preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $argStr, $m);
$distinct = '';
if (count($e2) > 1) {
if (strtoupper($e2[0]) == 'DISTINCT') {
$distinct = 'DISTINCT ';
if (isset($e[1])) {
if (strtoupper($e[1]) === 'AS') {
if ( ! isset($e[2])) {
throw new Doctrine_Query_Exception('Missing aggregate function alias.');
}
$args[0] = $e2[1];
$alias = $e[2];
} else {
$alias = $e[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];
} else {
$alias = substr($expr, 0, strpos($expr, '('));
}
// a function without parameters eg. RANDOM()
if ($owner === '') {
$owner = 0;
$this->pendingAggregates[] = array($expr, $m[0], $alias);
}
$this->pendingAggregates[$owner][] = array($name, $args, $distinct, $alias);
} catch(Doctrine_Expression_Exception $e) {
throw new Doctrine_Query_Exception('Unknown function ' . $func . '.');
}
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();
// iterate through the component references within the aggregate function
if ( ! empty ($components)) {
foreach ($components as $component) {
$e = explode('.', $component);
if(isset($this->pendingAggregates[$componentAlias])) {
$aggregates = $this->pendingAggregates[$componentAlias];
}
$field = array_pop($e);
$componentAlias = implode('.', $e);
if ($root === $table) {
if (isset($this->pendingAggregates[0])) {
$aggregates += $this->pendingAggregates[0];
}
// check the existence of the component alias
if ( ! isset($this->_aliasMap[$componentAlias])) {
throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias);
}
foreach($aggregates as $parts) {
list($name, $args, $distinct, $alias) = $parts;
$table = $this->_aliasMap[$componentAlias]['table'];
$arglist = array();
foreach($args as $arg) {
$e = explode('.', $arg);
$field = $table->getColumnName($field);
// check column existence
if ( ! $table->hasColumn($field)) {
throw new Doctrine_Query_Exception('Unknown column ' . $field);
}
if (is_numeric($arg)) {
$arglist[] = $arg;
} elseif (count($e) > 1) {
$map = $this->_aliasMap[$e[0]];
$table = $map['table'];
$tableAlias = $this->getTableAlias($componentAlias);
$e[1] = $table->getColumnName($e[1]);
$tableAliases[$tableAlias] = true;
if ( ! $table->hasColumn($e[1])) {
throw new Doctrine_Query_Exception('Unknown column ' . $e[1]);
// build sql expression
$expression = str_replace($component, $tableAlias . '.' . $field, $expression);
}
$arglist[] = $tableAlias . '.' . $e[1];
} else {
$arglist[] = $e[0];
}
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
......@@ -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();
......
......@@ -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');
}
}
?>
......@@ -32,25 +32,25 @@
*/
class Doctrine_Query_Orderby_TestCase extends Doctrine_UnitTestCase
{
public function testOrderByAggregateValueIsSupported()
public function testOrderByRandomIsSupported()
{
$q = new Doctrine_Query();
$q->select('u.name, COUNT(p.phonenumber) count')
$q->select('u.name, RANDOM() rand')
->from('User u')
->leftJoin('u.Phonenumber p')
->orderby('count DESC');
->orderby('rand DESC');
$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');
$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 testOrderByRandomIsSupported()
public function testOrderByAggregateValueIsSupported()
{
$q = new Doctrine_Query();
$q->select('u.name, RANDOM() rand')
$q->select('u.name, COUNT(p.phonenumber) count')
->from('User u')
->orderby('rand DESC');
->leftJoin('u.Phonenumber p')
->orderby('count 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');
$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');
}
}
......@@ -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,7 +101,6 @@ class Doctrine_Query_Select_TestCase extends Doctrine_UnitTestCase
}
}
public function testAggregateFunctionValueHydration()
{
$q = new Doctrine_Query();
......
......@@ -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;
}
......
......@@ -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 "<pre>";
foreach ($this->_test->getMessages() as $message) {
print $message . "\n";
}
print "</pre>";
$colour = ($this->_test->getFailCount() > 0 ? "red" : "green");
print "<div style=\"";
print "padding: 8px; margin-top: 1em; background-color: $colour; color: white;";
print "\">";
print $this->getTestCaseProgress() . "/" . $this->getTestCaseCount();
print $this->_test->getTestCaseCount() . ' test cases';
print " test cases complete:\n";
print "<strong>" . $this->getPassCount() . "</strong> passes, ";
print "<strong>" . $this->getFailCount() . "</strong> fails and ";
print "<strong>" . $this->getExceptionCount() . "</strong> exceptions.";
print "<strong>" . $this->_test->getPassCount() . "</strong> passes, ";
print "<strong>" . $this->_test->getFailCount() . "</strong> fails and ";
print "</div>\n";
}
}
if (TextReporter::inCli()) {
if ($argc == 4)
{
$dsn = $argv[1];
$username = $argv[2];
$password = $argv[3];
}
exit ($test->run(new TextReporter()) ? 0 : 1);
}
?>
<html>
<head>
<title>Doctrine Unit Tests</title>
<style>
.fail { color: red; } pre { background-color: lightgray; }
......@@ -313,60 +315,10 @@ if (TextReporter::inCli()) {
<body>
<h1>Doctrine Unit Tests</h1>
<h3>DSN Settings</h3>
<form method="post">
<table>
<tr>
<th>DSN</th>
<td><input type="text" name="dsn" /></td>
</tr>
<tr>
<th>Username</th>
<td><input type="text" name="username" /></td>
</tr>
<tr>
<th>Password</th>
<td><input type="text" name="password" /></td>
</tr>
<tr>
<td>&nbsp;</td>
<td><input type="submit" name="submit" /></td>
</tr>
</table>
</form>
<h3>Tests</h3>
<pre>
<?php
ob_start();
if (isset($_POST))
{
$dsn = isset($_POST["dsn"])?$_POST["dsn"]:null;
$username = isset($_POST["username"])?$_POST["username"]:null;
$password = isset($_POST["password"])?$_POST["password"]:null;
}
$test->run(new MyReporter());
$output = ob_get_clean();
/**
$cache = Doctrine_Manager::getInstance()->getCurrentConnection()->getCacheHandler();
if(isset($cache)) {
$a = $cache->getQueries();
print "Executed cache queries: ".count($a)."\n";
foreach($a as $query) {
print $query."\n";
}
}
*/
<?php
$test->run(new MyReporter());
?>
<?php echo $output; ?>
</pre>
<h3>Queries</h3>
<pre>
</pre>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment