Skip to content

Commit 3e04122

Browse files
committed
Test & fix mixed column
1 parent 6f73e49 commit 3e04122

File tree

4 files changed

+273
-39
lines changed

4 files changed

+273
-39
lines changed

src/Type/Doctrine/Query/QueryResultTypeWalker.php

Lines changed: 32 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use PHPStan\Type\IntersectionType;
3939
use PHPStan\Type\MixedType;
4040
use PHPStan\Type\NeverType;
41+
use PHPStan\Type\NullType;
4142
use PHPStan\Type\ObjectType;
4243
use PHPStan\Type\StringType;
4344
use PHPStan\Type\Type;
@@ -434,10 +435,10 @@ public function walkFunction($function): string
434435
$exprType = $this->generalizeLiteralType($exprType, false);
435436

436437
$exprTypeNoNull = TypeCombinator::removeNull($exprType);
437-
$nullable = TypeCombinator::containsNull($exprType);
438+
$nullable = $this->canBeNull($exprType);
438439

439440
if ($exprTypeNoNull->isInteger()->yes()) {
440-
$positiveInt = TypeCombinator::containsNull($exprType)
441+
$positiveInt = $this->canBeNull($exprType)
441442
? TypeCombinator::addNull(IntegerRangeType::fromInterval(0, null))
442443
: IntegerRangeType::fromInterval(0, null);
443444
return $this->marshalType($positiveInt);
@@ -459,20 +460,18 @@ public function walkFunction($function): string
459460
$secondExprType = $this->unmarshalType($function->secondArithmetic->dispatch($this));
460461

461462
$type = IntegerRangeType::fromInterval(0, null);
462-
if (TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) {
463+
if ($this->canBeNull($firstExprType) || $this->canBeNull($secondExprType)) {
463464
$type = TypeCombinator::addNull($type);
464465
}
465466

466-
// TODO invalid usages
467-
468467
return $this->marshalType($type);
469468

470469
case $function instanceof AST\Functions\ConcatFunction:
471470
$hasNull = false;
472471

473472
foreach ($function->concatExpressions as $expr) {
474473
$type = $this->unmarshalType($expr->dispatch($this));
475-
$hasNull = $hasNull || TypeCombinator::containsNull($type);
474+
$hasNull = $hasNull || $this->canBeNull($type);
476475
}
477476

478477
$type = new StringType();
@@ -493,7 +492,7 @@ public function walkFunction($function): string
493492
$intervalExprType = $this->unmarshalType($function->intervalExpression->dispatch($this));
494493

495494
$type = new StringType();
496-
if (TypeCombinator::containsNull($dateExprType) || TypeCombinator::containsNull($intervalExprType)) {
495+
if ($this->canBeNull($dateExprType) || $this->canBeNull($intervalExprType)) {
497496
$type = TypeCombinator::addNull($type);
498497
}
499498

@@ -511,7 +510,7 @@ public function walkFunction($function): string
511510
$type = new IntegerType();
512511
}
513512

514-
if (TypeCombinator::containsNull($date1ExprType) || TypeCombinator::containsNull($date2ExprType)) {
513+
if ($this->canBeNull($date1ExprType) || $this->canBeNull($date2ExprType)) {
515514
$type = TypeCombinator::addNull($type);
516515
}
517516

@@ -521,7 +520,7 @@ public function walkFunction($function): string
521520
$stringPrimaryType = $this->unmarshalType($function->stringPrimary->dispatch($this));
522521

523522
$type = IntegerRangeType::fromInterval(0, null);
524-
if (TypeCombinator::containsNull($stringPrimaryType)) {
523+
if ($this->canBeNull($stringPrimaryType)) {
525524
$type = TypeCombinator::addNull($type);
526525
}
527526

@@ -532,7 +531,7 @@ public function walkFunction($function): string
532531
$secondExprType = $this->unmarshalType($this->walkStringPrimary($function->secondStringPrimary));
533532

534533
$type = IntegerRangeType::fromInterval(0, null);
535-
if (TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) {
534+
if ($this->canBeNull($firstExprType) || $this->canBeNull($secondExprType)) {
536535
$type = TypeCombinator::addNull($type);
537536
}
538537

@@ -544,7 +543,7 @@ public function walkFunction($function): string
544543
$stringPrimaryType = $this->unmarshalType($function->stringPrimary->dispatch($this));
545544

546545
$type = new StringType();
547-
if (TypeCombinator::containsNull($stringPrimaryType)) {
546+
if ($this->canBeNull($stringPrimaryType)) {
548547
$type = TypeCombinator::addNull($type);
549548
}
550549

@@ -574,7 +573,7 @@ public function walkFunction($function): string
574573

575574
$type = IntegerRangeType::fromInterval(0, null);
576575

577-
if (TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) {
576+
if ($this->canBeNull($firstExprType) || $this->canBeNull($secondExprType)) {
578577
$type = TypeCombinator::addNull($type);
579578
}
580579

@@ -633,7 +632,7 @@ public function walkFunction($function): string
633632
$type = new MixedType();
634633
}
635634

636-
if (TypeCombinator::containsNull($exprType)) {
635+
if ($this->canBeNull($exprType)) {
637636
$type = TypeCombinator::addNull($type);
638637
}
639638

@@ -650,7 +649,7 @@ public function walkFunction($function): string
650649
}
651650

652651
$type = new StringType();
653-
if (TypeCombinator::containsNull($stringType) || TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) {
652+
if ($this->canBeNull($stringType) || $this->canBeNull($firstExprType) || $this->canBeNull($secondExprType)) {
654653
$type = TypeCombinator::addNull($type);
655654
}
656655

@@ -731,7 +730,7 @@ private function inferAvgFunction(AST\Functions\AvgFunction $function): Type
731730

732731
$exprType = $this->unmarshalType($function->getSql($this));
733732
$exprTypeNoNull = TypeCombinator::removeNull($exprType);
734-
$nullable = TypeCombinator::containsNull($exprType) || $this->hasAggregateWithoutGroupBy();
733+
$nullable = $this->canBeNull($exprType) || $this->hasAggregateWithoutGroupBy();
735734

736735
$driver = $this->em->getConnection()->getDriver();
737736

@@ -777,7 +776,7 @@ private function inferSumFunction(AST\Functions\SumFunction $function): Type
777776

778777
$exprType = $this->unmarshalType($function->getSql($this));
779778
$exprTypeNoNull = TypeCombinator::removeNull($exprType);
780-
$nullable = TypeCombinator::containsNull($exprType) || $this->hasAggregateWithoutGroupBy();
779+
$nullable = $this->canBeNull($exprType) || $this->hasAggregateWithoutGroupBy();
781780

782781
$driver = $this->em->getConnection()->getDriver();
783782

@@ -854,7 +853,7 @@ private function containsOnlyTypes(
854853
*/
855854
private function generalizeLiteralType(Type $type, bool $makeNullable): Type
856855
{
857-
$containsNull = TypeCombinator::containsNull($type);
856+
$containsNull = $this->canBeNull($type);
858857
$typeNoNull = TypeCombinator::removeNull($type);
859858

860859
if (!$typeNoNull->isConstantScalarValue()->yes()) {
@@ -941,7 +940,7 @@ public function walkCoalesceExpression($coalesceExpression): string
941940
}
942941

943942
$type = $this->unmarshalType($expression->dispatch($this));
944-
$allTypesContainNull = $allTypesContainNull && TypeCombinator::containsNull($type);
943+
$allTypesContainNull = $allTypesContainNull && $this->canBeNull($type);
945944

946945
$expressionTypes[] = $type;
947946
}
@@ -1123,7 +1122,7 @@ public function walkSelectExpression($selectExpression): string
11231122
if ($expr instanceof TypedExpression) {
11241123
$type = TypeCombinator::intersect( // e.g. count is typed as int, but we infer int<0, max>
11251124
$type,
1126-
$this->resolveDoctrineType(DbalType::lookupName($expr->getReturnType()), null, TypeCombinator::containsNull($type))
1125+
$this->resolveDoctrineType(DbalType::lookupName($expr->getReturnType()), null, $this->canBeNull($type))
11271126
);
11281127
} else {
11291128
// Expressions default to Doctrine's StringType, whose
@@ -1642,15 +1641,17 @@ private function inferPlusMinusTimesType(array $termTypes): Type
16421641
}
16431642

16441643
$union = TypeCombinator::union(...$types);
1645-
$nullable = TypeCombinator::containsNull($union);
1644+
$nullable = $this->canBeNull($union);
16461645
$unionWithoutNull = TypeCombinator::removeNull($union);
16471646

16481647
if ($unionWithoutNull->isInteger()->yes()) {
16491648
return $this->createInteger($nullable);
16501649
}
16511650

16521651
if ($driver instanceof PdoPgSQLDriver) {
1653-
return $this->createNumericString($nullable);
1652+
if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType(), $this->createNumericString(false)])) {
1653+
return $this->createNumericString($nullable);
1654+
}
16541655
}
16551656

16561657
if ($driver instanceof SQLite3Driver || $driver instanceof PdoSqliteDriver) {
@@ -1725,7 +1726,7 @@ private function inferDivisionType(array $termTypes): Type
17251726
}
17261727

17271728
$union = TypeCombinator::union(...$types);
1728-
$nullable = TypeCombinator::containsNull($union);
1729+
$nullable = $this->canBeNull($union);
17291730
$unionWithoutNull = TypeCombinator::removeNull($union);
17301731

17311732
if ($unionWithoutNull->isInteger()->yes()) {
@@ -1739,7 +1740,9 @@ private function inferDivisionType(array $termTypes): Type
17391740
}
17401741

17411742
if ($driver instanceof PdoPgSQLDriver) {
1742-
return $this->createNumericString($nullable);
1743+
if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType(), $this->createNumericString(false)])) {
1744+
return $this->createNumericString($nullable);
1745+
}
17431746
}
17441747

17451748
if ($driver instanceof SQLite3Driver || $driver instanceof PdoSqliteDriver) {
@@ -1937,6 +1940,11 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType
19371940
return $type;
19381941
}
19391942

1943+
private function canBeNull(Type $type): bool
1944+
{
1945+
return !$type->accepts(new NullType(), true)->no();
1946+
}
1947+
19401948
/**
19411949
* Returns whether the query has aggregate function and no group by clause
19421950
*
@@ -2041,24 +2049,10 @@ private function shouldStringifyExpressions(Type $type): TrinaryLogic
20412049
}
20422050
}
20432051

2044-
if ($driver instanceof PgSQLDriver) {
2045-
if ($type->isBoolean()->yes()) {
2046-
return TrinaryLogic::createNo();
2047-
} elseif ($type->isFloat()->yes()) {
2048-
return TrinaryLogic::createNo(); // AVG(col_float) is not, but 0.1 is
2049-
} elseif ($type->isInteger()->yes()) {
2050-
return TrinaryLogic::createNo();
2051-
}
2052-
}
2053-
2054-
if ($driver instanceof SQLite3Driver) {
2052+
if ($driver instanceof PgSQLDriver || $driver instanceof SQLite3Driver || $driver instanceof MysqliDriver) {
20552053
return TrinaryLogic::createNo();
20562054
}
20572055

2058-
if ($driver instanceof MysqliDriver) {
2059-
return TrinaryLogic::createNo(); // DECIMAL / FLOAT already decided in walkLiteral
2060-
}
2061-
20622056
return TrinaryLogic::createMaybe();
20632057
}
20642058

tests/Platform/Entity/PlatformEntity.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,10 @@ class PlatformEntity
9090
*/
9191
public $col_bigint_nullable;
9292

93+
/**
94+
* @ORM\Column(type="mixed", name="col_mixed", nullable=false)
95+
* @var mixed
96+
*/
97+
public $col_mixed;
98+
9399
}

tests/Platform/MixedCustomType.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Platform;
4+
5+
use Doctrine\DBAL\Types\IntegerType;
6+
7+
/**
8+
* Just a custom type without descriptor registered, so that it results to mixed.
9+
*/
10+
class MixedCustomType extends IntegerType
11+
{
12+
13+
public const NAME = 'mixed';
14+
15+
public function getName(): string
16+
{
17+
return self::NAME;
18+
}
19+
20+
}

0 commit comments

Comments
 (0)