phpstan-doctrine icon indicating copy to clipboard operation
phpstan-doctrine copied to clipboard

Query indexBy with OneToOne relation result in nullable TKey

Open noemi-salaun opened this issue 2 years ago • 3 comments

It works fine with version 1.3.28 but since version 1.3.29 with this commit https://github.com/phpstan/phpstan-doctrine/commit/4490e56f6fe3f8985a03be073d4545c3fb59cdbb , when using a query builder with indexBy refering to a OneToOne relation, I get the error

Method SomethingRepository::getAllIndexedByUsers() should return array<int, Something> but returns array<int|null, Something>.

The userId column in my table something is not nullable, it's literally the primary key

My entities are defined as follow

#[ORM\Entity]
class Something
{
    #[ORM\OneToOne, ORM\Id]
    private readonly User $user;

    #[Column]
    private int $value = 1;

    public function __construct(User $user) {
        $this->user = $user;
    }
}
#[ORM\Entity]
class User
{
    #[ORM\Column, ORM\Id, ORM\GeneratedValue]
    private int $id;

    #[Column]
    private string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }
}

and the query

    /**
     * @param list<User> $users
     *
     * @return array<int, Something>
     */
    public function getAllIndexedByUsers(array $users): array
    {
        return $this->entityManager->getRepository(Something::class)
            ->createQueryBuilder('something', 'something.user')
            ->where('something.user IN (:users)')->setParameter('users', $users)
            ->getQuery()
            ->getResult();
    }

I see in the unit test added in the commit that it's not tested with relationship, only with plain string or int column

noemi-salaun avatar Feb 01 '23 16:02 noemi-salaun

Explicitly specifying #[ORM\JoinColumn(nullable: false)] seems to fix the issue. But the column is already implicitly not nullable with #[ORM\Id] and PHPStan knows it. If I remove the #[ORM\Id] attribute, then PHPStan complains that

Property type mapping mismatch: database can contain User|null but property expects User.

noemi-salaun avatar Feb 01 '23 16:02 noemi-salaun

I'm running into a similar issue - same conclusion. It's broken since v1.3.29 and my wild guess would be the exact same commit as mentioned by @noemi-salaun .

