Skip to content

Commit 2b184a5

Browse files
committed
Fix wrong coalesce type inference
1 parent b09104d commit 2b184a5

File tree

2 files changed

+99
-16
lines changed

2 files changed

+99
-16
lines changed

src/Type/Doctrine/Query/QueryResultTypeWalker.php

+83-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Doctrine\ORM\Query\ParserResult;
1515
use Doctrine\ORM\Query\SqlWalker;
1616
use PHPStan\ShouldNotHappenException;
17+
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1718
use PHPStan\Type\BooleanType;
1819
use PHPStan\Type\Constant\ConstantBooleanType;
1920
use PHPStan\Type\Constant\ConstantFloatType;
@@ -574,6 +575,79 @@ public function walkFunction($function): string
574575
}
575576
}
576577

578+
private function createFloat(bool $nullable): Type
579+
{
580+
$float = new FloatType();
581+
return $nullable ? TypeCombinator::addNull($float) : $float;
582+
}
583+
584+
private function createFloatOrInt(bool $nullable): Type // @phpstan-ignore-line unused, but kept to ease conflict resolution with origin (#506)
585+
{
586+
$union = TypeCombinator::union(
587+
new FloatType(),
588+
new IntegerType()
589+
);
590+
return $nullable ? TypeCombinator::addNull($union) : $union;
591+
}
592+
593+
private function createInteger(bool $nullable): Type
594+
{
595+
$integer = new IntegerType();
596+
return $nullable ? TypeCombinator::addNull($integer) : $integer;
597+
}
598+
599+
private function createNonNegativeInteger(bool $nullable): Type // @phpstan-ignore-line unused, but kept to ease conflict resolution with origin (#506)
600+
{
601+
$integer = IntegerRangeType::fromInterval(0, null);
602+
return $nullable ? TypeCombinator::addNull($integer) : $integer;
603+
}
604+
605+
private function createNumericString(bool $nullable): Type
606+
{
607+
$numericString = TypeCombinator::intersect(
608+
new StringType(),
609+
new AccessoryNumericStringType()
610+
);
611+
612+
return $nullable ? TypeCombinator::addNull($numericString) : $numericString;
613+
}
614+
615+
private function createString(bool $nullable): Type
616+
{
617+
$string = new StringType();
618+
return $nullable ? TypeCombinator::addNull($string) : $string;
619+
}
620+
621+
/**
622+
* E.g. to ensure SUM(1) is inferred as int, not 1
623+
*/
624+
private function generalizeConstantType(Type $type, bool $makeNullable): Type
625+
{
626+
$containsNull = $this->canBeNull($type);
627+
$typeNoNull = TypeCombinator::removeNull($type);
628+
629+
if (!$typeNoNull->isConstantScalarValue()->yes()) {
630+
$result = $type;
631+
632+
} elseif ($typeNoNull->isInteger()->yes()) {
633+
$result = $this->createInteger($containsNull);
634+
635+
} elseif ($typeNoNull->isFloat()->yes()) {
636+
$result = $this->createFloat($containsNull);
637+
638+
} elseif ($typeNoNull->isNumericString()->yes()) {
639+
$result = $this->createNumericString($containsNull);
640+
641+
} elseif ($typeNoNull->isString()->yes()) {
642+
$result = $this->createString($containsNull);
643+
644+
} else {
645+
$result = $type;
646+
}
647+
648+
return $makeNullable ? TypeCombinator::addNull($result) : $result;
649+
}
650+
577651
/**
578652
* @param AST\OrderByClause $orderByClause
579653
*/
@@ -641,7 +715,10 @@ public function walkCoalesceExpression($coalesceExpression): string
641715
$type = $this->unmarshalType($expression->dispatch($this));
642716
$allTypesContainNull = $allTypesContainNull && TypeCombinator::containsNull($type);
643717

644-
$expressionTypes[] = $type;
718+
// Some drivers manipulate the types, lets avoid false positives by generalizing constant types
719+
// e.g. sqlsrv: "COALESCE returns the data type of value with the highest precedence"
720+
// e.g. mysql: COALESCE(1, 'foo') === '1' (undocumented? https://gist.github.com/jrunning/4535434)
721+
$expressionTypes[] = $this->generalizeConstantType($type, false);
645722
}
646723

647724
$type = TypeCombinator::union(...$expressionTypes);
@@ -1399,6 +1476,11 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType
13991476
return $type;
14001477
}
14011478

1479+
private function canBeNull(Type $type): bool
1480+
{
1481+
return !$type->accepts(new NullType(), true)->no();
1482+
}
1483+
14021484
private function toNumericOrNull(Type $type): Type
14031485
{
14041486
return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {

tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php

+16-15
Original file line numberDiff line numberDiff line change
@@ -547,26 +547,18 @@ public function getTestData(): iterable
547547
$this->constantArray([
548548
[
549549
new ConstantIntegerType(1),
550-
TypeCombinator::union(
551-
new ConstantStringType('a'),
552-
new ConstantStringType('b')
553-
),
550+
new StringType(),
554551
],
555552
[
556553
new ConstantIntegerType(2),
557554
TypeCombinator::union(
558-
new ConstantIntegerType(1),
559-
new ConstantIntegerType(2),
560-
new ConstantStringType('1'),
561-
new ConstantStringType('2')
555+
$this->numericString(),
556+
new IntegerType()
562557
),
563558
],
564559
[
565560
new ConstantIntegerType(3),
566-
TypeCombinator::union(
567-
new ConstantStringType('1'),
568-
new ConstantStringType('2')
569-
),
561+
$this->numericString(),
570562
],
571563
]),
572564
'
@@ -823,11 +815,20 @@ public function getTestData(): iterable
823815
new ConstantIntegerType(3),
824816
$this->intStringified(),
825817
],
818+
[
819+
new ConstantIntegerType(4),
820+
TypeCombinator::union(
821+
new IntegerType(),
822+
new FloatType(),
823+
$this->numericString()
824+
),
825+
],
826826
]),
827827
'
828828
SELECT COALESCE(m.stringNullColumn, m.intColumn, false),
829829
COALESCE(m.stringNullColumn, m.stringNullColumn),
830-
COALESCE(NULLIF(m.intColumn, 1), 0)
830+
COALESCE(NULLIF(m.intColumn, 1), 0),
831+
COALESCE(1, 1.1)
831832
FROM QueryResult\Entities\Many m
832833
',
833834
];
@@ -1123,8 +1124,8 @@ public function getTestData(): iterable
11231124
[
11241125
new ConstantIntegerType(2),
11251126
TypeCombinator::union(
1126-
new ConstantIntegerType(1),
1127-
new ConstantStringType('1')
1127+
new IntegerType(),
1128+
$this->numericString()
11281129
),
11291130
],
11301131
[

0 commit comments

Comments
 (0)