Skip to content

Commit e56aa3b

Browse files
authored
Merge pull request #12541 from HypeMC/case-null
Allow `NULL` literal in `SELECT` and `CASE` expressions
2 parents 1a5102c + 3beedf6 commit e56aa3b

8 files changed

Lines changed: 137 additions & 4 deletions

File tree

docs/en/reference/dql-doctrine-query-language.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ You can also nest several DTO :
617617
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, NEW AddressDTO(a.street, a.city, a.zip)) FROM Customer c JOIN c.email e JOIN c.address a');
618618
$users = $query->getResult(); // array of CustomerDTO
619619
620-
Note that you can only pass scalar expressions or other Data Transfer Objects to the constructor.
620+
Note that you can only pass scalar expressions, the ``NULL`` literal or other Data Transfer Objects to the constructor.
621621

622622
If you use your data transfer objects for multiple queries, and you would rather not have to
623623
specify arguments that precede the ones you are really interested in, you can use named arguments.
@@ -1780,7 +1780,7 @@ Scalar and Type Expressions
17801780

17811781
.. code-block:: php
17821782
1783-
ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | StateFieldPathExpression | BooleanPrimary | CaseExpression | InstanceOfExpression
1783+
ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | StateFieldPathExpression | BooleanPrimary | CaseExpression | InstanceOfExpression | "NULL"
17841784
StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
17851785
StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
17861786
BooleanExpression ::= BooleanPrimary | "(" Subselect ")"

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2725,7 +2725,7 @@ parameters:
27252725
path: src/Query/SqlWalker.php
27262726

27272727
-
2728-
message: '#^Match arm comparison between 3 and 3 is always true\.$#'
2728+
message: '#^Match arm comparison between 4 and 4 is always true\.$#'
27292729
identifier: match.alwaysTrue
27302730
count: 1
27312731
path: src/Query/SqlWalker.php

src/Query/AST/Literal.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Literal extends Node
1111
final public const STRING = 1;
1212
final public const BOOLEAN = 2;
1313
final public const NUMERIC = 3;
14+
final public const NULL = 4;
1415

1516
/** @phpstan-param self::* $type */
1617
public function __construct(

src/Query/Parser.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1951,7 +1951,7 @@ public function IndexBy(): AST\IndexBy
19511951
/**
19521952
* ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary |
19531953
* StateFieldPathExpression | BooleanPrimary | CaseExpression |
1954-
* InstanceOfExpression
1954+
* InstanceOfExpression | "NULL"
19551955
*
19561956
* @return mixed One of the possible expressions or subexpressions.
19571957
*/
@@ -1979,6 +1979,11 @@ public function ScalarExpression(): mixed
19791979

19801980
return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value);
19811981

1982+
case $lookahead === TokenType::T_NULL:
1983+
$this->match(TokenType::T_NULL);
1984+
1985+
return new AST\Literal(AST\Literal::NULL, null);
1986+
19821987
case $lookahead === TokenType::T_INPUT_PARAMETER:
19831988
return match (true) {
19841989
$this->isMathOperator($peek) => $this->SimpleArithmeticExpression(),
@@ -2199,6 +2204,11 @@ public function SelectExpression(): AST\SelectExpression
21992204
$expression = $identVariable = $this->IdentificationVariable();
22002205
break;
22012206

2207+
// ScalarExpression (NULL)
2208+
case $lookaheadType === TokenType::T_NULL:
2209+
$expression = $this->ScalarExpression();
2210+
break;
2211+
22022212
// CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
22032213
case $lookaheadType === TokenType::T_CASE:
22042214
case $lookaheadType === TokenType::T_COALESCE:

src/Query/SqlWalker.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2099,6 +2099,7 @@ public function walkLiteral(AST\Literal $literal): string
20992099
AST\Literal::STRING => $this->conn->quote($literal->value),
21002100
AST\Literal::BOOLEAN => (string) $this->conn->getDatabasePlatform()->convertBooleans(strtolower($literal->value) === 'true'),
21012101
AST\Literal::NUMERIC => (string) $literal->value,
2102+
AST\Literal::NULL => 'NULL',
21022103
default => throw QueryException::invalidLiteral($literal),
21032104
};
21042105
}

tests/Tests/ORM/Functional/NewOperatorTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,47 @@ public function testShouldSupportLiteralExpression(): void
266266
self::assertEquals(123, $result[2]->phonenumbers);
267267
}
268268