Here's my stack trace:

 Internal error: Internal error: Call to a member function dispatch() on null in file /path/to/project/src/VideoBundle/Command/FixSegmentsCompleteCommand.php                                                           
                                                                                                                                                                                                                                     
 Post the following stack trace to https://github.com/phpstan/phpstan/issues/new?template=Bug_report.md:                                                                                                                             
 #0 /path/to/project/vendor/doctrine/orm/lib/Doctrine/ORM/Query/AST/IndexBy.php(51): PHPStan\Type\Doctrine\Query\QueryResultTypeWalker->walkIndexBy()                                                                   
 #1 /path/to/project/vendor/phpstan/phpstan-doctrine/src/Type/Doctrine/Query/QueryResultTypeWalker.php(316): Doctrine\ORM\Query\AST\IndexBy->dispatch()                                                                 
 #2 /path/to/project/vendor/doctrine/orm/lib/Doctrine/ORM/Query/AST/IdentificationVariableDeclaration.php(65): PHPStan\Type\Doctrine\Query\QueryResultTypeWalker->walkIdentificationVariableDeclaration()               
 #3 /path/to/project/vendor/phpstan/phpstan-doctrine/src/Type/Doctrine/Query/QueryResultTypeWalker.php(304): Doctrine\ORM\Query\AST\IdentificationVariableDeclaration->dispatch()                                       
 #4 /path/to/project/vendor/phpstan/phpstan-doctrine/src/Type/Doctrine/Query/QueryResultTypeWalker.php(180): PHPStan\Type\Doctrine\Query\QueryResultTypeWalker->walkFromClause()                                        
 #5 /path/to/project/vendor/doctrine/orm/lib/Doctrine/ORM/Query/Exec/SingleSelectExecutor.php(42): PHPStan\Type\Doctrine\Query\QueryResultTypeWalker->walkSelectStatement()                                             
 #6 /path/to/project/vendor/doctrine/orm/lib/Doctrine/ORM/Query/SqlWalker.php(288): Doctrine\ORM\Query\Exec\SingleSelectExecutor->__construct()                                                                         
 #7 /path/to/project/vendor/doctrine/orm/lib/Doctrine/ORM/Query/Parser.php(411): Doctrine\ORM\Query\SqlWalker->getExecutor()                                                                                            
 #8 /path/to/project/vendor/phpstan/phpstan-doctrine/src/Type/Doctrine/Query/QueryResultTypeWalker.php(119): Doctrine\ORM\Query\Parser->parse()                                                                         
 #9 /path/to/project/vendor/phpstan/phpstan-doctrine/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php(163): PHPStan\Type\Doctrine\Query\QueryResultTypeWalker::walk()                  
 #10 /path/to/project/vendor/phpstan/phpstan-doctrine/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php(146):                                                                           
 PHPStan\Type\Doctrine\QueryBuilder\QueryBuilderGetQueryDynamicReturnTypeExtension->getQueryType()                                                                                                                                   
 #11 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(3353): PHPStan\Type\Doctrine\QueryBuilder\QueryBuilderGetQueryDynamicReturnTypeExtension->getTypeFromMethodCall()       
 #12 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(1365): PHPStan\Analyser\MutatingScope->methodCallReturnType()                                                           
 #13 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(1371): PHPStan\Analyser\MutatingScope->PHPStan\Analyser\{closure}()                                                     
 #14 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(557): PHPStan\Analyser\MutatingScope->resolveType()                                                                     
 #15 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(1365): PHPStan\Analyser\MutatingScope->getType()                                                                        
 #16 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(1371): PHPStan\Analyser\MutatingScope->PHPStan\Analyser\{closure}()                                                     
 #17 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(557): PHPStan\Analyser\MutatingScope->resolveType()                                                                     
 #18 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(777): PHPStan\Analyser\MutatingScope->getType()                                                                         
 #19 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/MutatingScope.php(557): PHPStan\Analyser\MutatingScope->resolveType()                                                                     
 #20 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(1446): PHPStan\Analyser\MutatingScope->getType()                                                                    
 #21 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(557): PHPStan\Analyser\NodeScopeResolver->findEarlyTerminatingExpr()                                                
 #22 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(360): PHPStan\Analyser\NodeScopeResolver->processStmtNode()                                                         
 #23 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(521): PHPStan\Analyser\NodeScopeResolver->processStmtNodes()                                                        
 #24 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(360): PHPStan\Analyser\NodeScopeResolver->processStmtNode()                                                         
 #25 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(599): PHPStan\Analyser\NodeScopeResolver->processStmtNodes()                                                        
 #26 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(360): PHPStan\Analyser\NodeScopeResolver->processStmtNode()                                                         
 #27 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(571): PHPStan\Analyser\NodeScopeResolver->processStmtNodes()                                                        
 #28 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(327): PHPStan\Analyser\NodeScopeResolver->processStmtNode()                                                         
 #29 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/FileAnalyser.php(175): PHPStan\Analyser\NodeScopeResolver->processNodes()                                                                 
 #30 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Command/WorkerCommand.php(148): PHPStan\Analyser\FileAnalyser->analyseFile()                                                                       
 #31 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/evenement/evenement/src/Evenement/EventEmitterTrait.php(97): PHPStan\Command\WorkerCommand->PHPStan\Command\{closure}()                         
 #32 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/clue/ndjson-react/src/Decoder.php(110): _PHPStan_4dd92cd93\Evenement\EventEmitter->emit()                                                       
 #33 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/evenement/evenement/src/Evenement/EventEmitterTrait.php(97): _PHPStan_4dd92cd93\Clue\React\NDJson\Decoder->handleData()                         
 #34 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/react/stream/src/Util.php(62): _PHPStan_4dd92cd93\Evenement\EventEmitter->emit()                                                                
 #35 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/evenement/evenement/src/Evenement/EventEmitterTrait.php(97): _PHPStan_4dd92cd93\React\Stream\Util::_PHPStan_4dd92cd93\React\Stream\{closure}()  
 #36 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/react/stream/src/DuplexResourceStream.php(154): _PHPStan_4dd92cd93\Evenement\EventEmitter->emit()                                               
 #37 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/react/event-loop/src/StreamSelectLoop.php(201): _PHPStan_4dd92cd93\React\Stream\DuplexResourceStream->handleData()                              
 #38 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/react/event-loop/src/StreamSelectLoop.php(173): _PHPStan_4dd92cd93\React\EventLoop\StreamSelectLoop->waitForStreamActivity()                    
 #39 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/src/Command/WorkerCommand.php(108): _PHPStan_4dd92cd93\React\EventLoop\StreamSelectLoop->run()                                                         
 #40 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/symfony/console/Command/Command.php(259): PHPStan\Command\WorkerCommand->execute()                                                              
 #41 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/symfony/console/Application.php(870): _PHPStan_4dd92cd93\Symfony\Component\Console\Command\Command->run()                                       
 #42 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/symfony/console/Application.php(261): _PHPStan_4dd92cd93\Symfony\Component\Console\Application->doRunCommand()                                  
 #43 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/vendor/symfony/console/Application.php(157): _PHPStan_4dd92cd93\Symfony\Component\Console\Application->doRun()                                         
 #44 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/bin/phpstan(124): _PHPStan_4dd92cd93\Symfony\Component\Console\Application->run()                                                                      
 #45 phar:///path/to/project/vendor/phpstan/phpstan/phpstan.phar/bin/phpstan(125): _PHPStan_4dd92cd93\{closure}()                                                                                                       
 #46 /path/to/project/vendor/phpstan/phpstan/phpstan(7): require('phar:///home/me...')                                                                                                                                  
 #47 /path/to/project/vendor/bin/phpstan(115): include('/home/meh/proje...')                                                                                                                                            
 #48 {main}                                                                                                                                                                                                                          
 Child process error (exit code 1):

