Skip to content

Commit 4aa0986

Browse files
authored
Merge pull request #7941 from Grafikart/feat-typed-functions
Allow DQL functions to specify return type
2 parents d90df59 + 24e9a7c commit 4aa0986

File tree

8 files changed

+147
-7
lines changed

8 files changed

+147
-7
lines changed

lib/Doctrine/ORM/Query/AST/Functions/AbsFunction.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
/**
2525
* "ABS" "(" SimpleArithmeticExpression ")"
2626
*
27-
*
27+
*
2828
* @link www.doctrine-project.org
2929
* @since 2.0
3030
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>

lib/Doctrine/ORM/Query/AST/Functions/CountFunction.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
namespace Doctrine\ORM\Query\AST\Functions;
2121

22+
use Doctrine\DBAL\Types\Type;
23+
use Doctrine\ORM\Query\AST\TypedExpression;
2224
use Doctrine\ORM\Query\Parser;
2325
use Doctrine\ORM\Query\SqlWalker;
2426
use Doctrine\ORM\Query\AST\AggregateExpression;
@@ -29,7 +31,7 @@
2931
* @since 2.6
3032
* @author Mathew Davies <thepixeldeveloper@icloud.com>
3133
*/
32-
final class CountFunction extends FunctionNode
34+
final class CountFunction extends FunctionNode implements TypedExpression
3335
{
3436
/**
3537
* @var AggregateExpression
@@ -51,4 +53,9 @@ public function parse(Parser $parser): void
5153
{
5254
$this->aggregateExpression = $parser->AggregateExpression();
5355
}
56+
57+
public function getReturnType() : Type
58+
{
59+
return Type::getType(Type::INTEGER);
60+
}
5461
}

lib/Doctrine/ORM/Query/AST/Functions/FunctionNode.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
/**
2525
* Abstract Function Node.
2626
*
27-
*
27+
*
2828
* @link www.doctrine-project.org
2929
* @since 2.0
3030
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>

lib/Doctrine/ORM/Query/AST/Functions/LengthFunction.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
namespace Doctrine\ORM\Query\AST\Functions;
2121

22+
use Doctrine\DBAL\Types\Type;
23+
use Doctrine\ORM\Query\AST\TypedExpression;
2224
use Doctrine\ORM\Query\Lexer;
2325

2426
/**
@@ -32,7 +34,7 @@
3234
* @author Roman Borschel <roman@code-factory.org>
3335
* @author Benjamin Eberlei <kontakt@beberlei.de>
3436
*/
35-
class LengthFunction extends FunctionNode
37+
class LengthFunction extends FunctionNode implements TypedExpression
3638
{
3739
public $stringPrimary;
3840

@@ -60,4 +62,9 @@ public function parse(\Doctrine\ORM\Query\Parser $parser)
6062

6163
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
6264
}
65+
66+
public function getReturnType() : Type
67+
{
68+
return Type::getType(Type::INTEGER);
69+
}
6370
}

lib/Doctrine/ORM/Query/AST/Functions/SqrtFunction.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
/**
2525
* "SQRT" "(" SimpleArithmeticExpression ")"
2626
*
27-
*
27+
*
2828
* @link www.doctrine-project.org
2929
* @since 2.0
3030
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM\Query\AST;
6+
7+
use Doctrine\DBAL\Types\Type;
8+
9+
/**
10+
* Provides an API for resolving the type of a Node
11+
*/
12+
interface TypedExpression
13+
{
14+
public function getReturnType() : Type;
15+
}

lib/Doctrine/ORM/Query/SqlWalker.php

+12-2
Original file line numberDiff line numberDiff line change
@@ -1339,10 +1339,20 @@ public function walkSelectExpression($selectExpression)
13391339

13401340
$this->scalarResultAliasMap[$resultAlias] = $columnAlias;
13411341

