Skip to content

Commit 3a3ba02

Browse files
hembergerstaabm
andauthored
SQL AST: ifnull/nullif improvements (#592)
Co-authored-by: Markus Staab <[email protected]>
1 parent 5d759ac commit 3a3ba02

10 files changed

+5604
-1357
lines changed

src/SqlAst/IfNullReturnTypeExtension.php

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,79 @@ final class IfNullReturnTypeExtension implements QueryFunctionReturnTypeExtensio
1313
{
1414
public function isFunctionSupported(FunctionCall $expression): bool
1515
{
16-
return \in_array($expression->getFunction()->getName(), [BuiltInFunction::IFNULL, BuiltInFunction::NULLIF], true);
16+
return \in_array($expression->getFunction()->getName(), [BuiltInFunction::IFNULL], true);
1717
}
1818

19-
public function getReturnType(FunctionCall $expression, QueryScope $scope): Type
19+
public function getReturnType(FunctionCall $expression, QueryScope $scope): ?Type
2020
{
2121
$args = $expression->getArguments();
2222

23-
$results = [];
24-
foreach ($args as $arg) {
25-
$argType = $scope->getType($arg);
23+
if (2 !== \count($args)) {
24+
return null;
25+
}
26+
27+
$argType1 = $scope->getType($args[0]);
28+
$argType2 = $scope->getType($args[1]);
29+
30+
// If arg1 is literal null, arg2 is always returned
31+
if ($argType1->isNull()->yes()) {
32+
return $argType2;
33+
}
2634

27-
$results[] = $argType;
35+
$arg1ContainsNull = TypeCombinator::containsNull($argType1);
36+
$arg2ContainsNull = TypeCombinator::containsNull($argType2);
37+
$argType1NoNull = TypeCombinator::removeNull($argType1);
38+
$argType2NoNull = TypeCombinator::removeNull($argType2);
39+
40+
// If arg1 can be null, the result can be arg1 or arg2;
41+
// otherwise, the result can only be arg1.
42+
if ($arg1ContainsNull) {
43+
$resultType = TypeCombinator::union($argType1NoNull, $argType2NoNull);
44+
} else {
45+
$resultType = $argType1;
46+
}
47+
48+
// The result type is always the "more general" of the two args
49+
// in the order: string, float, integer.
50+
// see https://dev.mysql.com/doc/refman/5.7/en/flow-control-functions.html#function_ifnull
51+
if ($this->isResultString($argType1NoNull, $argType2NoNull)) {
52+
$resultType = $resultType->toString();
53+
} elseif ($this->isResultFloat($argType1NoNull, $argType2NoNull)) {
54+
$resultType = $resultType->toFloat();
2855
}
2956

30-
return TypeCombinator::union(...$results);
57+
// Re-add null if arg2 can contain null
58+
if ($arg2ContainsNull) {
59+
$resultType = TypeCombinator::addNull($resultType);
60+
}
61+
return $resultType;
62+
}
63+
64+
private function isResultString(Type $type1, Type $type2): bool
65+
{
66+
return (
67+
// If either arg is a string, the result is a string
68+
$type1->isString()->yes() ||
69+
$type2->isString()->yes() ||
70+
71+
// Special case where args are a constant float and an int
72+
// results in a numeric string
73+
(
74+
$type1->isConstantScalarValue()->yes() &&
75+
$type1->isFloat()->yes() &&
76+
$type2->isInteger()->yes()
77+
) ||
78+
(
79+
$type2->isConstantScalarValue()->yes() &&
80+
$type2->isFloat()->yes() &&
81+
$type1->isInteger()->yes()
82+
)
83+
);
84+
}
85+
86+
private function isResultFloat(Type $type1, Type $type2): bool
87+
{
88+
// If either arg is a float, the result is a float
89+
return $type1->isFloat()->yes() || $type2->isFloat()->yes();
3190
}
3291
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace staabm\PHPStanDba\SqlAst;
6+
7+
use PHPStan\Type\NullType;
8+
use PHPStan\Type\Type;
9+
use PHPStan\Type\TypeCombinator;
10+
use SqlFtw\Sql\Expression\BuiltInFunction;
11+
use SqlFtw\Sql\Expression\FunctionCall;
12+
13+
final class NullIfReturnTypeExtension implements QueryFunctionReturnTypeExtension
14+
{
15+
public function isFunctionSupported(FunctionCall $expression): bool
16+
{
17+
return \in_array($expression->getFunction()->getName(), [BuiltInFunction::NULLIF], true);
18+
}
19+
20+
public function getReturnType(FunctionCall $expression, QueryScope $scope): ?Type
21+
{
22+
$args = $expression->getArguments();
23+
24+
if (2 !== \count($args)) {
25+
return null;
26+
}
27+
28+
$argType1 = $scope->getType($args[0]);
29+
$argType2 = $scope->getType($args[1]);
30+
31+
// Return null type if scalar constants are equal
32+
if (
33+
$argType1->isConstantScalarValue()->yes() &&
34+
$argType1->equals($argType2)
35+
) {
36+
return new NullType();
37+
}
38+
39+
// If the types *can* be equal, we return the first type or null type
40+
if ($argType1->isSuperTypeOf($argType2)->yes() || $argType2->isSuperTypeOf($argType1)->yes()) {
41+
return TypeCombinator::addNull($argType1);
42+
}
43+
44+
// Otherwise the first type is returned
45+
return $argType1;
46+
}
47+
}

src/SqlAst/QueryScope.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public function __construct(Table $fromTable, array $joinedTables)
5959
new PositiveIntReturnTypeExtension(),
6060
new CoalesceReturnTypeExtension(),
6161
new IfNullReturnTypeExtension(),
62+
new NullIfReturnTypeExtension(),
6263
new IfReturnTypeExtension(),
6364
new ConcatReturnTypeExtension(),
6465
new InstrReturnTypeExtension(),

0 commit comments

Comments
 (0)