From f900a51a7d6e52706abd4254e9767283a83280ed Mon Sep 17 00:00:00 2001 From: zYne Date: Wed, 3 Jan 2007 15:34:34 +0000 Subject: [PATCH] Custom join condition support for DQL --- lib/Doctrine/Query.php | 60 ++++++++++++-- lib/Doctrine/Query/Condition.php | 35 +++++++++ lib/Doctrine/Query/Having.php | 10 +-- lib/Doctrine/Query/JoinCondition.php | 104 +++++++++++++++++++++++++ lib/Doctrine/Query/Where.php | 40 +--------- tests/Query/JoinConditionTestCase.php | 4 +- tests/Query/ReferenceModelTestCase.php | 7 +- tests/Query/ShortAliasesTestCase.php | 4 +- tests/run.php | 1 + 9 files changed, 212 insertions(+), 53 deletions(-) create mode 100644 lib/Doctrine/Query/JoinCondition.php diff --git a/lib/Doctrine/Query.php b/lib/Doctrine/Query.php index d35670ed5..badc5409f 100644 --- a/lib/Doctrine/Query.php +++ b/lib/Doctrine/Query.php @@ -65,6 +65,8 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { private $relationStack = array(); private $isDistinct = false; + + private $neededTables = array(); /** * @var array $pendingFields */ @@ -125,6 +127,8 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { foreach($fields as $name) { $this->parts["select"][] = $tableAlias . '.' .$name . ' AS ' . $tableAlias . '__' . $name; } + + $this->neededTables[] = $tableAlias; } public function parseSelect($dql) @@ -221,6 +225,7 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { $this->parts['select'][] = $name . '(' . $distinct . implode(', ', $arglist) . ') AS ' . $tableAlias . '__' . count($this->aggregateMap); $this->aggregateMap[] = $table; + $this->neededTables[] = $tableAlias; } } /** @@ -562,12 +567,54 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { $q = $this->getQueryBase(); $q .= $this->parts['from']; + /** + var_dump($this->pendingFields); + var_dump($this->subqueryAliases); */ + //var_dump($this->parts['join']); + 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); + + $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; + } + } + /** if( ! empty($this->parts['join'])) { foreach($this->parts['join'] as $part) { $q .= ' '.implode(' ', $part); } } + */ if( ! empty($this->parts['set'])) { $q .= ' SET ' . implode(', ', $this->parts['set']); @@ -1202,36 +1249,37 @@ class Doctrine_Query extends Doctrine_Hydrate implements Countable { $join = 'LEFT JOIN '; break; default: - throw new Doctrine_Exception("Unknown operator '$mark'"); + throw new Doctrine_Query_Exception("Unknown operator '$mark'"); } if( ! $fk->isOneToOne()) { $this->needsSubquery = true; } - if( ! $loadFields || $fk->getTable()->usesInheritanceMap()) { + if( ! $loadFields || $fk->getTable()->usesInheritanceMap() || $joinCondition) { $this->subqueryAliases[] = $tname2; } + if($fk instanceof Doctrine_Relation_Association) { $asf = $fk->getAssociationFactory(); $assocTableName = $asf->getTableName(); - if( ! $loadFields) { + if( ! $loadFields || $joinCondition) { $this->subqueryAliases[] = $assocTableName; } - $this->parts["join"][$tname][$assocTableName] = $join . $assocTableName . ' ON ' . $tname . '.' + $this->parts['join'][$tname][$assocTableName] = $join . $assocTableName . ' ON ' . $tname . '.' . $table->getIdentifier() . ' = ' . $assocTableName . '.' . $fk->getLocal(); - $this->parts["join"][$tname][$tname2] = $join . $aliasString . ' ON ' . $tname2 . '.' + $this->parts['join'][$tname][$tname2] = $join . $aliasString . ' ON ' . $tname2 . '.' . $fk->getTable()->getIdentifier() . ' = ' . $assocTableName . '.' . $fk->getForeign() . $joinCondition; } else { - $this->parts["join"][$tname][$tname2] = $join . $aliasString + $this->parts['join'][$tname][$tname2] = $join . $aliasString . ' ON ' . $tname . '.' . $fk->getLocal() . ' = ' . $tname2 . '.' . $fk->getForeign() . $joinCondition; diff --git a/lib/Doctrine/Query/Condition.php b/lib/Doctrine/Query/Condition.php index 57a28db4c..005fdfef8 100644 --- a/lib/Doctrine/Query/Condition.php +++ b/lib/Doctrine/Query/Condition.php @@ -73,4 +73,39 @@ abstract class Doctrine_Query_Condition extends Doctrine_Query_Part return '(' . $r . ')'; } + /** + * parses a literal value and returns the parsed value + * + * boolean literals are parsed to integers + * components are parsed to associated table aliases + * + * @param string $value literal value to be parsed + * @return string + */ + public function parseLiteralValue($value) + { + // check that value isn't a string + if (strpos($value, '\'') === false) { + // parse booleans + if ($value == 'true') + $value = 1; + elseif ($value == 'false') + $value = 0; + + $a = explode('.', $value); + + if (count($a) > 1) { + // either a float or a component.. + + if ( ! is_numeric($a[0])) { + // a component found + $value = $this->query->getTableAlias($a[0]). '.' . $a[1]; + } + } + } else { + // string literal found + } + + return $value; + } } diff --git a/lib/Doctrine/Query/Having.php b/lib/Doctrine/Query/Having.php index 781e67900..6c4652273 100644 --- a/lib/Doctrine/Query/Having.php +++ b/lib/Doctrine/Query/Having.php @@ -40,28 +40,28 @@ class Doctrine_Query_Having extends Doctrine_Query_Condition */ private function parseAggregateFunction($func) { - $pos = strpos($func,"("); + $pos = strpos($func, '('); if ($pos !== false) { $funcs = array(); $name = substr($func, 0, $pos); $func = substr($func, ($pos + 1), -1); - $params = Doctrine_Query::bracketExplode($func, ",", "(", ")"); + $params = Doctrine_Query::bracketExplode($func, ',', '(', ')'); foreach ($params as $k => $param) { $params[$k] = $this->parseAggregateFunction($param); } - $funcs = $name."(".implode(", ", $params).")"; + $funcs = $name . '(' . implode(', ', $params) . ')'; return $funcs; } else { if ( ! is_numeric($func)) { - $a = explode(".",$func); + $a = explode('.', $func); $field = array_pop($a); - $reference = implode(".",$a); + $reference = implode('.', $a); $table = $this->query->load($reference, false); $func = $this->query->getTableAlias($reference).".".$field; diff --git a/lib/Doctrine/Query/JoinCondition.php b/lib/Doctrine/Query/JoinCondition.php new file mode 100644 index 000000000..d4630c43a --- /dev/null +++ b/lib/Doctrine/Query/JoinCondition.php @@ -0,0 +1,104 @@ +. + */ +Doctrine::autoload('Doctrine_Query_Part'); +/** + * Doctrine_Query_JoinCondition + * + * @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$ + * @author Konsta Vesterinen + */ +class Doctrine_Query_JoinCondition extends Doctrine_Query_Condition +{ + public function load($condition) + { + $condition = trim($condition); + + $e = Doctrine_Query::sqlExplode($condition); + + if(count($e) > 2) { + $a = explode('.', $e[0]); + $field = array_pop($a); + $reference = implode('.', $a); + $operator = $e[1]; + $value = $e[2]; + + $alias = $this->query->getTableAlias($reference); + + $table = $this->query->getTable($alias); + // check if value is enumerated value + $enumIndex = $table->enumIndex($field, trim($value, "'")); + + + if (substr($value, 0, 1) == '(') { + // trim brackets + $trimmed = Doctrine_Query::bracketTrim($value); + + if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { + // subquery found + $q = new Doctrine_Query(); + $value = '(' . $q->parseQuery($trimmed)->getQuery() . ')'; + } elseif (substr($trimmed, 0, 4) == 'SQL:') { + $value = '(' . substr($trimmed, 4) . ')'; + } else { + // simple in expression found + $e = Doctrine_Query::sqlExplode($trimmed, ','); + + $value = array(); + foreach ($e as $part) { + $index = $table->enumIndex($field, trim($part, "'")); + if ($index !== false) { + $value[] = $index; + } else { + $value[] = $this->parseLiteralValue($part); + } + } + $value = '(' . implode(', ', $value) . ')'; + } + } else { + if ($enumIndex !== false) { + $value = $enumIndex; + } else { + $value = $this->parseLiteralValue($value); + } + } + + switch ($operator) { + case '<': + case '>': + case '=': + case '!=': + if ($enumIndex !== false) { + $value = $enumIndex; + } + default: + $condition = $alias . '.' . $field . ' ' + . $operator . ' ' . $value; + } + + } + return $condition; + } +} diff --git a/lib/Doctrine/Query/Where.php b/lib/Doctrine/Query/Where.php index 7e4ed5c96..fc1d79373 100644 --- a/lib/Doctrine/Query/Where.php +++ b/lib/Doctrine/Query/Where.php @@ -46,7 +46,7 @@ class Doctrine_Query_Where extends Doctrine_Query_Condition $e = Doctrine_Query::sqlExplode($where); if (count($e) > 1) { - $tmp = $e[0].' '.$e[1]; + $tmp = $e[0] . ' ' . $e[1]; if (substr($tmp, 0, 6) == 'EXISTS') { return $this->parseExists($where, true); @@ -161,47 +161,13 @@ class Doctrine_Query_Where extends Doctrine_Query_Condition $value = $enumIndex; } default: - $where = $alias.'.'.$field.' '.$operator.' '.$value; + $where = $alias . '.' . $field . ' ' + . $operator . ' ' . $value; } } } return $where; } - /** - * parses a literal value and returns the parsed value - * - * boolean literals are parsed to integers - * components are parsed to associated table aliases - * - * @param string $value literal value to be parsed - * @return string - */ - public function parseLiteralValue($value) - { - // check that value isn't a string - if (strpos($value, '\'') === false) { - // parse booleans - if ($value == 'true') - $value = 1; - elseif ($value == 'false') - $value = 0; - - $a = explode('.', $value); - - if (count($a) > 1) { - // either a float or a component.. - - if ( ! is_numeric($a[0])) { - // a component found - $value = $this->query->getTableAlias($a[0]). '.' . $a[1]; - } - } - } else { - // string literal found - } - - return $value; - } /** * parses an EXISTS expression * diff --git a/tests/Query/JoinConditionTestCase.php b/tests/Query/JoinConditionTestCase.php index c9243ede9..3525aa7ba 100644 --- a/tests/Query/JoinConditionTestCase.php +++ b/tests/Query/JoinConditionTestCase.php @@ -54,7 +54,7 @@ class Doctrine_Query_JoinCondition_TestCase extends Doctrine_UnitTestCase $q->parseQuery("SELECT u.name, g.id FROM User u LEFT JOIN u.Group g ON g.id > 2"); - $this->assertEqual($q->getQuery(), "SELECT e.id AS e__id, e.name AS e__name, e2.id AS e2__id FROM entity e LEFT JOIN groupuser ON e.id = groupuser.user_id LEFT JOIN entity e2 ON e2.id = groupuser.group_id AND g.id > 2 WHERE (e.type = 0 AND (e2.type = 1 OR e2.type IS NULL))"); + $this->assertEqual($q->getQuery(), "SELECT e.id AS e__id, e.name AS e__name, e2.id AS e2__id FROM entity e LEFT JOIN groupuser ON e.id = groupuser.user_id LEFT JOIN entity e2 ON e2.id = groupuser.group_id AND e2.id > 2 WHERE (e.type = 0 AND (e2.type = 1 OR e2.type IS NULL))"); } public function testJoinConditionsAreSupportedForManyToManyInnerJoins() { @@ -62,6 +62,6 @@ class Doctrine_Query_JoinCondition_TestCase extends Doctrine_UnitTestCase $q->parseQuery("SELECT u.name, g.id FROM User u INNER JOIN u.Group g ON g.id > 2"); - $this->assertEqual($q->getQuery(), "SELECT e.id AS e__id, e.name AS e__name, e2.id AS e2__id FROM entity e INNER JOIN groupuser ON e.id = groupuser.user_id INNER JOIN entity e2 ON e2.id = groupuser.group_id AND g.id > 2 WHERE (e.type = 0 AND (e2.type = 1 OR e2.type IS NULL))"); + $this->assertEqual($q->getQuery(), "SELECT e.id AS e__id, e.name AS e__name, e2.id AS e2__id FROM entity e INNER JOIN groupuser ON e.id = groupuser.user_id INNER JOIN entity e2 ON e2.id = groupuser.group_id AND e2.id > 2 WHERE (e.type = 0 AND (e2.type = 1 OR e2.type IS NULL))"); } } diff --git a/tests/Query/ReferenceModelTestCase.php b/tests/Query/ReferenceModelTestCase.php index b905d21df..442098324 100644 --- a/tests/Query/ReferenceModelTestCase.php +++ b/tests/Query/ReferenceModelTestCase.php @@ -10,6 +10,7 @@ class Doctrine_Query_ReferenceModel_TestCase extends Doctrine_UnitTestCase { parent::prepareTables(); $this->connection->clear(); } + public function testInitializeData() { $query = new Doctrine_Query($this->connection); @@ -38,13 +39,13 @@ class Doctrine_Query_ReferenceModel_TestCase extends Doctrine_UnitTestCase { $this->connection->clear(); } + public function testSelfReferencingWithNestedOrderBy() { $query = new Doctrine_Query(); $query->from("Forum_Category.Subcategory.Subcategory"); $query->orderby("Forum_Category.id ASC, Forum_Category.Subcategory.name DESC"); - $coll = $query->execute(); $category = $coll[0]; @@ -90,6 +91,7 @@ class Doctrine_Query_ReferenceModel_TestCase extends Doctrine_UnitTestCase { $query->from("Forum_Category.Parent.Parent")->where("Forum_Category.name LIKE 'Sub%Sub%'"); $coll = $query->execute(); + $count = count($this->dbh); $this->assertEqual($coll->count(), 4); $this->assertEqual($coll[0]->name, "Sub 1 Sub 1"); @@ -116,7 +118,8 @@ class Doctrine_Query_ReferenceModel_TestCase extends Doctrine_UnitTestCase { public function testSelfReferencingWithNestingAndMultipleConditions() { $query = new Doctrine_Query(); $query->from("Forum_Category.Parent, Forum_Category.Subcategory")->where("Forum_Category.name = 'Sub 1' || Forum_Category.name = 'Sub 2'"); - + + $coll = $query->execute(); $count = count($this->dbh); diff --git a/tests/Query/ShortAliasesTestCase.php b/tests/Query/ShortAliasesTestCase.php index 8aced19a5..6f49f25b0 100644 --- a/tests/Query/ShortAliasesTestCase.php +++ b/tests/Query/ShortAliasesTestCase.php @@ -1,12 +1,14 @@ select('u.name')->from('User u'); $this->assertEqual($q->getQuery(), 'SELECT e.id AS e__id, e.name AS e__name FROM entity e WHERE (e.type = 0)'); } + */ public function testShortAliasesWithOneToManyLeftJoin() { $q = new Doctrine_Query(); diff --git a/tests/run.php b/tests/run.php index e81775fc4..dbf8e9a64 100644 --- a/tests/run.php +++ b/tests/run.php @@ -192,6 +192,7 @@ $test->addTestCase(new Doctrine_Query_AggregateValue_TestCase()); $test->addTestCase(new Doctrine_Query_Select_TestCase()); $test->addTestCase(new Doctrine_Query_Expression_TestCase()); $test->addTestCase(new Doctrine_Query_Having_TestCase()); + $test->addTestCase(new Doctrine_Query_JoinCondition_TestCase()); // Cache tests