1342-
if ( ! $hidden) {
1343-
// We cannot resolve field type here; assume 'string'.
1342+
if ($hidden) {
1343+
break;
1344+
}
1345+
1346+
if (! $expr instanceof Query\AST\TypedExpression) {
1347+
// Conceptually we could resolve field type here by traverse through AST to retrieve field type,
1348+
// but this is not a feasible solution; assume 'string'.
13441349
$this->rsm->addScalarResult($columnAlias, $resultAlias, 'string');
1350+
1351+
break;
13451352
}
1353+
1354+
$this->rsm->addScalarResult($columnAlias, $resultAlias, $expr->getReturnType()->getName());
1355+
13461356
break;
13471357

13481358
case ($expr instanceof AST\Subselect):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket;
6+
7+
use DateTimeImmutable;
8+
use Doctrine\Tests\OrmFunctionalTestCase;
9+
use function ltrim;
10+
use function strlen;
11+
12+
/** @group GH7941 */
13+
final class GH7941Test extends OrmFunctionalTestCase
14+
{
15+
private const PRODUCTS = [
16+
['name' => 'Test 1', 'price' => '100', 'square_root' => '/^10(\.0+)?$/'],
17+
['name' => 'Test 2', 'price' => '100', 'square_root' => '/^10(\.0+)?$/'],
18+
['name' => 'Test 3', 'price' => '100', 'square_root' => '/^10(\.0+)?$/'],
19+
['name' => 'Test 4', 'price' => '25', 'square_root' => '/^5(\.0+)?$/'],
20+
['name' => 'Test 5', 'price' => '25', 'square_root' => '/^5(\.0+)?$/'],
21+
['name' => 'Test 6', 'price' => '-25', 'square_root' => '/^5(\.0+)?$/'],
22+
];
23+
24+
protected function setUp() : void
25+
{
26+
parent::setUp();
27+
28+
$this->setUpEntitySchema([GH7941Product::class]);
29+
30+
foreach (self::PRODUCTS as $product) {
31+
$this->_em->persist(new GH7941Product($product['name'], $product['price']));
32+
}
33+
34+
$this->_em->flush();
35+
$this->_em->clear();
36+
}
37+
38+
/** @test */
39+
public function typesShouldBeConvertedForDQLFunctions() : void
40+
{
41+
$query = $this->_em->createQuery(
42+
'SELECT
43+
COUNT(product.id) as count,
44+
SUM(product.price) as sales,
45+
AVG(product.price) as average
46+
FROM ' . GH7941Product::class . ' product'
47+
);
48+
49+
$result = $query->getSingleResult();
50+
51+
self::assertSame(6, $result['count']);
52+
self::assertSame('325', $result['sales']);
53+
self::assertRegExp('/^54\.16+7$/', $result['average']);
54+
55+
$query = $this->_em->createQuery(
56+
'SELECT
57+
ABS(product.price) as absolute,
58+
SQRT(ABS(product.price)) as square_root,
59+
LENGTH(product.name) as length
60+
FROM ' . GH7941Product::class . ' product'
61+
);
62+
63+
foreach ($query->getResult() as $i => $item) {
64+
$product = self::PRODUCTS[$i];
65+
66+
self::assertSame(ltrim($product['price'], '-'), $item['absolute']);
67+
self::assertSame(strlen($product['name']), $item['length']);
68+
self::assertRegExp($product['square_root'], $item['square_root']);
69+
}
70+
}
71+
}
72+
73+
/**
74+
* @Entity
75+
* @Table()
76+
*/
77+
class GH7941Product
78+
{
79+
/**
80+
* @Id
81+
* @GeneratedValue
82+
* @Column(type="integer")
83+
*/
84+
public $id;
85+
86+
/** @Column(type="string") */
87+
public $name;
88+
89+
/** @Column(type="decimal") */
90+
public $price;
91+
92+
/** @Column(type="datetime_immutable") */
93+
public $createdAt;
94+
95+
public function __construct(string $name, string $price)
96+
{
97+
$this->name = $name;
98+
$this->price = $price;
99+
$this->createdAt = new DateTimeImmutable();
100+
}
101+
}

0 commit comments

Comments
 (0)