From 699ccfddb6050136e30eb45724761276b3afc64a Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Fri, 17 Jun 2011 16:16:22 -0300 Subject: [PATCH] Implemented COALESCE and NULLIF support in DQL. --- lib/Doctrine/ORM/Query/AST/FromClause.php | 3 - lib/Doctrine/ORM/Query/Parser.php | 75 ++++++++++++++++-- lib/Doctrine/ORM/Query/SqlWalker.php | 78 ++++++++++++++++++- .../ORM/Query/LanguageRecognitionTest.php | 10 +++ .../ORM/Query/SelectSqlGenerationTest.php | 16 ++++ 5 files changed, 170 insertions(+), 12 deletions(-) diff --git a/lib/Doctrine/ORM/Query/AST/FromClause.php b/lib/Doctrine/ORM/Query/AST/FromClause.php index 5325ea795..83aa85ccb 100644 --- a/lib/Doctrine/ORM/Query/AST/FromClause.php +++ b/lib/Doctrine/ORM/Query/AST/FromClause.php @@ -1,7 +1,5 @@ * @author Jonathan Wage * @author Roman Borschel diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 791f765e9..20a5122e6 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -1644,6 +1644,10 @@ class Parser return $this->StateFieldPathExpression(); } else if ($lookahead == Lexer::T_INTEGER || $lookahead == Lexer::T_FLOAT) { return $this->SimpleArithmeticExpression(); + } else if ($lookahead == Lexer::T_CASE || $lookahead == Lexer::T_COALESCE || $lookahead == Lexer::T_NULLIF) { + // Since NULLIF and COALESCE can be identified as a function, + // we need to check if before check for FunctionDeclaration + return $this->CaseExpression(); } else if ($this->_isFunction() || $this->_isAggregateFunction($this->_lexer->lookahead['type'])) { // We may be in an ArithmeticExpression (find the matching ")" and inspect for Math operator) $this->_lexer->peek(); // "(" @@ -1665,8 +1669,6 @@ class Parser } else if ($lookahead == Lexer::T_TRUE || $lookahead == Lexer::T_FALSE) { $this->match($lookahead); return new AST\Literal(AST\Literal::BOOLEAN, $this->_lexer->token['value']); - } else if ($lookahead == Lexer::T_CASE || $lookahead == Lexer::T_COALESCE || $lookahead == Lexer::T_NULLIF) { - return $this->CaseExpression(); } else { $this->syntaxError(); } @@ -1674,11 +1676,66 @@ class Parser public function CaseExpression() { + $lookahead = $this->_lexer->lookahead['type']; + // if "CASE" "WHEN" => GeneralCaseExpression // else if "CASE" => SimpleCaseExpression - // else if "COALESCE" => CoalesceExpression - // else if "NULLIF" => NullifExpression - $this->semanticalError('CaseExpression not yet supported.'); + // [DONE] else if "COALESCE" => CoalesceExpression + // [DONE] else if "NULLIF" => NullifExpression + switch ($lookahead) { + case Lexer::T_NULLIF: + return $this->NullIfExpression(); + + case Lexer::T_COALESCE: + return $this->CoalesceExpression(); + + default: + $this->semanticalError('CaseExpression not yet supported.'); + return null; + } + } + + /** + * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" + * + * @return Doctrine\ORM\Query\AST\CoalesceExpression + */ + public function CoalesceExpression() + { + $this->match(Lexer::T_COALESCE); + $this->match(Lexer::T_OPEN_PARENTHESIS); + + // Process ScalarExpressions (1..N) + $scalarExpressions = array(); + $scalarExpressions[] = $this->ScalarExpression(); + + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $scalarExpressions[] = $this->ScalarExpression(); + } + + $this->match(Lexer::T_CLOSE_PARENTHESIS); + + return new AST\CoalesceExpression($scalarExpressions); + } + + /** + * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" + * + * @return Doctrine\ORM\Query\AST\ExistsExpression + */ + public function NullIfExpression() + { + $this->match(Lexer::T_NULLIF); + $this->match(Lexer::T_OPEN_PARENTHESIS); + + $firstExpression = $this->ScalarExpression(); + $this->match(Lexer::T_COMMA); + $secondExpression = $this->ScalarExpression(); + + $this->match(Lexer::T_CLOSE_PARENTHESIS); + + return new AST\NullIfExpression($firstExpression, $secondExpression); } /** @@ -1717,12 +1774,16 @@ class Parser } } else if ($this->_isFunction()) { $this->_lexer->peek(); // "(" - $beyond = $this->_peekBeyondClosingParenthesis(); - + + $lookaheadType = $this->_lexer->lookahead['type']; + $beyond = $this->_peekBeyondClosingParenthesis(); + if ($this->_isMathOperator($beyond)) { $expression = $this->ScalarExpression(); } else if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { $expression = $this->AggregateExpression(); + } else if (in_array ($lookaheadType, array(Lexer::T_CASE, Lexer::T_COALESCE, Lexer::T_NULLIF))) { + $expression = $this->CaseExpression(); } else { // Shortcut: ScalarExpression => Function $expression = $this->FunctionDeclaration(); diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 270131ea2..f36e0ae8f 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -873,6 +873,60 @@ class SqlWalker implements TreeWalker return $sql; } + + /** + * Walks down a CoalesceExpression AST node and generates the corresponding SQL. + * + * @param CoalesceExpression $coalesceExpression + * @return string The SQL. + */ + public function walkCoalesceExpression($coalesceExpression) + { + $sql = 'COALESCE('; + + $scalarExpressions = array(); + + foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { + $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression); + } + + $sql .= implode(', ', $scalarExpressions) . ')'; + + return $sql; + } + + public function walkCaseExpression($expression) + { + switch (true) { + case ($expression instanceof AST\CoalesceExpression): + return $this->walkCoalesceExpression($expression); + + case ($expression instanceof AST\NullIfExpression): + return $this->walkNullIfExpression($expression); + + default: + return ''; + } + } + + /** + * Walks down a NullIfExpression AST node and generates the corresponding SQL. + * + * @param NullIfExpression $nullIfExpression + * @return string The SQL. + */ + public function walkNullIfExpression($nullIfExpression) + { + $firstExpression = is_string($nullIfExpression->firstExpression) + ? $this->_conn->quote($nullIfExpression->firstExpression) + : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression); + + $secondExpression = is_string($nullIfExpression->secondExpression) + ? $this->_conn->quote($nullIfExpression->secondExpression) + : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression); + + return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')'; + } /** * Walks down a SelectExpression AST node and generates the corresponding SQL. @@ -956,8 +1010,7 @@ class SqlWalker implements TreeWalker $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); $this->_rsm->addScalarResult($columnAlias, $resultAlias); - } - else if ( + } else if ( $expr instanceof AST\SimpleArithmeticExpression || $expr instanceof AST\ArithmeticTerm || $expr instanceof AST\ArithmeticFactor || @@ -971,11 +1024,32 @@ class SqlWalker implements TreeWalker } $columnAlias = 'sclr' . $this->_aliasCounter++; + if ($expr instanceof AST\Literal) { $sql .= $this->walkLiteral($expr) . ' AS ' .$columnAlias; } else { $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; } + + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; + + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } else if ( + $expr instanceof AST\NullIfExpression || + $expr instanceof AST\CoalesceExpression || + $expr instanceof AST\CaseExpression + ) { + if ( ! $selectExpression->fieldIdentificationVariable) { + $resultAlias = $this->_scalarResultCounter++; + } else { + $resultAlias = $selectExpression->fieldIdentificationVariable; + } + + $columnAlias = 'sclr' . $this->_aliasCounter++; + + $sql .= $this->walkCaseExpression($expr) . ' AS ' . $columnAlias; + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index 2733d80f7..cf9575869 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -570,6 +570,16 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase { $this->assertValidDQL("SELECT e, t FROM Doctrine\Tests\Models\DDC117\DDC117Editor e JOIN e.reviewingTranslations t WHERE SIZE(e.reviewingTranslations) > 0"); } + + public function testCaseSupportContainingNullIfExpression() + { + $this->assertValidDQL("SELECT u.id, NULLIF(u.name, u.name) AS shouldBeNull FROM Doctrine\Tests\Models\CMS\CmsUser u"); + } + + public function testCaseSupportContainingCoalesceExpression() + { + $this->assertValidDQL("select COALESCE(NULLIF(u.name, ''), u.username) as Display FROM Doctrine\Tests\Models\CMS\CmsUser u"); + } } /** @Entity */ diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 6652ad67a..deffb9232 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -906,6 +906,22 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase 'SELECT c0_.id AS id0, c0_.name AS name1, count(c1_.id) AS sclr2 FROM cms_groups c0_ INNER JOIN cms_users_groups c2_ ON c0_.id = c2_.group_id INNER JOIN cms_users c1_ ON c1_.id = c2_.user_id GROUP BY c0_.id' ); } + + public function testCaseContainingNullIf() + { + $this->assertSqlGeneration( + "SELECT NULLIF(g.id, g.name) AS NullIfEqual FROM Doctrine\Tests\Models\CMS\CmsGroup g", + 'SELECT NULLIF(c0_.id, c0_.name) AS sclr0 FROM cms_groups c0_' + ); + } + + public function testCaseContainingCoalesce() + { + $this->assertSqlGeneration( + "SELECT COALESCE(NULLIF(u.name, ''), u.username) as Display FROM Doctrine\Tests\Models\CMS\CmsUser u", + "SELECT COALESCE(NULLIF(c0_.name, ''), c0_.username) AS sclr0 FROM cms_users c0_" + ); + } }