my dynamic query:

$qb = $this->em->createQueryBuilder()
    ->select('episode.id')
    ->from('VideoBundle:Episode', 'episode', 'episode.id')
    ->where('episode.segmentsComplete = false')
    ->andWhere('0 != SIZE(episode.segments)')
    ->andWhere(
        '0 < (SELECT COUNT(last_segment.id) FROM VideoBundle:Segment as last_segment
              WHERE last_segment.episode = episode.id AND last_segment.isLastSegment = true)'
    )
    ->andWhere(
        "0 = (SELECT COUNT(segment.id) FROM VideoBundle:Segment as segment
              WHERE segment.episode = episode.id AND segment.state != 'distributed')"
    )
;
class Episode
{
    /**
     * @var Collection<Segment>
     * @ORM\OneToMany(
     *      targetEntity="App\VideoBundle\Entity\Segment",
     *      mappedBy="episode",
     *      cascade={"persist", "remove"},
     *      orphanRemoval=true
     * )
     * @ORM\OrderBy({"position" = "ASC"})
     */
    private $segments;
}
class Segment
{
/**
     * @ORM\ManyToOne(
     *     targetEntity="App\VideoBundle\Entity\Episode",
     *     inversedBy="segments",
     *     cascade={"persist"}
     * )
     * @ORM\JoinColumn(name="episode_id", referencedColumnName="id", nullable=false)
     * @var Episode
     */
    private $episode;
}

mvhirsch avatar Feb 03 '23 10:02 mvhirsch

@mvhirsch Wrong type inference, and an internal error crash, are not similar issues. Please track yours in a separate issue. Thanks.

ondrejmirtes avatar Feb 06 '23 12:02 ondrejmirtes