Skip to content

Commit

Permalink
SQL AST: ifnull/nullif improvements (#592)
Browse files Browse the repository at this point in the history
Co-authored-by: Markus Staab <[email protected]>
  • Loading branch information
hemberger and staabm authored Apr 9, 2023
1 parent 5d759ac commit 3a3ba02
Show file tree
Hide file tree
Showing 10 changed files with 5,604 additions and 1,357 deletions.
73 changes: 66 additions & 7 deletions src/SqlAst/IfNullReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,79 @@ final class IfNullReturnTypeExtension implements QueryFunctionReturnTypeExtensio
{
public function isFunctionSupported(FunctionCall $expression): bool
{
return \in_array($expression->getFunction()->getName(), [BuiltInFunction::IFNULL, BuiltInFunction::NULLIF], true);
return \in_array($expression->getFunction()->getName(), [BuiltInFunction::IFNULL], true);
}

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

$results = [];
foreach ($args as $arg) {
$argType = $scope->getType($arg);
if (2 !== \count($args)) {
return null;
}

$argType1 = $scope->getType($args[0]);
$argType2 = $scope->getType($args[1]);

// If arg1 is literal null, arg2 is always returned
if ($argType1->isNull()->yes()) {
return $argType2;
}

$results[] = $argType;
$arg1ContainsNull = TypeCombinator::containsNull($argType1);
$arg2ContainsNull = TypeCombinator::containsNull($argType2);
$argType1NoNull = TypeCombinator::removeNull($argType1);
$argType2NoNull = TypeCombinator::removeNull($argType2);

// If arg1 can be null, the result can be arg1 or arg2;
// otherwise, the result can only be arg1.
if ($arg1ContainsNull) {
$resultType = TypeCombinator::union($argType1NoNull, $argType2NoNull);
} else {
$resultType = $argType1;
}

// The result type is always the "more general" of the two args
// in the order: string, float, integer.
// see https://dev.mysql.com/doc/refman/5.7/en/flow-control-functions.html#function_ifnull
if ($this->isResultString($argType1NoNull, $argType2NoNull)) {
$resultType = $resultType->toString();
} elseif ($this->isResultFloat($argType1NoNull, $argType2NoNull)) {
$resultType = $resultType->toFloat();
}

return TypeCombinator::union(...$results);
// Re-add null if arg2 can contain null
if ($arg2ContainsNull) {
$resultType = TypeCombinator::addNull($resultType);
}
return $resultType;
}

private function isResultString(Type $type1, Type $type2): bool
{
return (
// If either arg is a string, the result is a string
$type1->isString()->yes() ||
$type2->isString()->yes() ||

// Special case where args are a constant float and an int
// results in a numeric string
(
$type1->isConstantScalarValue()->yes() &&
$type1->isFloat()->yes() &&
$type2->isInteger()->yes()
) ||
(
$type2->isConstantScalarValue()->yes() &&
$type2->isFloat()->yes() &&
$type1->isInteger()->yes()
)
);
}

private function isResultFloat(Type $type1, Type $type2): bool
{
// If either arg is a float, the result is a float
return $type1->isFloat()->yes() || $type2->isFloat()->yes();
}
}
47 changes: 47 additions & 0 deletions src/SqlAst/NullIfReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace staabm\PHPStanDba\SqlAst;

use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use SqlFtw\Sql\Expression\BuiltInFunction;
use SqlFtw\Sql\Expression\FunctionCall;

final class NullIfReturnTypeExtension implements QueryFunctionReturnTypeExtension
{
public function isFunctionSupported(FunctionCall $expression): bool
{
return \in_array($expression->getFunction()->getName(), [BuiltInFunction::NULLIF], true);
}

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

if (2 !== \count($args)) {
return null;
}

$argType1 = $scope->getType($args[0]);
$argType2 = $scope->getType($args[1]);

// Return null type if scalar constants are equal
if (
$argType1->isConstantScalarValue()->yes() &&
$argType1->equals($argType2)
) {
return new NullType();
}

// If the types *can* be equal, we return the first type or null type
if ($argType1->isSuperTypeOf($argType2)->yes() || $argType2->isSuperTypeOf($argType1)->yes()) {
return TypeCombinator::addNull($argType1);
}

// Otherwise the first type is returned
return $argType1;
}
}
1 change: 1 addition & 0 deletions src/SqlAst/QueryScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function __construct(Table $fromTable, array $joinedTables)
new PositiveIntReturnTypeExtension(),
new CoalesceReturnTypeExtension(),
new IfNullReturnTypeExtension(),
new NullIfReturnTypeExtension(),
new IfReturnTypeExtension(),
new ConcatReturnTypeExtension(),
new InstrReturnTypeExtension(),
Expand Down
Loading

0 comments on commit 3a3ba02

Please sign in to comment.