269+
public function testShouldSupportNullLiteralExpression(): void
270+
{
271+
$dql = "
272+
SELECT
273+
new Doctrine\Tests\Models\CMS\CmsUserDTO(
274+
u.name,
275+
'fabio.bat.silva@gmail.com',
276+
null,
277+
123
278+
)
279+
FROM
280+
Doctrine\Tests\Models\CMS\CmsUser u
281+
ORDER BY
282+
u.name";
283+
284+
$query = $this->_em->createQuery($dql);
285+
$result = $query->getResult();
286+
287+
self::assertCount(3, $result);
288+
289+
self::assertInstanceOf(CmsUserDTO::class, $result[0]);
290+
self::assertInstanceOf(CmsUserDTO::class, $result[1]);
291+
self::assertInstanceOf(CmsUserDTO::class, $result[2]);
292+
293+
self::assertEquals($this->fixtures[0]->name, $result[0]->name);
294+
self::assertEquals($this->fixtures[1]->name, $result[1]->name);
295+
self::assertEquals($this->fixtures[2]->name, $result[2]->name);
296+
297+
self::assertEquals('fabio.bat.silva@gmail.com', $result[0]->email);
298+
self::assertEquals('fabio.bat.silva@gmail.com', $result[1]->email);
299+
self::assertEquals('fabio.bat.silva@gmail.com', $result[2]->email);
300+
301+
self::assertNull($result[0]->address);
302+
self::assertNull($result[1]->address);
303+
self::assertNull($result[2]->address);
304+
305+
self::assertEquals(123, $result[0]->phonenumbers);
306+
self::assertEquals(123, $result[1]->phonenumbers);
307+
self::assertEquals(123, $result[2]->phonenumbers);
308+
}
309+
269310
public function testShouldSupportCaseExpression(): void
270311
{
271312
$dql = "

tests/Tests/ORM/Query/LanguageRecognitionTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,36 @@ public function testCaseSupportContainingCoalesceExpression(): void
640640
$this->assertValidDQL("select COALESCE(NULLIF(u.name, ''), u.username) as Display FROM Doctrine\Tests\Models\CMS\CmsUser u");
641641
}
642642

643+
public function testSelectClauseSupportsNullLiteral(): void
644+
{
645+
$this->assertValidDQL('SELECT u.name, NULL FROM Doctrine\Tests\Models\CMS\CmsUser u');
646+
}
647+
648+
public function testSelectClauseSupportsAliasedNullLiteral(): void
649+
{
650+
$this->assertValidDQL('SELECT NULL AS nil FROM Doctrine\Tests\Models\CMS\CmsUser u');
651+
}
652+
653+
public function testSubselectSupportsNullLiteral(): void
654+
{
655+
$this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id IN (SELECT NULL FROM Doctrine\Tests\Models\CMS\CmsUser u2)');
656+
}
657+
658+
public function testGeneralCaseSupportsNullLiteralInThenAndElseClauses(): void
659+
{
660+
$this->assertValidDQL('SELECT CASE WHEN u.id > 10 THEN NULL ELSE NULL END FROM Doctrine\Tests\Models\CMS\CmsUser u');
661+
}
662+
663+
public function testSimpleCaseSupportsNullLiteralInThenAndElseClauses(): void
664+
{
665+
$this->assertValidDQL("SELECT CASE u.name WHEN 'admin' THEN NULL ELSE NULL END FROM Doctrine\Tests\Models\CMS\CmsUser u");
666+
}
667+
668+
public function testCoalesceSupportsNullLiteral(): void
669+
{
670+
$this->assertValidDQL('SELECT COALESCE(NULL, u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u');
671+
}
672+
643673
#[Group('DDC-1858')]
644674
public function testHavingSupportIsNullExpression(): void
645675
{
@@ -670,6 +700,11 @@ public function testNewLiteralWithSubselectExpression(): void
670700
$this->assertValidDQL('SELECT new ' . __NAMESPACE__ . "\\DummyStruct(u.id, 'foo', (SELECT 1 FROM Doctrine\Tests\Models\CMS\CmsUser su), true) FROM Doctrine\Tests\Models\CMS\CmsUser u");
671701
}
672702

703+
public function testNewNullLiteralExpression(): void
704+
{
705+
$this->assertValidDQL('SELECT new ' . __NAMESPACE__ . "\\DummyStruct(u.id, 'foo', null, true) FROM Doctrine\Tests\Models\CMS\CmsUser u");
706+
}
707+
673708
public function testStringPrimaryAcceptsAggregateExpression(): void
674709
{
675710
$this->assertValidDQL(

tests/Tests/ORM/Query/SelectSqlGenerationTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,46 @@ public function testSimpleCaseWithMultipleWhenClauseInSubselect(): void
12771277
);
12781278
}
12791279

