diff --git a/lib/Doctrine/ORM/Query/Expr.php b/lib/Doctrine/ORM/Query/Expr.php new file mode 100644 index 000000000..e88b481be --- /dev/null +++ b/lib/Doctrine/ORM/Query/Expr.php @@ -0,0 +1,483 @@ +. + */ + +namespace Doctrine\ORM\Query; + +/** + * This class is used to generate DQL expressions via a set of PHP static functions + * + * @author Jonathan H. Wage + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 2.0 + * @version $Revision$ + */ +class Expr +{ + private static $_methodMap = array( + 'avg' => '_avgExpr', + 'max' => '_maxExpr', + 'min' => '_minExpr', + 'count' => '_countExpr', + 'countDistinct' => '_countDistinctExpr', + 'exists' => '_existsExpr', + 'all' => '_allExpr', + 'some' => '_someExpr', + 'any' => '_anyExpr', + 'not' => '_notExpr', + 'and' => '_andExpr', + 'or' => '_orExpr', + 'abs' => '_absExpr', + 'prod' => '_prodExpr', + 'diff' => '_diffExpr', + 'sum' => '_sumExpr', + 'quot' => '_quotientExpr', + 'sqrt' => '_squareRootExpr', + 'eq' => '_equalExpr', + 'in' => '_inExpr', + 'notIn' => '_notInExpr', + 'notEqual' => '_notEqualExpr', + 'like' => '_likeExpr', + 'concat' => '_concatExpr', + 'substr' => '_substrExpr', + 'lower' => '_lowerExpr', + 'upper' => '_upperExpr', + 'length' => '_lengthExpr', + 'gt' => '_greaterThanExpr', + 'lt' => '_lessThanExpr', + 'path' => '_pathExpr', + 'literal' => '_literalExpr', + 'gtoet' => '_greaterThanOrEqualToExpr', + 'ltoet' => '_lessThanOrEqualToExpr', + 'between' => '_betweenExpr', + 'trim' => '_trimExpr' + ); + + private $_type; + private $_parts; + + protected function __construct($type, array $parts) + { + $this->_type = $type; + $this->_parts = $parts; + } + + public function getDql() + { + return $this->{self::$_methodMap[$this->_type]}(); + } + + public function __toString() + { + return $this->getDql(); + } + + private function _avgExpr() + { + return 'AVG(' . $this->_parts[0] . ')'; + } + + private function _maxExpr() + { + return 'MAX(' . $this->_parts[0] . ')'; + } + + private function _minExpr() + { + return 'MIN(' . $this->_parts[0] . ')'; + } + + private function _countExpr() + { + return 'COUNT(' . $this->_parts[0] . ')'; + } + + private function _countDistinctExpr() + { + return 'COUNT(DISTINCT ' . $this->_parts[0] . ')'; + } + + private function _existsExpr() + { + return 'EXISTS(' . $this->_parts[0] . ')'; + } + + private function _allExpr() + { + return 'ALL(' . $this->_parts[0] . ')'; + } + + private function _someExpr() + { + return 'SOME(' . $this->_parts[0] . ')'; + } + + private function _anyExpr() + { + return 'ANY(' . $this->_parts[0] . ')'; + } + + private function _notExpr() + { + return 'NOT(' . $this->_parts[0] . ')'; + } + + private function _andExpr() + { + return '(' . $this->_parts[0] . ' AND ' . $this->_parts[1] . ')'; + } + + private function _orExpr() + { + return '(' . $this->_parts[0] . ' OR ' . $this->_parts[1] . ')'; + } + + private function _absExpr() + { + return 'ABS(' . $this->_parts[0] . ')'; + } + + private function _prodExpr() + { + return '(' . $this->_parts[0] . ' * ' . $this->_parts[1] . ')'; + } + + private function _diffExpr() + { + return '(' . $this->_parts[0] . ' - ' . $this->_parts[1] . ')'; + } + + private function _sumExpr() + { + return '(' . $this->_parts[0] . ' + ' . $this->_parts[1] . ')'; + } + + private function _quotientExpr() + { + return '(' . $this->_parts[0] . ' / ' . $this->_parts[1] . ')'; + } + + private function _squareRootExpr() + { + return 'SQRT(' . $this->_parts[0] . ')'; + } + + private function _equalExpr() + { + return $this->_parts[0] . ' = ' . $this->_parts[1]; + } + + private function _inExpr() + { + return $this->_parts[0] . ' IN(' . implode(', ', $this->_parts[1]) . ')'; + } + + private function _notInExpr() + { + return $this->_parts[0] . ' NOT IN(' . implode(', ', $this->_parts[1]) . ')'; + } + + private function _notEqualExpr() + { + return $this->_parts[0] . ' != ' . $this->_parts[1]; + } + + private function _likeExpr() + { + // TODO: How should we use $escapeChar which is in $this->_parts[2] + return '(' . $this->_parts[0] . ' LIKE ' . $this->_parts[1] . ')'; + } + + private function _concatExpr() + { + return 'CONCAT(' . $this->_parts[0] . ', ' . $this->_parts[1] . ')'; + } + + private function _substrExpr() + { + return 'SUBSTR(' . $this->_parts[0] . ', ' . $this->_parts[1] . ', ' . $this->_parts[2] . ')'; + } + + private function _lowerExpr() + { + return 'LOWER(' . $this->_parts[0] . ')'; + } + + private function _upperExpr() + { + return 'UPPER(' . $this->_parts[0] . ')'; + } + + private function _lengthExpr() + { + return 'LENGTH(' . $this->_parts[0] . ')'; + } + + private function _greaterThanExpr() + { + return $this->_parts[0] . ' > ' . $this->_parts[1]; + } + + private function _lessThanExpr() + { + return $this->_parts[0] . ' < ' . $this->_parts[1]; + } + + private function _pathExpr() + { + // TODO: What is this? + } + + private function _literalExpr() + { + if (is_numeric($this->_parts[0])) { + return (string) $this->_parts[0]; + } else { + return "'" . $this->_parts[0] . "'"; + } + } + + private function _greaterThanOrEqualToExpr() + { + return $this->_parts[0] . ' >= ' . $this->_parts[1]; + } + + private function _lessThanOrEqualToExpr() + { + return $this->_parts[0] . ' <= ' . $this->_parts[1]; + } + + private function _betweenExpr() + { + return 'BETWEEN(' . $this->_parts[0] . ', ' . $this->_parts[1] . ', ' . $this->_parts[2] . ')'; + } + + private function _ltExpr() + { + return '(' . $this->_parts[0] . ' < ' . $this->_parts[1] . ')'; + } + + private function _trimExpr() + { + return 'TRIM(' . $this->_parts[0] . ')'; + } + + public static function avg($x) + { + return new self('avg', array($x)); + } + + public static function max($x) + { + return new self('max', array($x)); + } + + public static function min($x) + { + return new self('min', array($x)); + } + + public static function count($x) + { + return new self('count', array($x)); + } + + public static function countDistinct($x) + { + return new self('countDistinct', array($x)); + } + + public static function exists($subquery) + { + return new self('exists', array($subquery)); + } + + public static function all($subquery) + { + return new self('all', array($subquery)); + } + + public static function some($subquery) + { + return new self('some', array($subquery)); + } + + public static function any($subquery) + { + return new self('any', array($subquery)); + } + + public static function not($restriction) + { + return new self('not', array($restriction)); + } + + public static function andx($x, $y) + { + return new self('and', array($x, $y)); + } + + public static function orx($x, $y) + { + return new self('or', array($x, $y)); + } + + public static function abs($x) + { + return new self('abs', array($x)); + } + + public static function prod($x, $y) + { + return new self('prod', array($x, $y)); + } + + public static function diff($x, $y) + { + return new self('diff', array($x, $y)); + } + + public static function sum($x, $y) + { + return new self('sum', array($x, $y)); + } + + public static function quot($x, $y) + { + return new self('quot', array($x, $y)); + } + + public static function sqrt($x) + { + return new self('sqrt', array($x)); + } + + public static function eq($x, $y) + { + return new self('eq', array($x, $y)); + } + + public static function in($x, $y) + { + return new self('in', array($x, $y)); + } + + public static function notIn($x, $y) + { + return new self('notIn', array($x, $y)); + } + + public static function notEqual($x, $y) + { + return new self('notEqual', array($x, $y)); + } + + public static function like($x, $pattern, $escapeChar = null) + { + return new self('like', array($x, $pattern, $escapeChar)); + } + + public static function concat($x, $y) + { + return new self('concat', array($x, $y)); + } + + public static function substr($x, $from = null, $len = null) + { + return new self('substr', array($x, $from, $len)); + } + + public static function lower($x) + { + return new self('lower', array($x)); + } + + public static function upper($x) + { + return new self('upper', array($x)); + } + + public static function length($x) + { + return new self('length', array($x)); + } + + public static function gt($x, $y) + { + return new self('gt', array($x, $y)); + } + + public static function greaterThan($x, $y) + { + return new self('gt', array($x, $y)); + } + + public static function lt($x, $y) + { + return new self('lt', array($x, $y)); + } + + public static function lessThan($x, $y) + { + return new self('lt', array($x, $y)); + } + + public static function path($path) + { + return new self('path', array($path)); + } + + public static function literal($literal) + { + return new self('literal', array($literal)); + } + + public static function greaterThanOrEqualTo($x, $y) + { + return new self('gtoet', array($x, $y)); + } + + public static function gtoet($x, $y) + { + return new self('gtoet', array($x, $y)); + } + + public static function lessThanOrEqualTo($x, $y) + { + return new self('ltoet', array($x, $y)); + } + + public static function ltoet($x, $y) + { + return new self('ltoet', array($x, $y)); + } + + public static function between($val, $x, $y) + { + return new self('between', array($val, $x, $y)); + } + + public static function trim($val, $spec = null, $char = null) + { + return new self('trim', array($val, $spec, $char)); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php new file mode 100644 index 000000000..d4bb89ca3 --- /dev/null +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -0,0 +1,357 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\ORM\Query\Expr; + +/** + * This class is responsible for building DQL query strings via a object oriented + * PHP interface + * + * TODO: I don't like the API of using the Expr::*() syntax inside of the QueryBuilder + * methods. What can we do to allow them to do it more fluently with the QueryBuilder. + * + * @author Jonathan H. Wage + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 2.0 + * @version $Revision$ + */ +class QueryBuilder +{ + const SELECT = 0; + const DELETE = 1; + const UPDATE = 2; + + const STATE_DIRTY = 0; + const STATE_CLEAN = 1; + + protected $_entityManager; + protected $_dqlParts = array( + 'select' => array(), + 'from' => array(), + 'where' => array(), + 'groupBy' => array(), + 'having' => array(), + 'orderBy' => array(), + 'limit' => array(), + 'offset' => array() + ); + protected $_type = self::SELECT; + protected $_state = self::STATE_CLEAN; + protected $_dql; + + public function __construct(EntityManager $entityManager) + { + $this->_entityManager = $entityManager; + } + + public static function create(EntityManager $entityManager) + { + return new self($entityManager); + } + + public function getType() + { + return $this->_type; + } + + public function getState() + { + return $this->_state; + } + + public function getDql() + { + if ($this->_dql !== null && self::STATE_CLEAN) { + return $this->_dql; + } + + $dql = ''; + + switch ($this->_type) { + case self::DELETE: + $dql = $this->_getDqlForDelete(); + break; + + case self::UPDATE: + $dql = $this->_getDqlForUpdate(); + break; + + case self::SELECT: + default: + $dql = $this->_getDqlForSelect(); + break; + } + + $this->_dql = $dql; + + return $dql; + } + + public function getQuery() + { + $q = new Query($this->_entityManager); + $q->setDql($this->getDql()); + + return $q; + } + + public function select($select = null) + { + $this->_type = self::SELECT; + + if ( ! $select) { + return $this; + } + + return $this->_addDqlQueryPart('select', $select, true); + } + + public function delete($delete = null, $alias = null) + { + $this->_type = self::DELETE; + + if ( ! $delete) { + return $this; + } + + return $this->_addDqlQueryPart('from', $delete . ' ' . $alias); + } + + public function update($update = null, $alias = null) + { + $this->_type = self::UPDATE; + + if ( ! $update) { + return $this; + } + + return $this->_addDqlQueryPart('from', $update . ' ' . $alias); + } + + public function set($key, $value = null) + { + return $this->_addDqlQueryPart('set', $key . ' = ' . $value, true); + } + + public function from($from, $alias) + { + return $this->_addDqlQueryPart('from', $from . ' ' . $alias, true); + } + + public function join($join, $alias) + { + return $this->_addDqlQueryPart('from', 'INNER JOIN ' . $join . ' ' . $alias, true); + } + + public function innerJoin($join, $alias) + { + return $this->join($join, $alias); + } + + public function leftJoin($join, $alias) + { + return $this->_addDqlQueryPart('from', 'LEFT JOIN ' . $join . ' ' . $alias, true); + } + + public function where($where) + { + return $this->_addDqlQueryPart('where', $where, false); + } + + public function andWhere($where) + { + if (count($this->getDqlQueryPart('where')) > 0) { + $this->_addDqlQueryPart('where', 'AND', true); + } + + return $this->_addDqlQueryPart('where', $where, true); + } + + public function orWhere($where) + { + if (count($this->getDqlQueryPart('where')) > 0) { + $this->_addDqlQueryPart('where', 'OR', true); + } + + return $this->_addDqlQueryPart('where', $where, true); + } + + public function groupBy($groupBy) + { + return $this->_addDqlQueryPart('groupBy', $groupBy, false); + } + + public function having($having) + { + return $this->_addDqlQueryPart('having', $having, false); + } + + public function andHaving($having) + { + if (count($this->getDqlQueryPart('having')) > 0) { + $this->_addDqlQueryPart('having', 'AND', true); + } + + return $this->_addDqlQueryPart('having', $having, true); + } + + public function orHaving($having) + { + if (count($this->getDqlQueryPart('having')) > 0) { + $this->_addDqlQueryPart('having', 'OR', true); + } + + return $this->_addDqlQueryPart('having', $having, true); + } + + public function orderBy($sort, $order) + { + return $this->_addDqlQueryPart('orderBy', $sort . ' ' . $order, false); + } + + public function addOrderBy($sort, $order) + { + return $this->_addDqlQueryPart('orderBy', $sort . ' ' . $order, true); + } + + public function limit($limit) + { + return $this->_addDqlQueryPart('limit', $limit); + } + + public function offset($offset) + { + return $this->_addDqlQueryPart('offset', $offset); + } + + /** + * Get the DQL query string for DELETE queries + * + * BNF: + * + * DeleteStatement = DeleteClause [WhereClause] [OrderByClause] [LimitClause] [OffsetClause] + * DeleteClause = "DELETE" "FROM" RangeVariableDeclaration + * WhereClause = "WHERE" ConditionalExpression + * OrderByClause = "ORDER" "BY" OrderByItem {"," OrderByItem} + * LimitClause = "LIMIT" integer + * OffsetClause = "OFFSET" integer + * + * @return string $dql + */ + private function _getDqlForDelete() + { + return 'DELETE' + . $this->_getReducedDqlQueryPart('from', array('pre' => ' ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('where', array('pre' => ' WHERE ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('orderBy', array('pre' => ' ORDER BY ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('limit', array('pre' => ' LIMIT ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('offset', array('pre' => ' OFFSET ', 'separator' => ' ')); + } + + /** + * Get the DQL query string for UPDATE queries + * + * BNF: + * + * UpdateStatement = UpdateClause [WhereClause] [OrderByClause] [LimitClause] [OffsetClause] + * UpdateClause = "UPDATE" RangeVariableDeclaration "SET" UpdateItem {"," UpdateItem} + * WhereClause = "WHERE" ConditionalExpression + * OrderByClause = "ORDER" "BY" OrderByItem {"," OrderByItem} + * LimitClause = "LIMIT" integer + * OffsetClause = "OFFSET" integer + * + * @return string $dql + */ + private function _getDqlForUpdate() + { + return 'UPDATE' + . $this->_getReducedDqlQueryPart('from', array('pre' => ' ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('set', array('pre' => ' SET ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('where', array('pre' => ' WHERE ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('orderBy', array('pre' => ' ORDER BY ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('limit', array('pre' => ' LIMIT ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('offset', array('pre' => ' OFFSET ', 'separator' => ' ')); + } + + /** + * Get the DQL query string for SELECT queries + * + * BNF: + * + * SelectStatement = [SelectClause] FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] [LimitClause] [OffsetClause] + * SelectClause = "SELECT" ["ALL" | "DISTINCT"] SelectExpression {"," SelectExpression} + * FromClause = "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration} + * WhereClause = "WHERE" ConditionalExpression + * GroupByClause = "GROUP" "BY" GroupByItem {"," GroupByItem} + * HavingClause = "HAVING" ConditionalExpression + * OrderByClause = "ORDER" "BY" OrderByItem {"," OrderByItem} + * LimitClause = "LIMIT" integer + * OffsetClause = "OFFSET" integer + * + * @return string $dql + */ + private function _getDqlForSelect() + { + return 'SELECT' + . $this->_getReducedDqlQueryPart('select', array('pre' => ' ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('from', array('pre' => ' FROM ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('where', array('pre' => ' WHERE ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('groupBy', array('pre' => ' GROUP BY ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('having', array('pre' => ' HAVING ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('orderBy', array('pre' => ' ORDER BY ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('limit', array('pre' => ' LIMIT ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('offset', array('pre' => ' OFFSET ', 'separator' => ' ')); + } + + private function _getReducedDqlQueryPart($queryPartName, $options = array()) + { + if (empty($this->_dqlParts[$queryPartName])) { + return (isset($options['empty']) ? $options['empty'] : ''); + } + + $str = (isset($options['pre']) ? $options['pre'] : ''); + $str .= implode($options['separator'], $this->getDqlQueryPart($queryPartName)); + $str .= (isset($options['post']) ? $options['post'] : ''); + + return $str; + } + + private function getDqlQueryPart($queryPartName) + { + return $this->_dqlParts[$queryPartName]; + } + + private function _addDqlQueryPart($queryPartName, $queryPart, $append = false) + { + if ($append) { + $this->_dqlParts[$queryPartName][] = $queryPart; + } else { + $this->_dqlParts[$queryPartName] = array($queryPart); + } + + $this->_state = self::STATE_DIRTY; + return $this; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/AllTests.php b/tests/Doctrine/Tests/ORM/AllTests.php index c7e208f9a..239a512ba 100644 --- a/tests/Doctrine/Tests/ORM/AllTests.php +++ b/tests/Doctrine/Tests/ORM/AllTests.php @@ -31,6 +31,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\UnitOfWorkTest'); $suite->addTestSuite('Doctrine\Tests\ORM\EntityManagerTest'); $suite->addTestSuite('Doctrine\Tests\ORM\CommitOrderCalculatorTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\QueryBuilderTest'); $suite->addTest(Query\AllTests::suite()); $suite->addTest(Hydration\AllTests::suite()); diff --git a/tests/Doctrine/Tests/ORM/Query/AllTests.php b/tests/Doctrine/Tests/ORM/Query/AllTests.php index 23813b51c..de4eb9c8c 100644 --- a/tests/Doctrine/Tests/ORM/Query/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Query/AllTests.php @@ -24,6 +24,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Query\LexerTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Query\DeleteSqlGenerationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Query\UpdateSqlGenerationTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Query\ExprTest'); return $suite; } diff --git a/tests/Doctrine/Tests/ORM/Query/ExprTest.php b/tests/Doctrine/Tests/ORM/Query/ExprTest.php new file mode 100644 index 000000000..9634b98b4 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Query/ExprTest.php @@ -0,0 +1,231 @@ +. + */ + +namespace Doctrine\Tests\ORM\Query; + +use Doctrine\ORM\Query\Expr; +use Doctrine\ORM\Query; + +require_once __DIR__ . '/../../TestInit.php'; + +/** + * Test case for the DQL Expr class used for generating DQL snippets through + * a programmatic interface + * + * @author Jonathan H. Wage + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 2.0 + * @version $Revision$ + */ +class ExprTest extends \Doctrine\Tests\OrmTestCase +{ + private $_em; + + protected function setUp() + { + $this->_em = $this->_getTestEntityManager(); + } + + public function testAvgExpr() + { + $this->assertEquals('AVG(u.id)', (string) Expr::avg('u.id')); + } + + public function testMaxExpr() + { + $this->assertEquals('MAX(u.id)', (string) Expr::max('u.id')); + } + + public function testMinExpr() + { + $this->assertEquals('MIN(u.id)', (string) Expr::min('u.id')); + } + + public function testCountExpr() + { + $this->assertEquals('MAX(u.id)', (string) Expr::max('u.id')); + } + + public function testCountDistinctExpr() + { + $this->assertEquals('COUNT(DISTINCT u.id)', (string) Expr::countDistinct('u.id')); + } + + public function testExistsExpr() + { + $this->assertEquals('EXISTS(SUBQUERY)', (string) Expr::exists('SUBQUERY')); + } + + public function testAllExpr() + { + $this->assertEquals('ALL(SUBQUERY)', (string) Expr::all('SUBQUERY')); + } + + public function testSomeExpr() + { + $this->assertEquals('SOME(SUBQUERY)', (string) Expr::some('SUBQUERY')); + } + + public function testAnyExpr() + { + $this->assertEquals('ANY(SUBQUERY)', (string) Expr::any('SUBQUERY')); + } + + public function testNotExpr() + { + $this->assertEquals('NOT(SUBQUERY)', (string) Expr::not('SUBQUERY')); + } + + public function testAndExpr() + { + $this->assertEquals('(1 = 1 AND 2 = 2)', (string) Expr::andx((string) Expr::eq(1, 1), (string) Expr::eq(2, 2))); + } + + public function testOrExpr() + { + $this->assertEquals('(1 = 1 OR 2 = 2)', (string) Expr::orx((string) Expr::eq(1, 1), (string) Expr::eq(2, 2))); + } + + public function testAbsExpr() + { + $this->assertEquals('ABS(1)', (string) Expr::abs(1)); + } + + public function testProdExpr() + { + $this->assertEquals('(1 * 2)', (string) Expr::prod(1, 2)); + } + + public function testDiffExpr() + { + $this->assertEquals('(1 - 2)', (string) Expr::diff(1, 2)); + } + + public function testSumExpr() + { + $this->assertEquals('(1 + 2)', (string) Expr::sum(1, 2)); + } + + public function testQuotientExpr() + { + $this->assertEquals('(10 / 2)', (string) Expr::quot(10, 2)); + } + + public function testSquareRootExpr() + { + $this->assertEquals('SQRT(1)', (string) Expr::sqrt(1)); + } + + public function testEqualExpr() + { + $this->assertEquals('1 = 1', (string) Expr::eq(1, 1)); + } + + public function testNotEqualExpr() + { + $this->assertEquals('1 != 2', (string) Expr::notEqual(1, 2)); + } + + public function testLikeExpr() + { + $this->assertEquals('(a.description LIKE :description)', (string) Expr::like('a.description', ':description')); + } + + public function testConcatExpr() + { + $this->assertEquals('CONCAT(u.first_name, u.last_name)', (string) Expr::concat('u.first_name', 'u.last_name')); + } + + public function testSubstrExpr() + { + $this->assertEquals('SUBSTR(a.title, 0, 25)', (string) Expr::substr('a.title', 0, 25)); + } + + public function testLowerExpr() + { + $this->assertEquals('LOWER(u.first_name)', (string) Expr::lower('u.first_name')); + } + + public function testUpperExpr() + { + $this->assertEquals('UPPER(u.first_name)', (string) Expr::upper('u.first_name')); + } + + public function testLengthExpr() + { + $this->assertEquals('LENGTH(u.first_name)', (string) Expr::length('u.first_name')); + } + + public function testGreaterThanExpr() + { + $this->assertEquals('5 > 2', (string) Expr::gt(5, 2)); + $this->assertEquals('5 > 2', (string) Expr::greaterThan(5, 2)); + } + + public function testLessThanExpr() + { + $this->assertEquals('2 < 5', (string) Expr::lt(2, 5)); + $this->assertEquals('2 < 5', (string) Expr::lessThan(2, 5)); + } + + public function testPathExpr() + { + // TODO: This functionality still needs to be written and tested + } + + public function testStringLiteralExpr() + { + $this->assertEquals("'word'", (string) Expr::literal('word')); + } + + public function testNumericLiteralExpr() + { + $this->assertEquals(5, (string) Expr::literal(5)); + } + + public function testGreaterThanOrEqualToExpr() + { + $this->assertEquals('5 >= 2', (string) Expr::gtoet(5, 2)); + $this->assertEquals('5 >= 2', (string) Expr::greaterThanOrEqualTo(5, 2)); + } + + public function testLessThanOrEqualTo() + { + $this->assertEquals('2 <= 5', (string) Expr::ltoet(2, 5)); + $this->assertEquals('2 <= 5', (string) Expr::lessThanOrEqualTo(2, 5)); + } + + public function testBetweenExpr() + { + $this->assertEquals('BETWEEN(u.id, 3, 6)', (string) Expr::between('u.id', 3, 6)); + } + + public function testTrimExpr() + { + $this->assertEquals('TRIM(u.id)', (string) Expr::trim('u.id')); + } + + public function testInExpr() + { + $this->assertEquals('u.id IN(1, 2, 3)', (string) Expr::in('u.id', array(1, 2, 3))); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php new file mode 100644 index 000000000..d50401c06 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -0,0 +1,300 @@ +. + */ + +namespace Doctrine\Tests\ORM; + +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Query\Expr; + +require_once __DIR__ . '/../TestInit.php'; + +/** + * Test case for the QueryBuilder class used to build DQL query string in a + * object oriented way. + * + * @author Jonathan H. Wage + * @author Roman Borschel _em = $this->_getTestEntityManager(); + } + + protected function assertValidQueryBuilder(QueryBuilder $qb, $expectedDql) + { + $dql = $qb->getDql(); + $q = $qb->getQuery(); + + try { + $q->getSql(); + } catch (\Exception $e) { + echo $dql . "\n"; + echo $e->getTraceAsString(); + $this->fail($e->getMessage()); + } + + $this->assertEquals($expectedDql, $dql); + } + + public function testSimpleSelect() + { + $qb = QueryBuilder::create($this->_em) + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->select('u.id, u.username'); + + $this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u'); + } + + public function testSimpleDelete() + { + $qb = QueryBuilder::create($this->_em) + ->delete('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + + $this->assertValidQueryBuilder($qb, 'DELETE Doctrine\Tests\Models\CMS\CmsUser u'); + } + + public function testSimpleUpdate() + { + $qb = QueryBuilder::create($this->_em) + ->update('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->set('u.username', ':username', 'jonwage'); + + $this->assertValidQueryBuilder($qb, 'UPDATE Doctrine\Tests\Models\CMS\CmsUser u SET u.username = :username'); + } + + public function testJoin() + { + $qb = QueryBuilder::create($this->_em) + ->select('u, a') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->join('u.articles', 'a'); + + $this->assertValidQueryBuilder($qb, 'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a'); + } + + public function testInnerJoin() + { + $qb = QueryBuilder::create($this->_em) + ->select('u, a') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->innerJoin('u.articles', 'a'); + + $this->assertValidQueryBuilder($qb, 'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a'); + } + + public function testLeftJoin() + { + $qb = QueryBuilder::create($this->_em) + ->select('u, a') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->leftJoin('u.articles', 'a'); + + $this->assertValidQueryBuilder($qb, 'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN u.articles a'); + } + + public function testWhere() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->where('u.id = :uid') + ->where('u.id = :id'); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id'); + } + + public function testAndWhere() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->where('u.id = :id') + ->andWhere('u.username = :username'); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id AND u.username = :username'); + } + + public function testOrWhere() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->where('u.id = :id') + ->orWhere('u.username = :username'); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id OR u.username = :username'); + } + + /* + public function testWhereIn() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->whereIn('u.id', array(1)); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id IN(1)'); + } + + public function testWhereNotIn() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->whereNotIn('u.id', array(1)); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id NOT IN(1)'); + } + + public function testAndWhereIn() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->where('u.id = :id') + ->andWhereIn('u.id', array(1, 2, 3)); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id AND u.id IN(1, 2, 3)'); + } + + public function testAndWhereNotIn() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->where('u.id = :id') + ->andWhereNotIn('u.id', array(1, 2, 3)); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id AND u.id NOT IN(1, 2, 3)'); + } + + public function testOrWhereIn() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->where('u.id = :id') + ->orWhereIn('u.id', array(1, 2, 3)); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id OR u.id IN(1, 2, 3)'); + } + + public function testOrWhereNotIn() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->where('u.id = :id') + ->orWhereNotIn('u.id', array(1, 2, 3)); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id = :id OR u.id NOT IN(1, 2, 3)'); + } + */ + + public function testGroupBy() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->groupBy('u.id'); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u GROUP BY u.id'); + } + + public function testHaving() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->groupBy('u.id') + ->having('COUNT(u.id) > 1'); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u GROUP BY u.id HAVING COUNT(u.id) > 1'); + } + + public function testAndHaving() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->groupBy('u.id') + ->having('COUNT(u.id) > 1') + ->andHaving('COUNT(u.id) < 1'); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u GROUP BY u.id HAVING COUNT(u.id) > 1 AND COUNT(u.id) < 1'); + } + + public function testOrHaving() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->groupBy('u.id') + ->having('COUNT(u.id) > 1') + ->andHaving('COUNT(u.id) < 1') + ->orHaving('COUNT(u.id) > 1'); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u GROUP BY u.id HAVING COUNT(u.id) > 1 AND COUNT(u.id) < 1 OR COUNT(u.id) > 1'); + } + + public function testOrderBy() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->orderBy('u.username', 'ASC'); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u ORDER BY u.username ASC'); + } + + public function testAddOrderBy() + { + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->orderBy('u.username', 'ASC') + ->addOrderBy('u.username', 'DESC'); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u ORDER BY u.username ASC, u.username DESC'); + } + + public function testLimit() + { + /* + TODO: Limit fails. Is this not implemented in the DQL parser? Will look tomorrow. + $qb = QueryBuilder::create($this->_em) + ->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->limit(10) + ->offset(0); + + $this->assertValidQueryBuilder($qb, 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u LIMIT 10'); + */ + } +} \ No newline at end of file