1280+
public function testNullLiteralInSelectClause(): void
1281+
{
1282+
$this->assertSqlGeneration(
1283+
'SELECT g.id, NULL FROM Doctrine\Tests\Models\CMS\CmsGroup g',
1284+
'SELECT c0_.id AS id_0, NULL AS sclr_1 FROM cms_groups c0_',
1285+
);
1286+
}
1287+
1288+
public function testAliasedNullLiteralInSelectClause(): void
1289+
{
1290+
$this->assertSqlGeneration(
1291+
'SELECT g.id, NULL AS nil FROM Doctrine\Tests\Models\CMS\CmsGroup g',
1292+
'SELECT c0_.id AS id_0, NULL AS sclr_1 FROM cms_groups c0_',
1293+
);
1294+
}
1295+
1296+
public function testNullLiteralInSubselect(): void
1297+
{
1298+
$this->assertSqlGeneration(
1299+
'SELECT g.id FROM Doctrine\Tests\Models\CMS\CmsGroup g WHERE g.id IN (SELECT NULL FROM Doctrine\Tests\Models\CMS\CmsGroup g2)',
1300+
'SELECT c0_.id AS id_0 FROM cms_groups c0_ WHERE c0_.id IN (SELECT NULL AS sclr_1 FROM cms_groups c1_)',
1301+
);
1302+
}
1303+
1304+
public function testGeneralCaseWithNullLiteralInElseClause(): void
1305+
{
1306+
$this->assertSqlGeneration(
1307+
'SELECT g.id, COUNT(CASE WHEN ((g.id / 2) > 18) THEN 1 ELSE NULL END) AS test FROM Doctrine\Tests\Models\CMS\CmsGroup g GROUP BY g.id',
1308+
'SELECT c0_.id AS id_0, COUNT(CASE WHEN ((c0_.id / 2) > 18) THEN 1 ELSE NULL END) AS sclr_1 FROM cms_groups c0_ GROUP BY c0_.id',
1309+
);
1310+
}
1311+
1312+
public function testSimpleCaseWithNullLiteralInThenClause(): void
1313+
{
1314+
$this->assertSqlGeneration(
1315+
"SELECT g.id, CASE g.name WHEN 'admin' THEN NULL ELSE 1 END AS test FROM Doctrine\Tests\Models\CMS\CmsGroup g",
1316+
"SELECT c0_.id AS id_0, CASE c0_.name WHEN 'admin' THEN NULL ELSE 1 END AS sclr_1 FROM cms_groups c0_",
1317+
);
1318+
}
1319+
12801320
#[Group('DDC-1696')]
12811321
public function testSimpleCaseWithStringPrimary(): void
12821322
{
@@ -1708,6 +1748,11 @@ public function testSupportsNewOperator(): void
17081748
'SELECT new Doctrine\Tests\Models\CMS\CmsUserDTO(a.id, (SELECT 1 FROM Doctrine\Tests\Models\CMS\CmsUser su), a.country, a.city), new Doctrine\Tests\Models\CMS\CmsAddressDTO(u.name, e.email) FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.email e JOIN u.address a ORDER BY u.name',
17091749
'SELECT c0_.id AS sclr_0, (SELECT 1 AS sclr_2 FROM cms_users c1_) AS sclr_1, c0_.country AS sclr_3, c0_.city AS sclr_4, c2_.name AS sclr_5, c3_.email AS sclr_6 FROM cms_users c2_ INNER JOIN cms_emails c3_ ON c2_.email_id = c3_.id INNER JOIN cms_addresses c0_ ON c2_.id = c0_.user_id ORDER BY c2_.name ASC',
17101750
);
1751+
1752+
$this->assertSqlGeneration(
1753+
'SELECT new Doctrine\Tests\Models\CMS\CmsUserDTO(u.name, e.email, NULL, 123) FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.email e',
1754+
'SELECT c0_.name AS sclr_0, c1_.email AS sclr_1, NULL AS sclr_2, 123 AS sclr_3 FROM cms_users c0_ INNER JOIN cms_emails c1_ ON c0_.email_id = c1_.id',
1755+
);
17111756
}
17121757

17131758
#[Group('DDC-2234')]

0 commit comments

Comments
 (0)