From c6f09ea1f29f317bf72fda5ce4703b3de93b2a87 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 8 Aug 2023 09:49:29 +0200 Subject: [PATCH 001/160] Open 1.4.x-dev --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd7256dd..961ec444 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.3.x" + - "1.4.x" jobs: lint: From 064c30a9115b237f171a3fa7279aaa2dee9a065b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Jun 2023 11:12:54 +0200 Subject: [PATCH 002/160] Require PHPStan 1.11 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 755f5b7d..002a3acc 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.12" + "phpstan/phpstan": "^1.11" }, "conflict": { "doctrine/collections": "<1.0", From 95a8b4f191abf9da166aaf08e72030380536fe0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 00:36:23 +0000 Subject: [PATCH 003/160] Update actions/checkout action to v4 --- .github/workflows/build.yml | 10 +++++----- .github/workflows/create-tag.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 961ec444..ea7342ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -55,10 +55,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Checkout build-cs" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: "phpstan/build-cs" path: "build-cs" @@ -106,7 +106,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -146,7 +146,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 8452d986..a8535014 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -21,7 +21,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92b72547..e4a8ac62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Generate changelog id: changelog From 49f5137ba4799f5699c8d95c34c090ae5680216f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 15 Nov 2023 14:51:28 +0100 Subject: [PATCH 004/160] Remove unneeded `@var` --- src/Rules/Doctrine/ORM/EntityColumnRule.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index 9406aa4f..85bb9e3d 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -94,7 +94,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var array{type: string, fieldName: string, columnName?: string, inherited?: class-string, nullable?: bool, enumType?: ?string} $fieldMapping */ $fieldMapping = $metadata->fieldMappings[$propertyName]; $errors = []; From a6da79018d2b26cd22ffbe6b894788103929e517 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 00:55:35 +0000 Subject: [PATCH 005/160] Update dessant/lock-threads action to v5 --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 4c7990df..c2b017b9 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -8,7 +8,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} issue-inactive-days: '31' From 4ec70ff5e526bd9ab5246d73d22c390f3b8358c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 00:55:30 +0000 Subject: [PATCH 006/160] Update metcalfc/changelog-generator action to v4.2.0 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4a8ac62..2fb750a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.1.0 + uses: metcalfc/changelog-generator@v4.2.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 4f58abc71713a796d5d7417ddea3a04acafea42b Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Fri, 5 Jan 2024 15:49:03 +0100 Subject: [PATCH 007/160] Add test for #501 --- .../data/QueryResult/Entities/Vehicle.php | 44 +++++++++++++++++++ .../data/QueryResult/queryBuilderGetQuery.php | 21 +++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/Type/Doctrine/data/QueryResult/Entities/Vehicle.php diff --git a/tests/Type/Doctrine/data/QueryResult/Entities/Vehicle.php b/tests/Type/Doctrine/data/QueryResult/Entities/Vehicle.php new file mode 100644 index 00000000..501c7f6d --- /dev/null +++ b/tests/Type/Doctrine/data/QueryResult/Entities/Vehicle.php @@ -0,0 +1,44 @@ +', $query); } + public function testQueryResultTypeIsMixedWhenDQLIsUsingAnInterfaceTypeDefinition(EntityManagerInterface $em): void + { + $vehicle = $this->createVehicule(); + + assertType(VehicleInterface::class, $vehicle); + + $query = $em->createQueryBuilder() + ->select('v') + ->from(get_class($vehicle), 'v') + ->getQuery(); + + assertType('Doctrine\ORM\Query', $query); + } + public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterface $em): void { $query = $em->getRepository(Many::class) @@ -261,4 +277,9 @@ private function getBranchingQueryBuilder(EntityManagerInterface $em): QueryBuil return $queryBuilder; } + + private function createVehicule(): VehicleInterface + { + return new Truck(); + } } From b15c279a03d140e756f6957f3a7518621d40c7af Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 5 Jan 2024 19:08:41 +0100 Subject: [PATCH 008/160] Update workflow --- .github/workflows/test-projects.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-projects.yml b/.github/workflows/test-projects.yml index cb66d296..d10750fe 100644 --- a/.github/workflows/test-projects.yml +++ b/.github/workflows/test-projects.yml @@ -5,7 +5,7 @@ name: "Test projects" on: push: branches: - - "1.3.x" + - "1.4.x" jobs: test-projects: @@ -26,4 +26,4 @@ jobs: token: ${{ secrets.REPO_ACCESS_TOKEN }} repository: "${{ matrix.repository }}" event-type: test_phpstan - client-payload: '{"ref": "1.10.x"}' + client-payload: '{"ref": "1.11.x"}' From e764a6aa344f9085a331334bff551655b4c53ee6 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 15 Jan 2024 10:23:49 +0100 Subject: [PATCH 009/160] Fix test --- tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php index d88ba764..21519225 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php @@ -131,7 +131,7 @@ public function testQueryResultTypeIsMixedWhenDQLIsUsingAnInterfaceTypeDefinitio ->from(get_class($vehicle), 'v') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterface $em): void From c714ac77296039548ddafd67cc20f05e6af218c2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 15 Jan 2024 11:32:58 +0100 Subject: [PATCH 010/160] Fix test --- tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php index 21519225..d88ba764 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php @@ -131,7 +131,7 @@ public function testQueryResultTypeIsMixedWhenDQLIsUsingAnInterfaceTypeDefinitio ->from(get_class($vehicle), 'v') ->getQuery(); - assertType('Doctrine\ORM\Query', $query); + assertType('Doctrine\ORM\Query', $query); } public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterface $em): void From 19dd2dddd33efe8254db0d1fce9b141be38224ed Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 16 Jan 2024 15:30:35 +0100 Subject: [PATCH 011/160] Handle all hydration mode in QueryResultDynamicReturnTypeExtension --- .../QueryResultDynamicReturnTypeExtension.php | 194 ++++++++- .../Doctrine/Query/QueryResultTypeWalker.php | 37 +- .../Query/QueryResultTypeWalkerTest.php | 195 +++++---- .../Doctrine/data/QueryResult/queryResult.php | 396 +++++++++++++++++- 4 files changed, 707 insertions(+), 115 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 20e23831..96b3f7bc 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -10,14 +10,22 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\IterableType; +use PHPStan\Type\MixedType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; +use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VoidType; +use function count; final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -32,6 +40,23 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn 'getSingleResult' => 0, ]; + private const METHOD_HYDRATION_MODE = [ + 'getArrayResult' => AbstractQuery::HYDRATE_ARRAY, + 'getScalarResult' => AbstractQuery::HYDRATE_SCALAR, + 'getSingleColumnResult' => AbstractQuery::HYDRATE_SCALAR_COLUMN, + 'getSingleScalarResult' => AbstractQuery::HYDRATE_SINGLE_SCALAR, + ]; + + /** @var ObjectMetadataResolver */ + private $objectMetadataResolver; + + public function __construct( + ObjectMetadataResolver $objectMetadataResolver + ) + { + $this->objectMetadataResolver = $objectMetadataResolver; + } + public function getClass(): string { return AbstractQuery::class; @@ -39,7 +64,8 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]); + return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]) + || isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]); } public function getTypeFromMethodCall( @@ -50,21 +76,23 @@ public function getTypeFromMethodCall( { $methodName = $methodReflection->getName(); - if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { - throw new ShouldNotHappenException(); - } - - $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; - $args = $methodCall->getArgs(); + if (isset(self::METHOD_HYDRATION_MODE[$methodName])) { + $hydrationMode = new ConstantIntegerType(self::METHOD_HYDRATION_MODE[$methodName]); + } elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { + $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; + $args = $methodCall->getArgs(); - if (isset($args[$argIndex])) { - $hydrationMode = $scope->getType($args[$argIndex]->value); + if (isset($args[$argIndex])) { + $hydrationMode = $scope->getType($args[$argIndex]->value); + } else { + $parametersAcceptor = ParametersAcceptorSelector::selectSingle( + $methodReflection->getVariants() + ); + $parameter = $parametersAcceptor->getParameters()[$argIndex]; + $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); + } } else { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() - ); - $parameter = $parametersAcceptor->getParameters()[$argIndex]; - $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); + throw new ShouldNotHappenException(); } $queryType = $scope->getType($methodCall->var); @@ -98,23 +126,54 @@ private function getMethodReturnTypeForHydrationMode( return $this->originalReturnType($methodReflection); } - if (!$this->isObjectHydrationMode($hydrationMode)) { - // We support only HYDRATE_OBJECT. For other hydration modes, we - // return the declared return type of the method. + if (!$hydrationMode instanceof ConstantIntegerType) { return $this->originalReturnType($methodReflection); } + $singleResult = false; + switch ($hydrationMode->getValue()) { + case AbstractQuery::HYDRATE_OBJECT: + break; + case AbstractQuery::HYDRATE_ARRAY: + $queryResultType = $this->getArrayHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SCALAR: + $queryResultType = $this->getScalarHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SINGLE_SCALAR: + $singleResult = true; + $queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SIMPLEOBJECT: + $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SCALAR_COLUMN: + $queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType); + break; + default: + return $this->originalReturnType($methodReflection); + } + switch ($methodReflection->getName()) { case 'getSingleResult': return $queryResultType; case 'getOneOrNullResult': - return TypeCombinator::addNull($queryResultType); + $nullableQueryResultType = TypeCombinator::addNull($queryResultType); + if ($queryResultType instanceof BenevolentUnionType) { + $nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType); + } + + return $nullableQueryResultType; case 'toIterable': return new IterableType( $queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType, $queryResultType ); default: + if ($singleResult) { + return $queryResultType; + } + if ($queryKeyType->isNull()->yes()) { return AccessoryArrayListType::intersectWith(new ArrayType( new IntegerType(), @@ -128,13 +187,104 @@ private function getMethodReturnTypeForHydrationMode( } } - private function isObjectHydrationMode(Type $type): bool + private function getArrayHydratedReturnType(Type $queryResultType): Type + { + $objectManager = $this->objectMetadataResolver->getObjectManager(); + + return TypeTraverser::map( + $queryResultType, + static function (Type $type, callable $traverse) use ($objectManager): Type { + $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); + if ($isObject->no()) { + return $traverse($type); + } + if ( + $isObject->maybe() + || !$type instanceof TypeWithClassName + || $objectManager === null + ) { + return new MixedType(); + } + + if (!$objectManager->getMetadataFactory()->hasMetadataFor($type->getClassName())) { + return $traverse($type); + } + + // We could return `new ArrayTyp(new MixedType(), new MixedType())` + // but the lack of precision in the array keys/values would give false positive + // @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 + return new MixedType(); + } + ); + } + + private function getScalarHydratedReturnType(Type $queryResultType): Type + { + if (!$queryResultType->isArray()->yes()) { + return new ArrayType(new MixedType(), new MixedType()); + } + + foreach ($queryResultType->getArrays() as $arrayType) { + $itemType = $arrayType->getItemType(); + + if ( + !(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no() + || !$itemType->isArray()->no() + ) { + return new ArrayType(new MixedType(), new MixedType()); + } + } + + return $queryResultType; + } + + private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type + { + if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { + return $queryResultType; + } + + return new MixedType(); + } + + private function getSingleScalarHydratedReturnType(Type $queryResultType): Type { - if (!$type instanceof ConstantIntegerType) { - return false; + $queryResultType = $this->getScalarHydratedReturnType($queryResultType); + if (!$queryResultType->isConstantArray()->yes()) { + return new MixedType(); + } + + $types = []; + foreach ($queryResultType->getConstantArrays() as $constantArrayType) { + $values = $constantArrayType->getValueTypes(); + if (count($values) !== 1) { + return new MixedType(); + } + + $types[] = $constantArrayType->getFirstIterableValueType(); + } + + return TypeCombinator::union(...$types); + } + + private function getScalarColumnHydratedReturnType(Type $queryResultType): Type + { + $queryResultType = $this->getScalarHydratedReturnType($queryResultType); + if (!$queryResultType->isConstantArray()->yes()) { + return new MixedType(); + } + + $types = []; + foreach ($queryResultType->getConstantArrays() as $constantArrayType) { + $values = $constantArrayType->getValueTypes(); + if (count($values) !== 1) { + return new MixedType(); + } + + $types[] = $constantArrayType->getFirstIterableValueType(); } - return $type->getValue() === AbstractQuery::HYDRATE_OBJECT; + return TypeCombinator::union(...$types); } private function originalReturnType(MethodReflection $methodReflection): Type diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 060dd092..e43afa88 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -107,6 +107,9 @@ class QueryResultTypeWalker extends SqlWalker /** @var bool */ private $hasGroupByClause; + /** @var bool */ + private $hasWhereClause; + /** * @param Query $query */ @@ -135,6 +138,7 @@ public function __construct($query, $parserResult, array $queryComponents) $this->nullableQueryComponents = []; $this->hasAggregateFunction = false; $this->hasGroupByClause = false; + $this->hasWhereClause = false; // The object is instantiated by Doctrine\ORM\Query\Parser, so receiving // dependencies through the constructor is not an option. Instead, we @@ -177,6 +181,7 @@ public function walkSelectStatement(AST\SelectStatement $AST) $this->typeBuilder->setSelectQuery(); $this->hasAggregateFunction = $this->hasAggregateFunction($AST); $this->hasGroupByClause = $AST->groupByClause !== null; + $this->hasWhereClause = $AST->whereClause !== null; $this->walkFromClause($AST->fromClause); @@ -795,7 +800,7 @@ public function walkSelectExpression($selectExpression) $type = $this->resolveDoctrineType($typeName, $enumType, $nullable); - $this->typeBuilder->addScalar($resultAlias, $type); + $this->addScalar($resultAlias, $type); return ''; } @@ -841,21 +846,32 @@ public function walkSelectExpression($selectExpression) // the driver and PHP version. // Here we assume that the value may or may not be casted to // string by the driver. - $type = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + $casted = false; + $originalType = $type; + + $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$casted): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } if ($type instanceof IntegerType || $type instanceof FloatType) { + $casted = true; return TypeCombinator::union($type->toString(), $type); } if ($type instanceof BooleanType) { + $casted = true; return TypeCombinator::union($type->toInteger()->toString(), $type); } return $traverse($type); }); + + // Since we made supposition about possibly casted values, + // we can only provide a benevolent union. + if ($casted && $type instanceof UnionType && !$originalType->equals($type)) { + $type = TypeUtils::toBenevolentUnion($type); + } } - $this->typeBuilder->addScalar($resultAlias, $type); + $this->addScalar($resultAlias, $type); return ''; } @@ -1276,6 +1292,21 @@ public function walkResultVariable($resultVariable) return $this->marshalType(new MixedType()); } + /** + * @param array-key $alias + */ + private function addScalar($alias, Type $type): void + { + // Since we don't check the condition inside the WHERE + // conditions, we cannot be sure all the union types are correct. + // For exemple, a condition `WHERE foo.bar IS NOT NULL` could be added. + if ($this->hasWhereClause && $type instanceof UnionType) { + $type = TypeUtils::toBenevolentUnion($type); + } + + $this->typeBuilder->addScalar($alias, $type); + } + private function unmarshalType(string $marshalledType): Type { $type = unserialize($marshalledType); diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 1393a9bc..9fb20455 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -28,7 +28,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; +use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use QueryResult\Entities\Embedded; use QueryResult\Entities\JoinedChild; @@ -442,6 +442,25 @@ public function getTestData(): iterable ', ]; + yield 'scalar with where condition' => [ + $this->constantArray([ + [new ConstantStringType('intColumn'), new IntegerType()], + [new ConstantStringType('stringColumn'), new StringType()], + [ + new ConstantStringType('stringNullColumn'), + TypeUtils::toBenevolentUnion(TypeCombinator::addNull(new StringType())), + ], + [new ConstantStringType('datetimeColumn'), new ObjectType(DateTime::class)], + [new ConstantStringType('datetimeImmutableColumn'), new ObjectType(DateTimeImmutable::class)], + ]), + ' + SELECT m.intColumn, m.stringColumn, m.stringNullColumn, + m.datetimeColumn, m.datetimeImmutableColumn + FROM QueryResult\Entities\Many m + WHERE m.stringNullColumn IS NOT NULL + ', + ]; + yield 'scalar with alias' => [ $this->constantArray([ [new ConstantStringType('i'), new IntegerType()], @@ -544,12 +563,12 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantIntegerType(2), new ConstantStringType('1'), new ConstantStringType('2') - ), + )), ], [ new ConstantIntegerType(3), @@ -596,7 +615,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(1), - TypeCombinator::addNull($this->intStringified()), + $this->intStringified(true), ], [ new ConstantIntegerType(2), @@ -610,7 +629,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(4), - TypeCombinator::addNull($this->intStringified()), + $this->intStringified(true), ], [ new ConstantIntegerType(5), @@ -620,7 +639,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(6), - TypeCombinator::addNull($this->intStringified()), + $this->intStringified(true), ], [ new ConstantIntegerType(7), @@ -646,7 +665,7 @@ public function getTestData(): iterable ], [ new ConstantStringType('max'), - TypeCombinator::addNull($this->intStringified()), + $this->intStringified(true), ], [ new ConstantStringType('arithmetic'), @@ -678,10 +697,10 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantStringType('1'), new ConstantIntegerType(1) - ), + )), ], [new ConstantIntegerType(2), new ConstantStringType('hello')], ]), @@ -695,11 +714,11 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1'), new NullType() - ), + )), ], ]), ' @@ -781,12 +800,12 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), new ConstantStringType('1') - ), + )), ], ]), ' @@ -802,12 +821,12 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), new ConstantStringType('1') - ), + )), ], ]), ' @@ -823,31 +842,31 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1') - ), + )), ], [ new ConstantIntegerType(2), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(0), new ConstantStringType('0') - ), + )), ], [ new ConstantIntegerType(3), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1') - ), + )), ], [ new ConstantIntegerType(4), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(0), new ConstantStringType('0') - ), + )), ], ]), ' @@ -1019,10 +1038,10 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeCombinator::union( + TypeUtils::toBenevolentUnion(TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1') - ), + )), ], [ new ConstantStringType('intColumn'), @@ -1066,7 +1085,7 @@ public function getTestData(): iterable yield 'new arguments affect scalar counter' => [ $this->constantArray([ - [new ConstantIntegerType(5), TypeCombinator::addNull($this->intStringified())], + [new ConstantIntegerType(5), $this->intStringified(true)], [new ConstantIntegerType(0), new ObjectType(ManyId::class)], [new ConstantIntegerType(1), new ObjectType(OneId::class)], ]), @@ -1083,7 +1102,7 @@ public function getTestData(): iterable [new ConstantStringType('intColumn'), new IntegerType()], [new ConstantIntegerType(1), $this->intStringified()], [new ConstantIntegerType(2), $this->intStringified()], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->intStringified())], + [new ConstantIntegerType(3), $this->intStringified(true)], [new ConstantIntegerType(4), $this->intStringified()], [new ConstantIntegerType(5), $this->intStringified()], [new ConstantIntegerType(6), $this->numericStringified()], @@ -1105,9 +1124,9 @@ public function getTestData(): iterable yield 'abs function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->unumericStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->unumericStringified())], + [new ConstantIntegerType(2), $this->unumericStringified(true)], [new ConstantIntegerType(3), $this->unumericStringified()], - [new ConstantIntegerType(4), TypeCombinator::union($this->unumericStringified())], + [new ConstantIntegerType(4), $this->unumericStringified()], ]), ' SELECT ABS(m.intColumn), @@ -1121,7 +1140,7 @@ public function getTestData(): iterable yield 'bit_and function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(2), $this->uintStringified(true)], [new ConstantIntegerType(3), $this->uintStringified()], ]), ' @@ -1135,7 +1154,7 @@ public function getTestData(): iterable yield 'bit_or function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(2), $this->uintStringified(true)], [new ConstantIntegerType(3), $this->uintStringified()], ]), ' @@ -1211,8 +1230,8 @@ public function getTestData(): iterable yield 'date_diff function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->numericStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->numericStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->numericStringified())], + [new ConstantIntegerType(2), $this->numericStringified(true)], + [new ConstantIntegerType(3), $this->numericStringified(true)], [new ConstantIntegerType(4), $this->numericStringified()], ]), ' @@ -1254,11 +1273,9 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeCombinator::addNull( - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified() - ), + $this->hasTypedExpressions() + ? $this->uint(true) + : $this->uintStringified(true), ], [ new ConstantIntegerType(3), @@ -1279,8 +1296,8 @@ public function getTestData(): iterable yield 'locate function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(2), $this->uintStringified(true)], + [new ConstantIntegerType(3), $this->uintStringified(true)], [new ConstantIntegerType(4), $this->uintStringified()], ]), ' @@ -1322,8 +1339,8 @@ public function getTestData(): iterable yield 'mod function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(2), $this->uintStringified(true)], + [new ConstantIntegerType(3), $this->uintStringified(true)], [new ConstantIntegerType(4), $this->uintStringified()], ]), ' @@ -1337,7 +1354,7 @@ public function getTestData(): iterable yield 'mod function error' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(1), $this->uintStringified(true)], ]), ' SELECT MOD(10, NULLIF(m.intColumn, m.intColumn)) @@ -1396,15 +1413,15 @@ public function getTestData(): iterable yield 'identity function' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], - [new ConstantIntegerType(2), $this->numericStringOrInt()], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), $this->intStringified(true)], + [new ConstantIntegerType(2), $this->intStringified()], + [new ConstantIntegerType(3), $this->intStringified(true)], [new ConstantIntegerType(4), TypeCombinator::addNull(new StringType())], [new ConstantIntegerType(5), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(6), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(6), $this->intStringified(true)], [new ConstantIntegerType(7), TypeCombinator::addNull(new MixedType())], - [new ConstantIntegerType(8), TypeCombinator::addNull($this->numericStringOrInt())], - [new ConstantIntegerType(9), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(8), $this->intStringified(true)], + [new ConstantIntegerType(9), $this->intStringified(true)], ]), ' SELECT IDENTITY(m.oneNull), @@ -1423,7 +1440,7 @@ public function getTestData(): iterable yield 'select nullable association' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), $this->intStringified(true)], ]), ' SELECT DISTINCT(m.oneNull) @@ -1433,7 +1450,7 @@ public function getTestData(): iterable yield 'select non null association' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->numericStringOrInt()], + [new ConstantIntegerType(1), $this->intStringified()], ]), ' SELECT DISTINCT(m.one) @@ -1443,7 +1460,7 @@ public function getTestData(): iterable yield 'select default nullability association' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), $this->intStringified(true)], ]), ' SELECT DISTINCT(m.oneDefaultNullability) @@ -1453,7 +1470,7 @@ public function getTestData(): iterable yield 'select non null association in aggregated query' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), $this->intStringified(true)], [ new ConstantIntegerType(2), $this->hasTypedExpressions() @@ -1523,17 +1540,6 @@ private function constantArray(array $elements): Type return $builder->getArray(); } - private function numericStringOrInt(): Type - { - return new UnionType([ - new IntegerType(), - new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]), - ]); - } - private function numericString(): Type { return new IntersectionType([ @@ -1542,42 +1548,67 @@ private function numericString(): Type ]); } - private function uint(): Type + private function uint(bool $nullable = false): Type { - return IntegerRangeType::fromInterval(0, null); + $type = IntegerRangeType::fromInterval(0, null); + if ($nullable) { + TypeCombinator::addNull($type); + } + + return $type; } - private function intStringified(): Type + private function intStringified(bool $nullable = false): Type { - return TypeCombinator::union( + $types = [ new IntegerType(), - $this->numericString() - ); + $this->numericString(), + ]; + if ($nullable) { + $types[] = new NullType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); } - private function uintStringified(): Type + private function uintStringified(bool $nullable = false): Type { - return TypeCombinator::union( + $types = [ $this->uint(), - $this->numericString() - ); + $this->numericString(), + ]; + if ($nullable) { + $types[] = new NullType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); } - private function numericStringified(): Type + private function numericStringified(bool $nullable = false): Type { - return TypeCombinator::union( + $types = [ new FloatType(), new IntegerType(), - $this->numericString() - ); + $this->numericString(), + ]; + if ($nullable) { + $types[] = new NullType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); } - private function unumericStringified(): Type + private function unumericStringified(bool $nullable = false): Type { - return TypeCombinator::union( + $types = [ new FloatType(), IntegerRangeType::fromInterval(0, null), - $this->numericString() - ); + $this->numericString(), + ]; + if ($nullable) { + $types[] = new NullType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); } private function hasTypedExpressions(): bool diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index 02469e46..d6972943 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -144,11 +144,11 @@ public function testReturnTypeOfQueryMethodsWithExplicitObjectHydrationMode(Enti } /** - * Test that we properly infer the return type of Query methods with explicit hydration mode that is not HYDRATE_OBJECT + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_ARRAY * - * We are never able to infer the return type here + * We can infer the return type by changing every object by an array */ - public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(EntityManagerInterface $em): void + public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(EntityManagerInterface $em): void { $query = $em->createQuery(' SELECT m @@ -156,23 +156,27 @@ public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(E '); assertType( - 'mixed', + 'list', $query->getResult(AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'iterable', + 'list', + $query->getArrayResult() + ); + assertType( + 'iterable', $query->toIterable([], AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'mixed', + 'list', $query->execute(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'mixed', + 'list', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'mixed', + 'list', $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( @@ -183,6 +187,382 @@ public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(E 'mixed', $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) ); + + $query = $em->createQuery(' + SELECT m.intColumn, m.stringNullColumn, m.datetimeColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'list', + $query->getArrayResult() + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}', + $query->getSingleResult(AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR + */ + public function testReturnTypeOfQueryMethodsWithExplicitScalarHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->getScalarResult() + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) + ); + + $query = $em->createQuery(' + SELECT m.intColumn, m.stringNullColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->getScalarResult() + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null}', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null}|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR + */ + public function testReturnTypeOfQueryMethodsWithExplicitSingleScalarHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'mixed', + $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->getSingleScalarResult() + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + + $query = $em->createQuery(' + SELECT m.intColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'int', + $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->getSingleScalarResult() + ); + assertType( + 'int', + $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + + $query = $em->createQuery(' + SELECT COUNT(m.id) + FROM QueryResult\Entities\Many m + '); + + assertType( + '(int<0, max>|numeric-string)', + $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + '(int<0, max>|numeric-string)', + $query->getSingleScalarResult() + ); + assertType( + '(int<0, max>|numeric-string)', + $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + '(int<0, max>|numeric-string)', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + '(int<0, max>|numeric-string)', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + '(int<0, max>|numeric-string)', + $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + '(int<0, max>|numeric-string|null)', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SIMPLEOBJECT + * + * We are never able to infer the return type here + */ + public function testReturnTypeOfQueryMethodsWithExplicitSimpleObjectHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'QueryResult\Entities\Many', + $query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'QueryResult\Entities\Many|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + + $query = $em->createQuery(' + SELECT m.intColumn, m.stringNullColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR_COLUMN + * + * We are never able to infer the return type here + */ + public function testReturnTypeOfQueryMethodsWithExplicitScalarColumnHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->getSingleColumnResult() + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + + $query = $em->createQuery(' + SELECT m.intColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->getSingleColumnResult() + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'int', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'int|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); } /** From 24adb0e94d00bc24a5b8131bee7d8ab3a2101853 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jan 2024 10:26:58 +0100 Subject: [PATCH 012/160] Revert "Handle all hydration mode in QueryResultDynamicReturnTypeExtension" This reverts commit 19dd2dddd33efe8254db0d1fce9b141be38224ed. --- .../QueryResultDynamicReturnTypeExtension.php | 194 +-------- .../Doctrine/Query/QueryResultTypeWalker.php | 37 +- .../Query/QueryResultTypeWalkerTest.php | 195 ++++----- .../Doctrine/data/QueryResult/queryResult.php | 396 +----------------- 4 files changed, 115 insertions(+), 707 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 96b3f7bc..20e23831 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -10,22 +10,14 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; -use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\IterableType; -use PHPStan\Type\MixedType; use PHPStan\Type\NullType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VoidType; -use function count; final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -40,23 +32,6 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn 'getSingleResult' => 0, ]; - private const METHOD_HYDRATION_MODE = [ - 'getArrayResult' => AbstractQuery::HYDRATE_ARRAY, - 'getScalarResult' => AbstractQuery::HYDRATE_SCALAR, - 'getSingleColumnResult' => AbstractQuery::HYDRATE_SCALAR_COLUMN, - 'getSingleScalarResult' => AbstractQuery::HYDRATE_SINGLE_SCALAR, - ]; - - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; - - public function __construct( - ObjectMetadataResolver $objectMetadataResolver - ) - { - $this->objectMetadataResolver = $objectMetadataResolver; - } - public function getClass(): string { return AbstractQuery::class; @@ -64,8 +39,7 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]) - || isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]); + return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]); } public function getTypeFromMethodCall( @@ -76,23 +50,21 @@ public function getTypeFromMethodCall( { $methodName = $methodReflection->getName(); - if (isset(self::METHOD_HYDRATION_MODE[$methodName])) { - $hydrationMode = new ConstantIntegerType(self::METHOD_HYDRATION_MODE[$methodName]); - } elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { - $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; - $args = $methodCall->getArgs(); + if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { + throw new ShouldNotHappenException(); + } - if (isset($args[$argIndex])) { - $hydrationMode = $scope->getType($args[$argIndex]->value); - } else { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() - ); - $parameter = $parametersAcceptor->getParameters()[$argIndex]; - $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); - } + $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; + $args = $methodCall->getArgs(); + + if (isset($args[$argIndex])) { + $hydrationMode = $scope->getType($args[$argIndex]->value); } else { - throw new ShouldNotHappenException(); + $parametersAcceptor = ParametersAcceptorSelector::selectSingle( + $methodReflection->getVariants() + ); + $parameter = $parametersAcceptor->getParameters()[$argIndex]; + $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); } $queryType = $scope->getType($methodCall->var); @@ -126,54 +98,23 @@ private function getMethodReturnTypeForHydrationMode( return $this->originalReturnType($methodReflection); } - if (!$hydrationMode instanceof ConstantIntegerType) { + if (!$this->isObjectHydrationMode($hydrationMode)) { + // We support only HYDRATE_OBJECT. For other hydration modes, we + // return the declared return type of the method. return $this->originalReturnType($methodReflection); } - $singleResult = false; - switch ($hydrationMode->getValue()) { - case AbstractQuery::HYDRATE_OBJECT: - break; - case AbstractQuery::HYDRATE_ARRAY: - $queryResultType = $this->getArrayHydratedReturnType($queryResultType); - break; - case AbstractQuery::HYDRATE_SCALAR: - $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - break; - case AbstractQuery::HYDRATE_SINGLE_SCALAR: - $singleResult = true; - $queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType); - break; - case AbstractQuery::HYDRATE_SIMPLEOBJECT: - $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); - break; - case AbstractQuery::HYDRATE_SCALAR_COLUMN: - $queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType); - break; - default: - return $this->originalReturnType($methodReflection); - } - switch ($methodReflection->getName()) { case 'getSingleResult': return $queryResultType; case 'getOneOrNullResult': - $nullableQueryResultType = TypeCombinator::addNull($queryResultType); - if ($queryResultType instanceof BenevolentUnionType) { - $nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType); - } - - return $nullableQueryResultType; + return TypeCombinator::addNull($queryResultType); case 'toIterable': return new IterableType( $queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType, $queryResultType ); default: - if ($singleResult) { - return $queryResultType; - } - if ($queryKeyType->isNull()->yes()) { return AccessoryArrayListType::intersectWith(new ArrayType( new IntegerType(), @@ -187,104 +128,13 @@ private function getMethodReturnTypeForHydrationMode( } } - private function getArrayHydratedReturnType(Type $queryResultType): Type - { - $objectManager = $this->objectMetadataResolver->getObjectManager(); - - return TypeTraverser::map( - $queryResultType, - static function (Type $type, callable $traverse) use ($objectManager): Type { - $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); - if ($isObject->no()) { - return $traverse($type); - } - if ( - $isObject->maybe() - || !$type instanceof TypeWithClassName - || $objectManager === null - ) { - return new MixedType(); - } - - if (!$objectManager->getMetadataFactory()->hasMetadataFor($type->getClassName())) { - return $traverse($type); - } - - // We could return `new ArrayTyp(new MixedType(), new MixedType())` - // but the lack of precision in the array keys/values would give false positive - // @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 - return new MixedType(); - } - ); - } - - private function getScalarHydratedReturnType(Type $queryResultType): Type - { - if (!$queryResultType->isArray()->yes()) { - return new ArrayType(new MixedType(), new MixedType()); - } - - foreach ($queryResultType->getArrays() as $arrayType) { - $itemType = $arrayType->getItemType(); - - if ( - !(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no() - || !$itemType->isArray()->no() - ) { - return new ArrayType(new MixedType(), new MixedType()); - } - } - - return $queryResultType; - } - - private function getSimpleObjectHydratedReturnType(Type $queryResultType): Type - { - if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { - return $queryResultType; - } - - return new MixedType(); - } - - private function getSingleScalarHydratedReturnType(Type $queryResultType): Type + private function isObjectHydrationMode(Type $type): bool { - $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if (!$queryResultType->isConstantArray()->yes()) { - return new MixedType(); - } - - $types = []; - foreach ($queryResultType->getConstantArrays() as $constantArrayType) { - $values = $constantArrayType->getValueTypes(); - if (count($values) !== 1) { - return new MixedType(); - } - - $types[] = $constantArrayType->getFirstIterableValueType(); - } - - return TypeCombinator::union(...$types); - } - - private function getScalarColumnHydratedReturnType(Type $queryResultType): Type - { - $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if (!$queryResultType->isConstantArray()->yes()) { - return new MixedType(); - } - - $types = []; - foreach ($queryResultType->getConstantArrays() as $constantArrayType) { - $values = $constantArrayType->getValueTypes(); - if (count($values) !== 1) { - return new MixedType(); - } - - $types[] = $constantArrayType->getFirstIterableValueType(); + if (!$type instanceof ConstantIntegerType) { + return false; } - return TypeCombinator::union(...$types); + return $type->getValue() === AbstractQuery::HYDRATE_OBJECT; } private function originalReturnType(MethodReflection $methodReflection): Type diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index e43afa88..060dd092 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -107,9 +107,6 @@ class QueryResultTypeWalker extends SqlWalker /** @var bool */ private $hasGroupByClause; - /** @var bool */ - private $hasWhereClause; - /** * @param Query $query */ @@ -138,7 +135,6 @@ public function __construct($query, $parserResult, array $queryComponents) $this->nullableQueryComponents = []; $this->hasAggregateFunction = false; $this->hasGroupByClause = false; - $this->hasWhereClause = false; // The object is instantiated by Doctrine\ORM\Query\Parser, so receiving // dependencies through the constructor is not an option. Instead, we @@ -181,7 +177,6 @@ public function walkSelectStatement(AST\SelectStatement $AST) $this->typeBuilder->setSelectQuery(); $this->hasAggregateFunction = $this->hasAggregateFunction($AST); $this->hasGroupByClause = $AST->groupByClause !== null; - $this->hasWhereClause = $AST->whereClause !== null; $this->walkFromClause($AST->fromClause); @@ -800,7 +795,7 @@ public function walkSelectExpression($selectExpression) $type = $this->resolveDoctrineType($typeName, $enumType, $nullable); - $this->addScalar($resultAlias, $type); + $this->typeBuilder->addScalar($resultAlias, $type); return ''; } @@ -846,32 +841,21 @@ public function walkSelectExpression($selectExpression) // the driver and PHP version. // Here we assume that the value may or may not be casted to // string by the driver. - $casted = false; - $originalType = $type; - - $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$casted): Type { + $type = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } if ($type instanceof IntegerType || $type instanceof FloatType) { - $casted = true; return TypeCombinator::union($type->toString(), $type); } if ($type instanceof BooleanType) { - $casted = true; return TypeCombinator::union($type->toInteger()->toString(), $type); } return $traverse($type); }); - - // Since we made supposition about possibly casted values, - // we can only provide a benevolent union. - if ($casted && $type instanceof UnionType && !$originalType->equals($type)) { - $type = TypeUtils::toBenevolentUnion($type); - } } - $this->addScalar($resultAlias, $type); + $this->typeBuilder->addScalar($resultAlias, $type); return ''; } @@ -1292,21 +1276,6 @@ public function walkResultVariable($resultVariable) return $this->marshalType(new MixedType()); } - /** - * @param array-key $alias - */ - private function addScalar($alias, Type $type): void - { - // Since we don't check the condition inside the WHERE - // conditions, we cannot be sure all the union types are correct. - // For exemple, a condition `WHERE foo.bar IS NOT NULL` could be added. - if ($this->hasWhereClause && $type instanceof UnionType) { - $type = TypeUtils::toBenevolentUnion($type); - } - - $this->typeBuilder->addScalar($alias, $type); - } - private function unmarshalType(string $marshalledType): Type { $type = unserialize($marshalledType); diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 9fb20455..1393a9bc 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -28,7 +28,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use QueryResult\Entities\Embedded; use QueryResult\Entities\JoinedChild; @@ -442,25 +442,6 @@ public function getTestData(): iterable ', ]; - yield 'scalar with where condition' => [ - $this->constantArray([ - [new ConstantStringType('intColumn'), new IntegerType()], - [new ConstantStringType('stringColumn'), new StringType()], - [ - new ConstantStringType('stringNullColumn'), - TypeUtils::toBenevolentUnion(TypeCombinator::addNull(new StringType())), - ], - [new ConstantStringType('datetimeColumn'), new ObjectType(DateTime::class)], - [new ConstantStringType('datetimeImmutableColumn'), new ObjectType(DateTimeImmutable::class)], - ]), - ' - SELECT m.intColumn, m.stringColumn, m.stringNullColumn, - m.datetimeColumn, m.datetimeImmutableColumn - FROM QueryResult\Entities\Many m - WHERE m.stringNullColumn IS NOT NULL - ', - ]; - yield 'scalar with alias' => [ $this->constantArray([ [new ConstantStringType('i'), new IntegerType()], @@ -563,12 +544,12 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantIntegerType(1), new ConstantIntegerType(2), new ConstantStringType('1'), new ConstantStringType('2') - )), + ), ], [ new ConstantIntegerType(3), @@ -615,7 +596,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(1), - $this->intStringified(true), + TypeCombinator::addNull($this->intStringified()), ], [ new ConstantIntegerType(2), @@ -629,7 +610,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(4), - $this->intStringified(true), + TypeCombinator::addNull($this->intStringified()), ], [ new ConstantIntegerType(5), @@ -639,7 +620,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(6), - $this->intStringified(true), + TypeCombinator::addNull($this->intStringified()), ], [ new ConstantIntegerType(7), @@ -665,7 +646,7 @@ public function getTestData(): iterable ], [ new ConstantStringType('max'), - $this->intStringified(true), + TypeCombinator::addNull($this->intStringified()), ], [ new ConstantStringType('arithmetic'), @@ -697,10 +678,10 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantStringType('1'), new ConstantIntegerType(1) - )), + ), ], [new ConstantIntegerType(2), new ConstantStringType('hello')], ]), @@ -714,11 +695,11 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1'), new NullType() - )), + ), ], ]), ' @@ -800,12 +781,12 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), new ConstantStringType('1') - )), + ), ], ]), ' @@ -821,12 +802,12 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), new ConstantStringType('1') - )), + ), ], ]), ' @@ -842,31 +823,31 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1') - )), + ), ], [ new ConstantIntegerType(2), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantIntegerType(0), new ConstantStringType('0') - )), + ), ], [ new ConstantIntegerType(3), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1') - )), + ), ], [ new ConstantIntegerType(4), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantIntegerType(0), new ConstantStringType('0') - )), + ), ], ]), ' @@ -1038,10 +1019,10 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeUtils::toBenevolentUnion(TypeCombinator::union( + TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1') - )), + ), ], [ new ConstantStringType('intColumn'), @@ -1085,7 +1066,7 @@ public function getTestData(): iterable yield 'new arguments affect scalar counter' => [ $this->constantArray([ - [new ConstantIntegerType(5), $this->intStringified(true)], + [new ConstantIntegerType(5), TypeCombinator::addNull($this->intStringified())], [new ConstantIntegerType(0), new ObjectType(ManyId::class)], [new ConstantIntegerType(1), new ObjectType(OneId::class)], ]), @@ -1102,7 +1083,7 @@ public function getTestData(): iterable [new ConstantStringType('intColumn'), new IntegerType()], [new ConstantIntegerType(1), $this->intStringified()], [new ConstantIntegerType(2), $this->intStringified()], - [new ConstantIntegerType(3), $this->intStringified(true)], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->intStringified())], [new ConstantIntegerType(4), $this->intStringified()], [new ConstantIntegerType(5), $this->intStringified()], [new ConstantIntegerType(6), $this->numericStringified()], @@ -1124,9 +1105,9 @@ public function getTestData(): iterable yield 'abs function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->unumericStringified()], - [new ConstantIntegerType(2), $this->unumericStringified(true)], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->unumericStringified())], [new ConstantIntegerType(3), $this->unumericStringified()], - [new ConstantIntegerType(4), $this->unumericStringified()], + [new ConstantIntegerType(4), TypeCombinator::union($this->unumericStringified())], ]), ' SELECT ABS(m.intColumn), @@ -1140,7 +1121,7 @@ public function getTestData(): iterable yield 'bit_and function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), $this->uintStringified(true)], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], [new ConstantIntegerType(3), $this->uintStringified()], ]), ' @@ -1154,7 +1135,7 @@ public function getTestData(): iterable yield 'bit_or function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), $this->uintStringified(true)], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], [new ConstantIntegerType(3), $this->uintStringified()], ]), ' @@ -1230,8 +1211,8 @@ public function getTestData(): iterable yield 'date_diff function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->numericStringified()], - [new ConstantIntegerType(2), $this->numericStringified(true)], - [new ConstantIntegerType(3), $this->numericStringified(true)], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->numericStringified())], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->numericStringified())], [new ConstantIntegerType(4), $this->numericStringified()], ]), ' @@ -1273,9 +1254,11 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - $this->hasTypedExpressions() - ? $this->uint(true) - : $this->uintStringified(true), + TypeCombinator::addNull( + $this->hasTypedExpressions() + ? $this->uint() + : $this->uintStringified() + ), ], [ new ConstantIntegerType(3), @@ -1296,8 +1279,8 @@ public function getTestData(): iterable yield 'locate function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), $this->uintStringified(true)], - [new ConstantIntegerType(3), $this->uintStringified(true)], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintStringified())], [new ConstantIntegerType(4), $this->uintStringified()], ]), ' @@ -1339,8 +1322,8 @@ public function getTestData(): iterable yield 'mod function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), $this->uintStringified(true)], - [new ConstantIntegerType(3), $this->uintStringified(true)], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintStringified())], [new ConstantIntegerType(4), $this->uintStringified()], ]), ' @@ -1354,7 +1337,7 @@ public function getTestData(): iterable yield 'mod function error' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->uintStringified(true)], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->uintStringified())], ]), ' SELECT MOD(10, NULLIF(m.intColumn, m.intColumn)) @@ -1413,15 +1396,15 @@ public function getTestData(): iterable yield 'identity function' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->intStringified(true)], - [new ConstantIntegerType(2), $this->intStringified()], - [new ConstantIntegerType(3), $this->intStringified(true)], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(2), $this->numericStringOrInt()], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->numericStringOrInt())], [new ConstantIntegerType(4), TypeCombinator::addNull(new StringType())], [new ConstantIntegerType(5), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(6), $this->intStringified(true)], + [new ConstantIntegerType(6), TypeCombinator::addNull($this->numericStringOrInt())], [new ConstantIntegerType(7), TypeCombinator::addNull(new MixedType())], - [new ConstantIntegerType(8), $this->intStringified(true)], - [new ConstantIntegerType(9), $this->intStringified(true)], + [new ConstantIntegerType(8), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(9), TypeCombinator::addNull($this->numericStringOrInt())], ]), ' SELECT IDENTITY(m.oneNull), @@ -1440,7 +1423,7 @@ public function getTestData(): iterable yield 'select nullable association' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->intStringified(true)], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], ]), ' SELECT DISTINCT(m.oneNull) @@ -1450,7 +1433,7 @@ public function getTestData(): iterable yield 'select non null association' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->intStringified()], + [new ConstantIntegerType(1), $this->numericStringOrInt()], ]), ' SELECT DISTINCT(m.one) @@ -1460,7 +1443,7 @@ public function getTestData(): iterable yield 'select default nullability association' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->intStringified(true)], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], ]), ' SELECT DISTINCT(m.oneDefaultNullability) @@ -1470,7 +1453,7 @@ public function getTestData(): iterable yield 'select non null association in aggregated query' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->intStringified(true)], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], [ new ConstantIntegerType(2), $this->hasTypedExpressions() @@ -1540,6 +1523,17 @@ private function constantArray(array $elements): Type return $builder->getArray(); } + private function numericStringOrInt(): Type + { + return new UnionType([ + new IntegerType(), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + ]); + } + private function numericString(): Type { return new IntersectionType([ @@ -1548,67 +1542,42 @@ private function numericString(): Type ]); } - private function uint(bool $nullable = false): Type + private function uint(): Type { - $type = IntegerRangeType::fromInterval(0, null); - if ($nullable) { - TypeCombinator::addNull($type); - } - - return $type; + return IntegerRangeType::fromInterval(0, null); } - private function intStringified(bool $nullable = false): Type + private function intStringified(): Type { - $types = [ + return TypeCombinator::union( new IntegerType(), - $this->numericString(), - ]; - if ($nullable) { - $types[] = new NullType(); - } - - return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + $this->numericString() + ); } - private function uintStringified(bool $nullable = false): Type + private function uintStringified(): Type { - $types = [ + return TypeCombinator::union( $this->uint(), - $this->numericString(), - ]; - if ($nullable) { - $types[] = new NullType(); - } - - return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + $this->numericString() + ); } - private function numericStringified(bool $nullable = false): Type + private function numericStringified(): Type { - $types = [ + return TypeCombinator::union( new FloatType(), new IntegerType(), - $this->numericString(), - ]; - if ($nullable) { - $types[] = new NullType(); - } - - return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + $this->numericString() + ); } - private function unumericStringified(bool $nullable = false): Type + private function unumericStringified(): Type { - $types = [ + return TypeCombinator::union( new FloatType(), IntegerRangeType::fromInterval(0, null), - $this->numericString(), - ]; - if ($nullable) { - $types[] = new NullType(); - } - - return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + $this->numericString() + ); } private function hasTypedExpressions(): bool diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index d6972943..02469e46 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -144,11 +144,11 @@ public function testReturnTypeOfQueryMethodsWithExplicitObjectHydrationMode(Enti } /** - * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_ARRAY + * Test that we properly infer the return type of Query methods with explicit hydration mode that is not HYDRATE_OBJECT * - * We can infer the return type by changing every object by an array + * We are never able to infer the return type here */ - public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(EntityManagerInterface $em): void + public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(EntityManagerInterface $em): void { $query = $em->createQuery(' SELECT m @@ -156,27 +156,23 @@ public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(Entit '); assertType( - 'list', + 'mixed', $query->getResult(AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', - $query->getArrayResult() - ); - assertType( - 'iterable', + 'iterable', $query->toIterable([], AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'mixed', $query->execute(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'mixed', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( - 'list', + 'mixed', $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) ); assertType( @@ -187,382 +183,6 @@ public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(Entit 'mixed', $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) ); - - $query = $em->createQuery(' - SELECT m.intColumn, m.stringNullColumn, m.datetimeColumn - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'list', - $query->getArrayResult() - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}', - $query->getSingleResult(AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) - ); - } - - /** - * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR - */ - public function testReturnTypeOfQueryMethodsWithExplicitScalarHydrationMode(EntityManagerInterface $em): void - { - $query = $em->createQuery(' - SELECT m - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->getScalarResult() - ); - assertType( - 'iterable', - $query->toIterable([], AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'array', - $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'array|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) - ); - - $query = $em->createQuery(' - SELECT m.intColumn, m.stringNullColumn - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->getScalarResult() - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'array{intColumn: int, stringNullColumn: string|null}', - $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'array{intColumn: int, stringNullColumn: string|null}|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) - ); - } - - /** - * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR - */ - public function testReturnTypeOfQueryMethodsWithExplicitSingleScalarHydrationMode(EntityManagerInterface $em): void - { - $query = $em->createQuery(' - SELECT m - FROM QueryResult\Entities\Many m - '); - - assertType( - 'mixed', - $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->getSingleScalarResult() - ); - assertType( - 'iterable', - $query->toIterable([], AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - - $query = $em->createQuery(' - SELECT m.intColumn - FROM QueryResult\Entities\Many m - '); - - assertType( - 'int', - $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int', - $query->getSingleScalarResult() - ); - assertType( - 'int', - $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int', - $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - - $query = $em->createQuery(' - SELECT COUNT(m.id) - FROM QueryResult\Entities\Many m - '); - - assertType( - '(int<0, max>|numeric-string)', - $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - '(int<0, max>|numeric-string)', - $query->getSingleScalarResult() - ); - assertType( - '(int<0, max>|numeric-string)', - $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - '(int<0, max>|numeric-string)', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - '(int<0, max>|numeric-string)', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - '(int<0, max>|numeric-string)', - $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - '(int<0, max>|numeric-string|null)', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - } - - /** - * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SIMPLEOBJECT - * - * We are never able to infer the return type here - */ - public function testReturnTypeOfQueryMethodsWithExplicitSimpleObjectHydrationMode(EntityManagerInterface $em): void - { - $query = $em->createQuery(' - SELECT m - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'iterable', - $query->toIterable([], AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'QueryResult\Entities\Many', - $query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'QueryResult\Entities\Many|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - - $query = $em->createQuery(' - SELECT m.intColumn, m.stringNullColumn - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'mixed', - $query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - assertType( - 'mixed', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) - ); - } - - /** - * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR_COLUMN - * - * We are never able to infer the return type here - */ - public function testReturnTypeOfQueryMethodsWithExplicitScalarColumnHydrationMode(EntityManagerInterface $em): void - { - $query = $em->createQuery(' - SELECT m - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->getSingleColumnResult() - ); - assertType( - 'iterable', - $query->toIterable([], AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'mixed', - $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'mixed', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - - $query = $em->createQuery(' - SELECT m.intColumn - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->getSingleColumnResult() - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'int', - $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'int|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); } /** From 3f5f7247922c3b7a0d0dddc69fa32d1fc39dc5b4 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 24 Jan 2024 11:54:26 +0100 Subject: [PATCH 013/160] Readme: add reportDynamicQueryBuilders --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 0e9a8b2d..ee01a1d6 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,20 @@ $query->getOneOrNullResult(Query::HYDRATE_OBJECT); // User This is due to the design of the `Query` class preventing from determining the hydration mode used by these functions unless it is specified explicitly during the call. +### Problematic approaches + +Not every QueryBuilder can be statically analysed, here are few advices to maximize type inferring: +- Do not pass QueryBuilder to methods +- Do not use dynamic expressions in QueryBuilder methods (mainly in `select`/`join`/`from`/`set`) + +You can enable reporting of places where inferring is unavailable by: + +```neon +parameters: + doctrine: + reportDynamicQueryBuilders: true +``` + ## Custom types If your application uses custom Doctrine types, you can write your own type descriptors to analyse them properly. From e3a05e7759d7ab7063d425462bb581042e7376ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 21:26:08 +0000 Subject: [PATCH 014/160] Update metcalfc/changelog-generator action to v4.3.1 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2fb750a4..b1a669a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.2.0 + uses: metcalfc/changelog-generator@v4.3.1 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 6c1d4cb28e886757454c44411e8f7f90efeb4205 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Feb 2024 19:29:39 +0000 Subject: [PATCH 015/160] Update github-actions --- .github/workflows/release-toot.yml | 2 +- .github/workflows/test-projects.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml index 6a1c8156..1ba4fd77 100644 --- a/.github/workflows/release-toot.yml +++ b/.github/workflows/release-toot.yml @@ -10,7 +10,7 @@ jobs: toot: runs-on: ubuntu-latest steps: - - uses: cbrgm/mastodon-github-action@v1 + - uses: cbrgm/mastodon-github-action@v2 if: ${{ !github.event.repository.private }} with: # GitHub event payload diff --git a/.github/workflows/test-projects.yml b/.github/workflows/test-projects.yml index d10750fe..6bd1e05b 100644 --- a/.github/workflows/test-projects.yml +++ b/.github/workflows/test-projects.yml @@ -21,7 +21,7 @@ jobs: - "packagist/private-packagist" steps: - - uses: peter-evans/repository-dispatch@v2 + - uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.REPO_ACCESS_TOKEN }} repository: "${{ matrix.repository }}" From 3819054ad807afad116605c764b02c5adff9f852 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 17 Apr 2024 13:21:10 +0200 Subject: [PATCH 016/160] Synchronize with doctrine --- stubs/Collections/Collection.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/Collections/Collection.stub b/stubs/Collections/Collection.stub index 455733c8..be162ef6 100644 --- a/stubs/Collections/Collection.stub +++ b/stubs/Collections/Collection.stub @@ -21,7 +21,7 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable * * @param T $element * - * @return true + * @return void */ public function add($element) {} From 179ca9b78e80379ede86f4d6ef0c208ce12bae84 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 17 Apr 2024 15:43:29 +0200 Subject: [PATCH 017/160] Use conditional loading --- extension.neon | 1 - .../Doctrine/StubFilesExtensionLoader.php | 9 ++++ stubs/Collections/Collection.stub | 2 +- stubs/Collections/Collection1.stub | 46 +++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 stubs/Collections/Collection1.stub diff --git a/extension.neon b/extension.neon index 01b69199..0c798f8f 100644 --- a/extension.neon +++ b/extension.neon @@ -38,7 +38,6 @@ parameters: - stubs/Persistence/ObjectRepository.stub - stubs/RepositoryFactory.stub - stubs/Collections/ArrayCollection.stub - - stubs/Collections/Collection.stub - stubs/Collections/ReadableCollection.stub - stubs/Collections/Selectable.stub - stubs/ORM/AbstractQuery.stub diff --git a/src/Stubs/Doctrine/StubFilesExtensionLoader.php b/src/Stubs/Doctrine/StubFilesExtensionLoader.php index 422f71da..68aa0d6c 100644 --- a/src/Stubs/Doctrine/StubFilesExtensionLoader.php +++ b/src/Stubs/Doctrine/StubFilesExtensionLoader.php @@ -2,10 +2,12 @@ namespace PHPStan\Stubs\Doctrine; +use Composer\InstalledVersions; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\PhpDoc\StubFilesExtension; use function dirname; +use function strpos; class StubFilesExtensionLoader implements StubFilesExtension { @@ -59,6 +61,13 @@ public function getFiles(): array $files[] = $stubsDir . '/ServiceEntityRepository.stub'; } + $collectionVersion = InstalledVersions::getVersion('doctrine/dbal'); + if ($collectionVersion !== null && strpos($collectionVersion, '1.') === 0) { + $files[] = $stubsDir . '/Collections/Collection1.stub'; + } else { + $files[] = $stubsDir . '/Collections/Collection.stub'; + } + return $files; } diff --git a/stubs/Collections/Collection.stub b/stubs/Collections/Collection.stub index be162ef6..455733c8 100644 --- a/stubs/Collections/Collection.stub +++ b/stubs/Collections/Collection.stub @@ -21,7 +21,7 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable * * @param T $element * - * @return void + * @return true */ public function add($element) {} diff --git a/stubs/Collections/Collection1.stub b/stubs/Collections/Collection1.stub new file mode 100644 index 00000000..be162ef6 --- /dev/null +++ b/stubs/Collections/Collection1.stub @@ -0,0 +1,46 @@ + + * @extends ArrayAccess + * @extends ReadableCollection + */ +interface Collection extends Countable, IteratorAggregate, ArrayAccess, ReadableCollection +{ + + /** + * @phpstan-impure + * + * @param T $element + * + * @return void + */ + public function add($element) {} + + /** + * @phpstan-impure + * + * @param TKey $key + * + * @return T|null + */ + public function remove($key) {} + + /** + * @phpstan-impure + * + * @param T $element + * + * @return bool + */ + public function removeElement($element) {} + +} From 2519289755f2349dfcaa8800b2b4b715b0ed1292 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 17 Apr 2024 15:54:18 +0200 Subject: [PATCH 018/160] Fix --- stubs/Collections/Collection.stub | 2 +- stubs/Collections/Collection1.stub | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stubs/Collections/Collection.stub b/stubs/Collections/Collection.stub index 455733c8..be162ef6 100644 --- a/stubs/Collections/Collection.stub +++ b/stubs/Collections/Collection.stub @@ -21,7 +21,7 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable * * @param T $element * - * @return true + * @return void */ public function add($element) {} diff --git a/stubs/Collections/Collection1.stub b/stubs/Collections/Collection1.stub index be162ef6..455733c8 100644 --- a/stubs/Collections/Collection1.stub +++ b/stubs/Collections/Collection1.stub @@ -21,7 +21,7 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable * * @param T $element * - * @return void + * @return true */ public function add($element) {} From c71d9360d0f1019f788ec880881eea2cec3622ea Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 17 Apr 2024 17:19:57 +0200 Subject: [PATCH 019/160] Fix --- src/Stubs/Doctrine/StubFilesExtensionLoader.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Stubs/Doctrine/StubFilesExtensionLoader.php b/src/Stubs/Doctrine/StubFilesExtensionLoader.php index 68aa0d6c..ed5d1590 100644 --- a/src/Stubs/Doctrine/StubFilesExtensionLoader.php +++ b/src/Stubs/Doctrine/StubFilesExtensionLoader.php @@ -61,7 +61,9 @@ public function getFiles(): array $files[] = $stubsDir . '/ServiceEntityRepository.stub'; } - $collectionVersion = InstalledVersions::getVersion('doctrine/dbal'); + $collectionVersion = class_exists(InstalledVersions::class) + ? InstalledVersions::getVersion('doctrine/collections') + : null; if ($collectionVersion !== null && strpos($collectionVersion, '1.') === 0) { $files[] = $stubsDir . '/Collections/Collection1.stub'; } else { From 849b856d1715a8dae8df131bdc188d87881e2982 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 17 Apr 2024 17:21:09 +0200 Subject: [PATCH 020/160] Add try catch --- src/Stubs/Doctrine/StubFilesExtensionLoader.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Stubs/Doctrine/StubFilesExtensionLoader.php b/src/Stubs/Doctrine/StubFilesExtensionLoader.php index ed5d1590..f53ef16a 100644 --- a/src/Stubs/Doctrine/StubFilesExtensionLoader.php +++ b/src/Stubs/Doctrine/StubFilesExtensionLoader.php @@ -3,9 +3,11 @@ namespace PHPStan\Stubs\Doctrine; use Composer\InstalledVersions; +use OutOfBoundsException; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\PhpDoc\StubFilesExtension; +use function class_exists; use function dirname; use function strpos; @@ -61,9 +63,13 @@ public function getFiles(): array $files[] = $stubsDir . '/ServiceEntityRepository.stub'; } - $collectionVersion = class_exists(InstalledVersions::class) - ? InstalledVersions::getVersion('doctrine/collections') - : null; + try { + $collectionVersion = class_exists(InstalledVersions::class) + ? InstalledVersions::getVersion('doctrine/collections') + : null; + } catch (OutOfBoundsException $e) { + $collectionVersion = null; + } if ($collectionVersion !== null && strpos($collectionVersion, '1.') === 0) { $files[] = $stubsDir . '/Collections/Collection1.stub'; } else { From bcb451fe270cef57656da93a68117cbc03a08dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Sat, 20 Apr 2024 08:38:13 +0200 Subject: [PATCH 021/160] Update lock-closed-issues.yml --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index c2b017b9..78405931 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -2,7 +2,7 @@ name: 'Lock Issues' on: schedule: - - cron: '0 0 * * *' + - cron: '3 0 * * *' jobs: lock: From d951254dd620c4b7fae248d80dcdadd43d8f5202 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 24 Apr 2024 15:37:55 +0200 Subject: [PATCH 022/160] Allow PHP errors in specific test to workaround Doctrine parser bug --- tests/Rules/Doctrine/ORM/DqlRuleTest.php | 6 ++++++ tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/tests/Rules/Doctrine/ORM/DqlRuleTest.php b/tests/Rules/Doctrine/ORM/DqlRuleTest.php index 007ed40b..32ea2396 100644 --- a/tests/Rules/Doctrine/ORM/DqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/DqlRuleTest.php @@ -52,4 +52,10 @@ public function testRule(): void ]); } + protected function shouldFailOnPhpErrors(): bool + { + // doctrine/orm/src/Query/Parser.php throws assert($this->lexer->lookahead !== null) + return false; + } + } diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php index 28700630..1166c816 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php @@ -171,4 +171,10 @@ public static function getAdditionalConfigFiles(): array ]; } + protected function shouldFailOnPhpErrors(): bool + { + // doctrine/orm/src/Query/Parser.php throws assert(): assert($peek !== null) error + return false; + } + } From 776038df01bc69fe4ad49291ba2234bf518050ed Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 24 Apr 2024 15:58:44 +0200 Subject: [PATCH 023/160] Fix --- tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php index e633b464..e6494866 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php @@ -134,4 +134,10 @@ public static function getAdditionalConfigFiles(): array ]; } + protected function shouldFailOnPhpErrors(): bool + { + // doctrine/orm/src/Query/Parser.php throws assert($peek !== null) failed + return false; + } + } From 4b66f5c996865a6085983cc90b5c8a242d1959e7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 24 Apr 2024 16:05:48 +0200 Subject: [PATCH 024/160] Fix --- tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php | 4 ++++ tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php index e6494866..4c59b7de 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\ObjectMetadataResolver; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -22,6 +23,9 @@ protected function getRule(): Rule public function testRule(): void { + if (PHP_VERSION_ID < 70300) { + self::markTestSkipped('For some reason PHP 7.2 cannot recover from Trying to get property value of non-object'); + } $this->analyse([__DIR__ . '/data/query-builder-dql.php'], [ [ "QueryBuilder: [Syntax Error] line 0, col 66: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1)", diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php index 1166c816..7b04762e 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\ObjectMetadataResolver; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -22,6 +23,10 @@ protected function getRule(): Rule public function testRule(): void { + if (PHP_VERSION_ID < 70300) { + self::markTestSkipped('For some reason PHP 7.2 cannot recover from Trying to get property value of non-object'); + } + $this->analyse([__DIR__ . '/data/query-builder-dql.php'], [ [ "QueryBuilder: [Syntax Error] line 0, col 66: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1)", From 4058fdc7333cb9e46f8cf5b952cd9cb59658f2e5 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 15 May 2024 16:50:54 +0200 Subject: [PATCH 025/160] Return null from dynamic return type extension instead of copying default type --- .../QueryGetDqlDynamicReturnTypeExtension.php | 9 ++------ .../QueryResultDynamicReturnTypeExtension.php | 17 ++++----------- ...QueryBuilderDynamicReturnTypeExtension.php | 5 ++--- ...seExpressionDynamicReturnTypeExtension.php | 11 ++++------ ...ssionBuilderDynamicReturnTypeExtension.php | 13 +++++------- ...lderGetQueryDynamicReturnTypeExtension.php | 21 +++++++------------ 6 files changed, 25 insertions(+), 51 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryGetDqlDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryGetDqlDynamicReturnTypeExtension.php index 1c9608b7..d58a5554 100644 --- a/src/Type/Doctrine/Query/QueryGetDqlDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryGetDqlDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Doctrine\DoctrineTypeUtils; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -30,16 +29,12 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { $calledOnType = $scope->getType($methodCall->var); $queryTypes = DoctrineTypeUtils::getQueryTypes($calledOnType); if (count($queryTypes) === 0) { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants() - )->getReturnType(); + return null; } $dqls = []; diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 20e23831..8bdc9239 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -46,7 +46,7 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { $methodName = $methodReflection->getName(); @@ -82,7 +82,7 @@ private function getMethodReturnTypeForHydrationMode( Type $hydrationMode, Type $queryKeyType, Type $queryResultType - ): Type + ): ?Type { $isVoidType = (new VoidType())->isSuperTypeOf($queryResultType); @@ -95,13 +95,13 @@ private function getMethodReturnTypeForHydrationMode( if ($isVoidType->maybe()) { // We can't be sure what the query type is, so we return the // declared return type of the method. - return $this->originalReturnType($methodReflection); + return null; } if (!$this->isObjectHydrationMode($hydrationMode)) { // We support only HYDRATE_OBJECT. For other hydration modes, we // return the declared return type of the method. - return $this->originalReturnType($methodReflection); + return null; } switch ($methodReflection->getName()) { @@ -137,13 +137,4 @@ private function isObjectHydrationMode(Type $type): bool return $type->getValue() === AbstractQuery::HYDRATE_OBJECT; } - private function originalReturnType(MethodReflection $methodReflection): Type - { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() - ); - - return $parametersAcceptor->getReturnType(); - } - } diff --git a/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php index a9ea14e5..f170b550 100644 --- a/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php @@ -8,7 +8,6 @@ use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; use function array_unshift; @@ -31,7 +30,7 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { $entityNameExpr = new MethodCall($methodCall->var, new Identifier('getEntityName')); @@ -41,7 +40,7 @@ public function getTypeFromMethodCall( } if (!isset($methodCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } $fromArgs = $methodCall->getArgs(); diff --git a/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php index 8564d374..d797b45f 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Doctrine\ORM\DynamicQueryBuilderArgumentException; use PHPStan\Type\Doctrine\ArgumentsProcessor; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -38,25 +37,23 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return in_array($methodReflection->getName(), ['add', 'addMultiple'], true); } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); - try { $args = $this->argumentsProcessor->processArgs($scope, $methodReflection->getName(), $methodCall->getArgs()); } catch (DynamicQueryBuilderArgumentException $e) { - return $defaultReturnType; + return null; } $calledOnType = $scope->getType($methodCall->var); if (!$calledOnType instanceof ExprType) { - return $defaultReturnType; + return null; } $expr = $calledOnType->getExprObject(); if (!method_exists($expr, $methodReflection->getName())) { - return $defaultReturnType; + return null; } $exprValue = $expr->{$methodReflection->getName()}(...$args); diff --git a/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php index f9033aa7..fae7cd28 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Doctrine\ORM\DynamicQueryBuilderArgumentException; use PHPStan\Type\Doctrine\ArgumentsProcessor; use PHPStan\Type\Doctrine\ObjectMetadataResolver; @@ -44,17 +43,15 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return true; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); - $objectManager = $this->objectMetadataResolver->getObjectManager(); if ($objectManager === null) { - return $defaultReturnType; + return null; } $entityManagerInterface = 'Doctrine\ORM\EntityManagerInterface'; if (!$objectManager instanceof $entityManagerInterface) { - return $defaultReturnType; + return null; } /** @var EntityManagerInterface $objectManager */ @@ -65,7 +62,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method try { $args = $this->argumentsProcessor->processArgs($scope, $methodReflection->getName(), $methodCall->getArgs()); } catch (DynamicQueryBuilderArgumentException $e) { - return $defaultReturnType; + return null; } $calledOnType = $scope->getType($methodCall->var); @@ -76,7 +73,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } if (!method_exists($expr, $methodReflection->getName())) { - return $defaultReturnType; + return null; } $exprValue = $expr->{$methodReflection->getName()}(...$args); diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php index c5df245e..bd0c26f9 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php @@ -12,7 +12,6 @@ use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Doctrine\ORM\DynamicQueryBuilderArgumentException; use PHPStan\Type\Doctrine\ArgumentsProcessor; use PHPStan\Type\Doctrine\DescriptorRegistry; @@ -93,26 +92,22 @@ public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope - ): Type + ): ?Type { $calledOnType = $scope->getType($methodCall->var); - $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants() - )->getReturnType(); + $queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType); if (count($queryBuilderTypes) === 0) { - return $defaultReturnType; + return null; } $objectManager = $this->objectMetadataResolver->getObjectManager(); if ($objectManager === null) { - return $defaultReturnType; + return null; } $entityManagerInterface = 'Doctrine\ORM\EntityManagerInterface'; if (!$objectManager instanceof $entityManagerInterface) { - return $defaultReturnType; + return null; } /** @var EntityManagerInterface $objectManager */ @@ -150,7 +145,7 @@ public function getTypeFromMethodCall( try { $args = $this->argumentsProcessor->processArgs($scope, $methodName, array_slice($calledMethodCall->getArgs(), 0, 1)); } catch (DynamicQueryBuilderArgumentException $e) { - return $defaultReturnType; + return null; } if (count($args) === 1) { $queryBuilder->set($args[0], $args[0]); @@ -168,13 +163,13 @@ public function getTypeFromMethodCall( if (in_array($lowerMethodName, self::METHODS_NOT_AFFECTING_RESULT_TYPE, true)) { continue; } - return $defaultReturnType; + return null; } try { $queryBuilder->{$methodName}(...$args); } catch (Throwable $e) { - return $defaultReturnType; + return null; } } From a223e357c5f153b446b8a5da57dbc1132eb7a88d Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 28 May 2024 15:38:29 +0200 Subject: [PATCH 026/160] Lowercase aggregate functions should not be inferred as mixed --- src/Type/Doctrine/Query/QueryResultTypeWalker.php | 3 ++- .../Doctrine/Query/QueryResultTypeWalkerTest.php | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 14519922..d68bcdb8 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -50,6 +50,7 @@ use function serialize; use function sprintf; use function strtolower; +use function strtoupper; use function unserialize; /** @@ -930,7 +931,7 @@ public function walkSimpleSelectExpression($simpleSelectExpression): string */ public function walkAggregateExpression($aggExpression): string { - switch ($aggExpression->functionName) { + switch (strtoupper($aggExpression->functionName)) { case 'MAX': case 'MIN': $type = $this->unmarshalType( diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index ea1e0aea..983362c2 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -645,6 +645,19 @@ public function getTestData(): iterable ', ]; + yield 'aggregate lowercase' => [ + $this->constantArray([ + [ + new ConstantStringType('foo'), + TypeCombinator::addNull($this->numericStringified()), + ], + ]), + ' + SELECT avg(m.intColumn) as foo + FROM QueryResult\Entities\Many m + ', + ]; + yield 'aggregate with group by' => [ $this->constantArray([ [ From 811046b380dd4d6d85c2af63af9755fb8341c9a9 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 24 Jan 2024 12:03:20 +0100 Subject: [PATCH 027/160] Readme: add reportUnknownTypes --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index ee01a1d6..da5a9429 100644 --- a/README.md +++ b/README.md @@ -210,3 +210,13 @@ services: factory: PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor('MyApp\MyCustomTypeName') tags: [phpstan.doctrine.typeDescriptor] ``` + +### Ensure types have descriptor + +If you want to be sure you never forget descriptor for some custom type, you can enable: + +```neon +parameters: + doctrine: + reportUnknownTypes: true +``` From f6081cf0d486267ebde7bd3d37ee6bed397c88f9 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 28 May 2024 15:54:35 +0200 Subject: [PATCH 028/160] Add example --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index da5a9429..a93658c5 100644 --- a/README.md +++ b/README.md @@ -220,3 +220,16 @@ parameters: doctrine: reportUnknownTypes: true ``` + +This causes failures when your entity uses custom type without descriptor: + +```php +#[Entity] +abstract class Uuid7Entity +{ + + #[Id] + #[Column(type: Uuid7Type::NAME)] // reported when descriptor for such type is missing + private Uuid7 $hsCode; + +``` From 6a25c9d1be3ac547b36e89fb029013017e8a4347 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 30 May 2024 16:30:06 +0200 Subject: [PATCH 029/160] QueryBuilderDqlRule: mention DQL for BranchingQueryBuilderType --- src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php | 11 ++++++++--- .../Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php | 4 ++++ tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php | 5 +++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php index 0cc99224..8596810d 100644 --- a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php +++ b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php @@ -118,10 +118,15 @@ public function processNode(Node $node, Scope $scope): array $message .= sprintf("\nDQL: %s", $dql->getValue()); } + $builder = RuleErrorBuilder::message($message) + ->identifier('doctrine.dql'); + + if (count($dqls) > 1) { + $builder->addTip('Detected from DQL branch: ' . $dql->getValue()); + } + // Use message as index to prevent duplicate - $messages[$message] = RuleErrorBuilder::message($message) - ->identifier('doctrine.dql') - ->build(); + $messages[$message] = $builder->build(); } catch (AssertionError $e) { continue; } diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php index 4c59b7de..adad87ee 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php @@ -112,18 +112,22 @@ public function testRuleBranches(): void [ 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', 59, + 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE p.id = 1', ], [ 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', 90, + 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND t.id = 1', ], [ 'QueryBuilder: [Semantical Error] line 0, col 95 near \'foo = 1\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named foo', 107, + 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND e.foo = 1', ], [ 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', 107, + 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND t.id = 1', ], ]; $this->analyse([__DIR__ . '/data/query-builder-branches-dql.php'], $errors); diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php index 7b04762e..ec3dafb7 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php @@ -113,18 +113,22 @@ public function testRuleBranches(): void [ 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', 59, + 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE p.id = 1', ], [ 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', 90, + 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND t.id = 1', ], [ 'QueryBuilder: [Semantical Error] line 0, col 95 near \'foo = 1\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named foo', 107, + 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND e.foo = 1', ], [ 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', 107, + 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND t.id = 1', ], ]; $this->analyse([__DIR__ . '/data/query-builder-branches-dql.php'], $errors); @@ -151,6 +155,7 @@ public function testBranchingPerformance(): void [ 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1 AND\': Error: \'p\' is not defined.', 121, + 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE p.id = 1 AND p.id = 3 AND p.id = 5 AND p.id = 7 AND p.id = 9 AND p.id = 11 AND p.id = 13 AND p.id = 15 AND p.id = 15 AND p.id = 15 AND p.id = 15 AND p.id = 15 AND p.id = 15 AND p.id = 15 AND p.id = 15 AND p.id = 15 AND p.id = 15 AND p.id = 15', ], ]); } From dd71401bea9eaa5ad1cadeeccdfe057fa836926d Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 31 May 2024 16:02:15 +0200 Subject: [PATCH 030/160] Platform test: compare inferred types with real SQL engine results --- .github/workflows/platform-matrix-test.yml | 61 +++ .gitignore | 1 + phpstan.neon | 6 + phpunit.xml | 6 + src/Type/Doctrine/Descriptors/BooleanType.php | 3 +- .../Doctrine/Query/QueryResultTypeWalker.php | 8 +- tests/Platform/MatrixEntity/TestEntity.php | 56 +++ ...eryResultTypeWalkerFetchTypeMatrixTest.php | 411 ++++++++++++++++++ tests/Platform/README.md | 19 + tests/Platform/data/config.neon | 5 + tests/Platform/docker/Dockerfile80 | 7 + tests/Platform/docker/Dockerfile81 | 7 + tests/Platform/docker/docker-compose.yml | 46 ++ tests/Platform/docker/docker-setup.sh | 7 + .../Query/QueryResultTypeWalkerTest.php | 28 +- 15 files changed, 660 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/platform-matrix-test.yml create mode 100644 tests/Platform/MatrixEntity/TestEntity.php create mode 100644 tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php create mode 100644 tests/Platform/README.md create mode 100644 tests/Platform/data/config.neon create mode 100644 tests/Platform/docker/Dockerfile80 create mode 100644 tests/Platform/docker/Dockerfile81 create mode 100644 tests/Platform/docker/docker-compose.yml create mode 100755 tests/Platform/docker/docker-setup.sh diff --git a/.github/workflows/platform-matrix-test.yml b/.github/workflows/platform-matrix-test.yml new file mode 100644 index 00000000..a74ec83c --- /dev/null +++ b/.github/workflows/platform-matrix-test.yml @@ -0,0 +1,61 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Platform matrix test" + +on: + pull_request: + push: + branches: + - "1.4.x" + +jobs: + tests: + name: "Platform matrix test" + runs-on: "ubuntu-latest" + env: + MYSQL_HOST: '127.0.0.1' + PGSQL_HOST: '127.0.0.1' + + strategy: + fail-fast: false + matrix: + php-version: + - "8.0" + - "8.1" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + ini-file: development + extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Run platform matrix test" + run: vendor/bin/phpunit --group=platform + + services: + postgres: + image: "postgres:latest" + env: + POSTGRES_PASSWORD: "secret" + POSTGRES_USER: root + POSTGRES_DB: foo + ports: + - "5432:5432" + + mysql: + image: "mysql:latest" + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: foo + ports: + - "3306:3306" diff --git a/.gitignore b/.gitignore index 7de9f3c5..e3d740a7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /build-cs /vendor /composer.lock +/.env .phpunit.result.cache diff --git a/phpstan.neon b/phpstan.neon index b3c5d642..8dfe69fa 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -43,3 +43,9 @@ parameters: message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ORM\\\\EntityManager'' and ''create'' will always evaluate to true\.$#' path: src/Doctrine/Mapping/ClassMetadataFactory.php reportUnmatched: false + - + messages: + - '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' + - '#^Cannot call method getWrappedResourceHandle\(\) on class\-string\|object\.$#' + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + reportUnmatched: false diff --git a/phpunit.xml b/phpunit.xml index 6d69639a..f4beeb21 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -36,5 +36,11 @@ + + + platform + + + diff --git a/src/Type/Doctrine/Descriptors/BooleanType.php b/src/Type/Doctrine/Descriptors/BooleanType.php index 0e6980c0..955883a8 100644 --- a/src/Type/Doctrine/Descriptors/BooleanType.php +++ b/src/Type/Doctrine/Descriptors/BooleanType.php @@ -28,7 +28,8 @@ public function getDatabaseInternalType(): Type { return TypeCombinator::union( new ConstantIntegerType(0), - new ConstantIntegerType(1) + new ConstantIntegerType(1), + new \PHPStan\Type\BooleanType() ); } diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index d68bcdb8..55375c5f 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -14,6 +14,7 @@ use Doctrine\ORM\Query\SqlWalker; use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -1113,8 +1114,11 @@ public function walkLiteral($literal): string break; case AST\Literal::BOOLEAN: - $value = strtolower($literal->value) === 'true' ? 1 : 0; - $type = new ConstantIntegerType($value); + $value = strtolower($literal->value) === 'true'; + $type = TypeCombinator::union( + new ConstantIntegerType($value ? 1 : 0), + new ConstantBooleanType($value) + ); break; case AST\Literal::NUMERIC: diff --git a/tests/Platform/MatrixEntity/TestEntity.php b/tests/Platform/MatrixEntity/TestEntity.php new file mode 100644 index 00000000..74f2ae57 --- /dev/null +++ b/tests/Platform/MatrixEntity/TestEntity.php @@ -0,0 +1,56 @@ + $connectionParams + * @param array $expectedOnPhp80AndBelow + * @param array $expectedOnPhp81AndAbove + * @param array $connectionAttributes + * + * @dataProvider provideCases + */ + public function testFetchedTypes( + array $connectionParams, + array $expectedOnPhp80AndBelow, + array $expectedOnPhp81AndAbove, + array $connectionAttributes + ): void + { + $phpVersion = PHP_MAJOR_VERSION * 10 + PHP_MINOR_VERSION; + + try { + $connection = DriverManager::getConnection($connectionParams + [ + 'user' => 'root', + 'password' => 'secret', + 'dbname' => 'foo', + ]); + + $nativeConnection = $this->getNativeConnection($connection); + $this->setupAttributes($nativeConnection, $connectionAttributes); + + $config = new Configuration(); + $config->setProxyNamespace('PHPstan\Doctrine\OrmMatrixProxies'); + $config->setProxyDir('/tmp/doctrine'); + $config->setAutoGenerateProxyClasses(false); + $config->setSecondLevelCacheEnabled(false); + $config->setMetadataCache(new ArrayCachePool()); + $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), [__DIR__ . '/MatrixEntity'])); + $entityManager = new EntityManager($connection, $config); + + } catch (DbalException $e) { + if (strpos($e->getMessage(), 'Doctrine currently supports only the following drivers') !== false) { + self::markTestSkipped($e->getMessage()); // older doctrine versions, needed for old PHP versions + } + throw $e; + } + + $schemaTool = new SchemaTool($entityManager); + $classes = $entityManager->getMetadataFactory()->getAllMetadata(); + $schemaTool->dropSchema($classes); + $schemaTool->createSchema($classes); + + $entity = new TestEntity(); + $entity->col_bool = true; + $entity->col_float = 0.125; + $entity->col_decimal = '0.1'; + $entity->col_int = 9; + $entity->col_bigint = '2147483648'; + $entity->col_string = 'foobar'; + + $entityManager->persist($entity); + $entityManager->flush(); + + $columnsQueryTemplate = 'SELECT %s FROM %s t GROUP BY t.col_int, t.col_float, t.col_decimal, t.col_bigint, t.col_bool, t.col_string'; + + $expected = $phpVersion >= 81 + ? $expectedOnPhp81AndAbove + : $expectedOnPhp80AndBelow; + + foreach ($expected as $select => $expectedType) { + if ($expectedType === null) { + continue; // e.g. no such function + } + $dql = sprintf($columnsQueryTemplate, $select, TestEntity::class); + + $query = $entityManager->createQuery($dql); + $result = $query->getSingleResult(); + + $typeBuilder = new QueryResultTypeBuilder(); + QueryResultTypeWalker::walk($query, $typeBuilder, self::getContainer()->getByType(DescriptorRegistry::class)); + + $inferredPhpStanType = $typeBuilder->getResultType(); + $realRowPhpStanType = ConstantTypeHelper::getTypeFromValue($result); + + $firstResult = reset($result); + $resultType = gettype($firstResult); + $resultExported = var_export($firstResult, true); + + self::assertTrue( + $inferredPhpStanType->accepts($realRowPhpStanType, true)->yes(), + sprintf( + "Result of 'SELECT %s' for '%s' and PHP %s was inferred as %s, but the real result was %s", + $select, + $this->dataName(), + $phpVersion, + $inferredPhpStanType->describe(VerbosityLevel::precise()), + $realRowPhpStanType->describe(VerbosityLevel::precise()) + ) + ); + + self::assertThat( + $firstResult, + new IsType($expectedType), + sprintf( + "Result of 'SELECT %s' for '%s' and PHP %s is expected to be %s, but %s returned (%s).", + $select, + $this->dataName(), + $phpVersion, + $expectedType, + $resultType, + $resultExported + ) + ); + } + } + + /** + * @return iterable + */ + public function provideCases(): iterable + { + // Preserve space-driven formatting for better readability + // phpcs:disable Squiz.WhiteSpace.OperatorSpacing.SpacingBefore + // phpcs:disable Squiz.WhiteSpace.OperatorSpacing.SpacingAfter + + // Notes: + // - Any direct column fetch uses the type declared in entity, but when passed to a function, the driver decides the type + + $testData = [ // mysql, sqlite, pdo_pgsql, pgsql, stringified, stringifiedOldPostgre + // bool-ish + '(TRUE)' => ['int', 'int', 'bool', 'bool', 'string', 'bool'], + 't.col_bool' => ['bool', 'bool', 'bool', 'bool', 'bool', 'bool'], + 'COALESCE(t.col_bool, TRUE)' => ['int', 'int', 'bool', 'bool', 'string', 'bool'], + + // float-ish + 't.col_float' => ['float', 'float', 'float', 'float', 'float', 'float'], + 'AVG(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], + 'SUM(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], + 'MIN(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], + 'MAX(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], + 'SQRT(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], + 'ABS(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], + + // decimal-ish + 't.col_decimal' => ['string', 'string', 'string', 'string', 'string', 'string'], + '0.1' => ['string', 'float', 'string', 'string', 'string', 'string'], + '0.125e0' => ['float', 'float', 'string', 'string', 'string', 'string'], + 'AVG(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'AVG(t.col_int)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'AVG(t.col_bigint)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'SUM(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'MIN(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'MAX(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'SQRT(t.col_decimal)' => ['float', 'float', 'string', 'string', 'string', 'string'], + 'SQRT(t.col_int)' => ['float', 'float', 'string', 'float', 'string', 'string'], + 'SQRT(t.col_bigint)' => ['float', null, 'string', 'float', null, null], // sqlite3 returns float, but pdo_sqlite returns NULL + 'ABS(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], + + // int-ish + '1' => ['int', 'int', 'int', 'int', 'string', 'string'], + '2147483648' => ['int', 'int', 'int', 'int', 'string', 'string'], + 't.col_int' => ['int', 'int', 'int', 'int', 'int', 'int'], + 't.col_bigint' => ['string', 'string', 'string', 'string', 'string', 'string'], + 'SUM(t.col_int)' => ['string', 'int', 'int', 'int', 'string', 'string'], + 'SUM(t.col_bigint)' => ['string', 'int', 'string', 'string', 'string', 'string'], + "LENGTH('')" => ['int', 'int', 'int', 'int', 'int', 'int'], + 'COUNT(t)' => ['int', 'int', 'int', 'int', 'int', 'int'], + 'COUNT(1)' => ['int', 'int', 'int', 'int', 'int', 'int'], + 'COUNT(t.col_int)' => ['int', 'int', 'int', 'int', 'int', 'int'], + 'MIN(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MIN(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MAX(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MAX(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MOD(t.col_int, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MOD(t.col_bigint, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'ABS(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'ABS(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], + + // string + 't.col_string' => ['string', 'string', 'string', 'string', 'string', 'string'], + 'LOWER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], + 'UPPER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], + 'TRIM(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], + ]; + + $selects = array_keys($testData); + + $nativeMysql = array_combine($selects, array_column($testData, 0)); + $nativeSqlite = array_combine($selects, array_column($testData, 1)); + $nativePdoPg = array_combine($selects, array_column($testData, 2)); + $nativePg = array_combine($selects, array_column($testData, 3)); + + $stringified = array_combine($selects, array_column($testData, 4)); + $stringifiedOldPostgre = array_combine($selects, array_column($testData, 5)); + + yield 'sqlite3' => [ + 'connection' => ['driver' => 'sqlite3', 'memory' => true], + 'php80-' => $nativeSqlite, + 'php81+' => $nativeSqlite, + 'setup' => [], + ]; + + yield 'pdo_sqlite, no stringify' => [ + 'connection' => ['driver' => 'pdo_sqlite', 'memory' => true], + 'php80-' => $stringified, + 'php81+' => $nativeSqlite, + 'setup' => [], + ]; + + yield 'pdo_sqlite, stringify' => [ + 'connection' => ['driver' => 'pdo_sqlite', 'memory' => true], + 'php80-' => $stringified, + 'php81+' => $stringified, + 'setup' => [PDO::ATTR_STRINGIFY_FETCHES => true], + ]; + + yield 'mysqli, no native numbers' => [ + 'connection' => ['driver' => 'mysqli', 'host' => getenv('MYSQL_HOST')], + 'php80-' => $nativeMysql, + 'php81+' => $nativeMysql, + 'setup' => [ + // This has no effect when using prepared statements (which is what doctrine/dbal uses) + // - prepared statements => always native types + // - non-prepared statements => stringified by default, can be changed by MYSQLI_OPT_INT_AND_FLOAT_NATIVE = true + // documented here: https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php#example-4303 + MYSQLI_OPT_INT_AND_FLOAT_NATIVE => false, + ], + ]; + + yield 'mysqli, native numbers' => [ + 'connection' => ['driver' => 'mysqli', 'host' => getenv('MYSQL_HOST')], + 'php80-' => $nativeMysql, + 'php81+' => $nativeMysql, + 'setup' => [MYSQLI_OPT_INT_AND_FLOAT_NATIVE => true], + ]; + + yield 'pdo_mysql, stringify, no emulate' => [ + 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], + 'php80-' => $stringified, + 'php81+' => $stringified, + 'setup' => [ + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_STRINGIFY_FETCHES => true, + ], + ]; + + yield 'pdo_mysql, no stringify, no emulate' => [ + 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], + 'php80-' => $nativeMysql, + 'php81+' => $nativeMysql, + 'setup' => [PDO::ATTR_EMULATE_PREPARES => false], + ]; + + yield 'pdo_mysql, no stringify, emulate' => [ + 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], + 'php80-' => $stringified, + 'php81+' => $nativeMysql, + 'setup' => [], // defaults + ]; + + yield 'pdo_mysql, stringify, emulate' => [ + 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], + 'php80-' => $stringified, + 'php81+' => $stringified, + 'setup' => [ + PDO::ATTR_STRINGIFY_FETCHES => true, + ], + ]; + + yield 'pdo_pgsql, stringify' => [ + 'connection' => ['driver' => 'pdo_pgsql', 'host' => getenv('PGSQL_HOST')], + + 'php80-' => $stringifiedOldPostgre, + 'php81+' => $stringified, + 'setup' => [PDO::ATTR_STRINGIFY_FETCHES => true], + ]; + + yield 'pdo_pgsql, no stringify' => [ + 'connection' => ['driver' => 'pdo_pgsql', 'host' => getenv('PGSQL_HOST')], + 'php80-' => $nativePdoPg, + 'php81+' => $nativePdoPg, + 'setup' => [], + ]; + + yield 'pgsql' => [ + 'connection' => ['driver' => 'pgsql', 'host' => getenv('PGSQL_HOST')], + 'php80-' => $nativePg, + 'php81+' => $nativePg, + 'setup' => [], + ]; + } + + /** + * @param mixed $nativeConnection + * @param array $attributes + */ + private function setupAttributes($nativeConnection, array $attributes): void + { + if ($nativeConnection instanceof PDO) { + foreach ($attributes as $attribute => $value) { + $set = $nativeConnection->setAttribute($attribute, $value); + if (!$set) { + throw new LogicException(sprintf('Failed to set attribute %s to %s', $attribute, $value)); + } + } + + } elseif ($nativeConnection instanceof mysqli) { + foreach ($attributes as $attribute => $value) { + $set = $nativeConnection->options($attribute, $value); + if (!$set) { + throw new LogicException(sprintf('Failed to set attribute %s to %s', $attribute, $value)); + } + } + + } elseif (is_a($nativeConnection, 'PgSql\Connection', true)) { + if ($attributes !== []) { + throw new LogicException('Cannot set attributes for PgSql\Connection driver'); + } + + } elseif ($nativeConnection instanceof SQLite3) { + if ($attributes !== []) { + throw new LogicException('Cannot set attributes for ' . SQLite3::class . ' driver'); + } + + } elseif (is_resource($nativeConnection)) { // e.g. `resource (pgsql link)` on PHP < 8.1 with pgsql driver + if ($attributes !== []) { + throw new LogicException('Cannot set attributes for this resource'); + } + + } else { + throw new LogicException('Unexpected connection: ' . (function_exists('get_debug_type') ? get_debug_type($nativeConnection) : gettype($nativeConnection))); + } + } + + /** + * @return mixed + */ + private function getNativeConnection(Connection $connection) + { + if (method_exists($connection, 'getNativeConnection')) { + return $connection->getNativeConnection(); + } + + if (method_exists($connection, 'getWrappedConnection')) { + if ($connection->getWrappedConnection() instanceof PDO) { + return $connection->getWrappedConnection(); + } + + if (method_exists($connection->getWrappedConnection(), 'getWrappedResourceHandle')) { + return $connection->getWrappedConnection()->getWrappedResourceHandle(); + } + } + + throw new LogicException('Unable to get native connection'); + } + +} diff --git a/tests/Platform/README.md b/tests/Platform/README.md new file mode 100644 index 00000000..b9c07d6a --- /dev/null +++ b/tests/Platform/README.md @@ -0,0 +1,19 @@ + +## How to run platform tests in docker + +Set current working directory to project root. + +```sh +# Init services & dependencies +- `printf "UID=$(id -u)\nGID=$(id -g)" > .env` +- `docker-compose -f tests/Platform/docker/docker-compose.yml up -d` +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 composer install` + +# Test behaviour with old stringification +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php80 php -d memory_limit=1G vendor/bin/phpunit --group=platform` + +# Test behaviour with new stringification +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform` +``` + +You can also run utilize those containers for PHPStorm PHPUnit configuration. diff --git a/tests/Platform/data/config.neon b/tests/Platform/data/config.neon new file mode 100644 index 00000000..38f26ed7 --- /dev/null +++ b/tests/Platform/data/config.neon @@ -0,0 +1,5 @@ +includes: + - ../../../extension.neon +parameters: + featureToggles: + listType: true diff --git a/tests/Platform/docker/Dockerfile80 b/tests/Platform/docker/Dockerfile80 new file mode 100644 index 00000000..37b6694c --- /dev/null +++ b/tests/Platform/docker/Dockerfile80 @@ -0,0 +1,7 @@ +FROM php:8.0-cli + +COPY ./docker-setup.sh /opt/src/scripts/setup.sh +RUN /opt/src/scripts/setup.sh + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + diff --git a/tests/Platform/docker/Dockerfile81 b/tests/Platform/docker/Dockerfile81 new file mode 100644 index 00000000..4ef5c3df --- /dev/null +++ b/tests/Platform/docker/Dockerfile81 @@ -0,0 +1,7 @@ +FROM php:8.1-cli + +COPY ./docker-setup.sh /opt/src/scripts/setup.sh +RUN /opt/src/scripts/setup.sh + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + diff --git a/tests/Platform/docker/docker-compose.yml b/tests/Platform/docker/docker-compose.yml new file mode 100644 index 00000000..5ff6fbb8 --- /dev/null +++ b/tests/Platform/docker/docker-compose.yml @@ -0,0 +1,46 @@ +# the setup here should be in sync with GitHub CI services, see platform-matrix-test.yml + +services: + mysql: + image: mysql:8.0 + command: mysqld --default-authentication-plugin=mysql_native_password + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: foo + + pgsql: + image: postgres:13 + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: secret + POSTGRES_USER: root + POSTGRES_DB: foo + + php80: + depends_on: [mysql, pgsql] + build: + context: . + dockerfile: ./Dockerfile80 + environment: + MYSQL_HOST: mysql + PGSQL_HOST: pgsql + working_dir: /app + user: ${UID:-1000}:${GID:-1000} + volumes: + - ../../../:/app + + php81: + depends_on: [mysql, pgsql] + build: + context: . + dockerfile: ./Dockerfile81 + environment: + MYSQL_HOST: mysql + PGSQL_HOST: pgsql + working_dir: /app + user: ${UID:-1000}:${GID:-1000} + volumes: + - ../../../:/app diff --git a/tests/Platform/docker/docker-setup.sh b/tests/Platform/docker/docker-setup.sh new file mode 100755 index 00000000..341c88c2 --- /dev/null +++ b/tests/Platform/docker/docker-setup.sh @@ -0,0 +1,7 @@ +set -ex \ + && apt update \ + && apt install -y bash zip libpq-dev libsqlite3-dev \ + && pecl install xdebug mongodb \ + && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ + && docker-php-ext-install pdo mysqli pgsql pdo_mysql pdo_pgsql pdo_sqlite \ + && docker-php-ext-enable xdebug mongodb diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 983362c2..f2f349ed 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -13,7 +13,9 @@ use Doctrine\ORM\Tools\SchemaTool; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; @@ -794,7 +796,8 @@ public function getTestData(): iterable TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1'), - new NullType() + new NullType(), + new ConstantBooleanType(true) ), ], ]), @@ -810,7 +813,8 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - new IntegerType() + new IntegerType(), + new ConstantBooleanType(false) ), ], [ @@ -839,6 +843,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), + new ConstantBooleanType(false), new ConstantIntegerType(0) ), ], @@ -859,6 +864,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), + new ConstantBooleanType(false), new ConstantIntegerType(0) ), ], @@ -881,7 +887,8 @@ public function getTestData(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), - new ConstantStringType('1') + new ConstantStringType('1'), + new BooleanType() ), ], ]), @@ -902,7 +909,8 @@ public function getTestData(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), - new ConstantStringType('1') + new ConstantStringType('1'), + new BooleanType() ), ], ]), @@ -921,28 +929,32 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new ConstantIntegerType(1), - new ConstantStringType('1') + new ConstantStringType('1'), + new ConstantBooleanType(true) ), ], [ new ConstantIntegerType(2), TypeCombinator::union( new ConstantIntegerType(0), - new ConstantStringType('0') + new ConstantStringType('0'), + new ConstantBooleanType(false) ), ], [ new ConstantIntegerType(3), TypeCombinator::union( new ConstantIntegerType(1), - new ConstantStringType('1') + new ConstantStringType('1'), + new ConstantBooleanType(true) ), ], [ new ConstantIntegerType(4), TypeCombinator::union( new ConstantIntegerType(0), - new ConstantStringType('0') + new ConstantStringType('0'), + new ConstantBooleanType(false) ), ], ]), From f39003e7be746e16afecc8699ec5c550df560165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olda=20=C5=A0=C3=A1lek?= Date: Fri, 7 Jun 2024 12:20:56 +0200 Subject: [PATCH 031/160] Add `immediately-invoked-callable` into EntityManager stub --- stubs/EntityManagerInterface.stub | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/stubs/EntityManagerInterface.stub b/stubs/EntityManagerInterface.stub index 87e8fbcb..caf65b47 100644 --- a/stubs/EntityManagerInterface.stub +++ b/stubs/EntityManagerInterface.stub @@ -68,4 +68,13 @@ interface EntityManagerInterface extends ObjectManager */ public function getClassMetadata($className); + /** + * @param-immediately-invoked-callable $func + * @param callable(): T $func + * @return T + * + * @template T + */ + public function wrapInTransaction(callable $func); + } From dd27a3e83777ba0d9e9cedfaf4ebf95ff67b271f Mon Sep 17 00:00:00 2001 From: John Bafford Date: Fri, 7 Jun 2024 16:51:59 -0400 Subject: [PATCH 032/160] Fix EntityManagerInterface wrapInTransaction stub `wrapInTransaction`'s callable takes `self` as a parameter. --- stubs/EntityManagerInterface.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/EntityManagerInterface.stub b/stubs/EntityManagerInterface.stub index caf65b47..2f1eba4c 100644 --- a/stubs/EntityManagerInterface.stub +++ b/stubs/EntityManagerInterface.stub @@ -70,7 +70,7 @@ interface EntityManagerInterface extends ObjectManager /** * @param-immediately-invoked-callable $func - * @param callable(): T $func + * @param callable(self): T $func * @return T * * @template T From 4f2113acd24ae263952250a5ab45a0c975c340fd Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 24 Jun 2024 16:40:59 +0200 Subject: [PATCH 033/160] Platform test: expand matrix --- ...form-matrix-test.yml => platform-test.yml} | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) rename .github/workflows/{platform-matrix-test.yml => platform-test.yml} (56%) diff --git a/.github/workflows/platform-matrix-test.yml b/.github/workflows/platform-test.yml similarity index 56% rename from .github/workflows/platform-matrix-test.yml rename to .github/workflows/platform-test.yml index a74ec83c..22e867ea 100644 --- a/.github/workflows/platform-matrix-test.yml +++ b/.github/workflows/platform-test.yml @@ -1,6 +1,6 @@ # https://help.github.com/en/categories/automating-your-workflow-with-github-actions -name: "Platform matrix test" +name: "Platform test" on: pull_request: @@ -10,18 +10,31 @@ on: jobs: tests: - name: "Platform matrix test" + name: "Platform test" runs-on: "ubuntu-latest" env: MYSQL_HOST: '127.0.0.1' PGSQL_HOST: '127.0.0.1' + MSSQL_HOST: '127.0.0.1' strategy: fail-fast: false matrix: php-version: + - "7.4" - "8.0" - "8.1" + - "8.2" + - "8.3" + update-packages: + - "" + include: + - php-version: "8.1" + update-packages: doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 + - php-version: "8.2" + update-packages: doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 + - php-version: "8.3" + update-packages: doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 steps: - name: "Checkout" @@ -38,6 +51,10 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" + - name: "Update packages" + if: ${{ matrix.update-packages != '' }} + run: composer require --dev ${{ matrix.update-packages }} -W + - name: "Run platform matrix test" run: vendor/bin/phpunit --group=platform @@ -59,3 +76,12 @@ jobs: MYSQL_DATABASE: foo ports: - "3306:3306" + + mssql: + image: mcr.microsoft.com/mssql/server:latest + env: + ACCEPT_EULA: Y + SA_PASSWORD: 'Secret.123' + MSSQL_PID: Developer + ports: + - 1433:1433 From 0f5e82fb93dd3294fb61450d40d0138bfd02b2fa Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 24 Jun 2024 16:58:04 +0200 Subject: [PATCH 034/160] Fix test in dbal4 --- .../PlatformEntity.php} | 27 +++++++++++----- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 31 ++++++++++++++++--- 2 files changed, 45 insertions(+), 13 deletions(-) rename tests/Platform/{MatrixEntity/TestEntity.php => Entity/PlatformEntity.php} (55%) diff --git a/tests/Platform/MatrixEntity/TestEntity.php b/tests/Platform/Entity/PlatformEntity.php similarity index 55% rename from tests/Platform/MatrixEntity/TestEntity.php rename to tests/Platform/Entity/PlatformEntity.php index 74f2ae57..d0c1a5aa 100644 --- a/tests/Platform/MatrixEntity/TestEntity.php +++ b/tests/Platform/Entity/PlatformEntity.php @@ -1,6 +1,6 @@ setAutoGenerateProxyClasses(false); $config->setSecondLevelCacheEnabled(false); $config->setMetadataCache(new ArrayCachePool()); - $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), [__DIR__ . '/MatrixEntity'])); + + if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/orm', '3.*')) { + $config->setMetadataDriverImpl(new AttributeDriver([__DIR__ . '/Entity'])); + } else { + $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), [__DIR__ . '/Entity'])); + } + $entityManager = new EntityManager($connection, $config); } catch (DbalException $e) { @@ -104,7 +115,8 @@ public function testFetchedTypes( $schemaTool->dropSchema($classes); $schemaTool->createSchema($classes); - $entity = new TestEntity(); + $entity = new PlatformEntity(); + $entity->id = '1'; $entity->col_bool = true; $entity->col_float = 0.125; $entity->col_decimal = '0.1'; @@ -125,7 +137,7 @@ public function testFetchedTypes( if ($expectedType === null) { continue; // e.g. no such function } - $dql = sprintf($columnsQueryTemplate, $select, TestEntity::class); + $dql = sprintf($columnsQueryTemplate, $select, PlatformEntity::class); $query = $entityManager->createQuery($dql); $result = $query->getSingleResult(); @@ -214,7 +226,7 @@ public function provideCases(): iterable '1' => ['int', 'int', 'int', 'int', 'string', 'string'], '2147483648' => ['int', 'int', 'int', 'int', 'string', 'string'], 't.col_int' => ['int', 'int', 'int', 'int', 'int', 'int'], - 't.col_bigint' => ['string', 'string', 'string', 'string', 'string', 'string'], + 't.col_bigint' => self::hasDbal4() ? array_fill(0, 6, 'int') : array_fill(0, 6, 'string'), 'SUM(t.col_int)' => ['string', 'int', 'int', 'int', 'string', 'string'], 'SUM(t.col_bigint)' => ['string', 'int', 'string', 'string', 'string', 'string'], "LENGTH('')" => ['int', 'int', 'int', 'int', 'int', 'int'], @@ -408,4 +420,13 @@ private function getNativeConnection(Connection $connection) throw new LogicException('Unable to get native connection'); } + private static function hasDbal4(): bool + { + if (!class_exists(InstalledVersions::class)) { + return false; + } + + return InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '4.*'); + } + } From b860565e92bcd740e8d5b4c4c4653eb644432609 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 09:27:31 +0200 Subject: [PATCH 035/160] More precise type inference with unary plus and minus --- src/Type/Doctrine/Query/QueryResultTypeWalker.php | 14 +++++++++++++- .../Doctrine/Query/QueryResultTypeWalkerTest.php | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 55375c5f..1292ed8d 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -1261,7 +1261,19 @@ public function walkArithmeticFactor($factor): string $primary = $factor->arithmeticPrimary; $type = $this->unmarshalType($this->walkArithmeticPrimary($primary)); - $type = TypeUtils::generalizeType($type, GeneralizePrecision::lessSpecific()); + + if ($type instanceof ConstantIntegerType && $factor->sign === false) { + $type = new ConstantIntegerType($type->getValue() * -1); + + } elseif ($type instanceof IntegerRangeType && $factor->sign === false) { + $type = IntegerRangeType::fromInterval( + $type->getMax() === null ? null : $type->getMax() * -1, + $type->getMin() === null ? null : $type->getMin() * -1 + ); + + } elseif ($type instanceof ConstantFloatType && $factor->sign === false) { + $type = new ConstantFloatType($type->getValue() * -1); + } return $this->marshalType($type); } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index f2f349ed..f54be18a 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -1189,7 +1189,7 @@ public function getTestData(): iterable yield 'arithmetic' => [ $this->constantArray([ [new ConstantStringType('intColumn'), new IntegerType()], - [new ConstantIntegerType(1), $this->intStringified()], + [new ConstantIntegerType(1), TypeCombinator::union(new ConstantIntegerType(1), new ConstantStringType('1'))], [new ConstantIntegerType(2), $this->intStringified()], [new ConstantIntegerType(3), TypeCombinator::addNull($this->intStringified())], [new ConstantIntegerType(4), $this->intStringified()], From 25e200b33221fbf2ae91feafd87cdea631325a1d Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 10:41:28 +0200 Subject: [PATCH 036/160] Add unary minus test --- .../Query/QueryResultTypeWalkerTest.php | 18 +++++++++++++++++- .../Doctrine/data/QueryResult/Entities/One.php | 7 +++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index f54be18a..e5e9aad9 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -82,6 +82,7 @@ public static function setUpBeforeClass(): void $dataOne = [ 'intColumn' => [1, 2], + 'floatColumn' => [0.1, 2.0], 'stringColumn' => ['A', 'B'], 'stringNullColumn' => ['A', null], ]; @@ -108,10 +109,11 @@ public static function setUpBeforeClass(): void $id = 1; foreach (self::combinations($dataOne) as $combination) { - [$intColumn, $stringColumn, $stringNullColumn] = $combination; + [$intColumn, $floatColumn, $stringColumn, $stringNullColumn] = $combination; $one = new One(); $one->id = (string) $id++; $one->intColumn = $intColumn; + $one->floatColumn = $floatColumn; $one->stringColumn = $stringColumn; $one->stringNullColumn = $stringNullColumn; $embedded = new Embedded(); @@ -1614,6 +1616,20 @@ public function getTestData(): iterable FROM QueryResult\Entities\One o ', ]; + + yield 'unary minus' => [ + $this->constantArray([ + [new ConstantStringType('minusInt'), $this->numericStringOrInt()], // should be nullable + [new ConstantStringType('minusFloat'), TypeCombinator::union(new FloatType(), $this->numericStringOrInt())], // should be nullable && should not include int + [new ConstantStringType('minusIntRange'), TypeCombinator::union(IntegerRangeType::fromInterval(null, 0), $this->numericString())], + ]), + ' + SELECT -o.intColumn as minusInt, + -o.floatColumn as minusFloat, + -COUNT(o.intColumn) as minusIntRange + FROM QueryResult\Entities\One o + ', + ]; } /** diff --git a/tests/Type/Doctrine/data/QueryResult/Entities/One.php b/tests/Type/Doctrine/data/QueryResult/Entities/One.php index 5605c945..81811618 100644 --- a/tests/Type/Doctrine/data/QueryResult/Entities/One.php +++ b/tests/Type/Doctrine/data/QueryResult/Entities/One.php @@ -31,6 +31,13 @@ class One */ public $intColumn; + /** + * @Column(type="float") + * + * @var float + */ + public $floatColumn; + /** * @Column(type="string") * From b09104d80909c8fcb4ee3d3f8a595d9e33a8cecb Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 09:18:40 +0200 Subject: [PATCH 037/160] QueryResultTypeWalker: fix TypedExpression handling --- .../Doctrine/Query/QueryResultTypeWalker.php | 34 ++++++------------- .../Query/QueryResultTypeWalkerTest.php | 34 ++++--------------- 2 files changed, 18 insertions(+), 50 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 1292ed8d..5e4bee91 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -3,7 +3,8 @@ namespace PHPStan\Type\Doctrine\Query; use BackedEnum; -use Doctrine\DBAL\Types\Types; +use Doctrine\DBAL\Types\StringType as DbalStringType; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; @@ -817,28 +818,15 @@ public function walkSelectExpression($selectExpression): string $resultAlias = $selectExpression->fieldIdentificationVariable ?? $this->scalarResultCounter++; $type = $this->unmarshalType($expr->dispatch($this)); - if (class_exists(TypedExpression::class) && $expr instanceof TypedExpression) { - $enforcedType = $this->resolveDoctrineType(Types::INTEGER); - $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($enforcedType): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - if ($type instanceof NullType) { - return $type; - } - if ($enforcedType->accepts($type, true)->yes()) { - return $type; - } - if ($enforcedType instanceof StringType) { - if ($type instanceof IntegerType || $type instanceof FloatType) { - return TypeCombinator::union($type->toString(), $type); - } - if ($type instanceof BooleanType) { - return TypeCombinator::union($type->toInteger()->toString(), $type); - } - } - return $enforcedType; - }); + if ( + $expr instanceof TypedExpression + && !$expr->getReturnType() instanceof DbalStringType // StringType is no-op, so using TypedExpression with that does nothing + ) { + $dbalTypeName = DbalType::getTypeRegistry()->lookupName($expr->getReturnType()); + $type = TypeCombinator::intersect( // e.g. count is typed as int, but we infer int<0, max> + $type, + $this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type)) + ); } else { // Expressions default to Doctrine's StringType, whose // convertToPHPValue() is a no-op. So the actual type depends on diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index e5e9aad9..117d4200 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -9,7 +9,6 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\Column; -use Doctrine\ORM\Query\AST\TypedExpression; use Doctrine\ORM\Tools\SchemaTool; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -615,9 +614,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(3), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), + $this->uint(), ], [ new ConstantIntegerType(4), @@ -625,9 +622,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(5), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), + $this->uint(), ], [ new ConstantIntegerType(6), @@ -682,9 +677,7 @@ public function getTestData(): iterable ], [ new ConstantStringType('count'), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), + $this->uint(), ], ]), ' @@ -1360,23 +1353,17 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), + $this->uint(), ], [ new ConstantIntegerType(2), TypeCombinator::addNull( - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified() + $this->uint() ), ], [ new ConstantIntegerType(3), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), + $this->uint(), ], ]), ' @@ -1568,9 +1555,7 @@ public function getTestData(): iterable [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], [ new ConstantIntegerType(2), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), + $this->uint(), ], ]), ' @@ -1706,11 +1691,6 @@ private function unumericStringified(): Type ); } - private function hasTypedExpressions(): bool - { - return class_exists(TypedExpression::class); - } - /** * @param array $arrays * From 4a1ece8cf496e05f3fa39fe65ae500d018b297a7 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 13:37:41 +0200 Subject: [PATCH 038/160] Introduce DoctrineTypeDriverAwareDescriptor & DriverDetector --- extension.neon | 4 + phpstan.neon | 11 ++ src/Doctrine/Driver/DriverDetector.php | 174 ++++++++++++++++++ src/Type/Doctrine/Descriptors/BooleanType.php | 37 +++- src/Type/Doctrine/Descriptors/DecimalType.php | 38 +++- .../Descriptors/DoctrineTypeDescriptor.php | 13 ++ .../DoctrineTypeDriverAwareDescriptor.php | 29 +++ src/Type/Doctrine/Descriptors/FloatType.php | 41 ++++- .../Doctrine/ORM/EntityColumnRuleTest.php | 3 +- 9 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 src/Doctrine/Driver/DriverDetector.php create mode 100644 src/Type/Doctrine/Descriptors/DoctrineTypeDriverAwareDescriptor.php diff --git a/extension.neon b/extension.neon index 0c798f8f..2f8daea1 100644 --- a/extension.neon +++ b/extension.neon @@ -89,6 +89,10 @@ services: class: PHPStan\Type\Doctrine\DefaultDescriptorRegistry factory: @PHPStan\Type\Doctrine\DescriptorRegistryFactory::createRegistry + - + class: PHPStan\Doctrine\Driver\DriverDetector + arguments: + failOnInvalidConnection: %featureToggles.bleedingEdge% - class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension - diff --git a/phpstan.neon b/phpstan.neon index 8dfe69fa..c467b761 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -49,3 +49,14 @@ parameters: - '#^Cannot call method getWrappedResourceHandle\(\) on class\-string\|object\.$#' path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php reportUnmatched: false + + - + message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' # needed for older DBAL versions + paths: + - src/Doctrine/Driver/DriverDetector.php + + - + messages: # needed for older DBAL versions + - '#^Class PgSql\\Connection not found\.$#' + - '#^Class Doctrine\\DBAL\\Driver\\PgSQL\\Driver not found\.$#' + - '#^Class Doctrine\\DBAL\\Driver\\SQLite3\\Driver not found\.$#' diff --git a/src/Doctrine/Driver/DriverDetector.php b/src/Doctrine/Driver/DriverDetector.php new file mode 100644 index 00000000..0a4371be --- /dev/null +++ b/src/Doctrine/Driver/DriverDetector.php @@ -0,0 +1,174 @@ +failOnInvalidConnection = $failOnInvalidConnection; + } + + /** + * @return self::*|null + */ + public function detect(Connection $connection): ?string + { + $driver = $connection->getDriver(); + + if ($driver instanceof MysqliDriver) { + return self::MYSQLI; + } + + if ($driver instanceof PdoMysqlDriver) { + return self::PDO_MYSQL; + } + + if ($driver instanceof PdoSQLiteDriver) { + return self::PDO_SQLITE; + } + + if ($driver instanceof PdoSqlSrvDriver) { + return self::PDO_SQLSRV; + } + + if ($driver instanceof PdoOciDriver) { + return self::PDO_OCI; + } + + if ($driver instanceof PdoPgSQLDriver) { + return self::PDO_PGSQL; + } + + if ($driver instanceof SQLite3Driver) { + return self::SQLITE3; + } + + if ($driver instanceof PgSQLDriver) { + return self::PGSQL; + } + + if ($driver instanceof SqlSrvDriver) { + return self::SQLSRV; + } + + if ($driver instanceof Oci8Driver) { + return self::OCI8; + } + + if ($driver instanceof IbmDb2Driver) { + return self::IBM_DB2; + } + + // fallback to connection-based detection when driver is wrapped by middleware + + if (!method_exists($connection, 'getNativeConnection')) { + return null; // dbal < 3.3 (released in 2022-01) + } + + try { + $nativeConnection = $connection->getNativeConnection(); + } catch (Throwable $e) { + if ($this->failOnInvalidConnection) { + throw $e; + } + return null; // connection cannot be established + } + + if ($nativeConnection instanceof mysqli) { + return self::MYSQLI; + } + + if ($nativeConnection instanceof SQLite3) { + return self::SQLITE3; + } + + if ($nativeConnection instanceof \PgSql\Connection) { + return self::PGSQL; + } + + if ($nativeConnection instanceof PDO) { + $driverName = $nativeConnection->getAttribute(PDO::ATTR_DRIVER_NAME); + + if ($driverName === 'mysql') { + return self::PDO_MYSQL; + } + + if ($driverName === 'sqlite') { + return self::PDO_SQLITE; + } + + if ($driverName === 'pgsql') { + return self::PDO_PGSQL; + } + + if ($driverName === 'oci') { // semi-verified (https://stackoverflow.com/questions/10090709/get-current-pdo-driver-from-existing-connection/10090754#comment12923198_10090754) + return self::PDO_OCI; + } + + if ($driverName === 'sqlsrv') { + return self::PDO_SQLSRV; + } + } + + if (is_resource($nativeConnection)) { + $resourceType = get_resource_type($nativeConnection); + + if (strpos($resourceType, 'oci') !== false) { // not verified + return self::OCI8; + } + + if (strpos($resourceType, 'db2') !== false) { // not verified + return self::IBM_DB2; + } + + if (strpos($resourceType, 'SQL Server Connection') !== false) { + return self::SQLSRV; + } + + if (strpos($resourceType, 'pgsql link') !== false) { + return self::PGSQL; + } + } + + return null; + } + +} diff --git a/src/Type/Doctrine/Descriptors/BooleanType.php b/src/Type/Doctrine/Descriptors/BooleanType.php index 955883a8..b9e59574 100644 --- a/src/Type/Doctrine/Descriptors/BooleanType.php +++ b/src/Type/Doctrine/Descriptors/BooleanType.php @@ -2,13 +2,24 @@ namespace PHPStan\Type\Doctrine\Descriptors; +use Doctrine\DBAL\Connection; +use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function in_array; -class BooleanType implements DoctrineTypeDescriptor +class BooleanType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { + /** @var DriverDetector */ + private $driverDetector; + + public function __construct(DriverDetector $driverDetector) + { + $this->driverDetector = $driverDetector; + } + public function getType(): string { return \Doctrine\DBAL\Types\BooleanType::class; @@ -33,4 +44,28 @@ public function getDatabaseInternalType(): Type ); } + public function getDatabaseInternalTypeForDriver(Connection $connection): Type + { + $driverType = $this->driverDetector->detect($connection); + + if ($driverType === DriverDetector::PGSQL || $driverType === DriverDetector::PDO_PGSQL) { + return new \PHPStan\Type\BooleanType(); + } + + if (in_array($driverType, [ + DriverDetector::SQLITE3, + DriverDetector::PDO_SQLITE, + DriverDetector::MYSQLI, + DriverDetector::PDO_MYSQL, + ], true)) { + return TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantIntegerType(1) + ); + } + + // not yet supported driver, return the old implementation guess + return $this->getDatabaseInternalType(); + } + } diff --git a/src/Type/Doctrine/Descriptors/DecimalType.php b/src/Type/Doctrine/Descriptors/DecimalType.php index b008ffe5..64184c45 100644 --- a/src/Type/Doctrine/Descriptors/DecimalType.php +++ b/src/Type/Doctrine/Descriptors/DecimalType.php @@ -2,16 +2,28 @@ namespace PHPStan\Type\Doctrine\Descriptors; +use Doctrine\DBAL\Connection; +use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function in_array; -class DecimalType implements DoctrineTypeDescriptor +class DecimalType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { + /** @var DriverDetector */ + private $driverDetector; + + public function __construct(DriverDetector $driverDetector) + { + $this->driverDetector = $driverDetector; + } + public function getType(): string { return \Doctrine\DBAL\Types\DecimalType::class; @@ -32,4 +44,28 @@ public function getDatabaseInternalType(): Type return TypeCombinator::union(new FloatType(), new IntegerType()); } + public function getDatabaseInternalTypeForDriver(Connection $connection): Type + { + $driverType = $this->driverDetector->detect($connection); + + if ($driverType === DriverDetector::SQLITE3 || $driverType === DriverDetector::PDO_SQLITE) { + return TypeCombinator::union(new FloatType(), new IntegerType()); + } + + if (in_array($driverType, [ + DriverDetector::MYSQLI, + DriverDetector::PDO_MYSQL, + DriverDetector::PGSQL, + DriverDetector::PDO_PGSQL, + ], true)) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + + // not yet supported driver, return the old implementation guess + return $this->getDatabaseInternalType(); + } + } diff --git a/src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php b/src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php index 75f56c9b..c0bffd92 100644 --- a/src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php +++ b/src/Type/Doctrine/Descriptors/DoctrineTypeDescriptor.php @@ -13,10 +13,23 @@ interface DoctrineTypeDescriptor */ public function getType(): string; + /** + * This is used for inferring direct column results, e.g. SELECT e.field + * It should comply with convertToPHPValue return value + */ public function getWritableToPropertyType(): Type; public function getWritableToDatabaseType(): Type; + /** + * This is used for inferring how database fetches column of such type + * + * This is not used for direct column type inferring, + * but when such column appears in expression like SELECT MAX(e.field) + * + * Sometimes, the type cannot be reliably decided without driver context, + * use DoctrineTypeDriverAwareDescriptor in such cases + */ public function getDatabaseInternalType(): Type; } diff --git a/src/Type/Doctrine/Descriptors/DoctrineTypeDriverAwareDescriptor.php b/src/Type/Doctrine/Descriptors/DoctrineTypeDriverAwareDescriptor.php new file mode 100644 index 00000000..765c8f2e --- /dev/null +++ b/src/Type/Doctrine/Descriptors/DoctrineTypeDriverAwareDescriptor.php @@ -0,0 +1,29 @@ +driverDetector = $driverDetector; + } + public function getType(): string { return \Doctrine\DBAL\Types\FloatType::class; @@ -29,4 +43,29 @@ public function getDatabaseInternalType(): Type return TypeCombinator::union(new \PHPStan\Type\FloatType(), new IntegerType()); } + public function getDatabaseInternalTypeForDriver(Connection $connection): Type + { + $driverType = $this->driverDetector->detect($connection); + + if ($driverType === DriverDetector::PDO_PGSQL) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + + if (in_array($driverType, [ + DriverDetector::SQLITE3, + DriverDetector::PDO_SQLITE, + DriverDetector::MYSQLI, + DriverDetector::PDO_MYSQL, + DriverDetector::PGSQL, + ], true)) { + return new \PHPStan\Type\FloatType(); + } + + // not yet supported driver, return the old implementation guess + return $this->getDatabaseInternalType(); + } + } diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 57561ef4..76183c49 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -7,6 +7,7 @@ use Composer\InstalledVersions; use Doctrine\DBAL\Types\Type; use Iterator; +use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\DefaultDescriptorRegistry; @@ -70,7 +71,7 @@ protected function getRule(): Rule new DateTimeImmutableType(), new DateTimeType(), new DateType(), - new DecimalType(), + new DecimalType(new DriverDetector(true)), new JsonType(), new IntegerType(), new StringType(), From 012cf14bee6b5a74fa2d4a83ab2ddfe90f22803f Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 13:38:40 +0200 Subject: [PATCH 039/160] ReflectionDescriptor: deduce database internal type based on parent --- README.md | 4 +++ .../Doctrine/DefaultDescriptorRegistry.php | 11 ++++++ .../Descriptors/ReflectionDescriptor.php | 36 +++++++++++++++++-- .../Doctrine/ORM/EntityColumnRuleTest.php | 8 ++--- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a93658c5..9ff192b1 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,10 @@ Type descriptors don't have to deal with nullable types, as these are transparen If your custom type's `convertToPHPValue()` and `convertToDatabaseValue()` methods have proper typehints, you don't have to write your own descriptor for it. The `PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor` can analyse the typehints and do the rest for you. +If parent of your type is one of the Doctrine's non-abstract ones, `ReflectionDescriptor` will reuse its descriptor even for expression resolution (e.g. `AVG(t.cost)`). +For example, if you extend `Doctrine\DBAL\Types\DecimalType`, it will know that sqlite fetches that as `float|int` and other drivers as `numeric-string`. +If you extend only `Doctrine\DBAL\Types\Type`, you should use custom descriptor and optionally implement even `DoctrineTypeDriverAwareDescriptor` to provide driver-specific resolution. + ### Registering type descriptors When you write a custom type descriptor, you have to let PHPStan know about it. Add something like this into your `phpstan.neon`: diff --git a/src/Type/Doctrine/DefaultDescriptorRegistry.php b/src/Type/Doctrine/DefaultDescriptorRegistry.php index 48886caa..2fc81131 100644 --- a/src/Type/Doctrine/DefaultDescriptorRegistry.php +++ b/src/Type/Doctrine/DefaultDescriptorRegistry.php @@ -36,4 +36,15 @@ public function get(string $type): DoctrineTypeDescriptor return $this->descriptors[$typeClass]; } + /** + * @throws DescriptorNotRegisteredException + */ + public function getByClassName(string $className): DoctrineTypeDescriptor + { + if (!isset($this->descriptors[$className])) { + throw new DescriptorNotRegisteredException(); + } + return $this->descriptors[$className]; + } + } diff --git a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php index 283d9506..82c44482 100644 --- a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php +++ b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php @@ -3,8 +3,12 @@ namespace PHPStan\Type\Doctrine\Descriptors; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type as DbalType; +use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\Doctrine\DefaultDescriptorRegistry; +use PHPStan\Type\Doctrine\DescriptorNotRegisteredException; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -13,19 +17,27 @@ class ReflectionDescriptor implements DoctrineTypeDescriptor { - /** @var class-string<\Doctrine\DBAL\Types\Type> */ + /** @var class-string */ private $type; /** @var ReflectionProvider */ private $reflectionProvider; + /** @var Container */ + private $container; + /** - * @param class-string<\Doctrine\DBAL\Types\Type> $type + * @param class-string $type */ - public function __construct(string $type, ReflectionProvider $reflectionProvider) + public function __construct( + string $type, + ReflectionProvider $reflectionProvider, + Container $container + ) { $this->type = $type; $this->reflectionProvider = $reflectionProvider; + $this->container = $container; } public function getType(): string @@ -57,6 +69,24 @@ public function getWritableToDatabaseType(): Type public function getDatabaseInternalType(): Type { + if (!$this->reflectionProvider->hasClass($this->type)) { + return new MixedType(); + } + + $registry = $this->container->getByType(DefaultDescriptorRegistry::class); + $parents = $this->reflectionProvider->getClass($this->type)->getParentClassesNames(); + + foreach ($parents as $dbalTypeParentClass) { + try { + // this assumes that if somebody inherits from DecimalType, + // the real database type remains decimal and we can reuse its descriptor + return $registry->getByClassName($dbalTypeParentClass)->getDatabaseInternalType(); + + } catch (DescriptorNotRegisteredException $e) { + continue; + } + } + return new MixedType(); } diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 76183c49..ffe5c299 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -77,10 +77,10 @@ protected function getRule(): Rule new StringType(), new SimpleArrayType(), new UuidTypeDescriptor(FakeTestingUuidType::class), - new ReflectionDescriptor(CarbonImmutableType::class, $this->createBroker()), - new ReflectionDescriptor(CarbonType::class, $this->createBroker()), - new ReflectionDescriptor(CustomType::class, $this->createBroker()), - new ReflectionDescriptor(CustomNumericType::class, $this->createBroker()), + new ReflectionDescriptor(CarbonImmutableType::class, $this->createBroker(), self::getContainer()), + new ReflectionDescriptor(CarbonType::class, $this->createBroker(), self::getContainer()), + new ReflectionDescriptor(CustomType::class, $this->createBroker(), self::getContainer()), + new ReflectionDescriptor(CustomNumericType::class, $this->createBroker(), self::getContainer()), ]), $this->createReflectionProvider(), true, From 40ae31561a6e5ce8269db5d1f153d22a2b396ed8 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 12:52:00 +0200 Subject: [PATCH 040/160] Fix unary minus test --- tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 117d4200..b64099ff 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -15,6 +15,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; @@ -1604,13 +1605,13 @@ public function getTestData(): iterable yield 'unary minus' => [ $this->constantArray([ - [new ConstantStringType('minusInt'), $this->numericStringOrInt()], // should be nullable - [new ConstantStringType('minusFloat'), TypeCombinator::union(new FloatType(), $this->numericStringOrInt())], // should be nullable && should not include int + [new ConstantStringType('minusInt'), TypeCombinator::union(new ConstantIntegerType(-1), new ConstantStringType('-1'))], // should be nullable + [new ConstantStringType('minusFloat'), TypeCombinator::union(new ConstantFloatType(-0.1), new ConstantStringType('-0.1'))], // should be nullable [new ConstantStringType('minusIntRange'), TypeCombinator::union(IntegerRangeType::fromInterval(null, 0), $this->numericString())], ]), ' - SELECT -o.intColumn as minusInt, - -o.floatColumn as minusFloat, + SELECT -1 as minusInt, + -0.1 as minusFloat, -COUNT(o.intColumn) as minusIntRange FROM QueryResult\Entities\One o ', From 3369068d994f462354dd021adbcf5b0c4008f3ae Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 10:06:30 +0200 Subject: [PATCH 041/160] Proper aggregate function detection --- ...eryAggregateFunctionDetectorTreeWalker.php | 321 ++++++++++++++++++ .../Doctrine/Query/QueryResultTypeWalker.php | 30 +- .../Query/QueryResultTypeWalkerTest.php | 18 + 3 files changed, 342 insertions(+), 27 deletions(-) create mode 100644 src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php diff --git a/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php b/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php new file mode 100644 index 00000000..11af2086 --- /dev/null +++ b/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php @@ -0,0 +1,321 @@ +doWalkSelectClause($selectStatement->selectClause); + } + + /** + * @param AST\SelectClause $selectClause + */ + public function doWalkSelectClause($selectClause): void + { + foreach ($selectClause->selectExpressions as $selectExpression) { + $this->doWalkSelectExpression($selectExpression); + } + } + + /** + * @param AST\SelectExpression $selectExpression + */ + public function doWalkSelectExpression($selectExpression): void + { + $this->doWalkNode($selectExpression->expression); + } + + /** + * @param mixed $expr + */ + private function doWalkNode($expr): void + { + if ($expr instanceof AST\AggregateExpression) { + $this->markAggregateFunctionFound(); + + } elseif ($expr instanceof AST\Functions\FunctionNode) { + if ($this->isAggregateFunction($expr)) { + $this->markAggregateFunctionFound(); + } + + } elseif ($expr instanceof AST\SimpleArithmeticExpression) { + foreach ($expr->arithmeticTerms as $term) { + $this->doWalkArithmeticTerm($term); + } + + } elseif ($expr instanceof AST\ArithmeticTerm) { + $this->doWalkArithmeticTerm($expr); + + } elseif ($expr instanceof AST\ArithmeticFactor) { + $this->doWalkArithmeticFactor($expr); + + } elseif ($expr instanceof AST\ParenthesisExpression) { + $this->doWalkArithmeticPrimary($expr->expression); + + } elseif ($expr instanceof AST\NullIfExpression) { + $this->doWalkNullIfExpression($expr); + + } elseif ($expr instanceof AST\CoalesceExpression) { + $this->doWalkCoalesceExpression($expr); + + } elseif ($expr instanceof AST\GeneralCaseExpression) { + $this->doWalkGeneralCaseExpression($expr); + + } elseif ($expr instanceof AST\SimpleCaseExpression) { + $this->doWalkSimpleCaseExpression($expr); + + } elseif ($expr instanceof AST\ArithmeticExpression) { + $this->doWalkArithmeticExpression($expr); + + } elseif ($expr instanceof AST\ComparisonExpression) { + $this->doWalkComparisonExpression($expr); + + } elseif ($expr instanceof AST\BetweenExpression) { + $this->doWalkBetweenExpression($expr); + } + } + + public function doWalkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): void + { + foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { + $this->doWalkSimpleArithmeticExpression($scalarExpression); + } + } + + public function doWalkNullIfExpression(AST\NullIfExpression $nullIfExpression): void + { + if (!is_string($nullIfExpression->firstExpression)) { + $this->doWalkSimpleArithmeticExpression($nullIfExpression->firstExpression); + } + + if (is_string($nullIfExpression->secondExpression)) { + return; + } + + $this->doWalkSimpleArithmeticExpression($nullIfExpression->secondExpression); + } + + public function doWalkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): void + { + foreach ($generalCaseExpression->whenClauses as $whenClause) { + $this->doWalkConditionalExpression($whenClause->caseConditionExpression); + $this->doWalkSimpleArithmeticExpression($whenClause->thenScalarExpression); + } + + $this->doWalkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression); + } + + public function doWalkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): void + { + foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) { + $this->doWalkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression); + $this->doWalkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression); + } + + $this->doWalkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression); + } + + /** + * @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr + */ + public function doWalkConditionalExpression($condExpr): void + { + if (!$condExpr instanceof AST\ConditionalExpression) { + $this->doWalkConditionalTerm($condExpr); // @phpstan-ignore-line PHPStan do not read @psalm-inheritors of Phase2OptimizableConditional + return; + } + + foreach ($condExpr->conditionalTerms as $conditionalTerm) { + $this->doWalkConditionalTerm($conditionalTerm); + } + } + + /** + * @param AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm + */ + public function doWalkConditionalTerm($condTerm): void + { + if (!$condTerm instanceof AST\ConditionalTerm) { + $this->doWalkConditionalFactor($condTerm); + return; + } + + foreach ($condTerm->conditionalFactors as $conditionalFactor) { + $this->doWalkConditionalFactor($conditionalFactor); + } + } + + /** + * @param AST\ConditionalFactor|AST\ConditionalPrimary $factor + */ + public function doWalkConditionalFactor($factor): void + { + if (!$factor instanceof AST\ConditionalFactor) { + $this->doWalkConditionalPrimary($factor); + } else { + $this->doWalkConditionalPrimary($factor->conditionalPrimary); + } + } + + /** + * @param AST\ConditionalPrimary $primary + */ + public function doWalkConditionalPrimary($primary): void + { + if ($primary->isSimpleConditionalExpression()) { + if ($primary->simpleConditionalExpression instanceof AST\ComparisonExpression) { + $this->doWalkComparisonExpression($primary->simpleConditionalExpression); + return; + } + $this->doWalkNode($primary->simpleConditionalExpression); + } + + if (!$primary->isConditionalExpression()) { + return; + } + + if ($primary->conditionalExpression === null) { + return; + } + + $this->doWalkConditionalExpression($primary->conditionalExpression); + } + + /** + * @param AST\BetweenExpression $betweenExpr + */ + public function doWalkBetweenExpression($betweenExpr): void + { + $this->doWalkArithmeticExpression($betweenExpr->expression); + $this->doWalkArithmeticExpression($betweenExpr->leftBetweenExpression); + $this->doWalkArithmeticExpression($betweenExpr->rightBetweenExpression); + } + + /** + * @param AST\ComparisonExpression $compExpr + */ + public function doWalkComparisonExpression($compExpr): void + { + $leftExpr = $compExpr->leftExpression; + $rightExpr = $compExpr->rightExpression; + + if ($leftExpr instanceof AST\Node) { + $this->doWalkNode($leftExpr); + } + + if (!($rightExpr instanceof AST\Node)) { + return; + } + + $this->doWalkNode($rightExpr); + } + + /** + * @param AST\ArithmeticExpression $arithmeticExpr + */ + public function doWalkArithmeticExpression($arithmeticExpr): void + { + if (!$arithmeticExpr->isSimpleArithmeticExpression()) { + return; + } + + if ($arithmeticExpr->simpleArithmeticExpression === null) { + return; + } + + $this->doWalkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression); + } + + /** + * @param AST\Node|string $simpleArithmeticExpr + */ + public function doWalkSimpleArithmeticExpression($simpleArithmeticExpr): void + { + if (!$simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression) { + $this->doWalkArithmeticTerm($simpleArithmeticExpr); + return; + } + + foreach ($simpleArithmeticExpr->arithmeticTerms as $term) { + $this->doWalkArithmeticTerm($term); + } + } + + /** + * @param AST\Node|string $term + */ + public function doWalkArithmeticTerm($term): void + { + if (is_string($term)) { + return; + } + + if (!$term instanceof AST\ArithmeticTerm) { + $this->doWalkArithmeticFactor($term); + return; + } + + foreach ($term->arithmeticFactors as $factor) { + $this->doWalkArithmeticFactor($factor); + } + } + + /** + * @param AST\Node|string $factor + */ + public function doWalkArithmeticFactor($factor): void + { + if (is_string($factor)) { + return; + } + + if (!$factor instanceof AST\ArithmeticFactor) { + $this->doWalkArithmeticPrimary($factor); + return; + } + + $this->doWalkArithmeticPrimary($factor->arithmeticPrimary); + } + + /** + * @param AST\Node|string $primary + */ + public function doWalkArithmeticPrimary($primary): void + { + if ($primary instanceof AST\SimpleArithmeticExpression) { + $this->doWalkSimpleArithmeticExpression($primary); + return; + } + + if (!($primary instanceof AST\Node)) { + return; + } + + $this->doWalkNode($primary); + } + + private function isAggregateFunction(AST\Node $node): bool + { + return $node instanceof AST\Functions\AvgFunction + || $node instanceof AST\Functions\CountFunction + || $node instanceof AST\Functions\MaxFunction + || $node instanceof AST\Functions\MinFunction + || $node instanceof AST\Functions\SumFunction + || $node instanceof AST\AggregateExpression; + } + + private function markAggregateFunctionFound(): void + { + $this->_getQuery()->setHint(self::HINT_HAS_AGGREGATE_FUNCTION, true); + } + +} diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 5e4bee91..4fb56be3 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -117,6 +117,7 @@ class QueryResultTypeWalker extends SqlWalker public static function walk(Query $query, QueryResultTypeBuilder $typeBuilder, DescriptorRegistry $descriptorRegistry): void { $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::class); + $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [QueryAggregateFunctionDetectorTreeWalker::class]); $query->setHint(self::HINT_TYPE_MAPPING, $typeBuilder); $query->setHint(self::HINT_DESCRIPTOR_REGISTRY, $descriptorRegistry); @@ -137,7 +138,8 @@ public function __construct($query, $parserResult, array $queryComponents) $this->em = $query->getEntityManager(); $this->queryComponents = $queryComponents; $this->nullableQueryComponents = []; - $this->hasAggregateFunction = false; + $this->hasAggregateFunction = $query->hasHint(QueryAggregateFunctionDetectorTreeWalker::HINT_HAS_AGGREGATE_FUNCTION); + $this->hasGroupByClause = false; // The object is instantiated by Doctrine\ORM\Query\Parser, so receiving @@ -176,7 +178,6 @@ public function __construct($query, $parserResult, array $queryComponents) public function walkSelectStatement(AST\SelectStatement $AST): string { $this->typeBuilder->setSelectQuery(); - $this->hasAggregateFunction = $this->hasAggregateFunction($AST); $this->hasGroupByClause = $AST->groupByClause !== null; $this->walkFromClause($AST->fromClause); @@ -1432,29 +1433,4 @@ private function hasAggregateWithoutGroupBy(): bool return $this->hasAggregateFunction && !$this->hasGroupByClause; } - private function hasAggregateFunction(AST\SelectStatement $AST): bool - { - foreach ($AST->selectClause->selectExpressions as $selectExpression) { - if (!$selectExpression instanceof AST\SelectExpression) { - continue; - } - - $expression = $selectExpression->expression; - - switch (true) { - case $expression instanceof AST\Functions\AvgFunction: - case $expression instanceof AST\Functions\CountFunction: - case $expression instanceof AST\Functions\MaxFunction: - case $expression instanceof AST\Functions\MinFunction: - case $expression instanceof AST\Functions\SumFunction: - case $expression instanceof AST\AggregateExpression: - return true; - default: - break; - } - } - - return false; - } - } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index b64099ff..a1ddb649 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -645,6 +645,24 @@ public function getTestData(): iterable ', ]; + yield 'aggregate deeper in AST' => [ + $this->constantArray([ + [ + new ConstantStringType('many'), + TypeCombinator::addNull(new ObjectType(Many::class)), + ], + [ + new ConstantStringType('max'), + $this->intStringified(), + ], + ]), + ' + SELECT m AS many, + COALESCE(MAX(m.intColumn), 0) as max + FROM QueryResult\Entities\Many m + ', + ]; + yield 'aggregate lowercase' => [ $this->constantArray([ [ From 3d10eab6887150588ad4a702df824bbe89339dbe Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 11:53:14 +0200 Subject: [PATCH 042/160] Fix wrong coalesce type inference --- .../Doctrine/Query/QueryResultTypeWalker.php | 84 ++++++++++++++++++- .../Query/QueryResultTypeWalkerTest.php | 31 +++---- 2 files changed, 99 insertions(+), 16 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 4fb56be3..6ca0c2e1 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -14,6 +14,7 @@ use Doctrine\ORM\Query\ParserResult; use Doctrine\ORM\Query\SqlWalker; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -575,6 +576,79 @@ public function walkFunction($function): string } } + private function createFloat(bool $nullable): Type + { + $float = new FloatType(); + return $nullable ? TypeCombinator::addNull($float) : $float; + } + + private function createFloatOrInt(bool $nullable): Type // @phpstan-ignore-line unused, but kept to ease conflict resolution with origin (#506) + { + $union = TypeCombinator::union( + new FloatType(), + new IntegerType() + ); + return $nullable ? TypeCombinator::addNull($union) : $union; + } + + private function createInteger(bool $nullable): Type + { + $integer = new IntegerType(); + return $nullable ? TypeCombinator::addNull($integer) : $integer; + } + + private function createNonNegativeInteger(bool $nullable): Type // @phpstan-ignore-line unused, but kept to ease conflict resolution with origin (#506) + { + $integer = IntegerRangeType::fromInterval(0, null); + return $nullable ? TypeCombinator::addNull($integer) : $integer; + } + + private function createNumericString(bool $nullable): Type + { + $numericString = TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType() + ); + + return $nullable ? TypeCombinator::addNull($numericString) : $numericString; + } + + private function createString(bool $nullable): Type + { + $string = new StringType(); + return $nullable ? TypeCombinator::addNull($string) : $string; + } + + /** + * E.g. to ensure SUM(1) is inferred as int, not 1 + */ + private function generalizeConstantType(Type $type, bool $makeNullable): Type + { + $containsNull = $this->canBeNull($type); + $typeNoNull = TypeCombinator::removeNull($type); + + if (!$typeNoNull->isConstantScalarValue()->yes()) { + $result = $type; + + } elseif ($typeNoNull->isInteger()->yes()) { + $result = $this->createInteger($containsNull); + + } elseif ($typeNoNull->isFloat()->yes()) { + $result = $this->createFloat($containsNull); + + } elseif ($typeNoNull->isNumericString()->yes()) { + $result = $this->createNumericString($containsNull); + + } elseif ($typeNoNull->isString()->yes()) { + $result = $this->createString($containsNull); + + } else { + $result = $type; + } + + return $makeNullable ? TypeCombinator::addNull($result) : $result; + } + /** * @param AST\OrderByClause $orderByClause */ @@ -642,7 +716,10 @@ public function walkCoalesceExpression($coalesceExpression): string $type = $this->unmarshalType($expression->dispatch($this)); $allTypesContainNull = $allTypesContainNull && TypeCombinator::containsNull($type); - $expressionTypes[] = $type; + // Some drivers manipulate the types, lets avoid false positives by generalizing constant types + // e.g. sqlsrv: "COALESCE returns the data type of value with the highest precedence" + // e.g. mysql: COALESCE(1, 'foo') === '1' (undocumented? https://gist.github.com/jrunning/4535434) + $expressionTypes[] = $this->generalizeConstantType($type, false); } $type = TypeCombinator::union(...$expressionTypes); @@ -1400,6 +1477,11 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType return $type; } + private function canBeNull(Type $type): bool + { + return !$type->isSuperTypeOf(new NullType())->no(); + } + private function toNumericOrNull(Type $type): Type { return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index a1ddb649..d0fc0306 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -548,26 +548,18 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( - new ConstantStringType('a'), - new ConstantStringType('b') - ), + new StringType(), ], [ new ConstantIntegerType(2), TypeCombinator::union( - new ConstantIntegerType(1), - new ConstantIntegerType(2), - new ConstantStringType('1'), - new ConstantStringType('2') + $this->numericString(), + new IntegerType() ), ], [ new ConstantIntegerType(3), - TypeCombinator::union( - new ConstantStringType('1'), - new ConstantStringType('2') - ), + $this->numericString(), ], ]), ' @@ -842,11 +834,20 @@ public function getTestData(): iterable new ConstantIntegerType(3), $this->intStringified(), ], + [ + new ConstantIntegerType(4), + TypeCombinator::union( + new IntegerType(), + new FloatType(), + $this->numericString() + ), + ], ]), ' SELECT COALESCE(m.stringNullColumn, m.intColumn, false), COALESCE(m.stringNullColumn, m.stringNullColumn), - COALESCE(NULLIF(m.intColumn, 1), 0) + COALESCE(NULLIF(m.intColumn, 1), 0), + COALESCE(1, 1.1) FROM QueryResult\Entities\Many m ', ]; @@ -1142,8 +1143,8 @@ public function getTestData(): iterable [ new ConstantIntegerType(2), TypeCombinator::union( - new ConstantIntegerType(1), - new ConstantStringType('1') + new IntegerType(), + $this->numericString() ), ], [ From ad9138826ed274f6ce9bb7e6b35774738fc2d6bc Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 25 Jun 2024 12:42:10 +0200 Subject: [PATCH 043/160] QueryResultTypeWalker: fix nullability checks over unknown type --- .../Doctrine/Query/QueryResultTypeWalker.php | 24 +++++++++---------- .../Query/QueryResultTypeWalkerTest.php | 16 +++++++++++++ .../data/QueryResult/CustomIntType.php | 17 +++++++++++++ .../data/QueryResult/Entities/One.php | 7 ++++++ 4 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 tests/Type/Doctrine/data/QueryResult/CustomIntType.php diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 6ca0c2e1..5a2b33b8 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -376,7 +376,7 @@ public function walkFunction($function): string new FloatType() ); - if (TypeCombinator::containsNull($exprType)) { + if ($this->canBeNull($exprType)) { $type = TypeCombinator::addNull($type); } @@ -388,7 +388,7 @@ public function walkFunction($function): string $secondExprType = $this->unmarshalType($function->secondArithmetic->dispatch($this)); $type = IntegerRangeType::fromInterval(0, null); - if (TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) { + if ($this->canBeNull($firstExprType) || $this->canBeNull($secondExprType)) { $type = TypeCombinator::addNull($type); } @@ -399,7 +399,7 @@ public function walkFunction($function): string foreach ($function->concatExpressions as $expr) { $type = $this->unmarshalType($expr->dispatch($this)); - $hasNull = $hasNull || TypeCombinator::containsNull($type); + $hasNull = $hasNull || $this->canBeNull($type); } $type = new StringType(); @@ -420,7 +420,7 @@ public function walkFunction($function): string $intervalExprType = $this->unmarshalType($function->intervalExpression->dispatch($this)); $type = new StringType(); - if (TypeCombinator::containsNull($dateExprType) || TypeCombinator::containsNull($intervalExprType)) { + if ($this->canBeNull($dateExprType) || $this->canBeNull($intervalExprType)) { $type = TypeCombinator::addNull($type); } @@ -434,7 +434,7 @@ public function walkFunction($function): string new IntegerType(), new FloatType() ); - if (TypeCombinator::containsNull($date1ExprType) || TypeCombinator::containsNull($date2ExprType)) { + if ($this->canBeNull($date1ExprType) || $this->canBeNull($date2ExprType)) { $type = TypeCombinator::addNull($type); } @@ -444,7 +444,7 @@ public function walkFunction($function): string $stringPrimaryType = $this->unmarshalType($function->stringPrimary->dispatch($this)); $type = IntegerRangeType::fromInterval(0, null); - if (TypeCombinator::containsNull($stringPrimaryType)) { + if ($this->canBeNull($stringPrimaryType)) { $type = TypeCombinator::addNull($type); } @@ -455,7 +455,7 @@ public function walkFunction($function): string $secondExprType = $this->unmarshalType($this->walkStringPrimary($function->secondStringPrimary)); $type = IntegerRangeType::fromInterval(0, null); - if (TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) { + if ($this->canBeNull($firstExprType) || $this->canBeNull($secondExprType)) { $type = TypeCombinator::addNull($type); } @@ -467,7 +467,7 @@ public function walkFunction($function): string $stringPrimaryType = $this->unmarshalType($function->stringPrimary->dispatch($this)); $type = new StringType(); - if (TypeCombinator::containsNull($stringPrimaryType)) { + if ($this->canBeNull($stringPrimaryType)) { $type = TypeCombinator::addNull($type); } @@ -478,7 +478,7 @@ public function walkFunction($function): string $secondExprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->secondSimpleArithmeticExpression)); $type = IntegerRangeType::fromInterval(0, null); - if (TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) { + if ($this->canBeNull($firstExprType) || $this->canBeNull($secondExprType)) { $type = TypeCombinator::addNull($type); } @@ -493,7 +493,7 @@ public function walkFunction($function): string $exprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->simpleArithmeticExpression)); $type = new FloatType(); - if (TypeCombinator::containsNull($exprType)) { + if ($this->canBeNull($exprType)) { $type = TypeCombinator::addNull($type); } @@ -510,7 +510,7 @@ public function walkFunction($function): string } $type = new StringType(); - if (TypeCombinator::containsNull($stringType) || TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) { + if ($this->canBeNull($stringType) || $this->canBeNull($firstExprType) || $this->canBeNull($secondExprType)) { $type = TypeCombinator::addNull($type); } @@ -714,7 +714,7 @@ public function walkCoalesceExpression($coalesceExpression): string } $type = $this->unmarshalType($expression->dispatch($this)); - $allTypesContainNull = $allTypesContainNull && TypeCombinator::containsNull($type); + $allTypesContainNull = $allTypesContainNull && $this->canBeNull($type); // Some drivers manipulate the types, lets avoid false positives by generalizing constant types // e.g. sqlsrv: "COALESCE returns the data type of value with the highest precedence" diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index d0fc0306..76611781 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -7,6 +7,7 @@ use DateTime; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Tools\SchemaTool; @@ -44,6 +45,7 @@ use QueryResult\EntitiesEnum\IntEnum; use QueryResult\EntitiesEnum\StringEnum; use Throwable; +use Type\Doctrine\data\QueryResult\CustomIntType; use function array_merge; use function array_shift; use function assert; @@ -76,6 +78,10 @@ public static function setUpBeforeClass(): void $em = require __DIR__ . '/../data/QueryResult/entity-manager.php'; self::$em = $em; + if (!DbalType::hasType(CustomIntType::NAME)) { + DbalType::addType(CustomIntType::NAME, CustomIntType::class); + } + $schemaTool = new SchemaTool($em); $classes = $em->getMetadataFactory()->getAllMetadata(); $schemaTool->createSchema($classes); @@ -1241,6 +1247,16 @@ public function getTestData(): iterable ', ]; + yield 'abs function with mixed' => [ + $this->constantArray([ + [new ConstantIntegerType(1), TypeCombinator::addNull($this->unumericStringified())], + ]), + ' + SELECT ABS(o.mixedColumn) + FROM QueryResult\Entities\One o + ', + ]; + yield 'bit_and function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->uintStringified()], diff --git a/tests/Type/Doctrine/data/QueryResult/CustomIntType.php b/tests/Type/Doctrine/data/QueryResult/CustomIntType.php new file mode 100644 index 00000000..785bed33 --- /dev/null +++ b/tests/Type/Doctrine/data/QueryResult/CustomIntType.php @@ -0,0 +1,17 @@ +subOne = new SubOne(); From a565fdb23a446b6fdc28fc881e3bdf7cdce09d63 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 26 Jun 2024 10:52:16 +0200 Subject: [PATCH 044/160] Autodetect driver setup for precise int/float/bool inference in expressions (stringified or not) --- README.md | 8 + phpstan.neon | 18 +- src/Doctrine/Driver/DriverDetector.php | 5 + .../CreateQueryDynamicReturnTypeExtension.php | 19 +- src/Type/Doctrine/Descriptors/FloatType.php | 8 +- .../Descriptors/ReflectionDescriptor.php | 19 +- .../Doctrine/Query/DqlConstantStringType.php | 31 + .../Doctrine/Query/QueryResultTypeWalker.php | 835 ++- ...lderGetQueryDynamicReturnTypeExtension.php | 16 +- tests/Platform/Entity/PlatformEntity.php | 66 + .../Platform/Entity/PlatformRelatedEntity.php | 25 + tests/Platform/MixedCustomType.php | 20 + ...eryResultTypeWalkerFetchTypeMatrixTest.php | 5129 ++++++++++++++++- tests/Platform/README.md | 8 +- .../TypedExpressionBooleanPiFunction.php | 33 + .../TypedExpressionIntegerPiFunction.php | 33 + .../TypedExpressionStringPiFunction.php | 33 + tests/Platform/docker/Dockerfile80 | 12 + tests/Platform/docker/Dockerfile81 | 12 + tests/Platform/docker/docker-compose.yml | 25 +- tests/Platform/docker/docker-setup.sh | 1 + .../Query/QueryResultTypeWalkerTest.php | 302 +- 22 files changed, 6061 insertions(+), 597 deletions(-) create mode 100644 src/Type/Doctrine/Query/DqlConstantStringType.php create mode 100644 tests/Platform/Entity/PlatformRelatedEntity.php create mode 100644 tests/Platform/MixedCustomType.php create mode 100644 tests/Platform/TypedExpressionBooleanPiFunction.php create mode 100644 tests/Platform/TypedExpressionIntegerPiFunction.php create mode 100644 tests/Platform/TypedExpressionStringPiFunction.php diff --git a/README.md b/README.md index 9ff192b1..e59c66c3 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,14 @@ Queries are analyzed statically and do not require a running database server. Th Most DQL features are supported, including `GROUP BY`, `DISTINCT`, all flavors of `JOIN`, arithmetic expressions, functions, aggregations, `NEW`, etc. Sub queries and `INDEX BY` are not yet supported (infered type will be `mixed`). +### Query type inference of expressions + +Whether e.g. `SUM(e.column)` is fetched as `float`, `numeric-string` or `int` highly [depends on drivers, their setup and PHP version](https://github.com/janedbal/php-database-drivers-fetch-test). +This extension autodetects your setup and provides quite accurate results for `pdo_mysql`, `mysqli`, `pdo_sqlite`, `sqlite3`, `pdo_pgsql` and `pgsql`. +Sadly, this autodetection often needs real database connection, so in order to utilize precise types, your `objectManagerLoader` need to be able to connect to real database. + +If you are using `bleedingEdge`, the connection failure is propagated. If not, it will be silently ignored and the type will be `mixed` or an union of possible types. + ### Supported methods The `getResult` method is supported when called without argument, or with the hydrateMode argument set to `Query::HYDRATE_OBJECT`: diff --git a/phpstan.neon b/phpstan.neon index c467b761..d099cb21 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -42,17 +42,11 @@ parameters: - message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ORM\\\\EntityManager'' and ''create'' will always evaluate to true\.$#' path: src/Doctrine/Mapping/ClassMetadataFactory.php - reportUnmatched: false - - - messages: - - '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' - - '#^Cannot call method getWrappedResourceHandle\(\) on class\-string\|object\.$#' - path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php - reportUnmatched: false - message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' # needed for older DBAL versions paths: + - src/Type/Doctrine/Query/QueryResultTypeWalker.php - src/Doctrine/Driver/DriverDetector.php - @@ -60,3 +54,13 @@ parameters: - '#^Class PgSql\\Connection not found\.$#' - '#^Class Doctrine\\DBAL\\Driver\\PgSQL\\Driver not found\.$#' - '#^Class Doctrine\\DBAL\\Driver\\SQLite3\\Driver not found\.$#' + + - + message: '#^Call to an undefined method Doctrine\\DBAL\\Connection\:\:getWrappedConnection\(\)\.$#' # dropped in DBAL 4 + path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + + - + messages: # oldest dbal has only getSchemaManager, dbal4 has only createSchemaManager + - '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''createSchemaManager'' will always evaluate to true\.$#' + - '#^Call to an undefined method Doctrine\\DBAL\\Connection\:\:getSchemaManager\(\)\.$#' + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php diff --git a/src/Doctrine/Driver/DriverDetector.php b/src/Doctrine/Driver/DriverDetector.php index 0a4371be..674585b6 100644 --- a/src/Doctrine/Driver/DriverDetector.php +++ b/src/Doctrine/Driver/DriverDetector.php @@ -46,6 +46,11 @@ public function __construct(bool $failOnInvalidConnection) $this->failOnInvalidConnection = $failOnInvalidConnection; } + public function failsOnInvalidConnection(): bool + { + return $this->failOnInvalidConnection; + } + /** * @return self::*|null */ diff --git a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php index 4c41613c..1cf5d50a 100644 --- a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php @@ -12,6 +12,8 @@ use Doctrine\Persistence\Mapping\MappingException; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\Doctrine\Driver\DriverDetector; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Doctrine\Query\QueryResultTypeBuilder; @@ -37,10 +39,23 @@ final class CreateQueryDynamicReturnTypeExtension implements DynamicMethodReturn /** @var DescriptorRegistry */ private $descriptorRegistry; - public function __construct(ObjectMetadataResolver $objectMetadataResolver, DescriptorRegistry $descriptorRegistry) + /** @var PhpVersion */ + private $phpVersion; + + /** @var DriverDetector */ + private $driverDetector; + + public function __construct( + ObjectMetadataResolver $objectMetadataResolver, + DescriptorRegistry $descriptorRegistry, + PhpVersion $phpVersion, + DriverDetector $driverDetector + ) { $this->objectMetadataResolver = $objectMetadataResolver; $this->descriptorRegistry = $descriptorRegistry; + $this->phpVersion = $phpVersion; + $this->driverDetector = $driverDetector; } public function getClass(): string @@ -87,7 +102,7 @@ public function getTypeFromMethodCall( try { $query = $em->createQuery($queryString); - QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry); + QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry, $this->phpVersion, $this->driverDetector); } catch (ORMException | DBALException | NewDBALException | CommonException | MappingException | \Doctrine\ORM\Exception\ORMException $e) { return new QueryType($queryString, null, null); } catch (AssertionError $e) { diff --git a/src/Type/Doctrine/Descriptors/FloatType.php b/src/Type/Doctrine/Descriptors/FloatType.php index dea7304b..2518e72d 100644 --- a/src/Type/Doctrine/Descriptors/FloatType.php +++ b/src/Type/Doctrine/Descriptors/FloatType.php @@ -40,7 +40,13 @@ public function getWritableToDatabaseType(): Type public function getDatabaseInternalType(): Type { - return TypeCombinator::union(new \PHPStan\Type\FloatType(), new IntegerType()); + return TypeCombinator::union( + new \PHPStan\Type\FloatType(), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]) + ); } public function getDatabaseInternalTypeForDriver(Connection $connection): Type diff --git a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php index 82c44482..7d7cb778 100644 --- a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php +++ b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Doctrine\Descriptors; +use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type as DbalType; use PHPStan\DependencyInjection\Container; @@ -14,7 +15,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ReflectionDescriptor implements DoctrineTypeDescriptor +class ReflectionDescriptor implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { /** @var class-string */ @@ -68,6 +69,16 @@ public function getWritableToDatabaseType(): Type } public function getDatabaseInternalType(): Type + { + return $this->doGetDatabaseInternalType(null); + } + + public function getDatabaseInternalTypeForDriver(Connection $connection): Type + { + return $this->doGetDatabaseInternalType($connection); + } + + private function doGetDatabaseInternalType(?Connection $connection): Type { if (!$this->reflectionProvider->hasClass($this->type)) { return new MixedType(); @@ -80,7 +91,11 @@ public function getDatabaseInternalType(): Type try { // this assumes that if somebody inherits from DecimalType, // the real database type remains decimal and we can reuse its descriptor - return $registry->getByClassName($dbalTypeParentClass)->getDatabaseInternalType(); + $descriptor = $registry->getByClassName($dbalTypeParentClass); + + return $descriptor instanceof DoctrineTypeDriverAwareDescriptor && $connection !== null + ? $descriptor->getDatabaseInternalTypeForDriver($connection) + : $descriptor->getDatabaseInternalType(); } catch (DescriptorNotRegisteredException $e) { continue; diff --git a/src/Type/Doctrine/Query/DqlConstantStringType.php b/src/Type/Doctrine/Query/DqlConstantStringType.php new file mode 100644 index 00000000..f4cc4821 --- /dev/null +++ b/src/Type/Doctrine/Query/DqlConstantStringType.php @@ -0,0 +1,31 @@ +originLiteralType = $originLiteralType; + } + + /** + * @return Literal::* + */ + public function getOriginLiteralType(): int + { + return $this->originLiteralType; + } + +} diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 5a2b33b8..f2e8c051 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -13,18 +13,22 @@ use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\ParserResult; use Doctrine\ORM\Query\SqlWalker; +use PDO; +use PDOException; +use PHPStan\Doctrine\Driver\DriverDetector; +use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\Doctrine\DescriptorNotRegisteredException; use PHPStan\Type\Doctrine\DescriptorRegistry; +use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDriverAwareDescriptor; use PHPStan\Type\FloatType; -use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -36,22 +40,26 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use Throwable; use function array_key_exists; use function array_map; +use function array_values; use function assert; use function class_exists; use function count; -use function floatval; use function get_class; use function gettype; -use function intval; +use function in_array; +use function is_int; use function is_numeric; use function is_object; use function is_string; +use function method_exists; use function serialize; use function sprintf; +use function stripos; +use function strpos; use function strtolower; use function strtoupper; use function unserialize; @@ -70,6 +78,10 @@ class QueryResultTypeWalker extends SqlWalker private const HINT_DESCRIPTOR_REGISTRY = self::class . '::HINT_DESCRIPTOR_REGISTRY'; + private const HINT_PHP_VERSION = self::class . '::HINT_PHP_VERSION'; + + private const HINT_DRIVER_DETECTOR = self::class . '::HINT_DRIVER_DETECTOR'; + /** * Counter for generating unique scalar result. * @@ -90,6 +102,12 @@ class QueryResultTypeWalker extends SqlWalker /** @var EntityManagerInterface */ private $em; + /** @var PhpVersion */ + private $phpVersion; + + /** @var DriverDetector::*|null */ + private $driverType; + /** * Map of all components/classes that appear in the DQL query. * @@ -112,15 +130,26 @@ class QueryResultTypeWalker extends SqlWalker /** @var bool */ private $hasGroupByClause; + /** @var bool */ + private $failOnInvalidConnection; + /** * @param Query $query */ - public static function walk(Query $query, QueryResultTypeBuilder $typeBuilder, DescriptorRegistry $descriptorRegistry): void + public static function walk( + Query $query, + QueryResultTypeBuilder $typeBuilder, + DescriptorRegistry $descriptorRegistry, + PhpVersion $phpVersion, + DriverDetector $driverDetector + ): void { $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::class); $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [QueryAggregateFunctionDetectorTreeWalker::class]); $query->setHint(self::HINT_TYPE_MAPPING, $typeBuilder); $query->setHint(self::HINT_DESCRIPTOR_REGISTRY, $descriptorRegistry); + $query->setHint(self::HINT_PHP_VERSION, $phpVersion); + $query->setHint(self::HINT_DRIVER_DETECTOR, $driverDetector); $parser = new Parser($query); $parser->parse(); @@ -140,7 +169,6 @@ public function __construct($query, $parserResult, array $queryComponents) $this->queryComponents = $queryComponents; $this->nullableQueryComponents = []; $this->hasAggregateFunction = $query->hasHint(QueryAggregateFunctionDetectorTreeWalker::HINT_HAS_AGGREGATE_FUNCTION); - $this->hasGroupByClause = false; // The object is instantiated by Doctrine\ORM\Query\Parser, so receiving @@ -173,6 +201,32 @@ public function __construct($query, $parserResult, array $queryComponents) $this->descriptorRegistry = $descriptorRegistry; + $phpVersion = $this->query->getHint(self::HINT_PHP_VERSION); + + if (!$phpVersion instanceof PhpVersion) { // @phpstan-ignore-line ignore bc promise + throw new ShouldNotHappenException(sprintf( + 'Expected the query hint %s to contain a %s, but got a %s', + self::HINT_PHP_VERSION, + PhpVersion::class, + is_object($phpVersion) ? get_class($phpVersion) : gettype($phpVersion) + )); + } + + $this->phpVersion = $phpVersion; + + $driverDetector = $this->query->getHint(self::HINT_DRIVER_DETECTOR); + + if (!$driverDetector instanceof DriverDetector) { + throw new ShouldNotHappenException(sprintf( + 'Expected the query hint %s to contain a %s, but got a %s', + self::HINT_DRIVER_DETECTOR, + DriverDetector::class, + is_object($driverDetector) ? get_class($driverDetector) : gettype($driverDetector) + )); + } + $this->driverType = $driverDetector->detect($this->em->getConnection()); + $this->failOnInvalidConnection = $driverDetector->failsOnInvalidConnection(); + parent::__construct($query, $parserResult, $queryComponents); } @@ -228,6 +282,8 @@ public function walkPathExpression($pathExpr): string $dqlAlias = $pathExpr->identificationVariable; $qComp = $this->queryComponents[$dqlAlias]; assert(array_key_exists('metadata', $qComp)); + + /** @var ClassMetadata $class */ $class = $qComp['metadata']; assert($fieldName !== null); @@ -362,25 +418,65 @@ public function walkFunction($function): string { switch (true) { case $function instanceof AST\Functions\AvgFunction: + return $this->marshalType($this->inferAvgFunction($function)); + case $function instanceof AST\Functions\MaxFunction: case $function instanceof AST\Functions\MinFunction: + // mysql sqlite pdo_pgsql pgsql + // col_float => float float string float + // col_decimal => string int|float string string + // col_int => int int int int + // col_bigint => int int int int + // + // MIN(col_float) => float float string float + // MIN(col_decimal) => string int|float string string + // MIN(col_int) => int int int int + // MIN(col_bigint) => int int int int + + $exprType = $this->unmarshalType($function->getSql($this)); + $exprType = $this->generalizeConstantType($exprType, $this->hasAggregateWithoutGroupBy()); + return $this->marshalType($exprType); // retains underlying type + case $function instanceof AST\Functions\SumFunction: + return $this->marshalType($this->inferSumFunction($function)); + case $function instanceof AST\Functions\CountFunction: - return $function->getSql($this); + return $this->marshalType(IntegerRangeType::fromInterval(0, null)); case $function instanceof AST\Functions\AbsFunction: + // mysql sqlite pdo_pgsql pgsql + // col_float => float float string float + // col_decimal => string int|float string string + // col_int => int int int int + // col_bigint => int int int int + // + // ABS(col_float) => float float string float + // ABS(col_decimal) => string int|float string string + // ABS(col_int) => int int int int + // ABS(col_bigint) => int int int int + // ABS(col_string) => float float x x + $exprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->simpleArithmeticExpression)); + $exprType = $this->castStringLiteralForFloatExpression($exprType); + $exprType = $this->generalizeConstantType($exprType, false); - $type = TypeCombinator::union( - IntegerRangeType::fromInterval(0, null), - new FloatType() - ); + $exprTypeNoNull = TypeCombinator::removeNull($exprType); + $nullable = $this->canBeNull($exprType); - if ($this->canBeNull($exprType)) { - $type = TypeCombinator::addNull($type); + if ($exprTypeNoNull->isInteger()->yes()) { + $nonNegativeInt = $this->createNonNegativeInteger($nullable); + return $this->marshalType($nonNegativeInt); } - return $this->marshalType($type); + if ($this->containsOnlyNumericTypes($exprTypeNoNull)) { + if ($this->driverType === DriverDetector::PDO_PGSQL) { + return $this->marshalType($this->createNumericString($nullable)); + } + + return $this->marshalType($exprType); // retains underlying type + } + + return $this->marshalType(new MixedType()); case $function instanceof AST\Functions\BitAndFunction: case $function instanceof AST\Functions\BitOrFunction: @@ -430,10 +526,12 @@ public function walkFunction($function): string $date1ExprType = $this->unmarshalType($function->date1->dispatch($this)); $date2ExprType = $this->unmarshalType($function->date2->dispatch($this)); - $type = TypeCombinator::union( - new IntegerType(), - new FloatType() - ); + if ($this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { + $type = new FloatType(); + } else { + $type = new IntegerType(); + } + if ($this->canBeNull($date1ExprType) || $this->canBeNull($date2ExprType)) { $type = TypeCombinator::addNull($type); } @@ -477,22 +575,79 @@ public function walkFunction($function): string $firstExprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->firstSimpleArithmeticExpression)); $secondExprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->secondSimpleArithmeticExpression)); + $union = TypeCombinator::union($firstExprType, $secondExprType); + $unionNoNull = TypeCombinator::removeNull($union); + + if (!$unionNoNull->isInteger()->yes()) { + return $this->marshalType(new MixedType()); // dont try to deal with non-integer chaos + } + $type = IntegerRangeType::fromInterval(0, null); + if ($this->canBeNull($firstExprType) || $this->canBeNull($secondExprType)) { $type = TypeCombinator::addNull($type); } - if ((new ConstantIntegerType(0))->isSuperTypeOf($secondExprType)->maybe()) { - // MOD(x, 0) returns NULL + $isPgSql = $this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL; + $mayBeZero = !(new ConstantIntegerType(0))->isSuperTypeOf($secondExprType)->no(); + + if (!$isPgSql && $mayBeZero) { // MOD(x, 0) returns NULL in non-strict platforms, fails in postgre $type = TypeCombinator::addNull($type); } - return $this->marshalType($type); + return $this->marshalType($this->generalizeConstantType($type, false)); case $function instanceof AST\Functions\SqrtFunction: + // mysql sqlite pdo_pgsql pgsql + // col_float => float float string float + // col_decimal => string float|int string string + // col_int => int int int int + // col_bigint => int int int int + // + // SQRT(col_float) => float float string float + // SQRT(col_decimal) => float float string string + // SQRT(col_int) => float float string float + // SQRT(col_bigint) => float float string float + $exprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->simpleArithmeticExpression)); + $exprTypeNoNull = TypeCombinator::removeNull($exprType); + + if (!$this->containsOnlyNumericTypes($exprTypeNoNull)) { + return $this->marshalType(new MixedType()); // dont try to deal with non-numeric args + } + + if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { + $type = new FloatType(); + + $cannotBeNegative = $exprType->isSmallerThan(new ConstantIntegerType(0))->no(); + $canBeNegative = !$cannotBeNegative; + if ($canBeNegative) { + $type = TypeCombinator::addNull($type); + } + + } elseif ($this->driverType === DriverDetector::PDO_PGSQL) { + $type = new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + + } elseif ($this->driverType === DriverDetector::PGSQL) { + $castedExprType = $this->castStringLiteralForNumericExpression($exprTypeNoNull); + + if ($castedExprType->isInteger()->yes() || $castedExprType->isFloat()->yes()) { + $type = $this->createFloat(false); + + } elseif ($castedExprType->isNumericString()->yes()) { + $type = $this->createNumericString(false); + + } else { + $type = TypeCombinator::union($this->createFloat(false), $this->createNumericString(false)); + } + + } else { + $type = new MixedType(); + } - $type = new FloatType(); if ($this->canBeNull($exprType)) { $type = TypeCombinator::addNull($type); } @@ -576,13 +731,108 @@ public function walkFunction($function): string } } + private function inferAvgFunction(AST\Functions\AvgFunction $function): Type + { + // mysql sqlite pdo_pgsql pgsql + // col_float => float float string float + // col_decimal => string int|float string string + // col_int => int int int int + // col_bigint => int int int int + // + // AVG(col_float) => float float string float + // AVG(col_decimal) => string float string string + // AVG(col_int) => string float string string + // AVG(col_bigint) => string float string string + + $exprType = $this->unmarshalType($function->getSql($this)); + $exprTypeNoNull = TypeCombinator::removeNull($exprType); + $nullable = $this->canBeNull($exprType) || $this->hasAggregateWithoutGroupBy(); + + if ($this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { + return $this->createFloat($nullable); + } + + if ($this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::MYSQLI) { + if ($exprTypeNoNull->isInteger()->yes()) { + return $this->createNumericString($nullable); + } + + if ($exprTypeNoNull->isString()->yes() && !$exprTypeNoNull->isNumericString()->yes()) { + return $this->createFloat($nullable); + } + + return $this->generalizeConstantType($exprType, $nullable); + } + + if ($this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { + if ($exprTypeNoNull->isInteger()->yes()) { + return $this->createNumericString($nullable); + } + + return $this->generalizeConstantType($exprType, $nullable); + } + + return new MixedType(); + } + + private function inferSumFunction(AST\Functions\SumFunction $function): Type + { + // mysql sqlite pdo_pgsql pgsql + // col_float => float float string float + // col_decimal => string int|float string string + // col_int => int int int int + // col_bigint => int int int int + // + // SUM(col_float) => float float string float + // SUM(col_decimal) => string int|float string string + // SUM(col_int) => string int int int + // SUM(col_bigint) => string int string string + + $exprType = $this->unmarshalType($function->getSql($this)); + $exprTypeNoNull = TypeCombinator::removeNull($exprType); + $nullable = $this->canBeNull($exprType) || $this->hasAggregateWithoutGroupBy(); + + if ($this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { + if ($exprTypeNoNull->isString()->yes() && !$exprTypeNoNull->isNumericString()->yes()) { + return $this->createFloat($nullable); + } + + return $this->generalizeConstantType($exprType, $nullable); + } + + if ($this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::MYSQLI) { + if ($exprTypeNoNull->isInteger()->yes()) { + return $this->createNumericString($nullable); + } + + if ($exprTypeNoNull->isString()->yes() && !$exprTypeNoNull->isNumericString()->yes()) { + return $this->createFloat($nullable); + } + + return $this->generalizeConstantType($exprType, $nullable); + } + + if ($this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { + if ($exprTypeNoNull->isInteger()->yes()) { + return TypeCombinator::union( + $this->createInteger($nullable), + $this->createNumericString($nullable) + ); + } + + return $this->generalizeConstantType($exprType, $nullable); + } + + return new MixedType(); + } + private function createFloat(bool $nullable): Type { $float = new FloatType(); return $nullable ? TypeCombinator::addNull($float) : $float; } - private function createFloatOrInt(bool $nullable): Type // @phpstan-ignore-line unused, but kept to ease conflict resolution with origin (#506) + private function createFloatOrInt(bool $nullable): Type { $union = TypeCombinator::union( new FloatType(), @@ -597,7 +847,7 @@ private function createInteger(bool $nullable): Type return $nullable ? TypeCombinator::addNull($integer) : $integer; } - private function createNonNegativeInteger(bool $nullable): Type // @phpstan-ignore-line unused, but kept to ease conflict resolution with origin (#506) + private function createNonNegativeInteger(bool $nullable): Type { $integer = IntegerRangeType::fromInterval(0, null); return $nullable ? TypeCombinator::addNull($integer) : $integer; @@ -619,6 +869,30 @@ private function createString(bool $nullable): Type return $nullable ? TypeCombinator::addNull($string) : $string; } + private function containsOnlyNumericTypes( + Type ...$checkedTypes + ): bool + { + foreach ($checkedTypes as $checkedType) { + if (!$this->containsOnlyTypes($checkedType, [new IntegerType(), new FloatType(), $this->createNumericString(false)])) { + return false; + } + } + return true; + } + + /** + * @param list $allowedTypes + */ + private function containsOnlyTypes( + Type $checkedType, + array $allowedTypes + ): bool + { + $allowedType = TypeCombinator::union(...$allowedTypes); + return $allowedType->isSuperTypeOf($checkedType)->yes(); + } + /** * E.g. to ensure SUM(1) is inferred as int, not 1 */ @@ -905,24 +1179,62 @@ public function walkSelectExpression($selectExpression): string $type, $this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type)) ); + } else { // Expressions default to Doctrine's StringType, whose // convertToPHPValue() is a no-op. So the actual type depends on // the driver and PHP version. - // Here we assume that the value may or may not be casted to - // string by the driver. - $type = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + + $type = TypeTraverser::map($type, function (Type $type, callable $traverse): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } - if ($type instanceof IntegerType || $type instanceof FloatType) { - return TypeCombinator::union($type->toString(), $type); + + if ($type instanceof IntegerType) { + $stringify = $this->shouldStringifyExpressions($type); + + if ($stringify->yes()) { + return $type->toString(); + } elseif ($stringify->maybe()) { + return TypeCombinator::union($type->toString(), $type); + } + + return $type; } + + if ($type instanceof FloatType) { + $stringify = $this->shouldStringifyExpressions($type); + + // e.g. 1.0 on sqlite results to '1' with pdo_stringify on PHP 8.1, but '1.0' on PHP 8.0 with no setup + // so we relax constant types and return just numeric-string to avoid those issues + $stringifiedFloat = $this->createNumericString(false); + + if ($stringify->yes()) { + return $stringifiedFloat; + } elseif ($stringify->maybe()) { + return TypeCombinator::union($stringifiedFloat, $type); + } + + return $type; + } + if ($type instanceof BooleanType) { - return TypeCombinator::union($type->toInteger()->toString(), $type); + $stringify = $this->shouldStringifyExpressions($type); + + if ($stringify->yes()) { + return $type->toInteger()->toString(); + } elseif ($stringify->maybe()) { + return TypeCombinator::union($type->toInteger()->toString(), $type); + } + + return $type; } return $traverse($type); }); + + if (!$this->isSupportedDriver()) { + $type = new MixedType(); // avoid guessing for unsupported drivers, there are too many differences + } } $this->typeBuilder->addScalar($resultAlias, $type); @@ -999,39 +1311,62 @@ public function walkSimpleSelectExpression($simpleSelectExpression): string public function walkAggregateExpression($aggExpression): string { switch (strtoupper($aggExpression->functionName)) { + case 'AVG': + case 'SUM': + $type = $this->unmarshalType($this->walkSimpleArithmeticExpression($aggExpression->pathExpression)); + $type = $this->castStringLiteralForNumericExpression($type); + return $this->marshalType($type); + case 'MAX': case 'MIN': - $type = $this->unmarshalType( - $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) - ); + return $this->walkSimpleArithmeticExpression($aggExpression->pathExpression); - return $this->marshalType(TypeCombinator::addNull($type)); + case 'COUNT': + return $this->marshalType(IntegerRangeType::fromInterval(0, null)); - case 'AVG': - $type = $this->unmarshalType( - $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) - ); + default: + return $this->marshalType(new MixedType()); + } + } - $type = TypeCombinator::union($type, $type->toFloat()); - $type = TypeUtils::generalizeType($type, GeneralizePrecision::lessSpecific()); + private function castStringLiteralForFloatExpression(Type $type): Type + { + if (!$type instanceof DqlConstantStringType || $type->getOriginLiteralType() !== AST\Literal::STRING) { + return $type; + } - return $this->marshalType(TypeCombinator::addNull($type)); + $value = $type->getValue(); - case 'SUM': - $type = $this->unmarshalType( - $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) - ); + if (is_numeric($value)) { + return new ConstantFloatType((float) $value); + } - $type = TypeUtils::generalizeType($type, GeneralizePrecision::lessSpecific()); + return $type; + } - return $this->marshalType(TypeCombinator::addNull($type)); + /** + * Numeric strings are kept as strings in literal usage, but casted to numeric value once used in numeric expression + * - SELECT '1' => '1' + * - SELECT 1 * '1' => 1 + */ + private function castStringLiteralForNumericExpression(Type $type): Type + { + if (!$type instanceof DqlConstantStringType || $type->getOriginLiteralType() !== AST\Literal::STRING) { + return $type; + } - case 'COUNT': - return $this->marshalType(IntegerRangeType::fromInterval(0, null)); + $isMysql = $this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL; + $value = $type->getValue(); - default: - return $this->marshalType(new MixedType()); + if (is_numeric($value)) { + if (strpos($value, '.') === false && strpos($value, 'e') === false && !$isMysql) { + return new ConstantIntegerType((int) $value); + } + + return new ConstantFloatType((float) $value); } + + return $type; } /** @@ -1176,25 +1511,46 @@ public function walkLiteral($literal): string case AST\Literal::STRING: $value = $literal->value; assert(is_string($value)); - $type = new ConstantStringType($value); + $type = new DqlConstantStringType($value, $literal->type); break; case AST\Literal::BOOLEAN: $value = strtolower($literal->value) === 'true'; - $type = TypeCombinator::union( - new ConstantIntegerType($value ? 1 : 0), - new ConstantBooleanType($value) - ); + if ($this->driverType === DriverDetector::PDO_PGSQL || $this->driverType === DriverDetector::PGSQL) { + $type = new ConstantBooleanType($value); + } else { + $type = new ConstantIntegerType($value ? 1 : 0); + } break; case AST\Literal::NUMERIC: $value = $literal->value; - assert(is_numeric($value)); + assert(is_int($value) || is_string($value)); // ensured in parser - if (floatval(intval($value)) === floatval($value)) { + if (is_int($value) || (strpos($value, '.') === false && strpos($value, 'e') === false)) { $type = new ConstantIntegerType((int) $value); + } else { - $type = new ConstantFloatType((float) $value); + if ($this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::MYSQLI) { + // both pdo_mysql and mysqli hydrates decimal literal (e.g. 123.4) as string no matter the configuration (e.g. PDO::ATTR_STRINGIFY_FETCHES being false) and PHP version + // the only way to force float is to use float literal with scientific notation (e.g. 123.4e0) + // https://dev.mysql.com/doc/refman/8.0/en/number-literals.html + + if (stripos($value, 'e') !== false) { + $type = new ConstantFloatType((float) $value); + } else { + $type = new DqlConstantStringType($value, $literal->type); + } + } elseif ($this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { + if (stripos($value, 'e') !== false) { + $type = new DqlConstantStringType((string) (float) $value, $literal->type); + } else { + $type = new DqlConstantStringType($value, $literal->type); + } + + } else { + $type = new ConstantFloatType((float) $value); + } } break; @@ -1279,14 +1635,13 @@ public function walkSimpleArithmeticExpression($simpleArithmeticExpr): string // Skip '+' or '-' continue; } - $type = $this->unmarshalType($this->walkArithmeticPrimary($term)); - $types[] = TypeUtils::generalizeType($type, GeneralizePrecision::lessSpecific()); - } - $type = TypeCombinator::union(...$types); - $type = $this->toNumericOrNull($type); + $types[] = $this->castStringLiteralForNumericExpression( + $this->unmarshalType($this->walkArithmeticPrimary($term)) + ); + } - return $this->marshalType($type); + return $this->marshalType($this->inferPlusMinusTimesType($types)); } /** @@ -1299,20 +1654,196 @@ public function walkArithmeticTerm($term): string } $types = []; + $operators = []; foreach ($term->arithmeticFactors as $factor) { if (!$factor instanceof AST\Node) { - // Skip '*' or '/' - continue; + assert(is_string($factor)); + $operators[$factor] = $factor; + continue; // Skip '*' or '/' } - $type = $this->unmarshalType($this->walkArithmeticPrimary($factor)); - $types[] = TypeUtils::generalizeType($type, GeneralizePrecision::lessSpecific()); + + $types[] = $this->castStringLiteralForNumericExpression( + $this->unmarshalType($this->walkArithmeticPrimary($factor)) + ); } - $type = TypeCombinator::union(...$types); - $type = $this->toNumericOrNull($type); + if (array_values($operators) === ['*']) { + return $this->marshalType($this->inferPlusMinusTimesType($types)); + } - return $this->marshalType($type); + return $this->marshalType($this->inferDivisionType($types)); + } + + /** + * @param list $termTypes + */ + private function inferPlusMinusTimesType(array $termTypes): Type + { + // mysql sqlite pdo_pgsql pgsql + // col_float float float string float + // col_decimal string float|int string string + // col_int int int int int + // col_bigint int int int int + // col_bool int int bool bool + // + // col_int + col_int int int int int + // col_int + col_float float float string float + // col_float + col_float float float string float + // col_float + col_decimal float float string float + // col_int + col_decimal string float|int string string + // col_decimal + col_decimal string float|int string string + // col_string + col_string float int x x + // col_int + col_string float int x x + // col_bool + col_bool int int x x + // col_int + col_bool int int x x + // col_float + col_string float float x x + // col_decimal + col_string float float|int x x + // col_float + col_bool float float x x + // col_decimal + col_bool string float|int x x + + $types = []; + $typesNoNull = []; + + foreach ($termTypes as $termType) { + $generalizedType = $this->generalizeConstantType($termType, false); + $types[] = $generalizedType; + $typesNoNull[] = TypeCombinator::removeNull($generalizedType); + } + + $union = TypeCombinator::union(...$types); + $nullable = $this->canBeNull($union); + $unionWithoutNull = TypeCombinator::removeNull($union); + + if ($unionWithoutNull->isInteger()->yes()) { + return $this->createInteger($nullable); + } + + if ($this->driverType === DriverDetector::PDO_PGSQL) { + if ($this->containsOnlyNumericTypes($unionWithoutNull)) { + return $this->createNumericString($nullable); + } + } + + if ($this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { + if (!$this->containsOnlyNumericTypes(...$typesNoNull)) { + return new MixedType(); + } + + foreach ($typesNoNull as $typeNoNull) { + if ($typeNoNull->isFloat()->yes()) { + return $this->createFloat($nullable); + } + } + + return $this->createFloatOrInt($nullable); + } + + if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL) { + if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType()])) { + return $this->createFloat($nullable); + } + + if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), $this->createNumericString(false)])) { + return $this->createNumericString($nullable); + } + + if ($this->containsOnlyNumericTypes($unionWithoutNull)) { + return $this->createFloat($nullable); + } + } + + return new MixedType(); + } + + /** + * @param list $termTypes + */ + private function inferDivisionType(array $termTypes): Type + { + // mysql sqlite pdo_pgsql pgsql + // col_float => float float string float + // col_decimal => string float|int string string + // col_int => int int int int + // col_bigint => int int int int + // + // col_int / col_int string int int int + // col_int / col_float float float string float + // col_float / col_float float float string float + // col_float / col_decimal float float string float + // col_int / col_decimal string float|int string string + // col_decimal / col_decimal string float|int string string + // col_string / col_string null null x x + // col_int / col_string null null x x + // col_bool / col_bool string int x x + // col_int / col_bool string int x x + // col_float / col_string null null x x + // col_decimal / col_string null null x x + // col_float / col_bool float float x x + // col_decimal / col_bool string float x x + + $types = []; + $typesNoNull = []; + + foreach ($termTypes as $termType) { + $generalizedType = $this->generalizeConstantType($termType, false); + $types[] = $generalizedType; + $typesNoNull[] = TypeCombinator::removeNull($generalizedType); + } + + $union = TypeCombinator::union(...$types); + $nullable = $this->canBeNull($union); + $unionWithoutNull = TypeCombinator::removeNull($union); + + if ($unionWithoutNull->isInteger()->yes()) { + if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL) { + return $this->createNumericString($nullable); + } elseif ($this->driverType === DriverDetector::PDO_PGSQL || $this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { + return $this->createInteger($nullable); + } + + return new MixedType(); + } + + if ($this->driverType === DriverDetector::PDO_PGSQL) { + if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType(), $this->createNumericString(false)])) { + return $this->createNumericString($nullable); + } + } + + if ($this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { + if (!$this->containsOnlyNumericTypes(...$typesNoNull)) { + return new MixedType(); + } + + foreach ($typesNoNull as $typeNoNull) { + if ($typeNoNull->isFloat()->yes()) { + return $this->createFloat($nullable); + } + } + + return $this->createFloatOrInt($nullable); + } + + if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL) { + if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType()])) { + return $this->createFloat($nullable); + } + + if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), $this->createNumericString(false)])) { + return $this->createNumericString($nullable); + } + + if ($this->containsOnlyTypes($unionWithoutNull, [new FloatType(), $this->createNumericString(false)])) { + return $this->createFloat($nullable); + } + + if ($this->containsOnlyNumericTypes($unionWithoutNull)) { + return $this->createFloat($nullable); + } + } + + return new MixedType(); } /** @@ -1454,9 +1985,11 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null, private function resolveDatabaseInternalType(string $typeName, ?string $enumType = null, bool $nullable = false): Type { try { - $type = $this->descriptorRegistry - ->get($typeName) - ->getDatabaseInternalType(); + $descriptor = $this->descriptorRegistry->get($typeName); + $type = $descriptor instanceof DoctrineTypeDriverAwareDescriptor + ? $descriptor->getDatabaseInternalTypeForDriver($this->em->getConnection()) + : $descriptor->getDatabaseInternalType(); + } catch (DescriptorNotRegisteredException $e) { $type = new MixedType(); } @@ -1482,25 +2015,6 @@ private function canBeNull(Type $type): bool return !$type->isSuperTypeOf(new NullType())->no(); } - private function toNumericOrNull(Type $type): Type - { - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - if ($type instanceof NullType || $type instanceof IntegerType) { - return $type; - } - if ($type instanceof BooleanType) { - return $type->toInteger(); - } - return TypeCombinator::union( - $type->toFloat(), - $type->toInteger() - ); - }); - } - /** * Returns whether the query has aggregate function and no group by clause * @@ -1515,4 +2029,135 @@ private function hasAggregateWithoutGroupBy(): bool return $this->hasAggregateFunction && !$this->hasGroupByClause; } + /** + * See analysis: https://github.com/janedbal/php-database-drivers-fetch-test + * + * Notable 8.1 changes: + * - pdo_mysql: https://github.com/php/php-src/commit/c18b1aea289e8ed6edb3f6e6a135018976a034c6 + * - pdo_sqlite: https://github.com/php/php-src/commit/438b025a28cda2935613af412fc13702883dd3a2 + * - pdo_pgsql: https://github.com/php/php-src/commit/737195c3ae6ac53b9501cfc39cc80fd462909c82 + * + * @param IntegerType|FloatType|BooleanType $type + */ + private function shouldStringifyExpressions(Type $type): TrinaryLogic + { + if (in_array($this->driverType, [DriverDetector::PDO_MYSQL, DriverDetector::PDO_PGSQL, DriverDetector::PDO_SQLITE], true)) { + try { + $nativeConnection = $this->getNativeConnection(); + assert($nativeConnection instanceof PDO); + } catch (Throwable $e) { // connection cannot be established + if ($this->failOnInvalidConnection) { + throw $e; + } + return TrinaryLogic::createMaybe(); + } + + $stringifyFetches = $this->isPdoStringifyEnabled($nativeConnection); + + if ($this->driverType === DriverDetector::PDO_MYSQL) { + $emulatedPrepares = $this->isPdoEmulatePreparesEnabled($nativeConnection); + + if ($stringifyFetches) { + return TrinaryLogic::createYes(); + } + + if ($this->phpVersion->getVersionId() >= 80100) { + return TrinaryLogic::createNo(); + } + + if ($emulatedPrepares) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createNo(); + } + + if ($this->driverType === DriverDetector::PDO_SQLITE) { + if ($stringifyFetches) { + return TrinaryLogic::createYes(); + } + + if ($this->phpVersion->getVersionId() >= 80100) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createYes(); + } + + if ($this->driverType === DriverDetector::PDO_PGSQL) { // @phpstan-ignore-line always true, but keep it readable + if ($type->isBoolean()->yes()) { + if ($this->phpVersion->getVersionId() >= 80100) { + return TrinaryLogic::createFromBoolean($stringifyFetches); + } + + return TrinaryLogic::createNo(); + + } + + return TrinaryLogic::createFromBoolean($stringifyFetches); + } + } + + if ($this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::MYSQLI) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); + } + + private function isPdoStringifyEnabled(PDO $pdo): bool + { + // this fails for most PHP versions, see https://github.com/php/php-src/issues/12969 + // working since 8.2.15 and 8.3.2 + try { + return (bool) $pdo->getAttribute(PDO::ATTR_STRINGIFY_FETCHES); + } catch (PDOException $e) { + $selectOne = $pdo->query('SELECT 1'); + if ($selectOne === false) { + return false; // this should not happen, just return attribute default value + } + $one = $selectOne->fetchColumn(); + + // string can be returned due to old PHP used or because ATTR_STRINGIFY_FETCHES is enabled, + // but it should not matter as it behaves the same way + // (the attribute is there to maintain BC) + return is_string($one); + } + } + + private function isPdoEmulatePreparesEnabled(PDO $pdo): bool + { + return (bool) $pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES); + } + + /** + * @return object|resource|null + */ + private function getNativeConnection() + { + $connection = $this->em->getConnection(); + + if (method_exists($connection, 'getNativeConnection')) { + return $connection->getNativeConnection(); + } + + if ($connection->getWrappedConnection() instanceof PDO) { + return $connection->getWrappedConnection(); + } + + return null; + } + + private function isSupportedDriver(): bool + { + return in_array($this->driverType, [ + DriverDetector::MYSQLI, + DriverDetector::PDO_MYSQL, + DriverDetector::PGSQL, + DriverDetector::PDO_PGSQL, + DriverDetector::SQLITE3, + DriverDetector::PDO_SQLITE, + ], true); + } + } diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php index bd0c26f9..366eaa60 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php @@ -11,6 +11,8 @@ use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; +use PHPStan\Doctrine\Driver\DriverDetector; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Doctrine\ORM\DynamicQueryBuilderArgumentException; use PHPStan\Type\Doctrine\ArgumentsProcessor; @@ -65,17 +67,27 @@ class QueryBuilderGetQueryDynamicReturnTypeExtension implements DynamicMethodRet /** @var DescriptorRegistry */ private $descriptorRegistry; + /** @var PhpVersion */ + private $phpVersion; + + /** @var DriverDetector */ + private $driverDetector; + public function __construct( ObjectMetadataResolver $objectMetadataResolver, ArgumentsProcessor $argumentsProcessor, ?string $queryBuilderClass, - DescriptorRegistry $descriptorRegistry + DescriptorRegistry $descriptorRegistry, + PhpVersion $phpVersion, + DriverDetector $driverDetector ) { $this->objectMetadataResolver = $objectMetadataResolver; $this->argumentsProcessor = $argumentsProcessor; $this->queryBuilderClass = $queryBuilderClass; $this->descriptorRegistry = $descriptorRegistry; + $this->phpVersion = $phpVersion; + $this->driverDetector = $driverDetector; } public function getClass(): string @@ -190,7 +202,7 @@ private function getQueryType(string $dql): Type try { $query = $em->createQuery($dql); - QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry); + QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry, $this->phpVersion, $this->driverDetector); } catch (ORMException | DBALException | CommonException | MappingException | \Doctrine\ORM\Exception\ORMException $e) { return new QueryType($dql, null); } catch (AssertionError $e) { diff --git a/tests/Platform/Entity/PlatformEntity.php b/tests/Platform/Entity/PlatformEntity.php index d0c1a5aa..6da280e9 100644 --- a/tests/Platform/Entity/PlatformEntity.php +++ b/tests/Platform/Entity/PlatformEntity.php @@ -2,6 +2,7 @@ namespace PHPStan\Platform\Entity; +use DateTimeInterface; use Doctrine\ORM\Mapping as ORM; /** @@ -22,6 +23,15 @@ class PlatformEntity #[ORM\Column(type: 'string', nullable: false)] public $id; + /** + * @ORM\ManyToOne(targetEntity=PlatformRelatedEntity::class) + * @ORM\JoinColumn(name="related_entity_id", referencedColumnName="id", nullable=false) + * @var PlatformRelatedEntity + */ + #[ORM\ManyToOne(targetEntity: PlatformRelatedEntity::class)] + #[ORM\JoinColumn(name: 'related_entity_id', referencedColumnName: 'id', nullable: false)] + public $related_entity; + /** * @ORM\Column(type="string", name="col_string", nullable=false) * @var string @@ -29,6 +39,13 @@ class PlatformEntity #[ORM\Column(type: 'string', name: 'col_string', nullable: false)] public $col_string; + /** + * @ORM\Column(type="string", name="col_string_nullable", nullable=true) + * @var string|null + */ + #[ORM\Column(type: 'string', name: 'col_string_nullable', nullable: true)] + public $col_string_nullable; + /** * @ORM\Column(type="boolean", name="col_bool", nullable=false) * @var bool @@ -36,6 +53,13 @@ class PlatformEntity #[ORM\Column(type: 'boolean', name: 'col_bool', nullable: false)] public $col_bool; + /** + * @ORM\Column(type="boolean", name="col_bool_nullable", nullable=true) + * @var bool|null + */ + #[ORM\Column(type: 'boolean', name: 'col_bool_nullable', nullable: true)] + public $col_bool_nullable; + /** * @ORM\Column(type="float", name="col_float", nullable=false) * @var float @@ -43,6 +67,13 @@ class PlatformEntity #[ORM\Column(type: 'float', name: 'col_float', nullable: false)] public $col_float; + /** + * @ORM\Column(type="float", name="col_float_nullable", nullable=true) + * @var float|null + */ + #[ORM\Column(type: 'float', name: 'col_float_nullable', nullable: true)] + public $col_float_nullable; + /** * @ORM\Column(type="decimal", name="col_decimal", nullable=false, scale=1, precision=2) * @var string @@ -50,6 +81,13 @@ class PlatformEntity #[ORM\Column(type: 'decimal', name: 'col_decimal', nullable: false, scale: 1, precision: 2)] public $col_decimal; + /** + * @ORM\Column(type="decimal", name="col_decimal_nullable", nullable=true, scale=1, precision=2) + * @var string|null + */ + #[ORM\Column(type: 'decimal', name: 'col_decimal_nullable', nullable: true, scale: 1, precision: 2)] + public $col_decimal_nullable; + /** * @ORM\Column(type="integer", name="col_int", nullable=false) * @var int @@ -57,6 +95,13 @@ class PlatformEntity #[ORM\Column(type: 'integer', name: 'col_int', nullable: false)] public $col_int; + /** + * @ORM\Column(type="integer", name="col_int_nullable", nullable=true) + * @var int|null + */ + #[ORM\Column(type: 'integer', name: 'col_int_nullable', nullable: true)] + public $col_int_nullable; + /** * @ORM\Column(type="bigint", name="col_bigint", nullable=false) * @var int|string @@ -64,4 +109,25 @@ class PlatformEntity #[ORM\Column(type: 'bigint', name: 'col_bigint', nullable: false)] public $col_bigint; + /** + * @ORM\Column(type="bigint", name="col_bigint_nullable", nullable=true) + * @var int|string|null + */ + #[ORM\Column(type: 'bigint', name: 'col_bigint_nullable', nullable: true)] + public $col_bigint_nullable; + + /** + * @ORM\Column(type="mixed", name="col_mixed", nullable=false) + * @var mixed + */ + #[ORM\Column(type: 'mixed', name: 'col_mixed', nullable: false)] + public $col_mixed; + + /** + * @ORM\Column(type="datetime", name="col_datetime", nullable=false) + * @var DateTimeInterface + */ + #[ORM\Column(type: 'datetime', name: 'col_datetime', nullable: false)] + public $col_datetime; + } diff --git a/tests/Platform/Entity/PlatformRelatedEntity.php b/tests/Platform/Entity/PlatformRelatedEntity.php new file mode 100644 index 00000000..86c4b00a --- /dev/null +++ b/tests/Platform/Entity/PlatformRelatedEntity.php @@ -0,0 +1,25 @@ + [], + self::CONFIG_STRINGIFY => [ + PDO::ATTR_STRINGIFY_FETCHES => true, + ], + self::CONFIG_NO_EMULATE => [ + PDO::ATTR_EMULATE_PREPARES => false, + ], + self::CONFIG_STRINGIFY_NO_EMULATE => [ + PDO::ATTR_STRINGIFY_FETCHES => true, + PDO::ATTR_EMULATE_PREPARES => false, + ], + ]; + public static function getAdditionalConfigFiles(): array { return [ @@ -62,362 +102,4757 @@ public static function getAdditionalConfigFiles(): array } /** - * @param array $connectionParams - * @param array $expectedOnPhp80AndBelow - * @param array $expectedOnPhp81AndAbove - * @param array $connectionAttributes - * - * @dataProvider provideCases + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoMysqlDefault( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_mysql', + self::CONFIG_DEFAULT, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $mysqlExpectedType, + $mysqlExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoMysqlStringify( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_mysql', + self::CONFIG_STRINGIFY, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $mysqlExpectedType, + $mysqlExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoMysqlNoEmulate( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_mysql', + self::CONFIG_NO_EMULATE, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $mysqlExpectedType, + $mysqlExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoMysqlStringifyNoEmulate( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_mysql', + self::CONFIG_STRINGIFY_NO_EMULATE, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $mysqlExpectedType, + $mysqlExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoMysqliDefault( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'mysqli', + self::CONFIG_DEFAULT, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $mysqlExpectedType, + $mysqlExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoSqliteDefault( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_sqlite', + self::CONFIG_DEFAULT, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $sqliteExpectedType, + $sqliteExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoSqliteStringify( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_sqlite', + self::CONFIG_STRINGIFY, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $sqliteExpectedType, + $sqliteExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoSqlite3( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'sqlite3', + self::CONFIG_DEFAULT, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $sqliteExpectedType, + $sqliteExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoPgsqlDefault( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_pgsql', + self::CONFIG_DEFAULT, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $pdoPgsqlExpectedType, + $pdoPgsqlExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPdoPgsqlStringify( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_pgsql', + self::CONFIG_STRINGIFY, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $pdoPgsqlExpectedType, + $pdoPgsqlExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testPgsql( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pgsql', + self::CONFIG_DEFAULT, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $pgsqlExpectedType, + $pgsqlExpectedResult, + $stringify + ); + } + + /** + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testUnsupportedDriver( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'sqlsrv', + self::CONFIG_DEFAULT, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $mssqlExpectedType, + $mssqlExpectedResult, + $stringify + ); + } + + /** + * Connection failure test + * + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testKnownDriverUnknownSetupDefault( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_mysql', + self::CONFIG_DEFAULT, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $this->determineTypeForKnownDriverUnknownSetup($mysqlExpectedType, $stringify), + $mysqlExpectedResult, + $stringify, + self::INVALID_CONNECTION + ); + } + + /** + * Connection failure test + * + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testKnownDriverUnknownSetupStringify( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_mysql', + self::CONFIG_STRINGIFY, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $this->determineTypeForKnownDriverUnknownSetup($mysqlExpectedType, $stringify), + $mysqlExpectedResult, + $stringify, + self::INVALID_CONNECTION + ); + } + + /** + * Connection failure test + * + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testUnknownDriverUnknownSetupDefault( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_mysql', + self::CONFIG_DEFAULT, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $this->determineTypeForUnknownDriverUnknownSetup($mysqlExpectedType, $stringify), + $mysqlExpectedResult, + $stringify, + self::INVALID_CONNECTION_UNKNOWN_DRIVER + ); + } + + /** + * Connection failure test + * + * @param array $data + * @param mixed $mysqlExpectedResult + * @param mixed $sqliteExpectedResult + * @param mixed $pdoPgsqlExpectedResult + * @param mixed $pgsqlExpectedResult + * @param mixed $mssqlExpectedResult + * @param self::STRINGIFY_* $stringify + * + * @dataProvider provideCases + */ + public function testUnknownDriverUnknownSetupStringify( + array $data, + string $dqlTemplate, + Type $mysqlExpectedType, + ?Type $sqliteExpectedType, + ?Type $pdoPgsqlExpectedType, + ?Type $pgsqlExpectedType, + ?Type $mssqlExpectedType, + $mysqlExpectedResult, + $sqliteExpectedResult, + $pdoPgsqlExpectedResult, + $pgsqlExpectedResult, + $mssqlExpectedResult, + string $stringify + ): void + { + $this->performDriverTest( + 'pdo_mysql', + self::CONFIG_STRINGIFY, + $data, + $dqlTemplate, + (string) $this->dataName(), + PHP_VERSION_ID, + $this->determineTypeForUnknownDriverUnknownSetup($mysqlExpectedType, $stringify), + $mysqlExpectedResult, + $stringify, + self::INVALID_CONNECTION_UNKNOWN_DRIVER + ); + } + + /** + * @return iterable + */ + public static function provideCases(): iterable + { + yield ' -1' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT -1 FROM %s t', + 'mysql' => new ConstantIntegerType(-1), + 'sqlite' => new ConstantIntegerType(-1), + 'pdo_pgsql' => new ConstantIntegerType(-1), + 'pgsql' => new ConstantIntegerType(-1), + 'mssql' => self::mixed(), + 'mysqlResult' => -1, + 'sqliteResult' => -1, + 'pdoPgsqlResult' => -1, + 'pgsqlResult' => -1, + 'mssqlResult' => -1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield ' 1' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT 1 FROM %s t', + 'mysql' => new ConstantIntegerType(1), + 'sqlite' => new ConstantIntegerType(1), + 'pdo_pgsql' => new ConstantIntegerType(1), + 'pgsql' => new ConstantIntegerType(1), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield ' 1.0' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT 1.0 FROM %s t', + 'mysql' => new ConstantStringType('1.0'), + 'sqlite' => new ConstantFloatType(1.0), + 'pdo_pgsql' => new ConstantStringType('1.0'), + 'pgsql' => new ConstantStringType('1.0'), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield ' 1.00' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT 1.00 FROM %s t', + 'mysql' => new ConstantStringType('1.00'), + 'sqlite' => new ConstantFloatType(1.0), + 'pdo_pgsql' => new ConstantStringType('1.00'), + 'pgsql' => new ConstantStringType('1.00'), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.00', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00', + 'pgsqlResult' => '1.00', + 'mssqlResult' => '1.00', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield ' 0.1' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT 0.1 FROM %s t', + 'mysql' => new ConstantStringType('0.1'), + 'sqlite' => new ConstantFloatType(0.1), + 'pdo_pgsql' => new ConstantStringType('0.1'), + 'pgsql' => new ConstantStringType('0.1'), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.1', + 'sqliteResult' => 0.1, + 'pdoPgsqlResult' => '0.1', + 'pgsqlResult' => '0.1', + 'mssqlResult' => '.1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield ' 0.10' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT 0.10 FROM %s t', + 'mysql' => new ConstantStringType('0.10'), + 'sqlite' => new ConstantFloatType(0.1), + 'pdo_pgsql' => new ConstantStringType('0.10'), + 'pgsql' => new ConstantStringType('0.10'), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.10', + 'sqliteResult' => 0.1, + 'pdoPgsqlResult' => '0.10', + 'pgsqlResult' => '0.10', + 'mssqlResult' => '.10', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '0.125e0' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT 0.125e0 FROM %s t', + 'mysql' => new ConstantFloatType(0.125), + 'sqlite' => new ConstantFloatType(0.125), + 'pdo_pgsql' => new ConstantStringType('0.125'), + 'pgsql' => new ConstantStringType('0.125'), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => '0.125', + 'pgsqlResult' => '0.125', + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield ' 1e0' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT 1e0 FROM %s t', + 'mysql' => new ConstantFloatType(1.0), + 'sqlite' => new ConstantFloatType(1.0), + 'pdo_pgsql' => new ConstantStringType('1'), + 'pgsql' => new ConstantStringType('1'), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield " '1'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT '1' FROM %s t", + 'mysql' => new ConstantStringType('1'), + 'sqlite' => new ConstantStringType('1'), + 'pdo_pgsql' => new ConstantStringType('1'), + 'pgsql' => new ConstantStringType('1'), + 'mssql' => self::mixed(), + 'mysqlResult' => '1', + 'sqliteResult' => '1', + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => '1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield " '1e0'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT '1e0' FROM %s t", + 'mysql' => new ConstantStringType('1e0'), + 'sqlite' => new ConstantStringType('1e0'), + 'pdo_pgsql' => new ConstantStringType('1e0'), + 'pgsql' => new ConstantStringType('1e0'), + 'mssql' => self::mixed(), + 'mysqlResult' => '1e0', + 'sqliteResult' => '1e0', + 'pdoPgsqlResult' => '1e0', + 'pgsqlResult' => '1e0', + 'mssqlResult' => '1e0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '1 + 1' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (1 + 1) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 2, + 'sqliteResult' => 2, + 'pdoPgsqlResult' => 2, + 'pgsqlResult' => 2, + 'mssqlResult' => 2, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "1 + 'foo'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT (1 + 'foo') FROM %s t", + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Invalid text representation + 'pgsql' => null, // Invalid text representation + 'mssql' => null, // Conversion failed + 'mysqlResult' => 1.0, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "1 + '1.0'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT (1 + '1.0') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => null, // Invalid text representation + 'pgsql' => null, // Invalid text representation + 'mssql' => null, // Conversion failed + 'mysqlResult' => 2.0, + 'sqliteResult' => 2.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "1 + '1'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT (1 + '1') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 2.0, + 'sqliteResult' => 2, + 'pdoPgsqlResult' => 2, + 'pgsqlResult' => 2, + 'mssqlResult' => 2, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "1 + '1e0'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT (1 + '1e0') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => null, // Invalid text representation + 'pgsql' => null, // Invalid text representation + 'mssql' => null, // Conversion failed + 'mysqlResult' => 2.0, + 'sqliteResult' => 2.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '1 + 1 * 1 - 1' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (1 + 1 * 1 - 1) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '1 + 1 * 1 / 1 - 1' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (1 + 1 * 1 / 1 - 1) FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_int' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int + t.col_int FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 18, + 'sqliteResult' => 18, + 'pdoPgsqlResult' => 18, + 'pgsqlResult' => 18, + 'mssqlResult' => 18, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_bigint + t.col_bigint' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_bigint + t.col_bigint FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 4294967296, + 'sqliteResult' => 4294967296, + 'pdoPgsqlResult' => 4294967296, + 'pgsqlResult' => 4294967296, + 'mssqlResult' => '4294967296', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_float' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int + t.col_float FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 9.125, + 'sqliteResult' => 9.125, + 'pdoPgsqlResult' => '9.125', + 'pgsqlResult' => 9.125, + 'mssqlResult' => 9.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_mixed' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int + t.col_mixed FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => 10, + 'sqliteResult' => 10, + 'pdoPgsqlResult' => 10, + 'pgsqlResult' => 10, + 'mssqlResult' => 10, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_bigint + t.col_float' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_bigint + t.col_float FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 2147483648.125, + 'sqliteResult' => 2147483648.125, + 'pdoPgsqlResult' => '2147483648.125', + 'pgsqlResult' => 2147483648.125, + 'mssqlResult' => 2147483648.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_bigint + t.col_float (int data)' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT t.col_bigint + t.col_float FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 2.0, + 'sqliteResult' => 2.0, + 'pdoPgsqlResult' => '2', + 'pgsqlResult' => 2.0, + 'mssqlResult' => 2.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_float + t.col_float' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_float + t.col_float FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.25, + 'sqliteResult' => 0.25, + 'pdoPgsqlResult' => '0.25', + 'pgsqlResult' => 0.25, + 'mssqlResult' => 0.25, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int + t.col_decimal FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '9.1', + 'sqliteResult' => 9.1, + 'pdoPgsqlResult' => '9.1', + 'pgsqlResult' => '9.1', + 'mssqlResult' => '9.1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_decimal (int data)' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT t.col_int + t.col_decimal FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '2.0', + 'sqliteResult' => 2, + 'pdoPgsqlResult' => '2.0', + 'pgsqlResult' => '2.0', + 'mssqlResult' => '2.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_float + t.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_float + t.col_decimal FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.225, + 'sqliteResult' => 0.225, + 'pdoPgsqlResult' => '0.225', + 'pgsqlResult' => 0.225, + 'mssqlResult' => 0.225, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_float + t.col_decimal (int data)' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT t.col_float + t.col_decimal FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 2.0, + 'sqliteResult' => 2.0, + 'pdoPgsqlResult' => '2', + 'pgsqlResult' => 2.0, + 'mssqlResult' => 2.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal + t.col_decimal (int data)' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '2.0', + 'sqliteResult' => 2, + 'pdoPgsqlResult' => '2.0', + 'pgsqlResult' => '2.0', + 'mssqlResult' => '2.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_float + t.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int + t.col_float + t.col_decimal FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 9.225, + 'sqliteResult' => 9.225, + 'pdoPgsqlResult' => '9.225', + 'pgsqlResult' => 9.225, + 'mssqlResult' => 9.225, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal + t.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.2', + 'sqliteResult' => 0.2, + 'pdoPgsqlResult' => '0.2', + 'pgsqlResult' => '0.2', + 'mssqlResult' => '.2', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_string' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int + t.col_string FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Error converting data type + 'mysqlResult' => 9.0, + 'sqliteResult' => 9, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_string (int data)' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT t.col_int + t.col_string FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => self::mixed(), + 'mysqlResult' => 2.0, + 'sqliteResult' => 2, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 2, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_bool' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int + t.col_bool FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, + 'mssql' => self::mixed(), // Undefined function + 'mysqlResult' => 10, + 'sqliteResult' => 10, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 10, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_float + t.col_string' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_float + t.col_string FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Error converting data type + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal + t.col_bool' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_decimal + t.col_bool FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => self::mixed(), + 'mysqlResult' => '1.1', + 'sqliteResult' => 1.1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => '1.1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal + t.col_string' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_decimal + t.col_string FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Error converting data type + 'mysqlResult' => 0.1, + 'sqliteResult' => 0.1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int + t.col_int_nullable' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int + t.col_int_nullable FROM %s t', + 'mysql' => self::intOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => self::intOrNull(), + 'pgsql' => self::intOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int / t.col_int' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int / t.col_int FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_bigint / t.col_bigint' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_bigint / t.col_bigint FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => '1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int / t.col_float' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int / t.col_float FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 72.0, + 'sqliteResult' => 72.0, + 'pdoPgsqlResult' => '72', + 'pgsqlResult' => 72.0, + 'mssqlResult' => 72.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int / t.col_float / t.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int / t.col_float / t.col_decimal FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 720.0, + 'sqliteResult' => 720.0, + 'pdoPgsqlResult' => '720', + 'pgsqlResult' => 720.0, + 'mssqlResult' => 720.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_bigint / t.col_float' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_bigint / t.col_float FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 17179869184.0, + 'sqliteResult' => 17179869184.0, + 'pdoPgsqlResult' => '17179869184', + 'pgsqlResult' => 17179869184.0, + 'mssqlResult' => 17179869184.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_float / t.col_float' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_float / t.col_float FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int / t.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int / t.col_decimal FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '90.0000', + 'sqliteResult' => 90.0, + 'pdoPgsqlResult' => '90.0000000000000000', + 'pgsqlResult' => '90.0000000000000000', + 'mssqlResult' => '90.000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int / t.col_decimal (int data)' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT t.col_int / t.col_decimal FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => '1.000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_float / t.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_float / t.col_decimal FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.25, + 'sqliteResult' => 1.25, + 'pdoPgsqlResult' => '1.25', + 'pgsqlResult' => 1.25, + 'mssqlResult' => 1.25, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal / t.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.00000', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => '1.000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal / t.col_decimal (int data)' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.00000', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => '1.000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal / t.col_mixed' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_decimal / t.col_mixed FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.10000', + 'sqliteResult' => 0.1, + 'pdoPgsqlResult' => '0.10000000000000000000', + 'pgsqlResult' => '0.10000000000000000000', + 'mssqlResult' => '.100000000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int / t.col_string' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int / t.col_string FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Conversion failed + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int / t.col_string (int data)' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT t.col_int / t.col_string FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_string / t.col_int' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_string / t.col_int FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Conversion failed + 'mysqlResult' => 0.0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int / t.col_bool' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int / t.col_bool FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::int(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => self::mixed(), + 'mysqlResult' => '9.0000', + 'sqliteResult' => 9, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_float / t.col_string' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_float / t.col_string FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Error converting data type + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_string / t.col_float' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_string / t.col_float FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Error converting data type + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal / t.col_bool' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_decimal / t.col_bool FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => self::mixed(), + 'mysqlResult' => '0.10000', + 'sqliteResult' => 0.1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => '.100000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal / t.col_bool (int data)' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT t.col_decimal / t.col_bool FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => self::mixed(), + 'mysqlResult' => '1.00000', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => '1.000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal / t.col_string' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_decimal / t.col_string FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Error converting data type + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_string / t.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_string / t.col_decimal FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Error converting data type + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_int / t.col_int_nullable' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int / t.col_int_nullable FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => self::intOrNull(), + 'pgsql' => self::intOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '1 - 1' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (1 - 1) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '1 * 1' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (1 * 1) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "1 * '1'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT (1 * '1') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "1 * '1.0'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT (1 * '1.0') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => null, // Invalid text representation + 'pgsql' => null, // Invalid text representation + 'mssql' => null, // Conversion failed + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '1 / 1' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (1 / 1) FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '1 / 1.0' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (1 / 1.0) FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => '1.000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '1 / 1e0' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (1 / 1e0) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "'foo' / 1" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT ('foo' / 1) FROM %s t", + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Invalid text representation + 'pgsql' => null, // Invalid text representation + 'mssql' => null, // Conversion failed + 'mysqlResult' => 0.0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "1 / 'foo'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT (1 / 'foo') FROM %s t", + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Invalid text representation + 'pgsql' => null, // Invalid text representation + 'mssql' => null, // Conversion failed + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "1 / '1'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT (1 / '1') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "'1' / 1" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT ('1' / 1) FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "1 / '1.0'" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT (1 / '1.0') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => null, // Invalid text representation + 'pgsql' => null, // Invalid text representation + 'mssql' => null, // Conversion failed + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '2147483648 ' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT 2147483648 FROM %s t', + 'mysql' => new ConstantIntegerType(2147483648), + 'sqlite' => new ConstantIntegerType(2147483648), + 'pdo_pgsql' => new ConstantIntegerType(2147483648), + 'pgsql' => new ConstantIntegerType(2147483648), + 'mssql' => self::mixed(), + 'mysqlResult' => 2147483648, + 'sqliteResult' => 2147483648, + 'pdoPgsqlResult' => 2147483648, + 'pgsqlResult' => 2147483648, + 'mssqlResult' => '2147483648', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "''" => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT \'\' FROM %s t', + 'mysql' => new ConstantStringType(''), + 'sqlite' => new ConstantStringType(''), + 'pdo_pgsql' => new ConstantStringType(''), + 'pgsql' => new ConstantStringType(''), + 'mssql' => self::mixed(), + 'mysqlResult' => '', + 'sqliteResult' => '', + 'pdoPgsqlResult' => '', + 'pgsqlResult' => '', + 'mssqlResult' => '', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '(TRUE)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (TRUE) FROM %s t', + 'mysql' => new ConstantIntegerType(1), + 'sqlite' => new ConstantIntegerType(1), + 'pdo_pgsql' => new ConstantBooleanType(true), + 'pgsql' => new ConstantBooleanType(true), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => true, + 'pgsqlResult' => true, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_PG_BOOL, + ]; + + yield '(FALSE)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT (FALSE) FROM %s t', + 'mysql' => new ConstantIntegerType(0), + 'sqlite' => new ConstantIntegerType(0), + 'pdo_pgsql' => new ConstantBooleanType(false), + 'pgsql' => new ConstantBooleanType(false), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => false, + 'pgsqlResult' => false, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_PG_BOOL, + ]; + + yield 't.col_bool' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_bool FROM %s t', + 'mysql' => self::bool(), + 'sqlite' => self::bool(), + 'pdo_pgsql' => self::bool(), + 'pgsql' => self::bool(), + 'mssql' => self::bool(), + 'mysqlResult' => true, + 'sqliteResult' => true, + 'pdoPgsqlResult' => true, + 'pgsqlResult' => true, + 'mssqlResult' => true, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 't.col_bool_nullable' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_bool_nullable FROM %s t', + 'mysql' => self::boolOrNull(), + 'sqlite' => self::boolOrNull(), + 'pdo_pgsql' => self::boolOrNull(), + 'pgsql' => self::boolOrNull(), + 'mssql' => self::boolOrNull(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 'COALESCE(t.col_bool, t.col_bool)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_bool, t.col_bool) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::bool(), + 'pgsql' => self::bool(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => true, + 'pgsqlResult' => true, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_PG_BOOL, + ]; + + yield 'COALESCE(t.col_decimal, t.col_decimal) + int data' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT COALESCE(t.col_decimal, t.col_decimal) FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_float, t.col_float) + int data' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT COALESCE(t.col_float, t.col_float) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 't.col_decimal' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_decimal FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::numericString(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::numericString(), + 'mysqlResult' => '0.1', + 'sqliteResult' => '0.1', + 'pdoPgsqlResult' => '0.1', + 'pgsqlResult' => '0.1', + 'mssqlResult' => '.1', + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 't.col_int' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_int FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::int(), + 'mysqlResult' => 9, + 'sqliteResult' => 9, + 'pdoPgsqlResult' => 9, + 'pgsqlResult' => 9, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 't.col_bigint' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_bigint FROM %s t', + 'mysql' => self::hasDbal4() ? self::int() : self::numericString(), + 'sqlite' => self::hasDbal4() ? self::int() : self::numericString(), + 'pdo_pgsql' => self::hasDbal4() ? self::int() : self::numericString(), + 'pgsql' => self::hasDbal4() ? self::int() : self::numericString(), + 'mssql' => self::hasDbal4() ? self::int() : self::numericString(), + 'mysqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'sqliteResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'pdoPgsqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'pgsqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'mssqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 't.col_float' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_float FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::float(), + 'pgsql' => self::float(), + 'mssql' => self::float(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => 0.125, + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 'AVG(t.col_float)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(t.col_float) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::floatOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => '0.125', + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_float) + no data' => [ + 'data' => self::dataNone(), + 'select' => 'SELECT AVG(t.col_float) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::floatOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_float) + GROUP BY' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(t.col_float) FROM %s t GROUP BY t.col_int', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => '0.125', + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_float_nullable) + GROUP BY' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(t.col_float_nullable) FROM %s t GROUP BY t.col_int', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::floatOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_decimal)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(t.col_decimal) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.10000', + 'sqliteResult' => 0.1, + 'pdoPgsqlResult' => '0.10000000000000000000', + 'pgsqlResult' => '0.10000000000000000000', + 'mssqlResult' => '.100000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_decimal) + int data' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT AVG(t.col_decimal) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.00000', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => '1.000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_mixed)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(t.col_mixed) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::floatOrNull(), // always float|null, see https://www.sqlite.org/lang_aggfunc.html + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(t.col_int) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '9.0000', + 'sqliteResult' => 9.0, + 'pdoPgsqlResult' => '9.0000000000000000', + 'pgsqlResult' => '9.0000000000000000', + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_bool)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(t.col_bool) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // perand data type bit is invalid for avg operator. + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_string)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(t.col_string) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type nvarchar is invalid for avg operator + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(1)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(1) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "AVG('1')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT AVG('1') FROM %s t", + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "AVG('1.0')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT AVG('1.0') FROM %s t", + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "AVG('1e0')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT AVG('1e0') FROM %s t", + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "AVG('foo')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT AVG('foo') FROM %s t", + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(1) + GROUP BY' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(1) FROM %s t GROUP BY t.col_int', + 'mysql' => self::numericString(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0000', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(1.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(1.0) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.00000', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => '1.000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(1e0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(1.0) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.00000', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.00000000000000000000', + 'pgsqlResult' => '1.00000000000000000000', + 'mssqlResult' => '1.000000', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'AVG(t.col_bigint)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT AVG(t.col_bigint) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '2147483648.0000', + 'sqliteResult' => 2147483648.0, + 'pdoPgsqlResult' => '2147483648.00000000', + 'pgsqlResult' => '2147483648.00000000', + 'mssqlResult' => '2147483648', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_float)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(t.col_float) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::floatOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => '0.125', + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_float) + no data' => [ + 'data' => self::dataNone(), + 'select' => 'SELECT SUM(t.col_float) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::floatOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_float) + GROUP BY' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(t.col_float) FROM %s t GROUP BY t.col_int', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => '0.125', + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '1 + -(CASE WHEN MIN(t.col_float) = 0 THEN SUM(t.col_float) ELSE 0 END)' => [ // agg function (causing null) deeply inside AST + 'data' => self::dataDefault(), + 'select' => 'SELECT 1 + -(CASE WHEN MIN(t.col_float) = 0 THEN SUM(t.col_float) ELSE 0 END) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrIntOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::floatOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_decimal)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(t.col_decimal) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrIntOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.1', + 'sqliteResult' => 0.1, + 'pdoPgsqlResult' => '0.1', + 'pgsqlResult' => '0.1', + 'mssqlResult' => '.1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_decimal) + int data' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT SUM(t.col_decimal) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrIntOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(t.col_int) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'mssql' => self::mixed(), + 'mysqlResult' => '9', + 'sqliteResult' => 9, + 'pdoPgsqlResult' => 9, + 'pgsqlResult' => 9, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '-SUM(t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT -SUM(t.col_int) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'mssql' => self::mixed(), + 'mysqlResult' => '-9', + 'sqliteResult' => -9, + 'pdoPgsqlResult' => -9, + 'pgsqlResult' => -9, + 'mssqlResult' => -9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '-SUM(t.col_int) + no data' => [ + 'data' => self::dataNone(), + 'select' => 'SELECT -SUM(t.col_int) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_mixed)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(t.col_mixed) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_bool)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(t.col_bool) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => '1', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_string)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(t.col_string) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "SUM('foo')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT SUM('foo') FROM %s t", + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "SUM('1')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT SUM('1') FROM %s t", + 'mysql' => self::floatOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 1.0, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "SUM('1.0')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT SUM('1.0') FROM %s t", + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "SUM('1.1')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT SUM('1.1') FROM %s t", + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 1.1, + 'sqliteResult' => 1.1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(1)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(1) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'mssql' => self::mixed(), + 'mysqlResult' => '1', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(1) + GROUP BY' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(1) FROM %s t GROUP BY t.col_int', + 'mysql' => self::numericString(), + 'sqlite' => self::int(), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'mssql' => self::mixed(), + 'mysqlResult' => '1', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(1.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(1.0) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(1e0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(1e0) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SUM(t.col_bigint)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT SUM(t.col_bigint) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'mssql' => self::mixed(), + 'mysqlResult' => '2147483648', + 'sqliteResult' => 2147483648, + 'pdoPgsqlResult' => '2147483648', + 'pgsqlResult' => '2147483648', + 'mssqlResult' => '2147483648', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_float)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(t.col_float) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::floatOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => '0.125', + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_float) + no data' => [ + 'data' => self::dataNone(), + 'select' => 'SELECT MAX(t.col_float) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::floatOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_float) + GROUP BY' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(t.col_float) FROM %s t GROUP BY t.col_int', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => '0.125', + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_decimal)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(t.col_decimal) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrIntOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.1', + 'sqliteResult' => 0.1, + 'pdoPgsqlResult' => '0.1', + 'pgsqlResult' => '0.1', + 'mssqlResult' => '.1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_decimal) + int data' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT MAX(t.col_decimal) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrIntOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(t.col_int) FROM %s t', + 'mysql' => self::intOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => self::intOrNull(), + 'pgsql' => self::intOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 9, + 'sqliteResult' => 9, + 'pdoPgsqlResult' => 9, + 'pgsqlResult' => 9, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_mixed)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(t.col_mixed) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_bool)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(t.col_bool) FROM %s t', + 'mysql' => self::intOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_string)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(t.col_string) FROM %s t', + 'mysql' => self::stringOrNull(), + 'sqlite' => self::stringOrNull(), + 'pdo_pgsql' => self::stringOrNull(), + 'pgsql' => self::stringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 'foobar', + 'sqliteResult' => 'foobar', + 'pdoPgsqlResult' => 'foobar', + 'pgsqlResult' => 'foobar', + 'mssqlResult' => 'foobar', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "MAX('foobar')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT MAX('foobar') FROM %s t", + 'mysql' => TypeCombinator::addNull(self::string()), + 'sqlite' => TypeCombinator::addNull(self::string()), + 'pdo_pgsql' => TypeCombinator::addNull(self::string()), + 'pgsql' => TypeCombinator::addNull(self::string()), + 'mssql' => self::mixed(), + 'mysqlResult' => 'foobar', + 'sqliteResult' => 'foobar', + 'pdoPgsqlResult' => 'foobar', + 'pgsqlResult' => 'foobar', + 'mssqlResult' => 'foobar', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "MAX('1')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT MAX('1') FROM %s t", + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1', + 'sqliteResult' => '1', + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => '1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "MAX('1.0')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT MAX('1.0') FROM %s t", + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => '1.0', + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(1)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(1) FROM %s t', + 'mysql' => self::intOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => self::intOrNull(), + 'pgsql' => self::intOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(1) + GROUP BY' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(1) FROM %s t GROUP BY t.col_int', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(1.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(1.0) FROM %s t', + 'mysql' => self::numericStringOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(1e0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(1e0) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::numericStringOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MAX(t.col_bigint)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MAX(t.col_bigint) FROM %s t', + 'mysql' => self::intOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => self::intOrNull(), + 'pgsql' => self::intOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 2147483648, + 'sqliteResult' => 2147483648, + 'pdoPgsqlResult' => 2147483648, + 'pgsqlResult' => 2147483648, + 'mssqlResult' => '2147483648', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_float)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(t.col_float) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => '0.125', + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_decimal)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(t.col_decimal) FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.1', + 'sqliteResult' => 0.1, + 'pdoPgsqlResult' => '0.1', + 'pgsqlResult' => '0.1', + 'mssqlResult' => '.1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_decimal) + int data' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT ABS(t.col_decimal) FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::floatOrInt(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(t.col_int) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 9, + 'sqliteResult' => 9, + 'pdoPgsqlResult' => 9, + 'pgsqlResult' => 9, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield '-ABS(t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT -ABS(t.col_int) FROM %s t', + 'mysql' => IntegerRangeType::fromInterval(null, 0), + 'sqlite' => IntegerRangeType::fromInterval(null, 0), + 'pdo_pgsql' => IntegerRangeType::fromInterval(null, 0), + 'pgsql' => IntegerRangeType::fromInterval(null, 0), + 'mssql' => self::mixed(), + 'mysqlResult' => -9, + 'sqliteResult' => -9, + 'pdoPgsqlResult' => -9, + 'pgsqlResult' => -9, + 'mssqlResult' => -9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_int_nullable)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(t.col_int_nullable) FROM %s t', + 'mysql' => self::intNonNegativeOrNull(), + 'sqlite' => self::intNonNegativeOrNull(), + 'pdo_pgsql' => self::intNonNegativeOrNull(), + 'pgsql' => self::intNonNegativeOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_string)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(t.col_string) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // Operand data type is invalid + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_string) + int data' => [ + 'data' => self::dataAllIntLike(), + 'select' => 'SELECT ABS(t.col_string) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_bool)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(t.col_bool) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(-1)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(-1) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(1)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(1) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(1.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(1.0) FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(1e0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(1e0) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "ABS('1.0')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT ABS('1.0') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "ABS('1')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT ABS('1') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_bigint)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(t.col_bigint) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 2147483648, + 'sqliteResult' => 2147483648, + 'pdoPgsqlResult' => 2147483648, + 'pgsqlResult' => 2147483648, + 'mssqlResult' => '2147483648', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'ABS(t.col_mixed)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT ABS(t.col_mixed) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_int, 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_int, 0) FROM %s t', + 'mysql' => self::intNonNegativeOrNull(), + 'sqlite' => self::intNonNegativeOrNull(), + 'pdo_pgsql' => null, + 'pgsql' => null, + 'mssql' => null, // Divide by zero error encountered. + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_int, 1)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_int, 1) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_mixed, 1)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_mixed, 1) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "MOD(t.col_int, '1')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT MOD(t.col_int, '1') FROM %s t", + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "MOD(t.col_int, '1.0')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT MOD(t.col_int, '1') FROM %s t", + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_int, t.col_float)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_int, t.col_float) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // The data types are incompatible in the modulo operator. + 'mysqlResult' => 0.0, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_int, t.col_decimal)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_int, t.col_decimal) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.0', + 'sqliteResult' => null, + 'pdoPgsqlResult' => '0.0', + 'pgsqlResult' => '0.0', + 'mssqlResult' => '.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_float, t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_float, t.col_int) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // The data types are incompatible in the modulo operator. + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_decimal, t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_decimal, t.col_int) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.1', + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => '0.1', + 'pgsqlResult' => '0.1', + 'mssqlResult' => '.1', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_string, t.col_string)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_string, t.col_string) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => null, // Undefined function + 'pgsql' => null, // Undefined function + 'mssql' => null, // The data types are incompatible in the modulo operator. + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_int, t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_int, t.col_int) FROM %s t', + 'mysql' => self::intNonNegativeOrNull(), + 'sqlite' => self::intNonNegativeOrNull(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_int, t.col_int_nullable)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_int, t.col_int_nullable) FROM %s t', + 'mysql' => self::intNonNegativeOrNull(), + 'sqlite' => self::intNonNegativeOrNull(), + 'pdo_pgsql' => self::intNonNegativeOrNull(), + 'pgsql' => self::intNonNegativeOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(10, 7)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(10, 7) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 3, + 'sqliteResult' => 3, + 'pdoPgsqlResult' => 3, + 'pgsqlResult' => 3, + 'mssqlResult' => 3, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(10, -7)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(10, -7) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 3, + 'sqliteResult' => 3, + 'pdoPgsqlResult' => 3, + 'pgsqlResult' => 3, + 'mssqlResult' => 3, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'MOD(t.col_bigint, t.col_bigint)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT MOD(t.col_bigint, t.col_bigint) FROM %s t', + 'mysql' => self::intNonNegativeOrNull(), + 'sqlite' => self::intNonNegativeOrNull(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => '0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'BIT_AND(t.col_bigint, t.col_bigint)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT BIT_AND(t.col_bigint, t.col_bigint) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 2147483648, + 'sqliteResult' => 2147483648, + 'pdoPgsqlResult' => 2147483648, + 'pgsqlResult' => 2147483648, + 'mssqlResult' => '2147483648', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'BIT_AND(t.col_int, t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT BIT_AND(t.col_int, t.col_int) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 9, + 'sqliteResult' => 9, + 'pdoPgsqlResult' => 9, + 'pgsqlResult' => 9, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'BIT_AND(t.col_mixed, t.col_mixed)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT BIT_AND(t.col_mixed, t.col_mixed) FROM %s t', + 'mysql' => self::intNonNegativeOrNull(), + 'sqlite' => self::intNonNegativeOrNull(), + 'pdo_pgsql' => self::intNonNegativeOrNull(), + 'pgsql' => self::intNonNegativeOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'BIT_AND(t.col_int, t.col_int_nullable)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT BIT_AND(t.col_int, t.col_int_nullable) FROM %s t', + 'mysql' => self::intNonNegativeOrNull(), + 'sqlite' => self::intNonNegativeOrNull(), + 'pdo_pgsql' => self::intNonNegativeOrNull(), + 'pgsql' => self::intNonNegativeOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'BIT_AND(1, 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT BIT_AND(1, 0) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'BIT_AND(t.col_string, t.col_string)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT BIT_AND(t.col_string, t.col_string) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => null, + 'pgsql' => null, + 'mssql' => null, + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'DATE_DIFF(CURRENT_DATE(), CURRENT_DATE())' => [ + 'data' => self::dataDefault(), + 'select' => "SELECT DATE_DIFF('2024-01-01 12:00', '2024-01-01 11:00') FROM %s t", + 'mysql' => self::int(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'DATE_DIFF(CURRENT_DATE(), t.col_string_nullable)' => [ + 'data' => self::dataDefault(), + 'select' => "SELECT DATE_DIFF('2024-01-01 12:00', t.col_string_nullable) FROM %s t", + 'mysql' => self::intOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::intOrNull(), + 'pgsql' => self::intOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'DATE_DIFF(CURRENT_DATE(), t.col_mixed)' => [ + 'data' => self::dataDefault(), + 'select' => "SELECT DATE_DIFF('2024-01-01 12:00', t.col_mixed) FROM %s t", + 'mysql' => self::intOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, + 'pgsql' => null, + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => 2460310.0, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 45289, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SQRT(t.col_float)' => [ + 'data' => self::dataSqrt(), + 'select' => 'SELECT SQRT(t.col_float) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SQRT(t.col_decimal)' => [ + 'data' => self::dataSqrt(), + 'select' => 'SELECT SQRT(t.col_decimal) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.000000000000000', + 'pgsqlResult' => '1.000000000000000', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SQRT(t.col_int)' => [ + 'data' => self::dataSqrt(), + 'select' => 'SELECT SQRT(t.col_int) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 3.0, + 'sqliteResult' => 3.0, + 'pdoPgsqlResult' => '3', + 'pgsqlResult' => 3.0, + 'mssqlResult' => 3.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SQRT(t.col_mixed)' => [ + 'data' => self::dataSqrt(), + 'select' => 'SELECT SQRT(t.col_mixed) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SQRT(t.col_int_nullable)' => [ + 'data' => self::dataSqrt(), + 'select' => 'SELECT SQRT(t.col_int_nullable) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => PHP_VERSION_ID >= 80100 && !self::hasDbal4() ? null : self::floatOrNull(), // fails in UDF since PHP 8.1: sqrt(): Passing null to parameter #1 ($num) of type float is deprecated + 'pdo_pgsql' => self::numericStringOrNull(), + 'pgsql' => self::floatOrNull(), + 'mssql' => self::mixed(), + 'mysqlResult' => null, + 'sqliteResult' => self::hasDbal4() ? null : 0.0, // 0.0 caused by UDF wired through PHP's sqrt() which returns 0.0 for null + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SQRT(-1)' => [ + 'data' => self::dataSqrt(), + 'select' => 'SELECT SQRT(-1) FROM %s t', + 'mysql' => self::floatOrNull(), + 'sqlite' => self::floatOrNull(), + 'pdo_pgsql' => null, // failure: cannot take square root of a negative number + 'pgsql' => null, // failure: cannot take square root of a negative number + 'mssql' => null, // An invalid floating point operation occurred. + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SQRT(1)' => [ + 'data' => self::dataSqrt(), + 'select' => 'SELECT SQRT(1) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "SQRT('1')" => [ + 'data' => self::dataSqrt(), + 'select' => "SELECT SQRT('1') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "SQRT('1.0')" => [ + 'data' => self::dataSqrt(), + 'select' => "SELECT SQRT('1.0') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "SQRT('1e0')" => [ + 'data' => self::dataSqrt(), + 'select' => "SELECT SQRT('1e0') FROM %s t", + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "SQRT('foo')" => [ + 'data' => self::dataSqrt(), + 'select' => "SELECT SQRT('foo') FROM %s t", + 'mysql' => self::mixed(), + 'sqlite' => self::hasDbal4() ? self::mixed() : null, // fails in UDF: sqrt(): Argument #1 ($num) must be of type float, string given + 'pdo_pgsql' => null, // Invalid text representation + 'pgsql' => null, // Invalid text representation + 'mssql' => null, // Error converting data type + 'mysqlResult' => 0.0, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SQRT(t.col_string)' => [ + 'data' => self::dataSqrt(), + 'select' => 'SELECT SQRT(t.col_string) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::hasDbal4() ? self::mixed() : null, // fails in UDF: sqrt(): Argument #1 ($num) must be of type float, string given + 'pdo_pgsql' => null, // undefined function + 'pgsql' => null, // undefined function + 'mssql' => null, // Error converting data type + 'mysqlResult' => 0.0, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'SQRT(1.0)' => [ + 'data' => self::dataSqrt(), + 'select' => 'SELECT SQRT(1.0) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.000000000000000', + 'pgsqlResult' => '1.000000000000000', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COUNT(t)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COUNT(t) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::intNonNegative(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 'COUNT(t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COUNT(t.col_int) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::intNonNegative(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 'COUNT(t.col_mixed)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COUNT(t.col_mixed) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::intNonNegative(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 'COUNT(1)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COUNT(1) FROM %s t', + 'mysql' => self::intNonNegative(), + 'sqlite' => self::intNonNegative(), + 'pdo_pgsql' => self::intNonNegative(), + 'pgsql' => self::intNonNegative(), + 'mssql' => self::intNonNegative(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 't.col_mixed' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t.col_mixed FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 'INT_PI()' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT INT_PI() FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::int(), + 'mysqlResult' => 3, + 'sqliteResult' => 3, + 'pdoPgsqlResult' => 3, + 'pgsqlResult' => 3, + 'mssqlResult' => 3, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 'BOOL_PI()' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT BOOL_PI() FROM %s t', + 'mysql' => self::bool(), + 'sqlite' => self::bool(), + 'pdo_pgsql' => self::bool(), + 'pgsql' => self::bool(), + 'mssql' => self::bool(), + 'mysqlResult' => true, + 'sqliteResult' => true, + 'pdoPgsqlResult' => true, + 'pgsqlResult' => true, + 'mssqlResult' => true, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 'STRING_PI()' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT STRING_PI() FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => '3.14159', + 'sqliteResult' => 3.14159, + 'pdoPgsqlResult' => '3.14159', + 'pgsqlResult' => '3.14159', + 'mssqlResult' => '3.14159', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_datetime, t.col_datetime)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_datetime, t.col_datetime) FROM %s t', + 'mysql' => self::string(), + 'sqlite' => self::string(), + 'pdo_pgsql' => self::string(), + 'pgsql' => self::string(), + 'mssql' => self::mixed(), + 'mysqlResult' => '2024-01-31 12:59:59', + 'sqliteResult' => '2024-01-31 12:59:59', + 'pdoPgsqlResult' => '2024-01-31 12:59:59', + 'pgsqlResult' => '2024-01-31 12:59:59', + 'mssqlResult' => '2024-01-31 12:59:59.000000', // doctrine/dbal changes default ReturnDatesAsStrings to true + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(SUM(t.col_int_nullable), 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(SUM(t.col_int_nullable), 0) FROM %s t', + 'mysql' => TypeCombinator::union(self::numericString(), self::int()), + 'sqlite' => self::int(), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'mssql' => self::mixed(), + 'mysqlResult' => '0', + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(SUM(ABS(t.col_int)), 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(SUM(ABS(t.col_int)), 0) FROM %s t', + 'mysql' => TypeCombinator::union(self::int(), self::numericString()), + 'sqlite' => self::int(), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'mssql' => self::mixed(), + 'mysqlResult' => '9', + 'sqliteResult' => 9, + 'pdoPgsqlResult' => 9, + 'pgsqlResult' => 9, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "COALESCE(t.col_int_nullable, 'foo')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT COALESCE(t.col_int_nullable, 'foo') FROM %s t", + 'mysql' => TypeCombinator::union(self::int(), self::string()), + 'sqlite' => TypeCombinator::union(self::int(), self::string()), + 'pdo_pgsql' => null, // COALESCE types cannot be matched + 'pgsql' => null, // COALESCE types cannot be matched + 'mssql' => null, // Conversion failed + 'mysqlResult' => 'foo', + 'sqliteResult' => 'foo', + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "COALESCE(t.col_int, 'foo')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT COALESCE(t.col_int, 'foo') FROM %s t", + 'mysql' => TypeCombinator::union(self::int(), self::string()), + 'sqlite' => TypeCombinator::union(self::int(), self::string()), + 'pdo_pgsql' => null, // COALESCE types cannot be matched + 'pgsql' => null, // COALESCE types cannot be matched + 'mssql' => self::mixed(), + 'mysqlResult' => '9', + 'sqliteResult' => 9, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "COALESCE(t.col_bool, 'foo')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT COALESCE(t.col_bool, 'foo') FROM %s t", + 'mysql' => TypeCombinator::union(self::int(), self::string()), + 'sqlite' => TypeCombinator::union(self::int(), self::string()), + 'pdo_pgsql' => null, // COALESCE types cannot be matched + 'pgsql' => null, // COALESCE types cannot be matched + 'mssql' => self::mixed(), + 'mysqlResult' => '1', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "COALESCE(1, 'foo')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT COALESCE(1, 'foo') FROM %s t", + 'mysql' => TypeCombinator::union(self::int(), self::string()), + 'sqlite' => TypeCombinator::union(self::int(), self::string()), + 'pdo_pgsql' => null, // COALESCE types cannot be matched + 'pgsql' => null, // COALESCE types cannot be matched + 'mssql' => self::mixed(), + 'mysqlResult' => '1', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_int_nullable, 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_int_nullable, 0) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_float_nullable, 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_float_nullable, 0) FROM %s t', + 'mysql' => TypeCombinator::union(self::float(), self::int()), + 'sqlite' => TypeCombinator::union(self::float(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pgsql' => TypeCombinator::union(self::float(), self::int()), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => '0', + 'pgsqlResult' => 0.0, + 'mssqlResult' => 0.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_float_nullable, 0.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_float_nullable, 0.0) FROM %s t', + 'mysql' => TypeCombinator::union(self::float(), self::numericString()), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => TypeCombinator::union(self::float(), self::numericString()), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => '0', + 'pgsqlResult' => 0.0, + 'mssqlResult' => 0.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, 0) FROM %s t', + 'mysql' => TypeCombinator::union(self::numericString(), self::int()), + 'sqlite' => TypeCombinator::union(self::float(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'mssql' => self::mixed(), + 'mysqlResult' => '0.0', + 'sqliteResult' => 0, + 'pdoPgsqlResult' => '0', + 'pgsqlResult' => '0', + 'mssqlResult' => '.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0) FROM %s t', + 'mysql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'sqlite' => TypeCombinator::union(self::float(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => '0', + 'pgsqlResult' => 0.0, + 'mssqlResult' => 0.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_string)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_string) FROM %s t', + 'mysql' => TypeCombinator::union(self::string(), self::int(), self::float()), + 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::string()), + 'pdo_pgsql' => null, // COALESCE types cannot be matched + 'pgsql' => null, // COALESCE types cannot be matched + 'mssql' => null, // Error converting data + 'mysqlResult' => 'foobar', + 'sqliteResult' => 'foobar', + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'IDENTITY(t.related_entity)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT IDENTITY(t.related_entity) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + } + + /** + * @param mixed $expectedFirstResult + * @param array $data + * @param self::STRINGIFY_* $stringification + * @param self::INVALID_*|null $invalidConnectionSetup */ - public function testFetchedTypes( - array $connectionParams, - array $expectedOnPhp80AndBelow, - array $expectedOnPhp81AndAbove, - array $connectionAttributes + private function performDriverTest( + string $driver, + string $configName, + array $data, + string $dqlTemplate, + string $dataset, + int $phpVersion, + ?Type $expectedInferredType, + $expectedFirstResult, + string $stringification, + ?string $invalidConnectionSetup = null ): void { - $phpVersion = PHP_MAJOR_VERSION * 10 + PHP_MINOR_VERSION; + $connectionParams = [ + 'driver' => $driver, + 'driverOptions' => self::CONNECTION_CONFIGS[$configName], + ] + $this->getConnectionParamsForDriver($driver); - try { - $connection = DriverManager::getConnection($connectionParams + [ - 'user' => 'root', - 'password' => 'secret', - 'dbname' => 'foo', - ]); + $dql = sprintf($dqlTemplate, PlatformEntity::class); + + $connection = $this->createConnection($connectionParams); + $query = $this->getQuery($connection, $dql, $data); + $sql = $query->getSQL(); - $nativeConnection = $this->getNativeConnection($connection); - $this->setupAttributes($nativeConnection, $connectionAttributes); + self::assertIsString($sql); - $config = new Configuration(); - $config->setProxyNamespace('PHPstan\Doctrine\OrmMatrixProxies'); - $config->setProxyDir('/tmp/doctrine'); - $config->setAutoGenerateProxyClasses(false); - $config->setSecondLevelCacheEnabled(false); - $config->setMetadataCache(new ArrayCachePool()); + try { + $result = $query->getSingleResult(); + $realResultType = ConstantTypeHelper::getTypeFromValue($result); - if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/orm', '3.*')) { - $config->setMetadataDriverImpl(new AttributeDriver([__DIR__ . '/Entity'])); + if ($invalidConnectionSetup !== null) { + $inferredType = $this->getInferredType($this->cloneQueryAndInjectInvalidConnection($query, $driver, $invalidConnectionSetup), false); } else { - $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), [__DIR__ . '/Entity'])); + $inferredType = $this->getInferredType($query, true); } - $entityManager = new EntityManager($connection, $config); - - } catch (DbalException $e) { - if (strpos($e->getMessage(), 'Doctrine currently supports only the following drivers') !== false) { - self::markTestSkipped($e->getMessage()); // older doctrine versions, needed for old PHP versions + } catch (Throwable $e) { + if ($expectedInferredType === null) { + return; } throw $e; + } finally { + $connection->close(); + } + + if ($expectedInferredType === null) { + self::fail(sprintf( + "Expected failure, but none occurred\n\nDriver: %s\nConfig: %s\nDataset: %s\nDQL: %s\nSQL: %s\nReal result: %s\nInferred type: %s\n", + $driver, + $configName, + $dataset, + $dql, + $sql, + $realResultType->describe(VerbosityLevel::precise()), + $inferredType->describe(VerbosityLevel::precise()) + )); + } + + $driverDetector = new DriverDetector(true); + $driverType = $driverDetector->detect($query->getEntityManager()->getConnection()); + + $stringify = $this->shouldStringify($stringification, $driverType, $phpVersion, $configName); + if ( + $stringify + && $invalidConnectionSetup === null // do not stringify, we already passed union with stringified one above + ) { + $expectedInferredType = self::stringifyType($expectedInferredType); + } + + $this->assertRealResultMatchesExpected($result, $expectedFirstResult, $driver, $configName, $dql, $sql, $dataset, $phpVersion, $stringify); + $this->assertRealResultMatchesInferred($result, $driver, $configName, $dql, $sql, $dataset, $phpVersion, $inferredType, $realResultType); + $this->assertInferredResultMatchesExpected($result, $driver, $configName, $dql, $sql, $dataset, $phpVersion, $inferredType, $expectedInferredType); + } + + /** + * @param array $connectionParams + */ + private function createConnection( + array $connectionParams + ): Connection + { + $connectionConfig = new DbalConfiguration(); + $connectionConfig->setMiddlewares([ + new Middleware($this->createMock(LoggerInterface::class)), // ensures DriverType fallback detection is tested + ]); + $connection = DriverManager::getConnection($connectionParams, $connectionConfig); + + $schemaManager = method_exists($connection, 'createSchemaManager') + ? $connection->createSchemaManager() + : $connection->getSchemaManager(); + + if (!isset($connectionParams['dbname'])) { + if (!in_array('foo', $schemaManager->listDatabases(), true)) { + $connection->executeQuery('CREATE DATABASE foo'); + } + $connection->executeQuery('USE foo'); + } + + if ($connectionParams['driver'] === 'pdo_mysql') { + $connection->executeQuery('SET GLOBAL max_connections = 1000'); + } + + return $connection; + } + + /** + * @param array $data + * @return Query $query + */ + private function getQuery( + Connection $connection, + string $dqlTemplate, + array $data + ): Query + { + if (!DbalType::hasType(MixedCustomType::NAME)) { + DbalType::addType(MixedCustomType::NAME, MixedCustomType::class); } + $config = $this->createOrmConfig(); + $entityManager = new EntityManager($connection, $config); + $schemaTool = new SchemaTool($entityManager); $classes = $entityManager->getMetadataFactory()->getAllMetadata(); $schemaTool->dropSchema($classes); $schemaTool->createSchema($classes); - $entity = new PlatformEntity(); - $entity->id = '1'; - $entity->col_bool = true; - $entity->col_float = 0.125; - $entity->col_decimal = '0.1'; - $entity->col_int = 9; - $entity->col_bigint = '2147483648'; - $entity->col_string = 'foobar'; + $relatedEntity = new PlatformRelatedEntity(); + $relatedEntity->id = 1; + $entityManager->persist($relatedEntity); - $entityManager->persist($entity); - $entityManager->flush(); + foreach ($data as $rowData) { + $entity = new PlatformEntity(); + $entity->related_entity = $relatedEntity; - $columnsQueryTemplate = 'SELECT %s FROM %s t GROUP BY t.col_int, t.col_float, t.col_decimal, t.col_bigint, t.col_bool, t.col_string'; + foreach ($rowData as $column => $value) { + $entity->$column = $value; // @phpstan-ignore-line Intentionally dynamic + } + $entityManager->persist($entity); + } - $expected = $phpVersion >= 81 - ? $expectedOnPhp81AndAbove - : $expectedOnPhp80AndBelow; + $entityManager->flush(); - foreach ($expected as $select => $expectedType) { - if ($expectedType === null) { - continue; // e.g. no such function - } - $dql = sprintf($columnsQueryTemplate, $select, PlatformEntity::class); + $dql = sprintf($dqlTemplate, PlatformEntity::class); - $query = $entityManager->createQuery($dql); - $result = $query->getSingleResult(); + return $entityManager->createQuery($dql); + } + + /** + * @param Query $query + */ + private function getInferredType(Query $query, bool $failOnInvalidConnection): Type + { + $typeBuilder = new QueryResultTypeBuilder(); + $phpVersion = new PhpVersion(PHP_VERSION_ID); // @phpstan-ignore-line ctor not in bc promise + QueryResultTypeWalker::walk( + $query, + $typeBuilder, + self::getContainer()->getByType(DescriptorRegistry::class), + $phpVersion, + new DriverDetector($failOnInvalidConnection) + ); - $typeBuilder = new QueryResultTypeBuilder(); - QueryResultTypeWalker::walk($query, $typeBuilder, self::getContainer()->getByType(DescriptorRegistry::class)); + return $typeBuilder->getResultType(); + } - $inferredPhpStanType = $typeBuilder->getResultType(); - $realRowPhpStanType = ConstantTypeHelper::getTypeFromValue($result); + /** + * @param mixed $realResult + * @param mixed $expectedFirstResult + */ + private function assertRealResultMatchesExpected( + $realResult, + $expectedFirstResult, + string $driver, + string $configName, + string $dql, + string $sql, + string $dataset, + int $phpVersion, + bool $stringified + ): void + { + $humanReadablePhpVersion = $this->getHumanReadablePhpVersion($phpVersion); - $firstResult = reset($result); - $resultType = gettype($firstResult); - $resultExported = var_export($firstResult, true); + $firstResult = reset($realResult); + $realFirstResult = var_export($firstResult, true); + $expectedFirstResultExported = var_export($expectedFirstResult, true); - self::assertTrue( - $inferredPhpStanType->accepts($realRowPhpStanType, true)->yes(), - sprintf( - "Result of 'SELECT %s' for '%s' and PHP %s was inferred as %s, but the real result was %s", - $select, - $this->dataName(), - $phpVersion, - $inferredPhpStanType->describe(VerbosityLevel::precise()), - $realRowPhpStanType->describe(VerbosityLevel::precise()) - ) - ); + $is = $stringified + ? new IsEqual($expectedFirstResult) // loose comparison for stringified + : new IsIdentical($expectedFirstResult); - self::assertThat( + if ($stringified && $firstResult !== null) { + self::assertIsString( $firstResult, - new IsType($expectedType), sprintf( - "Result of 'SELECT %s' for '%s' and PHP %s is expected to be %s, but %s returned (%s).", - $select, - $this->dataName(), - $phpVersion, - $expectedType, - $resultType, - $resultExported + "Stringified result returned non-string\n\nDriver: %s\nConfig: %s\nDataset: %s\nDQL: %s\nPHP: %s\nReal first item: %s\n", + $driver, + $configName, + $dataset, + $dql, + $humanReadablePhpVersion, + $realFirstResult ) ); } + + self::assertThat( + $firstResult, + $is, + sprintf( + "Mismatch between expected result and fetched result\n\nDriver: %s\nConfig: %s\nDataset: %s\nDQL: %s\nSQL: %s\nPHP: %s\nReal first item: %s\nExpected first item: %s\n", + $driver, + $configName, + $dataset, + $dql, + $sql, + $humanReadablePhpVersion, + $realFirstResult, + $expectedFirstResultExported + ) + ); } /** - * @return iterable + * @param mixed $realResult */ - public function provideCases(): iterable - { - // Preserve space-driven formatting for better readability - // phpcs:disable Squiz.WhiteSpace.OperatorSpacing.SpacingBefore - // phpcs:disable Squiz.WhiteSpace.OperatorSpacing.SpacingAfter - - // Notes: - // - Any direct column fetch uses the type declared in entity, but when passed to a function, the driver decides the type - - $testData = [ // mysql, sqlite, pdo_pgsql, pgsql, stringified, stringifiedOldPostgre - // bool-ish - '(TRUE)' => ['int', 'int', 'bool', 'bool', 'string', 'bool'], - 't.col_bool' => ['bool', 'bool', 'bool', 'bool', 'bool', 'bool'], - 'COALESCE(t.col_bool, TRUE)' => ['int', 'int', 'bool', 'bool', 'string', 'bool'], - - // float-ish - 't.col_float' => ['float', 'float', 'float', 'float', 'float', 'float'], - 'AVG(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], - 'SUM(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], - 'MIN(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], - 'MAX(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], - 'SQRT(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], - 'ABS(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'], - - // decimal-ish - 't.col_decimal' => ['string', 'string', 'string', 'string', 'string', 'string'], - '0.1' => ['string', 'float', 'string', 'string', 'string', 'string'], - '0.125e0' => ['float', 'float', 'string', 'string', 'string', 'string'], - 'AVG(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], - 'AVG(t.col_int)' => ['string', 'float', 'string', 'string', 'string', 'string'], - 'AVG(t.col_bigint)' => ['string', 'float', 'string', 'string', 'string', 'string'], - 'SUM(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], - 'MIN(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], - 'MAX(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], - 'SQRT(t.col_decimal)' => ['float', 'float', 'string', 'string', 'string', 'string'], - 'SQRT(t.col_int)' => ['float', 'float', 'string', 'float', 'string', 'string'], - 'SQRT(t.col_bigint)' => ['float', null, 'string', 'float', null, null], // sqlite3 returns float, but pdo_sqlite returns NULL - 'ABS(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], - - // int-ish - '1' => ['int', 'int', 'int', 'int', 'string', 'string'], - '2147483648' => ['int', 'int', 'int', 'int', 'string', 'string'], - 't.col_int' => ['int', 'int', 'int', 'int', 'int', 'int'], - 't.col_bigint' => self::hasDbal4() ? array_fill(0, 6, 'int') : array_fill(0, 6, 'string'), - 'SUM(t.col_int)' => ['string', 'int', 'int', 'int', 'string', 'string'], - 'SUM(t.col_bigint)' => ['string', 'int', 'string', 'string', 'string', 'string'], - "LENGTH('')" => ['int', 'int', 'int', 'int', 'int', 'int'], - 'COUNT(t)' => ['int', 'int', 'int', 'int', 'int', 'int'], - 'COUNT(1)' => ['int', 'int', 'int', 'int', 'int', 'int'], - 'COUNT(t.col_int)' => ['int', 'int', 'int', 'int', 'int', 'int'], - 'MIN(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MIN(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MAX(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MAX(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MOD(t.col_int, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MOD(t.col_bigint, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'ABS(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'ABS(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], - - // string - 't.col_string' => ['string', 'string', 'string', 'string', 'string', 'string'], - 'LOWER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], - 'UPPER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], - 'TRIM(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], - ]; - - $selects = array_keys($testData); - - $nativeMysql = array_combine($selects, array_column($testData, 0)); - $nativeSqlite = array_combine($selects, array_column($testData, 1)); - $nativePdoPg = array_combine($selects, array_column($testData, 2)); - $nativePg = array_combine($selects, array_column($testData, 3)); - - $stringified = array_combine($selects, array_column($testData, 4)); - $stringifiedOldPostgre = array_combine($selects, array_column($testData, 5)); - - yield 'sqlite3' => [ - 'connection' => ['driver' => 'sqlite3', 'memory' => true], - 'php80-' => $nativeSqlite, - 'php81+' => $nativeSqlite, - 'setup' => [], - ]; - - yield 'pdo_sqlite, no stringify' => [ - 'connection' => ['driver' => 'pdo_sqlite', 'memory' => true], - 'php80-' => $stringified, - 'php81+' => $nativeSqlite, - 'setup' => [], - ]; - - yield 'pdo_sqlite, stringify' => [ - 'connection' => ['driver' => 'pdo_sqlite', 'memory' => true], - 'php80-' => $stringified, - 'php81+' => $stringified, - 'setup' => [PDO::ATTR_STRINGIFY_FETCHES => true], - ]; - - yield 'mysqli, no native numbers' => [ - 'connection' => ['driver' => 'mysqli', 'host' => getenv('MYSQL_HOST')], - 'php80-' => $nativeMysql, - 'php81+' => $nativeMysql, - 'setup' => [ - // This has no effect when using prepared statements (which is what doctrine/dbal uses) - // - prepared statements => always native types - // - non-prepared statements => stringified by default, can be changed by MYSQLI_OPT_INT_AND_FLOAT_NATIVE = true - // documented here: https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php#example-4303 - MYSQLI_OPT_INT_AND_FLOAT_NATIVE => false, - ], - ]; + private function assertRealResultMatchesInferred( + $realResult, + string $driver, + string $configName, + string $dql, + string $sql, + string $dataset, + int $phpVersion, + Type $inferredType, + Type $realType + ): void + { + $firstResult = reset($realResult); + $realFirstResult = var_export($firstResult, true); - yield 'mysqli, native numbers' => [ - 'connection' => ['driver' => 'mysqli', 'host' => getenv('MYSQL_HOST')], - 'php80-' => $nativeMysql, - 'php81+' => $nativeMysql, - 'setup' => [MYSQLI_OPT_INT_AND_FLOAT_NATIVE => true], - ]; + self::assertTrue( + $inferredType->accepts($realType, true)->yes(), + sprintf( + "Inferred type does not accept fetched result!\n\nDriver: %s\nConfig: %s\nDataset: %s\nDQL: %s\nSQL: %s\nPHP: %s\nReal first result: %s\nInferred type: %s\nReal type: %s\n", + $driver, + $configName, + $dataset, + $dql, + $sql, + $this->getHumanReadablePhpVersion($phpVersion), + $realFirstResult, + $inferredType->describe(VerbosityLevel::precise()), + $realType->describe(VerbosityLevel::precise()) + ) + ); + } - yield 'pdo_mysql, stringify, no emulate' => [ - 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], - 'php80-' => $stringified, - 'php81+' => $stringified, - 'setup' => [ - PDO::ATTR_EMULATE_PREPARES => false, - PDO::ATTR_STRINGIFY_FETCHES => true, - ], - ]; + /** + * @param mixed $result + */ + private function assertInferredResultMatchesExpected( + $result, + string $driver, + string $configName, + string $dql, + string $sql, + string $dataset, + int $phpVersion, + Type $inferredType, + Type $expectedFirstItemType + ): void + { + $firstResult = reset($result); + $realFirstResult = var_export($firstResult, true); - yield 'pdo_mysql, no stringify, no emulate' => [ - 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], - 'php80-' => $nativeMysql, - 'php81+' => $nativeMysql, - 'setup' => [PDO::ATTR_EMULATE_PREPARES => false], - ]; + self::assertTrue($inferredType->isConstantArray()->yes()); + $inferredFirstItemType = $inferredType->getFirstIterableValueType(); - yield 'pdo_mysql, no stringify, emulate' => [ - 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], - 'php80-' => $stringified, - 'php81+' => $nativeMysql, - 'setup' => [], // defaults - ]; + self::assertTrue( + $inferredFirstItemType->equals($expectedFirstItemType), + sprintf( + "Mismatch between inferred result and expected type\n\nDriver: %s\nConfig: %s\nDataset: %s\nDQL: %s\nSQL: %s\nPHP: %s\nReal first result: %s\nFirst item inferred as: %s\nFirst item expected type: %s\n", + $driver, + $configName, + $dataset, + $dql, + $sql, + $this->getHumanReadablePhpVersion($phpVersion), + $realFirstResult, + $inferredFirstItemType->describe(VerbosityLevel::precise()), + $expectedFirstItemType->describe(VerbosityLevel::precise()) + ) + ); + } - yield 'pdo_mysql, stringify, emulate' => [ - 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], - 'php80-' => $stringified, - 'php81+' => $stringified, - 'setup' => [ - PDO::ATTR_STRINGIFY_FETCHES => true, - ], - ]; + /** + * @return array + */ + private function getConnectionParamsForDriver(string $driver): array + { + switch ($driver) { + case 'pdo_mysql': + case 'mysqli': + return [ + 'host' => getenv('MYSQL_HOST'), + 'user' => 'root', + 'password' => 'secret', + 'dbname' => 'foo', + ]; + case 'pdo_pgsql': + case 'pgsql': + return [ + 'host' => getenv('PGSQL_HOST'), + 'user' => 'root', + 'password' => 'secret', + 'dbname' => 'foo', + ]; + case 'pdo_sqlite': + case 'sqlite3': + return [ + 'memory' => true, + 'dbname' => 'foo', + ]; + case 'pdo_sqlsrv': + case 'sqlsrv': + return [ + 'host' => getenv('MSSQL_HOST'), + 'user' => 'SA', + 'password' => 'Secret.123', + // user database is created after connection + ]; + default: + throw new LogicException('Unknown driver: ' . $driver); + } + } + + private function getSampleServerVersionForDriver(string $driver): string + { + switch ($driver) { + case 'pdo_mysql': + case 'mysqli': + return '8.0.0'; + case 'pdo_pgsql': + case 'pgsql': + return '13.0.0'; + case 'pdo_sqlite': + case 'sqlite3': + return '3.0.0'; + case 'pdo_sqlsrv': + case 'sqlsrv': + return '15.0.0'; + default: + throw new LogicException('Unknown driver: ' . $driver); + } + } + + private static function bool(): Type + { + return new BooleanType(); + } - yield 'pdo_pgsql, stringify' => [ - 'connection' => ['driver' => 'pdo_pgsql', 'host' => getenv('PGSQL_HOST')], + private static function boolOrNull(): Type + { + return TypeCombinator::addNull(new BooleanType()); + } - 'php80-' => $stringifiedOldPostgre, - 'php81+' => $stringified, - 'setup' => [PDO::ATTR_STRINGIFY_FETCHES => true], - ]; + private static function numericString(): Type + { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + + private static function string(): Type + { + return new StringType(); + } + + private static function numericStringOrNull(): Type + { + return TypeCombinator::addNull(new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ])); + } + + private static function int(): Type + { + return new IntegerType(); + } + + private static function intNonNegative(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + + private static function intNonNegativeOrNull(): Type + { + return TypeCombinator::addNull(IntegerRangeType::fromInterval(0, null)); + } + + private static function intOrNull(): Type + { + return TypeCombinator::addNull(new IntegerType()); + } + + private static function stringOrNull(): Type + { + return TypeCombinator::addNull(new StringType()); + } + + private static function float(): Type + { + return new FloatType(); + } + + private static function floatOrInt(): Type + { + return TypeCombinator::union(self::float(), self::int()); + } + + private static function floatOrIntOrNull(): Type + { + return TypeCombinator::addNull(self::floatOrInt()); + } + + private static function mixed(): Type + { + return new MixedType(); + } + + private static function floatOrNull(): Type + { + return TypeCombinator::addNull(new FloatType()); + } + + /** + * @return array> + */ + public static function dataNone(): array + { + return []; + } - yield 'pdo_pgsql, no stringify' => [ - 'connection' => ['driver' => 'pdo_pgsql', 'host' => getenv('PGSQL_HOST')], - 'php80-' => $nativePdoPg, - 'php81+' => $nativePdoPg, - 'setup' => [], + /** + * @return array> + */ + public static function dataDefault(): array + { + return [ + [ + 'id' => '1', + 'col_bool' => true, + 'col_bool_nullable' => null, + 'col_float' => 0.125, + 'col_float_nullable' => null, + 'col_decimal' => '0.1', + 'col_decimal_nullable' => null, + 'col_int' => 9, + 'col_int_nullable' => null, + 'col_bigint' => '2147483648', + 'col_bigint_nullable' => null, + 'col_string' => 'foobar', + 'col_string_nullable' => null, + 'col_mixed' => 1, + 'col_datetime' => new DateTime('2024-01-31 12:59:59'), + ], ]; + } - yield 'pgsql' => [ - 'connection' => ['driver' => 'pgsql', 'host' => getenv('PGSQL_HOST')], - 'php80-' => $nativePg, - 'php81+' => $nativePg, - 'setup' => [], + /** + * @return array> + */ + public static function dataAllIntLike(): array + { + return [ + [ + 'id' => '1', + 'col_bool' => true, + 'col_bool_nullable' => null, + 'col_float' => 1, + 'col_float_nullable' => null, + 'col_decimal' => '1', + 'col_decimal_nullable' => null, + 'col_int' => 1, + 'col_int_nullable' => null, + 'col_bigint' => '1', + 'col_bigint_nullable' => null, + 'col_string' => '1', + 'col_string_nullable' => null, + 'col_mixed' => 1, + 'col_datetime' => new DateTime('2024-01-31 12:59:59'), + ], ]; } + /** - * @param mixed $nativeConnection - * @param array $attributes + * @return array> */ - private function setupAttributes($nativeConnection, array $attributes): void - { - if ($nativeConnection instanceof PDO) { - foreach ($attributes as $attribute => $value) { - $set = $nativeConnection->setAttribute($attribute, $value); - if (!$set) { - throw new LogicException(sprintf('Failed to set attribute %s to %s', $attribute, $value)); - } - } + public static function dataSqrt(): array + { + return [ + [ + 'id' => '1', + 'col_bool' => true, + 'col_bool_nullable' => null, + 'col_float' => 1.0, + 'col_float_nullable' => null, + 'col_decimal' => '1.0', + 'col_decimal_nullable' => null, + 'col_int' => 9, + 'col_int_nullable' => null, + 'col_bigint' => '90000000000', + 'col_bigint_nullable' => null, + 'col_string' => 'foobar', + 'col_string_nullable' => null, + 'col_mixed' => 1, + 'col_datetime' => new DateTime('2024-01-31 12:59:59'), + ], + ]; + } - } elseif ($nativeConnection instanceof mysqli) { - foreach ($attributes as $attribute => $value) { - $set = $nativeConnection->options($attribute, $value); - if (!$set) { - throw new LogicException(sprintf('Failed to set attribute %s to %s', $attribute, $value)); - } + private static function stringifyType(Type $type): Type + { + return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); } - } elseif (is_a($nativeConnection, 'PgSql\Connection', true)) { - if ($attributes !== []) { - throw new LogicException('Cannot set attributes for PgSql\Connection driver'); + if ($type instanceof IntegerType) { + return $type->toString(); } - } elseif ($nativeConnection instanceof SQLite3) { - if ($attributes !== []) { - throw new LogicException('Cannot set attributes for ' . SQLite3::class . ' driver'); + if ($type instanceof FloatType) { + return self::numericString(); } - } elseif (is_resource($nativeConnection)) { // e.g. `resource (pgsql link)` on PHP < 8.1 with pgsql driver - if ($attributes !== []) { - throw new LogicException('Cannot set attributes for this resource'); + if ($type instanceof BooleanType) { + return $type->toInteger()->toString(); } - } else { - throw new LogicException('Unexpected connection: ' . (function_exists('get_debug_type') ? get_debug_type($nativeConnection) : gettype($nativeConnection))); - } + return $traverse($type); + }); } - /** - * @return mixed - */ - private function getNativeConnection(Connection $connection) + private function resolveDefaultStringification(?string $driver, int $php, string $configName): bool { - if (method_exists($connection, 'getNativeConnection')) { - return $connection->getNativeConnection(); + if ($configName === self::CONFIG_DEFAULT) { + if ($php < 80100) { + return $driver === DriverDetector::PDO_MYSQL || $driver === DriverDetector::PDO_SQLITE; + } + + return false; } - if (method_exists($connection, 'getWrappedConnection')) { - if ($connection->getWrappedConnection() instanceof PDO) { - return $connection->getWrappedConnection(); - } + if ($configName === self::CONFIG_STRINGIFY || $configName === self::CONFIG_STRINGIFY_NO_EMULATE) { + return $driver === DriverDetector::PDO_PGSQL + || $driver === DriverDetector::PDO_MYSQL + || $driver === DriverDetector::PDO_SQLITE; + } - if (method_exists($connection->getWrappedConnection(), 'getWrappedResourceHandle')) { - return $connection->getWrappedConnection()->getWrappedResourceHandle(); - } + if ($configName === self::CONFIG_NO_EMULATE) { + return false; + } + + throw new LogicException('Unknown config name: ' . $configName); + } + + private function resolveDefaultBooleanStringification(?string $driver, int $php, string $configName): bool + { + if ($php < 80100 && $driver === DriverDetector::PDO_PGSQL) { + return false; // pdo_pgsql does not stringify booleans even with ATTR_STRINGIFY_FETCHES prior to PHP 8.1 } - throw new LogicException('Unable to get native connection'); + return $this->resolveDefaultStringification($driver, $php, $configName); + } + + private function getHumanReadablePhpVersion(int $phpVersion): string + { + return floor($phpVersion / 10000) . '.' . floor(($phpVersion % 10000) / 100); } private static function hasDbal4(): bool @@ -429,4 +4864,92 @@ private static function hasDbal4(): bool return InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '4.*'); } + private function shouldStringify(string $stringification, ?string $driverType, int $phpVersion, string $configName): bool + { + if ($stringification === self::STRINGIFY_NONE) { + return false; + } + + if ($stringification === self::STRINGIFY_DEFAULT) { + return $this->resolveDefaultStringification($driverType, $phpVersion, $configName); + } + + if ($stringification === self::STRINGIFY_PG_BOOL) { + return $this->resolveDefaultBooleanStringification($driverType, $phpVersion, $configName); + } + + throw new LogicException('Unknown stringification: ' . $stringification); + } + + /** + * @param Query $query + * @param self::INVALID_* $invalidSetup + * @return Query + */ + private function cloneQueryAndInjectInvalidConnection(Query $query, string $driver, string $invalidSetup): Query + { + if ($query->getDQL() === null) { + throw new LogicException('Query does not have DQL'); + } + + $connectionConfig = new DbalConfiguration(); + + if ($invalidSetup === self::INVALID_CONNECTION_UNKNOWN_DRIVER) { + $connectionConfig->setMiddlewares([ + new Middleware($this->createMock(LoggerInterface::class)), // ensures DriverType fallback detection is used + ]); + } + + $serverVersion = $this->getSampleServerVersionForDriver($driver); + $connection = DriverManager::getConnection([ // @phpstan-ignore-line ignore dynamic driver + 'driver' => $driver, + 'user' => 'invalid', + 'serverVersion' => $serverVersion, // otherwise the connection fails while trying to determine the platform + ], $connectionConfig); + $entityManager = new EntityManager($connection, $this->createOrmConfig()); + $newQuery = new Query($entityManager); + $newQuery->setDQL($query->getDQL()); + return $newQuery; + } + + private function createOrmConfig(): Configuration + { + $config = new Configuration(); + $config->setProxyNamespace('PHPstan\Doctrine\OrmMatrixProxies'); + $config->setProxyDir('/tmp/doctrine'); + $config->setAutoGenerateProxyClasses(false); + $config->setSecondLevelCacheEnabled(false); + $config->setMetadataCache(new ArrayCachePool()); + + if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/orm', '3.*')) { + $config->setMetadataDriverImpl(new AttributeDriver([__DIR__ . '/Entity'])); + } else { + $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), [__DIR__ . '/Entity'])); + } + + $config->addCustomStringFunction('INT_PI', TypedExpressionIntegerPiFunction::class); + $config->addCustomStringFunction('BOOL_PI', TypedExpressionBooleanPiFunction::class); + $config->addCustomStringFunction('STRING_PI', TypedExpressionStringPiFunction::class); + + return $config; + } + + private function determineTypeForKnownDriverUnknownSetup(Type $originalExpectedType, string $stringify): Type + { + if ($stringify === self::STRINGIFY_NONE) { + return $originalExpectedType; + } + + return TypeCombinator::union($originalExpectedType, self::stringifyType($originalExpectedType)); + } + + private function determineTypeForUnknownDriverUnknownSetup(Type $originalExpectedType, string $stringify): Type + { + if ($stringify === self::STRINGIFY_NONE) { + return $originalExpectedType; // those are direct column fetches, those always work (this is mild abuse of this flag) + } + + return new MixedType(); + } + } diff --git a/tests/Platform/README.md b/tests/Platform/README.md index b9c07d6a..8784f49e 100644 --- a/tests/Platform/README.md +++ b/tests/Platform/README.md @@ -7,13 +7,19 @@ Set current working directory to project root. # Init services & dependencies - `printf "UID=$(id -u)\nGID=$(id -g)" > .env` - `docker-compose -f tests/Platform/docker/docker-compose.yml up -d` -- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 composer install` # Test behaviour with old stringification +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php80 composer update` - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php80 php -d memory_limit=1G vendor/bin/phpunit --group=platform` # Test behaviour with new stringification +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 composer update` - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform` ``` You can also run utilize those containers for PHPStorm PHPUnit configuration. + +Since the dataset is huge and takes few minutes to run, you can filter only functions you are interested in: +```sh +`docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform --filter "AVG"` +``` diff --git a/tests/Platform/TypedExpressionBooleanPiFunction.php b/tests/Platform/TypedExpressionBooleanPiFunction.php new file mode 100644 index 00000000..ab7049e7 --- /dev/null +++ b/tests/Platform/TypedExpressionBooleanPiFunction.php @@ -0,0 +1,33 @@ +match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getReturnType(): Type + { + return Type::getType(Types::BOOLEAN); + } + +} diff --git a/tests/Platform/TypedExpressionIntegerPiFunction.php b/tests/Platform/TypedExpressionIntegerPiFunction.php new file mode 100644 index 00000000..57f4a2bd --- /dev/null +++ b/tests/Platform/TypedExpressionIntegerPiFunction.php @@ -0,0 +1,33 @@ +match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getReturnType(): Type + { + return Type::getType(Types::INTEGER); + } + +} diff --git a/tests/Platform/TypedExpressionStringPiFunction.php b/tests/Platform/TypedExpressionStringPiFunction.php new file mode 100644 index 00000000..1567ca60 --- /dev/null +++ b/tests/Platform/TypedExpressionStringPiFunction.php @@ -0,0 +1,33 @@ +match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getReturnType(): Type + { + return Type::getType(Types::STRING); + } + +} diff --git a/tests/Platform/docker/Dockerfile80 b/tests/Platform/docker/Dockerfile80 index 37b6694c..b5312737 100644 --- a/tests/Platform/docker/Dockerfile80 +++ b/tests/Platform/docker/Dockerfile80 @@ -1,5 +1,17 @@ FROM php:8.0-cli +# MSSQL +RUN apt update \ + && apt install -y gnupg2 \ + && apt install -y unixodbc-dev unixodbc \ + && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ + && curl https://packages.microsoft.com/config/debian/11/prod.list > /etc/apt/sources.list.d/mssql-release.list \ + && apt update \ + && ACCEPT_EULA=Y apt install -y msodbcsql17 \ + && pecl install sqlsrv-5.11.1 \ + && pecl install pdo_sqlsrv-5.11.1 \ + && docker-php-ext-enable sqlsrv pdo_sqlsrv + COPY ./docker-setup.sh /opt/src/scripts/setup.sh RUN /opt/src/scripts/setup.sh diff --git a/tests/Platform/docker/Dockerfile81 b/tests/Platform/docker/Dockerfile81 index 4ef5c3df..650c65f9 100644 --- a/tests/Platform/docker/Dockerfile81 +++ b/tests/Platform/docker/Dockerfile81 @@ -1,5 +1,17 @@ FROM php:8.1-cli +# MSSQL +RUN apt update \ + && apt install -y gnupg2 \ + && apt install -y unixodbc-dev unixodbc \ + && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ + && curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | tee /etc/apt/sources.list.d/mssql-tools.list \ + && apt update \ + && ACCEPT_EULA=Y apt install -y msodbcsql17 \ + && pecl install sqlsrv \ + && pecl install pdo_sqlsrv \ + && docker-php-ext-enable sqlsrv pdo_sqlsrv + COPY ./docker-setup.sh /opt/src/scripts/setup.sh RUN /opt/src/scripts/setup.sh diff --git a/tests/Platform/docker/docker-compose.yml b/tests/Platform/docker/docker-compose.yml index 5ff6fbb8..73596b72 100644 --- a/tests/Platform/docker/docker-compose.yml +++ b/tests/Platform/docker/docker-compose.yml @@ -7,26 +7,44 @@ services: ports: - 3306:3306 environment: - MYSQL_ROOT_PASSWORD: secret + MYSQL_ROOT_PASSWORD: 'secret' MYSQL_DATABASE: foo + volumes: + - + type: tmpfs + target: /var/lib/mysql pgsql: image: postgres:13 ports: - 5432:5432 environment: - POSTGRES_PASSWORD: secret + POSTGRES_PASSWORD: 'secret' POSTGRES_USER: root POSTGRES_DB: foo + volumes: + - + type: tmpfs + target: /var/lib/postgresql/data + + mssql: + image: mcr.microsoft.com/mssql/server:latest + environment: + ACCEPT_EULA: Y + SA_PASSWORD: 'Secret.123' + MSSQL_PID: Developer + ports: + - 1433:1433 php80: - depends_on: [mysql, pgsql] + depends_on: [mysql, pgsql, mssql] build: context: . dockerfile: ./Dockerfile80 environment: MYSQL_HOST: mysql PGSQL_HOST: pgsql + MSSQL_HOST: mssql working_dir: /app user: ${UID:-1000}:${GID:-1000} volumes: @@ -40,6 +58,7 @@ services: environment: MYSQL_HOST: mysql PGSQL_HOST: pgsql + MSSQL_HOST: mssql working_dir: /app user: ${UID:-1000}:${GID:-1000} volumes: diff --git a/tests/Platform/docker/docker-setup.sh b/tests/Platform/docker/docker-setup.sh index 341c88c2..6fb71310 100755 --- a/tests/Platform/docker/docker-setup.sh +++ b/tests/Platform/docker/docker-setup.sh @@ -1,3 +1,4 @@ +# common setup for PHP 8.0 and PHP 8.1 set -ex \ && apt update \ && apt install -y bash zip libpq-dev libsqlite3-dev \ diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 76611781..6daf17df 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -11,11 +11,11 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Tools\SchemaTool; +use PHPStan\Doctrine\Driver\DriverDetector; +use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryNumericStringType; -use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -31,7 +31,6 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use QueryResult\Entities\Embedded; use QueryResult\Entities\JoinedChild; @@ -215,7 +214,13 @@ public function test(Type $expectedType, string $dql, ?string $expectedException $this->expectDeprecationMessage($expectedDeprecationMessage); } - QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry); + QueryResultTypeWalker::walk( + $query, + $typeBuilder, + $this->descriptorRegistry, + self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(DriverDetector::class) + ); $type = $typeBuilder->getResultType(); @@ -558,10 +563,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeCombinator::union( - $this->numericString(), - new IntegerType() - ), + new IntegerType(), ], [ new ConstantIntegerType(3), @@ -605,7 +607,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(1), - TypeCombinator::addNull($this->intStringified()), + TypeCombinator::addNull($this->intOrStringified()), ], [ new ConstantIntegerType(2), @@ -617,7 +619,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(4), - TypeCombinator::addNull($this->intStringified()), + TypeCombinator::addNull($this->intOrStringified()), ], [ new ConstantIntegerType(5), @@ -625,11 +627,11 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(6), - TypeCombinator::addNull($this->intStringified()), + TypeCombinator::addNull($this->intOrStringified()), ], [ new ConstantIntegerType(7), - $this->intStringified(), + $this->intOrStringified(), ], ]), ' @@ -651,7 +653,7 @@ public function getTestData(): iterable ], [ new ConstantStringType('max'), - $this->intStringified(), + $this->intOrStringified(), ], ]), ' @@ -665,7 +667,7 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantStringType('foo'), - TypeCombinator::addNull($this->numericStringified()), + TypeCombinator::addNull($this->floatOrStringified()), ], ]), ' @@ -682,15 +684,15 @@ public function getTestData(): iterable ], [ new ConstantStringType('max'), - TypeCombinator::addNull($this->intStringified()), + $this->intOrStringified(), ], [ new ConstantStringType('arithmetic'), - $this->intStringified(), + $this->intOrStringified(), ], [ new ConstantStringType('coalesce'), - $this->intStringified(), + $this->intOrStringified(), ], [ new ConstantStringType('count'), @@ -713,62 +715,52 @@ public function getTestData(): iterable [ new ConstantIntegerType(1), TypeCombinator::union( - new ConstantStringType('1'), - new ConstantIntegerType(1), + $this->intOrStringified(), new NullType() ), ], [ new ConstantIntegerType(2), TypeCombinator::union( - new ConstantStringType('0'), - new ConstantIntegerType(0), - new ConstantStringType('1'), - new ConstantIntegerType(1), + $this->intOrStringified(), new NullType() ), ], [ new ConstantIntegerType(3), TypeCombinator::union( - new ConstantStringType('1'), - new ConstantIntegerType(1), + $this->intOrStringified(), new NullType() ), ], [ new ConstantIntegerType(4), TypeCombinator::union( - new ConstantStringType('0'), - new ConstantIntegerType(0), - new ConstantStringType('1'), - new ConstantIntegerType(1), + $this->intOrStringified(), new NullType() ), ], [ new ConstantIntegerType(5), TypeCombinator::union( - $this->intStringified(), - new FloatType(), + $this->floatOrStringified(), new NullType() ), ], [ new ConstantIntegerType(6), TypeCombinator::union( - $this->intStringified(), - new FloatType(), + $this->floatOrStringified(), new NullType() ), ], [ new ConstantIntegerType(7), - TypeCombinator::addNull($this->intStringified()), + TypeCombinator::addNull($this->intOrStringified()), ], [ new ConstantIntegerType(8), - TypeCombinator::addNull($this->intStringified()), + TypeCombinator::addNull($this->intOrStringified()), ], ]), ' @@ -788,10 +780,7 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( - new ConstantStringType('1'), - new ConstantIntegerType(1) - ), + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), ], [new ConstantIntegerType(2), new ConstantStringType('hello')], ]), @@ -806,10 +795,8 @@ public function getTestData(): iterable [ new ConstantIntegerType(1), TypeCombinator::union( - new ConstantIntegerType(1), - new ConstantStringType('1'), - new NullType(), - new ConstantBooleanType(true) + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), + new NullType() ), ], ]), @@ -825,8 +812,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - new IntegerType(), - new ConstantBooleanType(false) + $this->intOrStringified() ), ], [ @@ -838,15 +824,16 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(3), - $this->intStringified(), + $this->intOrStringified(), ], [ new ConstantIntegerType(4), - TypeCombinator::union( - new IntegerType(), - new FloatType(), - $this->numericString() - ), + $this->stringifies() + ? $this->numericString() + : TypeCombinator::union( + new IntegerType(), + new FloatType() + ), ], ]), ' @@ -864,8 +851,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - new ConstantBooleanType(false), - new ConstantIntegerType(0) + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0) ), ], ]), @@ -885,8 +871,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - new ConstantBooleanType(false), - new ConstantIntegerType(0) + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0) ), ], ]), @@ -905,11 +890,8 @@ public function getTestData(): iterable [ new ConstantIntegerType(1), TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantIntegerType(1), - new ConstantStringType('0'), - new ConstantStringType('1'), - new BooleanType() + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1) ), ], ]), @@ -927,11 +909,8 @@ public function getTestData(): iterable [ new ConstantIntegerType(1), TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantIntegerType(1), - new ConstantStringType('0'), - new ConstantStringType('1'), - new BooleanType() + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1) ), ], ]), @@ -948,35 +927,19 @@ public function getTestData(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - TypeCombinator::union( - new ConstantIntegerType(1), - new ConstantStringType('1'), - new ConstantBooleanType(true) - ), + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), ], [ new ConstantIntegerType(2), - TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantStringType('0'), - new ConstantBooleanType(false) - ), + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), ], [ new ConstantIntegerType(3), - TypeCombinator::union( - new ConstantIntegerType(1), - new ConstantStringType('1'), - new ConstantBooleanType(true) - ), + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), ], [ new ConstantIntegerType(4), - TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantStringType('0'), - new ConstantBooleanType(false) - ), + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), ], ]), ' @@ -1148,10 +1111,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeCombinator::union( - new IntegerType(), - $this->numericString() - ), + $this->intOrStringified(), ], [ new ConstantStringType('intColumn'), @@ -1195,7 +1155,7 @@ public function getTestData(): iterable yield 'new arguments affect scalar counter' => [ $this->constantArray([ - [new ConstantIntegerType(5), TypeCombinator::addNull($this->intStringified())], + [new ConstantIntegerType(5), TypeCombinator::addNull($this->intOrStringified())], [new ConstantIntegerType(0), new ObjectType(ManyId::class)], [new ConstantIntegerType(1), new ObjectType(OneId::class)], ]), @@ -1210,13 +1170,13 @@ public function getTestData(): iterable yield 'arithmetic' => [ $this->constantArray([ [new ConstantStringType('intColumn'), new IntegerType()], - [new ConstantIntegerType(1), TypeCombinator::union(new ConstantIntegerType(1), new ConstantStringType('1'))], - [new ConstantIntegerType(2), $this->intStringified()], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->intStringified())], - [new ConstantIntegerType(4), $this->intStringified()], - [new ConstantIntegerType(5), $this->intStringified()], - [new ConstantIntegerType(6), $this->numericStringified()], - [new ConstantIntegerType(7), $this->numericStringified()], + [new ConstantIntegerType(1), $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1)], + [new ConstantIntegerType(2), $this->intOrStringified()], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->intOrStringified())], + [new ConstantIntegerType(4), $this->intOrStringified()], + [new ConstantIntegerType(5), $this->intOrStringified()], + [new ConstantIntegerType(6), new MixedType()], + [new ConstantIntegerType(7), new MixedType()], ]), ' SELECT m.intColumn, @@ -1233,10 +1193,10 @@ public function getTestData(): iterable yield 'abs function' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->unumericStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->unumericStringified())], - [new ConstantIntegerType(3), $this->unumericStringified()], - [new ConstantIntegerType(4), TypeCombinator::union($this->unumericStringified())], + [new ConstantIntegerType(1), $this->uintOrStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(3), $this->uintOrStringified()], + [new ConstantIntegerType(4), new MixedType()], ]), ' SELECT ABS(m.intColumn), @@ -1249,7 +1209,7 @@ public function getTestData(): iterable yield 'abs function with mixed' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->unumericStringified())], + [new ConstantIntegerType(1), new MixedType()], ]), ' SELECT ABS(o.mixedColumn) @@ -1259,9 +1219,9 @@ public function getTestData(): iterable yield 'bit_and function' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), $this->uintStringified()], + [new ConstantIntegerType(1), $this->uintOrStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(3), $this->uintOrStringified()], ]), ' SELECT BIT_AND(m.intColumn, 1), @@ -1273,9 +1233,9 @@ public function getTestData(): iterable yield 'bit_or function' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), $this->uintStringified()], + [new ConstantIntegerType(1), $this->uintOrStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(3), $this->uintOrStringified()], ]), ' SELECT BIT_OR(m.intColumn, 1), @@ -1351,10 +1311,10 @@ public function getTestData(): iterable yield 'date_diff function' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->numericStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->numericStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->numericStringified())], - [new ConstantIntegerType(4), $this->numericStringified()], + [new ConstantIntegerType(1), $this->floatOrStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->floatOrStringified())], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->floatOrStringified())], + [new ConstantIntegerType(4), $this->floatOrStringified()], ]), ' SELECT DATE_DIFF(m.datetimeColumn, m.datetimeColumn), @@ -1393,9 +1353,7 @@ public function getTestData(): iterable ], [ new ConstantIntegerType(2), - TypeCombinator::addNull( - $this->uint() - ), + TypeCombinator::addNull($this->uint()), ], [ new ConstantIntegerType(3), @@ -1413,10 +1371,10 @@ public function getTestData(): iterable if (PHP_VERSION_ID >= 70400) { yield 'locate function' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(4), $this->uintStringified()], + [new ConstantIntegerType(1), $this->uintOrStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(4), $this->uintOrStringified()], ]), ' SELECT LOCATE(m.stringColumn, m.stringColumn, 0), @@ -1456,10 +1414,10 @@ public function getTestData(): iterable yield 'mod function' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(4), $this->uintStringified()], + [new ConstantIntegerType(1), $this->uintOrStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(4), $this->uintOrStringified()], ]), ' SELECT MOD(m.intColumn, 1), @@ -1472,7 +1430,7 @@ public function getTestData(): iterable yield 'mod function error' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->uintOrStringified())], ]), ' SELECT MOD(10, NULLIF(m.intColumn, m.intColumn)) @@ -1531,15 +1489,15 @@ public function getTestData(): iterable yield 'identity function' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], - [new ConstantIntegerType(2), $this->numericStringOrInt()], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->intOrStringified())], + [new ConstantIntegerType(2), $this->intOrStringified()], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->intOrStringified())], [new ConstantIntegerType(4), TypeCombinator::addNull(new StringType())], [new ConstantIntegerType(5), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(6), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(6), TypeCombinator::addNull($this->intOrStringified())], [new ConstantIntegerType(7), TypeCombinator::addNull(new MixedType())], - [new ConstantIntegerType(8), TypeCombinator::addNull($this->numericStringOrInt())], - [new ConstantIntegerType(9), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(8), TypeCombinator::addNull($this->intOrStringified())], + [new ConstantIntegerType(9), TypeCombinator::addNull($this->intOrStringified())], ]), ' SELECT IDENTITY(m.oneNull), @@ -1558,7 +1516,7 @@ public function getTestData(): iterable yield 'select nullable association' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->intOrStringified())], ]), ' SELECT DISTINCT(m.oneNull) @@ -1568,7 +1526,7 @@ public function getTestData(): iterable yield 'select non null association' => [ $this->constantArray([ - [new ConstantIntegerType(1), $this->numericStringOrInt()], + [new ConstantIntegerType(1), $this->intOrStringified()], ]), ' SELECT DISTINCT(m.one) @@ -1578,7 +1536,7 @@ public function getTestData(): iterable yield 'select default nullability association' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->intOrStringified())], ]), ' SELECT DISTINCT(m.oneDefaultNullability) @@ -1588,7 +1546,7 @@ public function getTestData(): iterable yield 'select non null association in aggregated query' => [ $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(1), TypeCombinator::addNull($this->intOrStringified())], [ new ConstantIntegerType(2), $this->uint(), @@ -1640,9 +1598,9 @@ public function getTestData(): iterable yield 'unary minus' => [ $this->constantArray([ - [new ConstantStringType('minusInt'), TypeCombinator::union(new ConstantIntegerType(-1), new ConstantStringType('-1'))], // should be nullable - [new ConstantStringType('minusFloat'), TypeCombinator::union(new ConstantFloatType(-0.1), new ConstantStringType('-0.1'))], // should be nullable - [new ConstantStringType('minusIntRange'), TypeCombinator::union(IntegerRangeType::fromInterval(null, 0), $this->numericString())], + [new ConstantStringType('minusInt'), $this->stringifies() ? new ConstantStringType('-1') : new ConstantIntegerType(-1)], + [new ConstantStringType('minusFloat'), $this->stringifies() ? $this->numericString() : new ConstantFloatType(-0.1)], + [new ConstantStringType('minusIntRange'), $this->stringifies() ? $this->numericString() : IntegerRangeType::fromInterval(null, 0)], ]), ' SELECT -1 as minusInt, @@ -1670,17 +1628,6 @@ private function constantArray(array $elements): Type return $builder->getArray(); } - private function numericStringOrInt(): Type - { - return new UnionType([ - new IntegerType(), - new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]), - ]); - } - private function numericString(): Type { return new IntersectionType([ @@ -1694,39 +1641,6 @@ private function uint(): Type return IntegerRangeType::fromInterval(0, null); } - private function intStringified(): Type - { - return TypeCombinator::union( - new IntegerType(), - $this->numericString() - ); - } - private function uintStringified(): Type - { - return TypeCombinator::union( - $this->uint(), - $this->numericString() - ); - } - - private function numericStringified(): Type - { - return TypeCombinator::union( - new FloatType(), - new IntegerType(), - $this->numericString() - ); - } - - private function unumericStringified(): Type - { - return TypeCombinator::union( - new FloatType(), - IntegerRangeType::fromInterval(0, null), - $this->numericString() - ); - } - /** * @param array $arrays * @@ -1757,4 +1671,30 @@ private function isDoctrine211(): bool && version_compare($version, '2.12', '<'); } + private function stringifies(): bool + { + return PHP_VERSION_ID < 80100; + } + + private function intOrStringified(): Type + { + return $this->stringifies() + ? $this->numericString() + : new IntegerType(); + } + + private function uintOrStringified(): Type + { + return $this->stringifies() + ? $this->numericString() + : $this->uint(); + } + + private function floatOrStringified(): Type + { + return $this->stringifies() + ? $this->numericString() + : new FloatType(); + } + } From 5745ea60dc5b68ff13fd1295091b6663e3e6de61 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 26 Jun 2024 11:25:19 +0200 Subject: [PATCH 045/160] Improve QueryResultDynamicReturnTypeExtension --- .../QueryResultDynamicReturnTypeExtension.php | 221 +++++++++- .../Doctrine/data/QueryResult/queryResult.php | 389 +++++++++++++++++- 2 files changed, 585 insertions(+), 25 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 8bdc9239..f0633a93 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -10,14 +10,22 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\IterableType; +use PHPStan\Type\MixedType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; +use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VoidType; +use function count; final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -32,6 +40,23 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn 'getSingleResult' => 0, ]; + private const METHOD_HYDRATION_MODE = [ + 'getArrayResult' => AbstractQuery::HYDRATE_ARRAY, + 'getScalarResult' => AbstractQuery::HYDRATE_SCALAR, + 'getSingleColumnResult' => AbstractQuery::HYDRATE_SCALAR_COLUMN, + 'getSingleScalarResult' => AbstractQuery::HYDRATE_SINGLE_SCALAR, + ]; + + /** @var ObjectMetadataResolver */ + private $objectMetadataResolver; + + public function __construct( + ObjectMetadataResolver $objectMetadataResolver + ) + { + $this->objectMetadataResolver = $objectMetadataResolver; + } + public function getClass(): string { return AbstractQuery::class; @@ -39,7 +64,8 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]); + return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]) + || isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]); } public function getTypeFromMethodCall( @@ -50,21 +76,23 @@ public function getTypeFromMethodCall( { $methodName = $methodReflection->getName(); - if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { - throw new ShouldNotHappenException(); - } - - $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; - $args = $methodCall->getArgs(); + if (isset(self::METHOD_HYDRATION_MODE[$methodName])) { + $hydrationMode = new ConstantIntegerType(self::METHOD_HYDRATION_MODE[$methodName]); + } elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { + $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; + $args = $methodCall->getArgs(); - if (isset($args[$argIndex])) { - $hydrationMode = $scope->getType($args[$argIndex]->value); + if (isset($args[$argIndex])) { + $hydrationMode = $scope->getType($args[$argIndex]->value); + } else { + $parametersAcceptor = ParametersAcceptorSelector::selectSingle( + $methodReflection->getVariants() + ); + $parameter = $parametersAcceptor->getParameters()[$argIndex]; + $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); + } } else { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() - ); - $parameter = $parametersAcceptor->getParameters()[$argIndex]; - $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); + throw new ShouldNotHappenException(); } $queryType = $scope->getType($methodCall->var); @@ -98,9 +126,35 @@ private function getMethodReturnTypeForHydrationMode( return null; } - if (!$this->isObjectHydrationMode($hydrationMode)) { - // We support only HYDRATE_OBJECT. For other hydration modes, we - // return the declared return type of the method. + if (!$hydrationMode instanceof ConstantIntegerType) { + return null; + } + + $singleResult = false; + switch ($hydrationMode->getValue()) { + case AbstractQuery::HYDRATE_OBJECT: + break; + case AbstractQuery::HYDRATE_ARRAY: + $queryResultType = $this->getArrayHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SCALAR: + $queryResultType = $this->getScalarHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SINGLE_SCALAR: + $singleResult = true; + $queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SIMPLEOBJECT: + $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); + break; + case AbstractQuery::HYDRATE_SCALAR_COLUMN: + $queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType); + break; + default: + return null; + } + + if ($queryResultType === null) { return null; } @@ -108,13 +162,22 @@ private function getMethodReturnTypeForHydrationMode( case 'getSingleResult': return $queryResultType; case 'getOneOrNullResult': - return TypeCombinator::addNull($queryResultType); + $nullableQueryResultType = TypeCombinator::addNull($queryResultType); + if ($queryResultType instanceof BenevolentUnionType) { + $nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType); + } + + return $nullableQueryResultType; case 'toIterable': return new IterableType( $queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType, $queryResultType ); default: + if ($singleResult) { + return $queryResultType; + } + if ($queryKeyType->isNull()->yes()) { return AccessoryArrayListType::intersectWith(new ArrayType( new IntegerType(), @@ -128,13 +191,127 @@ private function getMethodReturnTypeForHydrationMode( } } - private function isObjectHydrationMode(Type $type): bool + /** + * When we're array-hydrating object, we're not sure of the shape of the array. + * We could return `new ArrayTyp(new MixedType(), new MixedType())` + * but the lack of precision in the array keys/values would give false positive. + * + * @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 + */ + private function getArrayHydratedReturnType(Type $queryResultType): ?Type { - if (!$type instanceof ConstantIntegerType) { - return false; + $objectManager = $this->objectMetadataResolver->getObjectManager(); + + $mixedFound = false; + $queryResultType = TypeTraverser::map( + $queryResultType, + static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type { + $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); + if ($isObject->no()) { + return $traverse($type); + } + if ( + $isObject->maybe() + || !$type instanceof TypeWithClassName + || $objectManager === null + ) { + $mixedFound = true; + + return new MixedType(); + } + + /** @var class-string $className */ + $className = $type->getClassName(); + if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) { + return $traverse($type); + } + + $mixedFound = true; + + return new MixedType(); + } + ); + + return $mixedFound ? null : $queryResultType; + } + + /** + * When we're scalar-hydrating object, we're not sure of the shape of the array. + * We could return `new ArrayTyp(new MixedType(), new MixedType())` + * but the lack of precision in the array keys/values would give false positive. + * + * @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544 + */ + private function getScalarHydratedReturnType(Type $queryResultType): ?Type + { + if (!$queryResultType->isArray()->yes()) { + return null; + } + + foreach ($queryResultType->getArrays() as $arrayType) { + $itemType = $arrayType->getItemType(); + + if ( + !(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no() + || !$itemType->isArray()->no() + ) { + // We could return `new ArrayTyp(new MixedType(), new MixedType())` + // but the lack of precision in the array keys/values would give false positive + // @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544 + return null; + } + } + + return $queryResultType; + } + + private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type + { + if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { + return $queryResultType; + } + + return null; + } + + private function getSingleScalarHydratedReturnType(Type $queryResultType): ?Type + { + $queryResultType = $this->getScalarHydratedReturnType($queryResultType); + if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) { + return null; + } + + $types = []; + foreach ($queryResultType->getConstantArrays() as $constantArrayType) { + $values = $constantArrayType->getValueTypes(); + if (count($values) !== 1) { + return null; + } + + $types[] = $constantArrayType->getFirstIterableValueType(); + } + + return TypeCombinator::union(...$types); + } + + private function getScalarColumnHydratedReturnType(Type $queryResultType): ?Type + { + $queryResultType = $this->getScalarHydratedReturnType($queryResultType); + if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) { + return null; + } + + $types = []; + foreach ($queryResultType->getConstantArrays() as $constantArrayType) { + $values = $constantArrayType->getValueTypes(); + if (count($values) !== 1) { + return null; + } + + $types[] = $constantArrayType->getFirstIterableValueType(); } - return $type->getValue() === AbstractQuery::HYDRATE_OBJECT; + return TypeCombinator::union(...$types); } } diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index 02469e46..8721d898 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -144,11 +144,11 @@ public function testReturnTypeOfQueryMethodsWithExplicitObjectHydrationMode(Enti } /** - * Test that we properly infer the return type of Query methods with explicit hydration mode that is not HYDRATE_OBJECT + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_ARRAY * - * We are never able to infer the return type here + * We can infer the return type by changing every object by an array */ - public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(EntityManagerInterface $em): void + public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(EntityManagerInterface $em): void { $query = $em->createQuery(' SELECT m @@ -167,6 +167,11 @@ public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(E 'mixed', $query->execute(null, AbstractQuery::HYDRATE_ARRAY) ); + + assertType( + 'array', + $query->getArrayResult() + ); assertType( 'mixed', $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) @@ -183,6 +188,384 @@ public function testReturnTypeOfQueryMethodsWithExplicitNonObjectHydrationMode(E 'mixed', $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) ); + + + $query = $em->createQuery(' + SELECT m.intColumn, m.stringNullColumn, m.datetimeColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'list', + $query->getArrayResult() + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}', + $query->getSingleResult(AbstractQuery::HYDRATE_ARRAY) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) + ); + } + + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR + */ + public function testReturnTypeOfQueryMethodsWithExplicitScalarHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'mixed', + $query->getResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array', + $query->getScalarResult() + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'mixed', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'mixed', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'mixed', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) + ); + + $query = $em->createQuery(' + SELECT m.intColumn, m.stringNullColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->getScalarResult() + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null}', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) + ); + assertType( + 'array{intColumn: int, stringNullColumn: string|null}|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR + */ + public function testReturnTypeOfQueryMethodsWithExplicitSingleScalarHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'mixed', + $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'bool|float|int|string|null', + $query->getSingleScalarResult() + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + + $query = $em->createQuery(' + SELECT m.intColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'int', + $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->getSingleScalarResult() + ); + assertType( + 'int', + $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int', + $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + + $query = $em->createQuery(' + SELECT COUNT(m.id) + FROM QueryResult\Entities\Many m + '); + + assertType( + 'int<0, max>', + $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>', + $query->getSingleScalarResult() + ); + assertType( + 'int<0, max>', + $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>', + $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + assertType( + 'int<0, max>|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SIMPLEOBJECT + * + * We are never able to infer the return type here + */ + public function testReturnTypeOfQueryMethodsWithExplicitSimpleObjectHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'QueryResult\Entities\Many', + $query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'QueryResult\Entities\Many|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + + $query = $em->createQuery(' + SELECT m.intColumn, m.stringNullColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'mixed', + $query->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'mixed', + $query->execute(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'mixed', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'mixed', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT) + ); + } + + /** + * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR_COLUMN + * + * We are never able to infer the return type here + */ + public function testReturnTypeOfQueryMethodsWithExplicitScalarColumnHydrationMode(EntityManagerInterface $em): void + { + $query = $em->createQuery(' + SELECT m + FROM QueryResult\Entities\Many m + '); + + assertType( + 'mixed', + $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'array', + $query->getSingleColumnResult() + ); + assertType( + 'iterable', + $query->toIterable([], AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'mixed', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'mixed', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'mixed', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'mixed', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'mixed', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + + $query = $em->createQuery(' + SELECT m.intColumn + FROM QueryResult\Entities\Many m + '); + + assertType( + 'list', + $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->getSingleColumnResult() + ); + assertType( + 'list', + $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'list', + $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'int', + $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); + assertType( + 'int|null', + $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) + ); } /** From afb40db91677d553ae637efa159d8b2229748f0a Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 26 Jun 2024 14:03:27 +0200 Subject: [PATCH 046/160] Avoid real connection for type inference --- README.md | 3 - extension.neon | 2 - src/Doctrine/Driver/DriverDetector.php | 157 +++++++----------- .../Doctrine/Query/QueryResultTypeWalker.php | 71 +------- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 151 +++-------------- tests/Platform/UnknownDriver.php | 26 +++ .../Doctrine/ORM/EntityColumnRuleTest.php | 2 +- 7 files changed, 118 insertions(+), 294 deletions(-) create mode 100644 tests/Platform/UnknownDriver.php diff --git a/README.md b/README.md index e59c66c3..bba1119d 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,6 @@ Most DQL features are supported, including `GROUP BY`, `DISTINCT`, all flavors o Whether e.g. `SUM(e.column)` is fetched as `float`, `numeric-string` or `int` highly [depends on drivers, their setup and PHP version](https://github.com/janedbal/php-database-drivers-fetch-test). This extension autodetects your setup and provides quite accurate results for `pdo_mysql`, `mysqli`, `pdo_sqlite`, `sqlite3`, `pdo_pgsql` and `pgsql`. -Sadly, this autodetection often needs real database connection, so in order to utilize precise types, your `objectManagerLoader` need to be able to connect to real database. - -If you are using `bleedingEdge`, the connection failure is propagated. If not, it will be silently ignored and the type will be `mixed` or an union of possible types. ### Supported methods diff --git a/extension.neon b/extension.neon index 2f8daea1..fe686a66 100644 --- a/extension.neon +++ b/extension.neon @@ -91,8 +91,6 @@ services: - class: PHPStan\Doctrine\Driver\DriverDetector - arguments: - failOnInvalidConnection: %featureToggles.bleedingEdge% - class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension - diff --git a/src/Doctrine/Driver/DriverDetector.php b/src/Doctrine/Driver/DriverDetector.php index 674585b6..0c9e5df0 100644 --- a/src/Doctrine/Driver/DriverDetector.php +++ b/src/Doctrine/Driver/DriverDetector.php @@ -14,14 +14,8 @@ use Doctrine\DBAL\Driver\PgSQL\Driver as PgSQLDriver; use Doctrine\DBAL\Driver\SQLite3\Driver as SQLite3Driver; use Doctrine\DBAL\Driver\SQLSrv\Driver as SqlSrvDriver; -use mysqli; -use PDO; -use SQLite3; -use Throwable; -use function get_resource_type; -use function is_resource; -use function method_exists; -use function strpos; +use function get_class; +use function is_a; class DriverDetector { @@ -38,139 +32,114 @@ class DriverDetector public const SQLITE3 = 'sqlite3'; public const SQLSRV = 'sqlsrv'; - /** @var bool */ - private $failOnInvalidConnection; - - public function __construct(bool $failOnInvalidConnection) + /** + * @return self::*|null + */ + public function detect(Connection $connection): ?string { - $this->failOnInvalidConnection = $failOnInvalidConnection; + $driver = $connection->getDriver(); + + return $this->deduceFromDriverClass(get_class($driver)) ?? $this->deduceFromParams($connection); } - public function failsOnInvalidConnection(): bool + /** + * @return array + */ + public function detectDriverOptions(Connection $connection): array { - return $this->failOnInvalidConnection; + return $connection->getParams()['driverOptions'] ?? []; } /** * @return self::*|null */ - public function detect(Connection $connection): ?string + private function deduceFromDriverClass(string $driverClass): ?string { - $driver = $connection->getDriver(); - - if ($driver instanceof MysqliDriver) { + if (is_a($driverClass, MysqliDriver::class, true)) { return self::MYSQLI; } - if ($driver instanceof PdoMysqlDriver) { + if (is_a($driverClass, PdoMysqlDriver::class, true)) { return self::PDO_MYSQL; } - if ($driver instanceof PdoSQLiteDriver) { + if (is_a($driverClass, PdoSQLiteDriver::class, true)) { return self::PDO_SQLITE; } - if ($driver instanceof PdoSqlSrvDriver) { + if (is_a($driverClass, PdoSqlSrvDriver::class, true)) { return self::PDO_SQLSRV; } - if ($driver instanceof PdoOciDriver) { + if (is_a($driverClass, PdoOciDriver::class, true)) { return self::PDO_OCI; } - if ($driver instanceof PdoPgSQLDriver) { + if (is_a($driverClass, PdoPgSQLDriver::class, true)) { return self::PDO_PGSQL; } - if ($driver instanceof SQLite3Driver) { + if (is_a($driverClass, SQLite3Driver::class, true)) { return self::SQLITE3; } - if ($driver instanceof PgSQLDriver) { + if (is_a($driverClass, PgSQLDriver::class, true)) { return self::PGSQL; } - if ($driver instanceof SqlSrvDriver) { + if (is_a($driverClass, SqlSrvDriver::class, true)) { return self::SQLSRV; } - if ($driver instanceof Oci8Driver) { + if (is_a($driverClass, Oci8Driver::class, true)) { return self::OCI8; } - if ($driver instanceof IbmDb2Driver) { + if (is_a($driverClass, IbmDb2Driver::class, true)) { return self::IBM_DB2; } - // fallback to connection-based detection when driver is wrapped by middleware - - if (!method_exists($connection, 'getNativeConnection')) { - return null; // dbal < 3.3 (released in 2022-01) - } - - try { - $nativeConnection = $connection->getNativeConnection(); - } catch (Throwable $e) { - if ($this->failOnInvalidConnection) { - throw $e; - } - return null; // connection cannot be established - } - - if ($nativeConnection instanceof mysqli) { - return self::MYSQLI; - } - - if ($nativeConnection instanceof SQLite3) { - return self::SQLITE3; - } - - if ($nativeConnection instanceof \PgSql\Connection) { - return self::PGSQL; - } - - if ($nativeConnection instanceof PDO) { - $driverName = $nativeConnection->getAttribute(PDO::ATTR_DRIVER_NAME); - - if ($driverName === 'mysql') { - return self::PDO_MYSQL; - } - - if ($driverName === 'sqlite') { - return self::PDO_SQLITE; - } - - if ($driverName === 'pgsql') { - return self::PDO_PGSQL; - } - - if ($driverName === 'oci') { // semi-verified (https://stackoverflow.com/questions/10090709/get-current-pdo-driver-from-existing-connection/10090754#comment12923198_10090754) - return self::PDO_OCI; - } + return null; + } - if ($driverName === 'sqlsrv') { - return self::PDO_SQLSRV; + /** + * @return self::*|null + */ + private function deduceFromParams(Connection $connection): ?string + { + $params = $connection->getParams(); + + if (isset($params['driver'])) { + switch ($params['driver']) { + case 'pdo_mysql': + return self::PDO_MYSQL; + case 'pdo_sqlite': + return self::PDO_SQLITE; + case 'pdo_pgsql': + return self::PDO_PGSQL; + case 'pdo_oci': + return self::PDO_OCI; + case 'oci8': + return self::OCI8; + case 'ibm_db2': + return self::IBM_DB2; + case 'pdo_sqlsrv': + return self::PDO_SQLSRV; + case 'mysqli': + return self::MYSQLI; + case 'pgsql': // @phpstan-ignore-line never matches on PHP 7.3- with old dbal + return self::PGSQL; + case 'sqlsrv': + return self::SQLSRV; + case 'sqlite3': // @phpstan-ignore-line never matches on PHP 7.3- with old dbal + return self::SQLITE3; + default: + return null; } } - if (is_resource($nativeConnection)) { - $resourceType = get_resource_type($nativeConnection); - - if (strpos($resourceType, 'oci') !== false) { // not verified - return self::OCI8; - } - - if (strpos($resourceType, 'db2') !== false) { // not verified - return self::IBM_DB2; - } - - if (strpos($resourceType, 'SQL Server Connection') !== false) { - return self::SQLSRV; - } - - if (strpos($resourceType, 'pgsql link') !== false) { - return self::PGSQL; - } + if (isset($params['driverClass'])) { + return $this->deduceFromDriverClass($params['driverClass']); } return null; diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index f2e8c051..e5040fb8 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -14,7 +14,6 @@ use Doctrine\ORM\Query\ParserResult; use Doctrine\ORM\Query\SqlWalker; use PDO; -use PDOException; use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; @@ -41,7 +40,6 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; -use Throwable; use function array_key_exists; use function array_map; use function array_values; @@ -55,7 +53,6 @@ use function is_numeric; use function is_object; use function is_string; -use function method_exists; use function serialize; use function sprintf; use function stripos; @@ -108,6 +105,9 @@ class QueryResultTypeWalker extends SqlWalker /** @var DriverDetector::*|null */ private $driverType; + /** @var array */ + private $driverOptions; + /** * Map of all components/classes that appear in the DQL query. * @@ -130,8 +130,6 @@ class QueryResultTypeWalker extends SqlWalker /** @var bool */ private $hasGroupByClause; - /** @var bool */ - private $failOnInvalidConnection; /** * @param Query $query @@ -224,8 +222,10 @@ public function __construct($query, $parserResult, array $queryComponents) is_object($driverDetector) ? get_class($driverDetector) : gettype($driverDetector) )); } - $this->driverType = $driverDetector->detect($this->em->getConnection()); - $this->failOnInvalidConnection = $driverDetector->failsOnInvalidConnection(); + $connection = $this->em->getConnection(); + + $this->driverType = $driverDetector->detect($connection); + $this->driverOptions = $driverDetector->detectDriverOptions($connection); parent::__construct($query, $parserResult, $queryComponents); } @@ -2042,20 +2042,10 @@ private function hasAggregateWithoutGroupBy(): bool private function shouldStringifyExpressions(Type $type): TrinaryLogic { if (in_array($this->driverType, [DriverDetector::PDO_MYSQL, DriverDetector::PDO_PGSQL, DriverDetector::PDO_SQLITE], true)) { - try { - $nativeConnection = $this->getNativeConnection(); - assert($nativeConnection instanceof PDO); - } catch (Throwable $e) { // connection cannot be established - if ($this->failOnInvalidConnection) { - throw $e; - } - return TrinaryLogic::createMaybe(); - } - - $stringifyFetches = $this->isPdoStringifyEnabled($nativeConnection); + $stringifyFetches = isset($this->driverOptions[PDO::ATTR_STRINGIFY_FETCHES]) ? (bool) $this->driverOptions[PDO::ATTR_STRINGIFY_FETCHES] : false; if ($this->driverType === DriverDetector::PDO_MYSQL) { - $emulatedPrepares = $this->isPdoEmulatePreparesEnabled($nativeConnection); + $emulatedPrepares = isset($this->driverOptions[PDO::ATTR_EMULATE_PREPARES]) ? (bool) $this->driverOptions[PDO::ATTR_EMULATE_PREPARES] : true; if ($stringifyFetches) { return TrinaryLogic::createYes(); @@ -2105,49 +2095,6 @@ private function shouldStringifyExpressions(Type $type): TrinaryLogic return TrinaryLogic::createMaybe(); } - private function isPdoStringifyEnabled(PDO $pdo): bool - { - // this fails for most PHP versions, see https://github.com/php/php-src/issues/12969 - // working since 8.2.15 and 8.3.2 - try { - return (bool) $pdo->getAttribute(PDO::ATTR_STRINGIFY_FETCHES); - } catch (PDOException $e) { - $selectOne = $pdo->query('SELECT 1'); - if ($selectOne === false) { - return false; // this should not happen, just return attribute default value - } - $one = $selectOne->fetchColumn(); - - // string can be returned due to old PHP used or because ATTR_STRINGIFY_FETCHES is enabled, - // but it should not matter as it behaves the same way - // (the attribute is there to maintain BC) - return is_string($one); - } - } - - private function isPdoEmulatePreparesEnabled(PDO $pdo): bool - { - return (bool) $pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES); - } - - /** - * @return object|resource|null - */ - private function getNativeConnection() - { - $connection = $this->em->getConnection(); - - if (method_exists($connection, 'getNativeConnection')) { - return $connection->getNativeConnection(); - } - - if ($connection->getWrappedConnection() instanceof PDO) { - return $connection->getWrappedConnection(); - } - - return null; - } - private function isSupportedDriver(): bool { return in_array($this->driverType, [ diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 4543b9eb..0d1e3b34 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -77,9 +77,6 @@ final class QueryResultTypeWalkerFetchTypeMatrixTest extends PHPStanTestCase private const CONFIG_NO_EMULATE = 'pdo_no_emulate'; private const CONFIG_STRINGIFY_NO_EMULATE = 'pdo_stringify_no_emulate'; - private const INVALID_CONNECTION = 'invalid_connection'; - private const INVALID_CONNECTION_UNKNOWN_DRIVER = 'invalid_connection_and_unknown_driver'; - private const CONNECTION_CONFIGS = [ self::CONFIG_DEFAULT => [], self::CONFIG_STRINGIFY => [ @@ -582,8 +579,6 @@ public function testUnsupportedDriver( } /** - * Connection failure test - * * @param array $data * @param mixed $mysqlExpectedResult * @param mixed $sqliteExpectedResult @@ -594,93 +589,7 @@ public function testUnsupportedDriver( * * @dataProvider provideCases */ - public function testKnownDriverUnknownSetupDefault( - array $data, - string $dqlTemplate, - Type $mysqlExpectedType, - ?Type $sqliteExpectedType, - ?Type $pdoPgsqlExpectedType, - ?Type $pgsqlExpectedType, - ?Type $mssqlExpectedType, - $mysqlExpectedResult, - $sqliteExpectedResult, - $pdoPgsqlExpectedResult, - $pgsqlExpectedResult, - $mssqlExpectedResult, - string $stringify - ): void - { - $this->performDriverTest( - 'pdo_mysql', - self::CONFIG_DEFAULT, - $data, - $dqlTemplate, - (string) $this->dataName(), - PHP_VERSION_ID, - $this->determineTypeForKnownDriverUnknownSetup($mysqlExpectedType, $stringify), - $mysqlExpectedResult, - $stringify, - self::INVALID_CONNECTION - ); - } - - /** - * Connection failure test - * - * @param array $data - * @param mixed $mysqlExpectedResult - * @param mixed $sqliteExpectedResult - * @param mixed $pdoPgsqlExpectedResult - * @param mixed $pgsqlExpectedResult - * @param mixed $mssqlExpectedResult - * @param self::STRINGIFY_* $stringify - * - * @dataProvider provideCases - */ - public function testKnownDriverUnknownSetupStringify( - array $data, - string $dqlTemplate, - Type $mysqlExpectedType, - ?Type $sqliteExpectedType, - ?Type $pdoPgsqlExpectedType, - ?Type $pgsqlExpectedType, - ?Type $mssqlExpectedType, - $mysqlExpectedResult, - $sqliteExpectedResult, - $pdoPgsqlExpectedResult, - $pgsqlExpectedResult, - $mssqlExpectedResult, - string $stringify - ): void - { - $this->performDriverTest( - 'pdo_mysql', - self::CONFIG_STRINGIFY, - $data, - $dqlTemplate, - (string) $this->dataName(), - PHP_VERSION_ID, - $this->determineTypeForKnownDriverUnknownSetup($mysqlExpectedType, $stringify), - $mysqlExpectedResult, - $stringify, - self::INVALID_CONNECTION - ); - } - - /** - * Connection failure test - * - * @param array $data - * @param mixed $mysqlExpectedResult - * @param mixed $sqliteExpectedResult - * @param mixed $pdoPgsqlExpectedResult - * @param mixed $pgsqlExpectedResult - * @param mixed $mssqlExpectedResult - * @param self::STRINGIFY_* $stringify - * - * @dataProvider provideCases - */ - public function testUnknownDriverUnknownSetupDefault( + public function testUnknownDriver( array $data, string $dqlTemplate, Type $mysqlExpectedType, @@ -706,13 +615,11 @@ public function testUnknownDriverUnknownSetupDefault( $this->determineTypeForUnknownDriverUnknownSetup($mysqlExpectedType, $stringify), $mysqlExpectedResult, $stringify, - self::INVALID_CONNECTION_UNKNOWN_DRIVER + true ); } /** - * Connection failure test - * * @param array $data * @param mixed $mysqlExpectedResult * @param mixed $sqliteExpectedResult @@ -723,7 +630,7 @@ public function testUnknownDriverUnknownSetupDefault( * * @dataProvider provideCases */ - public function testUnknownDriverUnknownSetupStringify( + public function testUnknownDriverStringify( array $data, string $dqlTemplate, Type $mysqlExpectedType, @@ -749,7 +656,7 @@ public function testUnknownDriverUnknownSetupStringify( $this->determineTypeForUnknownDriverUnknownSetup($mysqlExpectedType, $stringify), $mysqlExpectedResult, $stringify, - self::INVALID_CONNECTION_UNKNOWN_DRIVER + true ); } @@ -4267,7 +4174,6 @@ public static function provideCases(): iterable * @param mixed $expectedFirstResult * @param array $data * @param self::STRINGIFY_* $stringification - * @param self::INVALID_*|null $invalidConnectionSetup */ private function performDriverTest( string $driver, @@ -4279,7 +4185,7 @@ private function performDriverTest( ?Type $expectedInferredType, $expectedFirstResult, string $stringification, - ?string $invalidConnectionSetup = null + bool $useUnknownDriverForInference = false ): void { $connectionParams = [ @@ -4299,12 +4205,12 @@ private function performDriverTest( $result = $query->getSingleResult(); $realResultType = ConstantTypeHelper::getTypeFromValue($result); - if ($invalidConnectionSetup !== null) { - $inferredType = $this->getInferredType($this->cloneQueryAndInjectInvalidConnection($query, $driver, $invalidConnectionSetup), false); - } else { - $inferredType = $this->getInferredType($query, true); + if ($useUnknownDriverForInference) { + $query = $this->cloneQueryAndInjectConnectionWithUnknownPdoMysqlDriver($query); } + $inferredType = $this->getInferredType($query); + } catch (Throwable $e) { if ($expectedInferredType === null) { return; @@ -4327,13 +4233,13 @@ private function performDriverTest( )); } - $driverDetector = new DriverDetector(true); - $driverType = $driverDetector->detect($query->getEntityManager()->getConnection()); + $driverDetector = new DriverDetector(); + $driverType = $driverDetector->detect($connection); $stringify = $this->shouldStringify($stringification, $driverType, $phpVersion, $configName); if ( $stringify - && $invalidConnectionSetup === null // do not stringify, we already passed union with stringified one above + && !$useUnknownDriverForInference // do not stringify, we already passed union with stringified one above ) { $expectedInferredType = self::stringifyType($expectedInferredType); } @@ -4420,7 +4326,7 @@ private function getQuery( /** * @param Query $query */ - private function getInferredType(Query $query, bool $failOnInvalidConnection): Type + private function getInferredType(Query $query): Type { $typeBuilder = new QueryResultTypeBuilder(); $phpVersion = new PhpVersion(PHP_VERSION_ID); // @phpstan-ignore-line ctor not in bc promise @@ -4429,7 +4335,7 @@ private function getInferredType(Query $query, bool $failOnInvalidConnection): T $typeBuilder, self::getContainer()->getByType(DescriptorRegistry::class), $phpVersion, - new DriverDetector($failOnInvalidConnection) + new DriverDetector() ); return $typeBuilder->getResultType(); @@ -4883,29 +4789,19 @@ private function shouldStringify(string $stringification, ?string $driverType, i /** * @param Query $query - * @param self::INVALID_* $invalidSetup * @return Query */ - private function cloneQueryAndInjectInvalidConnection(Query $query, string $driver, string $invalidSetup): Query + private function cloneQueryAndInjectConnectionWithUnknownPdoMysqlDriver(Query $query): Query { if ($query->getDQL() === null) { throw new LogicException('Query does not have DQL'); } - $connectionConfig = new DbalConfiguration(); - - if ($invalidSetup === self::INVALID_CONNECTION_UNKNOWN_DRIVER) { - $connectionConfig->setMiddlewares([ - new Middleware($this->createMock(LoggerInterface::class)), // ensures DriverType fallback detection is used - ]); - } + $connection = DriverManager::getConnection([ + 'driverClass' => UnknownDriver::class, + 'serverVersion' => $this->getSampleServerVersionForDriver('pdo_mysql'), + ]); - $serverVersion = $this->getSampleServerVersionForDriver($driver); - $connection = DriverManager::getConnection([ // @phpstan-ignore-line ignore dynamic driver - 'driver' => $driver, - 'user' => 'invalid', - 'serverVersion' => $serverVersion, // otherwise the connection fails while trying to determine the platform - ], $connectionConfig); $entityManager = new EntityManager($connection, $this->createOrmConfig()); $newQuery = new Query($entityManager); $newQuery->setDQL($query->getDQL()); @@ -4934,15 +4830,6 @@ private function createOrmConfig(): Configuration return $config; } - private function determineTypeForKnownDriverUnknownSetup(Type $originalExpectedType, string $stringify): Type - { - if ($stringify === self::STRINGIFY_NONE) { - return $originalExpectedType; - } - - return TypeCombinator::union($originalExpectedType, self::stringifyType($originalExpectedType)); - } - private function determineTypeForUnknownDriverUnknownSetup(Type $originalExpectedType, string $stringify): Type { if ($stringify === self::STRINGIFY_NONE) { diff --git a/tests/Platform/UnknownDriver.php b/tests/Platform/UnknownDriver.php new file mode 100644 index 00000000..52c408e0 --- /dev/null +++ b/tests/Platform/UnknownDriver.php @@ -0,0 +1,26 @@ + Date: Thu, 27 Jun 2024 13:29:51 +0200 Subject: [PATCH 047/160] Add test for wrapped TypedExpression --- .../QueryResultTypeWalkerFetchTypeMatrixTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 0d1e3b34..2623181e 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -3913,6 +3913,22 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_NONE, ]; + yield '-INT_PI()' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT -INT_PI() FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => '-3.14159', + 'sqliteResult' => -3.14159, + 'pdoPgsqlResult' => '-3.14159', + 'pgsqlResult' => '-3.14159', + 'mssqlResult' => '-3.14159', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + yield 'BOOL_PI()' => [ 'data' => self::dataDefault(), 'select' => 'SELECT BOOL_PI() FROM %s t', From c54ce9b5f67efb96830e4086c9d7c2d8b74a4e4d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 27 Jun 2024 11:27:32 +0200 Subject: [PATCH 048/160] Revert result inference for scalar method --- .../QueryResultDynamicReturnTypeExtension.php | 89 ------ .../Doctrine/data/QueryResult/queryResult.php | 270 ------------------ 2 files changed, 359 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index f0633a93..54345e7f 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -25,7 +25,6 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VoidType; -use function count; final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -42,9 +41,6 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn private const METHOD_HYDRATION_MODE = [ 'getArrayResult' => AbstractQuery::HYDRATE_ARRAY, - 'getScalarResult' => AbstractQuery::HYDRATE_SCALAR, - 'getSingleColumnResult' => AbstractQuery::HYDRATE_SCALAR_COLUMN, - 'getSingleScalarResult' => AbstractQuery::HYDRATE_SINGLE_SCALAR, ]; /** @var ObjectMetadataResolver */ @@ -130,26 +126,15 @@ private function getMethodReturnTypeForHydrationMode( return null; } - $singleResult = false; switch ($hydrationMode->getValue()) { case AbstractQuery::HYDRATE_OBJECT: break; case AbstractQuery::HYDRATE_ARRAY: $queryResultType = $this->getArrayHydratedReturnType($queryResultType); break; - case AbstractQuery::HYDRATE_SCALAR: - $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - break; - case AbstractQuery::HYDRATE_SINGLE_SCALAR: - $singleResult = true; - $queryResultType = $this->getSingleScalarHydratedReturnType($queryResultType); - break; case AbstractQuery::HYDRATE_SIMPLEOBJECT: $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); break; - case AbstractQuery::HYDRATE_SCALAR_COLUMN: - $queryResultType = $this->getScalarColumnHydratedReturnType($queryResultType); - break; default: return null; } @@ -174,10 +159,6 @@ private function getMethodReturnTypeForHydrationMode( $queryResultType ); default: - if ($singleResult) { - return $queryResultType; - } - if ($queryKeyType->isNull()->yes()) { return AccessoryArrayListType::intersectWith(new ArrayType( new IntegerType(), @@ -235,36 +216,6 @@ static function (Type $type, callable $traverse) use ($objectManager, &$mixedFou return $mixedFound ? null : $queryResultType; } - /** - * When we're scalar-hydrating object, we're not sure of the shape of the array. - * We could return `new ArrayTyp(new MixedType(), new MixedType())` - * but the lack of precision in the array keys/values would give false positive. - * - * @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544 - */ - private function getScalarHydratedReturnType(Type $queryResultType): ?Type - { - if (!$queryResultType->isArray()->yes()) { - return null; - } - - foreach ($queryResultType->getArrays() as $arrayType) { - $itemType = $arrayType->getItemType(); - - if ( - !(new ObjectWithoutClassType())->isSuperTypeOf($itemType)->no() - || !$itemType->isArray()->no() - ) { - // We could return `new ArrayTyp(new MixedType(), new MixedType())` - // but the lack of precision in the array keys/values would give false positive - // @see https://github.com/phpstan/phpstan-doctrine/pull/453#issuecomment-1895415544 - return null; - } - } - - return $queryResultType; - } - private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type { if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { @@ -274,44 +225,4 @@ private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type return null; } - private function getSingleScalarHydratedReturnType(Type $queryResultType): ?Type - { - $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) { - return null; - } - - $types = []; - foreach ($queryResultType->getConstantArrays() as $constantArrayType) { - $values = $constantArrayType->getValueTypes(); - if (count($values) !== 1) { - return null; - } - - $types[] = $constantArrayType->getFirstIterableValueType(); - } - - return TypeCombinator::union(...$types); - } - - private function getScalarColumnHydratedReturnType(Type $queryResultType): ?Type - { - $queryResultType = $this->getScalarHydratedReturnType($queryResultType); - if ($queryResultType === null || !$queryResultType->isConstantArray()->yes()) { - return null; - } - - $types = []; - foreach ($queryResultType->getConstantArrays() as $constantArrayType) { - $values = $constantArrayType->getValueTypes(); - if (count($values) !== 1) { - return null; - } - - $types[] = $constantArrayType->getFirstIterableValueType(); - } - - return TypeCombinator::union(...$types); - } - } diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index 8721d898..46734609 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -226,196 +226,6 @@ public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(Entit } - /** - * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR - */ - public function testReturnTypeOfQueryMethodsWithExplicitScalarHydrationMode(EntityManagerInterface $em): void - { - $query = $em->createQuery(' - SELECT m - FROM QueryResult\Entities\Many m - '); - - assertType( - 'mixed', - $query->getResult(AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'array', - $query->getScalarResult() - ); - assertType( - 'iterable', - $query->toIterable([], AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'mixed', - $query->execute(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'mixed', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'mixed', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'mixed', - $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'mixed', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) - ); - - $query = $em->createQuery(' - SELECT m.intColumn, m.stringNullColumn - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->getScalarResult() - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'array{intColumn: int, stringNullColumn: string|null}', - $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR) - ); - assertType( - 'array{intColumn: int, stringNullColumn: string|null}|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR) - ); - } - - /** - * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR - */ - public function testReturnTypeOfQueryMethodsWithExplicitSingleScalarHydrationMode(EntityManagerInterface $em): void - { - $query = $em->createQuery(' - SELECT m - FROM QueryResult\Entities\Many m - '); - - assertType( - 'mixed', - $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'bool|float|int|string|null', - $query->getSingleScalarResult() - ); - assertType( - 'iterable', - $query->toIterable([], AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'mixed', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - - $query = $em->createQuery(' - SELECT m.intColumn - FROM QueryResult\Entities\Many m - '); - - assertType( - 'int', - $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int', - $query->getSingleScalarResult() - ); - assertType( - 'int', - $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int', - $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - - $query = $em->createQuery(' - SELECT COUNT(m.id) - FROM QueryResult\Entities\Many m - '); - - assertType( - 'int<0, max>', - $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int<0, max>', - $query->getSingleScalarResult() - ); - assertType( - 'int<0, max>', - $query->execute(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int<0, max>', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int<0, max>', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int<0, max>', - $query->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - assertType( - 'int<0, max>|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR) - ); - } - /** * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SIMPLEOBJECT * @@ -488,86 +298,6 @@ public function testReturnTypeOfQueryMethodsWithExplicitSimpleObjectHydrationMod ); } - /** - * Test that we properly infer the return type of Query methods with explicit hydration mode of HYDRATE_SCALAR_COLUMN - * - * We are never able to infer the return type here - */ - public function testReturnTypeOfQueryMethodsWithExplicitScalarColumnHydrationMode(EntityManagerInterface $em): void - { - $query = $em->createQuery(' - SELECT m - FROM QueryResult\Entities\Many m - '); - - assertType( - 'mixed', - $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'array', - $query->getSingleColumnResult() - ); - assertType( - 'iterable', - $query->toIterable([], AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'mixed', - $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'mixed', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'mixed', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'mixed', - $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'mixed', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - - $query = $em->createQuery(' - SELECT m.intColumn - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->getSingleColumnResult() - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'int', - $query->getSingleResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - assertType( - 'int|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN) - ); - } - /** * Test that we properly infer the return type of Query methods with explicit hydration mode that is not a constant value * From 18ab94daf8ca4480e65378d4529252a33eeade43 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 28 Jun 2024 15:32:17 +0200 Subject: [PATCH 049/160] Test hydration modes --- extension.neon | 2 + .../HydrationModeReturnTypeResolver.php | 143 ++++++ .../QueryResultDynamicReturnTypeExtension.php | 151 +----- ...QueryResultTypeWalkerHydrationModeTest.php | 484 ++++++++++++++++++ .../data/QueryResult/Entities/Simple.php | 71 +++ 5 files changed, 713 insertions(+), 138 deletions(-) create mode 100644 src/Type/Doctrine/HydrationModeReturnTypeResolver.php create mode 100644 tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php create mode 100644 tests/Type/Doctrine/data/QueryResult/Entities/Simple.php diff --git a/extension.neon b/extension.neon index fe686a66..81469b91 100644 --- a/extension.neon +++ b/extension.neon @@ -91,6 +91,8 @@ services: - class: PHPStan\Doctrine\Driver\DriverDetector + - + class: PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver - class: PHPStan\Reflection\Doctrine\DoctrineSelectableClassReflectionExtension - diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php new file mode 100644 index 00000000..301731f5 --- /dev/null +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -0,0 +1,143 @@ +isSuperTypeOf($queryResultType); + + if ($isVoidType->yes()) { + // A void query result type indicates an UPDATE or DELETE query. + // In this case all methods return the number of affected rows. + return new IntegerType(); + } + + if ($isVoidType->maybe()) { + // We can't be sure what the query type is, so we return the + // declared return type of the method. + return null; + } + + switch ($hydrationMode) { + case AbstractQuery::HYDRATE_OBJECT: + break; + case AbstractQuery::HYDRATE_ARRAY: + $queryResultType = $this->getArrayHydratedReturnType($queryResultType, $objectManager); + break; + case AbstractQuery::HYDRATE_SIMPLEOBJECT: + $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); + break; + default: + return null; + } + + if ($queryResultType === null) { + return null; + } + + switch ($methodName) { + case 'getSingleResult': + return $queryResultType; + case 'getOneOrNullResult': + $nullableQueryResultType = TypeCombinator::addNull($queryResultType); + if ($queryResultType instanceof BenevolentUnionType) { + $nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType); + } + + return $nullableQueryResultType; + case 'toIterable': + return new IterableType( + $queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType, + $queryResultType + ); + default: + if ($queryKeyType->isNull()->yes()) { + return AccessoryArrayListType::intersectWith(new ArrayType( + new IntegerType(), + $queryResultType + )); + } + return new ArrayType( + $queryKeyType, + $queryResultType + ); + } + } + + /** + * When we're array-hydrating object, we're not sure of the shape of the array. + * We could return `new ArrayTyp(new MixedType(), new MixedType())` + * but the lack of precision in the array keys/values would give false positive. + * + * @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 + */ + private function getArrayHydratedReturnType(Type $queryResultType, ?ObjectManager $objectManager): ?Type + { + $mixedFound = false; + $queryResultType = TypeTraverser::map( + $queryResultType, + static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type { + $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); + if ($isObject->no()) { + return $traverse($type); + } + if ( + $isObject->maybe() + || !$type instanceof TypeWithClassName + || $objectManager === null + ) { + $mixedFound = true; + + return new MixedType(); + } + + /** @var class-string $className */ + $className = $type->getClassName(); + if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) { + return $traverse($type); + } + + $mixedFound = true; + + return new MixedType(); + } + ); + + return $mixedFound ? null : $queryResultType; + } + + private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type + { + if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { + return $queryResultType; + } + + return null; + } + +} diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 54345e7f..818c3869 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -8,23 +8,12 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Accessory\AccessoryArrayListType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\IntegerType; -use PHPStan\Type\IterableType; -use PHPStan\Type\MixedType; use PHPStan\Type\NullType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; -use PHPStan\Type\VoidType; final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -46,11 +35,16 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn /** @var ObjectMetadataResolver */ private $objectMetadataResolver; + /** @var HydrationModeReturnTypeResolver */ + private $hydrationModeReturnTypeResolver; + public function __construct( - ObjectMetadataResolver $objectMetadataResolver + ObjectMetadataResolver $objectMetadataResolver, + HydrationModeReturnTypeResolver $hydrationModeReturnTypeResolver ) { $this->objectMetadataResolver = $objectMetadataResolver; + $this->hydrationModeReturnTypeResolver = $hydrationModeReturnTypeResolver; } public function getClass(): string @@ -93,136 +87,17 @@ public function getTypeFromMethodCall( $queryType = $scope->getType($methodCall->var); - return $this->getMethodReturnTypeForHydrationMode( - $methodReflection, - $hydrationMode, - $queryType->getTemplateType(AbstractQuery::class, 'TKey'), - $queryType->getTemplateType(AbstractQuery::class, 'TResult') - ); - } - - private function getMethodReturnTypeForHydrationMode( - MethodReflection $methodReflection, - Type $hydrationMode, - Type $queryKeyType, - Type $queryResultType - ): ?Type - { - $isVoidType = (new VoidType())->isSuperTypeOf($queryResultType); - - if ($isVoidType->yes()) { - // A void query result type indicates an UPDATE or DELETE query. - // In this case all methods return the number of affected rows. - return new IntegerType(); - } - - if ($isVoidType->maybe()) { - // We can't be sure what the query type is, so we return the - // declared return type of the method. - return null; - } - if (!$hydrationMode instanceof ConstantIntegerType) { return null; } - switch ($hydrationMode->getValue()) { - case AbstractQuery::HYDRATE_OBJECT: - break; - case AbstractQuery::HYDRATE_ARRAY: - $queryResultType = $this->getArrayHydratedReturnType($queryResultType); - break; - case AbstractQuery::HYDRATE_SIMPLEOBJECT: - $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); - break; - default: - return null; - } - - if ($queryResultType === null) { - return null; - } - - switch ($methodReflection->getName()) { - case 'getSingleResult': - return $queryResultType; - case 'getOneOrNullResult': - $nullableQueryResultType = TypeCombinator::addNull($queryResultType); - if ($queryResultType instanceof BenevolentUnionType) { - $nullableQueryResultType = TypeUtils::toBenevolentUnion($nullableQueryResultType); - } - - return $nullableQueryResultType; - case 'toIterable': - return new IterableType( - $queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType, - $queryResultType - ); - default: - if ($queryKeyType->isNull()->yes()) { - return AccessoryArrayListType::intersectWith(new ArrayType( - new IntegerType(), - $queryResultType - )); - } - return new ArrayType( - $queryKeyType, - $queryResultType - ); - } - } - - /** - * When we're array-hydrating object, we're not sure of the shape of the array. - * We could return `new ArrayTyp(new MixedType(), new MixedType())` - * but the lack of precision in the array keys/values would give false positive. - * - * @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 - */ - private function getArrayHydratedReturnType(Type $queryResultType): ?Type - { - $objectManager = $this->objectMetadataResolver->getObjectManager(); - - $mixedFound = false; - $queryResultType = TypeTraverser::map( - $queryResultType, - static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type { - $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); - if ($isObject->no()) { - return $traverse($type); - } - if ( - $isObject->maybe() - || !$type instanceof TypeWithClassName - || $objectManager === null - ) { - $mixedFound = true; - - return new MixedType(); - } - - /** @var class-string $className */ - $className = $type->getClassName(); - if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) { - return $traverse($type); - } - - $mixedFound = true; - - return new MixedType(); - } + return $this->hydrationModeReturnTypeResolver->getMethodReturnTypeForHydrationMode( + $methodReflection->getName(), + $hydrationMode->getValue(), + $queryType->getTemplateType(AbstractQuery::class, 'TKey'), + $queryType->getTemplateType(AbstractQuery::class, 'TResult'), + $this->objectMetadataResolver->getObjectManager() ); - - return $mixedFound ? null : $queryResultType; - } - - private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type - { - if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { - return $queryResultType; - } - - return null; } } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php new file mode 100644 index 00000000..de3bf93b --- /dev/null +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -0,0 +1,484 @@ +getMetadataFactory()->getAllMetadata(); + $schemaTool->dropSchema($classes); + $schemaTool->createSchema($classes); + + $simple = new Simple(); + $simple->id = '1'; + $simple->intColumn = 1; + $simple->floatColumn = 0.1; + $simple->decimalColumn = '1.1'; + $simple->stringColumn = 'foobar'; + $simple->stringNullColumn = null; + + $entityManager->persist($simple); + $entityManager->flush(); + + $query = $entityManager->createQuery($dql); + + $typeBuilder = new QueryResultTypeBuilder(); + + QueryResultTypeWalker::walk( + $query, + $typeBuilder, + self::getContainer()->getByType(DescriptorRegistry::class), + self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(DriverDetector::class) + ); + + $resolver = self::getContainer()->getByType(HydrationModeReturnTypeResolver::class); + + $type = $resolver->getMethodReturnTypeForHydrationMode( + $methodName, + $this->getRealHydrationMode($methodName, $hydrationMode), + $typeBuilder->getIndexType(), + $typeBuilder->getResultType(), + $entityManager + ) ?? new MixedType(); + + self::assertSame( + $expectedType->describe(VerbosityLevel::precise()), + $type->describe(VerbosityLevel::precise()) + ); + + $query = $entityManager->createQuery($dql); + $result = $this->getQueryResult($query, $methodName, $hydrationMode); + + $resultType = ConstantTypeHelper::getTypeFromValue($result); + self::assertTrue( + $type->accepts($resultType, true)->yes(), + sprintf( + "The inferred type\n%s\nshould accept actual type\n%s", + $type->describe(VerbosityLevel::precise()), + $resultType->describe(VerbosityLevel::precise()) + ) + ); + } + + /** + * @return iterable + */ + public static function getTestData(): iterable + { + AccessoryArrayListType::setListTypeEnabled(true); + + yield 'getResult(object), full entity' => [ + self::list(new ObjectType(Simple::class)), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_OBJECT, + ]; + + yield 'getResult(simple_object), full entity' => [ + self::list(new ObjectType(Simple::class)), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_SIMPLEOBJECT, + ]; + + yield 'getResult(array), full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_ARRAY, + ]; + + yield 'getResult(array), fields' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('floatColumn'), new FloatType()], + ])), + ' + SELECT s.decimalColumn, s.floatColumn + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_ARRAY, + ]; + + yield 'getResult(array), expressions' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], + [new ConstantStringType('floatColumn'), self::floatOrStringified()], + ])), + ' + SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_ARRAY, + ]; + + yield 'getResult(object), fields' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('floatColumn'), new FloatType()], + ])), + ' + SELECT s.decimalColumn, s.floatColumn + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_OBJECT, + ]; + + yield 'getResult(object), expressions' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], + [new ConstantStringType('floatColumn'), self::floatOrStringified()], + ])), + ' + SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_OBJECT, + ]; + + yield 'toIterable(object), full entity' => [ + new IterableType(new IntegerType(), new ObjectType(Simple::class)), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'toIterable', + Query::HYDRATE_OBJECT, + ]; + + yield 'toIterable(object), fields' => [ + new IterableType(new IntegerType(), self::constantArray([ + [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('floatColumn'), new FloatType()], + ])), + ' + SELECT s.decimalColumn, s.floatColumn + FROM QueryResult\Entities\Simple s + ', + 'toIterable', + Query::HYDRATE_OBJECT, + ]; + + yield 'toIterable(object), expressions' => [ + new IterableType(new IntegerType(), self::constantArray([ + [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], + [new ConstantStringType('floatColumn'), self::floatOrStringified()], + ])), + ' + SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn + FROM QueryResult\Entities\Simple s + ', + 'toIterable', + Query::HYDRATE_OBJECT, + ]; + + yield 'toIterable(simple_object), full entity' => [ + new IterableType(new IntegerType(), new ObjectType(Simple::class)), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'toIterable', + Query::HYDRATE_SIMPLEOBJECT, + ]; + + yield 'toIterable(array), full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'toIterable', + Query::HYDRATE_ARRAY, + ]; + + yield 'getArrayResult(), full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getArrayResult', + ]; + + yield 'getArrayResult(), fields' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('floatColumn'), new FloatType()], + ])), + ' + SELECT s.decimalColumn, s.floatColumn + FROM QueryResult\Entities\Simple s + ', + 'getArrayResult', + ]; + + yield 'getArrayResult(), expressions' => [ + self::list(self::constantArray([ + [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], + [new ConstantStringType('floatColumn'), self::floatOrStringified()], + ])), + ' + SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn + FROM QueryResult\Entities\Simple s + ', + 'getArrayResult', + ]; + + yield 'getResult(single_scalar), decimal field' => [ + new MixedType(), + ' + SELECT s.decimalColumn + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_SINGLE_SCALAR, + ]; + + yield 'getResult(scalar), full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_SCALAR, + ]; + + yield 'getResult(scalar), decimal field' => [ + new MixedType(), + ' + SELECT s.decimalColumn + FROM QueryResult\Entities\Simple s + ', + 'getResult', + Query::HYDRATE_SCALAR, + ]; + + yield 'getScalarResult, full entity' => [ + new MixedType(), + ' + SELECT s + FROM QueryResult\Entities\Simple s + ', + 'getScalarResult', + ]; + + yield 'getScalarResult(), decimal field' => [ + new MixedType(), + ' + SELECT s.decimalColumn + FROM QueryResult\Entities\Simple s + ', + 'getScalarResult', + ]; + + yield 'getScalarResult(), decimal expression' => [ + new MixedType(), + ' + SELECT -s.decimalColumn as col + FROM QueryResult\Entities\Simple s + ', + 'getScalarResult', + ]; + + yield 'getSingleScalarResult(), decimal field' => [ + new MixedType(), + ' + SELECT s.decimalColumn + FROM QueryResult\Entities\Simple s + ', + 'getSingleScalarResult', + ]; + } + + /** + * @param array $elements + */ + private static function constantArray(array $elements): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($elements as $element) { + $offsetType = $element[0]; + $valueType = $element[1]; + $optional = $element[2] ?? false; + $builder->setOffsetValueType($offsetType, $valueType, $optional); + } + + return $builder->getArray(); + } + + private static function list(Type $values): Type + { + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $values)); + } + + private static function numericString(): Type + { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + + /** + * @param Query $query + * @return mixed + */ + private function getQueryResult(Query $query, string $methodName, ?int $hydrationMode) + { + if ($methodName === 'getResult') { + if ($hydrationMode === null) { + throw new LogicException('Hydration mode must be set for getResult() method.'); + } + return $query->getResult($hydrationMode); // @phpstan-ignore-line dynamic arg + } + + if ($methodName === 'getArrayResult') { + if ($hydrationMode !== null) { + throw new LogicException('Hydration mode must NOT be set for getArrayResult() method.'); + } + return $query->getArrayResult(); + } + + if ($methodName === 'getScalarResult') { + if ($hydrationMode !== null) { + throw new LogicException('Hydration mode must NOT be set for getScalarResult() method.'); + } + return $query->getScalarResult(); + } + + if ($methodName === 'getSingleResult') { + if ($hydrationMode === null) { + throw new LogicException('Hydration mode must be set for getSingleResult() method.'); + } + + return $query->getSingleResult($hydrationMode); // @phpstan-ignore-line dynamic arg + } + + if ($methodName === 'getSingleScalarResult') { + if ($hydrationMode !== null) { + throw new LogicException('Hydration mode must NOT be set for getSingleScalarResult() method.'); + } + + return $query->getSingleScalarResult(); + } + + if ($methodName === 'toIterable') { + if ($hydrationMode === null) { + throw new LogicException('Hydration mode must be set for toIterable() method.'); + } + + return $query->toIterable([], $hydrationMode); // @phpstan-ignore-line dynamic arg + } + + throw new LogicException(sprintf('Unsupported method %s.', $methodName)); + } + + private function getRealHydrationMode(string $methodName, ?int $hydrationMode): int + { + if ($hydrationMode !== null) { + return $hydrationMode; + } + + if ($methodName === 'getArrayResult') { + return Query::HYDRATE_ARRAY; + } + + if ($methodName === 'getScalarResult') { + return Query::HYDRATE_SCALAR; + } + + if ($methodName === 'getSingleScalarResult') { + return Query::HYDRATE_SCALAR; + } + + throw new LogicException(sprintf('Using %s without hydration mode is not supported.', $methodName)); + } + + + private static function stringifies(): bool + { + return PHP_VERSION_ID < 80100; + } + + private static function floatOrStringified(): Type + { + return self::stringifies() + ? self::numericString() + : new FloatType(); + } + + private static function floatOrIntOrStringified(): Type + { + return self::stringifies() + ? self::numericString() + : TypeCombinator::union(new FloatType(), new IntegerType()); + } + +} diff --git a/tests/Type/Doctrine/data/QueryResult/Entities/Simple.php b/tests/Type/Doctrine/data/QueryResult/Entities/Simple.php new file mode 100644 index 00000000..5169a9e5 --- /dev/null +++ b/tests/Type/Doctrine/data/QueryResult/Entities/Simple.php @@ -0,0 +1,71 @@ + Date: Fri, 28 Jun 2024 15:33:57 +0200 Subject: [PATCH 050/160] Affected rows can be `int<0, max>` --- src/Type/Doctrine/HydrationModeReturnTypeResolver.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php index 301731f5..cfcc5f45 100644 --- a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -7,6 +7,7 @@ use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; @@ -34,7 +35,7 @@ public function getMethodReturnTypeForHydrationMode( if ($isVoidType->yes()) { // A void query result type indicates an UPDATE or DELETE query. // In this case all methods return the number of affected rows. - return new IntegerType(); + return IntegerRangeType::fromInterval(0, null); } if ($isVoidType->maybe()) { From 66d6943cd8d7ab18efd5705aa72ac0ae177e2c16 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 28 Jun 2024 15:34:13 +0200 Subject: [PATCH 051/160] Typo --- src/Type/Doctrine/HydrationModeReturnTypeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php index cfcc5f45..c37a0c96 100644 --- a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -92,7 +92,7 @@ public function getMethodReturnTypeForHydrationMode( /** * When we're array-hydrating object, we're not sure of the shape of the array. - * We could return `new ArrayTyp(new MixedType(), new MixedType())` + * We could return `new ArrayType(new MixedType(), new MixedType())` * but the lack of precision in the array keys/values would give false positive. * * @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 From 6051e97bf3aa530e0a1d8f8bf5489ee7649396ca Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 28 Jun 2024 16:49:09 +0200 Subject: [PATCH 052/160] Do not infer getArrayResult for now --- .../HydrationModeReturnTypeResolver.php | 48 ------------- .../QueryResultDynamicReturnTypeExtension.php | 37 ++++------ ...QueryResultTypeWalkerHydrationModeTest.php | 70 ------------------- .../Doctrine/data/QueryResult/queryResult.php | 35 ---------- 4 files changed, 15 insertions(+), 175 deletions(-) diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php index c37a0c96..dabf4d9b 100644 --- a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -10,13 +10,10 @@ use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IterableType; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VoidType; class HydrationModeReturnTypeResolver @@ -47,9 +44,6 @@ public function getMethodReturnTypeForHydrationMode( switch ($hydrationMode) { case AbstractQuery::HYDRATE_OBJECT: break; - case AbstractQuery::HYDRATE_ARRAY: - $queryResultType = $this->getArrayHydratedReturnType($queryResultType, $objectManager); - break; case AbstractQuery::HYDRATE_SIMPLEOBJECT: $queryResultType = $this->getSimpleObjectHydratedReturnType($queryResultType); break; @@ -90,48 +84,6 @@ public function getMethodReturnTypeForHydrationMode( } } - /** - * When we're array-hydrating object, we're not sure of the shape of the array. - * We could return `new ArrayType(new MixedType(), new MixedType())` - * but the lack of precision in the array keys/values would give false positive. - * - * @see https://github.com/phpstan/phpstan-doctrine/pull/412#issuecomment-1497092934 - */ - private function getArrayHydratedReturnType(Type $queryResultType, ?ObjectManager $objectManager): ?Type - { - $mixedFound = false; - $queryResultType = TypeTraverser::map( - $queryResultType, - static function (Type $type, callable $traverse) use ($objectManager, &$mixedFound): Type { - $isObject = (new ObjectWithoutClassType())->isSuperTypeOf($type); - if ($isObject->no()) { - return $traverse($type); - } - if ( - $isObject->maybe() - || !$type instanceof TypeWithClassName - || $objectManager === null - ) { - $mixedFound = true; - - return new MixedType(); - } - - /** @var class-string $className */ - $className = $type->getClassName(); - if (!$objectManager->getMetadataFactory()->hasMetadataFor($className)) { - return $traverse($type); - } - - $mixedFound = true; - - return new MixedType(); - } - ); - - return $mixedFound ? null : $queryResultType; - } - private function getSimpleObjectHydratedReturnType(Type $queryResultType): ?Type { if ((new ObjectWithoutClassType())->isSuperTypeOf($queryResultType)->yes()) { diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 818c3869..ef638cad 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -28,10 +28,6 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn 'getSingleResult' => 0, ]; - private const METHOD_HYDRATION_MODE = [ - 'getArrayResult' => AbstractQuery::HYDRATE_ARRAY, - ]; - /** @var ObjectMetadataResolver */ private $objectMetadataResolver; @@ -54,8 +50,7 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]) - || isset(self::METHOD_HYDRATION_MODE[$methodReflection->getName()]); + return isset(self::METHOD_HYDRATION_MODE_ARG[$methodReflection->getName()]); } public function getTypeFromMethodCall( @@ -66,25 +61,23 @@ public function getTypeFromMethodCall( { $methodName = $methodReflection->getName(); - if (isset(self::METHOD_HYDRATION_MODE[$methodName])) { - $hydrationMode = new ConstantIntegerType(self::METHOD_HYDRATION_MODE[$methodName]); - } elseif (isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { - $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; - $args = $methodCall->getArgs(); - - if (isset($args[$argIndex])) { - $hydrationMode = $scope->getType($args[$argIndex]->value); - } else { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() - ); - $parameter = $parametersAcceptor->getParameters()[$argIndex]; - $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); - } - } else { + if (!isset(self::METHOD_HYDRATION_MODE_ARG[$methodName])) { throw new ShouldNotHappenException(); } + $argIndex = self::METHOD_HYDRATION_MODE_ARG[$methodName]; + $args = $methodCall->getArgs(); + + if (isset($args[$argIndex])) { + $hydrationMode = $scope->getType($args[$argIndex]->value); + } else { + $parametersAcceptor = ParametersAcceptorSelector::selectSingle( + $methodReflection->getVariants() + ); + $parameter = $parametersAcceptor->getParameters()[$argIndex]; + $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); + } + $queryType = $scope->getType($methodCall->var); if (!$hydrationMode instanceof ConstantIntegerType) { diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index de3bf93b..f78b81a4 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -138,42 +138,6 @@ public static function getTestData(): iterable Query::HYDRATE_SIMPLEOBJECT, ]; - yield 'getResult(array), full entity' => [ - new MixedType(), - ' - SELECT s - FROM QueryResult\Entities\Simple s - ', - 'getResult', - Query::HYDRATE_ARRAY, - ]; - - yield 'getResult(array), fields' => [ - self::list(self::constantArray([ - [new ConstantStringType('decimalColumn'), self::numericString()], - [new ConstantStringType('floatColumn'), new FloatType()], - ])), - ' - SELECT s.decimalColumn, s.floatColumn - FROM QueryResult\Entities\Simple s - ', - 'getResult', - Query::HYDRATE_ARRAY, - ]; - - yield 'getResult(array), expressions' => [ - self::list(self::constantArray([ - [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], - [new ConstantStringType('floatColumn'), self::floatOrStringified()], - ])), - ' - SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn - FROM QueryResult\Entities\Simple s - ', - 'getResult', - Query::HYDRATE_ARRAY, - ]; - yield 'getResult(object), fields' => [ self::list(self::constantArray([ [new ConstantStringType('decimalColumn'), self::numericString()], @@ -246,16 +210,6 @@ public static function getTestData(): iterable Query::HYDRATE_SIMPLEOBJECT, ]; - yield 'toIterable(array), full entity' => [ - new MixedType(), - ' - SELECT s - FROM QueryResult\Entities\Simple s - ', - 'toIterable', - Query::HYDRATE_ARRAY, - ]; - yield 'getArrayResult(), full entity' => [ new MixedType(), ' @@ -265,30 +219,6 @@ public static function getTestData(): iterable 'getArrayResult', ]; - yield 'getArrayResult(), fields' => [ - self::list(self::constantArray([ - [new ConstantStringType('decimalColumn'), self::numericString()], - [new ConstantStringType('floatColumn'), new FloatType()], - ])), - ' - SELECT s.decimalColumn, s.floatColumn - FROM QueryResult\Entities\Simple s - ', - 'getArrayResult', - ]; - - yield 'getArrayResult(), expressions' => [ - self::list(self::constantArray([ - [new ConstantStringType('decimalColumn'), self::floatOrIntOrStringified()], - [new ConstantStringType('floatColumn'), self::floatOrStringified()], - ])), - ' - SELECT -s.decimalColumn as decimalColumn, -s.floatColumn as floatColumn - FROM QueryResult\Entities\Simple s - ', - 'getArrayResult', - ]; - yield 'getResult(single_scalar), decimal field' => [ new MixedType(), ' diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index 46734609..de4cc5bf 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -188,41 +188,6 @@ public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(Entit 'mixed', $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) ); - - - $query = $em->createQuery(' - SELECT m.intColumn, m.stringNullColumn, m.datetimeColumn - FROM QueryResult\Entities\Many m - '); - - assertType( - 'list', - $query->getResult(AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'list', - $query->getArrayResult() - ); - assertType( - 'list', - $query->execute(null, AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'list', - $query->executeIgnoreQueryCache(null, AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'list', - $query->executeUsingQueryCache(null, AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}', - $query->getSingleResult(AbstractQuery::HYDRATE_ARRAY) - ); - assertType( - 'array{intColumn: int, stringNullColumn: string|null, datetimeColumn: DateTime}|null', - $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY) - ); } From 2d3c230bf16aa00782f62d3af523de70d638a1d6 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 28 Jun 2024 15:50:42 +0200 Subject: [PATCH 053/160] QueryResultTypeWalkerTest: make dataset cases clickable in PHPStorm --- .../Query/QueryResultTypeWalkerTest.php | 248 +++++++++--------- 1 file changed, 119 insertions(+), 129 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 6daf17df..003a0737 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -254,9 +254,6 @@ public function test(Type $expectedType, string $dql, ?string $expectedException */ public function getTestData(): iterable { - $ormVersion = InstalledVersions::getVersion('doctrine/orm'); - $hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0; - $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); $hasDbal4 = $dbalVersion !== null && strpos($dbalVersion, '4.') === 0; @@ -537,48 +534,6 @@ public function getTestData(): iterable ', ]; - if (property_exists(Column::class, 'enumType') && PHP_VERSION_ID >= 80100) { - assert(class_exists(StringEnum::class)); - assert(class_exists(IntEnum::class)); - - // https://github.com/doctrine/orm/issues/9622 - if (!$this->isDoctrine211()) { - yield 'enum' => [ - $this->constantArray([ - [new ConstantStringType('stringEnumColumn'), new ObjectType(StringEnum::class)], - [new ConstantStringType('intEnumColumn'), new ObjectType(IntEnum::class)], - ]), - ' - SELECT e.stringEnumColumn, e.intEnumColumn - FROM QueryResult\EntitiesEnum\EntityWithEnum e - ', - ]; - } - - yield 'enum in expression' => [ - $this->constantArray([ - [ - new ConstantIntegerType(1), - new StringType(), - ], - [ - new ConstantIntegerType(2), - new IntegerType(), - ], - [ - new ConstantIntegerType(3), - $this->numericString(), - ], - ]), - ' - SELECT COALESCE(e.stringEnumColumn, e.stringEnumColumn), - COALESCE(e.intEnumColumn, e.intEnumColumn), - COALESCE(e.intEnumOnStringColumn, e.intEnumOnStringColumn) - FROM QueryResult\EntitiesEnum\EntityWithEnum e - ', - ]; - } - yield 'hidden' => [ $this->constantArray([ [new ConstantStringType('intColumn'), new IntegerType()], @@ -1275,40 +1230,6 @@ public function getTestData(): iterable ', ]; - if (!$hasOrm3) { - yield 'date_add function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new StringType()], - [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(3), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(4), new StringType()], - ]), - ' - SELECT DATE_ADD(m.datetimeColumn, m.intColumn, \'day\'), - DATE_ADD(m.stringNullColumn, m.intColumn, \'day\'), - DATE_ADD(m.datetimeColumn, NULLIF(m.intColumn, 1), \'day\'), - DATE_ADD(\'2020-01-01\', 7, \'day\') - FROM QueryResult\Entities\Many m - ', - ]; - - yield 'date_sub function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new StringType()], - [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(3), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(4), new StringType()], - ]), - ' - SELECT DATE_SUB(m.datetimeColumn, m.intColumn, \'day\'), - DATE_SUB(m.stringNullColumn, m.intColumn, \'day\'), - DATE_SUB(m.datetimeColumn, NULLIF(m.intColumn, 1), \'day\'), - DATE_SUB(\'2020-01-01\', 7, \'day\') - FROM QueryResult\Entities\Many m - ', - ]; - } - yield 'date_diff function' => [ $this->constantArray([ [new ConstantIntegerType(1), $this->floatOrStringified()], @@ -1325,26 +1246,6 @@ public function getTestData(): iterable ', ]; - /*yield 'sqrt function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), $this->floatStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->floatStringified())], - [new ConstantIntegerType(3), $this->floatStringified()], - ]), - ' - SELECT SQRT(m.intColumn), - SQRT(NULLIF(m.intColumn, 1)), - SQRT(1) - FROM QueryResult\Entities\Many m - ', - InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '<3') && PHP_VERSION_ID >= 80100 - ? 'sqrt(): Passing null to parameter #1 ($num) of type float is deprecated' - : null, - InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=3') && PHP_VERSION_ID >= 80100 - ? 'sqrt(): Passing null to parameter #1 ($num) of type float is deprecated' - : null, - ];*/ - yield 'length function' => [ $this->constantArray([ [ @@ -1368,36 +1269,6 @@ public function getTestData(): iterable ', ]; - if (PHP_VERSION_ID >= 70400) { - yield 'locate function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), $this->uintOrStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintOrStringified())], - [new ConstantIntegerType(4), $this->uintOrStringified()], - ]), - ' - SELECT LOCATE(m.stringColumn, m.stringColumn, 0), - LOCATE(m.stringNullColumn, m.stringColumn, 0), - LOCATE(m.stringColumn, m.stringNullColumn, 0), - LOCATE(\'f\', \'foo\', 0) - FROM QueryResult\Entities\Many m - ', - null, - InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=3.4') - ? null - : ( - PHP_VERSION_ID >= 80100 - ? 'strpos(): Passing null to parameter #2 ($needle) of type string is deprecated' - : ( - PHP_VERSION_ID < 80000 - ? 'strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior' - : null - ) - ), - ]; - } - yield 'lower function' => [ $this->constantArray([ [new ConstantIntegerType(1), new StringType()], @@ -1609,6 +1480,125 @@ public function getTestData(): iterable FROM QueryResult\Entities\One o ', ]; + + yield from $this->yieldConditionalDataset(); + } + + /** + * @return iterable + */ + private function yieldConditionalDataset(): iterable + { + if (property_exists(Column::class, 'enumType') && PHP_VERSION_ID >= 80100) { + assert(class_exists(StringEnum::class)); + assert(class_exists(IntEnum::class)); + + // https://github.com/doctrine/orm/issues/9622 + if (!$this->isDoctrine211()) { + yield 'enum' => [ + $this->constantArray([ + [new ConstantStringType('stringEnumColumn'), new ObjectType(StringEnum::class)], + [new ConstantStringType('intEnumColumn'), new ObjectType(IntEnum::class)], + ]), + ' + SELECT e.stringEnumColumn, e.intEnumColumn + FROM QueryResult\EntitiesEnum\EntityWithEnum e + ', + ]; + } + + yield 'enum in expression' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + new StringType(), + ], + [ + new ConstantIntegerType(2), + new IntegerType(), + ], + [ + new ConstantIntegerType(3), + $this->numericString(), + ], + ]), + ' + SELECT COALESCE(e.stringEnumColumn, e.stringEnumColumn), + COALESCE(e.intEnumColumn, e.intEnumColumn), + COALESCE(e.intEnumOnStringColumn, e.intEnumOnStringColumn) + FROM QueryResult\EntitiesEnum\EntityWithEnum e + ', + ]; + } + + if (PHP_VERSION_ID >= 70400) { + yield 'locate function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), $this->uintOrStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(4), $this->uintOrStringified()], + ]), + ' + SELECT LOCATE(m.stringColumn, m.stringColumn, 0), + LOCATE(m.stringNullColumn, m.stringColumn, 0), + LOCATE(m.stringColumn, m.stringNullColumn, 0), + LOCATE(\'f\', \'foo\', 0) + FROM QueryResult\Entities\Many m + ', + null, + InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=3.4') + ? null + : ( + PHP_VERSION_ID >= 80100 + ? 'strpos(): Passing null to parameter #2 ($needle) of type string is deprecated' + : ( + PHP_VERSION_ID < 80000 + ? 'strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior' + : null + ) + ), + ]; + } + + $ormVersion = InstalledVersions::getVersion('doctrine/orm'); + $hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0; + + if ($hasOrm3) { + return; + } + + yield 'date_add function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new StringType()], + [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(3), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(4), new StringType()], + ]), + ' + SELECT DATE_ADD(m.datetimeColumn, m.intColumn, \'day\'), + DATE_ADD(m.stringNullColumn, m.intColumn, \'day\'), + DATE_ADD(m.datetimeColumn, NULLIF(m.intColumn, 1), \'day\'), + DATE_ADD(\'2020-01-01\', 7, \'day\') + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'date_sub function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new StringType()], + [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(3), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(4), new StringType()], + ]), + ' + SELECT DATE_SUB(m.datetimeColumn, m.intColumn, \'day\'), + DATE_SUB(m.stringNullColumn, m.intColumn, \'day\'), + DATE_SUB(m.datetimeColumn, NULLIF(m.intColumn, 1), \'day\'), + DATE_SUB(\'2020-01-01\', 7, \'day\') + FROM QueryResult\Entities\Many m + ', + ]; } /** From 2004f84e520dc70b5bebaf29ad7604ae262f4855 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 2 Jul 2024 19:00:50 +0200 Subject: [PATCH 054/160] Fix Query::execute() return type --- src/Type/Doctrine/HydrationModeReturnTypeResolver.php | 9 +++++++-- .../Query/QueryResultDynamicReturnTypeExtension.php | 7 +------ .../Query/QueryResultTypeWalkerHydrationModeTest.php | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php index dabf4d9b..2f1c8e36 100644 --- a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -7,6 +7,7 @@ use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IterableType; @@ -21,7 +22,7 @@ class HydrationModeReturnTypeResolver public function getMethodReturnTypeForHydrationMode( string $methodName, - int $hydrationMode, + Type $hydrationMode, Type $queryKeyType, Type $queryResultType, ?ObjectManager $objectManager @@ -41,7 +42,11 @@ public function getMethodReturnTypeForHydrationMode( return null; } - switch ($hydrationMode) { + if (!$hydrationMode instanceof ConstantIntegerType) { + return null; + } + + switch ($hydrationMode->getValue()) { case AbstractQuery::HYDRATE_OBJECT: break; case AbstractQuery::HYDRATE_SIMPLEOBJECT: diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index ef638cad..091e3716 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -8,7 +8,6 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -80,13 +79,9 @@ public function getTypeFromMethodCall( $queryType = $scope->getType($methodCall->var); - if (!$hydrationMode instanceof ConstantIntegerType) { - return null; - } - return $this->hydrationModeReturnTypeResolver->getMethodReturnTypeForHydrationMode( $methodReflection->getName(), - $hydrationMode->getValue(), + $hydrationMode, $queryType->getTemplateType(AbstractQuery::class, 'TKey'), $queryType->getTemplateType(AbstractQuery::class, 'TResult'), $this->objectMetadataResolver->getObjectManager() diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index f78b81a4..b9d02c85 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -86,7 +86,7 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ $type = $resolver->getMethodReturnTypeForHydrationMode( $methodName, - $this->getRealHydrationMode($methodName, $hydrationMode), + new ConstantIntegerType($this->getRealHydrationMode($methodName, $hydrationMode)), $typeBuilder->getIndexType(), $typeBuilder->getResultType(), $entityManager From c87ee295de7573d9c015c492693e7af7f8cd85dc Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 2 Jul 2024 17:32:42 +0200 Subject: [PATCH 055/160] Improve COALESCE inference for MySQL --- .../Doctrine/Query/QueryResultTypeWalker.php | 88 +++++++- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 198 +++++++++++++++++- tests/Platform/README.md | 2 +- 3 files changed, 270 insertions(+), 18 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index e5040fb8..5157a631 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -978,6 +978,7 @@ public function walkJoin($join): string */ public function walkCoalesceExpression($coalesceExpression): string { + $rawTypes = []; $expressionTypes = []; $allTypesContainNull = true; @@ -987,22 +988,67 @@ public function walkCoalesceExpression($coalesceExpression): string continue; } - $type = $this->unmarshalType($expression->dispatch($this)); - $allTypesContainNull = $allTypesContainNull && $this->canBeNull($type); + $rawType = $this->unmarshalType($expression->dispatch($this)); + $rawTypes[] = $rawType; + + $allTypesContainNull = $allTypesContainNull && $this->canBeNull($rawType); // Some drivers manipulate the types, lets avoid false positives by generalizing constant types // e.g. sqlsrv: "COALESCE returns the data type of value with the highest precedence" // e.g. mysql: COALESCE(1, 'foo') === '1' (undocumented? https://gist.github.com/jrunning/4535434) - $expressionTypes[] = $this->generalizeConstantType($type, false); + $expressionTypes[] = $this->generalizeConstantType($rawType, false); } - $type = TypeCombinator::union(...$expressionTypes); + $generalizedUnion = TypeCombinator::union(...$expressionTypes); if (!$allTypesContainNull) { - $type = TypeCombinator::removeNull($type); + $generalizedUnion = TypeCombinator::removeNull($generalizedUnion); } - return $this->marshalType($type); + if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL) { + return $this->marshalType( + $this->inferCoalesceForMySql($rawTypes, $generalizedUnion) + ); + } + + return $this->marshalType($generalizedUnion); + } + + /** + * @param list $rawTypes + */ + private function inferCoalesceForMySql(array $rawTypes, Type $originalResult): Type + { + $containsString = false; + $containsFloat = false; + $allIsNumericExcludingLiteralString = true; + + foreach ($rawTypes as $rawType) { + $rawTypeNoNull = TypeCombinator::removeNull($rawType); + $isLiteralString = $rawTypeNoNull instanceof DqlConstantStringType && $rawTypeNoNull->getOriginLiteralType() === AST\Literal::STRING; + + if (!$this->containsOnlyNumericTypes($rawTypeNoNull) || $isLiteralString) { + $allIsNumericExcludingLiteralString = false; + } + + if ($rawTypeNoNull->isString()->yes()) { + $containsString = true; + } + + if (!$rawTypeNoNull->isFloat()->yes()) { + continue; + } + + $containsFloat = true; + } + + if ($containsFloat && $allIsNumericExcludingLiteralString) { + return $this->simpleFloatify($originalResult); + } elseif ($containsString) { + return $this->simpleStringify($originalResult); + } + + return $originalResult; } /** @@ -2107,4 +2153,34 @@ private function isSupportedDriver(): bool ], true); } + private function simpleStringify(Type $type): Type + { + return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof IntegerType || $type instanceof FloatType || $type instanceof BooleanType) { + return $type->toString(); + } + + return $traverse($type); + }); + } + + private function simpleFloatify(Type $type): Type + { + return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof IntegerType || $type instanceof BooleanType || $type instanceof StringType) { + return $type->toFloat(); + } + + return $traverse($type); + }); + } + } diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 2623181e..dcf9221f 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -3980,7 +3980,7 @@ public static function provideCases(): iterable yield 'COALESCE(SUM(t.col_int_nullable), 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(SUM(t.col_int_nullable), 0) FROM %s t', - 'mysql' => TypeCombinator::union(self::numericString(), self::int()), + 'mysql' => self::numericString(), 'sqlite' => self::int(), 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), @@ -3996,7 +3996,7 @@ public static function provideCases(): iterable yield 'COALESCE(SUM(ABS(t.col_int)), 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(SUM(ABS(t.col_int)), 0) FROM %s t', - 'mysql' => TypeCombinator::union(self::int(), self::numericString()), + 'mysql' => self::numericString(), 'sqlite' => self::int(), 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), @@ -4012,7 +4012,7 @@ public static function provideCases(): iterable yield "COALESCE(t.col_int_nullable, 'foo')" => [ 'data' => self::dataDefault(), 'select' => "SELECT COALESCE(t.col_int_nullable, 'foo') FROM %s t", - 'mysql' => TypeCombinator::union(self::int(), self::string()), + 'mysql' => self::string(), 'sqlite' => TypeCombinator::union(self::int(), self::string()), 'pdo_pgsql' => null, // COALESCE types cannot be matched 'pgsql' => null, // COALESCE types cannot be matched @@ -4028,7 +4028,7 @@ public static function provideCases(): iterable yield "COALESCE(t.col_int, 'foo')" => [ 'data' => self::dataDefault(), 'select' => "SELECT COALESCE(t.col_int, 'foo') FROM %s t", - 'mysql' => TypeCombinator::union(self::int(), self::string()), + 'mysql' => self::string(), 'sqlite' => TypeCombinator::union(self::int(), self::string()), 'pdo_pgsql' => null, // COALESCE types cannot be matched 'pgsql' => null, // COALESCE types cannot be matched @@ -4044,7 +4044,7 @@ public static function provideCases(): iterable yield "COALESCE(t.col_bool, 'foo')" => [ 'data' => self::dataDefault(), 'select' => "SELECT COALESCE(t.col_bool, 'foo') FROM %s t", - 'mysql' => TypeCombinator::union(self::int(), self::string()), + 'mysql' => self::string(), 'sqlite' => TypeCombinator::union(self::int(), self::string()), 'pdo_pgsql' => null, // COALESCE types cannot be matched 'pgsql' => null, // COALESCE types cannot be matched @@ -4060,7 +4060,7 @@ public static function provideCases(): iterable yield "COALESCE(1, 'foo')" => [ 'data' => self::dataDefault(), 'select' => "SELECT COALESCE(1, 'foo') FROM %s t", - 'mysql' => TypeCombinator::union(self::int(), self::string()), + 'mysql' => self::string(), 'sqlite' => TypeCombinator::union(self::int(), self::string()), 'pdo_pgsql' => null, // COALESCE types cannot be matched 'pgsql' => null, // COALESCE types cannot be matched @@ -4073,6 +4073,86 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_DEFAULT, ]; + yield "COALESCE(1, '1')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT COALESCE(1, '1') FROM %s t", + 'mysql' => self::numericString(), + 'sqlite' => TypeCombinator::union(self::int(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'mssql' => self::mixed(), + 'mysqlResult' => '1', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => 1, + 'pgsqlResult' => 1, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(1, 1.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(1, 1.0) FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => TypeCombinator::union(self::int(), self::float()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(1e0, 1.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(1e0, 1.0) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(1, 1.0, 1e0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(1, 1.0, 1e0) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => TypeCombinator::union(self::float(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "COALESCE(1, 1.0, 1e0, '1')" => [ + 'data' => self::dataDefault(), + 'select' => "SELECT COALESCE(1, 1.0, 1e0, '1') FROM %s t", + 'mysql' => self::numericString(), + 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'mssql' => self::mixed(), + 'mysqlResult' => '1', + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => '1', + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + yield 'COALESCE(t.col_int_nullable, 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(t.col_int_nullable, 0) FROM %s t', @@ -4089,10 +4169,26 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_DEFAULT, ]; + yield 'COALESCE(t.col_int_nullable, t.col_bool)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_bool) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => null, // COALESCE types cannot be matched + 'pgsql' => null, // COALESCE types cannot be matched + 'mssql' => self::mixed(), + 'mysqlResult' => 1, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 1, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + yield 'COALESCE(t.col_float_nullable, 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(t.col_float_nullable, 0) FROM %s t', - 'mysql' => TypeCombinator::union(self::float(), self::int()), + 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), 'pgsql' => TypeCombinator::union(self::float(), self::int()), @@ -4108,7 +4204,7 @@ public static function provideCases(): iterable yield 'COALESCE(t.col_float_nullable, 0.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(t.col_float_nullable, 0.0) FROM %s t', - 'mysql' => TypeCombinator::union(self::float(), self::numericString()), + 'mysql' => self::float(), 'sqlite' => self::float(), 'pdo_pgsql' => self::numericString(), 'pgsql' => TypeCombinator::union(self::float(), self::numericString()), @@ -4124,7 +4220,7 @@ public static function provideCases(): iterable yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, 0) FROM %s t', - 'mysql' => TypeCombinator::union(self::numericString(), self::int()), + 'mysql' => self::numericString(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), @@ -4140,7 +4236,7 @@ public static function provideCases(): iterable yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0) FROM %s t', - 'mysql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), @@ -4153,10 +4249,58 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_DEFAULT, ]; + yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => TypeCombinator::union(self::float(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => '0', + 'pgsqlResult' => 0.0, + 'mssqlResult' => 0.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => TypeCombinator::union(self::float(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.0, + 'sqliteResult' => 0.0, + 'pdoPgsqlResult' => '0', + 'pgsqlResult' => 0.0, + 'mssqlResult' => 0.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield "COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, '0')" => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, \'0\') FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'mssql' => self::mixed(), + 'mysqlResult' => '0', + 'sqliteResult' => '0', + 'pdoPgsqlResult' => '0', + 'pgsqlResult' => 0.0, + 'mssqlResult' => 0.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_string)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_string) FROM %s t', - 'mysql' => TypeCombinator::union(self::string(), self::int(), self::float()), + 'mysql' => self::string(), 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::string()), 'pdo_pgsql' => null, // COALESCE types cannot be matched 'pgsql' => null, // COALESCE types cannot be matched @@ -4169,6 +4313,38 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_DEFAULT, ]; + yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_mixed)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_mixed) FROM %s t', + 'mysql' => self::mixed(), + 'sqlite' => self::mixed(), + 'pdo_pgsql' => self::mixed(), + 'pgsql' => self::mixed(), + 'mssql' => self::mixed(), + 'mysqlResult' => 1.0, + 'sqliteResult' => 1, + 'pdoPgsqlResult' => '1', + 'pgsqlResult' => 1.0, + 'mssqlResult' => 1.0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(t.col_string_nullable, t.col_int)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_string_nullable, t.col_int) FROM %s t', + 'mysql' => self::string(), + 'sqlite' => TypeCombinator::union(self::int(), self::string()), + 'pdo_pgsql' => null, // COALESCE types cannot be matched + 'pgsql' => null, // COALESCE types cannot be matched + 'mssql' => self::mixed(), + 'mysqlResult' => '9', + 'sqliteResult' => 9, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + yield 'IDENTITY(t.related_entity)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT IDENTITY(t.related_entity) FROM %s t', diff --git a/tests/Platform/README.md b/tests/Platform/README.md index 8784f49e..d3678117 100644 --- a/tests/Platform/README.md +++ b/tests/Platform/README.md @@ -21,5 +21,5 @@ You can also run utilize those containers for PHPStorm PHPUnit configuration. Since the dataset is huge and takes few minutes to run, you can filter only functions you are interested in: ```sh -`docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform --filter "AVG"` +docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform --filter "AVG" ``` From d453424b1df22fbd9b895990b216153415087205 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 3 Jul 2024 13:42:37 +0200 Subject: [PATCH 056/160] Describe DescriptorNotRegisteredException --- src/Type/Doctrine/DefaultDescriptorRegistry.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Type/Doctrine/DefaultDescriptorRegistry.php b/src/Type/Doctrine/DefaultDescriptorRegistry.php index 2fc81131..83647e3b 100644 --- a/src/Type/Doctrine/DefaultDescriptorRegistry.php +++ b/src/Type/Doctrine/DefaultDescriptorRegistry.php @@ -25,13 +25,13 @@ public function get(string $type): DoctrineTypeDescriptor { $typesMap = Type::getTypesMap(); if (!isset($typesMap[$type])) { - throw new DescriptorNotRegisteredException(); + throw new DescriptorNotRegisteredException($type); } /** @var class-string $typeClass */ $typeClass = $typesMap[$type]; if (!isset($this->descriptors[$typeClass])) { - throw new DescriptorNotRegisteredException(); + throw new DescriptorNotRegisteredException($typeClass); } return $this->descriptors[$typeClass]; } @@ -42,7 +42,7 @@ public function get(string $type): DoctrineTypeDescriptor public function getByClassName(string $className): DoctrineTypeDescriptor { if (!isset($this->descriptors[$className])) { - throw new DescriptorNotRegisteredException(); + throw new DescriptorNotRegisteredException($className); } return $this->descriptors[$className]; } From 6339dffef7c3168ebf705b7737e0022d0038d78f Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 12 Jul 2024 13:02:43 +0200 Subject: [PATCH 057/160] Fix detection of aggregate functions inside custom functions --- ...eryAggregateFunctionDetectorTreeWalker.php | 295 ++---------------- .../Doctrine/Query/QueryResultTypeWalker.php | 4 + ...eryResultTypeWalkerFetchTypeMatrixTest.php | 33 ++ .../TypedExpressionIntegerWrapFunction.php | 38 +++ 4 files changed, 97 insertions(+), 273 deletions(-) create mode 100644 tests/Platform/TypedExpressionIntegerWrapFunction.php diff --git a/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php b/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php index 11af2086..c82527a3 100644 --- a/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php +++ b/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php @@ -4,7 +4,7 @@ use Doctrine\ORM\Query; use Doctrine\ORM\Query\AST; -use function is_string; +use function is_array; class QueryAggregateFunctionDetectorTreeWalker extends Query\TreeWalkerAdapter { @@ -13,294 +13,38 @@ class QueryAggregateFunctionDetectorTreeWalker extends Query\TreeWalkerAdapter public function walkSelectStatement(AST\SelectStatement $selectStatement): void { - $this->doWalkSelectClause($selectStatement->selectClause); + $this->walkNode($selectStatement->selectClause); } /** - * @param AST\SelectClause $selectClause + * @param mixed $node */ - public function doWalkSelectClause($selectClause): void + public function walkNode($node): void { - foreach ($selectClause->selectExpressions as $selectExpression) { - $this->doWalkSelectExpression($selectExpression); - } - } - - /** - * @param AST\SelectExpression $selectExpression - */ - public function doWalkSelectExpression($selectExpression): void - { - $this->doWalkNode($selectExpression->expression); - } - - /** - * @param mixed $expr - */ - private function doWalkNode($expr): void - { - if ($expr instanceof AST\AggregateExpression) { - $this->markAggregateFunctionFound(); - - } elseif ($expr instanceof AST\Functions\FunctionNode) { - if ($this->isAggregateFunction($expr)) { - $this->markAggregateFunctionFound(); - } - - } elseif ($expr instanceof AST\SimpleArithmeticExpression) { - foreach ($expr->arithmeticTerms as $term) { - $this->doWalkArithmeticTerm($term); - } - - } elseif ($expr instanceof AST\ArithmeticTerm) { - $this->doWalkArithmeticTerm($expr); - - } elseif ($expr instanceof AST\ArithmeticFactor) { - $this->doWalkArithmeticFactor($expr); - - } elseif ($expr instanceof AST\ParenthesisExpression) { - $this->doWalkArithmeticPrimary($expr->expression); - - } elseif ($expr instanceof AST\NullIfExpression) { - $this->doWalkNullIfExpression($expr); - - } elseif ($expr instanceof AST\CoalesceExpression) { - $this->doWalkCoalesceExpression($expr); - - } elseif ($expr instanceof AST\GeneralCaseExpression) { - $this->doWalkGeneralCaseExpression($expr); - - } elseif ($expr instanceof AST\SimpleCaseExpression) { - $this->doWalkSimpleCaseExpression($expr); - - } elseif ($expr instanceof AST\ArithmeticExpression) { - $this->doWalkArithmeticExpression($expr); - - } elseif ($expr instanceof AST\ComparisonExpression) { - $this->doWalkComparisonExpression($expr); - - } elseif ($expr instanceof AST\BetweenExpression) { - $this->doWalkBetweenExpression($expr); - } - } - - public function doWalkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): void - { - foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { - $this->doWalkSimpleArithmeticExpression($scalarExpression); - } - } - - public function doWalkNullIfExpression(AST\NullIfExpression $nullIfExpression): void - { - if (!is_string($nullIfExpression->firstExpression)) { - $this->doWalkSimpleArithmeticExpression($nullIfExpression->firstExpression); - } - - if (is_string($nullIfExpression->secondExpression)) { + if (!$node instanceof AST\Node) { return; } - $this->doWalkSimpleArithmeticExpression($nullIfExpression->secondExpression); - } - - public function doWalkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): void - { - foreach ($generalCaseExpression->whenClauses as $whenClause) { - $this->doWalkConditionalExpression($whenClause->caseConditionExpression); - $this->doWalkSimpleArithmeticExpression($whenClause->thenScalarExpression); - } - - $this->doWalkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression); - } - - public function doWalkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): void - { - foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) { - $this->doWalkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression); - $this->doWalkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression); - } - - $this->doWalkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression); - } - - /** - * @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr - */ - public function doWalkConditionalExpression($condExpr): void - { - if (!$condExpr instanceof AST\ConditionalExpression) { - $this->doWalkConditionalTerm($condExpr); // @phpstan-ignore-line PHPStan do not read @psalm-inheritors of Phase2OptimizableConditional - return; - } - - foreach ($condExpr->conditionalTerms as $conditionalTerm) { - $this->doWalkConditionalTerm($conditionalTerm); - } - } - - /** - * @param AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm - */ - public function doWalkConditionalTerm($condTerm): void - { - if (!$condTerm instanceof AST\ConditionalTerm) { - $this->doWalkConditionalFactor($condTerm); + if ($this->isAggregateFunction($node)) { + $this->markAggregateFunctionFound(); return; } - foreach ($condTerm->conditionalFactors as $conditionalFactor) { - $this->doWalkConditionalFactor($conditionalFactor); - } - } + foreach ((array) $node as $property) { + if ($property instanceof AST\Node) { + $this->walkNode($property); + } - /** - * @param AST\ConditionalFactor|AST\ConditionalPrimary $factor - */ - public function doWalkConditionalFactor($factor): void - { - if (!$factor instanceof AST\ConditionalFactor) { - $this->doWalkConditionalPrimary($factor); - } else { - $this->doWalkConditionalPrimary($factor->conditionalPrimary); - } - } + if (is_array($property)) { + foreach ($property as $propertyValue) { + $this->walkNode($propertyValue); + } + } - /** - * @param AST\ConditionalPrimary $primary - */ - public function doWalkConditionalPrimary($primary): void - { - if ($primary->isSimpleConditionalExpression()) { - if ($primary->simpleConditionalExpression instanceof AST\ComparisonExpression) { - $this->doWalkComparisonExpression($primary->simpleConditionalExpression); + if ($this->wasAggregateFunctionFound()) { return; } - $this->doWalkNode($primary->simpleConditionalExpression); } - - if (!$primary->isConditionalExpression()) { - return; - } - - if ($primary->conditionalExpression === null) { - return; - } - - $this->doWalkConditionalExpression($primary->conditionalExpression); - } - - /** - * @param AST\BetweenExpression $betweenExpr - */ - public function doWalkBetweenExpression($betweenExpr): void - { - $this->doWalkArithmeticExpression($betweenExpr->expression); - $this->doWalkArithmeticExpression($betweenExpr->leftBetweenExpression); - $this->doWalkArithmeticExpression($betweenExpr->rightBetweenExpression); - } - - /** - * @param AST\ComparisonExpression $compExpr - */ - public function doWalkComparisonExpression($compExpr): void - { - $leftExpr = $compExpr->leftExpression; - $rightExpr = $compExpr->rightExpression; - - if ($leftExpr instanceof AST\Node) { - $this->doWalkNode($leftExpr); - } - - if (!($rightExpr instanceof AST\Node)) { - return; - } - - $this->doWalkNode($rightExpr); - } - - /** - * @param AST\ArithmeticExpression $arithmeticExpr - */ - public function doWalkArithmeticExpression($arithmeticExpr): void - { - if (!$arithmeticExpr->isSimpleArithmeticExpression()) { - return; - } - - if ($arithmeticExpr->simpleArithmeticExpression === null) { - return; - } - - $this->doWalkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression); - } - - /** - * @param AST\Node|string $simpleArithmeticExpr - */ - public function doWalkSimpleArithmeticExpression($simpleArithmeticExpr): void - { - if (!$simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression) { - $this->doWalkArithmeticTerm($simpleArithmeticExpr); - return; - } - - foreach ($simpleArithmeticExpr->arithmeticTerms as $term) { - $this->doWalkArithmeticTerm($term); - } - } - - /** - * @param AST\Node|string $term - */ - public function doWalkArithmeticTerm($term): void - { - if (is_string($term)) { - return; - } - - if (!$term instanceof AST\ArithmeticTerm) { - $this->doWalkArithmeticFactor($term); - return; - } - - foreach ($term->arithmeticFactors as $factor) { - $this->doWalkArithmeticFactor($factor); - } - } - - /** - * @param AST\Node|string $factor - */ - public function doWalkArithmeticFactor($factor): void - { - if (is_string($factor)) { - return; - } - - if (!$factor instanceof AST\ArithmeticFactor) { - $this->doWalkArithmeticPrimary($factor); - return; - } - - $this->doWalkArithmeticPrimary($factor->arithmeticPrimary); - } - - /** - * @param AST\Node|string $primary - */ - public function doWalkArithmeticPrimary($primary): void - { - if ($primary instanceof AST\SimpleArithmeticExpression) { - $this->doWalkSimpleArithmeticExpression($primary); - return; - } - - if (!($primary instanceof AST\Node)) { - return; - } - - $this->doWalkNode($primary); } private function isAggregateFunction(AST\Node $node): bool @@ -318,4 +62,9 @@ private function markAggregateFunctionFound(): void $this->_getQuery()->setHint(self::HINT_HAS_AGGREGATE_FUNCTION, true); } + private function wasAggregateFunctionFound(): bool + { + return $this->_getQuery()->hasHint(self::HINT_HAS_AGGREGATE_FUNCTION); + } + } diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 5157a631..2ce3e1ce 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -1226,6 +1226,10 @@ public function walkSelectExpression($selectExpression): string $this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type)) ); + if ($this->hasAggregateWithoutGroupBy() && !$expr instanceof AST\Functions\CountFunction) { + $type = TypeCombinator::addNull($type); + } + } else { // Expressions default to Doctrine's StringType, whose // convertToPHPValue() is a no-op. So the actual type depends on diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index dcf9221f..4df903c0 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -3961,6 +3961,38 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_DEFAULT, ]; + yield 'INT_WRAP(MIN(t.col_float)) + no data' => [ + 'data' => self::dataNone(), + 'select' => 'SELECT INT_WRAP(MIN(t.col_float)) FROM %s t', + 'mysql' => self::intOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => self::intOrNull(), + 'pgsql' => self::intOrNull(), + 'mssql' => self::intOrNull(), + 'mysqlResult' => null, + 'sqliteResult' => null, + 'pdoPgsqlResult' => null, + 'pgsqlResult' => null, + 'mssqlResult' => null, + 'stringify' => self::STRINGIFY_NONE, + ]; + + yield 'INT_WRAP(MIN(t.col_float))' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT INT_WRAP(MIN(t.col_float)) FROM %s t', + 'mysql' => self::intOrNull(), + 'sqlite' => self::intOrNull(), + 'pdo_pgsql' => self::intOrNull(), + 'pgsql' => self::intOrNull(), + 'mssql' => self::intOrNull(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_NONE, + ]; + yield 'COALESCE(t.col_datetime, t.col_datetime)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(t.col_datetime, t.col_datetime) FROM %s t', @@ -5018,6 +5050,7 @@ private function createOrmConfig(): Configuration $config->addCustomStringFunction('INT_PI', TypedExpressionIntegerPiFunction::class); $config->addCustomStringFunction('BOOL_PI', TypedExpressionBooleanPiFunction::class); $config->addCustomStringFunction('STRING_PI', TypedExpressionStringPiFunction::class); + $config->addCustomStringFunction('INT_WRAP', TypedExpressionIntegerWrapFunction::class); return $config; } diff --git a/tests/Platform/TypedExpressionIntegerWrapFunction.php b/tests/Platform/TypedExpressionIntegerWrapFunction.php new file mode 100644 index 00000000..e46d39bc --- /dev/null +++ b/tests/Platform/TypedExpressionIntegerWrapFunction.php @@ -0,0 +1,38 @@ +walkArithmeticPrimary($this->expr) . ')'; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $this->expr = $parser->ArithmeticPrimary(); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getReturnType(): Type + { + return Type::getType(Types::INTEGER); + } + +} From 05873236ecb07c6c7ea4134594b1fbd50a6b617b Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 12 Jul 2024 13:37:58 +0200 Subject: [PATCH 058/160] Readme: how to type custom DQL functions --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index bba1119d..6606512d 100644 --- a/README.md +++ b/README.md @@ -242,3 +242,47 @@ abstract class Uuid7Entity private Uuid7 $hsCode; ``` + +## Custom DQL functions + +Any custom DQL function that implements Doctrine's `TypedExpression` is understood by this extension and is inferred with the type used in its `getReturnType()` method. +All other custom DQL functions are inferred as `mixed`. +Please note that you cannot use native `StringType` to cast (and infer) string results (see [ORM issue](https://github.com/doctrine/orm/issues/11537)). + +```php + +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Query\AST\TypedExpression; +use Doctrine\ORM\Query\AST\Functions\FunctionNode; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\TokenType; + +class Floor extends FunctionNode implements TypedExpression +{ + private AST\Node|string $arithmeticExpression; + + public function getSql(SqlWalker $sqlWalker): string + { + return 'FLOOR(' . $sqlWalker->walkSimpleArithmeticExpression($this->arithmeticExpression) . ')'; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->arithmeticExpression = $parser->SimpleArithmeticExpression(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + + public function getReturnType(): Type + { + return Type::getType(Types::INTEGER); + } +} + +``` From 21eb848a51d519fdf97acbd5b1a56a5fac2284c3 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 12 Jul 2024 13:40:34 +0200 Subject: [PATCH 059/160] Add link to Doctrine docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6606512d..4f148e8f 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,7 @@ abstract class Uuid7Entity ## Custom DQL functions -Any custom DQL function that implements Doctrine's `TypedExpression` is understood by this extension and is inferred with the type used in its `getReturnType()` method. +Any [custom DQL function](https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/cookbook/dql-user-defined-functions.html) that implements Doctrine's `TypedExpression` is understood by this extension and is inferred with the type used in its `getReturnType()` method. All other custom DQL functions are inferred as `mixed`. Please note that you cannot use native `StringType` to cast (and infer) string results (see [ORM issue](https://github.com/doctrine/orm/issues/11537)). From 3f2cb3d364851e54080277499a9a7c150364538d Mon Sep 17 00:00:00 2001 From: Zachary Lund Date: Fri, 12 Jul 2024 13:53:29 -0500 Subject: [PATCH 060/160] Fix subselect aggregate functions --- .../QueryAggregateFunctionDetectorTreeWalker.php | 4 ++++ .../QueryResultTypeWalkerFetchTypeMatrixTest.php | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php b/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php index c82527a3..7e891222 100644 --- a/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php +++ b/src/Type/Doctrine/Query/QueryAggregateFunctionDetectorTreeWalker.php @@ -25,6 +25,10 @@ public function walkNode($node): void return; } + if ($node instanceof AST\Subselect) { + return; + } + if ($this->isAggregateFunction($node)) { $this->markAggregateFunctionFound(); return; diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 4df903c0..04fdcba8 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -3833,6 +3833,22 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_NONE, ]; + yield 'SUBSELECT' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT t1.col_int, (SELECT COUNT(t2.col_int) FROM ' . PlatformEntity::class . ' t2) FROM %s t1', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::int(), + 'mysqlResult' => 9, + 'sqliteResult' => 9, + 'pdoPgsqlResult' => 9, + 'pgsqlResult' => 9, + 'mssqlResult' => 9, + 'stringify' => self::STRINGIFY_NONE, + ]; + yield 'COUNT(t.col_int)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COUNT(t.col_int) FROM %s t', From fa497c5cf8a3f9cd3db8cb4033daf5244793d3e1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jul 2024 13:15:57 +0200 Subject: [PATCH 061/160] DiagnoseExtension --- composer.json | 2 +- extension.neon | 4 ++ src/Doctrine/DoctrineDiagnoseExtension.php | 83 ++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/Doctrine/DoctrineDiagnoseExtension.php diff --git a/composer.json b/composer.json index 6818f33d..e992ccae 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^1.11.7" }, "conflict": { "doctrine/collections": "<1.0", diff --git a/extension.neon b/extension.neon index 81469b91..f4d3b7ef 100644 --- a/extension.neon +++ b/extension.neon @@ -91,6 +91,10 @@ services: - class: PHPStan\Doctrine\Driver\DriverDetector + - + class: PHPStan\Doctrine\DoctrineDiagnoseExtension + tags: + - phpstan.diagnoseExtension - class: PHPStan\Type\Doctrine\HydrationModeReturnTypeResolver - diff --git a/src/Doctrine/DoctrineDiagnoseExtension.php b/src/Doctrine/DoctrineDiagnoseExtension.php new file mode 100644 index 00000000..58fda398 --- /dev/null +++ b/src/Doctrine/DoctrineDiagnoseExtension.php @@ -0,0 +1,83 @@ +objectMetadataResolver = $objectMetadataResolver; + $this->driverDetector = $driverDetector; + } + + public function print(Output $output): void + { + $output->writeLineFormatted(sprintf( + 'Doctrine\'s objectManagerLoader: %s', + $this->objectMetadataResolver->hasObjectManagerLoader() ? 'In use' : 'No' + )); + + $objectManager = $this->objectMetadataResolver->getObjectManager(); + if ($objectManager instanceof EntityManagerInterface) { + $connection = $objectManager->getConnection(); + $driver = $this->driverDetector->detect($connection); + + $output->writeLineFormatted(sprintf( + 'Detected driver: %s', + $driver === null ? 'None' : $driver + )); + } + + $packages = []; + $candidates = [ + 'doctrine/dbal', + 'doctrine/orm', + 'doctrine/common', + 'doctrine/collections', + 'doctrine/persistence', + ]; + foreach ($candidates as $package) { + try { + $installedVersion = InstalledVersions::getPrettyVersion($package); + } catch (OutOfBoundsException $e) { + continue; + } + + if ($installedVersion === null) { + continue; + } + + $packages[$package] = $installedVersion; + } + + if (count($packages) > 0) { + $output->writeLineFormatted('Installed Doctrine packages:'); + foreach ($packages as $package => $version) { + $output->writeLineFormatted(sprintf('%s: %s', $package, $version)); + } + } + + $output->writeLineFormatted(''); + } + +} From ca69937830f90726affd9775371d8700ca32e162 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 5 Aug 2024 15:08:08 +0200 Subject: [PATCH 062/160] Open 1.5.x-dev --- .github/workflows/build.yml | 2 +- .github/workflows/platform-test.yml | 2 +- .github/workflows/test-projects.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 476203bb..28b40c02 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.4.x" + - "1.5.x" jobs: lint: diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml index 22e867ea..5c063f0e 100644 --- a/.github/workflows/platform-test.yml +++ b/.github/workflows/platform-test.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.4.x" + - "1.5.x" jobs: tests: diff --git a/.github/workflows/test-projects.yml b/.github/workflows/test-projects.yml index 6bd1e05b..d33dcf6f 100644 --- a/.github/workflows/test-projects.yml +++ b/.github/workflows/test-projects.yml @@ -5,7 +5,7 @@ name: "Test projects" on: push: branches: - - "1.4.x" + - "1.5.x" jobs: test-projects: From e71f08d9f69be65da76e61b72d684a581bc982b4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 5 Aug 2024 15:14:53 +0200 Subject: [PATCH 063/160] Get rid of bleedingEdge stubs, use a separate config option for enabling literal-string parameters --- extension.neon | 9 +- ...LiteralStringTypeNodeResolverExtension.php | 45 +++++ .../Doctrine/StubFilesExtensionLoader.php | 28 +--- .../DBAL/ArrayParameterType.stub | 0 stubs/DBAL/Connection.stub | 56 +++++++ .../{bleedingEdge => }/DBAL/Connection4.stub | 6 +- .../DBAL/ParameterType.stub | 0 stubs/EntityRepository.stub | 8 + stubs/ORM/QueryBuilder.stub | 134 +++++++++++++++ stubs/bleedingEdge/DBAL/Connection.stub | 64 ------- stubs/bleedingEdge/EntityRepository.stub | 74 --------- stubs/bleedingEdge/ORM/QueryBuilder.stub | 156 ------------------ 12 files changed, 261 insertions(+), 319 deletions(-) create mode 100644 src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php rename stubs/{bleedingEdge => }/DBAL/ArrayParameterType.stub (100%) rename stubs/{bleedingEdge => }/DBAL/Connection4.stub (89%) rename stubs/{bleedingEdge => }/DBAL/ParameterType.stub (100%) delete mode 100644 stubs/bleedingEdge/DBAL/Connection.stub delete mode 100644 stubs/bleedingEdge/EntityRepository.stub delete mode 100644 stubs/bleedingEdge/ORM/QueryBuilder.stub diff --git a/extension.neon b/extension.neon index f4d3b7ef..84bc0d04 100644 --- a/extension.neon +++ b/extension.neon @@ -196,8 +196,6 @@ services: class: PHPStan\Stubs\Doctrine\StubFilesExtensionLoader tags: - phpstan.stubFilesExtension - arguments: - bleedingEdge: %featureToggles.bleedingEdge% doctrineQueryBuilderArgumentsProcessor: class: PHPStan\Type\Doctrine\ArgumentsProcessor @@ -434,6 +432,13 @@ services: tags: - phpstan.phpDoc.typeNodeResolverExtension + - + class: PHPStan\PhpDoc\Doctrine\DoctrineLiteralStringTypeNodeResolverExtension + arguments: + bleedingEdge: %featureToggles.bleedingEdge% + tags: + - phpstan.phpDoc.typeNodeResolverExtension + - class: PHPStan\Type\Doctrine\EntityManagerInterfaceThrowTypeExtension tags: diff --git a/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php b/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php new file mode 100644 index 00000000..ce73011e --- /dev/null +++ b/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php @@ -0,0 +1,45 @@ +bleedingEdge = $bleedingEdge; + } + + public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type + { + if (!$typeNode instanceof IdentifierTypeNode) { + return null; + } + + if ($typeNode->name !== '__doctrine-literal-string') { + return null; + } + + if ($this->bleedingEdge) { + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); + } + + return new StringType(); + } + +} diff --git a/src/Stubs/Doctrine/StubFilesExtensionLoader.php b/src/Stubs/Doctrine/StubFilesExtensionLoader.php index 95d43a66..97656d2d 100644 --- a/src/Stubs/Doctrine/StubFilesExtensionLoader.php +++ b/src/Stubs/Doctrine/StubFilesExtensionLoader.php @@ -9,7 +9,6 @@ use PHPStan\PhpDoc\StubFilesExtension; use function class_exists; use function dirname; -use function file_exists; use function strpos; class StubFilesExtensionLoader implements StubFilesExtension @@ -18,39 +17,28 @@ class StubFilesExtensionLoader implements StubFilesExtension /** @var Reflector */ private $reflector; - /** @var bool */ - private $bleedingEdge; - public function __construct( - Reflector $reflector, - bool $bleedingEdge + Reflector $reflector ) { $this->reflector = $reflector; - $this->bleedingEdge = $bleedingEdge; } public function getFiles(): array { $stubsDir = dirname(dirname(dirname(__DIR__))) . '/stubs'; - $path = $stubsDir; - - if ($this->bleedingEdge === true) { - $path .= '/bleedingEdge'; - } - $files = []; - if (file_exists($path . '/DBAL/Connection4.stub') && $this->isInstalledVersion('doctrine/dbal', 4)) { - $files[] = $path . '/DBAL/Connection4.stub'; - $files[] = $path . '/DBAL/ArrayParameterType.stub'; - $files[] = $path . '/DBAL/ParameterType.stub'; + if ($this->isInstalledVersion('doctrine/dbal', 4)) { + $files[] = $stubsDir . '/DBAL/Connection4.stub'; + $files[] = $stubsDir . '/DBAL/ArrayParameterType.stub'; + $files[] = $stubsDir . '/DBAL/ParameterType.stub'; } else { - $files[] = $path . '/DBAL/Connection.stub'; + $files[] = $stubsDir . '/DBAL/Connection.stub'; } - $files[] = $path . '/ORM/QueryBuilder.stub'; - $files[] = $path . '/EntityRepository.stub'; + $files[] = $stubsDir . '/ORM/QueryBuilder.stub'; + $files[] = $stubsDir . '/EntityRepository.stub'; $hasLazyServiceEntityRepositoryAsParent = false; diff --git a/stubs/bleedingEdge/DBAL/ArrayParameterType.stub b/stubs/DBAL/ArrayParameterType.stub similarity index 100% rename from stubs/bleedingEdge/DBAL/ArrayParameterType.stub rename to stubs/DBAL/ArrayParameterType.stub diff --git a/stubs/DBAL/Connection.stub b/stubs/DBAL/Connection.stub index 8e88410a..d11b040f 100644 --- a/stubs/DBAL/Connection.stub +++ b/stubs/DBAL/Connection.stub @@ -2,7 +2,63 @@ namespace Doctrine\DBAL; +use Doctrine\DBAL\Cache\CacheException; +use Doctrine\DBAL\Cache\QueryCacheProfile; +use Doctrine\DBAL\Types\Type; + class Connection { + /** + * Executes an SQL statement with the given parameters and returns the number of affected rows. + * + * Could be used for: + * - DML statements: INSERT, UPDATE, DELETE, etc. + * - DDL statements: CREATE, DROP, ALTER, etc. + * - DCL statements: GRANT, REVOKE, etc. + * - Session control statements: ALTER SESSION, SET, DECLARE, etc. + * - Other statements that don't yield a row set. + * + * This method supports PDO binding types as well as DBAL mapping types. + * + * @param __doctrine-literal-string $sql SQL statement + * @param list|array $params Statement parameters + * @param array|array $types Parameter types + * + * @return int|string The number of affected rows. + * + * @throws Exception + */ + public function executeStatement($sql, array $params = [], array $types = []); + + /** + * Executes an, optionally parameterized, SQL query. + * + * If the query is parametrized, a prepared statement is used. + * If an SQLLogger is configured, the execution is logged. + * + * @param __doctrine-literal-string $sql SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @throws Exception + */ + public function executeQuery( + string $sql, + array $params = [], + $types = [], + ?QueryCacheProfile $qcp = null + ): Result; + + /** + * Executes a caching query. + * + * @param __doctrine-literal-string $sql SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @throws CacheException + * @throws Exception + */ + public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result; } diff --git a/stubs/bleedingEdge/DBAL/Connection4.stub b/stubs/DBAL/Connection4.stub similarity index 89% rename from stubs/bleedingEdge/DBAL/Connection4.stub rename to stubs/DBAL/Connection4.stub index 0e6edd58..0500252f 100644 --- a/stubs/bleedingEdge/DBAL/Connection4.stub +++ b/stubs/DBAL/Connection4.stub @@ -24,7 +24,7 @@ class Connection * * This method supports PDO binding types as well as DBAL mapping types. * - * @param literal-string $sql SQL statement + * @param __doctrine-literal-string-string $sql SQL statement * @param list|array $params Statement parameters * @param WrapperParameterTypeArray $types Parameter types * @@ -40,7 +40,7 @@ class Connection * If the query is parametrized, a prepared statement is used. * If an SQLLogger is configured, the execution is logged. * - * @param literal-string $sql SQL query + * @param __doctrine-literal-string-string $sql SQL query * @param list|array $params Query parameters * @param WrapperParameterTypeArray $types Parameter types * @@ -56,7 +56,7 @@ class Connection /** * Executes a caching query. * - * @param literal-string $sql SQL query + * @param __doctrine-literal-string-string $sql SQL query * @param list|array $params Query parameters * @param WrapperParameterTypeArray $types Parameter types * diff --git a/stubs/bleedingEdge/DBAL/ParameterType.stub b/stubs/DBAL/ParameterType.stub similarity index 100% rename from stubs/bleedingEdge/DBAL/ParameterType.stub rename to stubs/DBAL/ParameterType.stub diff --git a/stubs/EntityRepository.stub b/stubs/EntityRepository.stub index cfa838fd..f6fc8d17 100644 --- a/stubs/EntityRepository.stub +++ b/stubs/EntityRepository.stub @@ -63,4 +63,12 @@ class EntityRepository implements ObjectRepository */ public function matching(Criteria $criteria); + /** + * @param __doctrine-literal-string $alias + * @param __doctrine-literal-string|null $indexBy + * + * @return QueryBuilder + */ + public function createQueryBuilder($alias, $indexBy = null); + } diff --git a/stubs/ORM/QueryBuilder.stub b/stubs/ORM/QueryBuilder.stub index db14c5b2..46b8e0ee 100644 --- a/stubs/ORM/QueryBuilder.stub +++ b/stubs/ORM/QueryBuilder.stub @@ -2,6 +2,8 @@ namespace Doctrine\ORM; +use Doctrine\ORM\Query\Expr; + class QueryBuilder { @@ -21,4 +23,136 @@ class QueryBuilder { } + /** + * @param string $dqlPartName + * @param __doctrine-literal-string|object|list<__doctrine-literal-string>|array{join: array} $dqlPart + * @param bool $append + * + * @return $this + */ + public function add($dqlPartName, $dqlPart, $append = false) + { + + } + + /** + * @param __doctrine-literal-string|null $delete + * @param __doctrine-literal-string|null $alias + * + * @return $this + */ + public function delete($delete = null, $alias = null) + { + + } + + /** + * @param __doctrine-literal-string|null $update + * @param __doctrine-literal-string|null $alias + * + * @return $this + */ + public function update($update = null, $alias = null) + { + + } + + /** + * @param __doctrine-literal-string|class-string $from + * @param __doctrine-literal-string $alias + * @param __doctrine-literal-string|null $indexBy + * + * @return $this + */ + public function from($from, $alias, $indexBy = null) + { + + } + + /** + * @param __doctrine-literal-string|class-string $join + * @param __doctrine-literal-string $alias + * @param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * @param __doctrine-literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition + * @param __doctrine-literal-string|null $indexBy + * + * @return $this + */ + public function innerJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null) + { + + } + + /** + * @param __doctrine-literal-string|class-string $join + * @param __doctrine-literal-string $alias + * @param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * @param __doctrine-literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition + * @param __doctrine-literal-string|null $indexBy + * + * @return $this + */ + public function leftJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null) + { + + } + + /** + * @param __doctrine-literal-string|class-string $join + * @param __doctrine-literal-string $alias + * @param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * @param __doctrine-literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition + * @param __doctrine-literal-string|null $indexBy + * + * @return $this + */ + public function join($join, $alias, $conditionType = null, $condition = null, $indexBy = null) + { + + } + + /** + * @return __doctrine-literal-string + */ + public function getRootAlias() + { + + } + + /** + * @return list<__doctrine-literal-string> + */ + public function getRootAliases() + { + + } + + /** + * @return list<__doctrine-literal-string> + */ + public function getAllAlias() + { + + } + + /** + * @param __doctrine-literal-string|object|array $predicates + * @return $this + */ + public function where($predicates) + { + + } + + /** + * @param __doctrine-literal-string|object|array $predicates + * @return $this + */ + public function andWhere($predicates) + { + + } + + + } diff --git a/stubs/bleedingEdge/DBAL/Connection.stub b/stubs/bleedingEdge/DBAL/Connection.stub deleted file mode 100644 index 6bb4a01d..00000000 --- a/stubs/bleedingEdge/DBAL/Connection.stub +++ /dev/null @@ -1,64 +0,0 @@ -|array $params Statement parameters - * @param array|array $types Parameter types - * - * @return int|string The number of affected rows. - * - * @throws Exception - */ - public function executeStatement($sql, array $params = [], array $types = []); - - /** - * Executes an, optionally parameterized, SQL query. - * - * If the query is parametrized, a prepared statement is used. - * If an SQLLogger is configured, the execution is logged. - * - * @param literal-string $sql SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types - * - * @throws Exception - */ - public function executeQuery( - string $sql, - array $params = [], - $types = [], - ?QueryCacheProfile $qcp = null - ): Result; - - /** - * Executes a caching query. - * - * @param literal-string $sql SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types - * - * @throws CacheException - * @throws Exception - */ - public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result; - -} diff --git a/stubs/bleedingEdge/EntityRepository.stub b/stubs/bleedingEdge/EntityRepository.stub deleted file mode 100644 index 6e22e323..00000000 --- a/stubs/bleedingEdge/EntityRepository.stub +++ /dev/null @@ -1,74 +0,0 @@ - - */ -class EntityRepository implements ObjectRepository -{ - - /** @var class-string */ - protected $_entityName; - - /** - * @phpstan-param mixed $id - * @phpstan-param int|null $lockMode - * @phpstan-param int|null $lockVersion - * @phpstan-return TEntityClass|null - */ - public function find($id, $lockMode = null, $lockVersion = null); - - /** - * @phpstan-return list - */ - public function findAll(); - - /** - * @phpstan-param array $criteria - * @phpstan-param array|null $orderBy - * @phpstan-param int|null $limit - * @phpstan-param int|null $offset - * @phpstan-return list - */ - public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null); - - /** - * @phpstan-param array $criteria The criteria. - * @phpstan-param array|null $orderBy - * @phpstan-return TEntityClass|null - */ - public function findOneBy(array $criteria, array $orderBy = null); - - /** - * @phpstan-return class-string - */ - public function getClassName(); - - /** - * @phpstan-return class-string - */ - protected function getEntityName(); - - /** - * @param \Doctrine\Common\Collections\Criteria $criteria - * - * @return \Doctrine\Common\Collections\Collection - * - * @psalm-return \Doctrine\Common\Collections\Collection - */ - public function matching(Criteria $criteria); - - /** - * @param literal-string $alias - * @param literal-string|null $indexBy - * - * @return QueryBuilder - */ - public function createQueryBuilder($alias, $indexBy = null); - -} diff --git a/stubs/bleedingEdge/ORM/QueryBuilder.stub b/stubs/bleedingEdge/ORM/QueryBuilder.stub deleted file mode 100644 index 25e05ee2..00000000 --- a/stubs/bleedingEdge/ORM/QueryBuilder.stub +++ /dev/null @@ -1,156 +0,0 @@ - - */ - public function getQuery() - { - } - - /** - * @param \Doctrine\Common\Collections\ArrayCollection|array $parameters - * @return static - */ - public function setParameters($parameters) - { - - } - - /** - * @param string $dqlPartName - * @param literal-string|object|list|array{join: array} $dqlPart - * @param bool $append - * - * @return $this - */ - public function add($dqlPartName, $dqlPart, $append = false) - { - - } - - /** - * @param literal-string|null $delete - * @param literal-string|null $alias - * - * @return $this - */ - public function delete($delete = null, $alias = null) - { - - } - - /** - * @param literal-string|null $update - * @param literal-string|null $alias - * - * @return $this - */ - public function update($update = null, $alias = null) - { - - } - - /** - * @param literal-string|class-string $from - * @param literal-string $alias - * @param literal-string|null $indexBy - * - * @return $this - */ - public function from($from, $alias, $indexBy = null) - { - - } - - /** - * @param literal-string|class-string $join - * @param literal-string $alias - * @param Expr\Join::ON|Expr\Join::WITH|null $conditionType - * @param literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition - * @param literal-string|null $indexBy - * - * @return $this - */ - public function innerJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null) - { - - } - - /** - * @param literal-string|class-string $join - * @param literal-string $alias - * @param Expr\Join::ON|Expr\Join::WITH|null $conditionType - * @param literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition - * @param literal-string|null $indexBy - * - * @return $this - */ - public function leftJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null) - { - - } - - /** - * @param literal-string|class-string $join - * @param literal-string $alias - * @param Expr\Join::ON|Expr\Join::WITH|null $conditionType - * @param literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition - * @param literal-string|null $indexBy - * - * @return $this - */ - public function join($join, $alias, $conditionType = null, $condition = null, $indexBy = null) - { - - } - - /** - * @return literal-string - */ - public function getRootAlias() - { - - } - - /** - * @return list - */ - public function getRootAliases() - { - - } - - /** - * @return list - */ - public function getAllAlias() - { - - } - - /** - * @param literal-string|object|array $predicates - * @return $this - */ - public function where($predicates) - { - - } - - /** - * @param literal-string|object|array $predicates - * @return $this - */ - public function andWhere($predicates) - { - - } - -} From f7b533de0c76826f0eca49ebefd85a60094a5571 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 5 Aug 2024 15:35:34 +0200 Subject: [PATCH 064/160] Fix --- stubs/DBAL/Connection4.stub | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stubs/DBAL/Connection4.stub b/stubs/DBAL/Connection4.stub index 0500252f..2e80b001 100644 --- a/stubs/DBAL/Connection4.stub +++ b/stubs/DBAL/Connection4.stub @@ -24,7 +24,7 @@ class Connection * * This method supports PDO binding types as well as DBAL mapping types. * - * @param __doctrine-literal-string-string $sql SQL statement + * @param __doctrine-literal-string $sql SQL statement * @param list|array $params Statement parameters * @param WrapperParameterTypeArray $types Parameter types * @@ -40,7 +40,7 @@ class Connection * If the query is parametrized, a prepared statement is used. * If an SQLLogger is configured, the execution is logged. * - * @param __doctrine-literal-string-string $sql SQL query + * @param __doctrine-literal-string $sql SQL query * @param list|array $params Query parameters * @param WrapperParameterTypeArray $types Parameter types * @@ -56,7 +56,7 @@ class Connection /** * Executes a caching query. * - * @param __doctrine-literal-string-string $sql SQL query + * @param __doctrine-literal-string $sql SQL query * @param list|array $params Query parameters * @param WrapperParameterTypeArray $types Parameter types * From 0c7771b7d91bf4c62eb8d4710a763d749ccd018f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 5 Aug 2024 15:46:13 +0200 Subject: [PATCH 065/160] Update README --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 4f148e8f..83bc5364 100644 --- a/README.md +++ b/README.md @@ -286,3 +286,19 @@ class Floor extends FunctionNode implements TypedExpression } ``` + +## Literal strings + +Stub files in phpstan-doctrine come with many parameters marked with `literal-string`. This is a security-focused type that only allows literal strings written in code to be passed into these parameters. + +This reduces risk of SQL injection because dynamic strings from user input are not accepted in place of `literal-string`. + +An example where this type is used is `$sql` parameter in `Doctrine\Dbal\Connection::executeQuery()`. + +To enable this advanced type in phpstan-doctrine, use this configuration parameter: + +```neon +parameters: + doctrine: + literalString: true +``` From caa046bd6152818e781260fb3a7a96d6b0fadeed Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 5 Aug 2024 15:40:25 +0200 Subject: [PATCH 066/160] Clean up parameters config --- extension.neon | 10 +++++++++- rules.neon | 1 + .../DoctrineLiteralStringTypeNodeResolverExtension.php | 8 ++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/extension.neon b/extension.neon index 84bc0d04..fc7ad592 100644 --- a/extension.neon +++ b/extension.neon @@ -1,5 +1,8 @@ parameters: doctrine: + reportDynamicQueryBuilders: false + reportUnknownTypes: false + allowNullablePropertyForRequiredField: false repositoryClass: null ormRepositoryClass: null odmRepositoryClass: null @@ -8,6 +11,7 @@ parameters: objectManagerLoader: null searchOtherMethodsForQueryBuilderBeginning: true queryBuilderFastAlgorithm: false + literalString: false featureToggles: skipCheckGenericClasses: - Doctrine\ODM\MongoDB\Mapping\ClassMetadata @@ -75,6 +79,10 @@ parametersSchema: objectManagerLoader: schema(string(), nullable()) searchOtherMethodsForQueryBuilderBeginning: bool() queryBuilderFastAlgorithm: bool() + reportDynamicQueryBuilders: bool() + reportUnknownTypes: bool() + allowNullablePropertyForRequiredField: bool() + literalString: bool() ]) conditionalTags: @@ -435,7 +443,7 @@ services: - class: PHPStan\PhpDoc\Doctrine\DoctrineLiteralStringTypeNodeResolverExtension arguments: - bleedingEdge: %featureToggles.bleedingEdge% + enabled: %doctrine.literalString% tags: - phpstan.phpDoc.typeNodeResolverExtension diff --git a/rules.neon b/rules.neon index feb41184..de338d2a 100644 --- a/rules.neon +++ b/rules.neon @@ -17,6 +17,7 @@ parametersSchema: reportDynamicQueryBuilders: bool() reportUnknownTypes: bool() allowNullablePropertyForRequiredField: bool() + literalString: bool() ]) rules: diff --git a/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php b/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php index ce73011e..3aa371d5 100644 --- a/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php +++ b/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php @@ -15,11 +15,11 @@ class DoctrineLiteralStringTypeNodeResolverExtension implements TypeNodeResolver { /** @var bool */ - private $bleedingEdge; + private $enabled; - public function __construct(bool $bleedingEdge) + public function __construct(bool $enabled) { - $this->bleedingEdge = $bleedingEdge; + $this->enabled = $enabled; } public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type @@ -32,7 +32,7 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type return null; } - if ($this->bleedingEdge) { + if ($this->enabled) { return new IntersectionType([ new StringType(), new AccessoryLiteralStringType(), From 285d78d01a8542544ac559f537967dff50a5f5c4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 5 Aug 2024 17:43:51 +0200 Subject: [PATCH 067/160] Simplify StubFilesExtensionLoader --- extension.neon | 2 ++ src/Stubs/Doctrine/StubFilesExtensionLoader.php | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/extension.neon b/extension.neon index fc7ad592..a4f36b15 100644 --- a/extension.neon +++ b/extension.neon @@ -34,6 +34,7 @@ parameters: - stubs/EntityManager.stub - stubs/EntityManagerDecorator.stub - stubs/EntityManagerInterface.stub + - stubs/EntityRepository.stub - stubs/MongoClassMetadataInfo.stub - stubs/Persistence/ManagerRegistry.stub @@ -61,6 +62,7 @@ parameters: - stubs/ORM/UnexpectedResultException.stub - stubs/ORM/Query/Expr.stub - stubs/ORM/Query.stub + - stubs/ORM/QueryBuilder.stub - stubs/ORM/Query/Expr/Comparison.stub - stubs/ORM/Query/Expr/Composite.stub - stubs/ORM/Query/Expr/Func.stub diff --git a/src/Stubs/Doctrine/StubFilesExtensionLoader.php b/src/Stubs/Doctrine/StubFilesExtensionLoader.php index 97656d2d..0b3a69d8 100644 --- a/src/Stubs/Doctrine/StubFilesExtensionLoader.php +++ b/src/Stubs/Doctrine/StubFilesExtensionLoader.php @@ -37,9 +37,6 @@ public function getFiles(): array $files[] = $stubsDir . '/DBAL/Connection.stub'; } - $files[] = $stubsDir . '/ORM/QueryBuilder.stub'; - $files[] = $stubsDir . '/EntityRepository.stub'; - $hasLazyServiceEntityRepositoryAsParent = false; try { From 6632f382180746fa4b317b4dae30e242f2e990f9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 19 Aug 2024 16:11:06 +0200 Subject: [PATCH 068/160] Try to reproduce internal error See https://github.com/phpstan/phpstan-doctrine/issues/602 --- tests/Rules/Doctrine/ORM/data/query-builder-dql.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Rules/Doctrine/ORM/data/query-builder-dql.php b/tests/Rules/Doctrine/ORM/data/query-builder-dql.php index 49aa95f2..f94085a3 100644 --- a/tests/Rules/Doctrine/ORM/data/query-builder-dql.php +++ b/tests/Rules/Doctrine/ORM/data/query-builder-dql.php @@ -291,6 +291,14 @@ public function qbExprMethod(): void $queryBuilder->getQuery(); } + public function bug602(array $objectConditions): void + { + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->select('e') + ->from(MyEntity::class, 'e') + ->andWhere($queryBuilder->expr()->orX(...$objectConditions)); + } + } class CustomExpr extends \Doctrine\ORM\Query\Expr From a9bb990e0a1ab33078aa925114845769b4abbb96 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Tue, 20 Aug 2024 17:12:42 +0200 Subject: [PATCH 069/160] EntityRepository::count returns positive int - fixes #603 --- stubs/EntityRepository.stub | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stubs/EntityRepository.stub b/stubs/EntityRepository.stub index f6fc8d17..4dcde8bf 100644 --- a/stubs/EntityRepository.stub +++ b/stubs/EntityRepository.stub @@ -71,4 +71,11 @@ class EntityRepository implements ObjectRepository */ public function createQueryBuilder($alias, $indexBy = null); + /** + * @param array $criteria + * + * @return int<0, max> + */ + public function count(array $criteria); + } From 4d17bed8a33aa8220c1f2a21a6b14fcdb0e5b02c Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Fri, 23 Aug 2024 11:47:37 +0200 Subject: [PATCH 070/160] Fix support for `enumType` on array fields --- src/Rules/Doctrine/ORM/EntityColumnRule.php | 66 ++++++++++++++----- .../Doctrine/Query/QueryResultTypeWalker.php | 35 ++++++---- .../Doctrine/ORM/EntityColumnRuleTest.php | 18 ++++- .../ORM/data-attributes/enum-type.php | 21 ++++++ .../Query/QueryResultTypeWalkerTest.php | 6 +- .../EntitiesEnum/EntityWithEnum.php | 6 ++ 6 files changed, 121 insertions(+), 31 deletions(-) diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index e51e9c95..70c1fcea 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -21,6 +21,7 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use Throwable; use function get_class; @@ -115,25 +116,58 @@ public function processNode(Node $node, Scope $scope): array $enumTypeString = $fieldMapping['enumType'] ?? null; if ($enumTypeString !== null) { - if ($this->reflectionProvider->hasClass($enumTypeString)) { - $enumReflection = $this->reflectionProvider->getClass($enumTypeString); - $backedEnumType = $enumReflection->getBackedEnumType(); - if ($backedEnumType !== null) { - if (!$backedEnumType->equals($writableToDatabaseType) || !$backedEnumType->equals($writableToPropertyType)) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match database type %s.', - $className, - $propertyName, - $backedEnumType->describe(VerbosityLevel::typeOnly()), - $enumReflection->getDisplayName(), - $writableToDatabaseType->describe(VerbosityLevel::typeOnly()) - ))->identifier('doctrine.enumType')->build(); + if ($writableToDatabaseType->isArray()->no() && $writableToPropertyType->isArray()->no()) { + if ($this->reflectionProvider->hasClass($enumTypeString)) { + $enumReflection = $this->reflectionProvider->getClass($enumTypeString); + $backedEnumType = $enumReflection->getBackedEnumType(); + if ($backedEnumType !== null) { + if (!$backedEnumType->equals($writableToDatabaseType) || !$backedEnumType->equals($writableToPropertyType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match database type %s.', + $className, + $propertyName, + $backedEnumType->describe(VerbosityLevel::typeOnly()), + $enumReflection->getDisplayName(), + $writableToDatabaseType->describe(VerbosityLevel::typeOnly()) + ))->identifier('doctrine.enumType')->build(); + } } } + $enumType = new ObjectType($enumTypeString); + $writableToPropertyType = $enumType; + $writableToDatabaseType = $enumType; + } else { + $enumType = new ObjectType($enumTypeString); + if ($this->reflectionProvider->hasClass($enumTypeString)) { + $enumReflection = $this->reflectionProvider->getClass($enumTypeString); + $backedEnumType = $enumReflection->getBackedEnumType(); + if ($backedEnumType !== null) { + if (!$backedEnumType->equals($writableToDatabaseType->getIterableValueType()) || !$backedEnumType->equals($writableToPropertyType->getIterableValueType())) { + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match value type %s of the database type %s.', + $className, + $propertyName, + $backedEnumType->describe(VerbosityLevel::typeOnly()), + $enumReflection->getDisplayName(), + $writableToDatabaseType->getIterableValueType()->describe(VerbosityLevel::typeOnly()), + $writableToDatabaseType->describe(VerbosityLevel::typeOnly()) + ) + )->identifier('doctrine.enumType')->build(); + } + } + } + + $writableToPropertyType = TypeCombinator::intersect(new ArrayType( + $writableToPropertyType->getIterableKeyType(), + $enumType + ), ...TypeUtils::getAccessoryTypes($writableToPropertyType)); + $writableToDatabaseType = TypeCombinator::intersect(new ArrayType( + $writableToDatabaseType->getIterableKeyType(), + $enumType + ), ...TypeUtils::getAccessoryTypes($writableToDatabaseType)); + } - $enumType = new ObjectType($enumTypeString); - $writableToPropertyType = $enumType; - $writableToDatabaseType = $enumType; } $identifiers = []; diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 2ce3e1ce..4ef105d9 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -19,6 +19,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -39,6 +40,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function array_key_exists; use function array_map; @@ -2009,17 +2011,28 @@ private function getTypeOfField(ClassMetadata $class, string $fieldName): array /** @param ?class-string $enumType */ private function resolveDoctrineType(string $typeName, ?string $enumType = null, bool $nullable = false): Type { - if ($enumType !== null) { - $type = new ObjectType($enumType); - } else { - try { - $type = $this->descriptorRegistry - ->get($typeName) - ->getWritableToPropertyType(); - if ($type instanceof NeverType) { - $type = new MixedType(); + try { + $type = $this->descriptorRegistry + ->get($typeName) + ->getWritableToPropertyType(); + + if ($enumType !== null) { + if ($type->isArray()->no()) { + $type = new ObjectType($enumType); + } else { + $type = TypeCombinator::intersect(new ArrayType( + $type->getIterableKeyType(), + new ObjectType($enumType) + ), ...TypeUtils::getAccessoryTypes($type)); } - } catch (DescriptorNotRegisteredException $e) { + } + if ($type instanceof NeverType) { + $type = new MixedType(); + } + } catch (DescriptorNotRegisteredException $e) { + if ($enumType !== null) { + $type = new ObjectType($enumType); + } else { $type = new MixedType(); } } @@ -2028,7 +2041,7 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null, $type = TypeCombinator::addNull($type); } - return $type; + return $type; } /** @param ?class-string $enumType */ diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 5dfcc639..c7de7957 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -391,15 +391,27 @@ public function testEnumType(?string $objectManagerLoader): void $this->analyse([__DIR__ . '/data-attributes/enum-type.php'], [ [ 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type2 type mapping mismatch: database can contain PHPStan\Rules\Doctrine\ORMAttributes\FooEnum but property expects PHPStan\Rules\Doctrine\ORMAttributes\BarEnum.', - 35, + 42, ], [ 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type2 type mapping mismatch: property can contain PHPStan\Rules\Doctrine\ORMAttributes\BarEnum but database expects PHPStan\Rules\Doctrine\ORMAttributes\FooEnum.', - 35, + 42, ], [ 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type3 type mapping mismatch: backing type string of enum PHPStan\Rules\Doctrine\ORMAttributes\FooEnum does not match database type int.', - 38, + 45, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: database can contain array but property expects PHPStan\Rules\Doctrine\ORMAttributes\FooEnum.', + 51, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: property can contain PHPStan\Rules\Doctrine\ORMAttributes\FooEnum but database expects array.', + 51, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type7 type mapping mismatch: backing type int of enum PHPStan\Rules\Doctrine\ORMAttributes\BazEnum does not match value type string of the database type array.', + 63, ], ]); } diff --git a/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php b/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php index a0c3b937..50e0a2ec 100644 --- a/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php +++ b/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php @@ -18,6 +18,13 @@ enum BarEnum: string { } +enum BazEnum: int { + + case ONE = 1; + case TWO = 2; + +} + #[ORM\Entity] class Foo { @@ -40,4 +47,18 @@ class Foo #[ORM\Column] public FooEnum $type4; + #[ORM\Column(type: "simple_array", enumType: FooEnum::class)] + public FooEnum $type5; + + /** + * @var list + */ + #[ORM\Column(type: "simple_array", enumType: FooEnum::class)] + public array $type6; + + /** + * @var list + */ + #[ORM\Column(type: "simple_array", enumType: BazEnum::class)] + public array $type7; } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 003a0737..fd6fee74 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -14,7 +14,9 @@ use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -181,6 +183,7 @@ public static function setUpBeforeClass(): void $entityWithEnum->stringEnumColumn = StringEnum::A; $entityWithEnum->intEnumColumn = IntEnum::A; $entityWithEnum->intEnumOnStringColumn = IntEnum::A; + $entityWithEnum->stringEnumListColumn = [StringEnum::A, StringEnum::B]; $em->persist($entityWithEnum); } @@ -1499,9 +1502,10 @@ private function yieldConditionalDataset(): iterable $this->constantArray([ [new ConstantStringType('stringEnumColumn'), new ObjectType(StringEnum::class)], [new ConstantStringType('intEnumColumn'), new ObjectType(IntEnum::class)], + [new ConstantStringType('stringEnumListColumn'), AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new ObjectType(StringEnum::class)))], ]), ' - SELECT e.stringEnumColumn, e.intEnumColumn + SELECT e.stringEnumColumn, e.intEnumColumn, e.stringEnumListColumn FROM QueryResult\EntitiesEnum\EntityWithEnum e ', ]; diff --git a/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php index c22acd45..2a4c535a 100644 --- a/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php +++ b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php @@ -38,4 +38,10 @@ class EntityWithEnum * @Column(type="string", enumType="QueryResult\EntitiesEnum\IntEnum") */ public $intEnumOnStringColumn; + + /** + * @var list + * @Column(type="simple_array", enumType="QueryResult\EntitiesEnum\StringEnum") + */ + public $stringEnumListColumn; } From ba9563e1c0294ff4fd0d59491ff4802af6462313 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 1 Sep 2024 13:43:40 +0200 Subject: [PATCH 071/160] Remove obsolete patches --- .github/workflows/build.yml | 3 +-- compatibility/patches/Base.patch | 13 ------------- compatibility/patches/DateAddFunction.patch | 10 ---------- compatibility/patches/DateSubFunction.patch | 10 ---------- compatibility/patches/DateTimeImmutableType.patch | 11 ----------- compatibility/patches/DateTimeType.patch | 11 ----------- 6 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 compatibility/patches/Base.patch delete mode 100644 compatibility/patches/DateAddFunction.patch delete mode 100644 compatibility/patches/DateSubFunction.patch delete mode 100644 compatibility/patches/DateTimeImmutableType.patch delete mode 100644 compatibility/patches/DateTimeType.patch diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28b40c02..e654a7a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,8 +112,7 @@ jobs: - php-version: "8.3" dependencies: "highest" update-packages: | - composer config extra.patches.doctrine/orm --json --merge '["compatibility/patches/Base.patch", "compatibility/patches/Column.patch", "compatibility/patches/DateAddFunction.patch", "compatibility/patches/DateSubFunction.patch", "compatibility/patches/DiscriminatorColumn.patch", "compatibility/patches/DiscriminatorMap.patch", "compatibility/patches/Embeddable.patch", "compatibility/patches/Embedded.patch", "compatibility/patches/Entity.patch", "compatibility/patches/GeneratedValue.patch", "compatibility/patches/Id.patch", "compatibility/patches/InheritanceType.patch", "compatibility/patches/JoinColumn.patch", "compatibility/patches/JoinColumns.patch", "compatibility/patches/ManyToMany.patch", "compatibility/patches/ManyToOne.patch", "compatibility/patches/MappedSuperclass.patch", "compatibility/patches/OneToMany.patch", "compatibility/patches/OneToOne.patch", "compatibility/patches/OrderBy.patch", "compatibility/patches/UniqueConstraint.patch", "compatibility/patches/Version.patch"]' - composer config extra.patches.carbonphp/carbon-doctrine-types --json --merge '["compatibility/patches/DateTimeImmutableType.patch", "compatibility/patches/DateTimeType.patch"]' + composer config extra.patches.doctrine/orm --json --merge '["compatibility/patches/Column.patch", "compatibility/patches/DiscriminatorColumn.patch", "compatibility/patches/DiscriminatorMap.patch", "compatibility/patches/Embeddable.patch", "compatibility/patches/Embedded.patch", "compatibility/patches/Entity.patch", "compatibility/patches/GeneratedValue.patch", "compatibility/patches/Id.patch", "compatibility/patches/InheritanceType.patch", "compatibility/patches/JoinColumn.patch", "compatibility/patches/JoinColumns.patch", "compatibility/patches/ManyToMany.patch", "compatibility/patches/ManyToOne.patch", "compatibility/patches/MappedSuperclass.patch", "compatibility/patches/OneToMany.patch", "compatibility/patches/OneToOne.patch", "compatibility/patches/OrderBy.patch", "compatibility/patches/UniqueConstraint.patch", "compatibility/patches/Version.patch"]' composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 -W steps: diff --git a/compatibility/patches/Base.patch b/compatibility/patches/Base.patch deleted file mode 100644 index 9a5f2ace..00000000 --- a/compatibility/patches/Base.patch +++ /dev/null @@ -1,13 +0,0 @@ ---- src/Query/Expr/Base.php 2024-02-09 14:21:17 -+++ src/Query/Expr/Base.php 2024-02-09 14:21:24 -@@ -33,6 +33,10 @@ - - public function __construct(mixed $args = []) - { -+ if (is_array($args) && array_key_exists(0, $args) && is_array($args[0])) { -+ $args = $args[0]; -+ } -+ - $this->addMultiple($args); - } - diff --git a/compatibility/patches/DateAddFunction.patch b/compatibility/patches/DateAddFunction.patch deleted file mode 100644 index 79a7606a..00000000 --- a/compatibility/patches/DateAddFunction.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- src/Query/AST/Functions/DateAddFunction.php 2024-02-09 14:22:59 -+++ src/Query/AST/Functions/DateAddFunction.php 2024-02-09 14:23:02 -@@ -71,7 +71,6 @@ - private function dispatchIntervalExpression(SqlWalker $sqlWalker): string - { - $sql = $this->intervalExpression->dispatch($sqlWalker); -- assert(is_numeric($sql)); - - return $sql; - } diff --git a/compatibility/patches/DateSubFunction.patch b/compatibility/patches/DateSubFunction.patch deleted file mode 100644 index 12a8fcf3..00000000 --- a/compatibility/patches/DateSubFunction.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- src/Query/AST/Functions/DateSubFunction.php 2024-02-09 14:22:31 -+++ src/Query/AST/Functions/DateSubFunction.php 2024-02-09 14:22:50 -@@ -64,7 +64,6 @@ - private function dispatchIntervalExpression(SqlWalker $sqlWalker): string - { - $sql = $this->intervalExpression->dispatch($sqlWalker); -- assert(is_numeric($sql)); - - return $sql; - } diff --git a/compatibility/patches/DateTimeImmutableType.patch b/compatibility/patches/DateTimeImmutableType.patch deleted file mode 100644 index e8525247..00000000 --- a/compatibility/patches/DateTimeImmutableType.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- src/Carbon/Doctrine/DateTimeImmutableType.php 2023-12-10 16:33:53 -+++ src/Carbon/Doctrine/DateTimeImmutableType.php 2024-02-09 11:36:50 -@@ -17,7 +17,7 @@ - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -- public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable -+ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?CarbonImmutable - { - return $this->doConvertToPHPValue($value); - } diff --git a/compatibility/patches/DateTimeType.patch b/compatibility/patches/DateTimeType.patch deleted file mode 100644 index 0a36920f..00000000 --- a/compatibility/patches/DateTimeType.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- src/Carbon/Doctrine/DateTimeType.php 2023-12-10 16:33:53 -+++ src/Carbon/Doctrine/DateTimeType.php 2024-02-09 11:36:58 -@@ -17,7 +17,7 @@ - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -- public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime -+ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Carbon - { - return $this->doConvertToPHPValue($value); - } From 66c248d09cde902ff83a6a2d53e7be92f5426e98 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 1 Sep 2024 14:15:29 +0200 Subject: [PATCH 072/160] Precise return type for `Result::rowCount()` based on detected driver --- extension.neon | 12 ++ ...wCountMethodDynamicReturnTypeExtension.php | 118 ++++++++++++++++++ .../MysqliResultRowCountReturnTypeTest.php | 35 ++++++ .../DBAL/PDOResultRowCountReturnTypeTest.php | 35 ++++++ .../DBAL/data/mysqli-result-row-count.php | 15 +++ .../DBAL/data/pdo-result-row-count.php | 15 +++ tests/Type/Doctrine/DBAL/mysqli.neon | 6 + tests/Type/Doctrine/DBAL/mysqli.php | 25 ++++ tests/Type/Doctrine/DBAL/pdo.neon | 6 + tests/Type/Doctrine/DBAL/pdo.php | 25 ++++ 10 files changed, 292 insertions(+) create mode 100644 src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php create mode 100644 tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php create mode 100644 tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php create mode 100644 tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php create mode 100644 tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php create mode 100644 tests/Type/Doctrine/DBAL/mysqli.neon create mode 100644 tests/Type/Doctrine/DBAL/mysqli.php create mode 100644 tests/Type/Doctrine/DBAL/pdo.neon create mode 100644 tests/Type/Doctrine/DBAL/pdo.php diff --git a/extension.neon b/extension.neon index a4f36b15..63365d37 100644 --- a/extension.neon +++ b/extension.neon @@ -313,6 +313,18 @@ services: class: PHPStan\Type\Doctrine\DBAL\QueryBuilder\QueryBuilderExecuteMethodExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Doctrine\DBAL\RowCountMethodDynamicReturnTypeExtension + arguments: + class: Doctrine\DBAL\Result + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Doctrine\DBAL\RowCountMethodDynamicReturnTypeExtension + arguments: + class: Doctrine\DBAL\Driver\Result + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension # Type descriptors - diff --git a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php new file mode 100644 index 00000000..bb4f626b --- /dev/null +++ b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php @@ -0,0 +1,118 @@ +class = $class; + $this->objectMetadataResolver = $objectMetadataResolver; + $this->driverDetector = $driverDetector; + $this->reflectionProvider = $reflectionProvider; + } + + public function getClass(): string + { + return $this->class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'rowCount'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $objectManager = $this->objectMetadataResolver->getObjectManager(); + if (!$objectManager instanceof EntityManagerInterface) { + return null; + } + + $connection = $objectManager->getConnection(); + $driver = $this->driverDetector->detect($connection); + if ($driver === null) { + return null; + } + + $resultClass = $this->getResultClass($driver); + if ($resultClass === null) { + return null; + } + + if (!$this->reflectionProvider->hasClass($resultClass)) { + return null; + } + + $resultReflection = $this->reflectionProvider->getClass($resultClass); + if (!$resultReflection->hasNativeMethod('rowCount')) { + return null; + } + + $rowCountMethod = $resultReflection->getNativeMethod('rowCount'); + $variant = ParametersAcceptorSelector::selectSingle($rowCountMethod->getVariants()); + + return $variant->getReturnType(); + } + + /** + * @param DriverDetector::* $driver + * @return class-string|null + */ + private function getResultClass(string $driver): ?string + { + switch ($driver) { + case DriverDetector::IBM_DB2: + return 'Doctrine\DBAL\Driver\IBMDB2\Result'; + case DriverDetector::MYSQLI: + return 'Doctrine\DBAL\Driver\Mysqli\Result'; + case DriverDetector::OCI8: + return 'Doctrine\DBAL\Driver\OCI8\Result'; + case DriverDetector::PDO_MYSQL: + case DriverDetector::PDO_OCI: + case DriverDetector::PDO_PGSQL: + case DriverDetector::PDO_SQLITE: + case DriverDetector::PDO_SQLSRV: + return 'Doctrine\DBAL\Driver\PDO\Result'; + case DriverDetector::PGSQL: + return 'Doctrine\DBAL\Driver\PgSQL\Result'; + case DriverDetector::SQLITE3: + return 'Doctrine\DBAL\Driver\SQLite3\Result'; + case DriverDetector::SQLSRV: + return 'Doctrine\DBAL\Driver\SQLSrv\Result'; + } + + return null; + } + +} diff --git a/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php new file mode 100644 index 00000000..5a4841f5 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php @@ -0,0 +1,35 @@ + */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** @return string[] */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/mysqli.neon']; + } + +} diff --git a/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php new file mode 100644 index 00000000..0b6aa6bc --- /dev/null +++ b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php @@ -0,0 +1,35 @@ + */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** @return string[] */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/pdo.neon']; + } + +} diff --git a/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php new file mode 100644 index 00000000..84f69543 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int|numeric-string', $r->rowCount()); +}; diff --git a/tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php b/tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php new file mode 100644 index 00000000..ec73a00c --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int', $r->rowCount()); +}; diff --git a/tests/Type/Doctrine/DBAL/mysqli.neon b/tests/Type/Doctrine/DBAL/mysqli.neon new file mode 100644 index 00000000..e287719f --- /dev/null +++ b/tests/Type/Doctrine/DBAL/mysqli.neon @@ -0,0 +1,6 @@ +includes: + - ../../../../extension.neon + +parameters: + doctrine: + objectManagerLoader: mysqli.php diff --git a/tests/Type/Doctrine/DBAL/mysqli.php b/tests/Type/Doctrine/DBAL/mysqli.php new file mode 100644 index 00000000..2bc11294 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/mysqli.php @@ -0,0 +1,25 @@ +setProxyDir(__DIR__); +$config->setProxyNamespace('App\GeneratedProxy'); +$config->setMetadataCache(new ArrayCachePool()); +$config->setMetadataDriverImpl(new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/data'] +)); + +return new EntityManager( + DriverManager::getConnection([ + 'driver' => 'mysqli', + 'memory' => true, + ]), + $config +); diff --git a/tests/Type/Doctrine/DBAL/pdo.neon b/tests/Type/Doctrine/DBAL/pdo.neon new file mode 100644 index 00000000..ee4897c8 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/pdo.neon @@ -0,0 +1,6 @@ +includes: + - ../../../../extension.neon + +parameters: + doctrine: + objectManagerLoader: pdo.php diff --git a/tests/Type/Doctrine/DBAL/pdo.php b/tests/Type/Doctrine/DBAL/pdo.php new file mode 100644 index 00000000..c7e48751 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/pdo.php @@ -0,0 +1,25 @@ +setProxyDir(__DIR__); +$config->setProxyNamespace('App\GeneratedProxy'); +$config->setMetadataCache(new ArrayCachePool()); +$config->setMetadataDriverImpl(new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/data'] +)); + +return new EntityManager( + DriverManager::getConnection([ + 'driver' => 'pdo_pgsql', + 'memory' => true, + ]), + $config +); From a9db60d6f68ef4c4184fd69ee0307e43b3a31039 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 1 Sep 2024 14:49:29 +0200 Subject: [PATCH 073/160] Fix test --- .../DBAL/MysqliResultRowCountReturnTypeTest.php | 8 +++++++- .../DBAL/data/mysqli-result-row-count-dbal-3.php | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-3.php diff --git a/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php index 5a4841f5..8990bda1 100644 --- a/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php +++ b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php @@ -2,6 +2,8 @@ namespace PHPStan\Type\Doctrine\DBAL; +use Composer\InstalledVersions; +use Composer\Semver\VersionParser; use PHPStan\Testing\TypeInferenceTestCase; class MysqliResultRowCountReturnTypeTest extends TypeInferenceTestCase @@ -10,7 +12,11 @@ class MysqliResultRowCountReturnTypeTest extends TypeInferenceTestCase /** @return iterable */ public function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count.php'); + if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=4.0')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count-dbal-3.php'); + } } /** diff --git a/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-3.php b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-3.php new file mode 100644 index 00000000..8226f6e4 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-3.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int', $r->rowCount()); +}; From 6fac80bcdceb58f553724fa154275bd52c26b608 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 1 Sep 2024 14:52:38 +0200 Subject: [PATCH 074/160] Fix build --- .../DBAL/RowCountMethodDynamicReturnTypeExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php index bb4f626b..8fe17348 100644 --- a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php @@ -105,9 +105,9 @@ private function getResultClass(string $driver): ?string case DriverDetector::PDO_SQLSRV: return 'Doctrine\DBAL\Driver\PDO\Result'; case DriverDetector::PGSQL: - return 'Doctrine\DBAL\Driver\PgSQL\Result'; + return 'Doctrine\DBAL\Driver\PgSQL\Result'; // @phpstan-ignore return.type case DriverDetector::SQLITE3: - return 'Doctrine\DBAL\Driver\SQLite3\Result'; + return 'Doctrine\DBAL\Driver\SQLite3\Result'; // @phpstan-ignore return.type case DriverDetector::SQLSRV: return 'Doctrine\DBAL\Driver\SQLSrv\Result'; } From 00439d504eb4baa3f608e3857ff72c91b2188b4a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 1 Sep 2024 14:59:12 +0200 Subject: [PATCH 075/160] Fix build --- .../DBAL/MysqliResultRowCountReturnTypeTest.php | 7 +++++-- .../DBAL/data/mysqli-result-row-count-dbal-2.php | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-2.php diff --git a/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php index 8990bda1..cdfa8490 100644 --- a/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php +++ b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php @@ -12,10 +12,13 @@ class MysqliResultRowCountReturnTypeTest extends TypeInferenceTestCase /** @return iterable */ public function dataFileAsserts(): iterable { - if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=4.0')) { + $versionParser = new VersionParser(); + if (InstalledVersions::satisfies($versionParser, 'doctrine/dbal', '>=4.0')) { yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count.php'); - } else { + } elseif (InstalledVersions::satisfies($versionParser, 'doctrine/dbal', '>=3.0')) { yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count-dbal-3.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count-dbal-2.php'); } } diff --git a/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-2.php b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-2.php new file mode 100644 index 00000000..235e6142 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-2.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int|string', $r->rowCount()); +}; From 7414a7d2ba3746bc6def9cccaa233d8a12b7d60a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 1 Sep 2024 15:05:16 +0200 Subject: [PATCH 076/160] Fix build --- .../DBAL/PDOResultRowCountReturnTypeTest.php | 9 ++++++++- .../DBAL/data/pdo-result-row-count-dbal-2.php | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/Type/Doctrine/DBAL/data/pdo-result-row-count-dbal-2.php diff --git a/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php index 0b6aa6bc..d10ef8ab 100644 --- a/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php +++ b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php @@ -2,6 +2,8 @@ namespace PHPStan\Type\Doctrine\DBAL; +use Composer\InstalledVersions; +use Composer\Semver\VersionParser; use PHPStan\Testing\TypeInferenceTestCase; class PDOResultRowCountReturnTypeTest extends TypeInferenceTestCase @@ -10,7 +12,12 @@ class PDOResultRowCountReturnTypeTest extends TypeInferenceTestCase /** @return iterable */ public function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count.php'); + $versionParser = new VersionParser(); + if (InstalledVersions::satisfies($versionParser, 'doctrine/dbal', '<3')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count-dbal-2.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count.php'); + } } /** diff --git a/tests/Type/Doctrine/DBAL/data/pdo-result-row-count-dbal-2.php b/tests/Type/Doctrine/DBAL/data/pdo-result-row-count-dbal-2.php new file mode 100644 index 00000000..ff6a8cf8 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/pdo-result-row-count-dbal-2.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int|string', $r->rowCount()); +}; From 38db3bad8f1567d7bf64806738d724261f8a2b5c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 1 Sep 2024 15:16:09 +0200 Subject: [PATCH 077/160] Fix internal error Closes https://github.com/phpstan/phpstan-doctrine/issues/602 --- src/Type/Doctrine/ArgumentsProcessor.php | 3 +++ tests/Rules/Doctrine/ORM/data/query-builder-dql.php | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Type/Doctrine/ArgumentsProcessor.php b/src/Type/Doctrine/ArgumentsProcessor.php index f2f88b82..4ace7f16 100644 --- a/src/Type/Doctrine/ArgumentsProcessor.php +++ b/src/Type/Doctrine/ArgumentsProcessor.php @@ -34,6 +34,9 @@ public function processArgs( { $args = []; foreach ($methodCallArgs as $arg) { + if ($arg->unpack) { + throw new DynamicQueryBuilderArgumentException(); + } $value = $scope->getType($arg->value); if ( $value instanceof ExprType diff --git a/tests/Rules/Doctrine/ORM/data/query-builder-dql.php b/tests/Rules/Doctrine/ORM/data/query-builder-dql.php index f94085a3..e221f3ed 100644 --- a/tests/Rules/Doctrine/ORM/data/query-builder-dql.php +++ b/tests/Rules/Doctrine/ORM/data/query-builder-dql.php @@ -291,12 +291,17 @@ public function qbExprMethod(): void $queryBuilder->getQuery(); } - public function bug602(array $objectConditions): void + public function bug602(array $objectConditions, bool $rand): void { + $orParts = ['e.title LIKE :termLike']; + if ($rand) { + $orParts[] = 'p.version = :term'; + } $queryBuilder = $this->entityManager->createQueryBuilder(); $queryBuilder->select('e') ->from(MyEntity::class, 'e') - ->andWhere($queryBuilder->expr()->orX(...$objectConditions)); + ->andWhere($queryBuilder->expr()->orX(...$orParts)) + ->setParameter('termLike', 'someTerm'); } } From 6cc1cb7405d6958cec844d1cda780dd7cf12e490 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 14:28:54 +0200 Subject: [PATCH 078/160] Test newer PHP versions --- .github/workflows/build.yml | 4 ++++ .github/workflows/platform-test.yml | 3 +++ composer.json | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e654a7a2..95471736 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" steps: - name: "Checkout" @@ -103,6 +104,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" @@ -163,6 +166,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" update-packages: - "" include: diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml index 5c063f0e..293c85fb 100644 --- a/.github/workflows/platform-test.yml +++ b/.github/workflows/platform-test.yml @@ -26,6 +26,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" update-packages: - "" include: @@ -35,6 +36,8 @@ jobs: update-packages: doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 - php-version: "8.3" update-packages: doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 + - php-version: "8.4" + update-packages: doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 steps: - name: "Checkout" diff --git a/composer.json b/composer.json index e992ccae..98c7981d 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11.7" + "phpstan/phpstan": "^1.12" }, "conflict": { "doctrine/collections": "<1.0", From d8b96f9b3125b9a245c4c1faf13e8764596467ec Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 14:29:24 +0200 Subject: [PATCH 079/160] Pin build-cs --- .github/workflows/build.yml | 1 + Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95471736..ccdeb5cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,6 +65,7 @@ jobs: with: repository: "phpstan/build-cs" path: "build-cs" + ref: "1.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/Makefile b/Makefile index 2ec6452c..c62886f9 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ lint: .PHONY: cs-install cs-install: git clone https://github.com/phpstan/build-cs.git || true - git -C build-cs fetch origin && git -C build-cs reset --hard origin/main + git -C build-cs fetch origin && git -C build-cs reset --hard origin/1.x composer install --working-dir build-cs .PHONY: cs From bfb1fc958a857b305f5986c56420e10a2a1b34ea Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 14:38:59 +0200 Subject: [PATCH 080/160] Allow installing dependencies on PHP 8.4 even when not all of them support it --- .github/workflows/build.yml | 12 ++++++++++++ .github/workflows/platform-test.yml | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ccdeb5cf..065346b2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,6 +41,10 @@ jobs: - name: "Validate Composer" run: "composer validate" + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" + - name: "Downgrade PHPUnit" if: matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^8.5.31 --no-update --update-with-dependencies" @@ -131,6 +135,10 @@ jobs: ini-file: development extensions: "mongodb" + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" + - name: "Downgrade PHPUnit" if: matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^8.5.31 --no-update --update-with-dependencies" @@ -186,6 +194,10 @@ jobs: extensions: "mongodb" ini-file: development + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" + - name: "Downgrade PHPUnit" if: matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^8.5.31 --no-update --update-with-dependencies" diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml index 293c85fb..fe88b6c9 100644 --- a/.github/workflows/platform-test.yml +++ b/.github/workflows/platform-test.yml @@ -51,6 +51,10 @@ jobs: ini-file: development extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" + - name: "Install dependencies" run: "composer install --no-interaction --no-progress" From 8badde6a55c02c186f9af98992012d1d26b817d3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 14:41:21 +0200 Subject: [PATCH 081/160] Bump PHPUnit --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 98c7981d..94c15b34 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-phpunit": "^1.3.13", "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^9.6.16", + "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", "symfony/cache": "^5.4" }, From 767698a41b3f00e05fbbdafa485d67b5ae7a85a7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 14:42:19 +0200 Subject: [PATCH 082/160] Open 2.0.x --- .github/workflows/build.yml | 2 +- .github/workflows/platform-test.yml | 2 +- .github/workflows/test-projects.yml | 2 +- composer.json | 9 ++++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 065346b2..bb82f08a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.5.x" + - "2.0.x" jobs: lint: diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml index fe88b6c9..38110353 100644 --- a/.github/workflows/platform-test.yml +++ b/.github/workflows/platform-test.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.5.x" + - "2.0.x" jobs: tests: diff --git a/.github/workflows/test-projects.yml b/.github/workflows/test-projects.yml index d33dcf6f..2b85f70e 100644 --- a/.github/workflows/test-projects.yml +++ b/.github/workflows/test-projects.yml @@ -5,7 +5,7 @@ name: "Test projects" on: push: branches: - - "1.5.x" + - "2.0.x" jobs: test-projects: diff --git a/composer.json b/composer.json index 94c15b34..7c12d599 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,8 @@ "MIT" ], "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" }, "conflict": { "doctrine/collections": "<1.0", @@ -30,10 +30,9 @@ "doctrine/persistence": "^2.2.1 || ^3.2", "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", - "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", "symfony/cache": "^5.4" From bf122092e27faf219adb723721c1dff95fda160e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 14:52:12 +0200 Subject: [PATCH 083/160] Stop testing PHP 7.2 and 7.3 --- .github/workflows/build.yml | 20 ------------------- composer.json | 10 +++++----- .../MysqliResultRowCountReturnTypeTest.php | 4 +--- .../DBAL/PDOResultRowCountReturnTypeTest.php | 9 +-------- .../data/mysqli-result-row-count-dbal-2.php | 15 -------------- .../DBAL/data/pdo-result-row-count-dbal-2.php | 15 -------------- 6 files changed, 7 insertions(+), 66 deletions(-) delete mode 100644 tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-2.php delete mode 100644 tests/Type/Doctrine/DBAL/data/pdo-result-row-count-dbal-2.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb82f08a..ebed3920 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,8 +17,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -45,10 +43,6 @@ jobs: if: matrix.php-version == '8.4' run: "composer config platform.php 8.3.99" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^8.5.31 --no-update --update-with-dependencies" - - name: "Install dependencies" run: "composer install --no-interaction --no-progress" @@ -103,8 +97,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -139,18 +131,10 @@ jobs: if: matrix.php-version == '8.4' run: "composer config platform.php 8.3.99" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^8.5.31 --no-update --update-with-dependencies" - - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} run: "composer update --prefer-lowest --no-interaction --no-progress" - - name: "Update Doctrine DBAl to ^3" - if: matrix.php-version != '7.2' && matrix.dependencies == 'lowest' - run: "composer require --dev doctrine/dbal:^3.3.8 --no-interaction --no-progress" - - name: "Install highest dependencies" if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" @@ -198,10 +182,6 @@ jobs: if: matrix.php-version == '8.4' run: "composer config platform.php 8.3.99" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^8.5.31 --no-update --update-with-dependencies" - - name: "Install dependencies" run: "composer update --no-interaction --no-progress" diff --git a/composer.json b/composer.json index 7c12d599..1bad25c5 100644 --- a/composer.json +++ b/composer.json @@ -20,12 +20,12 @@ "cache/array-adapter": "^1.1", "composer/semver": "^3.3.2", "cweagans/composer-patches": "^1.7.3", - "doctrine/annotations": "^1.11 || ^2.0", - "doctrine/collections": "^1.6 || ^2.1", + "doctrine/annotations": "^2.0", + "doctrine/collections": "^2.1", "doctrine/common": "^2.7 || ^3.0", - "doctrine/dbal": "^2.13.8 || ^3.3.3", - "doctrine/lexer": "^2.0 || ^3.0", - "doctrine/mongodb-odm": "^1.3 || ^2.4.3", + "doctrine/dbal": "^3.3.8", + "doctrine/lexer": "^3.0", + "doctrine/mongodb-odm": "^2.4.3", "doctrine/orm": "^2.16.0", "doctrine/persistence": "^2.2.1 || ^3.2", "gedmo/doctrine-extensions": "^3.8", diff --git a/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php index cdfa8490..89ac5eb6 100644 --- a/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php +++ b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php @@ -15,10 +15,8 @@ public function dataFileAsserts(): iterable $versionParser = new VersionParser(); if (InstalledVersions::satisfies($versionParser, 'doctrine/dbal', '>=4.0')) { yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count.php'); - } elseif (InstalledVersions::satisfies($versionParser, 'doctrine/dbal', '>=3.0')) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count-dbal-3.php'); } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count-dbal-2.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count-dbal-3.php'); } } diff --git a/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php index d10ef8ab..0b6aa6bc 100644 --- a/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php +++ b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php @@ -2,8 +2,6 @@ namespace PHPStan\Type\Doctrine\DBAL; -use Composer\InstalledVersions; -use Composer\Semver\VersionParser; use PHPStan\Testing\TypeInferenceTestCase; class PDOResultRowCountReturnTypeTest extends TypeInferenceTestCase @@ -12,12 +10,7 @@ class PDOResultRowCountReturnTypeTest extends TypeInferenceTestCase /** @return iterable */ public function dataFileAsserts(): iterable { - $versionParser = new VersionParser(); - if (InstalledVersions::satisfies($versionParser, 'doctrine/dbal', '<3')) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count-dbal-2.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count.php'); - } + yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count.php'); } /** diff --git a/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-2.php b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-2.php deleted file mode 100644 index 235e6142..00000000 --- a/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-2.php +++ /dev/null @@ -1,15 +0,0 @@ -rowCount()); -}; - -function (DriverResult $r): void { - assertType('int|string', $r->rowCount()); -}; diff --git a/tests/Type/Doctrine/DBAL/data/pdo-result-row-count-dbal-2.php b/tests/Type/Doctrine/DBAL/data/pdo-result-row-count-dbal-2.php deleted file mode 100644 index ff6a8cf8..00000000 --- a/tests/Type/Doctrine/DBAL/data/pdo-result-row-count-dbal-2.php +++ /dev/null @@ -1,15 +0,0 @@ -rowCount()); -}; - -function (DriverResult $r): void { - assertType('int|string', $r->rowCount()); -}; From df300b9b2e61c4db6e5f8b760fe697991b15d390 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 14:55:04 +0200 Subject: [PATCH 084/160] Update build-cs --- .github/workflows/build.yml | 2 +- Makefile | 2 +- ...trineProxyForbiddenClassNamesExtension.php | 3 +- src/Doctrine/DoctrineDiagnoseExtension.php | 10 +-- src/Doctrine/Mapping/ClassMetadataFactory.php | 3 +- src/Doctrine/Mapping/MappingDriverChain.php | 2 +- ...LiteralStringTypeNodeResolverExtension.php | 3 +- .../QueryTypeNodeResolverExtension.php | 5 +- ...rineSelectableClassReflectionExtension.php | 3 +- src/Reflection/Doctrine/DummyParameter.php | 18 ++-- ...tityRepositoryClassReflectionExtension.php | 3 +- .../MagicRepositoryMethodReflection.php | 11 +-- src/Rules/Doctrine/ORM/DqlRule.php | 3 +- src/Rules/Doctrine/ORM/EntityColumnRule.php | 36 ++++---- .../ORM/EntityConstructorNotFinalRule.php | 5 +- .../ORM/EntityMappingExceptionRule.php | 3 +- src/Rules/Doctrine/ORM/EntityNotFinalRule.php | 5 +- src/Rules/Doctrine/ORM/EntityRelationRule.php | 19 ++--- .../Doctrine/ORM/PropertiesExtension.php | 3 +- .../Doctrine/ORM/QueryBuilderDqlRule.php | 6 +- .../Doctrine/ORM/RepositoryMethodCallRule.php | 5 +- src/Rules/Gedmo/PropertiesExtension.php | 6 +- .../Doctrine/StubFilesExtensionLoader.php | 3 +- src/Type/Doctrine/ArgumentsProcessor.php | 3 +- .../IsEmptyTypeSpecifyingExtension.php | 9 +- .../CreateQueryDynamicReturnTypeExtension.php | 16 ++-- .../QueryBuilderExecuteMethodExtension.php | 3 +- ...wCountMethodDynamicReturnTypeExtension.php | 12 +-- .../Doctrine/DefaultDescriptorRegistry.php | 2 +- .../Doctrine/DescriptorRegistryFactory.php | 3 +- src/Type/Doctrine/Descriptors/BooleanType.php | 7 +- src/Type/Doctrine/Descriptors/DecimalType.php | 3 +- src/Type/Doctrine/Descriptors/FloatType.php | 5 +- .../Descriptors/Ramsey/UuidTypeDescriptor.php | 7 +- .../Descriptors/ReflectionDescriptor.php | 6 +- ...tityManagerInterfaceThrowTypeExtension.php | 4 +- ...etRepositoryDynamicReturnTypeExtension.php | 26 +++--- .../HydrationModeReturnTypeResolver.php | 6 +- src/Type/Doctrine/ObjectMetadataResolver.php | 13 ++- .../QueryResultDynamicReturnTypeExtension.php | 10 +-- .../Doctrine/Query/QueryResultTypeBuilder.php | 17 ++-- .../Doctrine/Query/QueryResultTypeWalker.php | 70 +++++++--------- src/Type/Doctrine/Query/QueryType.php | 9 +- ...QueryBuilderDynamicReturnTypeExtension.php | 8 +- ...seExpressionDynamicReturnTypeExtension.php | 3 +- .../Doctrine/QueryBuilder/Expr/ExprType.php | 3 +- ...ssionBuilderDynamicReturnTypeExtension.php | 6 +- .../NewExprDynamicReturnTypeExtension.php | 13 ++- .../OtherMethodQueryBuilderParser.php | 11 +-- ...uilderGetDqlDynamicReturnTypeExtension.php | 5 +- ...lderGetQueryDynamicReturnTypeExtension.php | 18 ++-- ...uilderMethodDynamicReturnTypeExtension.php | 5 +- .../QueryBuilder/QueryBuilderType.php | 2 +- .../QueryBuilderTypeSpecifyingExtension.php | 10 +-- ...BuilderExpressionTypeResolverExtension.php | 3 +- ...eProxyForbiddenClassNamesExtensionTest.php | 2 +- tests/Classes/entity-manager.php | 8 +- .../ODM/document-manager.php | 6 +- .../ORM/entity-manager.php | 6 +- tests/Platform/Entity/PlatformEntity.php | 83 ++++++------------- .../Platform/Entity/PlatformRelatedEntity.php | 3 +- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 48 +++++------ ...SelectableClassReflectionExtensionTest.php | 6 +- tests/Rules/DeadCode/entity-manager.php | 6 +- .../Doctrine/ORM/EntityColumnRuleTest.php | 8 +- .../ORM/EntityConstructorNotFinalRuleTest.php | 5 +- .../ORM/EntityMappingExceptionRuleTest.php | 2 +- .../Doctrine/ORM/EntityNotFinalRuleTest.php | 5 +- .../Doctrine/ORM/EntityRelationRuleTest.php | 8 +- .../ORM/QueryBuilderDqlRuleSlowTest.php | 2 +- .../Doctrine/ORM/QueryBuilderDqlRuleTest.php | 2 +- tests/Rules/Doctrine/ORM/entity-manager.php | 8 +- tests/Rules/Properties/entity-manager.php | 6 +- tests/Type/Doctrine/DBAL/mysqli.php | 4 +- tests/Type/Doctrine/DBAL/pdo.php | 4 +- ...lectableDynamicReturnTypeExtensionTest.php | 9 +- ...QueryResultTypeWalkerHydrationModeTest.php | 10 +-- .../Query/QueryResultTypeWalkerTest.php | 60 +++++++------- 78 files changed, 311 insertions(+), 467 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ebed3920..3230e295 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,7 +63,7 @@ jobs: with: repository: "phpstan/build-cs" path: "build-cs" - ref: "1.x" + ref: "2.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/Makefile b/Makefile index c62886f9..efc169db 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ lint: .PHONY: cs-install cs-install: git clone https://github.com/phpstan/build-cs.git || true - git -C build-cs fetch origin && git -C build-cs reset --hard origin/1.x + git -C build-cs fetch origin && git -C build-cs reset --hard origin/2.x composer install --working-dir build-cs .PHONY: cs diff --git a/src/Classes/DoctrineProxyForbiddenClassNamesExtension.php b/src/Classes/DoctrineProxyForbiddenClassNamesExtension.php index 1ff7f4f6..dbca26af 100644 --- a/src/Classes/DoctrineProxyForbiddenClassNamesExtension.php +++ b/src/Classes/DoctrineProxyForbiddenClassNamesExtension.php @@ -8,8 +8,7 @@ class DoctrineProxyForbiddenClassNamesExtension implements ForbiddenClassNameExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Doctrine/DoctrineDiagnoseExtension.php b/src/Doctrine/DoctrineDiagnoseExtension.php index 58fda398..0502c736 100644 --- a/src/Doctrine/DoctrineDiagnoseExtension.php +++ b/src/Doctrine/DoctrineDiagnoseExtension.php @@ -15,11 +15,9 @@ class DoctrineDiagnoseExtension implements DiagnoseExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct( ObjectMetadataResolver $objectMetadataResolver, @@ -34,7 +32,7 @@ public function print(Output $output): void { $output->writeLineFormatted(sprintf( 'Doctrine\'s objectManagerLoader: %s', - $this->objectMetadataResolver->hasObjectManagerLoader() ? 'In use' : 'No' + $this->objectMetadataResolver->hasObjectManagerLoader() ? 'In use' : 'No', )); $objectManager = $this->objectMetadataResolver->getObjectManager(); @@ -44,7 +42,7 @@ public function print(Output $output): void $output->writeLineFormatted(sprintf( 'Detected driver: %s', - $driver === null ? 'None' : $driver + $driver === null ? 'None' : $driver, )); } diff --git a/src/Doctrine/Mapping/ClassMetadataFactory.php b/src/Doctrine/Mapping/ClassMetadataFactory.php index b2f82388..764268f1 100644 --- a/src/Doctrine/Mapping/ClassMetadataFactory.php +++ b/src/Doctrine/Mapping/ClassMetadataFactory.php @@ -18,8 +18,7 @@ class ClassMetadataFactory extends \Doctrine\ORM\Mapping\ClassMetadataFactory { - /** @var string */ - private $tmpDir; + private string $tmpDir; public function __construct(string $tmpDir) { diff --git a/src/Doctrine/Mapping/MappingDriverChain.php b/src/Doctrine/Mapping/MappingDriverChain.php index ebcdc423..532b208b 100644 --- a/src/Doctrine/Mapping/MappingDriverChain.php +++ b/src/Doctrine/Mapping/MappingDriverChain.php @@ -11,7 +11,7 @@ class MappingDriverChain implements MappingDriver { /** @var MappingDriver[] */ - private $drivers; + private array $drivers; /** * @param MappingDriver[] $drivers diff --git a/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php b/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php index 3aa371d5..b034f57a 100644 --- a/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php +++ b/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php @@ -14,8 +14,7 @@ class DoctrineLiteralStringTypeNodeResolverExtension implements TypeNodeResolverExtension { - /** @var bool */ - private $enabled; + private bool $enabled; public function __construct(bool $enabled) { diff --git a/src/PhpDoc/Doctrine/QueryTypeNodeResolverExtension.php b/src/PhpDoc/Doctrine/QueryTypeNodeResolverExtension.php index 39cd65d9..c8193892 100644 --- a/src/PhpDoc/Doctrine/QueryTypeNodeResolverExtension.php +++ b/src/PhpDoc/Doctrine/QueryTypeNodeResolverExtension.php @@ -18,8 +18,7 @@ class QueryTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension { - /** @var TypeNodeResolver */ - private $typeNodeResolver; + private TypeNodeResolver $typeNodeResolver; public function setTypeNodeResolver(TypeNodeResolver $typeNodeResolver): void { @@ -47,7 +46,7 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type [ new NullType(), $this->typeNodeResolver->resolve($typeNode->genericTypes[0], $nameScope), - ] + ], ); } diff --git a/src/Reflection/Doctrine/DoctrineSelectableClassReflectionExtension.php b/src/Reflection/Doctrine/DoctrineSelectableClassReflectionExtension.php index 16970a60..ca726d8c 100644 --- a/src/Reflection/Doctrine/DoctrineSelectableClassReflectionExtension.php +++ b/src/Reflection/Doctrine/DoctrineSelectableClassReflectionExtension.php @@ -10,8 +10,7 @@ class DoctrineSelectableClassReflectionExtension implements MethodsClassReflectionExtension { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; public function __construct(ReflectionProvider $reflectionProvider) { diff --git a/src/Reflection/Doctrine/DummyParameter.php b/src/Reflection/Doctrine/DummyParameter.php index 725f29f6..00eefa2c 100644 --- a/src/Reflection/Doctrine/DummyParameter.php +++ b/src/Reflection/Doctrine/DummyParameter.php @@ -9,23 +9,17 @@ class DummyParameter implements ParameterReflection { - /** @var string */ - private $name; + private string $name; - /** @var Type */ - private $type; + private Type $type; - /** @var bool */ - private $optional; + private bool $optional; - /** @var PassedByReference */ - private $passedByReference; + private PassedByReference $passedByReference; - /** @var bool */ - private $variadic; + private bool $variadic; - /** @var Type|null */ - private $defaultValue; + private ?Type $defaultValue = null; public function __construct(string $name, Type $type, bool $optional, ?PassedByReference $passedByReference, bool $variadic, ?Type $defaultValue) { diff --git a/src/Reflection/Doctrine/EntityRepositoryClassReflectionExtension.php b/src/Reflection/Doctrine/EntityRepositoryClassReflectionExtension.php index 799349da..52a8dcff 100644 --- a/src/Reflection/Doctrine/EntityRepositoryClassReflectionExtension.php +++ b/src/Reflection/Doctrine/EntityRepositoryClassReflectionExtension.php @@ -22,8 +22,7 @@ class EntityRepositoryClassReflectionExtension implements MethodsClassReflectionExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Reflection/Doctrine/MagicRepositoryMethodReflection.php b/src/Reflection/Doctrine/MagicRepositoryMethodReflection.php index 125f1440..94dc6011 100644 --- a/src/Reflection/Doctrine/MagicRepositoryMethodReflection.php +++ b/src/Reflection/Doctrine/MagicRepositoryMethodReflection.php @@ -21,14 +21,11 @@ class MagicRepositoryMethodReflection implements MethodReflection { - /** @var ClassReflection */ - private $declaringClass; + private ClassReflection $declaringClass; - /** @var string */ - private $name; + private string $name; - /** @var Type */ - private $type; + private Type $type; public function __construct( ClassReflection $declaringClass, @@ -104,7 +101,7 @@ public function getVariants(): array null, $arguments, false, - $this->type + $this->type, ), ]; } diff --git a/src/Rules/Doctrine/ORM/DqlRule.php b/src/Rules/Doctrine/ORM/DqlRule.php index 7a121525..77cd3664 100644 --- a/src/Rules/Doctrine/ORM/DqlRule.php +++ b/src/Rules/Doctrine/ORM/DqlRule.php @@ -21,8 +21,7 @@ class DqlRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index 70c1fcea..c2ee7a15 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -34,23 +34,17 @@ class EntityColumnRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var bool */ - private $reportUnknownTypes; + private bool $reportUnknownTypes; - /** @var bool */ - private $allowNullablePropertyForRequiredField; + private bool $allowNullablePropertyForRequiredField; - /** @var bool */ - private $bleedingEdge; + private bool $bleedingEdge; public function __construct( ObjectMetadataResolver $objectMetadataResolver, @@ -106,7 +100,7 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s: Doctrine type "%s" does not have any registered descriptor.', $className, $propertyName, - $fieldMapping['type'] + $fieldMapping['type'], ))->identifier('doctrine.descriptorNotFound')->build(), ] : []; } @@ -128,7 +122,7 @@ public function processNode(Node $node, Scope $scope): array $propertyName, $backedEnumType->describe(VerbosityLevel::typeOnly()), $enumReflection->getDisplayName(), - $writableToDatabaseType->describe(VerbosityLevel::typeOnly()) + $writableToDatabaseType->describe(VerbosityLevel::typeOnly()), ))->identifier('doctrine.enumType')->build(); } } @@ -151,8 +145,8 @@ public function processNode(Node $node, Scope $scope): array $backedEnumType->describe(VerbosityLevel::typeOnly()), $enumReflection->getDisplayName(), $writableToDatabaseType->getIterableValueType()->describe(VerbosityLevel::typeOnly()), - $writableToDatabaseType->describe(VerbosityLevel::typeOnly()) - ) + $writableToDatabaseType->describe(VerbosityLevel::typeOnly()), + ), )->identifier('doctrine.enumType')->build(); } } @@ -160,11 +154,11 @@ public function processNode(Node $node, Scope $scope): array $writableToPropertyType = TypeCombinator::intersect(new ArrayType( $writableToPropertyType->getIterableKeyType(), - $enumType + $enumType, ), ...TypeUtils::getAccessoryTypes($writableToPropertyType)); $writableToDatabaseType = TypeCombinator::intersect(new ArrayType( $writableToDatabaseType->getIterableKeyType(), - $enumType + $enumType, ), ...TypeUtils::getAccessoryTypes($writableToDatabaseType)); } @@ -211,7 +205,7 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, $writableToPropertyType->describe(VerbosityLevel::getRecommendedLevelByType($propertyTransformedType, $writableToPropertyType)), - $propertyType->describe(VerbosityLevel::getRecommendedLevelByType($propertyTransformedType, $writableToPropertyType)) + $propertyType->describe(VerbosityLevel::getRecommendedLevelByType($propertyTransformedType, $writableToPropertyType)), ))->identifier('doctrine.columnType')->build(); } @@ -219,7 +213,7 @@ public function processNode(Node $node, Scope $scope): array !$writableToDatabaseType->isSuperTypeOf( $this->allowNullablePropertyForRequiredField || (in_array($propertyName, $identifiers, true) && !$nullable) ? TypeCombinator::removeNull($propertyType) - : $propertyType + : $propertyType, )->yes() ) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -227,7 +221,7 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, $propertyTransformedType->describe(VerbosityLevel::getRecommendedLevelByType($writableToDatabaseType, $propertyType)), - $writableToDatabaseType->describe(VerbosityLevel::getRecommendedLevelByType($writableToDatabaseType, $propertyType)) + $writableToDatabaseType->describe(VerbosityLevel::getRecommendedLevelByType($writableToDatabaseType, $propertyType)), ))->identifier('doctrine.columnType')->build(); } return $errors; diff --git a/src/Rules/Doctrine/ORM/EntityConstructorNotFinalRule.php b/src/Rules/Doctrine/ORM/EntityConstructorNotFinalRule.php index 5fd01213..24876b38 100644 --- a/src/Rules/Doctrine/ORM/EntityConstructorNotFinalRule.php +++ b/src/Rules/Doctrine/ORM/EntityConstructorNotFinalRule.php @@ -17,8 +17,7 @@ class EntityConstructorNotFinalRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { @@ -57,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Constructor of class %s is final which can cause problems with proxies.', - $classReflection->getDisplayName() + $classReflection->getDisplayName(), ))->identifier('doctrine.finalConstructor')->build(), ]; } diff --git a/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php b/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php index d74019ba..d0ce97f7 100644 --- a/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php +++ b/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php @@ -18,8 +18,7 @@ class EntityMappingExceptionRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct( ObjectMetadataResolver $objectMetadataResolver diff --git a/src/Rules/Doctrine/ORM/EntityNotFinalRule.php b/src/Rules/Doctrine/ORM/EntityNotFinalRule.php index b4dfe4c7..9dd39ff3 100644 --- a/src/Rules/Doctrine/ORM/EntityNotFinalRule.php +++ b/src/Rules/Doctrine/ORM/EntityNotFinalRule.php @@ -17,8 +17,7 @@ class EntityNotFinalRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { @@ -52,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Entity class %s is final which can cause problems with proxies.', - $classReflection->getDisplayName() + $classReflection->getDisplayName(), ))->identifier('doctrine.finalEntity')->build(), ]; } diff --git a/src/Rules/Doctrine/ORM/EntityRelationRule.php b/src/Rules/Doctrine/ORM/EntityRelationRule.php index 69e0c2a5..97e3ae2f 100644 --- a/src/Rules/Doctrine/ORM/EntityRelationRule.php +++ b/src/Rules/Doctrine/ORM/EntityRelationRule.php @@ -28,14 +28,11 @@ class EntityRelationRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var bool */ - private $allowNullablePropertyForRequiredField; + private bool $allowNullablePropertyForRequiredField; - /** @var bool */ - private $bleedingEdge; + private bool $bleedingEdge; public function __construct( ObjectMetadataResolver $objectMetadataResolver, @@ -102,7 +99,7 @@ public function processNode(Node $node, Scope $scope): array $toMany = true; $columnType = TypeCombinator::intersect( new ObjectType('Doctrine\Common\Collections\Collection'), - new IterableType(new MixedType(), new ObjectType($associationMapping['targetEntity'])) + new IterableType(new MixedType(), new ObjectType($associationMapping['targetEntity'])), ); } @@ -125,7 +122,7 @@ public function processNode(Node $node, Scope $scope): array ) { $propertyTypeToCheckAgainst = TypeCombinator::intersect( $collectionObjectType, - new IterableType(new MixedType(true), $propertyType->getIterableValueType()) + new IterableType(new MixedType(true), $propertyType->getIterableValueType()), ); } if (!$propertyTypeToCheckAgainst->isSuperTypeOf($columnType)->yes()) { @@ -134,14 +131,14 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, $columnType->describe(VerbosityLevel::typeOnly()), - $propertyType->describe(VerbosityLevel::typeOnly()) + $propertyType->describe(VerbosityLevel::typeOnly()), ))->identifier('doctrine.associationType')->build(); } if ( !$columnType->isSuperTypeOf( $this->allowNullablePropertyForRequiredField ? TypeCombinator::removeNull($propertyType) - : $propertyType + : $propertyType, )->yes() ) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -149,7 +146,7 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, $propertyType->describe(VerbosityLevel::typeOnly()), - $columnType->describe(VerbosityLevel::typeOnly()) + $columnType->describe(VerbosityLevel::typeOnly()), ))->identifier('doctrine.associationType')->build(); } } diff --git a/src/Rules/Doctrine/ORM/PropertiesExtension.php b/src/Rules/Doctrine/ORM/PropertiesExtension.php index 4fdbf57d..9eab7dd7 100644 --- a/src/Rules/Doctrine/ORM/PropertiesExtension.php +++ b/src/Rules/Doctrine/ORM/PropertiesExtension.php @@ -12,8 +12,7 @@ class PropertiesExtension implements ReadWritePropertiesExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php index 8596810d..62155d01 100644 --- a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php +++ b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php @@ -26,11 +26,9 @@ class QueryBuilderDqlRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var bool */ - private $reportDynamicQueryBuilders; + private bool $reportDynamicQueryBuilders; public function __construct( ObjectMetadataResolver $objectMetadataResolver, diff --git a/src/Rules/Doctrine/ORM/RepositoryMethodCallRule.php b/src/Rules/Doctrine/ORM/RepositoryMethodCallRule.php index 9061d6c4..9f5231fe 100644 --- a/src/Rules/Doctrine/ORM/RepositoryMethodCallRule.php +++ b/src/Rules/Doctrine/ORM/RepositoryMethodCallRule.php @@ -19,8 +19,7 @@ class RepositoryMethodCallRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { @@ -82,7 +81,7 @@ public function processNode(Node $node, Scope $scope): array $calledOnType->describe(VerbosityLevel::typeOnly()), $methodName, $entityClassNames[0], - $fieldName->getValue() + $fieldName->getValue(), ))->identifier(sprintf('doctrine.%sArgument', $methodName))->build(); } } diff --git a/src/Rules/Gedmo/PropertiesExtension.php b/src/Rules/Gedmo/PropertiesExtension.php index d43b4308..e82f3bc5 100644 --- a/src/Rules/Gedmo/PropertiesExtension.php +++ b/src/Rules/Gedmo/PropertiesExtension.php @@ -41,11 +41,9 @@ class PropertiesExtension implements ReadWritePropertiesExtension Gedmo\Language::class, ]; - /** @var AnnotationReader|null */ - private $annotationReader; + private ?AnnotationReader $annotationReader = null; - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Stubs/Doctrine/StubFilesExtensionLoader.php b/src/Stubs/Doctrine/StubFilesExtensionLoader.php index 0b3a69d8..cb93222d 100644 --- a/src/Stubs/Doctrine/StubFilesExtensionLoader.php +++ b/src/Stubs/Doctrine/StubFilesExtensionLoader.php @@ -14,8 +14,7 @@ class StubFilesExtensionLoader implements StubFilesExtension { - /** @var Reflector */ - private $reflector; + private Reflector $reflector; public function __construct( Reflector $reflector diff --git a/src/Type/Doctrine/ArgumentsProcessor.php b/src/Type/Doctrine/ArgumentsProcessor.php index 4ace7f16..77eeb5d7 100644 --- a/src/Type/Doctrine/ArgumentsProcessor.php +++ b/src/Type/Doctrine/ArgumentsProcessor.php @@ -13,8 +13,7 @@ class ArgumentsProcessor { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php b/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php index 0d440940..d94a455a 100644 --- a/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php +++ b/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php @@ -19,11 +19,10 @@ final class IsEmptyTypeSpecifyingExtension implements MethodTypeSpecifyingExtens private const FIRST_METHOD_NAME = 'first'; private const LAST_METHOD_NAME = 'last'; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; /** @var class-string */ - private $collectionClass; + private string $collectionClass; /** * @param class-string $collectionClass @@ -61,13 +60,13 @@ public function specifyTypes( $first = $this->typeSpecifier->create( new MethodCall($node->var, self::FIRST_METHOD_NAME), new ConstantBooleanType(false), - $context + $context, ); $last = $this->typeSpecifier->create( new MethodCall($node->var, self::LAST_METHOD_NAME), new ConstantBooleanType(false), - $context + $context, ); return $first->unionWith($last); diff --git a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php index 1cf5d50a..b78f8467 100644 --- a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php @@ -33,17 +33,13 @@ final class CreateQueryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; - /** @var PhpVersion */ - private $phpVersion; + private PhpVersion $phpVersion; - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct( ObjectMetadataResolver $objectMetadataResolver, @@ -80,7 +76,7 @@ public function getTypeFromMethodCall( if (!isset($args[$queryStringArgIndex])) { return new GenericObjectType( Query::class, - [new MixedType(), new MixedType()] + [new MixedType(), new MixedType()], ); } @@ -113,7 +109,7 @@ public function getTypeFromMethodCall( } return new GenericObjectType( Query::class, - [new MixedType(), new MixedType()] + [new MixedType(), new MixedType()], ); }); } diff --git a/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php b/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php index 31170cfc..a997c2da 100644 --- a/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php +++ b/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php @@ -19,8 +19,7 @@ class QueryBuilderExecuteMethodExtension implements DynamicMethodReturnTypeExtension { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; public function __construct(ReflectionProvider $reflectionProvider) { diff --git a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php index 8fe17348..03a988e1 100644 --- a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php @@ -17,17 +17,13 @@ class RowCountMethodDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string */ - private $class; + private string $class; - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; public function __construct( string $class, diff --git a/src/Type/Doctrine/DefaultDescriptorRegistry.php b/src/Type/Doctrine/DefaultDescriptorRegistry.php index 83647e3b..74ea184d 100644 --- a/src/Type/Doctrine/DefaultDescriptorRegistry.php +++ b/src/Type/Doctrine/DefaultDescriptorRegistry.php @@ -9,7 +9,7 @@ class DefaultDescriptorRegistry implements DescriptorRegistry { /** @var array, DoctrineTypeDescriptor> */ - private $descriptors = []; + private array $descriptors = []; /** * @param DoctrineTypeDescriptor[] $descriptors diff --git a/src/Type/Doctrine/DescriptorRegistryFactory.php b/src/Type/Doctrine/DescriptorRegistryFactory.php index 3e7dd190..2dbf70aa 100644 --- a/src/Type/Doctrine/DescriptorRegistryFactory.php +++ b/src/Type/Doctrine/DescriptorRegistryFactory.php @@ -9,8 +9,7 @@ class DescriptorRegistryFactory public const TYPE_DESCRIPTOR_TAG = 'phpstan.doctrine.typeDescriptor'; - /** @var Container */ - private $container; + private Container $container; public function __construct(Container $container) { diff --git a/src/Type/Doctrine/Descriptors/BooleanType.php b/src/Type/Doctrine/Descriptors/BooleanType.php index b9e59574..54e7f662 100644 --- a/src/Type/Doctrine/Descriptors/BooleanType.php +++ b/src/Type/Doctrine/Descriptors/BooleanType.php @@ -12,8 +12,7 @@ class BooleanType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct(DriverDetector $driverDetector) { @@ -40,7 +39,7 @@ public function getDatabaseInternalType(): Type return TypeCombinator::union( new ConstantIntegerType(0), new ConstantIntegerType(1), - new \PHPStan\Type\BooleanType() + new \PHPStan\Type\BooleanType(), ); } @@ -60,7 +59,7 @@ public function getDatabaseInternalTypeForDriver(Connection $connection): Type ], true)) { return TypeCombinator::union( new ConstantIntegerType(0), - new ConstantIntegerType(1) + new ConstantIntegerType(1), ); } diff --git a/src/Type/Doctrine/Descriptors/DecimalType.php b/src/Type/Doctrine/Descriptors/DecimalType.php index 64184c45..08773fdc 100644 --- a/src/Type/Doctrine/Descriptors/DecimalType.php +++ b/src/Type/Doctrine/Descriptors/DecimalType.php @@ -16,8 +16,7 @@ class DecimalType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct(DriverDetector $driverDetector) { diff --git a/src/Type/Doctrine/Descriptors/FloatType.php b/src/Type/Doctrine/Descriptors/FloatType.php index 2518e72d..f435293b 100644 --- a/src/Type/Doctrine/Descriptors/FloatType.php +++ b/src/Type/Doctrine/Descriptors/FloatType.php @@ -15,8 +15,7 @@ class FloatType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct(DriverDetector $driverDetector) { @@ -45,7 +44,7 @@ public function getDatabaseInternalType(): Type new IntersectionType([ new StringType(), new AccessoryNumericStringType(), - ]) + ]), ); } diff --git a/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php b/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php index 78501f2c..549b14c4 100644 --- a/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php +++ b/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php @@ -23,8 +23,7 @@ class UuidTypeDescriptor implements DoctrineTypeDescriptor FakeTestingUuidType::class, ]; - /** @var string */ - private $uuidTypeName; + private string $uuidTypeName; public function __construct( string $uuidTypeName @@ -33,7 +32,7 @@ public function __construct( if (!in_array($uuidTypeName, self::SUPPORTED_UUID_TYPES, true)) { throw new ShouldNotHappenException(sprintf( 'Unexpected UUID column type "%s" provided', - $uuidTypeName + $uuidTypeName, )); } @@ -55,7 +54,7 @@ public function getWritableToDatabaseType(): Type { return TypeCombinator::union( new StringType(), - new ObjectType(UuidInterface::class) + new ObjectType(UuidInterface::class), ); } diff --git a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php index 7d7cb778..f4b5dba0 100644 --- a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php +++ b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php @@ -21,11 +21,9 @@ class ReflectionDescriptor implements DoctrineTypeDescriptor, DoctrineTypeDriver /** @var class-string */ private $type; - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var Container */ - private $container; + private Container $container; /** * @param class-string $type diff --git a/src/Type/Doctrine/EntityManagerInterfaceThrowTypeExtension.php b/src/Type/Doctrine/EntityManagerInterfaceThrowTypeExtension.php index 0070af9f..be3407c3 100644 --- a/src/Type/Doctrine/EntityManagerInterfaceThrowTypeExtension.php +++ b/src/Type/Doctrine/EntityManagerInterfaceThrowTypeExtension.php @@ -37,9 +37,7 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M if ((new ObjectType(EntityManagerInterface::class))->isSuperTypeOf($type)->yes()) { return TypeCombinator::union( - ...array_map(static function ($class): Type { - return new ObjectType($class); - }, self::SUPPORTED_METHOD[$methodReflection->getName()]) + ...array_map(static fn ($class): Type => new ObjectType($class), self::SUPPORTED_METHOD[$methodReflection->getName()]), ); } diff --git a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php index fd90fdd5..d514a12b 100644 --- a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php @@ -27,23 +27,17 @@ class GetRepositoryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var string|null */ - private $repositoryClass; + private ?string $repositoryClass = null; - /** @var string|null */ - private $ormRepositoryClass; + private ?string $ormRepositoryClass = null; - /** @var string|null */ - private $odmRepositoryClass; + private ?string $odmRepositoryClass = null; - /** @var string */ - private $managerClass; + private string $managerClass; - /** @var ObjectMetadataResolver */ - private $metadataResolver; + private ObjectMetadataResolver $metadataResolver; public function __construct( ReflectionProvider $reflectionProvider, @@ -87,7 +81,7 @@ public function getTypeFromMethodCall( if (count($methodCall->getArgs()) === 0) { return new GenericObjectType( $defaultRepositoryClass, - [new ObjectWithoutClassType()] + [new ObjectWithoutClassType()], ); } $argType = $scope->getType($methodCall->getArgs()[0]->value); @@ -101,7 +95,7 @@ public function getTypeFromMethodCall( if (count($objectNames) === 0) { return new GenericObjectType( $defaultRepositoryClass, - [$classType] + [$classType], ); } @@ -127,13 +121,13 @@ private function getDefaultReturnType(Scope $scope, array $args, MethodReflectio $defaultType = ParametersAcceptorSelector::selectFromArgs( $scope, $args, - $methodReflection->getVariants() + $methodReflection->getVariants(), )->getReturnType(); $entity = $defaultType->getTemplateType(ObjectRepository::class, 'TEntityClass'); if (!$entity instanceof ErrorType) { return new GenericObjectType( $defaultRepositoryClass, - [$entity] + [$entity], ); } diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php index 2f1c8e36..c0522e7f 100644 --- a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -73,18 +73,18 @@ public function getMethodReturnTypeForHydrationMode( case 'toIterable': return new IterableType( $queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType, - $queryResultType + $queryResultType, ); default: if ($queryKeyType->isNull()->yes()) { return AccessoryArrayListType::intersectWith(new ArrayType( new IntegerType(), - $queryResultType + $queryResultType, )); } return new ArrayType( $queryKeyType, - $queryResultType + $queryResultType, ); } } diff --git a/src/Type/Doctrine/ObjectMetadataResolver.php b/src/Type/Doctrine/ObjectMetadataResolver.php index 6a8b4fd2..e1d97106 100644 --- a/src/Type/Doctrine/ObjectMetadataResolver.php +++ b/src/Type/Doctrine/ObjectMetadataResolver.php @@ -17,17 +17,14 @@ final class ObjectMetadataResolver { - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; /** @var ObjectManager|false|null */ private $objectManager; - /** @var ClassMetadataFactory|null */ - private $metadataFactory; + private ?ClassMetadataFactory $metadataFactory = null; - /** @var string */ - private $tmpDir; + private string $tmpDir; public function __construct( ?string $objectManagerLoader, @@ -150,14 +147,14 @@ private function loadObjectManager(string $objectManagerLoader): ?ObjectManager if (!is_file($objectManagerLoader)) { throw new ShouldNotHappenException(sprintf( 'Object manager could not be loaded: file "%s" does not exist', - $objectManagerLoader + $objectManagerLoader, )); } if (!is_readable($objectManagerLoader)) { throw new ShouldNotHappenException(sprintf( 'Object manager could not be loaded: file "%s" is not readable', - $objectManagerLoader + $objectManagerLoader, )); } diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 091e3716..24aecb38 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -27,11 +27,9 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn 'getSingleResult' => 0, ]; - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var HydrationModeReturnTypeResolver */ - private $hydrationModeReturnTypeResolver; + private HydrationModeReturnTypeResolver $hydrationModeReturnTypeResolver; public function __construct( ObjectMetadataResolver $objectMetadataResolver, @@ -71,7 +69,7 @@ public function getTypeFromMethodCall( $hydrationMode = $scope->getType($args[$argIndex]->value); } else { $parametersAcceptor = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() + $methodReflection->getVariants(), ); $parameter = $parametersAcceptor->getParameters()[$argIndex]; $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); @@ -84,7 +82,7 @@ public function getTypeFromMethodCall( $hydrationMode, $queryType->getTemplateType(AbstractQuery::class, 'TKey'), $queryType->getTemplateType(AbstractQuery::class, 'TResult'), - $this->objectMetadataResolver->getObjectManager() + $this->objectMetadataResolver->getObjectManager(), ); } diff --git a/src/Type/Doctrine/Query/QueryResultTypeBuilder.php b/src/Type/Doctrine/Query/QueryResultTypeBuilder.php index 30941cb3..2f6fae11 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeBuilder.php +++ b/src/Type/Doctrine/Query/QueryResultTypeBuilder.php @@ -21,15 +21,13 @@ final class QueryResultTypeBuilder { - /** @var bool */ - private $selectQuery = false; + private bool $selectQuery = false; /** * Whether the result is an array shape or a single entity or NEW object * - * @var bool */ - private $isShape = false; + private bool $isShape = false; /** * Map from selected entity aliases to entity types @@ -38,7 +36,7 @@ final class QueryResultTypeBuilder * * @var array */ - private $entities = []; + private array $entities = []; /** * Map from selected entity alias to result alias @@ -47,24 +45,23 @@ final class QueryResultTypeBuilder * * @var array */ - private $entityResultAliases = []; + private array $entityResultAliases = []; /** * Map from selected scalar result alias to scalar type * * @var array */ - private $scalars = []; + private array $scalars = []; /** * Map from selected NEW objcet result alias to NEW object type * * @var array */ - private $newObjects = []; + private array $newObjects = []; - /** @var Type */ - private $indexedBy; + private Type $indexedBy; public function __construct() { diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 4ef105d9..9e5c475f 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -84,53 +84,45 @@ class QueryResultTypeWalker extends SqlWalker /** * Counter for generating unique scalar result. * - * @var int */ - private $scalarResultCounter = 1; + private int $scalarResultCounter = 1; /** * Counter for generating indexes. * - * @var int */ - private $newObjectCounter = 0; + private int $newObjectCounter = 0; /** @var Query */ - private $query; + private Query $query; - /** @var EntityManagerInterface */ - private $em; + private EntityManagerInterface $em; - /** @var PhpVersion */ - private $phpVersion; + private PhpVersion $phpVersion; /** @var DriverDetector::*|null */ private $driverType; /** @var array */ - private $driverOptions; + private array $driverOptions; /** * Map of all components/classes that appear in the DQL query. * * @var array $queryComponents */ - private $queryComponents; + private array $queryComponents; /** @var array */ - private $nullableQueryComponents; + private array $nullableQueryComponents; - /** @var QueryResultTypeBuilder */ - private $typeBuilder; + private QueryResultTypeBuilder $typeBuilder; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; - /** @var bool */ - private $hasAggregateFunction; + private bool $hasAggregateFunction; - /** @var bool */ - private $hasGroupByClause; + private bool $hasGroupByClause; /** @@ -182,7 +174,7 @@ public function __construct($query, $parserResult, array $queryComponents) 'Expected the query hint %s to contain a %s, but got a %s', self::HINT_TYPE_MAPPING, QueryResultTypeBuilder::class, - is_object($typeBuilder) ? get_class($typeBuilder) : gettype($typeBuilder) + is_object($typeBuilder) ? get_class($typeBuilder) : gettype($typeBuilder), )); } @@ -195,7 +187,7 @@ public function __construct($query, $parserResult, array $queryComponents) 'Expected the query hint %s to contain a %s, but got a %s', self::HINT_DESCRIPTOR_REGISTRY, DescriptorRegistry::class, - is_object($descriptorRegistry) ? get_class($descriptorRegistry) : gettype($descriptorRegistry) + is_object($descriptorRegistry) ? get_class($descriptorRegistry) : gettype($descriptorRegistry), )); } @@ -208,7 +200,7 @@ public function __construct($query, $parserResult, array $queryComponents) 'Expected the query hint %s to contain a %s, but got a %s', self::HINT_PHP_VERSION, PhpVersion::class, - is_object($phpVersion) ? get_class($phpVersion) : gettype($phpVersion) + is_object($phpVersion) ? get_class($phpVersion) : gettype($phpVersion), )); } @@ -221,7 +213,7 @@ public function __construct($query, $parserResult, array $queryComponents) 'Expected the query hint %s to contain a %s, but got a %s', self::HINT_DRIVER_DETECTOR, DriverDetector::class, - is_object($driverDetector) ? get_class($driverDetector) : gettype($driverDetector) + is_object($driverDetector) ? get_class($driverDetector) : gettype($driverDetector), )); } $connection = $this->em->getConnection(); @@ -818,7 +810,7 @@ private function inferSumFunction(AST\Functions\SumFunction $function): Type if ($exprTypeNoNull->isInteger()->yes()) { return TypeCombinator::union( $this->createInteger($nullable), - $this->createNumericString($nullable) + $this->createNumericString($nullable), ); } @@ -838,7 +830,7 @@ private function createFloatOrInt(bool $nullable): Type { $union = TypeCombinator::union( new FloatType(), - new IntegerType() + new IntegerType(), ); return $nullable ? TypeCombinator::addNull($union) : $union; } @@ -859,7 +851,7 @@ private function createNumericString(bool $nullable): Type { $numericString = TypeCombinator::intersect( new StringType(), - new AccessoryNumericStringType() + new AccessoryNumericStringType(), ); return $nullable ? TypeCombinator::addNull($numericString) : $numericString; @@ -1009,7 +1001,7 @@ public function walkCoalesceExpression($coalesceExpression): string if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL) { return $this->marshalType( - $this->inferCoalesceForMySql($rawTypes, $generalizedUnion) + $this->inferCoalesceForMySql($rawTypes, $generalizedUnion), ); } @@ -1091,13 +1083,13 @@ public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCase } $types[] = $this->unmarshalType( - $thenScalarExpression->dispatch($this) + $thenScalarExpression->dispatch($this), ); } if ($elseScalarExpression instanceof AST\Node) { $types[] = $this->unmarshalType( - $elseScalarExpression->dispatch($this) + $elseScalarExpression->dispatch($this), ); } @@ -1128,13 +1120,13 @@ public function walkSimpleCaseExpression($simpleCaseExpression): string } $types[] = $this->unmarshalType( - $thenScalarExpression->dispatch($this) + $thenScalarExpression->dispatch($this), ); } if ($elseScalarExpression instanceof AST\Node) { $types[] = $this->unmarshalType( - $elseScalarExpression->dispatch($this) + $elseScalarExpression->dispatch($this), ); } @@ -1225,7 +1217,7 @@ public function walkSelectExpression($selectExpression): string $dbalTypeName = DbalType::getTypeRegistry()->lookupName($expr->getReturnType()); $type = TypeCombinator::intersect( // e.g. count is typed as int, but we infer int<0, max> $type, - $this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type)) + $this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type)), ); if ($this->hasAggregateWithoutGroupBy() && !$expr instanceof AST\Functions\CountFunction) { @@ -1689,7 +1681,7 @@ public function walkSimpleArithmeticExpression($simpleArithmeticExpr): string } $types[] = $this->castStringLiteralForNumericExpression( - $this->unmarshalType($this->walkArithmeticPrimary($term)) + $this->unmarshalType($this->walkArithmeticPrimary($term)), ); } @@ -1716,7 +1708,7 @@ public function walkArithmeticTerm($term): string } $types[] = $this->castStringLiteralForNumericExpression( - $this->unmarshalType($this->walkArithmeticPrimary($factor)) + $this->unmarshalType($this->walkArithmeticPrimary($factor)), ); } @@ -1917,7 +1909,7 @@ public function walkArithmeticFactor($factor): string } elseif ($type instanceof IntegerRangeType && $factor->sign === false) { $type = IntegerRangeType::fromInterval( $type->getMax() === null ? null : $type->getMax() * -1, - $type->getMin() === null ? null : $type->getMin() * -1 + $type->getMin() === null ? null : $type->getMin() * -1, ); } elseif ($type instanceof ConstantFloatType && $factor->sign === false) { @@ -2022,7 +2014,7 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null, } else { $type = TypeCombinator::intersect(new ArrayType( $type->getIterableKeyType(), - new ObjectType($enumType) + new ObjectType($enumType), ), ...TypeUtils::getAccessoryTypes($type)); } } @@ -2058,9 +2050,7 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType } if ($enumType !== null) { - $enumTypes = array_map(static function ($enumType) { - return ConstantTypeHelper::getTypeFromValue($enumType->value); - }, $enumType::cases()); + $enumTypes = array_map(static fn ($enumType) => ConstantTypeHelper::getTypeFromValue($enumType->value), $enumType::cases()); $enumType = TypeCombinator::union(...$enumTypes); $enumType = TypeCombinator::union($enumType, $enumType->toString()); $type = TypeCombinator::intersect($enumType, $type); diff --git a/src/Type/Doctrine/Query/QueryType.php b/src/Type/Doctrine/Query/QueryType.php index b0cb7968..ead7ef2b 100644 --- a/src/Type/Doctrine/Query/QueryType.php +++ b/src/Type/Doctrine/Query/QueryType.php @@ -11,14 +11,11 @@ class QueryType extends GenericObjectType { - /** @var Type */ - private $indexType; + private Type $indexType; - /** @var Type */ - private $resultType; + private Type $resultType; - /** @var string */ - private $dql; + private string $dql; public function __construct(string $dql, ?Type $indexType = null, ?Type $resultType = null, ?Type $subtractedType = null) { diff --git a/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php index 263f2284..8389395a 100644 --- a/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php @@ -11,11 +11,9 @@ class CreateQueryBuilderDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string|null */ - private $queryBuilderClass; + private ?string $queryBuilderClass = null; - /** @var bool */ - private $fasterVersion; + private bool $fasterVersion; public function __construct( ?string $queryBuilderClass, @@ -48,7 +46,7 @@ public function getTypeFromMethodCall( } return new $class( - $this->queryBuilderClass ?? 'Doctrine\ORM\QueryBuilder' + $this->queryBuilderClass ?? 'Doctrine\ORM\QueryBuilder', ); } diff --git a/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php index d797b45f..7dd9729e 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php @@ -17,8 +17,7 @@ class BaseExpressionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ArgumentsProcessor */ - private $argumentsProcessor; + private ArgumentsProcessor $argumentsProcessor; public function __construct( ArgumentsProcessor $argumentsProcessor diff --git a/src/Type/Doctrine/QueryBuilder/Expr/ExprType.php b/src/Type/Doctrine/QueryBuilder/Expr/ExprType.php index 00610d03..30a97a59 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/ExprType.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/ExprType.php @@ -8,8 +8,7 @@ class ExprType extends ObjectType { - /** @var object */ - private $exprObject; + private object $exprObject; /** * @param object $exprObject diff --git a/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php index fae7cd28..a6a6896a 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php @@ -18,11 +18,9 @@ class ExpressionBuilderDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var ArgumentsProcessor */ - private $argumentsProcessor; + private ArgumentsProcessor $argumentsProcessor; public function __construct( ObjectMetadataResolver $objectMetadataResolver, diff --git a/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php index 1690c63c..6f9a69c8 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php @@ -18,14 +18,11 @@ class NewExprDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { - /** @var ArgumentsProcessor */ - private $argumentsProcessor; + private ArgumentsProcessor $argumentsProcessor; - /** @var string */ - private $class; + private string $class; - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; public function __construct( ArgumentsProcessor $argumentsProcessor, @@ -68,8 +65,8 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, ...$this->argumentsProcessor->processArgs( $scope, $methodReflection->getName(), - $methodCall->getArgs() - ) + $methodCall->getArgs(), + ), ); } catch (DynamicQueryBuilderArgumentException $e) { return new ObjectType($this->reflectionProvider->getClassName($className)); diff --git a/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php b/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php index 4375c17a..cddd3a93 100644 --- a/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php +++ b/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php @@ -28,21 +28,18 @@ class OtherMethodQueryBuilderParser { - /** @var bool */ - private $descendIntoOtherMethods; + private bool $descendIntoOtherMethods; - /** @var Parser */ - private $parser; + private Parser $parser; - /** @var Container */ - private $container; + private Container $container; /** * Null if the method is currently being processed * * @var array|null> */ - private $cache = []; + private array $cache = []; public function __construct(bool $descendIntoOtherMethods, Parser $parser, Container $container) { diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php index 3b7dfb19..c6f4a5a7 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php @@ -13,8 +13,7 @@ class QueryBuilderGetDqlDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string|null */ - private $queryBuilderClass; + private ?string $queryBuilderClass = null; public function __construct( ?string $queryBuilderClass @@ -41,7 +40,7 @@ public function getTypeFromMethodCall( { $type = $scope->getType(new MethodCall( new MethodCall($methodCall->var, new Identifier('getQuery')), - new Identifier('getDQL') + new Identifier('getDQL'), )); return TypeCombinator::removeNull($type); diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php index 366eaa60..b02018be 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php @@ -55,23 +55,17 @@ class QueryBuilderGetQueryDynamicReturnTypeExtension implements DynamicMethodRet 'orhaving', ]; - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var ArgumentsProcessor */ - private $argumentsProcessor; + private ArgumentsProcessor $argumentsProcessor; - /** @var string|null */ - private $queryBuilderClass; + private ?string $queryBuilderClass = null; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; - /** @var PhpVersion */ - private $phpVersion; + private PhpVersion $phpVersion; - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct( ObjectMetadataResolver $objectMetadataResolver, diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php index cb76ba29..4e9ea601 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php @@ -23,8 +23,7 @@ class QueryBuilderMethodDynamicReturnTypeExtension implements DynamicMethodRetur private const MAX_COMBINATIONS = 16; - /** @var string|null */ - private $queryBuilderClass; + private ?string $queryBuilderClass = null; public function __construct( ?string $queryBuilderClass @@ -41,7 +40,7 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { $returnType = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() + $methodReflection->getVariants(), )->getReturnType(); if ($returnType instanceof MixedType) { return false; diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderType.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderType.php index 729cda17..f707888c 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderType.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderType.php @@ -14,7 +14,7 @@ abstract class QueryBuilderType extends ObjectType { /** @var array */ - private $methodCalls = []; + private array $methodCalls = []; final public function __construct( string $className, diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php index 4b72f650..83966118 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php @@ -24,11 +24,9 @@ class QueryBuilderTypeSpecifyingExtension implements MethodTypeSpecifyingExtensi private const MAX_COMBINATIONS = 16; - /** @var string|null */ - private $queryBuilderClass; + private ?string $queryBuilderClass = null; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function __construct(?string $queryBuilderClass) { @@ -62,7 +60,7 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod $returnType = ParametersAcceptorSelector::selectFromArgs( $scope, $node->getArgs(), - $methodReflection->getVariants() + $methodReflection->getVariants(), )->getReturnType(); if ($returnType instanceof MixedType) { return new SpecifiedTypes([]); @@ -100,7 +98,7 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod $queryBuilderNode, TypeCombinator::union(...$resultTypes), TypeSpecifierContext::createTruthy(), - true + true, ); } diff --git a/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php b/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php index 2f780f22..5f308ba1 100644 --- a/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php +++ b/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php @@ -23,8 +23,7 @@ class ReturnQueryBuilderExpressionTypeResolverExtension implements ExpressionTypeResolverExtension { - /** @var OtherMethodQueryBuilderParser */ - private $otherMethodQueryBuilderParser; + private OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser; public function __construct( OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser diff --git a/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php b/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php index f1f7dd32..7c92d905 100644 --- a/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php +++ b/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php @@ -33,7 +33,7 @@ public function testForbiddenClassNameExtension(): void 20, 'This is most likely unintentional. Did you mean to type \TestPhpStanEntity?', ], - ] + ], ); } diff --git a/tests/Classes/entity-manager.php b/tests/Classes/entity-manager.php index a84bdc54..9c0301d1 100644 --- a/tests/Classes/entity-manager.php +++ b/tests/Classes/entity-manager.php @@ -19,13 +19,13 @@ $metadataDriver = new MappingDriverChain(); $metadataDriver->addDriver(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], ), 'PHPStan\\Rules\\Doctrine\\ORM\\'); if (PHP_VERSION_ID >= 80100) { $metadataDriver->addDriver( new AttributeDriver([__DIR__ . '/data-attributes']), - 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\' + 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\', ); } @@ -33,7 +33,7 @@ Type::overrideType( 'date', - DateTimeImmutableType::class + DateTimeImmutableType::class, ); return new EntityManager( @@ -41,5 +41,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/DoctrineIntegration/ODM/document-manager.php b/tests/DoctrineIntegration/ODM/document-manager.php index bfdb4141..d104e205 100644 --- a/tests/DoctrineIntegration/ODM/document-manager.php +++ b/tests/DoctrineIntegration/ODM/document-manager.php @@ -16,11 +16,11 @@ $config->setMetadataDriverImpl( new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] - ) + [__DIR__ . '/data'], + ), ); return DocumentManager::create( null, - $config + $config, ); diff --git a/tests/DoctrineIntegration/ORM/entity-manager.php b/tests/DoctrineIntegration/ORM/entity-manager.php index 23f27436..fb21532a 100644 --- a/tests/DoctrineIntegration/ORM/entity-manager.php +++ b/tests/DoctrineIntegration/ORM/entity-manager.php @@ -15,8 +15,8 @@ $config->setMetadataDriverImpl( new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] - ) + [__DIR__ . '/data'], + ), ); return new EntityManager( @@ -24,5 +24,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/Platform/Entity/PlatformEntity.php b/tests/Platform/Entity/PlatformEntity.php index 6da280e9..efc1f68d 100644 --- a/tests/Platform/Entity/PlatformEntity.php +++ b/tests/Platform/Entity/PlatformEntity.php @@ -17,90 +17,58 @@ class PlatformEntity /** * @ORM\Id * @ORM\Column(type="string",nullable=false) - * @var string */ #[ORM\Id] #[ORM\Column(type: 'string', nullable: false)] - public $id; + public string $id; /** * @ORM\ManyToOne(targetEntity=PlatformRelatedEntity::class) * @ORM\JoinColumn(name="related_entity_id", referencedColumnName="id", nullable=false) - * @var PlatformRelatedEntity */ #[ORM\ManyToOne(targetEntity: PlatformRelatedEntity::class)] #[ORM\JoinColumn(name: 'related_entity_id', referencedColumnName: 'id', nullable: false)] - public $related_entity; + public PlatformRelatedEntity $related_entity; - /** - * @ORM\Column(type="string", name="col_string", nullable=false) - * @var string - */ + /** @ORM\Column(type="string", name="col_string", nullable=false) */ #[ORM\Column(type: 'string', name: 'col_string', nullable: false)] - public $col_string; + public string $col_string; - /** - * @ORM\Column(type="string", name="col_string_nullable", nullable=true) - * @var string|null - */ + /** @ORM\Column(type="string", name="col_string_nullable", nullable=true) */ #[ORM\Column(type: 'string', name: 'col_string_nullable', nullable: true)] - public $col_string_nullable; + public ?string $col_string_nullable = null; - /** - * @ORM\Column(type="boolean", name="col_bool", nullable=false) - * @var bool - */ + /** @ORM\Column(type="boolean", name="col_bool", nullable=false) */ #[ORM\Column(type: 'boolean', name: 'col_bool', nullable: false)] - public $col_bool; + public bool $col_bool; - /** - * @ORM\Column(type="boolean", name="col_bool_nullable", nullable=true) - * @var bool|null - */ + /** @ORM\Column(type="boolean", name="col_bool_nullable", nullable=true) */ #[ORM\Column(type: 'boolean', name: 'col_bool_nullable', nullable: true)] - public $col_bool_nullable; + public ?bool $col_bool_nullable = null; - /** - * @ORM\Column(type="float", name="col_float", nullable=false) - * @var float - */ + /** @ORM\Column(type="float", name="col_float", nullable=false) */ #[ORM\Column(type: 'float', name: 'col_float', nullable: false)] - public $col_float; + public float $col_float; - /** - * @ORM\Column(type="float", name="col_float_nullable", nullable=true) - * @var float|null - */ + /** @ORM\Column(type="float", name="col_float_nullable", nullable=true) */ #[ORM\Column(type: 'float', name: 'col_float_nullable', nullable: true)] - public $col_float_nullable; + public ?float $col_float_nullable = null; - /** - * @ORM\Column(type="decimal", name="col_decimal", nullable=false, scale=1, precision=2) - * @var string - */ + /** @ORM\Column(type="decimal", name="col_decimal", nullable=false, scale=1, precision=2) */ #[ORM\Column(type: 'decimal', name: 'col_decimal', nullable: false, scale: 1, precision: 2)] - public $col_decimal; + public string $col_decimal; - /** - * @ORM\Column(type="decimal", name="col_decimal_nullable", nullable=true, scale=1, precision=2) - * @var string|null - */ + /** @ORM\Column(type="decimal", name="col_decimal_nullable", nullable=true, scale=1, precision=2) */ #[ORM\Column(type: 'decimal', name: 'col_decimal_nullable', nullable: true, scale: 1, precision: 2)] - public $col_decimal_nullable; + public ?string $col_decimal_nullable = null; - /** - * @ORM\Column(type="integer", name="col_int", nullable=false) - * @var int - */ + /** @ORM\Column(type="integer", name="col_int", nullable=false) */ #[ORM\Column(type: 'integer', name: 'col_int', nullable: false)] - public $col_int; + public int $col_int; - /** - * @ORM\Column(type="integer", name="col_int_nullable", nullable=true) - * @var int|null - */ + /** @ORM\Column(type="integer", name="col_int_nullable", nullable=true) */ #[ORM\Column(type: 'integer', name: 'col_int_nullable', nullable: true)] - public $col_int_nullable; + public ?int $col_int_nullable = null; /** * @ORM\Column(type="bigint", name="col_bigint", nullable=false) @@ -123,11 +91,8 @@ class PlatformEntity #[ORM\Column(type: 'mixed', name: 'col_mixed', nullable: false)] public $col_mixed; - /** - * @ORM\Column(type="datetime", name="col_datetime", nullable=false) - * @var DateTimeInterface - */ + /** @ORM\Column(type="datetime", name="col_datetime", nullable=false) */ #[ORM\Column(type: 'datetime', name: 'col_datetime', nullable: false)] - public $col_datetime; + public DateTimeInterface $col_datetime; } diff --git a/tests/Platform/Entity/PlatformRelatedEntity.php b/tests/Platform/Entity/PlatformRelatedEntity.php index 86c4b00a..fec1a0a5 100644 --- a/tests/Platform/Entity/PlatformRelatedEntity.php +++ b/tests/Platform/Entity/PlatformRelatedEntity.php @@ -16,10 +16,9 @@ class PlatformRelatedEntity /** * @ORM\Id * @ORM\Column(type="integer", nullable=false) - * @var int */ #[ORM\Id] #[ORM\Column(type: 'integer', nullable: false)] - public $id; + public int $id; } diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 04fdcba8..97df9e20 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -134,7 +134,7 @@ public function testPdoMysqlDefault( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -174,7 +174,7 @@ public function testPdoMysqlStringify( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -214,7 +214,7 @@ public function testPdoMysqlNoEmulate( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -254,7 +254,7 @@ public function testPdoMysqlStringifyNoEmulate( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -294,7 +294,7 @@ public function testPdoMysqliDefault( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -334,7 +334,7 @@ public function testPdoSqliteDefault( PHP_VERSION_ID, $sqliteExpectedType, $sqliteExpectedResult, - $stringify + $stringify, ); } @@ -374,7 +374,7 @@ public function testPdoSqliteStringify( PHP_VERSION_ID, $sqliteExpectedType, $sqliteExpectedResult, - $stringify + $stringify, ); } @@ -414,7 +414,7 @@ public function testPdoSqlite3( PHP_VERSION_ID, $sqliteExpectedType, $sqliteExpectedResult, - $stringify + $stringify, ); } @@ -454,7 +454,7 @@ public function testPdoPgsqlDefault( PHP_VERSION_ID, $pdoPgsqlExpectedType, $pdoPgsqlExpectedResult, - $stringify + $stringify, ); } @@ -494,7 +494,7 @@ public function testPdoPgsqlStringify( PHP_VERSION_ID, $pdoPgsqlExpectedType, $pdoPgsqlExpectedResult, - $stringify + $stringify, ); } @@ -534,7 +534,7 @@ public function testPgsql( PHP_VERSION_ID, $pgsqlExpectedType, $pgsqlExpectedResult, - $stringify + $stringify, ); } @@ -574,7 +574,7 @@ public function testUnsupportedDriver( PHP_VERSION_ID, $mssqlExpectedType, $mssqlExpectedResult, - $stringify + $stringify, ); } @@ -615,7 +615,7 @@ public function testUnknownDriver( $this->determineTypeForUnknownDriverUnknownSetup($mysqlExpectedType, $stringify), $mysqlExpectedResult, $stringify, - true + true, ); } @@ -656,7 +656,7 @@ public function testUnknownDriverStringify( $this->determineTypeForUnknownDriverUnknownSetup($mysqlExpectedType, $stringify), $mysqlExpectedResult, $stringify, - true + true, ); } @@ -4469,7 +4469,7 @@ private function performDriverTest( $dql, $sql, $realResultType->describe(VerbosityLevel::precise()), - $inferredType->describe(VerbosityLevel::precise()) + $inferredType->describe(VerbosityLevel::precise()), )); } @@ -4575,7 +4575,7 @@ private function getInferredType(Query $query): Type $typeBuilder, self::getContainer()->getByType(DescriptorRegistry::class), $phpVersion, - new DriverDetector() + new DriverDetector(), ); return $typeBuilder->getResultType(); @@ -4617,8 +4617,8 @@ private function assertRealResultMatchesExpected( $dataset, $dql, $humanReadablePhpVersion, - $realFirstResult - ) + $realFirstResult, + ), ); } @@ -4634,8 +4634,8 @@ private function assertRealResultMatchesExpected( $sql, $humanReadablePhpVersion, $realFirstResult, - $expectedFirstResultExported - ) + $expectedFirstResultExported, + ), ); } @@ -4669,8 +4669,8 @@ private function assertRealResultMatchesInferred( $this->getHumanReadablePhpVersion($phpVersion), $realFirstResult, $inferredType->describe(VerbosityLevel::precise()), - $realType->describe(VerbosityLevel::precise()) - ) + $realType->describe(VerbosityLevel::precise()), + ), ); } @@ -4707,8 +4707,8 @@ private function assertInferredResultMatchesExpected( $this->getHumanReadablePhpVersion($phpVersion), $realFirstResult, $inferredFirstItemType->describe(VerbosityLevel::precise()), - $expectedFirstItemType->describe(VerbosityLevel::precise()) - ) + $expectedFirstItemType->describe(VerbosityLevel::precise()), + ), ); } diff --git a/tests/Reflection/Doctrine/DoctrineSelectableClassReflectionExtensionTest.php b/tests/Reflection/Doctrine/DoctrineSelectableClassReflectionExtensionTest.php index 1debe15e..61c90e4f 100644 --- a/tests/Reflection/Doctrine/DoctrineSelectableClassReflectionExtensionTest.php +++ b/tests/Reflection/Doctrine/DoctrineSelectableClassReflectionExtensionTest.php @@ -9,11 +9,9 @@ final class DoctrineSelectableClassReflectionExtensionTest extends PHPStanTestCase { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var DoctrineSelectableClassReflectionExtension */ - private $extension; + private DoctrineSelectableClassReflectionExtension $extension; protected function setUp(): void { diff --git a/tests/Rules/DeadCode/entity-manager.php b/tests/Rules/DeadCode/entity-manager.php index bc0d5ccb..30eeec97 100644 --- a/tests/Rules/DeadCode/entity-manager.php +++ b/tests/Rules/DeadCode/entity-manager.php @@ -17,12 +17,12 @@ $metadataDriver = new MappingDriverChain(); $metadataDriver->addDriver(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], ), 'PHPStan\\Rules\\Doctrine\\ORM\\'); if (PHP_VERSION_ID >= 80100) { $metadataDriver->addDriver( new AttributeDriver([__DIR__ . '/data']), - 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\' + 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\', ); } @@ -33,5 +33,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index c7de7957..091ef9c3 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -35,11 +35,9 @@ class EntityColumnRuleTest extends RuleTestCase { - /** @var bool */ - private $allowNullablePropertyForRequiredField; + private bool $allowNullablePropertyForRequiredField; - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; protected function getRule(): Rule { @@ -85,7 +83,7 @@ protected function getRule(): Rule $this->createReflectionProvider(), true, $this->allowNullablePropertyForRequiredField, - true + true, ); } diff --git a/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php b/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php index 7647704b..a94fdfde 100644 --- a/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php @@ -13,13 +13,12 @@ class EntityConstructorNotFinalRuleTest extends RuleTestCase { - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; protected function getRule(): Rule { return new EntityConstructorNotFinalRule( - new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp') + new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp'), ); } diff --git a/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php b/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php index 26a4e744..65b8f209 100644 --- a/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php @@ -16,7 +16,7 @@ class EntityMappingExceptionRuleTest extends RuleTestCase protected function getRule(): Rule { return new EntityMappingExceptionRule( - new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp') + new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp'), ); } diff --git a/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php b/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php index 9f1494a7..d3f05395 100644 --- a/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php @@ -13,13 +13,12 @@ class EntityNotFinalRuleTest extends RuleTestCase { - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; protected function getRule(): Rule { return new EntityNotFinalRule( - new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp') + new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp'), ); } diff --git a/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php b/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php index cd83bebe..5222b65f 100644 --- a/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php @@ -14,18 +14,16 @@ class EntityRelationRuleTest extends RuleTestCase { - /** @var bool */ - private $allowNullablePropertyForRequiredField; + private bool $allowNullablePropertyForRequiredField; - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; protected function getRule(): Rule { return new EntityRelationRule( new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp'), $this->allowNullablePropertyForRequiredField, - true + true, ); } diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php index adad87ee..49cec42d 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule { return new QueryBuilderDqlRule( new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp'), - true + true, ); } diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php index ec3dafb7..bb7f6b87 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule { return new QueryBuilderDqlRule( new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp'), - true + true, ); } diff --git a/tests/Rules/Doctrine/ORM/entity-manager.php b/tests/Rules/Doctrine/ORM/entity-manager.php index 9181f8b8..500a2029 100644 --- a/tests/Rules/Doctrine/ORM/entity-manager.php +++ b/tests/Rules/Doctrine/ORM/entity-manager.php @@ -19,13 +19,13 @@ $metadataDriver = new MappingDriverChain(); $metadataDriver->addDriver(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], ), 'PHPStan\\Rules\\Doctrine\\ORM\\'); if (PHP_VERSION_ID >= 80100) { $metadataDriver->addDriver( new AttributeDriver([__DIR__ . '/data-attributes']), - 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\' + 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\', ); } @@ -33,7 +33,7 @@ Type::overrideType( 'date', - DateTimeImmutableType::class + DateTimeImmutableType::class, ); return new EntityManager( @@ -41,5 +41,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/Rules/Properties/entity-manager.php b/tests/Rules/Properties/entity-manager.php index 99f7a07e..f36730f8 100644 --- a/tests/Rules/Properties/entity-manager.php +++ b/tests/Rules/Properties/entity-manager.php @@ -17,13 +17,13 @@ $metadataDriver = new MappingDriverChain(); $metadataDriver->addDriver(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], ), 'PHPStan\\Rules\\Doctrine\\ORM\\'); if (PHP_VERSION_ID >= 80100) { $metadataDriver->addDriver( new AttributeDriver([__DIR__ . '/data']), - 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\' + 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\', ); } @@ -34,5 +34,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/Type/Doctrine/DBAL/mysqli.php b/tests/Type/Doctrine/DBAL/mysqli.php index 2bc11294..ce3859e5 100644 --- a/tests/Type/Doctrine/DBAL/mysqli.php +++ b/tests/Type/Doctrine/DBAL/mysqli.php @@ -13,7 +13,7 @@ $config->setMetadataCache(new ArrayCachePool()); $config->setMetadataDriverImpl(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], )); return new EntityManager( @@ -21,5 +21,5 @@ 'driver' => 'mysqli', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/Type/Doctrine/DBAL/pdo.php b/tests/Type/Doctrine/DBAL/pdo.php index c7e48751..7b6b0da3 100644 --- a/tests/Type/Doctrine/DBAL/pdo.php +++ b/tests/Type/Doctrine/DBAL/pdo.php @@ -13,7 +13,7 @@ $config->setMetadataCache(new ArrayCachePool()); $config->setMetadataDriverImpl(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], )); return new EntityManager( @@ -21,5 +21,5 @@ 'driver' => 'pdo_pgsql', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/Type/Doctrine/DoctrineSelectableDynamicReturnTypeExtensionTest.php b/tests/Type/Doctrine/DoctrineSelectableDynamicReturnTypeExtensionTest.php index a34c7b22..01b66d7b 100644 --- a/tests/Type/Doctrine/DoctrineSelectableDynamicReturnTypeExtensionTest.php +++ b/tests/Type/Doctrine/DoctrineSelectableDynamicReturnTypeExtensionTest.php @@ -15,8 +15,7 @@ final class DoctrineSelectableDynamicReturnTypeExtensionTest extends TestCase { - /** @var DoctrineSelectableDynamicReturnTypeExtension */ - private $extension; + private DoctrineSelectableDynamicReturnTypeExtension $extension; protected function setUp(): void { @@ -52,10 +51,8 @@ public function testGetTypeFromMethodCall(): void $scope = $this->createMock(Scope::class); $scope->method('getType')->will( self::returnCallback( - static function (): Type { - return new ObjectType(Collection::class); - } - ) + static fn (): Type => new ObjectType(Collection::class), + ), ); $var = $this->createMock(Expr::class); diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index b9d02c85..52e08911 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -79,7 +79,7 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ $typeBuilder, self::getContainer()->getByType(DescriptorRegistry::class), self::getContainer()->getByType(PhpVersion::class), - self::getContainer()->getByType(DriverDetector::class) + self::getContainer()->getByType(DriverDetector::class), ); $resolver = self::getContainer()->getByType(HydrationModeReturnTypeResolver::class); @@ -89,12 +89,12 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ new ConstantIntegerType($this->getRealHydrationMode($methodName, $hydrationMode)), $typeBuilder->getIndexType(), $typeBuilder->getResultType(), - $entityManager + $entityManager, ) ?? new MixedType(); self::assertSame( $expectedType->describe(VerbosityLevel::precise()), - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ); $query = $entityManager->createQuery($dql); @@ -106,8 +106,8 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ sprintf( "The inferred type\n%s\nshould accept actual type\n%s", $type->describe(VerbosityLevel::precise()), - $resultType->describe(VerbosityLevel::precise()) - ) + $resultType->describe(VerbosityLevel::precise()), + ), ); } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index fd6fee74..d7d42b14 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -61,11 +61,9 @@ final class QueryResultTypeWalkerTest extends PHPStanTestCase { - /** @var EntityManagerInterface */ - private static $em; + private static EntityManagerInterface $em; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; public static function getAdditionalConfigFiles(): array { @@ -222,14 +220,14 @@ public function test(Type $expectedType, string $dql, ?string $expectedException $typeBuilder, $this->descriptorRegistry, self::getContainer()->getByType(PhpVersion::class), - self::getContainer()->getByType(DriverDetector::class) + self::getContainer()->getByType(DriverDetector::class), ); $type = $typeBuilder->getResultType(); self::assertSame( $expectedType->describe(VerbosityLevel::precise()), - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ); // Double-check our expectations @@ -246,8 +244,8 @@ public function test(Type $expectedType, string $dql, ?string $expectedException sprintf( "The inferred type\n%s\nshould accept actual type\n%s", $type->describe(VerbosityLevel::precise()), - $rowType->describe(VerbosityLevel::precise()) - ) + $rowType->describe(VerbosityLevel::precise()), + ), ); } } @@ -293,7 +291,7 @@ public function getTestData(): iterable yield 'arbitrary left join, selected' => [ TypeCombinator::union( new ObjectType(Many::class), - TypeCombinator::addNull(new ObjectType(One::class)) + TypeCombinator::addNull(new ObjectType(One::class)), ), ' SELECT m, o @@ -306,7 +304,7 @@ public function getTestData(): iterable yield 'arbitrary inner join, selected' => [ TypeCombinator::union( new ObjectType(Many::class), - new ObjectType(One::class) + new ObjectType(One::class), ), ' SELECT m, o @@ -323,7 +321,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantIntegerType(0), TypeCombinator::addNull(new ObjectType(One::class))], - ]) + ]), ), ' SELECT m AS many, o @@ -340,7 +338,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantStringType('one'), TypeCombinator::addNull(new ObjectType(One::class))], - ]) + ]), ), ' SELECT m AS many, o AS one @@ -357,7 +355,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantStringType('one'), new ObjectType(One::class)], - ]) + ]), ), ' SELECT m AS many, o AS one @@ -378,7 +376,7 @@ public function getTestData(): iterable [new ConstantIntegerType(0), new ObjectType(One::class)], [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], [new ConstantStringType('intColumn'), new IntegerType()], - ]) + ]), ), ' SELECT m, o, m.id, o.intColumn @@ -400,7 +398,7 @@ public function getTestData(): iterable [new ConstantIntegerType(0), new ObjectType(Many::class)], [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], [new ConstantStringType('intColumn'), new IntegerType()], - ]) + ]), ), ' SELECT o, m2, m, m.id, o.intColumn @@ -421,7 +419,7 @@ public function getTestData(): iterable [new ConstantStringType('one'), new ObjectType(One::class)], [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], [new ConstantStringType('intColumn'), new IntegerType()], - ]) + ]), ), ' SELECT m AS many, o AS one, m.id, o.intColumn @@ -674,42 +672,42 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( $this->intOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(2), TypeCombinator::union( $this->intOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(3), TypeCombinator::union( $this->intOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(4), TypeCombinator::union( $this->intOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(5), TypeCombinator::union( $this->floatOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(6), TypeCombinator::union( $this->floatOrStringified(), - new NullType() + new NullType(), ), ], [ @@ -754,7 +752,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), - new NullType() + new NullType(), ), ], ]), @@ -770,14 +768,14 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - $this->intOrStringified() + $this->intOrStringified(), ), ], [ new ConstantIntegerType(2), TypeCombinator::union( new StringType(), - new NullType() + new NullType(), ), ], [ @@ -790,7 +788,7 @@ public function getTestData(): iterable ? $this->numericString() : TypeCombinator::union( new IntegerType(), - new FloatType() + new FloatType(), ), ], ]), @@ -809,7 +807,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0) + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), ), ], ]), @@ -829,7 +827,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0) + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), ), ], ]), @@ -849,7 +847,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), - $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1) + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), ), ], ]), @@ -868,7 +866,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), - $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1) + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), ), ], ]), @@ -1083,7 +1081,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), new ObjectType(OneId::class), ], - ]) + ]), ), ' SELECT NEW QueryResult\Entities\ManyId(m.id), From 731ac4cdbebc7a08c7ca87f972f9b6c568412f16 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 14:55:55 +0200 Subject: [PATCH 085/160] Remove obsolete skips --- .../UnusedPrivatePropertyRuleTest.php | 4 -- .../ORM/QueryBuilderDqlRuleSlowTest.php | 4 -- .../Doctrine/ORM/QueryBuilderDqlRuleTest.php | 5 --- ...ingGedmoByPhpDocPropertyAssignRuleTest.php | 5 --- ...ReadOnlyByPhpDocPropertyAssignRuleTest.php | 5 --- .../Query/QueryResultTypeWalkerTest.php | 44 +++++++++---------- 6 files changed, 21 insertions(+), 46 deletions(-) diff --git a/tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php b/tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php index 8c582618..e7a88b28 100644 --- a/tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -27,10 +27,6 @@ public static function getAdditionalConfigFiles(): array public function testRule(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/unused-private-property.php'], [ [ 'Property PHPStan\Rules\Doctrine\ORM\UnusedPrivateProperty\EntityWithAGeneratedId::$unused is never written, only read.', diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php index 49cec42d..2c4e03f7 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\ObjectMetadataResolver; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -23,9 +22,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 70300) { - self::markTestSkipped('For some reason PHP 7.2 cannot recover from Trying to get property value of non-object'); - } $this->analyse([__DIR__ . '/data/query-builder-dql.php'], [ [ "QueryBuilder: [Syntax Error] line 0, col 66: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1)", diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php index bb7f6b87..65fd38d6 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\ObjectMetadataResolver; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -23,10 +22,6 @@ protected function getRule(): Rule public function testRule(): void { - if (PHP_VERSION_ID < 70300) { - self::markTestSkipped('For some reason PHP 7.2 cannot recover from Trying to get property value of non-object'); - } - $this->analyse([__DIR__ . '/data/query-builder-dql.php'], [ [ "QueryBuilder: [Syntax Error] line 0, col 66: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1)", diff --git a/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php b/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php index 683ddff3..7b66451e 100644 --- a/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php +++ b/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php @@ -7,7 +7,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\ObjectMetadataResolver; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -34,10 +33,6 @@ public static function getAdditionalConfigFiles(): array public function testRule(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/gedmo-property-assign-phpdoc.php'], [ // No errors expected ]); diff --git a/tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php index 2f42a9a4..8a72494f 100644 --- a/tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -24,10 +23,6 @@ public static function getAdditionalConfigFiles(): array public function testRule(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/missing-readonly-property-assign-phpdoc.php'], [ [ 'Class MissingReadOnlyPropertyAssignPhpDoc\EntityWithAGeneratedId has an uninitialized @readonly property $unassigned. Assign it in the constructor.', diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index d7d42b14..9b17c3c5 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -1533,35 +1533,33 @@ private function yieldConditionalDataset(): iterable ]; } - if (PHP_VERSION_ID >= 70400) { - yield 'locate function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), $this->uintOrStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintOrStringified())], - [new ConstantIntegerType(4), $this->uintOrStringified()], - ]), - ' + yield 'locate function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), $this->uintOrStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintOrStringified())], + [new ConstantIntegerType(4), $this->uintOrStringified()], + ]), + ' SELECT LOCATE(m.stringColumn, m.stringColumn, 0), LOCATE(m.stringNullColumn, m.stringColumn, 0), LOCATE(m.stringColumn, m.stringNullColumn, 0), LOCATE(\'f\', \'foo\', 0) FROM QueryResult\Entities\Many m ', - null, - InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=3.4') - ? null - : ( - PHP_VERSION_ID >= 80100 - ? 'strpos(): Passing null to parameter #2 ($needle) of type string is deprecated' - : ( - PHP_VERSION_ID < 80000 - ? 'strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior' - : null - ) - ), - ]; - } + null, + InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=3.4') + ? null + : ( + PHP_VERSION_ID >= 80100 + ? 'strpos(): Passing null to parameter #2 ($needle) of type string is deprecated' + : ( + PHP_VERSION_ID < 80000 + ? 'strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior' + : null + ) + ), + ]; $ormVersion = InstalledVersions::getVersion('doctrine/orm'); $hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0; From 9ddc146f78776d5eff79a354bebb0838ac593b86 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:00:32 +0200 Subject: [PATCH 086/160] Put doctrine/lexer:^2.0 back --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1bad25c5..58c0ac4e 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "doctrine/collections": "^2.1", "doctrine/common": "^2.7 || ^3.0", "doctrine/dbal": "^3.3.8", - "doctrine/lexer": "^3.0", + "doctrine/lexer": "^2.0 || ^3.0", "doctrine/mongodb-odm": "^2.4.3", "doctrine/orm": "^2.16.0", "doctrine/persistence": "^2.2.1 || ^3.2", From 22a33bc188f1228252597acb794e4d629daa3352 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:00:53 +0200 Subject: [PATCH 087/160] Do not run PHPStan on 7.3 --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3230e295..ad3633e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.3" - "7.4" - "8.0" - "8.1" From da4e194fcdba5b141f095d0509d2e35d632fc12a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:02:17 +0200 Subject: [PATCH 088/160] Put doctrine/collections:^1.6 back --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 58c0ac4e..fc15096e 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "composer/semver": "^3.3.2", "cweagans/composer-patches": "^1.7.3", "doctrine/annotations": "^2.0", - "doctrine/collections": "^2.1", + "doctrine/collections": "^1.6 || ^2.1", "doctrine/common": "^2.7 || ^3.0", "doctrine/dbal": "^3.3.8", "doctrine/lexer": "^2.0 || ^3.0", From d462eb98c6d2b7c2311ac8a4a9ad33f20e99a623 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:02:54 +0200 Subject: [PATCH 089/160] Fix CS --- tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 9b17c3c5..bceab2fc 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -1554,10 +1554,10 @@ private function yieldConditionalDataset(): iterable PHP_VERSION_ID >= 80100 ? 'strpos(): Passing null to parameter #2 ($needle) of type string is deprecated' : ( - PHP_VERSION_ID < 80000 - ? 'strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior' - : null - ) + PHP_VERSION_ID < 80000 + ? 'strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior' + : null + ) ), ]; From 0f98bd177aac3eaf529d1aa9dee6456545c77016 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:08:34 +0200 Subject: [PATCH 090/160] Do not expect deprecation in test --- .../Doctrine/Query/QueryResultTypeWalkerTest.php | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index bceab2fc..b5ac7ee8 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -199,7 +199,7 @@ public function setUp(): void } /** @dataProvider getTestData */ - public function test(Type $expectedType, string $dql, ?string $expectedExceptionMessage = null, ?string $expectedDeprecationMessage = null): void + public function test(Type $expectedType, string $dql, ?string $expectedExceptionMessage = null): void { $em = self::$em; @@ -210,9 +210,6 @@ public function test(Type $expectedType, string $dql, ?string $expectedException if ($expectedExceptionMessage !== null) { $this->expectException(Throwable::class); $this->expectExceptionMessage($expectedExceptionMessage); - } elseif ($expectedDeprecationMessage !== null) { - $this->expectDeprecation(); - $this->expectDeprecationMessage($expectedDeprecationMessage); } QueryResultTypeWalker::walk( @@ -1548,17 +1545,6 @@ private function yieldConditionalDataset(): iterable FROM QueryResult\Entities\Many m ', null, - InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=3.4') - ? null - : ( - PHP_VERSION_ID >= 80100 - ? 'strpos(): Passing null to parameter #2 ($needle) of type string is deprecated' - : ( - PHP_VERSION_ID < 80000 - ? 'strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior' - : null - ) - ), ]; $ormVersion = InstalledVersions::getVersion('doctrine/orm'); From 039d325e5aed2b047b6d8a5d143ec6753defceed Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:18:12 +0200 Subject: [PATCH 091/160] Remove test about deprecated LOCATE() function in DBAL SQLite platform See https://github.com/doctrine/dbal/pull/5749 --- .../Query/QueryResultTypeWalkerTest.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index b5ac7ee8..33fa5bbd 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -1530,23 +1530,6 @@ private function yieldConditionalDataset(): iterable ]; } - yield 'locate function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), $this->uintOrStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintOrStringified())], - [new ConstantIntegerType(4), $this->uintOrStringified()], - ]), - ' - SELECT LOCATE(m.stringColumn, m.stringColumn, 0), - LOCATE(m.stringNullColumn, m.stringColumn, 0), - LOCATE(m.stringColumn, m.stringNullColumn, 0), - LOCATE(\'f\', \'foo\', 0) - FROM QueryResult\Entities\Many m - ', - null, - ]; - $ormVersion = InstalledVersions::getVersion('doctrine/orm'); $hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0; From a103cfebd51a25508bbe667793781775209486d1 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 9 Sep 2024 13:50:30 +0200 Subject: [PATCH 092/160] QueryResultTypeWalker: support pdo_pgsql float fetches on PHP 8.4 --- src/Type/Doctrine/Descriptors/FloatType.php | 8 +- .../Doctrine/Query/QueryResultTypeWalker.php | 38 +-- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 266 ++++++++++-------- 3 files changed, 162 insertions(+), 150 deletions(-) diff --git a/src/Type/Doctrine/Descriptors/FloatType.php b/src/Type/Doctrine/Descriptors/FloatType.php index 2518e72d..e475c0a2 100644 --- a/src/Type/Doctrine/Descriptors/FloatType.php +++ b/src/Type/Doctrine/Descriptors/FloatType.php @@ -53,18 +53,12 @@ public function getDatabaseInternalTypeForDriver(Connection $connection): Type { $driverType = $this->driverDetector->detect($connection); - if ($driverType === DriverDetector::PDO_PGSQL) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); - } - if (in_array($driverType, [ DriverDetector::SQLITE3, DriverDetector::PDO_SQLITE, DriverDetector::MYSQLI, DriverDetector::PDO_MYSQL, + DriverDetector::PDO_PGSQL, DriverDetector::PGSQL, ], true)) { return new \PHPStan\Type\FloatType(); diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 4ef105d9..54d836c6 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -471,10 +471,6 @@ public function walkFunction($function): string } if ($this->containsOnlyNumericTypes($exprTypeNoNull)) { - if ($this->driverType === DriverDetector::PDO_PGSQL) { - return $this->marshalType($this->createNumericString($nullable)); - } - return $this->marshalType($exprType); // retains underlying type } @@ -627,13 +623,7 @@ public function walkFunction($function): string $type = TypeCombinator::addNull($type); } - } elseif ($this->driverType === DriverDetector::PDO_PGSQL) { - $type = new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); - - } elseif ($this->driverType === DriverDetector::PGSQL) { + } elseif ($this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { $castedExprType = $this->castStringLiteralForNumericExpression($exprTypeNoNull); if ($castedExprType->isInteger()->yes() || $castedExprType->isFloat()->yes()) { @@ -1771,12 +1761,6 @@ private function inferPlusMinusTimesType(array $termTypes): Type return $this->createInteger($nullable); } - if ($this->driverType === DriverDetector::PDO_PGSQL) { - if ($this->containsOnlyNumericTypes($unionWithoutNull)) { - return $this->createNumericString($nullable); - } - } - if ($this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { if (!$this->containsOnlyNumericTypes(...$typesNoNull)) { return new MixedType(); @@ -1791,7 +1775,7 @@ private function inferPlusMinusTimesType(array $termTypes): Type return $this->createFloatOrInt($nullable); } - if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL) { + if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType()])) { return $this->createFloat($nullable); } @@ -1857,12 +1841,6 @@ private function inferDivisionType(array $termTypes): Type return new MixedType(); } - if ($this->driverType === DriverDetector::PDO_PGSQL) { - if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType(), $this->createNumericString(false)])) { - return $this->createNumericString($nullable); - } - } - if ($this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { if (!$this->containsOnlyNumericTypes(...$typesNoNull)) { return new MixedType(); @@ -1877,7 +1855,7 @@ private function inferDivisionType(array $termTypes): Type return $this->createFloatOrInt($nullable); } - if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL) { + if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType()])) { return $this->createFloat($nullable); } @@ -2100,6 +2078,9 @@ private function hasAggregateWithoutGroupBy(): bool * - pdo_sqlite: https://github.com/php/php-src/commit/438b025a28cda2935613af412fc13702883dd3a2 * - pdo_pgsql: https://github.com/php/php-src/commit/737195c3ae6ac53b9501cfc39cc80fd462909c82 * + * Notable 8.4 changes: + * - pdo_pgsql: https://github.com/php/php-src/commit/6d10a6989897e9089d62edf939344437128e93ad + * * @param IntegerType|FloatType|BooleanType $type */ private function shouldStringifyExpressions(Type $type): TrinaryLogic @@ -2144,7 +2125,14 @@ private function shouldStringifyExpressions(Type $type): TrinaryLogic } return TrinaryLogic::createNo(); + } + if ($type->isFloat()->yes()) { + if ($this->phpVersion->getVersionId() >= 80400) { + return TrinaryLogic::createFromBoolean($stringifyFetches); + } + + return TrinaryLogic::createYes(); } return TrinaryLogic::createFromBoolean($stringifyFetches); diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 04fdcba8..3faae5a6 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -71,6 +71,7 @@ final class QueryResultTypeWalkerFetchTypeMatrixTest extends PHPStanTestCase private const STRINGIFY_NONE = 'none'; private const STRINGIFY_DEFAULT = 'default'; private const STRINGIFY_PG_BOOL = 'pg_bool'; + private const STRINGIFY_PG_FLOAT = 'pg_float'; private const CONFIG_DEFAULT = 'default'; private const CONFIG_STRINGIFY = 'pdo_stringify'; @@ -974,15 +975,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_int + t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 9.125, 'sqliteResult' => 9.125, - 'pdoPgsqlResult' => '9.125', + 'pdoPgsqlResult' => 9.125, 'pgsqlResult' => 9.125, 'mssqlResult' => 9.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int + t.col_mixed' => [ @@ -1006,15 +1007,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_bigint + t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 2147483648.125, 'sqliteResult' => 2147483648.125, - 'pdoPgsqlResult' => '2147483648.125', + 'pdoPgsqlResult' => 2147483648.125, 'pgsqlResult' => 2147483648.125, 'mssqlResult' => 2147483648.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_bigint + t.col_float (int data)' => [ @@ -1022,15 +1023,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_bigint + t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 2.0, 'sqliteResult' => 2.0, - 'pdoPgsqlResult' => '2', + 'pdoPgsqlResult' => 2.0, 'pgsqlResult' => 2.0, 'mssqlResult' => 2.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_float + t.col_float' => [ @@ -1038,15 +1039,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float + t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.25, 'sqliteResult' => 0.25, - 'pdoPgsqlResult' => '0.25', + 'pdoPgsqlResult' => 0.25, 'pgsqlResult' => 0.25, 'mssqlResult' => 0.25, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int + t.col_decimal' => [ @@ -1086,15 +1087,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float + t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.225, 'sqliteResult' => 0.225, - 'pdoPgsqlResult' => '0.225', + 'pdoPgsqlResult' => 0.225, 'pgsqlResult' => 0.225, 'mssqlResult' => 0.225, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_float + t.col_decimal (int data)' => [ @@ -1102,15 +1103,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float + t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 2.0, 'sqliteResult' => 2.0, - 'pdoPgsqlResult' => '2', + 'pdoPgsqlResult' => 2.0, 'pgsqlResult' => 2.0, 'mssqlResult' => 2.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal + t.col_decimal (int data)' => [ @@ -1134,15 +1135,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_int + t.col_float + t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 9.225, 'sqliteResult' => 9.225, - 'pdoPgsqlResult' => '9.225', + 'pdoPgsqlResult' => 9.225, 'pgsqlResult' => 9.225, 'mssqlResult' => 9.225, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal + t.col_decimal' => [ @@ -1310,15 +1311,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_int / t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 72.0, 'sqliteResult' => 72.0, - 'pdoPgsqlResult' => '72', + 'pdoPgsqlResult' => 72.0, 'pgsqlResult' => 72.0, 'mssqlResult' => 72.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int / t.col_float / t.col_decimal' => [ @@ -1326,15 +1327,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_int / t.col_float / t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 720.0, 'sqliteResult' => 720.0, - 'pdoPgsqlResult' => '720', + 'pdoPgsqlResult' => 720.0, 'pgsqlResult' => 720.0, 'mssqlResult' => 720.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_bigint / t.col_float' => [ @@ -1342,15 +1343,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_bigint / t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 17179869184.0, 'sqliteResult' => 17179869184.0, - 'pdoPgsqlResult' => '17179869184', + 'pdoPgsqlResult' => 17179869184.0, 'pgsqlResult' => 17179869184.0, 'mssqlResult' => 17179869184.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_float / t.col_float' => [ @@ -1358,15 +1359,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float / t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int / t.col_decimal' => [ @@ -1406,15 +1407,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float / t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.25, 'sqliteResult' => 1.25, - 'pdoPgsqlResult' => '1.25', + 'pdoPgsqlResult' => 1.25, 'pgsqlResult' => 1.25, 'mssqlResult' => 1.25, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal / t.col_decimal' => [ @@ -1961,20 +1962,36 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_DEFAULT, ]; + yield 'COALESCE(t.col_float, t.col_float)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_float, t.col_float) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::float(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => 0.125, + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_PG_FLOAT, + ]; + yield 'COALESCE(t.col_float, t.col_float) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT COALESCE(t.col_float, t.col_float) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal' => [ @@ -2046,15 +2063,15 @@ public static function provideCases(): iterable 'select' => 'SELECT AVG(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_float) + no data' => [ @@ -2062,7 +2079,7 @@ public static function provideCases(): iterable 'select' => 'SELECT AVG(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -2070,7 +2087,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_float) + GROUP BY' => [ @@ -2078,15 +2095,15 @@ public static function provideCases(): iterable 'select' => 'SELECT AVG(t.col_float) FROM %s t GROUP BY t.col_int', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_float_nullable) + GROUP BY' => [ @@ -2094,7 +2111,7 @@ public static function provideCases(): iterable 'select' => 'SELECT AVG(t.col_float_nullable) FROM %s t GROUP BY t.col_int', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -2102,7 +2119,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_decimal)' => [ @@ -2350,15 +2367,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SUM(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SUM(t.col_float) + no data' => [ @@ -2366,7 +2383,7 @@ public static function provideCases(): iterable 'select' => 'SELECT SUM(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -2374,7 +2391,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SUM(t.col_float) + GROUP BY' => [ @@ -2382,15 +2399,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SUM(t.col_float) FROM %s t GROUP BY t.col_int', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield '1 + -(CASE WHEN MIN(t.col_float) = 0 THEN SUM(t.col_float) ELSE 0 END)' => [ // agg function (causing null) deeply inside AST @@ -2398,15 +2415,15 @@ public static function provideCases(): iterable 'select' => 'SELECT 1 + -(CASE WHEN MIN(t.col_float) = 0 THEN SUM(t.col_float) ELSE 0 END) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SUM(t.col_decimal)' => [ @@ -2686,15 +2703,15 @@ public static function provideCases(): iterable 'select' => 'SELECT MAX(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'MAX(t.col_float) + no data' => [ @@ -2702,7 +2719,7 @@ public static function provideCases(): iterable 'select' => 'SELECT MAX(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -2710,7 +2727,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'MAX(t.col_float) + GROUP BY' => [ @@ -2718,12 +2735,12 @@ public static function provideCases(): iterable 'select' => 'SELECT MAX(t.col_float) FROM %s t GROUP BY t.col_int', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, 'stringify' => self::STRINGIFY_DEFAULT, @@ -2958,15 +2975,15 @@ public static function provideCases(): iterable 'select' => 'SELECT ABS(t.col_float) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'ABS(t.col_decimal)' => [ @@ -3166,15 +3183,15 @@ public static function provideCases(): iterable 'select' => "SELECT ABS('1.0') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "ABS('1')" => [ @@ -3182,15 +3199,15 @@ public static function provideCases(): iterable 'select' => "SELECT ABS('1') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'ABS(t.col_bigint)' => [ @@ -3614,15 +3631,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(t.col_decimal)' => [ @@ -3646,15 +3663,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(t.col_int) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 3.0, 'sqliteResult' => 3.0, - 'pdoPgsqlResult' => '3', + 'pdoPgsqlResult' => 3.0, 'pgsqlResult' => 3.0, 'mssqlResult' => 3.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(t.col_mixed)' => [ @@ -3667,10 +3684,10 @@ public static function provideCases(): iterable 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(t.col_int_nullable)' => [ @@ -3678,7 +3695,7 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(t.col_int_nullable) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => PHP_VERSION_ID >= 80100 && !self::hasDbal4() ? null : self::floatOrNull(), // fails in UDF since PHP 8.1: sqrt(): Passing null to parameter #1 ($num) of type float is deprecated - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -3686,7 +3703,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(-1)' => [ @@ -3710,15 +3727,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(1) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('1')" => [ @@ -3726,15 +3743,15 @@ public static function provideCases(): iterable 'select' => "SELECT SQRT('1') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('1.0')" => [ @@ -3742,15 +3759,15 @@ public static function provideCases(): iterable 'select' => "SELECT SQRT('1.0') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('1e0')" => [ @@ -3758,15 +3775,15 @@ public static function provideCases(): iterable 'select' => "SELECT SQRT('1e0') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('foo')" => [ @@ -4238,15 +4255,15 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_float_nullable, 0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::float(), self::int()), 'pgsql' => TypeCombinator::union(self::float(), self::int()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_float_nullable, 0.0)' => [ @@ -4254,15 +4271,15 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_float_nullable, 0.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => TypeCombinator::union(self::float(), self::numericString()), 'pgsql' => TypeCombinator::union(self::float(), self::numericString()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, 0)' => [ @@ -4286,15 +4303,15 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0)' => [ @@ -4302,15 +4319,15 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0)' => [ @@ -4318,15 +4335,15 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, '0')" => [ @@ -4334,15 +4351,15 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, \'0\') FROM %s t', 'mysql' => self::numericString(), 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => '0', 'sqliteResult' => '0', - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_string)' => [ @@ -4371,10 +4388,10 @@ public static function provideCases(): iterable 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_string_nullable, t.col_int)' => [ @@ -4996,6 +5013,15 @@ private function resolveDefaultBooleanStringification(?string $driver, int $php, return $this->resolveDefaultStringification($driver, $php, $configName); } + private function resolveDefaultFloatStringification(?string $driver, int $php, string $configName): bool + { + if ($php < 80400 && $driver === DriverDetector::PDO_PGSQL) { + return true; // pdo_pgsql does stringify floats even without ATTR_STRINGIFY_FETCHES prior to PHP 8.4 + } + + return $this->resolveDefaultStringification($driver, $php, $configName); + } + private function getHumanReadablePhpVersion(int $phpVersion): string { return floor($phpVersion / 10000) . '.' . floor(($phpVersion % 10000) / 100); @@ -5024,6 +5050,10 @@ private function shouldStringify(string $stringification, ?string $driverType, i return $this->resolveDefaultBooleanStringification($driverType, $phpVersion, $configName); } + if ($stringification === self::STRINGIFY_PG_FLOAT) { + return $this->resolveDefaultFloatStringification($driverType, $phpVersion, $configName); + } + throw new LogicException('Unknown stringification: ' . $stringification); } From 516967588266f7476fdff05350ff60130fa76b0c Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 9 Sep 2024 15:54:38 +0200 Subject: [PATCH 093/160] Fix asserts, add Dockerfile for PHP 8.4 --- .../Doctrine/Query/QueryResultTypeWalker.php | 5 ++ ...eryResultTypeWalkerFetchTypeMatrixTest.php | 74 +++++++++++++++---- tests/Platform/README.md | 10 ++- tests/Platform/docker/Dockerfile84 | 24 ++++++ tests/Platform/docker/docker-compose.yml | 14 ++++ 5 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 tests/Platform/docker/Dockerfile84 diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 54d836c6..970523de 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -1587,6 +1587,11 @@ public function walkLiteral($literal): string if (stripos($value, 'e') !== false) { $type = new DqlConstantStringType((string) (float) $value, $literal->type); } else { + // if ($this->phpVersion->getVersionId() >= 80400) { + // $type = new ConstantFloatType((float) $value); + // } else { + // $type = new DqlConstantStringType($value, $literal->type); + // } $type = new DqlConstantStringType($value, $literal->type); } diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 3faae5a6..28f9c2ed 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -2743,7 +2743,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'MAX(t.col_decimal)' => [ @@ -4170,6 +4170,38 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_DEFAULT, ]; + yield 'COALESCE(0, 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(0, 0) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(1.0, 1.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(1.0, 1.0) FROM %s t', + 'mysql' => self::numericString(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(), + 'pgsql' => self::numericString(), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + yield 'COALESCE(1e0, 1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(1e0, 1.0) FROM %s t', @@ -4255,15 +4287,17 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_float_nullable, 0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::float(), self::int()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::float(), self::int()), 'pgsql' => TypeCombinator::union(self::float(), self::int()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0.0, + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_PG_FLOAT, + 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_float_nullable, 0.0)' => [ @@ -4303,15 +4337,17 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0.0, + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_PG_FLOAT, + 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0)' => [ @@ -4319,15 +4355,17 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => 0.0, + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_PG_FLOAT, + 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0)' => [ @@ -4335,15 +4373,17 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => 0.0, + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_PG_FLOAT, + 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, '0')" => [ @@ -4351,15 +4391,17 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, \'0\') FROM %s t', 'mysql' => self::numericString(), 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => '0', 'sqliteResult' => '0', - 'pdoPgsqlResult' => 0.0, + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_PG_FLOAT, + 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_string)' => [ diff --git a/tests/Platform/README.md b/tests/Platform/README.md index d3678117..06d8b843 100644 --- a/tests/Platform/README.md +++ b/tests/Platform/README.md @@ -8,18 +8,22 @@ Set current working directory to project root. - `printf "UID=$(id -u)\nGID=$(id -g)" > .env` - `docker-compose -f tests/Platform/docker/docker-compose.yml up -d` -# Test behaviour with old stringification +# Test behaviour for PHP 8.0 (old stringification) - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php80 composer update` - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php80 php -d memory_limit=1G vendor/bin/phpunit --group=platform` -# Test behaviour with new stringification +# Test behaviour for PHP 8.1 (adjusted stringification) - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 composer update` - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform` + +# Test behaviour for PHP 8.4 (pdo_pgsql float stringification fix) +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php84 composer update` +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php84 php -d memory_limit=1G vendor/bin/phpunit --group=platform` ``` You can also run utilize those containers for PHPStorm PHPUnit configuration. Since the dataset is huge and takes few minutes to run, you can filter only functions you are interested in: ```sh -docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform --filter "AVG" +docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php84 php -d memory_limit=1G vendor/bin/phpunit --group=platform --filter "AVG" ``` diff --git a/tests/Platform/docker/Dockerfile84 b/tests/Platform/docker/Dockerfile84 new file mode 100644 index 00000000..81ac9834 --- /dev/null +++ b/tests/Platform/docker/Dockerfile84 @@ -0,0 +1,24 @@ +FROM php:8.4.0beta4-cli + +# MSSQL +RUN apt update \ + && apt install -y gnupg2 \ + && apt install -y unixodbc-dev unixodbc \ + && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ + && curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | tee /etc/apt/sources.list.d/mssql-tools.list \ + && apt update \ + && ACCEPT_EULA=Y apt install -y msodbcsql17 \ + && pecl install sqlsrv \ + && pecl install pdo_sqlsrv \ + && docker-php-ext-enable sqlsrv pdo_sqlsrv + +RUN set -ex \ + && apt update \ + && apt install -y bash zip libpq-dev libsqlite3-dev \ + && pecl install xdebug-3.4 mongodb \ + && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ + && docker-php-ext-install pdo mysqli pgsql pdo_mysql pdo_pgsql pdo_sqlite \ + && docker-php-ext-enable mongodb # TODO xdebug not yet supported here + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + diff --git a/tests/Platform/docker/docker-compose.yml b/tests/Platform/docker/docker-compose.yml index 73596b72..4a3b0f48 100644 --- a/tests/Platform/docker/docker-compose.yml +++ b/tests/Platform/docker/docker-compose.yml @@ -63,3 +63,17 @@ services: user: ${UID:-1000}:${GID:-1000} volumes: - ../../../:/app + + php84: + depends_on: [mysql, pgsql] + build: + context: . + dockerfile: ./Dockerfile84 + environment: + MYSQL_HOST: mysql + PGSQL_HOST: pgsql + MSSQL_HOST: mssql + working_dir: /app + user: ${UID:-1000}:${GID:-1000} + volumes: + - ../../../:/app From 0d8866981a1662629a0559e969ad0c1408faaf45 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 9 Sep 2024 15:55:54 +0200 Subject: [PATCH 094/160] Remove commented code --- src/Type/Doctrine/Query/QueryResultTypeWalker.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 970523de..54d836c6 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -1587,11 +1587,6 @@ public function walkLiteral($literal): string if (stripos($value, 'e') !== false) { $type = new DqlConstantStringType((string) (float) $value, $literal->type); } else { - // if ($this->phpVersion->getVersionId() >= 80400) { - // $type = new ConstantFloatType((float) $value); - // } else { - // $type = new DqlConstantStringType($value, $literal->type); - // } $type = new DqlConstantStringType($value, $literal->type); } From d198a78b0b3ad70758c43c822f9bb86ebbcd0a48 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 13 Sep 2024 14:51:58 +0200 Subject: [PATCH 095/160] Fixes after TypeSpecifier BC break --- .../Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php | 2 ++ .../QueryBuilder/QueryBuilderTypeSpecifyingExtension.php | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php b/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php index d94a455a..32765e79 100644 --- a/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php +++ b/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php @@ -61,12 +61,14 @@ public function specifyTypes( new MethodCall($node->var, self::FIRST_METHOD_NAME), new ConstantBooleanType(false), $context, + $scope, ); $last = $this->typeSpecifier->create( new MethodCall($node->var, self::LAST_METHOD_NAME), new ConstantBooleanType(false), $context, + $scope, ); return $first->unionWith($last); diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php index 83966118..a6ab404d 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php @@ -98,8 +98,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod $queryBuilderNode, TypeCombinator::union(...$resultTypes), TypeSpecifierContext::createTruthy(), - true, - ); + $scope, + )->setAlwaysOverwriteTypes(); } } From d45a6401fb657e43a90965ed5adbf486b630580f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Sep 2024 17:55:23 +0200 Subject: [PATCH 096/160] Uncover everything behind the bleedingEdge flag --- rules.neon | 16 ++-------------- src/Rules/Doctrine/ORM/EntityColumnRule.php | 7 ------- src/Rules/Doctrine/ORM/EntityRelationRule.php | 10 +--------- .../Rules/Doctrine/ORM/EntityColumnRuleTest.php | 1 - .../Doctrine/ORM/EntityRelationRuleTest.php | 1 - 5 files changed, 3 insertions(+), 32 deletions(-) diff --git a/rules.neon b/rules.neon index de338d2a..cf2396b2 100644 --- a/rules.neon +++ b/rules.neon @@ -23,14 +23,10 @@ parametersSchema: rules: - PHPStan\Rules\Doctrine\ORM\DqlRule - PHPStan\Rules\Doctrine\ORM\RepositoryMethodCallRule + - PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule + - PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule - PHPStan\Rules\Doctrine\ORM\EntityNotFinalRule -conditionalTags: - PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% - PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% - services: - class: PHPStan\Rules\Doctrine\ORM\QueryBuilderDqlRule @@ -43,23 +39,15 @@ services: arguments: reportUnknownTypes: %doctrine.reportUnknownTypes% allowNullablePropertyForRequiredField: %doctrine.allowNullablePropertyForRequiredField% - bleedingEdge: %featureToggles.bleedingEdge% descriptorRegistry: @doctrineTypeDescriptorRegistry tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule - - - class: PHPStan\Rules\Doctrine\ORM\EntityNotFinalRule - class: PHPStan\Rules\Doctrine\ORM\EntityRelationRule arguments: allowNullablePropertyForRequiredField: %doctrine.allowNullablePropertyForRequiredField% - bleedingEdge: %featureToggles.bleedingEdge% tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule - class: PHPStan\Classes\DoctrineProxyForbiddenClassNamesExtension tags: diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index c2ee7a15..86cde306 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -44,15 +44,12 @@ class EntityColumnRule implements Rule private bool $allowNullablePropertyForRequiredField; - private bool $bleedingEdge; - public function __construct( ObjectMetadataResolver $objectMetadataResolver, DescriptorRegistry $descriptorRegistry, ReflectionProvider $reflectionProvider, bool $reportUnknownTypes, bool $allowNullablePropertyForRequiredField, - bool $bleedingEdge ) { $this->objectMetadataResolver = $objectMetadataResolver; @@ -60,7 +57,6 @@ public function __construct( $this->reflectionProvider = $reflectionProvider; $this->reportUnknownTypes = $reportUnknownTypes; $this->allowNullablePropertyForRequiredField = $allowNullablePropertyForRequiredField; - $this->bleedingEdge = $bleedingEdge; } public function getNodeType(): string @@ -70,9 +66,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$this->bleedingEdge && !$this->objectMetadataResolver->hasObjectManagerLoader()) { - return []; - } $class = $scope->getClassReflection(); if ($class === null) { return []; diff --git a/src/Rules/Doctrine/ORM/EntityRelationRule.php b/src/Rules/Doctrine/ORM/EntityRelationRule.php index 97e3ae2f..62f8a22f 100644 --- a/src/Rules/Doctrine/ORM/EntityRelationRule.php +++ b/src/Rules/Doctrine/ORM/EntityRelationRule.php @@ -32,17 +32,13 @@ class EntityRelationRule implements Rule private bool $allowNullablePropertyForRequiredField; - private bool $bleedingEdge; - public function __construct( ObjectMetadataResolver $objectMetadataResolver, - bool $allowNullablePropertyForRequiredField, - bool $bleedingEdge + bool $allowNullablePropertyForRequiredField ) { $this->objectMetadataResolver = $objectMetadataResolver; $this->allowNullablePropertyForRequiredField = $allowNullablePropertyForRequiredField; - $this->bleedingEdge = $bleedingEdge; } public function getNodeType(): string @@ -52,10 +48,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$this->bleedingEdge && !$this->objectMetadataResolver->hasObjectManagerLoader()) { - return []; - } - $class = $scope->getClassReflection(); if ($class === null) { return []; diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 091ef9c3..60a05db7 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -83,7 +83,6 @@ protected function getRule(): Rule $this->createReflectionProvider(), true, $this->allowNullablePropertyForRequiredField, - true, ); } diff --git a/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php b/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php index 5222b65f..7e81545f 100644 --- a/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php @@ -23,7 +23,6 @@ protected function getRule(): Rule return new EntityRelationRule( new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp'), $this->allowNullablePropertyForRequiredField, - true, ); } From 674c9d44b5d2e385eaa5061615aa8a0ffac6e1fd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Sep 2024 18:00:11 +0200 Subject: [PATCH 097/160] Fixes after PHPStan update --- src/Type/Doctrine/Descriptors/SimpleArrayType.php | 3 ++- src/Type/Doctrine/HydrationModeReturnTypeResolver.php | 4 ++-- tests/Platform/data/config.neon | 3 --- .../Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php | 4 +--- tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php | 2 +- tests/Type/Doctrine/data/QueryResult/config.neon | 2 -- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Type/Doctrine/Descriptors/SimpleArrayType.php b/src/Type/Doctrine/Descriptors/SimpleArrayType.php index 8044caca..2154eb91 100644 --- a/src/Type/Doctrine/Descriptors/SimpleArrayType.php +++ b/src/Type/Doctrine/Descriptors/SimpleArrayType.php @@ -8,6 +8,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; class SimpleArrayType implements DoctrineTypeDescriptor { @@ -19,7 +20,7 @@ public function getType(): string public function getWritableToPropertyType(): Type { - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new StringType()), new AccessoryArrayListType()); } public function getWritableToDatabaseType(): Type diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php index c0522e7f..e978ae2f 100644 --- a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -77,10 +77,10 @@ public function getMethodReturnTypeForHydrationMode( ); default: if ($queryKeyType->isNull()->yes()) { - return AccessoryArrayListType::intersectWith(new ArrayType( + return TypeCombinator::intersect(new ArrayType( new IntegerType(), $queryResultType, - )); + ), new AccessoryArrayListType()); } return new ArrayType( $queryKeyType, diff --git a/tests/Platform/data/config.neon b/tests/Platform/data/config.neon index 38f26ed7..e6dd2808 100644 --- a/tests/Platform/data/config.neon +++ b/tests/Platform/data/config.neon @@ -1,5 +1,2 @@ includes: - ../../../extension.neon -parameters: - featureToggles: - listType: true diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index 52e08911..307a867d 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -116,8 +116,6 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ */ public static function getTestData(): iterable { - AccessoryArrayListType::setListTypeEnabled(true); - yield 'getResult(object), full entity' => [ self::list(new ObjectType(Simple::class)), ' @@ -305,7 +303,7 @@ private static function constantArray(array $elements): Type private static function list(Type $values): Type { - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $values)); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $values), new AccessoryArrayListType()); } private static function numericString(): Type diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 33fa5bbd..427212a4 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -1497,7 +1497,7 @@ private function yieldConditionalDataset(): iterable $this->constantArray([ [new ConstantStringType('stringEnumColumn'), new ObjectType(StringEnum::class)], [new ConstantStringType('intEnumColumn'), new ObjectType(IntEnum::class)], - [new ConstantStringType('stringEnumListColumn'), AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new ObjectType(StringEnum::class)))], + [new ConstantStringType('stringEnumListColumn'), TypeCombinator::intersect(new ArrayType(new IntegerType(), new ObjectType(StringEnum::class)), new AccessoryArrayListType())], ]), ' SELECT e.stringEnumColumn, e.intEnumColumn, e.stringEnumListColumn diff --git a/tests/Type/Doctrine/data/QueryResult/config.neon b/tests/Type/Doctrine/data/QueryResult/config.neon index 5ce3210a..147e4f94 100644 --- a/tests/Type/Doctrine/data/QueryResult/config.neon +++ b/tests/Type/Doctrine/data/QueryResult/config.neon @@ -3,5 +3,3 @@ includes: parameters: doctrine: objectManagerLoader: entity-manager.php - featureToggles: - listType: true From a110d8a13ace9c5fcdfd63cc3c6e081ab1cc4da4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Sep 2024 20:51:55 +0200 Subject: [PATCH 098/160] Fix --- src/Rules/Doctrine/ORM/EntityColumnRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index 86cde306..06b9343f 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -49,7 +49,7 @@ public function __construct( DescriptorRegistry $descriptorRegistry, ReflectionProvider $reflectionProvider, bool $reportUnknownTypes, - bool $allowNullablePropertyForRequiredField, + bool $allowNullablePropertyForRequiredField ) { $this->objectMetadataResolver = $objectMetadataResolver; From c17a736f8c5d4c0d7ac2179e109541a9f4a7ba2d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 26 Sep 2024 14:53:22 +0200 Subject: [PATCH 099/160] Fixes after PHPStan updates --- .../QueryBuilder/QueryBuilderExecuteMethodExtension.php | 6 +++++- .../DBAL/RowCountMethodDynamicReturnTypeExtension.php | 3 +-- .../Query/QueryResultDynamicReturnTypeExtension.php | 6 ++++-- src/Type/Doctrine/Query/QueryResultTypeWalker.php | 2 +- .../QueryBuilderMethodDynamicReturnTypeExtension.php | 5 +---- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php b/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php index a997c2da..ad30e430 100644 --- a/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php +++ b/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php @@ -38,7 +38,11 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants() + )->getReturnType(); $queryBuilderType = new ObjectType(QueryBuilder::class); $var = $methodCall->var; diff --git a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php index 03a988e1..60c17e79 100644 --- a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php @@ -8,7 +8,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -76,7 +75,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } $rowCountMethod = $resultReflection->getNativeMethod('rowCount'); - $variant = ParametersAcceptorSelector::selectSingle($rowCountMethod->getVariants()); + $variant = $rowCountMethod->getOnlyVariant(); return $variant->getReturnType(); } diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 24aecb38..dda9e637 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -68,8 +68,10 @@ public function getTypeFromMethodCall( if (isset($args[$argIndex])) { $hydrationMode = $scope->getType($args[$argIndex]->value); } else { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants(), + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants() ); $parameter = $parametersAcceptor->getParameters()[$argIndex]; $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 4b96044d..e508d925 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -609,7 +609,7 @@ public function walkFunction($function): string if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { $type = new FloatType(); - $cannotBeNegative = $exprType->isSmallerThan(new ConstantIntegerType(0))->no(); + $cannotBeNegative = $exprType->isSmallerThan(new ConstantIntegerType(0), $this->phpVersion)->no(); $canBeNegative = !$cannotBeNegative; if ($canBeNegative) { $type = TypeCombinator::addNull($type); diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php index 4e9ea601..67003dc6 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php @@ -7,7 +7,6 @@ use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Doctrine\DoctrineTypeUtils; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\MixedType; @@ -39,9 +38,7 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - $returnType = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants(), - )->getReturnType(); + $returnType = $methodReflection->getVariants()[0]->getReturnType(); if ($returnType instanceof MixedType) { return false; } From c5856104344ad18ad5e405ada91d384be5ca0e26 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 30 Sep 2024 16:58:44 +0200 Subject: [PATCH 100/160] [BCB] Remove options that existed only for performance reasons --- extension.neon | 6 - rules.neon | 2 - ...QueryBuilderDynamicReturnTypeExtension.php | 13 +- .../OtherMethodQueryBuilderParser.php | 9 +- .../QueryBuilder/SimpleQueryBuilderType.php | 33 ---- .../ORM/QueryBuilderDqlRuleSlowTest.php | 147 ------------------ tests/Rules/Doctrine/ORM/slow.neon | 3 - 7 files changed, 3 insertions(+), 210 deletions(-) delete mode 100644 src/Type/Doctrine/QueryBuilder/SimpleQueryBuilderType.php delete mode 100644 tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php delete mode 100644 tests/Rules/Doctrine/ORM/slow.neon diff --git a/extension.neon b/extension.neon index 63365d37..b8f68a86 100644 --- a/extension.neon +++ b/extension.neon @@ -9,8 +9,6 @@ parameters: queryBuilderClass: null allCollectionsSelectable: true objectManagerLoader: null - searchOtherMethodsForQueryBuilderBeginning: true - queryBuilderFastAlgorithm: false literalString: false featureToggles: skipCheckGenericClasses: @@ -79,8 +77,6 @@ parametersSchema: queryBuilderClass: schema(string(), nullable()) allCollectionsSelectable: bool() objectManagerLoader: schema(string(), nullable()) - searchOtherMethodsForQueryBuilderBeginning: bool() - queryBuilderFastAlgorithm: bool() reportDynamicQueryBuilders: bool() reportUnknownTypes: bool() allowNullablePropertyForRequiredField: bool() @@ -117,7 +113,6 @@ services: class: PHPStan\Type\Doctrine\QueryBuilder\CreateQueryBuilderDynamicReturnTypeExtension arguments: queryBuilderClass: %doctrine.queryBuilderClass% - fasterVersion: %doctrine.queryBuilderFastAlgorithm% tags: - phpstan.broker.dynamicMethodReturnTypeExtension - @@ -194,7 +189,6 @@ services: - class: PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser arguments: - descendIntoOtherMethods: %doctrine.searchOtherMethodsForQueryBuilderBeginning% parser: @defaultAnalysisParser - diff --git a/rules.neon b/rules.neon index cf2396b2..0853184d 100644 --- a/rules.neon +++ b/rules.neon @@ -12,8 +12,6 @@ parametersSchema: queryBuilderClass: schema(string(), nullable()) allCollectionsSelectable: bool() objectManagerLoader: schema(string(), nullable()) - searchOtherMethodsForQueryBuilderBeginning: bool() - queryBuilderFastAlgorithm: bool() reportDynamicQueryBuilders: bool() reportUnknownTypes: bool() allowNullablePropertyForRequiredField: bool() diff --git a/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php index 8389395a..3fe7e9b1 100644 --- a/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php @@ -13,15 +13,11 @@ class CreateQueryBuilderDynamicReturnTypeExtension implements DynamicMethodRetur private ?string $queryBuilderClass = null; - private bool $fasterVersion; - public function __construct( - ?string $queryBuilderClass, - bool $fasterVersion + ?string $queryBuilderClass ) { $this->queryBuilderClass = $queryBuilderClass; - $this->fasterVersion = $fasterVersion; } public function getClass(): string @@ -40,12 +36,7 @@ public function getTypeFromMethodCall( Scope $scope ): Type { - $class = SimpleQueryBuilderType::class; - if (!$this->fasterVersion) { - $class = BranchingQueryBuilderType::class; - } - - return new $class( + return new BranchingQueryBuilderType( $this->queryBuilderClass ?? 'Doctrine\ORM\QueryBuilder', ); } diff --git a/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php b/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php index cddd3a93..153291f5 100644 --- a/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php +++ b/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php @@ -28,8 +28,6 @@ class OtherMethodQueryBuilderParser { - private bool $descendIntoOtherMethods; - private Parser $parser; private Container $container; @@ -41,9 +39,8 @@ class OtherMethodQueryBuilderParser */ private array $cache = []; - public function __construct(bool $descendIntoOtherMethods, Parser $parser, Container $container) + public function __construct(Parser $parser, Container $container) { - $this->descendIntoOtherMethods = $descendIntoOtherMethods; $this->parser = $parser; $this->container = $container; } @@ -53,10 +50,6 @@ public function __construct(bool $descendIntoOtherMethods, Parser $parser, Conta */ public function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodReflection $methodReflection): array { - if (!$this->descendIntoOtherMethods) { - return []; - } - $methodName = $methodReflection->getName(); $className = $methodReflection->getDeclaringClass()->getName(); $fileName = $methodReflection->getDeclaringClass()->getFileName(); diff --git a/src/Type/Doctrine/QueryBuilder/SimpleQueryBuilderType.php b/src/Type/Doctrine/QueryBuilder/SimpleQueryBuilderType.php deleted file mode 100644 index 98660de8..00000000 --- a/src/Type/Doctrine/QueryBuilder/SimpleQueryBuilderType.php +++ /dev/null @@ -1,33 +0,0 @@ -getMethodCalls()) === count($type->getMethodCalls()); - } - - return parent::equals($type); - } - - public function isSuperTypeOf(Type $type): TrinaryLogic - { - if ($type instanceof parent) { - $thisCount = count($this->getMethodCalls()); - $thatCount = count($type->getMethodCalls()); - - return TrinaryLogic::createFromBoolean($thisCount === $thatCount); - } - - return parent::isSuperTypeOf($type); - } - -} diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php deleted file mode 100644 index 2c4e03f7..00000000 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php +++ /dev/null @@ -1,147 +0,0 @@ - - */ -class QueryBuilderDqlRuleSlowTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new QueryBuilderDqlRule( - new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp'), - true, - ); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/query-builder-dql.php'], [ - [ - "QueryBuilder: [Syntax Error] line 0, col 66: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1)", - 31, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col 68: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = :id)", - 43, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col 68: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = :id)", - 55, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'transient = \': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named transient', - 62, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 14 near \'Foo e\': Error: Class \'Foo\' is not defined.', - 71, - ], - [ - 'Could not analyse QueryBuilder with dynamic arguments.', - 99, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'transient = \': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named transient', - 107, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col 82: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1 ORDER BY e.name) ASC", - 129, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 78 near \'name ASC\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named name', - 139, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 78 near \'name ASC\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named name', - 160, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'transient = 1\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named transient', - 170, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 72 near \'nickname LIKE\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named nickname', - 194, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 72 near \'nickname IS \': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named nickname', - 206, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col 80: Error: Expected =, <, <=, <>, >, >=, !=, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1 OR e.nickname) IS NULL", - 218, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'transient = \': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named transient', - 234, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'nonexistent =\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named nonexistent', - 251, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col -1: Error: Expected =, <, <=, <>, >, >=, !=, got end of string.\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE foo", - 281, - ], - ]); - } - - public function testRuleBranches(): void - { - $errors = [ - [ - 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', - 31, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', - 45, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', - 59, - 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE p.id = 1', - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', - 90, - 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND t.id = 1', - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 95 near \'foo = 1\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named foo', - 107, - 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND e.foo = 1', - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', - 107, - 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND t.id = 1', - ], - ]; - $this->analyse([__DIR__ . '/data/query-builder-branches-dql.php'], $errors); - } - - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/../../../../extension.neon', - __DIR__ . '/entity-manager.neon', - __DIR__ . '/slow.neon', - ]; - } - - protected function shouldFailOnPhpErrors(): bool - { - // doctrine/orm/src/Query/Parser.php throws assert($peek !== null) failed - return false; - } - -} diff --git a/tests/Rules/Doctrine/ORM/slow.neon b/tests/Rules/Doctrine/ORM/slow.neon deleted file mode 100644 index bfda5b8a..00000000 --- a/tests/Rules/Doctrine/ORM/slow.neon +++ /dev/null @@ -1,3 +0,0 @@ -parameters: - doctrine: - queryBuilderFastAlgorithm: false From 493457ac434afdbfa5604f01f98261cdd4e45d6b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 30 Sep 2024 17:01:10 +0200 Subject: [PATCH 101/160] Fix CS --- .../DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php | 2 +- .../Doctrine/Query/QueryResultDynamicReturnTypeExtension.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php b/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php index ad30e430..0a644f3a 100644 --- a/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php +++ b/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php @@ -41,7 +41,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( $scope, $methodCall->getArgs(), - $methodReflection->getVariants() + $methodReflection->getVariants(), )->getReturnType(); $queryBuilderType = new ObjectType(QueryBuilder::class); diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index dda9e637..22ed0b5a 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -71,7 +71,7 @@ public function getTypeFromMethodCall( $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $methodCall->getArgs(), - $methodReflection->getVariants() + $methodReflection->getVariants(), ); $parameter = $parametersAcceptor->getParameters()[$argIndex]; $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); From 261b19d3a59d02caa4f7d1da8c78976d01a20913 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 30 Sep 2024 17:03:34 +0200 Subject: [PATCH 102/160] Fixes after PHPStan update --- src/Rules/Doctrine/ORM/DqlRule.php | 3 +-- src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php | 3 +-- src/Type/Doctrine/ArgumentsProcessor.php | 2 +- .../Doctrine/GetRepositoryDynamicReturnTypeExtension.php | 2 +- ...sitoryCreateQueryBuilderDynamicReturnTypeExtension.php | 2 +- tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php | 8 ++++---- 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Rules/Doctrine/ORM/DqlRule.php b/src/Rules/Doctrine/ORM/DqlRule.php index 77cd3664..23f4da41 100644 --- a/src/Rules/Doctrine/ORM/DqlRule.php +++ b/src/Rules/Doctrine/ORM/DqlRule.php @@ -11,7 +11,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeUtils; use function count; use function sprintf; @@ -54,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $dqls = TypeUtils::getConstantStrings($scope->getType($node->getArgs()[0]->value)); + $dqls = $scope->getType($node->getArgs()[0]->value)->getConstantStrings(); if (count($dqls) === 0) { return []; } diff --git a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php index 62155d01..69f9791a 100644 --- a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php +++ b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\Doctrine\DoctrineTypeUtils; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeUtils; use Throwable; use function array_values; use function count; @@ -81,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array ]; } - $dqls = TypeUtils::getConstantStrings($dqlType); + $dqls = $dqlType->getConstantStrings(); if (count($dqls) === 0) { if ($this->reportDynamicQueryBuilders) { return [ diff --git a/src/Type/Doctrine/ArgumentsProcessor.php b/src/Type/Doctrine/ArgumentsProcessor.php index 77eeb5d7..2cd94fd7 100644 --- a/src/Type/Doctrine/ArgumentsProcessor.php +++ b/src/Type/Doctrine/ArgumentsProcessor.php @@ -58,7 +58,7 @@ public function processArgs( continue; } - if ($value->isClassStringType()->yes() && count($value->getClassStringObjectType()->getObjectClassNames()) === 1) { + if ($value->isClassString()->yes() && count($value->getClassStringObjectType()->getObjectClassNames()) === 1) { /** @var class-string $className */ $className = $value->getClassStringObjectType()->getObjectClassNames()[0]; if ($this->objectMetadataResolver->isTransient($className)) { diff --git a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php index d514a12b..8daaece8 100644 --- a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php @@ -85,7 +85,7 @@ public function getTypeFromMethodCall( ); } $argType = $scope->getType($methodCall->getArgs()[0]->value); - if (!$argType->isClassStringType()->yes()) { + if (!$argType->isClassString()->yes()) { return $this->getDefaultReturnType($scope, $methodCall->getArgs(), $methodReflection, $defaultRepositoryClass); } diff --git a/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php index f170b550..44201e45 100644 --- a/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php @@ -35,7 +35,7 @@ public function getTypeFromMethodCall( $entityNameExpr = new MethodCall($methodCall->var, new Identifier('getEntityName')); $entityNameExprType = $scope->getType($entityNameExpr); - if ($entityNameExprType->isClassStringType()->yes() && count($entityNameExprType->getClassStringObjectType()->getObjectClassNames()) === 1) { + if ($entityNameExprType->isClassString()->yes() && count($entityNameExprType->getClassStringObjectType()->getObjectClassNames()) === 1) { $entityNameExpr = new String_($entityNameExprType->getClassStringObjectType()->getObjectClassNames()[0]); } diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 60a05db7..d03da719 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -75,10 +75,10 @@ protected function getRule(): Rule new StringType(), new SimpleArrayType(), new UuidTypeDescriptor(FakeTestingUuidType::class), - new ReflectionDescriptor(CarbonImmutableType::class, $this->createBroker(), self::getContainer()), - new ReflectionDescriptor(CarbonType::class, $this->createBroker(), self::getContainer()), - new ReflectionDescriptor(CustomType::class, $this->createBroker(), self::getContainer()), - new ReflectionDescriptor(CustomNumericType::class, $this->createBroker(), self::getContainer()), + new ReflectionDescriptor(CarbonImmutableType::class, $this->createReflectionProvider(), self::getContainer()), + new ReflectionDescriptor(CarbonType::class, $this->createReflectionProvider(), self::getContainer()), + new ReflectionDescriptor(CustomType::class, $this->createReflectionProvider(), self::getContainer()), + new ReflectionDescriptor(CustomNumericType::class, $this->createReflectionProvider(), self::getContainer()), ]), $this->createReflectionProvider(), true, From 4f513337c20586277cdc76871eb482b614396bf2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 4 Oct 2024 13:36:14 +0200 Subject: [PATCH 103/160] Fixes after PHPStan update --- src/Rules/Doctrine/ORM/EntityColumnRule.php | 3 +-- src/Rules/Doctrine/ORM/EntityRelationRule.php | 3 +-- .../DBAL/RowCountMethodDynamicReturnTypeExtension.php | 4 ++++ .../Doctrine/GetRepositoryDynamicReturnTypeExtension.php | 4 ++++ .../QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php | 4 ++++ .../QueryBuilderGetDqlDynamicReturnTypeExtension.php | 4 ++++ .../QueryBuilderGetQueryDynamicReturnTypeExtension.php | 4 ++++ .../QueryBuilderMethodDynamicReturnTypeExtension.php | 4 ++++ .../QueryBuilder/QueryBuilderTypeSpecifyingExtension.php | 4 ++++ stubs/MongoClassMetadataInfo.stub | 5 ----- .../ORM/EntityRepositoryDynamicReturnIntegrationTest.php | 2 +- ...ithoutObjectManagerLoaderDynamicReturnIntegrationTest.php | 2 +- tests/Rules/Doctrine/ORM/FakeTestingUuidType.php | 1 + 13 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index 06b9343f..b5f42bb6 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -16,7 +16,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; @@ -176,7 +175,7 @@ public function processNode(Node $node, Scope $scope): array } $phpDocType = $node->getPhpDocType(); - $nativeType = $node->getNativeType() !== null ? ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection()) : new MixedType(); + $nativeType = $node->getNativeType() ?? new MixedType(); $propertyType = TypehintHelper::decideType($nativeType, $phpDocType); if (get_class($propertyType) === MixedType::class || $propertyType instanceof ErrorType || $propertyType instanceof NeverType) { diff --git a/src/Rules/Doctrine/ORM/EntityRelationRule.php b/src/Rules/Doctrine/ORM/EntityRelationRule.php index 62f8a22f..84ce9120 100644 --- a/src/Rules/Doctrine/ORM/EntityRelationRule.php +++ b/src/Rules/Doctrine/ORM/EntityRelationRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VerbosityLevel; @@ -96,7 +95,7 @@ public function processNode(Node $node, Scope $scope): array } $phpDocType = $node->getPhpDocType(); - $nativeType = $node->getNativeType() !== null ? ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection()) : new MixedType(); + $nativeType = $node->getNativeType() ?? new MixedType(); $propertyType = TypehintHelper::decideType($nativeType, $phpDocType); $errors = []; diff --git a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php index 60c17e79..7781fc9f 100644 --- a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php @@ -16,6 +16,7 @@ class RowCountMethodDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { + /** @var class-string */ private string $class; private ObjectMetadataResolver $objectMetadataResolver; @@ -24,6 +25,9 @@ class RowCountMethodDynamicReturnTypeExtension implements DynamicMethodReturnTyp private ReflectionProvider $reflectionProvider; + /** + * @param class-string $class + */ public function __construct( string $class, ObjectMetadataResolver $objectMetadataResolver, diff --git a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php index 8daaece8..07d40ffa 100644 --- a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php @@ -35,10 +35,14 @@ class GetRepositoryDynamicReturnTypeExtension implements DynamicMethodReturnType private ?string $odmRepositoryClass = null; + /** @var class-string */ private string $managerClass; private ObjectMetadataResolver $metadataResolver; + /** + * @param class-string $managerClass + */ public function __construct( ReflectionProvider $reflectionProvider, ?string $repositoryClass, diff --git a/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php index 6f9a69c8..79206e1c 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php @@ -20,10 +20,14 @@ class NewExprDynamicReturnTypeExtension implements DynamicStaticMethodReturnType private ArgumentsProcessor $argumentsProcessor; + /** @var class-string */ private string $class; private ReflectionProvider $reflectionProvider; + /** + * @param class-string $class + */ public function __construct( ArgumentsProcessor $argumentsProcessor, string $class, diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php index c6f4a5a7..69ceb824 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php @@ -13,8 +13,12 @@ class QueryBuilderGetDqlDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { + /** @var class-string|null */ private ?string $queryBuilderClass = null; + /** + * @param class-string|null $queryBuilderClass + */ public function __construct( ?string $queryBuilderClass ) diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php index b02018be..98fdefb3 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php @@ -59,6 +59,7 @@ class QueryBuilderGetQueryDynamicReturnTypeExtension implements DynamicMethodRet private ArgumentsProcessor $argumentsProcessor; + /** @var class-string|null */ private ?string $queryBuilderClass = null; private DescriptorRegistry $descriptorRegistry; @@ -67,6 +68,9 @@ class QueryBuilderGetQueryDynamicReturnTypeExtension implements DynamicMethodRet private DriverDetector $driverDetector; + /** + * @param class-string|null $queryBuilderClass + */ public function __construct( ObjectMetadataResolver $objectMetadataResolver, ArgumentsProcessor $argumentsProcessor, diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php index 67003dc6..0c6d9266 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php @@ -22,8 +22,12 @@ class QueryBuilderMethodDynamicReturnTypeExtension implements DynamicMethodRetur private const MAX_COMBINATIONS = 16; + /** @var class-string|null */ private ?string $queryBuilderClass = null; + /** + * @param class-string|null $queryBuilderClass + */ public function __construct( ?string $queryBuilderClass ) diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php index a6ab404d..66b18f71 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php @@ -24,10 +24,14 @@ class QueryBuilderTypeSpecifyingExtension implements MethodTypeSpecifyingExtensi private const MAX_COMBINATIONS = 16; + /** @var class-string|null */ private ?string $queryBuilderClass = null; private TypeSpecifier $typeSpecifier; + /** + * @param class-string|null $queryBuilderClass + */ public function __construct(?string $queryBuilderClass) { $this->queryBuilderClass = $queryBuilderClass; diff --git a/stubs/MongoClassMetadataInfo.stub b/stubs/MongoClassMetadataInfo.stub index cfdba938..433ef2a4 100644 --- a/stubs/MongoClassMetadataInfo.stub +++ b/stubs/MongoClassMetadataInfo.stub @@ -15,11 +15,6 @@ class ClassMetadata implements BaseClassMetadata /** @var string|null */ public $customRepositoryClassName; - /** - * @var class-string - */ - public $name; - /** * @param class-string $documentName */ diff --git a/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php b/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php index e64f096d..de7910fc 100644 --- a/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php +++ b/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php @@ -10,7 +10,7 @@ final class EntityRepositoryDynamicReturnIntegrationTest extends LevelsTestCase /** * @return string[][] */ - public function dataTopics(): array + public static function dataTopics(): array { return [ ['entityRepositoryDynamicReturn'], diff --git a/tests/DoctrineIntegration/ORM/EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTest.php b/tests/DoctrineIntegration/ORM/EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTest.php index bb2d04c1..ea596a6e 100644 --- a/tests/DoctrineIntegration/ORM/EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTest.php +++ b/tests/DoctrineIntegration/ORM/EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTest.php @@ -10,7 +10,7 @@ final class EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTe /** * @return string[][] */ - public function dataTopics(): array + public static function dataTopics(): array { return [ ['entityRepositoryDynamicReturn'], diff --git a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php b/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php index fcb28f7e..f6255563 100644 --- a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php +++ b/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php @@ -47,6 +47,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str return null; } + /** @throws ConversionException */ return (string) $value; } From 14a59bb5b719d3740224b6ba8aec80c0294142f9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 7 Oct 2024 09:02:25 +0200 Subject: [PATCH 104/160] Fix stub --- stubs/MongoClassMetadataInfo.stub | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stubs/MongoClassMetadataInfo.stub b/stubs/MongoClassMetadataInfo.stub index 433ef2a4..65cfa40f 100644 --- a/stubs/MongoClassMetadataInfo.stub +++ b/stubs/MongoClassMetadataInfo.stub @@ -15,6 +15,12 @@ class ClassMetadata implements BaseClassMetadata /** @var string|null */ public $customRepositoryClassName; + /** + * @readonly + * @var class-string + */ + public $name; + /** * @param class-string $documentName */ From bcb88b6f1690052fcc45e6bfbc65921fe4385457 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 7 Oct 2024 09:10:55 +0200 Subject: [PATCH 105/160] Fix tests after PHPStan update --- tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index d03da719..f0a799e6 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -159,7 +159,7 @@ public function testRule(?string $objectManagerLoader): void 156, ], [ - 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: database can contain array but property expects array.', + 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: database can contain list but property expects array.', 162, ], [ @@ -230,7 +230,7 @@ public function testRuleWithAllowedNullableProperty(?string $objectManagerLoader 156, ], [ - 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: database can contain array but property expects array.', + 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: database can contain list but property expects array.', 162, ], [ @@ -399,7 +399,7 @@ public function testEnumType(?string $objectManagerLoader): void 45, ], [ - 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: database can contain array but property expects PHPStan\Rules\Doctrine\ORMAttributes\FooEnum.', + 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: database can contain list but property expects PHPStan\Rules\Doctrine\ORMAttributes\FooEnum.', 51, ], [ From de811c781e27b16b37407577f685beff3e53a057 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 7 Oct 2024 10:57:10 +0200 Subject: [PATCH 106/160] Fix tests for PHPStan 1.12.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Mirtes Co-authored-by: Markus Staab --- composer.json | 2 +- .../QueryResultTypeWalkerHydrationModeTest.php | 12 +++++++++--- .../Query/QueryResultTypeWalkerTest.php | 18 ++++++++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 94c15b34..3d2655d7 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" + "phpstan/phpstan": "^1.12.6" }, "conflict": { "doctrine/collections": "<1.0", diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index b9d02c85..be915cad 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -11,6 +11,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -308,12 +309,17 @@ private static function list(Type $values): Type return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $values)); } - private static function numericString(): Type + private static function numericString(bool $lowercase = false): Type { - return new IntersectionType([ + $types = [ new StringType(), new AccessoryNumericStringType(), - ]); + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + + return new IntersectionType($types); } /** diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index fd6fee74..905c5221 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -15,6 +15,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -1474,7 +1475,7 @@ public function getTestData(): iterable $this->constantArray([ [new ConstantStringType('minusInt'), $this->stringifies() ? new ConstantStringType('-1') : new ConstantIntegerType(-1)], [new ConstantStringType('minusFloat'), $this->stringifies() ? $this->numericString() : new ConstantFloatType(-0.1)], - [new ConstantStringType('minusIntRange'), $this->stringifies() ? $this->numericString() : IntegerRangeType::fromInterval(null, 0)], + [new ConstantStringType('minusIntRange'), $this->stringifies() ? $this->numericString(true) : IntegerRangeType::fromInterval(null, 0)], ]), ' SELECT -1 as minusInt, @@ -1622,12 +1623,17 @@ private function constantArray(array $elements): Type return $builder->getArray(); } - private function numericString(): Type + private function numericString(bool $lowercase = false): Type { - return new IntersectionType([ + $types = [ new StringType(), new AccessoryNumericStringType(), - ]); + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + + return new IntersectionType($types); } private function uint(): Type @@ -1673,14 +1679,14 @@ private function stringifies(): bool private function intOrStringified(): Type { return $this->stringifies() - ? $this->numericString() + ? $this->numericString(true) : new IntegerType(); } private function uintOrStringified(): Type { return $this->stringifies() - ? $this->numericString() + ? $this->numericString(true) : $this->uint(); } From 4a00482cf3b27c9ee6ec41970d543554f0c716fe Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 7 Oct 2024 11:15:08 +0200 Subject: [PATCH 107/160] Fixes after PHPStan update --- src/Type/Doctrine/Query/QueryType.php | 6 +++--- .../Doctrine/QueryBuilder/BranchingQueryBuilderType.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryType.php b/src/Type/Doctrine/Query/QueryType.php index ead7ef2b..53842dd2 100644 --- a/src/Type/Doctrine/Query/QueryType.php +++ b/src/Type/Doctrine/Query/QueryType.php @@ -2,8 +2,8 @@ namespace PHPStan\Type\Doctrine\Query; -use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -44,10 +44,10 @@ public function changeSubtractedType(?Type $subtractedType): Type return new self('Doctrine\ORM\Query', $this->indexType, $this->resultType, $subtractedType); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean($this->equals($type)); + return IsSuperTypeOfResult::createFromBoolean($this->equals($type)); } return parent::isSuperTypeOf($type); diff --git a/src/Type/Doctrine/QueryBuilder/BranchingQueryBuilderType.php b/src/Type/Doctrine/QueryBuilder/BranchingQueryBuilderType.php index e9c7bfb8..473dffb5 100644 --- a/src/Type/Doctrine/QueryBuilder/BranchingQueryBuilderType.php +++ b/src/Type/Doctrine/QueryBuilder/BranchingQueryBuilderType.php @@ -2,7 +2,7 @@ namespace PHPStan\Type\Doctrine\QueryBuilder; -use PHPStan\TrinaryLogic; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Type; use function array_keys; use function count; @@ -35,10 +35,10 @@ public function equals(Type $type): bool return parent::equals($type); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof parent) { - return TrinaryLogic::createFromBoolean($this->equals($type)); + return IsSuperTypeOfResult::createFromBoolean($this->equals($type)); } return parent::isSuperTypeOf($type); From 8b557dec231a93bc7aaab9d449ce4d28d4cee5ff Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 8 Oct 2024 11:05:12 +0200 Subject: [PATCH 108/160] Readme: `index by` is supported --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83bc5364..c8a587b1 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ $query->getResult(); // array Queries are analyzed statically and do not require a running database server. This makes use of the Doctrine DQL parser and entities metadata. -Most DQL features are supported, including `GROUP BY`, `DISTINCT`, all flavors of `JOIN`, arithmetic expressions, functions, aggregations, `NEW`, etc. Sub queries and `INDEX BY` are not yet supported (infered type will be `mixed`). +Most DQL features are supported, including `GROUP BY`, `INDEX BY`, `DISTINCT`, all flavors of `JOIN`, arithmetic expressions, functions, aggregations, `NEW`, etc. Sub queries are not yet supported (infered type will be `mixed`). ### Query type inference of expressions From b129887388641766b5d306492351b96a4ee7cbe4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 9 Oct 2024 16:39:36 +0200 Subject: [PATCH 109/160] Cleanup `skipCheckGenericClasses` --- extension.neon | 9 --------- 1 file changed, 9 deletions(-) diff --git a/extension.neon b/extension.neon index b8f68a86..6b09c624 100644 --- a/extension.neon +++ b/extension.neon @@ -10,15 +10,6 @@ parameters: allCollectionsSelectable: true objectManagerLoader: null literalString: false - featureToggles: - skipCheckGenericClasses: - - Doctrine\ODM\MongoDB\Mapping\ClassMetadata - - Doctrine\ORM\Mapping\ClassMetadata - - Doctrine\ORM\Mapping\ClassMetadataInfo - - Doctrine\Persistence\Mapping\ClassMetadata - - Doctrine\ORM\AbstractQuery - - Doctrine\ORM\Query - - Doctrine\ORM\Tools\Pagination\Paginator stubFiles: - stubs/Criteria.stub - stubs/DBAL/Cache/CacheException.stub From 992a62e337eff90308e88c67f5a807ab21ffe644 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 05:21:12 +0200 Subject: [PATCH 110/160] Fix after PHPStan update --- tests/Type/Doctrine/data/QueryResult/queryResult.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index de4cc5bf..54eef205 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -169,7 +169,7 @@ public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(Entit ); assertType( - 'array', + 'array', $query->getArrayResult() ); assertType( From e26fd3c8969d7e2a4c3056f6a2585cb7d5a752db Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 05:51:47 +0200 Subject: [PATCH 111/160] Fix nonexistent BackedEnum in stubs --- compatibility/BackedEnum.stub | 6 ++++++ phpstan.neon | 6 ++++++ tests/BackedEnumStubExtension.php | 22 ++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 compatibility/BackedEnum.stub create mode 100644 tests/BackedEnumStubExtension.php diff --git a/compatibility/BackedEnum.stub b/compatibility/BackedEnum.stub new file mode 100644 index 00000000..2376a2fc --- /dev/null +++ b/compatibility/BackedEnum.stub @@ -0,0 +1,6 @@ += 80100) { + return []; + } + + return [ + __DIR__ . '/../compatibility/BackedEnum.stub', + ]; + } + +} From f61f963a80e823462146d76b57b73788c367cc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Sun, 27 Oct 2024 12:52:54 +0100 Subject: [PATCH 112/160] Mark immediately invoked callback params --- stubs/Collections/ReadableCollection.stub | 70 +++++++++++++++++++++++ stubs/DBAL/Connection.stub | 13 +++++ stubs/DBAL/Connection4.stub | 13 +++++ 3 files changed, 96 insertions(+) diff --git a/stubs/Collections/ReadableCollection.stub b/stubs/Collections/ReadableCollection.stub index df07c526..d9fba5ae 100644 --- a/stubs/Collections/ReadableCollection.stub +++ b/stubs/Collections/ReadableCollection.stub @@ -2,6 +2,7 @@ namespace Doctrine\Common\Collections; +use Closure; use Countable; use IteratorAggregate; @@ -13,4 +14,73 @@ use IteratorAggregate; interface ReadableCollection extends Countable, IteratorAggregate { + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool + */ + public function exists(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return ReadableCollection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return ReadableCollection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: ReadableCollection, 1: ReadableCollection} + */ + public function partition(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + public function forAll(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return T|null + */ + public function findFirst(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(TReturn|TInitial, T):TReturn $func + * @param TInitial $initial + * + * @return TReturn|TInitial + * + * @template TReturn + * @template TInitial + */ + public function reduce(Closure $func, mixed $initial = null); + } diff --git a/stubs/DBAL/Connection.stub b/stubs/DBAL/Connection.stub index d11b040f..15c8e6e1 100644 --- a/stubs/DBAL/Connection.stub +++ b/stubs/DBAL/Connection.stub @@ -2,9 +2,11 @@ namespace Doctrine\DBAL; +use Closure; use Doctrine\DBAL\Cache\CacheException; use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Types\Type; +use Throwable; class Connection { @@ -61,4 +63,15 @@ class Connection */ public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result; + /** + * @param-immediately-invoked-callable $func + * @param Closure(self): T $func + * @return T + * + * @template T + * + * @throws Throwable + */ + public function transactional(Closure $func); + } diff --git a/stubs/DBAL/Connection4.stub b/stubs/DBAL/Connection4.stub index 2e80b001..77b13d78 100644 --- a/stubs/DBAL/Connection4.stub +++ b/stubs/DBAL/Connection4.stub @@ -2,9 +2,11 @@ namespace Doctrine\DBAL; +use Closure; use Doctrine\DBAL\Cache\CacheException; use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Types\Type; +use Throwable; /** * @phpstan-type WrapperParameterType = string|Type|ParameterType|ArrayParameterType @@ -65,4 +67,15 @@ class Connection */ public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result; + /** + * @param-immediately-invoked-callable $func + * @param Closure(self): T $func + * @return T + * + * @template T + * + * @throws Throwable + */ + public function transactional(Closure $func); + } From 80f3b5f30803dadcd96722380be6f254d2c88b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Tue, 29 Oct 2024 09:00:14 +0100 Subject: [PATCH 113/160] Add phpdoc for transformation methods into other Collection stubs (fixes #621) --- stubs/Collections/ArrayCollection.stub | 31 ++++++++++ stubs/Collections/Collection.stub | 30 ++++++++++ stubs/Collections/Collection1.stub | 30 ++++++++++ .../DoctrineIntegration/TypeInferenceTest.php | 1 + tests/DoctrineIntegration/data/Collection.php | 56 +++++++++++++++++++ 5 files changed, 148 insertions(+) create mode 100644 tests/DoctrineIntegration/data/Collection.php diff --git a/stubs/Collections/ArrayCollection.stub b/stubs/Collections/ArrayCollection.stub index 2ecb9ea6..34d07450 100644 --- a/stubs/Collections/ArrayCollection.stub +++ b/stubs/Collections/ArrayCollection.stub @@ -2,6 +2,8 @@ namespace Doctrine\Common\Collections; +use Closure; + /** * @template TKey of array-key * @template T @@ -11,4 +13,33 @@ namespace Doctrine\Common\Collections; class ArrayCollection implements Collection, Selectable { + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return static + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return static + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: static, 1: static} + */ + public function partition(Closure $p); + } diff --git a/stubs/Collections/Collection.stub b/stubs/Collections/Collection.stub index be162ef6..05139edd 100644 --- a/stubs/Collections/Collection.stub +++ b/stubs/Collections/Collection.stub @@ -3,6 +3,7 @@ namespace Doctrine\Common\Collections; use ArrayAccess; +use Closure; use Countable; use IteratorAggregate; @@ -43,4 +44,33 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable */ public function removeElement($element) {} + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return Collection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return Collection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: Collection, 1: Collection} + */ + public function partition(Closure $p); + } diff --git a/stubs/Collections/Collection1.stub b/stubs/Collections/Collection1.stub index 455733c8..0f5ad708 100644 --- a/stubs/Collections/Collection1.stub +++ b/stubs/Collections/Collection1.stub @@ -3,6 +3,7 @@ namespace Doctrine\Common\Collections; use ArrayAccess; +use Closure; use Countable; use IteratorAggregate; @@ -43,4 +44,33 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable */ public function removeElement($element) {} + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return Collection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return Collection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: Collection, 1: Collection} + */ + public function partition(Closure $p); + } diff --git a/tests/DoctrineIntegration/TypeInferenceTest.php b/tests/DoctrineIntegration/TypeInferenceTest.php index 82a4c236..ba2c4fd6 100644 --- a/tests/DoctrineIntegration/TypeInferenceTest.php +++ b/tests/DoctrineIntegration/TypeInferenceTest.php @@ -14,6 +14,7 @@ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/getRepository.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/isEmpty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/Collection.php'); } /** diff --git a/tests/DoctrineIntegration/data/Collection.php b/tests/DoctrineIntegration/data/Collection.php new file mode 100644 index 00000000..f56e69d7 --- /dev/null +++ b/tests/DoctrineIntegration/data/Collection.php @@ -0,0 +1,56 @@ + */ + private $items; + + public function __construct() + { + /** @var ArrayCollection $numbers */ + $numbers = new ArrayCollection([1, 2, 3]); + + $filteredNumbers = $numbers->filter(function (int $number): bool { + return $number % 2 === 1; + }); + assertType('Doctrine\Common\Collections\ArrayCollection', $filteredNumbers); + + $items = $filteredNumbers->map(static function (int $number): Item { + return new Item(); + }); + assertType('Doctrine\Common\Collections\ArrayCollection', $items); + + $this->items = $items; + } + + public function removeOdd(): void + { + $this->items = $this->items->filter(function (Item $item, int $idx): bool { + return $idx % 2 === 1; + }); + assertType('Doctrine\Common\Collections\Collection', $this->items); + } + + public function __clone() + { + $this->items = $this->items->map( + static function (Item $item): Item { + return clone $item; + } + ); + assertType('Doctrine\Common\Collections\Collection', $this->items); + } + +} + +class Item +{ + +} From 4e9c77fc7bc3293f773fb2d8155c99572a3c89a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Tue, 29 Oct 2024 10:09:45 +0100 Subject: [PATCH 114/160] Add modified stub for ReadableCollection from doctrine/collections 1.x --- extension.neon | 1 - .../Doctrine/StubFilesExtensionLoader.php | 2 + stubs/Collections/ReadableCollection1.stub | 86 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 stubs/Collections/ReadableCollection1.stub diff --git a/extension.neon b/extension.neon index 63365d37..35108264 100644 --- a/extension.neon +++ b/extension.neon @@ -43,7 +43,6 @@ parameters: - stubs/Persistence/ObjectRepository.stub - stubs/RepositoryFactory.stub - stubs/Collections/ArrayCollection.stub - - stubs/Collections/ReadableCollection.stub - stubs/Collections/Selectable.stub - stubs/ORM/AbstractQuery.stub - stubs/ORM/Exception/ORMException.stub diff --git a/src/Stubs/Doctrine/StubFilesExtensionLoader.php b/src/Stubs/Doctrine/StubFilesExtensionLoader.php index 0b3a69d8..390e35a5 100644 --- a/src/Stubs/Doctrine/StubFilesExtensionLoader.php +++ b/src/Stubs/Doctrine/StubFilesExtensionLoader.php @@ -64,8 +64,10 @@ public function getFiles(): array $collectionVersion = null; } if ($collectionVersion !== null && strpos($collectionVersion, '1.') === 0) { + $files[] = $stubsDir . '/Collections/ReadableCollection1.stub'; $files[] = $stubsDir . '/Collections/Collection1.stub'; } else { + $files[] = $stubsDir . '/Collections/ReadableCollection.stub'; $files[] = $stubsDir . '/Collections/Collection.stub'; } diff --git a/stubs/Collections/ReadableCollection1.stub b/stubs/Collections/ReadableCollection1.stub new file mode 100644 index 00000000..dec73af0 --- /dev/null +++ b/stubs/Collections/ReadableCollection1.stub @@ -0,0 +1,86 @@ + + */ +interface ReadableCollection extends Countable, IteratorAggregate +{ + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool + */ + public function exists(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return ReadableCollection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return Collection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: ReadableCollection, 1: ReadableCollection} + */ + public function partition(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + public function forAll(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return T|null + */ + public function findFirst(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(TReturn|TInitial, T):TReturn $func + * @param TInitial $initial + * + * @return TReturn|TInitial + * + * @template TReturn + * @template TInitial + */ + public function reduce(Closure $func, mixed $initial = null); + +} From 8ba022846e79238872e315fff61e19b42ba2f139 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 7 Nov 2024 22:51:36 +0100 Subject: [PATCH 115/160] Add api annotation --- src/Type/Doctrine/ObjectMetadataResolver.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Type/Doctrine/ObjectMetadataResolver.php b/src/Type/Doctrine/ObjectMetadataResolver.php index 6a8b4fd2..68024891 100644 --- a/src/Type/Doctrine/ObjectMetadataResolver.php +++ b/src/Type/Doctrine/ObjectMetadataResolver.php @@ -106,6 +106,8 @@ private function getMetadataFactory(): ?ClassMetadataFactory } /** + * @api + * * @template T of object * @param class-string $className * @return ClassMetadata|null From f36b6d609fca0f68b8180a601b4a81b5e38ac43e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 20 Nov 2024 16:50:52 +0100 Subject: [PATCH 116/160] Fix tests --- composer.json | 2 +- src/Type/Doctrine/Descriptors/BigIntType.php | 3 +- src/Type/Doctrine/Descriptors/DecimalType.php | 9 +- src/Type/Doctrine/Descriptors/FloatType.php | 8 +- .../Doctrine/Query/QueryResultTypeWalker.php | 76 +++- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 353 +++++++++--------- ...QueryResultTypeWalkerHydrationModeTest.php | 14 +- .../Query/QueryResultTypeWalkerTest.php | 30 +- 8 files changed, 269 insertions(+), 226 deletions(-) diff --git a/composer.json b/composer.json index 3d2655d7..8bd0d8f0 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12.6" + "phpstan/phpstan": "^1.12.12" }, "conflict": { "doctrine/collections": "<1.0", diff --git a/src/Type/Doctrine/Descriptors/BigIntType.php b/src/Type/Doctrine/Descriptors/BigIntType.php index 14b3ca2a..01b85eff 100644 --- a/src/Type/Doctrine/Descriptors/BigIntType.php +++ b/src/Type/Doctrine/Descriptors/BigIntType.php @@ -3,7 +3,6 @@ namespace PHPStan\Type\Doctrine\Descriptors; use Composer\InstalledVersions; -use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -25,7 +24,7 @@ public function getWritableToPropertyType(): Type return new IntegerType(); } - return TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); + return (new IntegerType())->toString(); } public function getWritableToDatabaseType(): Type diff --git a/src/Type/Doctrine/Descriptors/DecimalType.php b/src/Type/Doctrine/Descriptors/DecimalType.php index 64184c45..066c0d93 100644 --- a/src/Type/Doctrine/Descriptors/DecimalType.php +++ b/src/Type/Doctrine/Descriptors/DecimalType.php @@ -4,10 +4,8 @@ use Doctrine\DBAL\Connection; use PHPStan\Doctrine\Driver\DriverDetector; -use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -31,7 +29,7 @@ public function getType(): string public function getWritableToPropertyType(): Type { - return TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); + return (new FloatType())->toString(); } public function getWritableToDatabaseType(): Type @@ -58,10 +56,7 @@ public function getDatabaseInternalTypeForDriver(Connection $connection): Type DriverDetector::PGSQL, DriverDetector::PDO_PGSQL, ], true)) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); + return (new FloatType())->toString(); } // not yet supported driver, return the old implementation guess diff --git a/src/Type/Doctrine/Descriptors/FloatType.php b/src/Type/Doctrine/Descriptors/FloatType.php index e475c0a2..92accffa 100644 --- a/src/Type/Doctrine/Descriptors/FloatType.php +++ b/src/Type/Doctrine/Descriptors/FloatType.php @@ -4,10 +4,7 @@ use Doctrine\DBAL\Connection; use PHPStan\Doctrine\Driver\DriverDetector; -use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function in_array; @@ -42,10 +39,7 @@ public function getDatabaseInternalType(): Type { return TypeCombinator::union( new \PHPStan\Type\FloatType(), - new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]) + (new \PHPStan\Type\FloatType())->toString() ); } diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 54d836c6..d6a898dc 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -18,7 +18,9 @@ use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -630,10 +632,10 @@ public function walkFunction($function): string $type = $this->createFloat(false); } elseif ($castedExprType->isNumericString()->yes()) { - $type = $this->createNumericString(false); + $type = $this->createNumericString(false, $castedExprType->isLowercaseString()->yes(), $castedExprType->isUppercaseString()->yes()); } else { - $type = TypeCombinator::union($this->createFloat(false), $this->createNumericString(false)); + $type = TypeCombinator::union($this->createFloat(false), $this->createNumericString(false, false, true)); } } else { @@ -746,7 +748,7 @@ private function inferAvgFunction(AST\Functions\AvgFunction $function): Type if ($this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::MYSQLI) { if ($exprTypeNoNull->isInteger()->yes()) { - return $this->createNumericString($nullable); + return $this->createNumericString($nullable, true, true); } if ($exprTypeNoNull->isString()->yes() && !$exprTypeNoNull->isNumericString()->yes()) { @@ -758,7 +760,7 @@ private function inferAvgFunction(AST\Functions\AvgFunction $function): Type if ($this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { if ($exprTypeNoNull->isInteger()->yes()) { - return $this->createNumericString($nullable); + return $this->createNumericString($nullable, true, true); } return $this->generalizeConstantType($exprType, $nullable); @@ -794,7 +796,7 @@ private function inferSumFunction(AST\Functions\SumFunction $function): Type if ($this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::MYSQLI) { if ($exprTypeNoNull->isInteger()->yes()) { - return $this->createNumericString($nullable); + return $this->createNumericString($nullable, true, true); } if ($exprTypeNoNull->isString()->yes() && !$exprTypeNoNull->isNumericString()->yes()) { @@ -808,7 +810,7 @@ private function inferSumFunction(AST\Functions\SumFunction $function): Type if ($exprTypeNoNull->isInteger()->yes()) { return TypeCombinator::union( $this->createInteger($nullable), - $this->createNumericString($nullable) + $this->createNumericString($nullable, true, true) ); } @@ -845,19 +847,41 @@ private function createNonNegativeInteger(bool $nullable): Type return $nullable ? TypeCombinator::addNull($integer) : $integer; } - private function createNumericString(bool $nullable): Type + private function createNumericString(bool $nullable, bool $lowercase = false, bool $uppercase = false): Type { - $numericString = TypeCombinator::intersect( + $types = [ new StringType(), - new AccessoryNumericStringType() - ); + new AccessoryNumericStringType(), + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } + + $numericString = new IntersectionType($types); return $nullable ? TypeCombinator::addNull($numericString) : $numericString; } - private function createString(bool $nullable): Type + private function createString(bool $nullable, bool $lowercase = false, bool $uppercase = false): Type { - $string = new StringType(); + if ($lowercase || $uppercase) { + $types = [ + new StringType(), + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } + $string = new IntersectionType($types); + } else { + $string = new StringType(); + } + return $nullable ? TypeCombinator::addNull($string) : $string; } @@ -903,10 +927,18 @@ private function generalizeConstantType(Type $type, bool $makeNullable): Type $result = $this->createFloat($containsNull); } elseif ($typeNoNull->isNumericString()->yes()) { - $result = $this->createNumericString($containsNull); + $result = $this->createNumericString( + $containsNull, + $typeNoNull->isLowercaseString()->yes(), + $typeNoNull->isUppercaseString()->yes() + ); } elseif ($typeNoNull->isString()->yes()) { - $result = $this->createString($containsNull); + $result = $this->createString( + $containsNull, + $typeNoNull->isLowercaseString()->yes(), + $typeNoNull->isUppercaseString()->yes() + ); } else { $result = $type; @@ -1249,7 +1281,7 @@ public function walkSelectExpression($selectExpression): string // e.g. 1.0 on sqlite results to '1' with pdo_stringify on PHP 8.1, but '1.0' on PHP 8.0 with no setup // so we relax constant types and return just numeric-string to avoid those issues - $stringifiedFloat = $this->createNumericString(false); + $stringifiedFloat = $this->createNumericString(false, false, true); if ($stringify->yes()) { return $stringifiedFloat; @@ -1781,7 +1813,11 @@ private function inferPlusMinusTimesType(array $termTypes): Type } if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), $this->createNumericString(false)])) { - return $this->createNumericString($nullable); + return $this->createNumericString( + $nullable, + $unionWithoutNull->toString()->isLowercaseString()->yes(), + $unionWithoutNull->toString()->isUppercaseString()->yes() + ); } if ($this->containsOnlyNumericTypes($unionWithoutNull)) { @@ -1833,7 +1869,7 @@ private function inferDivisionType(array $termTypes): Type if ($unionWithoutNull->isInteger()->yes()) { if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL) { - return $this->createNumericString($nullable); + return $this->createNumericString($nullable, true, true); } elseif ($this->driverType === DriverDetector::PDO_PGSQL || $this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { return $this->createInteger($nullable); } @@ -1861,7 +1897,11 @@ private function inferDivisionType(array $termTypes): Type } if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), $this->createNumericString(false)])) { - return $this->createNumericString($nullable); + return $this->createNumericString( + $nullable, + $unionWithoutNull->toString()->isLowercaseString()->yes(), + $unionWithoutNull->toString()->isUppercaseString()->yes() + ); } if ($this->containsOnlyTypes($unionWithoutNull, [new FloatType(), $this->createNumericString(false)])) { diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 28f9c2ed..03e5ed12 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -25,7 +25,9 @@ use PHPStan\Platform\Entity\PlatformEntity; use PHPStan\Platform\Entity\PlatformRelatedEntity; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -925,7 +927,7 @@ public static function provideCases(): iterable yield '1 + 1 * 1 / 1 - 1' => [ 'data' => self::dataDefault(), 'select' => 'SELECT (1 + 1 * 1 / 1 - 1) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => self::int(), 'pgsql' => self::int(), @@ -1053,10 +1055,10 @@ public static function provideCases(): iterable yield 't.col_int + t.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int + t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '9.1', 'sqliteResult' => 9.1, @@ -1069,10 +1071,10 @@ public static function provideCases(): iterable yield 't.col_int + t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_int + t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '2.0', 'sqliteResult' => 2, @@ -1117,10 +1119,10 @@ public static function provideCases(): iterable yield 't.col_decimal + t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '2.0', 'sqliteResult' => 2, @@ -1149,10 +1151,10 @@ public static function provideCases(): iterable yield 't.col_decimal + t.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.2', 'sqliteResult' => 0.2, @@ -1229,7 +1231,7 @@ public static function provideCases(): iterable yield 't.col_decimal + t.col_bool' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal + t.col_bool FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -1277,7 +1279,7 @@ public static function provideCases(): iterable yield 't.col_int / t.col_int' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int / t.col_int FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => self::int(), 'pgsql' => self::int(), @@ -1293,7 +1295,7 @@ public static function provideCases(): iterable yield 't.col_bigint / t.col_bigint' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_bigint / t.col_bigint FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => self::int(), 'pgsql' => self::int(), @@ -1373,10 +1375,10 @@ public static function provideCases(): iterable yield 't.col_int / t.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int / t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '90.0000', 'sqliteResult' => 90.0, @@ -1389,10 +1391,10 @@ public static function provideCases(): iterable yield 't.col_int / t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_int / t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0000', 'sqliteResult' => 1, @@ -1421,10 +1423,10 @@ public static function provideCases(): iterable yield 't.col_decimal / t.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1.0, @@ -1437,10 +1439,10 @@ public static function provideCases(): iterable yield 't.col_decimal / t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1, @@ -1517,7 +1519,7 @@ public static function provideCases(): iterable yield 't.col_int / t.col_bool' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int / t.col_bool FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -1565,7 +1567,7 @@ public static function provideCases(): iterable yield 't.col_decimal / t.col_bool' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal / t.col_bool FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -1581,7 +1583,7 @@ public static function provideCases(): iterable yield 't.col_decimal / t.col_bool (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_decimal / t.col_bool FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -1629,7 +1631,7 @@ public static function provideCases(): iterable yield 't.col_int / t.col_int_nullable' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int / t.col_int_nullable FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), 'pdo_pgsql' => self::intOrNull(), 'pgsql' => self::intOrNull(), @@ -1709,7 +1711,7 @@ public static function provideCases(): iterable yield '1 / 1' => [ 'data' => self::dataDefault(), 'select' => 'SELECT (1 / 1) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => self::int(), 'pgsql' => self::int(), @@ -1725,10 +1727,10 @@ public static function provideCases(): iterable yield '1 / 1.0' => [ 'data' => self::dataDefault(), 'select' => 'SELECT (1 / 1.0) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0000', 'sqliteResult' => 1.0, @@ -1743,8 +1745,8 @@ public static function provideCases(): iterable 'select' => 'SELECT (1 / 1e0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -1949,10 +1951,10 @@ public static function provideCases(): iterable yield 'COALESCE(t.col_decimal, t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT COALESCE(t.col_decimal, t.col_decimal) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -1997,11 +1999,11 @@ public static function provideCases(): iterable yield 't.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal FROM %s t', - 'mysql' => self::numericString(), - 'sqlite' => self::numericString(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), - 'mssql' => self::numericString(), + 'mysql' => self::numericString(false, true), + 'sqlite' => self::numericString(false, true), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), + 'mssql' => self::numericString(false, true), 'mysqlResult' => '0.1', 'sqliteResult' => '0.1', 'pdoPgsqlResult' => '0.1', @@ -2029,11 +2031,11 @@ public static function provideCases(): iterable yield 't.col_bigint' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_bigint FROM %s t', - 'mysql' => self::hasDbal4() ? self::int() : self::numericString(), - 'sqlite' => self::hasDbal4() ? self::int() : self::numericString(), - 'pdo_pgsql' => self::hasDbal4() ? self::int() : self::numericString(), - 'pgsql' => self::hasDbal4() ? self::int() : self::numericString(), - 'mssql' => self::hasDbal4() ? self::int() : self::numericString(), + 'mysql' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'sqlite' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'pdo_pgsql' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'pgsql' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'mssql' => self::hasDbal4() ? self::int() : self::numericString(true, true), 'mysqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', 'sqliteResult' => self::hasDbal4() ? 2147483648 : '2147483648', 'pdoPgsqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', @@ -2125,10 +2127,10 @@ public static function provideCases(): iterable yield 'AVG(t.col_decimal)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.10000', 'sqliteResult' => 0.1, @@ -2141,10 +2143,10 @@ public static function provideCases(): iterable yield 'AVG(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT AVG(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1.0, @@ -2173,10 +2175,10 @@ public static function provideCases(): iterable yield 'AVG(t.col_int)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '9.0000', 'sqliteResult' => 9.0, @@ -2189,7 +2191,7 @@ public static function provideCases(): iterable yield 'AVG(t.col_bool)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(t.col_bool) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -2221,10 +2223,10 @@ public static function provideCases(): iterable yield 'AVG(1)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(1) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0000', 'sqliteResult' => 1.0, @@ -2301,10 +2303,10 @@ public static function provideCases(): iterable yield 'AVG(1) + GROUP BY' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(1) FROM %s t GROUP BY t.col_int', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0000', 'sqliteResult' => 1.0, @@ -2317,10 +2319,10 @@ public static function provideCases(): iterable yield 'AVG(1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1.0, @@ -2333,10 +2335,10 @@ public static function provideCases(): iterable yield 'AVG(1e0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1.0, @@ -2349,10 +2351,10 @@ public static function provideCases(): iterable yield 'AVG(t.col_bigint)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(t.col_bigint) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '2147483648.0000', 'sqliteResult' => 2147483648.0, @@ -2429,10 +2431,10 @@ public static function provideCases(): iterable yield 'SUM(t.col_decimal)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.1', 'sqliteResult' => 0.1, @@ -2445,10 +2447,10 @@ public static function provideCases(): iterable yield 'SUM(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT SUM(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -2461,10 +2463,10 @@ public static function provideCases(): iterable yield 'SUM(t.col_int)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => '9', 'sqliteResult' => 9, @@ -2477,10 +2479,10 @@ public static function provideCases(): iterable yield '-SUM(t.col_int)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT -SUM(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => '-9', 'sqliteResult' => -9, @@ -2493,10 +2495,10 @@ public static function provideCases(): iterable yield '-SUM(t.col_int) + no data' => [ 'data' => self::dataNone(), 'select' => 'SELECT -SUM(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => null, 'sqliteResult' => null, @@ -2525,7 +2527,7 @@ public static function provideCases(): iterable yield 'SUM(t.col_bool)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(t.col_bool) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -2621,10 +2623,10 @@ public static function provideCases(): iterable yield 'SUM(1)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(1) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => 1, @@ -2637,10 +2639,10 @@ public static function provideCases(): iterable yield 'SUM(1) + GROUP BY' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(1) FROM %s t GROUP BY t.col_int', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), - 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => 1, @@ -2653,10 +2655,10 @@ public static function provideCases(): iterable yield 'SUM(1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1.0, @@ -2671,8 +2673,8 @@ public static function provideCases(): iterable 'select' => 'SELECT SUM(1e0) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -2685,10 +2687,10 @@ public static function provideCases(): iterable yield 'SUM(t.col_bigint)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(t.col_bigint) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => '2147483648', 'sqliteResult' => 2147483648, @@ -2749,10 +2751,10 @@ public static function provideCases(): iterable yield 'MAX(t.col_decimal)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT MAX(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.1', 'sqliteResult' => 0.1, @@ -2765,10 +2767,10 @@ public static function provideCases(): iterable yield 'MAX(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT MAX(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -2861,10 +2863,10 @@ public static function provideCases(): iterable yield "MAX('1')" => [ 'data' => self::dataDefault(), 'select' => "SELECT MAX('1') FROM %s t", - 'mysql' => self::numericStringOrNull(), - 'sqlite' => self::numericStringOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), + 'sqlite' => self::numericStringOrNull(true, true), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => '1', @@ -2877,10 +2879,10 @@ public static function provideCases(): iterable yield "MAX('1.0')" => [ 'data' => self::dataDefault(), 'select' => "SELECT MAX('1.0') FROM %s t", - 'mysql' => self::numericStringOrNull(), - 'sqlite' => self::numericStringOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), + 'sqlite' => self::numericStringOrNull(true, true), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => '1.0', @@ -2925,10 +2927,10 @@ public static function provideCases(): iterable yield 'MAX(1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT MAX(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1.0, @@ -2943,8 +2945,8 @@ public static function provideCases(): iterable 'select' => 'SELECT MAX(1e0) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -2989,10 +2991,10 @@ public static function provideCases(): iterable yield 'ABS(t.col_decimal)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT ABS(t.col_decimal) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.1', 'sqliteResult' => 0.1, @@ -3005,10 +3007,10 @@ public static function provideCases(): iterable yield 'ABS(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT ABS(t.col_decimal) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -3149,10 +3151,10 @@ public static function provideCases(): iterable yield 'ABS(1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT ABS(1.0) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1.0, @@ -3167,8 +3169,8 @@ public static function provideCases(): iterable 'select' => 'SELECT ABS(1e0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -3647,8 +3649,8 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(t.col_decimal) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -3823,8 +3825,8 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(1.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -4045,10 +4047,10 @@ public static function provideCases(): iterable yield 'COALESCE(SUM(t.col_int_nullable), 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(SUM(t.col_int_nullable), 0) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), - 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), 'mssql' => self::mixed(), 'mysqlResult' => '0', 'sqliteResult' => 0, @@ -4061,10 +4063,10 @@ public static function provideCases(): iterable yield 'COALESCE(SUM(ABS(t.col_int)), 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(SUM(ABS(t.col_int)), 0) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => '9', 'sqliteResult' => 9, @@ -4141,10 +4143,10 @@ public static function provideCases(): iterable yield "COALESCE(1, '1')" => [ 'data' => self::dataDefault(), 'select' => "SELECT COALESCE(1, '1') FROM %s t", - 'mysql' => self::numericString(), - 'sqlite' => TypeCombinator::union(self::int(), self::numericString()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'mysql' => self::numericString(true, true), + 'sqlite' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => 1, @@ -4159,8 +4161,8 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(1, 1.0) FROM %s t', 'mysql' => self::numericString(), 'sqlite' => TypeCombinator::union(self::int(), self::float()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -4189,10 +4191,10 @@ public static function provideCases(): iterable yield 'COALESCE(1.0, 1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(1.0, 1.0) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1.0, @@ -4207,8 +4209,8 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(1e0, 1.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -4223,8 +4225,8 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(1, 1.0, 1e0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1, @@ -4238,9 +4240,9 @@ public static function provideCases(): iterable 'data' => self::dataDefault(), 'select' => "SELECT COALESCE(1, 1.0, 1e0, '1') FROM %s t", 'mysql' => self::numericString(), - 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString(true, true)), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => 1, @@ -4305,8 +4307,8 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_float_nullable, 0.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => TypeCombinator::union(self::float(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::float(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::float(), self::numericString(false, true)), + 'pgsql' => TypeCombinator::union(self::float(), self::numericString(false, true)), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0.0, @@ -4755,7 +4757,7 @@ private function assertInferredResultMatchesExpected( $inferredFirstItemType = $inferredType->getFirstIterableValueType(); self::assertTrue( - $inferredFirstItemType->equals($expectedFirstItemType), + $expectedFirstItemType->accepts($inferredFirstItemType, true)->yes(), sprintf( "Mismatch between inferred result and expected type\n\nDriver: %s\nConfig: %s\nDataset: %s\nDQL: %s\nSQL: %s\nPHP: %s\nReal first result: %s\nFirst item inferred as: %s\nFirst item expected type: %s\n", $driver, @@ -4842,12 +4844,20 @@ private static function boolOrNull(): Type return TypeCombinator::addNull(new BooleanType()); } - private static function numericString(): Type + private static function numericString(bool $lowercase = false, bool $uppercase = false): Type { - return new IntersectionType([ + $types = [ new StringType(), new AccessoryNumericStringType(), - ]); + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } + + return new IntersectionType($types); } private static function string(): Type @@ -4855,12 +4865,9 @@ private static function string(): Type return new StringType(); } - private static function numericStringOrNull(): Type + private static function numericStringOrNull(bool $lowercase = false, bool $uppercase = false): Type { - return TypeCombinator::addNull(new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ])); + return TypeCombinator::addNull(self::numericString($lowercase, $uppercase)); } private static function int(): Type diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index be915cad..d29f86ec 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -13,6 +13,7 @@ use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; @@ -141,7 +142,7 @@ public static function getTestData(): iterable yield 'getResult(object), fields' => [ self::list(self::constantArray([ - [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('decimalColumn'), self::numericString(false, true)], [new ConstantStringType('floatColumn'), new FloatType()], ])), ' @@ -177,7 +178,7 @@ public static function getTestData(): iterable yield 'toIterable(object), fields' => [ new IterableType(new IntegerType(), self::constantArray([ - [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('decimalColumn'), self::numericString(false, true)], [new ConstantStringType('floatColumn'), new FloatType()], ])), ' @@ -309,7 +310,7 @@ private static function list(Type $values): Type return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $values)); } - private static function numericString(bool $lowercase = false): Type + private static function numericString(bool $lowercase = false, bool $uppercase = false): Type { $types = [ new StringType(), @@ -318,6 +319,9 @@ private static function numericString(bool $lowercase = false): Type if ($lowercase) { $types[] = new AccessoryLowercaseStringType(); } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } return new IntersectionType($types); } @@ -406,14 +410,14 @@ private static function stringifies(): bool private static function floatOrStringified(): Type { return self::stringifies() - ? self::numericString() + ? self::numericString(false, true) : new FloatType(); } private static function floatOrIntOrStringified(): Type { return self::stringifies() - ? self::numericString() + ? self::numericString(false, true) : TypeCombinator::union(new FloatType(), new IntegerType()); } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 905c5221..aa0ce591 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -17,6 +17,7 @@ use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantFloatType; @@ -377,7 +378,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(One::class)], - [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString(true, true)], [new ConstantStringType('intColumn'), new IntegerType()], ]) ), @@ -399,7 +400,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(Many::class)], - [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString(true, true)], [new ConstantStringType('intColumn'), new IntegerType()], ]) ), @@ -420,7 +421,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantStringType('one'), new ObjectType(One::class)], - [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString(true, true)], [new ConstantStringType('intColumn'), new IntegerType()], ]) ), @@ -530,7 +531,7 @@ public function getTestData(): iterable yield 'just root entity and scalars' => [ $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(One::class)], - [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString(true, true)], ]), ' SELECT o, o.id @@ -788,7 +789,7 @@ public function getTestData(): iterable [ new ConstantIntegerType(4), $this->stringifies() - ? $this->numericString() + ? $this->numericString(false, true) : TypeCombinator::union( new IntegerType(), new FloatType() @@ -1474,8 +1475,8 @@ public function getTestData(): iterable yield 'unary minus' => [ $this->constantArray([ [new ConstantStringType('minusInt'), $this->stringifies() ? new ConstantStringType('-1') : new ConstantIntegerType(-1)], - [new ConstantStringType('minusFloat'), $this->stringifies() ? $this->numericString() : new ConstantFloatType(-0.1)], - [new ConstantStringType('minusIntRange'), $this->stringifies() ? $this->numericString(true) : IntegerRangeType::fromInterval(null, 0)], + [new ConstantStringType('minusFloat'), $this->stringifies() ? $this->numericString(false, true) : new ConstantFloatType(-0.1)], + [new ConstantStringType('minusIntRange'), $this->stringifies() ? $this->numericString(true, true) : IntegerRangeType::fromInterval(null, 0)], ]), ' SELECT -1 as minusInt, @@ -1516,7 +1517,7 @@ private function yieldConditionalDataset(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], [ new ConstantIntegerType(2), @@ -1524,7 +1525,7 @@ private function yieldConditionalDataset(): iterable ], [ new ConstantIntegerType(3), - $this->numericString(), + $this->numericString(true, true), ], ]), ' @@ -1623,7 +1624,7 @@ private function constantArray(array $elements): Type return $builder->getArray(); } - private function numericString(bool $lowercase = false): Type + private function numericString(bool $lowercase = false, bool $uppercase = false): Type { $types = [ new StringType(), @@ -1632,6 +1633,9 @@ private function numericString(bool $lowercase = false): Type if ($lowercase) { $types[] = new AccessoryLowercaseStringType(); } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } return new IntersectionType($types); } @@ -1679,21 +1683,21 @@ private function stringifies(): bool private function intOrStringified(): Type { return $this->stringifies() - ? $this->numericString(true) + ? $this->numericString(true, true) : new IntegerType(); } private function uintOrStringified(): Type { return $this->stringifies() - ? $this->numericString(true) + ? $this->numericString(true, true) : $this->uint(); } private function floatOrStringified(): Type { return $this->stringifies() - ? $this->numericString() + ? $this->numericString(false, true) : new FloatType(); } From 25ab1ec770c3a9227da37964da60f5217079de4d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 20 Nov 2024 16:58:28 +0100 Subject: [PATCH 117/160] Test projects only in 2.0.x-dev --- .github/workflows/test-projects.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-projects.yml b/.github/workflows/test-projects.yml index d33dcf6f..2b85f70e 100644 --- a/.github/workflows/test-projects.yml +++ b/.github/workflows/test-projects.yml @@ -5,7 +5,7 @@ name: "Test projects" on: push: branches: - - "1.5.x" + - "2.0.x" jobs: test-projects: From 231d3f795ed5ef54c98961fd3958868cbe091207 Mon Sep 17 00:00:00 2001 From: Vlasta Neubauer Date: Mon, 2 Dec 2024 10:38:49 +0100 Subject: [PATCH 118/160] Mark all result methods in AbstractQuery as impure --- stubs/ORM/AbstractQuery.stub | 71 ++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/stubs/ORM/AbstractQuery.stub b/stubs/ORM/AbstractQuery.stub index 1b970ad0..25f7e320 100644 --- a/stubs/ORM/AbstractQuery.stub +++ b/stubs/ORM/AbstractQuery.stub @@ -4,6 +4,7 @@ namespace Doctrine\ORM; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; /** * @template-covariant TKey The type of column used in indexBy @@ -12,21 +13,75 @@ use Doctrine\ORM\NonUniqueResultException; abstract class AbstractQuery { + public const HYDRATE_OBJECT = 1; + /** * @param ArrayCollection|array $parameters * @return static */ public function setParameters($parameters) { - } - /** - * @return bool|float|int|string|null - * - * @throws NoResultException - * @throws NonUniqueResultException - */ - public function getSingleScalarResult(); + /** + * @phpstan-impure + * @param string|AbstractQuery::HYDRATE_* $hydrationMode + */ + public function getResult($hydrationMode = self::HYDRATE_OBJECT): mixed + { + } + + /** + * @phpstan-impure + * @return mixed[] + */ + public function getArrayResult(): array + { + } + + /** + * @phpstan-impure + * @return mixed[] + */ + public function getSingleColumnResult(): array + { + } + + /** + * @phpstan-impure + * @return mixed[] + */ + public function getScalarResult(): array + { + } + + /** + * @phpstan-impure + * @param string|AbstractQuery::HYDRATE_*|null $hydrationMode + * @throws NonUniqueResultException + */ + public function getOneOrNullResult($hydrationMode = null): mixed + { + } + + /** + * @phpstan-impure + * @param string|AbstractQuery::HYDRATE_*|null $hydrationMode + * @throws NonUniqueResultException + * @throws NoResultException + */ + public function getSingleResult($hydrationMode = null): mixed + { + } + + /** + * @phpstan-impure + * @return bool|float|int|string|null + * @throws NoResultException + * @throws NonUniqueResultException + */ + public function getSingleScalarResult() + { + } } From 815d5ae40ffe3a7788df1cd557e30e474f3aba1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 28 Jan 2025 10:26:47 +0100 Subject: [PATCH 119/160] Update LICENSE --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 7c0f2b7b..e5f34e60 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2016 Ondřej Mirtes +Copyright (c) 2025 PHPStan s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From a61a04a361b60014ec04881ccb87252d3bf02e94 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 3 Mar 2025 10:22:39 +0100 Subject: [PATCH 120/160] EntityRepository.stub fix nullability in findOneBy --- stubs/EntityRepository.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/EntityRepository.stub b/stubs/EntityRepository.stub index 4dcde8bf..784b14c8 100644 --- a/stubs/EntityRepository.stub +++ b/stubs/EntityRepository.stub @@ -42,7 +42,7 @@ class EntityRepository implements ObjectRepository * @phpstan-param array|null $orderBy * @phpstan-return TEntityClass|null */ - public function findOneBy(array $criteria, array $orderBy = null); + public function findOneBy(array $criteria, ?array $orderBy = null); /** * @phpstan-return class-string From baf73e368f2cbdc13ced4c42df3269ba577621a0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 18 Mar 2025 11:17:26 +0100 Subject: [PATCH 121/160] Introduce phpstan-deprecation-rules --- composer.json | 1 + phpstan-baseline-deprecations.neon | 127 ++++++++++++++++++ phpstan-baseline-orm-3.neon | 99 ++++++++++++++ phpstan.neon | 2 + .../IsEmptyTypeSpecifyingExtension.php | 7 +- 5 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 phpstan-baseline-deprecations.neon diff --git a/composer.json b/composer.json index daedadb0..4ce656ed 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", diff --git a/phpstan-baseline-deprecations.neon b/phpstan-baseline-deprecations.neon new file mode 100644 index 00000000..bd96685e --- /dev/null +++ b/phpstan-baseline-deprecations.neon @@ -0,0 +1,127 @@ +parameters: + ignoreErrors: + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\DBAL\\Types\\ArrayType\: + Use \{@link JsonType\} instead\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Type/Doctrine/Descriptors/ArrayType.php + + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\DBAL\\Types\\ObjectType\: + Use \{@link JsonType\} instead\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Type/Doctrine/Descriptors/ObjectType.php + + - + message: ''' + #^Call to deprecated method getSchemaManager\(\) of class Doctrine\\DBAL\\Connection\: + Use \{@see createSchemaManager\(\)\} instead\.$# + ''' + identifier: method.deprecated + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\DBAL\\Types\\ArrayType\: + Use \{@link JsonType\} instead\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php + + - + message: ''' + #^Call to deprecated method create\(\) of class Doctrine\\ORM\\EntityManager\: + Use \{@see DriverManager\:\:getConnection\(\)\} to bootstrap the connection and call the constructor\.$# + ''' + identifier: staticMethod.deprecated + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Classes/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/DoctrineIntegration/ORM/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Rules/DeadCode/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Rules/Properties/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Type/Doctrine/DBAL/mysqli.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Type/Doctrine/DBAL/pdo.php diff --git a/phpstan-baseline-orm-3.neon b/phpstan-baseline-orm-3.neon index 92e5f87c..ae5e8dfd 100644 --- a/phpstan-baseline-orm-3.neon +++ b/phpstan-baseline-orm-3.neon @@ -54,3 +54,102 @@ parameters: message: "#^Parameter \\#2 \\$className of static method Doctrine\\\\DBAL\\\\Types\\\\Type\\:\\:addType\\(\\) expects class\\-string\\, string given\\.$#" count: 1 path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php + + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Classes/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/DoctrineIntegration/ORM/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Rules/DeadCode/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Rules/Properties/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Type/Doctrine/DBAL/mysqli.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Type/Doctrine/DBAL/pdo.php diff --git a/phpstan.neon b/phpstan.neon index 8009fd82..efbf455d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,9 +2,11 @@ includes: - extension.neon - rules.neon - phpstan-baseline.neon + - phpstan-baseline-deprecations.neon - phpstan-baseline-dbal-3.neon - compatibility/orm-3-baseline.php - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon - phar://phpstan.phar/conf/bleedingEdge.neon diff --git a/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php b/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php index 32765e79..9a177304 100644 --- a/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php +++ b/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php @@ -43,11 +43,8 @@ public function isMethodSupported( TypeSpecifierContext $context ): bool { - return ( - $methodReflection->getDeclaringClass()->getName() === $this->collectionClass - || $methodReflection->getDeclaringClass()->isSubclassOf($this->collectionClass) - ) - && $methodReflection->getName() === self::IS_EMPTY_METHOD_NAME; + return $methodReflection->getDeclaringClass()->is($this->collectionClass) + && $methodReflection->getName() === self::IS_EMPTY_METHOD_NAME; } public function specifyTypes( From 4a0d9cb9f9216d040e8d10f00fda2d25a995235e Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 24 Mar 2025 10:19:24 +0100 Subject: [PATCH 122/160] Adapt for recent PHPStan --- composer.json | 2 +- .../DBAL/RowCountMethodDynamicReturnTypeExtension.php | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 8bd0d8f0..5c9b02bb 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12.12" + "phpstan/phpstan": "^1.12.23" }, "conflict": { "doctrine/collections": "<1.0", diff --git a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php index 8fe17348..c754a41b 100644 --- a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php @@ -66,9 +66,6 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } $resultClass = $this->getResultClass($driver); - if ($resultClass === null) { - return null; - } if (!$this->reflectionProvider->hasClass($resultClass)) { return null; @@ -87,9 +84,9 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method /** * @param DriverDetector::* $driver - * @return class-string|null + * @return class-string */ - private function getResultClass(string $driver): ?string + private function getResultClass(string $driver): string { switch ($driver) { case DriverDetector::IBM_DB2: @@ -111,8 +108,6 @@ private function getResultClass(string $driver): ?string case DriverDetector::SQLSRV: return 'Doctrine\DBAL\Driver\SQLSrv\Result'; } - - return null; } } From 6d00a6c08c934dd02b9d77b1ec32518378922e63 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 02:32:35 +0000 Subject: [PATCH 123/160] Update metcalfc/changelog-generator action to v4.5.0 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1a669a9..be6cad08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.3.1 + uses: metcalfc/changelog-generator@v4.5.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From ce89f09ef294aae03e08133313014d8d542d835a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 02:34:18 +0000 Subject: [PATCH 124/160] Update metcalfc/changelog-generator action to v4.6.2 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be6cad08..b8c96d48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.5.0 + uses: metcalfc/changelog-generator@v4.6.2 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 6317ec1d1ba6cd4ad6ee1e78f17ece1d1eb7f3de Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 19 Apr 2025 22:03:22 +0200 Subject: [PATCH 125/160] Fix build --- composer.json | 4 ++-- phpstan-baseline.neon | 54 +++++++++++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/composer.json b/composer.json index 1fd25ef6..24ea4782 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.10" + "phpstan/phpstan": "^2.1.13" }, "conflict": { "doctrine/collections": "<1.0", @@ -31,7 +31,7 @@ "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.2", "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cc1e2048..73d72b45 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,71 +1,85 @@ parameters: ignoreErrors: - - message: "#^Calling PHPStan\\\\Type\\\\ParserNodeTypeToPHPStanType\\:\\:resolve\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#" - count: 1 - path: src/Rules/Doctrine/ORM/EntityColumnRule.php + message: '#^Call to internal method Doctrine\\DBAL\\Connection\:\:getParams\(\) from outside its root namespace Doctrine\.$#' + identifier: method.internal + count: 2 + path: src/Doctrine/Driver/DriverDetector.php - - message: "#^Calling PHPStan\\\\Type\\\\TypehintHelper\\:\\:decideType\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#" + message: '#^Calling PHPStan\\Type\\TypehintHelper\:\:decideType\(\) is not covered by backward compatibility promise\. The method might change in a minor PHPStan version\.$#' + identifier: phpstanApi.method count: 1 path: src/Rules/Doctrine/ORM/EntityColumnRule.php - - message: "#^Calling PHPStan\\\\Type\\\\ParserNodeTypeToPHPStanType\\:\\:resolve\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#" - count: 1 - path: src/Rules/Doctrine/ORM/EntityRelationRule.php - - - - message: "#^Calling PHPStan\\\\Type\\\\TypehintHelper\\:\\:decideType\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#" + message: '#^Calling PHPStan\\Type\\TypehintHelper\:\:decideType\(\) is not covered by backward compatibility promise\. The method might change in a minor PHPStan version\.$#' + identifier: phpstanApi.method count: 1 path: src/Rules/Doctrine/ORM/EntityRelationRule.php - - message: "#^PHPDoc tag @var with type class\\-string is not subtype of native type 'Doctrine\\\\\\\\Bundle…'\\.$#" + message: '#^PHPDoc tag @var with type class\-string is not subtype of native type ''Doctrine\\\\Bundle…''\.$#' + identifier: varTag.nativeType count: 1 path: src/Stubs/Doctrine/StubFilesExtensionLoader.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Classes\\\\InstantiationRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Classes\\InstantiationRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Call to internal method PHPUnit\\Framework\\TestCase\:\:dataName\(\) from outside its root namespace PHPUnit\.$#' + identifier: method.internal + count: 14 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + message: '#^Accessing PHPStan\\Rules\\DeadCode\\UnusedPrivatePropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Methods\\\\CallMethodsRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Methods\\CallMethodsRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Doctrine/ORM/MagicRepositoryMethodCallRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Exceptions\\\\CatchWithUnthrownExceptionRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Exceptions\\CatchWithUnthrownExceptionRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Exceptions\\\\TooWideMethodThrowTypeRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Exceptions\\TooWideMethodThrowTypeRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\DeadCode\\UnusedPrivatePropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\DeadCode\\UnusedPrivatePropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Properties/MissingGedmoPropertyAssignRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Properties\\\\MissingReadOnlyByPhpDocPropertyAssignRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Properties\\MissingReadOnlyByPhpDocPropertyAssignRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Properties\\\\MissingReadOnlyPropertyAssignRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Properties\\MissingReadOnlyPropertyAssignRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php From 93d1307c50079c9d8447d72f73ca0b888cb75a07 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 24 Apr 2025 17:49:40 +0200 Subject: [PATCH 126/160] Fix build --- phpstan-baseline.neon | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 73d72b45..15848bab 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -24,6 +24,12 @@ parameters: count: 1 path: src/Stubs/Doctrine/StubFilesExtensionLoader.php + - + message: '#^Parameter references internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional in its type\.$#' + identifier: parameter.internalInterface + count: 2 + path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + - message: '#^Accessing PHPStan\\Rules\\Classes\\InstantiationRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' identifier: phpstanApi.classConstant From 8e76755816cece825b91c96034f969f673864593 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 24 Apr 2025 17:47:35 +0200 Subject: [PATCH 127/160] Use stable PHP 8.4 with xdebug in docker image --- tests/Platform/docker/Dockerfile84 | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/Platform/docker/Dockerfile84 b/tests/Platform/docker/Dockerfile84 index 81ac9834..30b47ed2 100644 --- a/tests/Platform/docker/Dockerfile84 +++ b/tests/Platform/docker/Dockerfile84 @@ -1,4 +1,4 @@ -FROM php:8.4.0beta4-cli +FROM php:8.4-cli # MSSQL RUN apt update \ @@ -12,13 +12,8 @@ RUN apt update \ && pecl install pdo_sqlsrv \ && docker-php-ext-enable sqlsrv pdo_sqlsrv -RUN set -ex \ - && apt update \ - && apt install -y bash zip libpq-dev libsqlite3-dev \ - && pecl install xdebug-3.4 mongodb \ - && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ - && docker-php-ext-install pdo mysqli pgsql pdo_mysql pdo_pgsql pdo_sqlite \ - && docker-php-ext-enable mongodb # TODO xdebug not yet supported here +COPY ./docker-setup.sh /opt/src/scripts/setup.sh +RUN /opt/src/scripts/setup.sh COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer From b22695147e835fdcd117624a7f9ad2c943c80af7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 25 Apr 2025 22:36:15 +0200 Subject: [PATCH 128/160] Fix build --- phpstan-baseline.neon | 50 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 15848bab..01f874ed 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6,6 +6,12 @@ parameters: count: 2 path: src/Doctrine/Driver/DriverDetector.php + - + message: '#^Access to constant on deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\.$#' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + - message: '#^Calling PHPStan\\Type\\TypehintHelper\:\:decideType\(\) is not covered by backward compatibility promise\. The method might change in a minor PHPStan version\.$#' identifier: phpstanApi.method @@ -25,11 +31,47 @@ parameters: path: src/Stubs/Doctrine/StubFilesExtensionLoader.php - - message: '#^Parameter references internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional in its type\.$#' + message: '#^Catching deprecated class Doctrine\\Common\\CommonException\.$#' + identifier: catch.deprecatedClass + count: 1 + path: src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php + + - + message: '#^Catching deprecated class Doctrine\\ORM\\ORMException\.$#' + identifier: catch.deprecatedClass + count: 1 + path: src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php + + - + message: '#^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ArrayType\.$#' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Type/Doctrine/Descriptors/ArrayType.php + + - + message: '#^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ObjectType\.$#' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Type/Doctrine/Descriptors/ObjectType.php + + - + message: '#^Parameter \$condExpr references internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional in its type\.$#' identifier: parameter.internalInterface count: 2 path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + - + message: '#^Catching deprecated class Doctrine\\Common\\CommonException\.$#' + identifier: catch.deprecatedClass + count: 1 + path: src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php + + - + message: '#^Catching deprecated class Doctrine\\ORM\\ORMException\.$#' + identifier: catch.deprecatedClass + count: 1 + path: src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php + - message: '#^Accessing PHPStan\\Rules\\Classes\\InstantiationRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' identifier: phpstanApi.classConstant @@ -48,6 +90,12 @@ parameters: count: 1 path: tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php + - + message: '#^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ArrayType\.$#' + identifier: classConstant.deprecatedClass + count: 1 + path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php + - message: '#^Accessing PHPStan\\Rules\\Methods\\CallMethodsRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' identifier: phpstanApi.classConstant From 1a479e5d212bb5f9191a164d28df7e89c1bb5cc2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Apr 2025 11:30:51 +0200 Subject: [PATCH 129/160] Fix build --- phpstan-baseline.neon | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 01f874ed..38a63aef 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -55,9 +55,15 @@ parameters: path: src/Type/Doctrine/Descriptors/ObjectType.php - - message: '#^Parameter \$condExpr references internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional in its type\.$#' + message: '#^Parameter \$condExpr of method PHPStan\\Type\\Doctrine\\Query\\QueryResultTypeWalker\:\:walkConditionalExpression\(\) has typehint with internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional\.$#' identifier: parameter.internalInterface - count: 2 + count: 1 + path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + + - + message: '#^Parameter \$condExpr of method PHPStan\\Type\\Doctrine\\Query\\QueryResultTypeWalker\:\:walkJoinAssociationDeclaration\(\) has typehint with internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional\.$#' + identifier: parameter.internalInterface + count: 1 path: src/Type/Doctrine/Query/QueryResultTypeWalker.php - From 913dac215f5f5ba8ba800780243461f4a9859e31 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Apr 2025 11:36:04 +0200 Subject: [PATCH 130/160] Fix test-projects.yml --- .github/workflows/test-projects.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-projects.yml b/.github/workflows/test-projects.yml index 2b85f70e..6f3a7fb7 100644 --- a/.github/workflows/test-projects.yml +++ b/.github/workflows/test-projects.yml @@ -26,4 +26,4 @@ jobs: token: ${{ secrets.REPO_ACCESS_TOKEN }} repository: "${{ matrix.repository }}" event-type: test_phpstan - client-payload: '{"ref": "1.11.x"}' + client-payload: '{"ref": "${{ github.ref_name }}"}' From 3bf5dc8cf97dd1fd8b2d8ba160b400fc7afeee2f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Apr 2025 22:06:31 +0200 Subject: [PATCH 131/160] Fix build --- phpstan-baseline.neon | 112 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 8 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 38a63aef..64b1a032 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -7,11 +7,23 @@ parameters: path: src/Doctrine/Driver/DriverDetector.php - - message: '#^Access to constant on deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\.$#' + message: ''' + #^Access to constant on deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' identifier: classConstant.deprecatedClass count: 1 path: src/Doctrine/Mapping/ClassMetadataFactory.php + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + - message: '#^Calling PHPStan\\Type\\TypehintHelper\:\:decideType\(\) is not covered by backward compatibility promise\. The method might change in a minor PHPStan version\.$#' identifier: phpstanApi.method @@ -31,25 +43,37 @@ parameters: path: src/Stubs/Doctrine/StubFilesExtensionLoader.php - - message: '#^Catching deprecated class Doctrine\\Common\\CommonException\.$#' + message: ''' + #^Catching deprecated class Doctrine\\Common\\CommonException\: + The doctrine/common package is deprecated, please use specific packages and their exceptions instead\.$# + ''' identifier: catch.deprecatedClass count: 1 path: src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php - - message: '#^Catching deprecated class Doctrine\\ORM\\ORMException\.$#' + message: ''' + #^Catching deprecated class Doctrine\\ORM\\ORMException\: + Use Doctrine\\ORM\\Exception\\ORMException for catch and instanceof$# + ''' identifier: catch.deprecatedClass count: 1 path: src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php - - message: '#^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ArrayType\.$#' + message: ''' + #^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ArrayType\: + Use \{@link JsonType\} instead\.$# + ''' identifier: classConstant.deprecatedClass count: 1 path: src/Type/Doctrine/Descriptors/ArrayType.php - - message: '#^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ObjectType\.$#' + message: ''' + #^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ObjectType\: + Use \{@link JsonType\} instead\.$# + ''' identifier: classConstant.deprecatedClass count: 1 path: src/Type/Doctrine/Descriptors/ObjectType.php @@ -67,13 +91,19 @@ parameters: path: src/Type/Doctrine/Query/QueryResultTypeWalker.php - - message: '#^Catching deprecated class Doctrine\\Common\\CommonException\.$#' + message: ''' + #^Catching deprecated class Doctrine\\Common\\CommonException\: + The doctrine/common package is deprecated, please use specific packages and their exceptions instead\.$# + ''' identifier: catch.deprecatedClass count: 1 path: src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php - - message: '#^Catching deprecated class Doctrine\\ORM\\ORMException\.$#' + message: ''' + #^Catching deprecated class Doctrine\\ORM\\ORMException\: + Use Doctrine\\ORM\\Exception\\ORMException for catch and instanceof$# + ''' identifier: catch.deprecatedClass count: 1 path: src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php @@ -84,12 +114,39 @@ parameters: count: 1 path: tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Classes/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/DoctrineIntegration/ORM/entity-manager.php + - message: '#^Call to internal method PHPUnit\\Framework\\TestCase\:\:dataName\(\) from outside its root namespace PHPUnit\.$#' identifier: method.internal count: 14 path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + - message: '#^Accessing PHPStan\\Rules\\DeadCode\\UnusedPrivatePropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' identifier: phpstanApi.classConstant @@ -97,7 +154,19 @@ parameters: path: tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php - - message: '#^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ArrayType\.$#' + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Rules/DeadCode/entity-manager.php + + - + message: ''' + #^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ArrayType\: + Use \{@link JsonType\} instead\.$# + ''' identifier: classConstant.deprecatedClass count: 1 path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -143,3 +212,30 @@ parameters: identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Rules/Properties/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/mysqli.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/pdo.php From b1209a31a7ffdfc31cd37142a03e172a97827265 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 26 Apr 2025 22:11:07 +0200 Subject: [PATCH 132/160] Fix test-projects.yml --- .github/workflows/test-projects.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-projects.yml b/.github/workflows/test-projects.yml index 6f3a7fb7..cebc3431 100644 --- a/.github/workflows/test-projects.yml +++ b/.github/workflows/test-projects.yml @@ -26,4 +26,4 @@ jobs: token: ${{ secrets.REPO_ACCESS_TOKEN }} repository: "${{ matrix.repository }}" event-type: test_phpstan - client-payload: '{"ref": "${{ github.ref_name }}"}' + client-payload: '{"ref": "2.1.x"}' From 4497663eb17b9d29211830df5aceaa3a4d256a35 Mon Sep 17 00:00:00 2001 From: Edvin Malinovskis <507466+nCrazed@users.noreply.github.com> Date: Thu, 13 Mar 2025 20:58:24 +0000 Subject: [PATCH 133/160] Mark nullable EntityRepository methods as impure The result of these functions depends on the underlying database state which can change between multiple invocations. For example: Calling find() or findOneBy() and asserting the result to be null as a precondition of a test will generate false errors later in the test when the underlying record is added and the method is called again See #550 --- stubs/EntityRepository.stub | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stubs/EntityRepository.stub b/stubs/EntityRepository.stub index 784b14c8..bdc11eca 100644 --- a/stubs/EntityRepository.stub +++ b/stubs/EntityRepository.stub @@ -20,6 +20,7 @@ class EntityRepository implements ObjectRepository * @phpstan-param int|null $lockMode * @phpstan-param int|null $lockVersion * @phpstan-return TEntityClass|null + * @phpstan-impure */ public function find($id, $lockMode = null, $lockVersion = null); @@ -41,6 +42,7 @@ class EntityRepository implements ObjectRepository * @phpstan-param array $criteria The criteria. * @phpstan-param array|null $orderBy * @phpstan-return TEntityClass|null + * @phpstan-impure */ public function findOneBy(array $criteria, ?array $orderBy = null); From a1a9efb64708580a9d8b0d150340f7777d2b8aa0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 16 May 2025 11:36:12 +0200 Subject: [PATCH 134/160] Fix build --- phpstan-baseline.neon | 78 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 64b1a032..d635d53e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -15,6 +15,15 @@ parameters: count: 1 path: src/Doctrine/Mapping/ClassMetadataFactory.php + - + message: ''' + #^Call to method __construct\(\) of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: method.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + - message: ''' #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: @@ -114,6 +123,15 @@ parameters: count: 1 path: tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php + - + message: ''' + #^Call to method __construct\(\) of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Classes/entity-manager.php + - message: ''' #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: @@ -123,6 +141,15 @@ parameters: count: 1 path: tests/Classes/entity-manager.php + - + message: ''' + #^Call to method __construct\(\) of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/DoctrineIntegration/ORM/entity-manager.php + - message: ''' #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: @@ -138,6 +165,15 @@ parameters: count: 14 path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + - + message: ''' + #^Call to method __construct\(\) of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + - message: ''' #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: @@ -147,12 +183,27 @@ parameters: count: 1 path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + - + message: '#^Call to internal method Doctrine\\DBAL\\Driver\\PDO\\Connection\:\:__construct\(\) from outside its root namespace Doctrine\.$#' + identifier: method.internal + count: 1 + path: tests/Platform/UnknownDriver.php + - message: '#^Accessing PHPStan\\Rules\\DeadCode\\UnusedPrivatePropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' identifier: phpstanApi.classConstant count: 1 path: tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php + - + message: ''' + #^Call to method __construct\(\) of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Rules/DeadCode/entity-manager.php + - message: ''' #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: @@ -213,6 +264,15 @@ parameters: count: 1 path: tests/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php + - + message: ''' + #^Call to method __construct\(\) of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Rules/Properties/entity-manager.php + - message: ''' #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: @@ -222,6 +282,15 @@ parameters: count: 1 path: tests/Rules/Properties/entity-manager.php + - + message: ''' + #^Call to method __construct\(\) of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/mysqli.php + - message: ''' #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: @@ -231,6 +300,15 @@ parameters: count: 1 path: tests/Type/Doctrine/DBAL/mysqli.php + - + message: ''' + #^Call to method __construct\(\) of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/pdo.php + - message: ''' #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: From adf18268e72cb828bddd2f61dda73013e379b701 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 17 Jul 2025 13:57:02 +0200 Subject: [PATCH 135/160] Add missing descriptors for `SmallFloatType` and `EnumType` --- extension.neon | 6 ++ phpstan-baseline-dbal-4.neon | 33 +++++++ phpstan-baseline.neon | 6 ++ phpstan.neon | 1 + src/Type/Doctrine/Descriptors/EnumType.php | 31 +++++++ .../Doctrine/Descriptors/SmallFloatType.php | 13 +++ .../Doctrine/Query/QueryResultTypeWalker.php | 91 +++++++++++++++---- .../Query/QueryResultTypeWalkerTest.php | 34 +++++++ .../EntitiesDbal42/Dbal4Entity.php | 36 ++++++++ .../data/QueryResult/entity-manager.php | 10 ++ 10 files changed, 244 insertions(+), 17 deletions(-) create mode 100644 phpstan-baseline-dbal-4.neon create mode 100644 src/Type/Doctrine/Descriptors/EnumType.php create mode 100644 src/Type/Doctrine/Descriptors/SmallFloatType.php create mode 100644 tests/Type/Doctrine/data/QueryResult/EntitiesDbal42/Dbal4Entity.php diff --git a/extension.neon b/extension.neon index 93102dc0..803ffaed 100644 --- a/extension.neon +++ b/extension.neon @@ -353,6 +353,9 @@ services: - class: PHPStan\Type\Doctrine\Descriptors\DecimalType tags: [phpstan.doctrine.typeDescriptor] + - + class: PHPStan\Type\Doctrine\Descriptors\EnumType + tags: [phpstan.doctrine.typeDescriptor] - class: PHPStan\Type\Doctrine\Descriptors\FloatType tags: [phpstan.doctrine.typeDescriptor] @@ -374,6 +377,9 @@ services: - class: PHPStan\Type\Doctrine\Descriptors\SimpleArrayType tags: [phpstan.doctrine.typeDescriptor] + - + class: PHPStan\Type\Doctrine\Descriptors\SmallFloatType + tags: [phpstan.doctrine.typeDescriptor] - class: PHPStan\Type\Doctrine\Descriptors\SmallIntType tags: [phpstan.doctrine.typeDescriptor] diff --git a/phpstan-baseline-dbal-4.neon b/phpstan-baseline-dbal-4.neon new file mode 100644 index 00000000..24536b91 --- /dev/null +++ b/phpstan-baseline-dbal-4.neon @@ -0,0 +1,33 @@ +parameters: + ignoreErrors: + - + message: '#^Class Doctrine\\DBAL\\Types\\EnumType not found\.$#' + identifier: class.notFound + count: 1 + path: src/Type/Doctrine/Descriptors/EnumType.php + + - + message: '#^Method PHPStan\\Type\\Doctrine\\Descriptors\\EnumType\:\:getType\(\) should return class\-string\ but returns string\.$#' + identifier: return.type + count: 1 + path: src/Type/Doctrine/Descriptors/EnumType.php + + - + message: '#^Class Doctrine\\DBAL\\Types\\SmallFloatType not found\.$#' + identifier: class.notFound + count: 1 + path: src/Type/Doctrine/Descriptors/SmallFloatType.php + + - + message: '#^Method PHPStan\\Type\\Doctrine\\Descriptors\\SmallFloatType\:\:getType\(\) should return class\-string\ but returns string\.$#' + identifier: return.type + count: 1 + path: src/Type/Doctrine/Descriptors/SmallFloatType.php + + - + message: '#^Class Doctrine\\DBAL\\Types\\EnumType not found\.$#' + identifier: class.notFound + count: 1 + path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d635d53e..cebcd4e2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -317,3 +317,9 @@ parameters: identifier: new.deprecatedClass count: 1 path: tests/Type/Doctrine/DBAL/pdo.php + + - + message: '#^Parameter references internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional in its type\.$#' + identifier: parameter.internalInterface + count: 2 + path: src/Type/Doctrine/Query/QueryResultTypeWalker.php diff --git a/phpstan.neon b/phpstan.neon index efbf455d..ae9f2df6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,6 +4,7 @@ includes: - phpstan-baseline.neon - phpstan-baseline-deprecations.neon - phpstan-baseline-dbal-3.neon + - phpstan-baseline-dbal-4.neon - compatibility/orm-3-baseline.php - vendor/phpstan/phpstan-strict-rules/rules.neon - vendor/phpstan/phpstan-deprecation-rules/rules.neon diff --git a/src/Type/Doctrine/Descriptors/EnumType.php b/src/Type/Doctrine/Descriptors/EnumType.php new file mode 100644 index 00000000..cc10d3f8 --- /dev/null +++ b/src/Type/Doctrine/Descriptors/EnumType.php @@ -0,0 +1,31 @@ +type) { case AST\PathExpression::TYPE_STATE_FIELD: - [$typeName, $enumType] = $this->getTypeOfField($class, $fieldName); + [$typeName, $enumType, $enumValues] = $this->getTypeOfField($class, $fieldName); $nullable = $this->isQueryComponentNullable($dqlAlias) || $class->isNullable($fieldName) || $this->hasAggregateWithoutGroupBy(); - $fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $nullable); + $fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $enumValues, $nullable); return $this->marshalType($fieldType); @@ -326,12 +329,12 @@ public function walkPathExpression($pathExpr): string } $targetFieldName = $identifierFieldNames[0]; - [$typeName, $enumType] = $this->getTypeOfField($targetClass, $targetFieldName); + [$typeName, $enumType, $enumValues] = $this->getTypeOfField($targetClass, $targetFieldName); $nullable = ($joinColumn['nullable'] ?? true) || $this->hasAggregateWithoutGroupBy(); - $fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $nullable); + $fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $enumValues, $nullable); return $this->marshalType($fieldType); @@ -685,7 +688,7 @@ public function walkFunction($function): string return $this->marshalType(new MixedType()); } - [$typeName, $enumType] = $this->getTypeOfField($targetClass, $targetFieldName); + [$typeName, $enumType, $enumValues] = $this->getTypeOfField($targetClass, $targetFieldName); if (!isset($assoc['joinColumns'])) { return $this->marshalType(new MixedType()); @@ -708,7 +711,7 @@ public function walkFunction($function): string || $this->isQueryComponentNullable($dqlAlias) || $this->hasAggregateWithoutGroupBy(); - $fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $nullable); + $fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $enumValues, $nullable); return $this->marshalType($fieldType); @@ -1206,13 +1209,13 @@ public function walkSelectExpression($selectExpression): string assert(array_key_exists('metadata', $qComp)); $class = $qComp['metadata']; - [$typeName, $enumType] = $this->getTypeOfField($class, $fieldName); + [$typeName, $enumType, $enumValues] = $this->getTypeOfField($class, $fieldName); $nullable = $this->isQueryComponentNullable($dqlAlias) || $class->isNullable($fieldName) || $this->hasAggregateWithoutGroupBy(); - $type = $this->resolveDoctrineType($typeName, $enumType, $nullable); + $type = $this->resolveDoctrineType($typeName, $enumType, $enumValues, $nullable); $this->typeBuilder->addScalar($resultAlias, $type); @@ -1235,11 +1238,12 @@ public function walkSelectExpression($selectExpression): string if ( $expr instanceof TypedExpression && !$expr->getReturnType() instanceof DbalStringType // StringType is no-op, so using TypedExpression with that does nothing + && !$expr->getReturnType() instanceof DbalEnumType // EnumType is also no-op ) { $dbalTypeName = DbalType::getTypeRegistry()->lookupName($expr->getReturnType()); $type = TypeCombinator::intersect( // e.g. count is typed as int, but we infer int<0, max> $type, - $this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type)), + $this->resolveDoctrineType($dbalTypeName, null, null, TypeCombinator::containsNull($type)), ); if ($this->hasAggregateWithoutGroupBy() && !$expr instanceof AST\Functions\CountFunction) { @@ -1997,7 +2001,7 @@ private function isQueryComponentNullable(string $dqlAlias): bool /** * @param ClassMetadata $class - * @return array{string, ?class-string} Doctrine type name and enum type of field + * @return array{string, ?class-string, ?list} Doctrine type name, enum type of field, enum values */ private function getTypeOfField(ClassMetadata $class, string $fieldName): array { @@ -2015,11 +2019,45 @@ private function getTypeOfField(ClassMetadata $class, string $fieldName): array $enumType = null; } - return [$type, $enumType]; + return [$type, $enumType, $this->detectEnumValues($type, $metadata)]; } - /** @param ?class-string $enumType */ - private function resolveDoctrineType(string $typeName, ?string $enumType = null, bool $nullable = false): Type + /** + * @param mixed $metadata + * + * @return list|null + */ + private function detectEnumValues(string $typeName, $metadata): ?array + { + if ($typeName !== 'enum') { + return null; + } + + $values = $metadata['options']['values'] ?? []; + + if (!is_array($values) || count($values) === 0) { + return null; + } + + foreach ($values as $value) { + if (!is_string($value)) { + return null; + } + } + + return array_values($values); + } + + /** + * @param ?class-string $enumType + * @param ?list $enumValues + */ + private function resolveDoctrineType( + string $typeName, + ?string $enumType = null, + ?array $enumValues = null, + bool $nullable = false + ): Type { try { $type = $this->descriptorRegistry @@ -2036,8 +2074,14 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null, ), ...TypeUtils::getAccessoryTypes($type)); } } + + if ($enumValues !== null) { + $enumValuesType = TypeCombinator::union(...array_map(static fn (string $value) => new ConstantStringType($value), $enumValues)); + $type = TypeCombinator::intersect($enumValuesType, $type); + } + if ($type instanceof NeverType) { - $type = new MixedType(); + $type = new MixedType(); } } catch (DescriptorNotRegisteredException $e) { if ($enumType !== null) { @@ -2051,11 +2095,19 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null, $type = TypeCombinator::addNull($type); } - return $type; + return $type; } - /** @param ?class-string $enumType */ - private function resolveDatabaseInternalType(string $typeName, ?string $enumType = null, bool $nullable = false): Type + /** + * @param ?class-string $enumType + * @param ?list $enumValues + */ + private function resolveDatabaseInternalType( + string $typeName, + ?string $enumType = null, + ?array $enumValues = null, + bool $nullable = false + ): Type { try { $descriptor = $this->descriptorRegistry->get($typeName); @@ -2074,6 +2126,11 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType $type = TypeCombinator::intersect($enumType, $type); } + if ($enumValues !== null) { + $enumValuesType = TypeCombinator::union(...array_map(static fn (string $value) => new ConstantStringType($value), $enumValues)); + $type = TypeCombinator::intersect($enumValuesType, $type); + } + if ($nullable) { $type = TypeCombinator::addNull($type); } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 2b8a3c14..cd3da8de 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -35,6 +35,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use QueryResult\Entities\Embedded; use QueryResult\Entities\JoinedChild; @@ -44,6 +45,7 @@ use QueryResult\Entities\One; use QueryResult\Entities\OneId; use QueryResult\Entities\SingleTableChild; +use QueryResult\EntitiesDbal42\Dbal4Entity; use QueryResult\EntitiesEnum\EntityWithEnum; use QueryResult\EntitiesEnum\IntEnum; use QueryResult\EntitiesEnum\StringEnum; @@ -187,6 +189,15 @@ public static function setUpBeforeClass(): void $em->persist($entityWithEnum); } + if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=4.2')) { + assert(class_exists(Dbal4Entity::class)); + + $dbal4Entity = new Dbal4Entity(); + $dbal4Entity->enum = 'a'; + $dbal4Entity->smallfloat = 1.1; + $em->persist($dbal4Entity); + } + $em->flush(); } @@ -1532,6 +1543,29 @@ private function yieldConditionalDataset(): iterable ]; } + if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=4.2')) { + yield 'enum and smallfloat' => [ + $this->constantArray([ + [ + new ConstantStringType('enum'), + new UnionType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + ]), + ], + [ + new ConstantStringType('smallfloat'), + new FloatType(), + ], + ]), + ' + SELECT e.enum, e.smallfloat + FROM QueryResult\EntitiesDbal42\Dbal4Entity e + ', + ]; + } + $ormVersion = InstalledVersions::getVersion('doctrine/orm'); $hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0; diff --git a/tests/Type/Doctrine/data/QueryResult/EntitiesDbal42/Dbal4Entity.php b/tests/Type/Doctrine/data/QueryResult/EntitiesDbal42/Dbal4Entity.php new file mode 100644 index 00000000..9d410c5c --- /dev/null +++ b/tests/Type/Doctrine/data/QueryResult/EntitiesDbal42/Dbal4Entity.php @@ -0,0 +1,36 @@ +setProxyDir(__DIR__); @@ -29,6 +32,13 @@ ), 'QueryResult\EntitiesEnum\\'); } +if (InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=4.2')) { + $metadataDriver->addDriver(new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/EntitiesDbal42'] + ), 'QueryResult\EntitiesDbal42\\'); +} + $config->setMetadataDriverImpl($metadataDriver); return new EntityManager( From d805e1472deed991971ba19687868fc7af091299 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 13 Jun 2025 17:23:13 +0200 Subject: [PATCH 136/160] PlatformTest: drop MSSQL database asserts --- .github/workflows/platform-test.yml | 10 ----- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 40 ------------------- tests/Platform/docker/Dockerfile80 | 12 ------ tests/Platform/docker/Dockerfile81 | 12 ------ tests/Platform/docker/Dockerfile84 | 12 ------ tests/Platform/docker/docker-compose.yml | 14 +------ tests/Platform/docker/docker-setup.sh | 2 +- 7 files changed, 2 insertions(+), 100 deletions(-) diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml index 38110353..e86f8704 100644 --- a/.github/workflows/platform-test.yml +++ b/.github/workflows/platform-test.yml @@ -15,7 +15,6 @@ jobs: env: MYSQL_HOST: '127.0.0.1' PGSQL_HOST: '127.0.0.1' - MSSQL_HOST: '127.0.0.1' strategy: fail-fast: false @@ -83,12 +82,3 @@ jobs: MYSQL_DATABASE: foo ports: - "3306:3306" - - mssql: - image: mcr.microsoft.com/mssql/server:latest - env: - ACCEPT_EULA: Y - SA_PASSWORD: 'Secret.123' - MSSQL_PID: Developer - ports: - - 1433:1433 diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 48846980..ff21a4b5 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -541,46 +541,6 @@ public function testPgsql( ); } - /** - * @param array $data - * @param mixed $mysqlExpectedResult - * @param mixed $sqliteExpectedResult - * @param mixed $pdoPgsqlExpectedResult - * @param mixed $pgsqlExpectedResult - * @param mixed $mssqlExpectedResult - * @param self::STRINGIFY_* $stringify - * - * @dataProvider provideCases - */ - public function testUnsupportedDriver( - array $data, - string $dqlTemplate, - Type $mysqlExpectedType, - ?Type $sqliteExpectedType, - ?Type $pdoPgsqlExpectedType, - ?Type $pgsqlExpectedType, - ?Type $mssqlExpectedType, - $mysqlExpectedResult, - $sqliteExpectedResult, - $pdoPgsqlExpectedResult, - $pgsqlExpectedResult, - $mssqlExpectedResult, - string $stringify - ): void - { - $this->performDriverTest( - 'sqlsrv', - self::CONFIG_DEFAULT, - $data, - $dqlTemplate, - (string) $this->dataName(), - PHP_VERSION_ID, - $mssqlExpectedType, - $mssqlExpectedResult, - $stringify, - ); - } - /** * @param array $data * @param mixed $mysqlExpectedResult diff --git a/tests/Platform/docker/Dockerfile80 b/tests/Platform/docker/Dockerfile80 index b5312737..37b6694c 100644 --- a/tests/Platform/docker/Dockerfile80 +++ b/tests/Platform/docker/Dockerfile80 @@ -1,17 +1,5 @@ FROM php:8.0-cli -# MSSQL -RUN apt update \ - && apt install -y gnupg2 \ - && apt install -y unixodbc-dev unixodbc \ - && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ - && curl https://packages.microsoft.com/config/debian/11/prod.list > /etc/apt/sources.list.d/mssql-release.list \ - && apt update \ - && ACCEPT_EULA=Y apt install -y msodbcsql17 \ - && pecl install sqlsrv-5.11.1 \ - && pecl install pdo_sqlsrv-5.11.1 \ - && docker-php-ext-enable sqlsrv pdo_sqlsrv - COPY ./docker-setup.sh /opt/src/scripts/setup.sh RUN /opt/src/scripts/setup.sh diff --git a/tests/Platform/docker/Dockerfile81 b/tests/Platform/docker/Dockerfile81 index 650c65f9..4ef5c3df 100644 --- a/tests/Platform/docker/Dockerfile81 +++ b/tests/Platform/docker/Dockerfile81 @@ -1,17 +1,5 @@ FROM php:8.1-cli -# MSSQL -RUN apt update \ - && apt install -y gnupg2 \ - && apt install -y unixodbc-dev unixodbc \ - && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ - && curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | tee /etc/apt/sources.list.d/mssql-tools.list \ - && apt update \ - && ACCEPT_EULA=Y apt install -y msodbcsql17 \ - && pecl install sqlsrv \ - && pecl install pdo_sqlsrv \ - && docker-php-ext-enable sqlsrv pdo_sqlsrv - COPY ./docker-setup.sh /opt/src/scripts/setup.sh RUN /opt/src/scripts/setup.sh diff --git a/tests/Platform/docker/Dockerfile84 b/tests/Platform/docker/Dockerfile84 index 30b47ed2..442415a5 100644 --- a/tests/Platform/docker/Dockerfile84 +++ b/tests/Platform/docker/Dockerfile84 @@ -1,17 +1,5 @@ FROM php:8.4-cli -# MSSQL -RUN apt update \ - && apt install -y gnupg2 \ - && apt install -y unixodbc-dev unixodbc \ - && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ - && curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | tee /etc/apt/sources.list.d/mssql-tools.list \ - && apt update \ - && ACCEPT_EULA=Y apt install -y msodbcsql17 \ - && pecl install sqlsrv \ - && pecl install pdo_sqlsrv \ - && docker-php-ext-enable sqlsrv pdo_sqlsrv - COPY ./docker-setup.sh /opt/src/scripts/setup.sh RUN /opt/src/scripts/setup.sh diff --git a/tests/Platform/docker/docker-compose.yml b/tests/Platform/docker/docker-compose.yml index 4a3b0f48..f9203806 100644 --- a/tests/Platform/docker/docker-compose.yml +++ b/tests/Platform/docker/docker-compose.yml @@ -27,24 +27,14 @@ services: type: tmpfs target: /var/lib/postgresql/data - mssql: - image: mcr.microsoft.com/mssql/server:latest - environment: - ACCEPT_EULA: Y - SA_PASSWORD: 'Secret.123' - MSSQL_PID: Developer - ports: - - 1433:1433 - php80: - depends_on: [mysql, pgsql, mssql] + depends_on: [mysql, pgsql] build: context: . dockerfile: ./Dockerfile80 environment: MYSQL_HOST: mysql PGSQL_HOST: pgsql - MSSQL_HOST: mssql working_dir: /app user: ${UID:-1000}:${GID:-1000} volumes: @@ -58,7 +48,6 @@ services: environment: MYSQL_HOST: mysql PGSQL_HOST: pgsql - MSSQL_HOST: mssql working_dir: /app user: ${UID:-1000}:${GID:-1000} volumes: @@ -72,7 +61,6 @@ services: environment: MYSQL_HOST: mysql PGSQL_HOST: pgsql - MSSQL_HOST: mssql working_dir: /app user: ${UID:-1000}:${GID:-1000} volumes: diff --git a/tests/Platform/docker/docker-setup.sh b/tests/Platform/docker/docker-setup.sh index 6fb71310..d482fc89 100755 --- a/tests/Platform/docker/docker-setup.sh +++ b/tests/Platform/docker/docker-setup.sh @@ -2,7 +2,7 @@ set -ex \ && apt update \ && apt install -y bash zip libpq-dev libsqlite3-dev \ - && pecl install xdebug mongodb \ + && pecl install xdebug mongodb-1.19.4 \ && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ && docker-php-ext-install pdo mysqli pgsql pdo_mysql pdo_pgsql pdo_sqlite \ && docker-php-ext-enable xdebug mongodb From 6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 17 Jul 2025 13:57:55 +0200 Subject: [PATCH 137/160] Silence deprecation of assert.exception --- phpstan.neon | 3 +++ phpunit.xml | 3 --- tests/bootstrap.php | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index ae9f2df6..f701caa0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -45,6 +45,9 @@ parameters: - message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ORM\\\\EntityManager'' and ''create'' will always evaluate to true\.$#' path: src/Doctrine/Mapping/ClassMetadataFactory.php + - + message: '#^Call to function ini_set\(\) with deprecated option ''assert\.exception''\.$#' + path: tests/bootstrap.php - message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' # needed for older DBAL versions diff --git a/phpunit.xml b/phpunit.xml index f4beeb21..93b7e4c7 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,9 +13,6 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" convertDeprecationsToExceptions="true" > - - - ./src diff --git a/tests/bootstrap.php b/tests/bootstrap.php index edf6599d..7994f793 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -4,6 +4,8 @@ require_once __DIR__ . '/../vendor/autoload.php'; +@ini_set('assert.exception', '0'); // @ deprecated in PHP 8.3 + PHPStanTestCase::getContainer(); require_once __DIR__ . '/orm-3-bootstrap.php'; From bddac51f0ffeb323e23bc379838bc08a4de3d0ba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:36:24 +0000 Subject: [PATCH 138/160] Update Eomm/why-don-t-you-tweet action to v2 --- .github/workflows/release-tweet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-tweet.yml b/.github/workflows/release-tweet.yml index 09b39ded..d81f34ca 100644 --- a/.github/workflows/release-tweet.yml +++ b/.github/workflows/release-tweet.yml @@ -10,7 +10,7 @@ jobs: tweet: runs-on: ubuntu-latest steps: - - uses: Eomm/why-don-t-you-tweet@v1 + - uses: Eomm/why-don-t-you-tweet@v2 if: ${{ !github.event.repository.private }} with: # GitHub event payload From 6fe9e9dfe6bb2fe6149c794ec3bb46658e9e9b64 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 02:12:06 +0000 Subject: [PATCH 139/160] Update actions/checkout action to v5 --- .github/workflows/build.yml | 10 +++++----- .github/workflows/create-tag.yml | 2 +- .github/workflows/platform-test.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad3633e9..17328a08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -56,10 +56,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Checkout build-cs" - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: "phpstan/build-cs" path: "build-cs" @@ -117,7 +117,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -167,7 +167,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index a8535014..fd918164 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -21,7 +21,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml index e86f8704..21f0d515 100644 --- a/.github/workflows/platform-test.yml +++ b/.github/workflows/platform-test.yml @@ -40,7 +40,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8c96d48..ed7e51ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Generate changelog id: changelog From cb37e282ca9a8d877cd95c4f1b3ef19a67cbd442 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 6 Sep 2025 13:33:38 +0200 Subject: [PATCH 140/160] Fix CS --- src/Doctrine/DoctrineDiagnoseExtension.php | 2 +- src/Type/Doctrine/Query/QueryResultTypeBuilder.php | 1 - src/Type/Doctrine/Query/QueryResultTypeWalker.php | 1 - tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php | 1 - .../Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php | 1 - 5 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Doctrine/DoctrineDiagnoseExtension.php b/src/Doctrine/DoctrineDiagnoseExtension.php index 0502c736..8973fc7c 100644 --- a/src/Doctrine/DoctrineDiagnoseExtension.php +++ b/src/Doctrine/DoctrineDiagnoseExtension.php @@ -42,7 +42,7 @@ public function print(Output $output): void $output->writeLineFormatted(sprintf( 'Detected driver: %s', - $driver === null ? 'None' : $driver, + $driver ?? 'None', )); } diff --git a/src/Type/Doctrine/Query/QueryResultTypeBuilder.php b/src/Type/Doctrine/Query/QueryResultTypeBuilder.php index 2f6fae11..e50c75ab 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeBuilder.php +++ b/src/Type/Doctrine/Query/QueryResultTypeBuilder.php @@ -236,7 +236,6 @@ private function resolveOffsetType($alias): Type return new ConstantStringType($alias); } - public function setIndexedBy(Type $type): void { $this->indexedBy = $type; diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index d9233d6e..d2129f41 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -129,7 +129,6 @@ class QueryResultTypeWalker extends SqlWalker private bool $hasGroupByClause; - /** * @param Query $query */ diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index ff21a4b5..04cbb552 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -4940,7 +4940,6 @@ public static function dataAllIntLike(): array ]; } - /** * @return array> */ diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index 11cf5016..4c86b2b3 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -399,7 +399,6 @@ private function getRealHydrationMode(string $methodName, ?int $hydrationMode): throw new LogicException(sprintf('Using %s without hydration mode is not supported.', $methodName)); } - private static function stringifies(): bool { return PHP_VERSION_ID < 80100; From eeff19808f8ae3a6f7c4e43e388a2848eb2b0865 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 3 Sep 2025 15:37:59 +0200 Subject: [PATCH 141/160] Improve EntityColumnRule for enums --- src/Rules/Doctrine/ORM/EntityColumnRule.php | 25 ++++++++++++++ .../Doctrine/ORM/EntityColumnRuleTest.php | 20 +++++++++++ tests/Rules/Doctrine/ORM/data/bug-677.php | 33 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 tests/Rules/Doctrine/ORM/data/bug-677.php diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index b5f42bb6..5a9f5fff 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -9,6 +9,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Doctrine\DescriptorNotRegisteredException; use PHPStan\Type\Doctrine\DescriptorRegistry; use PHPStan\Type\Doctrine\ObjectMetadataResolver; @@ -21,10 +22,14 @@ use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use Throwable; +use function count; use function get_class; use function in_array; +use function is_array; +use function is_string; use function sprintf; /** @@ -100,6 +105,26 @@ public function processNode(Node $node, Scope $scope): array $writableToPropertyType = $descriptor->getWritableToPropertyType(); $writableToDatabaseType = $descriptor->getWritableToDatabaseType(); + if ($fieldMapping['type'] === 'enum') { + $values = $fieldMapping['options']['values'] ?? null; + if (is_array($values)) { + $enumTypes = []; + foreach ($values as $value) { + if (!is_string($value)) { + $enumTypes = []; + break; + } + + $enumTypes[] = new ConstantStringType($value); + } + + if (count($enumTypes) > 0) { + $writableToPropertyType = new UnionType($enumTypes); + $writableToDatabaseType = new UnionType($enumTypes); + } + } + } + $enumTypeString = $fieldMapping['enumType'] ?? null; if ($enumTypeString !== null) { if ($writableToDatabaseType->isArray()->no() && $writableToPropertyType->isArray()->no()) { diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index f0a799e6..a39f6151 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -18,6 +18,7 @@ use PHPStan\Type\Doctrine\Descriptors\DateTimeType; use PHPStan\Type\Doctrine\Descriptors\DateType; use PHPStan\Type\Doctrine\Descriptors\DecimalType; +use PHPStan\Type\Doctrine\Descriptors\EnumType; use PHPStan\Type\Doctrine\Descriptors\IntegerType; use PHPStan\Type\Doctrine\Descriptors\JsonType; use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor; @@ -26,6 +27,7 @@ use PHPStan\Type\Doctrine\Descriptors\StringType; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use function array_unshift; +use function class_exists; use function strpos; use const PHP_VERSION_ID; @@ -75,6 +77,7 @@ protected function getRule(): Rule new StringType(), new SimpleArrayType(), new UuidTypeDescriptor(FakeTestingUuidType::class), + new EnumType(), new ReflectionDescriptor(CarbonImmutableType::class, $this->createReflectionProvider(), self::getContainer()), new ReflectionDescriptor(CarbonType::class, $this->createReflectionProvider(), self::getContainer()), new ReflectionDescriptor(CustomType::class, $this->createReflectionProvider(), self::getContainer()), @@ -441,4 +444,21 @@ public function testBug306(?string $objectManagerLoader): void ]); } + /** + * @dataProvider dataObjectManagerLoader + */ + public function testBug677(?string $objectManagerLoader): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1'); + } + if (!class_exists(\Doctrine\DBAL\Types\EnumType::class)) { + self::markTestSkipped('Test requires EnumType.'); + } + + $this->allowNullablePropertyForRequiredField = false; + $this->objectManagerLoader = $objectManagerLoader; + $this->analyse([__DIR__ . '/data/bug-677.php'], []); + } + } diff --git a/tests/Rules/Doctrine/ORM/data/bug-677.php b/tests/Rules/Doctrine/ORM/data/bug-677.php new file mode 100644 index 00000000..f84d1349 --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data/bug-677.php @@ -0,0 +1,33 @@ += 8.1 + +namespace PHPStan\Rules\Doctrine\ORM\Bug677; + +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +class MyBrokenEntity +{ + public const LOGIN_METHOD_BASIC_AUTH = 'BasicAuth'; + public const LOGIN_METHOD_SSO = 'SSO'; + public const LOGIN_METHOD_SAML = 'SAML'; + + public const LOGIN_METHODS = [ + self::LOGIN_METHOD_BASIC_AUTH, + self::LOGIN_METHOD_SSO, + self::LOGIN_METHOD_SAML, + ]; + + /** + * @var int|null + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private $id; + + /** + * @var self::LOGIN_METHOD_* + */ + #[ORM\Column(name: 'login_method', type: 'enum', options: ['default' => self::LOGIN_METHOD_BASIC_AUTH, 'values' => self::LOGIN_METHODS])] + private string $loginMethod = self::LOGIN_METHOD_BASIC_AUTH; +} From 934f5734812341358fc41c44006b30fa00c785f0 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 9 Sep 2025 23:52:45 +0200 Subject: [PATCH 142/160] Fix enum type --- src/Rules/Doctrine/ORM/EntityColumnRule.php | 38 +++++++++---------- .../Doctrine/ORM/EntityColumnRuleTest.php | 17 +++++++++ tests/Rules/Doctrine/ORM/data/bug-679.php | 27 +++++++++++++ 3 files changed, 62 insertions(+), 20 deletions(-) create mode 100644 tests/Rules/Doctrine/ORM/data/bug-679.php diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index 5a9f5fff..aa8343b7 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -105,26 +105,6 @@ public function processNode(Node $node, Scope $scope): array $writableToPropertyType = $descriptor->getWritableToPropertyType(); $writableToDatabaseType = $descriptor->getWritableToDatabaseType(); - if ($fieldMapping['type'] === 'enum') { - $values = $fieldMapping['options']['values'] ?? null; - if (is_array($values)) { - $enumTypes = []; - foreach ($values as $value) { - if (!is_string($value)) { - $enumTypes = []; - break; - } - - $enumTypes[] = new ConstantStringType($value); - } - - if (count($enumTypes) > 0) { - $writableToPropertyType = new UnionType($enumTypes); - $writableToDatabaseType = new UnionType($enumTypes); - } - } - } - $enumTypeString = $fieldMapping['enumType'] ?? null; if ($enumTypeString !== null) { if ($writableToDatabaseType->isArray()->no() && $writableToPropertyType->isArray()->no()) { @@ -179,6 +159,24 @@ public function processNode(Node $node, Scope $scope): array ), ...TypeUtils::getAccessoryTypes($writableToDatabaseType)); } + } elseif ($fieldMapping['type'] === 'enum') { + $values = $fieldMapping['options']['values'] ?? null; + if (is_array($values)) { + $enumTypes = []; + foreach ($values as $value) { + if (!is_string($value)) { + $enumTypes = []; + break; + } + + $enumTypes[] = new ConstantStringType($value); + } + + if (count($enumTypes) > 0) { + $writableToPropertyType = new UnionType($enumTypes); + $writableToDatabaseType = new UnionType($enumTypes); + } + } } $identifiers = []; diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index a39f6151..c55b5cd5 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -461,4 +461,21 @@ public function testBug677(?string $objectManagerLoader): void $this->analyse([__DIR__ . '/data/bug-677.php'], []); } + /** + * @dataProvider dataObjectManagerLoader + */ + public function testBug679(?string $objectManagerLoader): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1'); + } + if (!class_exists(\Doctrine\DBAL\Types\EnumType::class)) { + self::markTestSkipped('Test requires EnumType.'); + } + + $this->allowNullablePropertyForRequiredField = false; + $this->objectManagerLoader = $objectManagerLoader; + $this->analyse([__DIR__ . '/data/bug-679.php'], []); + } + } diff --git a/tests/Rules/Doctrine/ORM/data/bug-679.php b/tests/Rules/Doctrine/ORM/data/bug-679.php new file mode 100644 index 00000000..f3f4b23c --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data/bug-679.php @@ -0,0 +1,27 @@ += 8.1 + +namespace PHPStan\Rules\Doctrine\ORM\Bug679; + +use Doctrine\ORM\Mapping as ORM; + +enum FooEnum: string { + + case ONE = 'one'; + case TWO = 'two'; + +} + +#[ORM\Entity] +class MyBrokenEntity +{ + /** + * @var int|null + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private $id; + + #[ORM\Column(type: "enum", enumType: FooEnum::class)] + public FooEnum $type1; +} From 873c32c043181b47c57eea12540389334ce599b4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 5 Oct 2025 08:34:13 +0200 Subject: [PATCH 143/160] Run tests in random order --- phpunit.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 93b7e4c7..02222436 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,7 +11,8 @@ failOnRisky="true" failOnWarning="true" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" - convertDeprecationsToExceptions="true" + convertDeprecationsToExceptions="true" + executionOrder="random" > From 8f083bfe17bb0180c22f5f70c13a1574baf4d0a4 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 Oct 2025 13:17:39 +0200 Subject: [PATCH 144/160] Mark more method with phpstan-impure --- stubs/DocumentManager.stub | 1 + stubs/DocumentRepository.stub | 4 ++++ stubs/EntityManager.stub | 1 + stubs/EntityManagerDecorator.stub | 1 + stubs/EntityManagerInterface.stub | 1 + stubs/EntityRepository.stub | 4 ++++ 6 files changed, 12 insertions(+) diff --git a/stubs/DocumentManager.stub b/stubs/DocumentManager.stub index 1e73316d..648ec03c 100644 --- a/stubs/DocumentManager.stub +++ b/stubs/DocumentManager.stub @@ -15,6 +15,7 @@ class DocumentManager implements ObjectManager * @phpstan-param integer|null $lockMode * @phpstan-param integer|null $lockVersion * @phpstan-return T|null + * @phpstan-impure */ public function find($documentName, $identifier, $lockMode = null, $lockVersion = null); diff --git a/stubs/DocumentRepository.stub b/stubs/DocumentRepository.stub index 34b970cb..e3bf3df6 100644 --- a/stubs/DocumentRepository.stub +++ b/stubs/DocumentRepository.stub @@ -16,11 +16,13 @@ class DocumentRepository implements ObjectRepository * @phpstan-param int|null $lockMode * @phpstan-param int|null $lockVersion * @phpstan-return TDocumentClass|null + * @phpstan-impure */ public function find($id, $lockMode = null, $lockVersion = null); /** * @phpstan-return array + * @phpstan-impure */ public function findAll(); @@ -30,12 +32,14 @@ class DocumentRepository implements ObjectRepository * @phpstan-param int|null $limit * @phpstan-param int|null $skip * @phpstan-return array + * @phpstan-impure */ public function findBy(array $criteria, ?array $sort = null, $limit = null, $skip = null); /** * @phpstan-param array $criteria The criteria. * @phpstan-return TDocumentClass|null + * @phpstan-impure */ public function findOneBy(array $criteria); diff --git a/stubs/EntityManager.stub b/stubs/EntityManager.stub index 33a8af0e..6dc13e1c 100644 --- a/stubs/EntityManager.stub +++ b/stubs/EntityManager.stub @@ -14,6 +14,7 @@ class EntityManager implements EntityManagerInterface * @phpstan-param integer|null $lockMode * @phpstan-param integer|null $lockVersion * @phpstan-return T|null + * @phpstan-impure */ public function find($entityName, $id, $lockMode = null, $lockVersion = null); diff --git a/stubs/EntityManagerDecorator.stub b/stubs/EntityManagerDecorator.stub index 552cee9e..670d2e8a 100644 --- a/stubs/EntityManagerDecorator.stub +++ b/stubs/EntityManagerDecorator.stub @@ -16,6 +16,7 @@ class EntityManagerDecorator implements EntityManagerInterface * @phpstan-param integer|null $lockMode * @phpstan-param integer|null $lockVersion * @phpstan-return T|null + * @phpstan-impure */ public function find($entityName, $id, $lockMode = null, $lockVersion = null); diff --git a/stubs/EntityManagerInterface.stub b/stubs/EntityManagerInterface.stub index 2f1eba4c..5625f33c 100644 --- a/stubs/EntityManagerInterface.stub +++ b/stubs/EntityManagerInterface.stub @@ -15,6 +15,7 @@ interface EntityManagerInterface extends ObjectManager * @phpstan-param class-string $className * @phpstan-param mixed $id * @phpstan-return T|null + * @phpstan-impure */ public function find($className, $id); diff --git a/stubs/EntityRepository.stub b/stubs/EntityRepository.stub index bdc11eca..77b5b337 100644 --- a/stubs/EntityRepository.stub +++ b/stubs/EntityRepository.stub @@ -26,6 +26,7 @@ class EntityRepository implements ObjectRepository /** * @phpstan-return list + * @phpstan-impure */ public function findAll(); @@ -35,6 +36,7 @@ class EntityRepository implements ObjectRepository * @phpstan-param int|null $limit * @phpstan-param int|null $offset * @phpstan-return list + * @phpstan-impure */ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null); @@ -77,6 +79,8 @@ class EntityRepository implements ObjectRepository * @param array $criteria * * @return int<0, max> + * + * @phpstan-impure */ public function count(array $criteria); From cd42b1731a062148c6de75a036c0437433e24974 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 6 Oct 2025 11:48:43 +0200 Subject: [PATCH 145/160] Fix PHPStan crashing with single value enum --- src/Rules/Doctrine/ORM/EntityColumnRule.php | 6 ++-- .../Doctrine/ORM/EntityColumnRuleTest.php | 17 +++++++++++ .../Doctrine/ORM/data/bug-single-enum.php | 29 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 tests/Rules/Doctrine/ORM/data/bug-single-enum.php diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index aa8343b7..75aa1ae6 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -22,7 +22,6 @@ use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use Throwable; use function count; @@ -173,8 +172,9 @@ public function processNode(Node $node, Scope $scope): array } if (count($enumTypes) > 0) { - $writableToPropertyType = new UnionType($enumTypes); - $writableToDatabaseType = new UnionType($enumTypes); + $unionType = TypeCombinator::union(...$enumTypes); + $writableToPropertyType = $unionType; + $writableToDatabaseType = $unionType; } } } diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index c55b5cd5..56878cc7 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -478,4 +478,21 @@ public function testBug679(?string $objectManagerLoader): void $this->analyse([__DIR__ . '/data/bug-679.php'], []); } + /** + * @dataProvider dataObjectManagerLoader + */ + public function testBugSingleEnum(?string $objectManagerLoader): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1'); + } + if (!class_exists(\Doctrine\DBAL\Types\EnumType::class)) { + self::markTestSkipped('Test requires EnumType.'); + } + + $this->allowNullablePropertyForRequiredField = false; + $this->objectManagerLoader = $objectManagerLoader; + $this->analyse([__DIR__ . '/data/bug-single-enum.php'], []); + } + } diff --git a/tests/Rules/Doctrine/ORM/data/bug-single-enum.php b/tests/Rules/Doctrine/ORM/data/bug-single-enum.php new file mode 100644 index 00000000..564e3dcb --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data/bug-single-enum.php @@ -0,0 +1,29 @@ += 8.1 + +namespace PHPStan\Rules\Doctrine\ORM\BugSingleEnum; + +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +class MyBrokenEntity +{ + public const LOGIN_METHOD_BASIC_AUTH = 'BasicAuth'; + + public const LOGIN_METHODS = [ + self::LOGIN_METHOD_BASIC_AUTH, + ]; + + /** + * @var int|null + */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private $id; + + /** + * @var self::LOGIN_METHOD_* + */ + #[ORM\Column(name: 'login_method', type: 'enum', options: ['default' => self::LOGIN_METHOD_BASIC_AUTH, 'values' => self::LOGIN_METHODS])] + private string $loginMethod = self::LOGIN_METHOD_BASIC_AUTH; +} From 4a761b6dfd4ee08298e1327cc49e6101aa2efc68 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 6 Oct 2025 11:54:25 +0200 Subject: [PATCH 146/160] Add support for symfony uuid --- composer.json | 3 +- extension.neon | 10 ++ .../Descriptors/Ramsey/UuidTypeDescriptor.php | 27 +---- .../Symfony/UlidTypeDescriptor.php | 49 ++++++++ .../Symfony/UuidTypeDescriptor.php | 49 ++++++++ .../Doctrine/ORM/EntityColumnRuleTest.php | 50 +++++++- ...Type.php => FakeTestingRamseyUuidType.php} | 2 +- .../ORM/FakeTestingSymfonyUlidType.php | 113 ++++++++++++++++++ .../ORM/FakeTestingSymfonyUuidType.php | 113 ++++++++++++++++++ .../ORM/data/EntityWithSymfonyUid.php | 45 +++++++ 10 files changed, 433 insertions(+), 28 deletions(-) create mode 100644 src/Type/Doctrine/Descriptors/Symfony/UlidTypeDescriptor.php create mode 100644 src/Type/Doctrine/Descriptors/Symfony/UuidTypeDescriptor.php rename tests/Rules/Doctrine/ORM/{FakeTestingUuidType.php => FakeTestingRamseyUuidType.php} (96%) create mode 100644 tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php create mode 100644 tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php create mode 100644 tests/Rules/Doctrine/ORM/data/EntityWithSymfonyUid.php diff --git a/composer.json b/composer.json index 24ea4782..9185e8b9 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", - "symfony/cache": "^5.4" + "symfony/cache": "^5.4", + "symfony/uid": "^5.4 || ^6.4 || ^7.3" }, "config": { "sort-packages": true, diff --git a/extension.neon b/extension.neon index 803ffaed..046d05d6 100644 --- a/extension.neon +++ b/extension.neon @@ -418,6 +418,16 @@ services: tags: [phpstan.doctrine.typeDescriptor] arguments: uuidTypeName: Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType + - + class: PHPStan\Type\Doctrine\Descriptors\Symfony\UuidTypeDescriptor + tags: [phpstan.doctrine.typeDescriptor] + arguments: + uuidTypeName: Symfony\Bridge\Doctrine\Types\UuidType + - + class: PHPStan\Type\Doctrine\Descriptors\Symfony\UlidTypeDescriptor + tags: [phpstan.doctrine.typeDescriptor] + arguments: + uuidTypeName: Symfony\Bridge\Doctrine\Types\UlidType # Doctrine Collection - diff --git a/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php b/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php index 549b14c4..7b0d8bf5 100644 --- a/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php +++ b/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php @@ -2,46 +2,29 @@ namespace PHPStan\Type\Doctrine\Descriptors\Ramsey; -use PHPStan\Rules\Doctrine\ORM\FakeTestingUuidType; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Doctrine\Descriptors\DoctrineTypeDescriptor; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use Ramsey\Uuid\UuidInterface; -use function in_array; -use function sprintf; class UuidTypeDescriptor implements DoctrineTypeDescriptor { - private const SUPPORTED_UUID_TYPES = [ - 'Ramsey\Uuid\Doctrine\UuidType', - 'Ramsey\Uuid\Doctrine\UuidBinaryType', - 'Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType', - FakeTestingUuidType::class, - ]; - + /** @var class-string<\Doctrine\DBAL\Types\Type> */ private string $uuidTypeName; - public function __construct( - string $uuidTypeName - ) + /** + * @param class-string<\Doctrine\DBAL\Types\Type> $uuidTypeName + */ + public function __construct(string $uuidTypeName) { - if (!in_array($uuidTypeName, self::SUPPORTED_UUID_TYPES, true)) { - throw new ShouldNotHappenException(sprintf( - 'Unexpected UUID column type "%s" provided', - $uuidTypeName, - )); - } - $this->uuidTypeName = $uuidTypeName; } public function getType(): string { - /** @var class-string<\Doctrine\DBAL\Types\Type> */ return $this->uuidTypeName; } diff --git a/src/Type/Doctrine/Descriptors/Symfony/UlidTypeDescriptor.php b/src/Type/Doctrine/Descriptors/Symfony/UlidTypeDescriptor.php new file mode 100644 index 00000000..58fb1083 --- /dev/null +++ b/src/Type/Doctrine/Descriptors/Symfony/UlidTypeDescriptor.php @@ -0,0 +1,49 @@ + */ + private string $uuidTypeName; + + /** + * @param class-string<\Doctrine\DBAL\Types\Type> $uuidTypeName + */ + public function __construct(string $uuidTypeName) + { + $this->uuidTypeName = $uuidTypeName; + } + + public function getType(): string + { + return $this->uuidTypeName; + } + + public function getWritableToPropertyType(): Type + { + return new ObjectType(Ulid::class); + } + + public function getWritableToDatabaseType(): Type + { + return TypeCombinator::union( + new StringType(), + new ObjectType(Ulid::class), + ); + } + + public function getDatabaseInternalType(): Type + { + return new StringType(); + } + +} diff --git a/src/Type/Doctrine/Descriptors/Symfony/UuidTypeDescriptor.php b/src/Type/Doctrine/Descriptors/Symfony/UuidTypeDescriptor.php new file mode 100644 index 00000000..03151cec --- /dev/null +++ b/src/Type/Doctrine/Descriptors/Symfony/UuidTypeDescriptor.php @@ -0,0 +1,49 @@ + */ + private string $uuidTypeName; + + /** + * @param class-string<\Doctrine\DBAL\Types\Type> $uuidTypeName + */ + public function __construct(string $uuidTypeName) + { + $this->uuidTypeName = $uuidTypeName; + } + + public function getType(): string + { + return $this->uuidTypeName; + } + + public function getWritableToPropertyType(): Type + { + return new ObjectType(Uuid::class); + } + + public function getWritableToDatabaseType(): Type + { + return TypeCombinator::union( + new StringType(), + new ObjectType(Uuid::class), + ); + } + + public function getDatabaseInternalType(): Type + { + return new StringType(); + } + +} diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 56878cc7..5d3c66f7 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -21,10 +21,12 @@ use PHPStan\Type\Doctrine\Descriptors\EnumType; use PHPStan\Type\Doctrine\Descriptors\IntegerType; use PHPStan\Type\Doctrine\Descriptors\JsonType; -use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor; +use PHPStan\Type\Doctrine\Descriptors\Ramsey\UuidTypeDescriptor as RamseyUuidTypeDescriptor; use PHPStan\Type\Doctrine\Descriptors\ReflectionDescriptor; use PHPStan\Type\Doctrine\Descriptors\SimpleArrayType; use PHPStan\Type\Doctrine\Descriptors\StringType; +use PHPStan\Type\Doctrine\Descriptors\Symfony\UlidTypeDescriptor as SymfonyUlidTypeDescriptor; +use PHPStan\Type\Doctrine\Descriptors\Symfony\UuidTypeDescriptor as SymfonyUuidTypeDescriptor; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use function array_unshift; use function class_exists; @@ -41,6 +43,8 @@ class EntityColumnRuleTest extends RuleTestCase private ?string $objectManagerLoader = null; + private bool $useSymfonyUuid = false; + protected function getRule(): Rule { if (!Type::hasType(CustomType::NAME)) { @@ -49,8 +53,23 @@ protected function getRule(): Rule if (!Type::hasType(CustomNumericType::NAME)) { Type::addType(CustomNumericType::NAME, CustomNumericType::class); } - if (!Type::hasType(FakeTestingUuidType::NAME)) { - Type::addType(FakeTestingUuidType::NAME, FakeTestingUuidType::class); + if ($this->useSymfonyUuid) { + if (!Type::hasType(FakeTestingSymfonyUuidType::NAME)) { + Type::addType(FakeTestingSymfonyUuidType::NAME, FakeTestingSymfonyUuidType::class); + } else { + // Override Ramsay definition + Type::overrideType(FakeTestingSymfonyUuidType::NAME, FakeTestingSymfonyUuidType::class); + } + if (!Type::hasType(FakeTestingSymfonyUlidType::NAME)) { + Type::addType(FakeTestingSymfonyUlidType::NAME, FakeTestingSymfonyUlidType::class); + } + } else { + if (!Type::hasType(FakeTestingRamseyUuidType::NAME)) { + Type::addType(FakeTestingRamseyUuidType::NAME, FakeTestingRamseyUuidType::class); + } else { + // Override Symfony definition + Type::overrideType(FakeTestingRamseyUuidType::NAME, FakeTestingRamseyUuidType::class); + } } if (!Type::hasType('carbon')) { Type::addType('carbon', CarbonType::class); @@ -76,8 +95,10 @@ protected function getRule(): Rule new IntegerType(), new StringType(), new SimpleArrayType(), - new UuidTypeDescriptor(FakeTestingUuidType::class), new EnumType(), + new RamseyUuidTypeDescriptor(FakeTestingRamseyUuidType::class), + new SymfonyUuidTypeDescriptor(FakeTestingSymfonyUuidType::class), + new SymfonyUlidTypeDescriptor(FakeTestingSymfonyUlidType::class), new ReflectionDescriptor(CarbonImmutableType::class, $this->createReflectionProvider(), self::getContainer()), new ReflectionDescriptor(CarbonType::class, $this->createReflectionProvider(), self::getContainer()), new ReflectionDescriptor(CustomType::class, $this->createReflectionProvider(), self::getContainer()), @@ -461,6 +482,27 @@ public function testBug677(?string $objectManagerLoader): void $this->analyse([__DIR__ . '/data/bug-677.php'], []); } + /** + * @dataProvider dataObjectManagerLoader + */ + public function testSymfonyUuid(?string $objectManagerLoader): void + { + $this->allowNullablePropertyForRequiredField = true; + $this->objectManagerLoader = $objectManagerLoader; + $this->useSymfonyUuid = true; + + $this->analyse([__DIR__ . '/data/EntityWithSymfonyUid.php'], [ + [ + 'Property PHPStan\Rules\Doctrine\ORM\EntityWithSymfonyUid::$uuidInvalidType type mapping mismatch: database can contain Symfony\Component\Uid\Uuid but property expects string.', + 32, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORM\EntityWithSymfonyUid::$ulidInvalidType type mapping mismatch: database can contain Symfony\Component\Uid\Ulid but property expects string.', + 44, + ], + ]); + } + /** * @dataProvider dataObjectManagerLoader */ diff --git a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php b/tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php similarity index 96% rename from tests/Rules/Doctrine/ORM/FakeTestingUuidType.php rename to tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php index f6255563..a81fdc64 100644 --- a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php +++ b/tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php @@ -13,7 +13,7 @@ * From https://github.com/ramsey/uuid-doctrine/blob/fafebbe972cdaba9274c286ea8923e2de2579027/src/UuidType.php * Copyright (c) 2012-2022 Ben Ramsey */ -final class FakeTestingUuidType extends GuidType +final class FakeTestingRamseyUuidType extends GuidType { public const NAME = 'uuid'; diff --git a/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php new file mode 100644 index 00000000..389303bf --- /dev/null +++ b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php @@ -0,0 +1,113 @@ + + */ +final class FakeTestingSymfonyUlidType extends Type +{ + + public const NAME = 'ulid'; + + /** + * @not-deprecated + */ + public function getName(): string + { + return self::NAME; + } + + protected function getUidClass(): string + { + return Ulid::class; + } + + /** + * {@inheritdoc} + */ + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + if ($this->hasNativeGuidType($platform)) { + return $platform->getGuidTypeDeclarationSQL($column); + } + + return $platform->getBinaryTypeDeclarationSQL([ + 'length' => '16', + 'fixed' => true, + ]); + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?AbstractUid + { + if ($value instanceof AbstractUid || $value === null) { + return $value; + } + + if (!is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + /** @phpstan-ignore-next-line method.dynamicName */ + return $this->getUidClass()::fromString($value); + } catch (InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + $toString = $this->hasNativeGuidType($platform) ? 'toRfc4122' : 'toBinary'; + + if ($value instanceof AbstractUid) { + /** @phpstan-ignore-next-line method.dynamicName */ + return $value->$toString(); + } + + if ($value === null || $value === '') { + return null; + } + + if (!is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + /** @phpstan-ignore-next-line method.dynamicName */ + return $this->getUidClass()::fromString($value)->$toString(); + } catch (InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + } + + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + return true; + } + + private function hasNativeGuidType(AbstractPlatform $platform): bool + { + return $platform->getGuidTypeDeclarationSQL([]) !== $platform->getStringTypeDeclarationSQL(['fixed' => true, 'length' => 36]); + } + +} diff --git a/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php new file mode 100644 index 00000000..89d853ef --- /dev/null +++ b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php @@ -0,0 +1,113 @@ + + */ +final class FakeTestingSymfonyUuidType extends Type +{ + + public const NAME = 'uuid'; + + /** + * @not-deprecated + */ + public function getName(): string + { + return self::NAME; + } + + protected function getUidClass(): string + { + return Uuid::class; + } + + /** + * {@inheritdoc} + */ + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + if ($this->hasNativeGuidType($platform)) { + return $platform->getGuidTypeDeclarationSQL($column); + } + + return $platform->getBinaryTypeDeclarationSQL([ + 'length' => '16', + 'fixed' => true, + ]); + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?AbstractUid + { + if ($value instanceof AbstractUid || $value === null) { + return $value; + } + + if (!is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + /** @phpstan-ignore-next-line method.dynamicName */ + return $this->getUidClass()::fromString($value); + } catch (InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + $toString = $this->hasNativeGuidType($platform); + + if ($value instanceof AbstractUid) { + /** @phpstan-ignore-next-line method.dynamicName */ + return $value->$toString(); + } + + if ($value === null || $value === '') { + return null; + } + + if (!is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + /** @phpstan-ignore-next-line method.dynamicName */ + return $this->getUidClass()::fromString($value)->$toString(); + } catch (InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + } + + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + return true; + } + + private function hasNativeGuidType(AbstractPlatform $platform): bool + { + return $platform->getGuidTypeDeclarationSQL([]) !== $platform->getStringTypeDeclarationSQL(['fixed' => true, 'length' => 36]); + } + +} diff --git a/tests/Rules/Doctrine/ORM/data/EntityWithSymfonyUid.php b/tests/Rules/Doctrine/ORM/data/EntityWithSymfonyUid.php new file mode 100644 index 00000000..a1c742e2 --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data/EntityWithSymfonyUid.php @@ -0,0 +1,45 @@ + Date: Mon, 6 Oct 2025 12:01:02 +0200 Subject: [PATCH 147/160] Fix BinaryType with dbal 4 --- src/Type/Doctrine/Descriptors/BinaryType.php | 21 ++++++++++ .../Doctrine/ORM/EntityColumnRuleTest.php | 39 ++++++++++++++++++- tests/Rules/Doctrine/ORM/data/bug-659.php | 32 +++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/Rules/Doctrine/ORM/data/bug-659.php diff --git a/src/Type/Doctrine/Descriptors/BinaryType.php b/src/Type/Doctrine/Descriptors/BinaryType.php index 5b3c848a..21a38092 100644 --- a/src/Type/Doctrine/Descriptors/BinaryType.php +++ b/src/Type/Doctrine/Descriptors/BinaryType.php @@ -2,10 +2,13 @@ namespace PHPStan\Type\Doctrine\Descriptors; +use Composer\InstalledVersions; use PHPStan\Type\MixedType; use PHPStan\Type\ResourceType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use function class_exists; +use function strpos; class BinaryType implements DoctrineTypeDescriptor { @@ -17,6 +20,10 @@ public function getType(): string public function getWritableToPropertyType(): Type { + if ($this->hasDbal4()) { + return new StringType(); + } + return new ResourceType(); } @@ -30,4 +37,18 @@ public function getDatabaseInternalType(): Type return new StringType(); } + private function hasDbal4(): bool + { + if (!class_exists(InstalledVersions::class)) { + return false; + } + + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + if ($dbalVersion === null) { + return false; + } + + return strpos($dbalVersion, '4.') === 0; + } + } diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 5d3c66f7..31c64ad9 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -30,6 +30,7 @@ use PHPStan\Type\Doctrine\ObjectMetadataResolver; use function array_unshift; use function class_exists; +use function sprintf; use function strpos; use const PHP_VERSION_ID; @@ -292,9 +293,16 @@ public function testSuperclass(?string $objectManagerLoader): void { $this->allowNullablePropertyForRequiredField = false; $this->objectManagerLoader = $objectManagerLoader; + + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + $hasDbal4 = $dbalVersion !== null && strpos($dbalVersion, '4.') === 0; + $this->analyse([__DIR__ . '/data/MyBrokenSuperclass.php'], [ [ - 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenSuperclass::$five type mapping mismatch: database can contain resource but property expects int.', + sprintf( + 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenSuperclass::$five type mapping mismatch: database can contain %s but property expects int.', + $hasDbal4 ? 'string' : 'resource', + ), 17, ], ]); @@ -537,4 +545,33 @@ public function testBugSingleEnum(?string $objectManagerLoader): void $this->analyse([__DIR__ . '/data/bug-single-enum.php'], []); } + /** + * @dataProvider dataObjectManagerLoader + */ + public function testBug659(?string $objectManagerLoader): void + { + $this->allowNullablePropertyForRequiredField = false; + $this->objectManagerLoader = $objectManagerLoader; + + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + $hasDbal4 = $dbalVersion !== null && strpos($dbalVersion, '4.') === 0; + if ($hasDbal4) { + $errors = [ + [ + 'Property PHPStan\Rules\Doctrine\ORM\MyEntity659::$binaryResource type mapping mismatch: database can contain string but property expects resource.', + 31, + ], + ]; + } else { + $errors = [ + [ + 'Property PHPStan\Rules\Doctrine\ORM\MyEntity659::$binaryString type mapping mismatch: database can contain resource but property expects string.', + 25, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/bug-659.php'], $errors); + } + } diff --git a/tests/Rules/Doctrine/ORM/data/bug-659.php b/tests/Rules/Doctrine/ORM/data/bug-659.php new file mode 100644 index 00000000..e4ac3f68 --- /dev/null +++ b/tests/Rules/Doctrine/ORM/data/bug-659.php @@ -0,0 +1,32 @@ += 8.0 + +namespace PHPStan\Rules\Doctrine\ORM; + +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity() + */ +class MyEntity659 +{ + + /** + * @ORM\Id() + * @ORM\GeneratedValue() + * @ORM\Column(type="integer") + * @var int + */ + private $id; + + /** + * @var string + * @ORM\Column(type="binary") + */ + private $binaryString; + + /** + * @var resource + * @ORM\Column(type="binary") + */ + private $binaryResource; +} From f8359a074dc8423b75501ec7dfc1c00efe9e6878 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 6 Oct 2025 16:02:38 +0200 Subject: [PATCH 148/160] Run using global type overrides in isolation --- tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php | 1 + tests/Rules/Doctrine/ORM/DqlRuleTest.php | 1 + tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php | 1 + tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php | 1 + tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php | 1 + tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php | 1 + tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php | 1 + 7 files changed, 7 insertions(+) diff --git a/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php b/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php index 7c92d905..5d6800fd 100644 --- a/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php +++ b/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php @@ -9,6 +9,7 @@ /** * @extends RuleTestCase + * @runInSeparateProcess */ class DoctrineProxyForbiddenClassNamesExtensionTest extends RuleTestCase { diff --git a/tests/Rules/Doctrine/ORM/DqlRuleTest.php b/tests/Rules/Doctrine/ORM/DqlRuleTest.php index 32ea2396..6e019ebc 100644 --- a/tests/Rules/Doctrine/ORM/DqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/DqlRuleTest.php @@ -11,6 +11,7 @@ /** * @extends RuleTestCase + * @runInSeparateProcess */ class DqlRuleTest extends RuleTestCase { diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 31c64ad9..5fd80dcc 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -36,6 +36,7 @@ /** * @extends RuleTestCase + * @runInSeparateProcess */ class EntityColumnRuleTest extends RuleTestCase { diff --git a/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php b/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php index a94fdfde..5427f788 100644 --- a/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php @@ -9,6 +9,7 @@ /** * @extends RuleTestCase + * @runInSeparateProcess */ class EntityConstructorNotFinalRuleTest extends RuleTestCase { diff --git a/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php b/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php index 65b8f209..b5881adb 100644 --- a/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php @@ -9,6 +9,7 @@ /** * @extends RuleTestCase + * @runInSeparateProcess */ class EntityMappingExceptionRuleTest extends RuleTestCase { diff --git a/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php b/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php index d3f05395..a1474f56 100644 --- a/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php @@ -9,6 +9,7 @@ /** * @extends RuleTestCase + * @runInSeparateProcess */ class EntityNotFinalRuleTest extends RuleTestCase { diff --git a/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php b/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php index 7e81545f..477b9157 100644 --- a/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php @@ -10,6 +10,7 @@ /** * @extends RuleTestCase + * @runInSeparateProcess */ class EntityRelationRuleTest extends RuleTestCase { From 8a77f4ad8be87a2dee7370b1ccc8c3d58b6f8db0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 7 Oct 2025 07:58:58 +0200 Subject: [PATCH 149/160] Regenerate baseline (#693) --- phpstan-baseline-dbal-4.neon | 215 ++++++++++++++++++ .../ORM/FakeTestingRamseyUuidType.php | 2 +- .../ORM/FakeTestingSymfonyUlidType.php | 2 +- .../ORM/FakeTestingSymfonyUuidType.php | 2 +- 4 files changed, 218 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline-dbal-4.neon b/phpstan-baseline-dbal-4.neon index 24536b91..2e0880ce 100644 --- a/phpstan-baseline-dbal-4.neon +++ b/phpstan-baseline-dbal-4.neon @@ -30,4 +30,219 @@ parameters: count: 1 path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + - + rawMessage: ''' + Call to method __construct() of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Classes/entity-manager.php + + - + rawMessage: ''' + Instantiation of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Classes/entity-manager.php + + - + rawMessage: ''' + Call to method __construct() of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/DoctrineIntegration/ORM/entity-manager.php + + - + rawMessage: ''' + Instantiation of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/DoctrineIntegration/ORM/entity-manager.php + + - + rawMessage: ''' + Call to method __construct() of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + rawMessage: ''' + Instantiation of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + rawMessage: ''' + Call to method __construct() of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Rules/DeadCode/entity-manager.php + + - + rawMessage: ''' + Instantiation of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Rules/DeadCode/entity-manager.php + + - + rawMessage: 'Parameter #2 $type of static method Doctrine\DBAL\Types\Type::addType() expects class-string|Doctrine\DBAL\Types\Type, string given.' + identifier: argument.type + count: 1 + path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php + + - + rawMessage: ''' + Call to method __construct() of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Rules/Properties/entity-manager.php + + - + rawMessage: ''' + Instantiation of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Rules/Properties/entity-manager.php + + - + rawMessage: ''' + Call to method __construct() of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/mysqli.php + + - + rawMessage: ''' + Instantiation of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/mysqli.php + + - + rawMessage: ''' + Call to method __construct() of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: method.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/pdo.php + + - + rawMessage: ''' + Instantiation of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/pdo.php + + - + rawMessage: 'Call to an undefined static method Doctrine\DBAL\Types\ConversionException::conversionFailed().' + identifier: staticMethod.notFound + count: 2 + path: tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php + + - + rawMessage: 'Call to an undefined static method Doctrine\DBAL\Types\ConversionException::conversionFailedInvalidType().' + identifier: staticMethod.notFound + count: 2 + path: tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php + + - + rawMessage: 'Call to an undefined static method Doctrine\DBAL\Types\ConversionException::conversionFailed().' + identifier: staticMethod.notFound + count: 2 + path: tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php + + - + rawMessage: 'Call to an undefined static method Doctrine\DBAL\Types\ConversionException::conversionFailedInvalidType().' + identifier: staticMethod.notFound + count: 2 + path: tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php + - + rawMessage: ''' + Access to constant on deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + rawMessage: ''' + Call to method __construct() of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: method.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + rawMessage: ''' + Instantiation of deprecated class Doctrine\ORM\Mapping\Driver\AnnotationDriver: + This class will be removed in 3.0 without replacement. + Copyright (c) Doctrine Project + From https://github.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver.php + ''' + identifier: new.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php diff --git a/tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php b/tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php index a81fdc64..51594f97 100644 --- a/tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php +++ b/tests/Rules/Doctrine/ORM/FakeTestingRamseyUuidType.php @@ -56,7 +56,7 @@ public function getName(): string return self::NAME; } - public function requiresSQLCommentHint(AbstractPlatform $platform): bool + public function requiresSQLCommentHint(AbstractPlatform $platform): bool // @phpstan-ignore return.tooWideBool { return true; } diff --git a/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php index 389303bf..bd6b87c0 100644 --- a/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php +++ b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUlidType.php @@ -100,7 +100,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str } } - public function requiresSQLCommentHint(AbstractPlatform $platform): bool + public function requiresSQLCommentHint(AbstractPlatform $platform): bool // @phpstan-ignore return.tooWideBool { return true; } diff --git a/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php index 89d853ef..8217af22 100644 --- a/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php +++ b/tests/Rules/Doctrine/ORM/FakeTestingSymfonyUuidType.php @@ -100,7 +100,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str } } - public function requiresSQLCommentHint(AbstractPlatform $platform): bool + public function requiresSQLCommentHint(AbstractPlatform $platform): bool // @phpstan-ignore return.tooWideBool { return true; } From 38fc20dce359703f48c076c9a9bab0e2cc30c533 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 7 Oct 2025 11:20:35 +0200 Subject: [PATCH 150/160] Move phpstan analyse paths into phpstan.neon (#694) --- Makefile | 4 ++-- phpstan.neon | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index efc169db..8c112038 100644 --- a/Makefile +++ b/Makefile @@ -27,8 +27,8 @@ cs-fix: .PHONY: phpstan phpstan: - php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests + php vendor/bin/phpstan analyse -c phpstan.neon .PHONY: phpstan-generate-baseline phpstan-generate-baseline: - php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests -b phpstan-baseline.neon + php vendor/bin/phpstan analyse -c phpstan.neon -b phpstan-baseline.neon diff --git a/phpstan.neon b/phpstan.neon index f701caa0..eabc5225 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -13,6 +13,11 @@ includes: - phar://phpstan.phar/conf/bleedingEdge.neon parameters: + level: 8 + paths: + - src + - tests + excludePaths: - tests/*/data/* - tests/*/data-attributes/* From 9b118ac640b163a7cfe3ace4bc79130d2e2a468c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 8 Oct 2025 12:02:52 +0200 Subject: [PATCH 151/160] Set up mutation testing --- .github/workflows/build.yml | 55 ++++++++++++++++++++ infection.json5 | 17 ++++++ phpstan.neon | 3 ++ tests/Infection/TrinaryLogicMutator.php | 69 +++++++++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 infection.json5 create mode 100644 tests/Infection/TrinaryLogicMutator.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 17328a08..de64c74b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,6 +145,55 @@ jobs: - name: "Tests" run: "make tests" + mutation-testing: + name: "Mutation Testing" + runs-on: "ubuntu-latest" + needs: ["tests", "static-analysis"] + + strategy: + fail-fast: false + matrix: + php-version: + - "8.2" + - "8.3" + - "8.4" + + steps: + - name: "Checkout" + uses: actions/checkout@v5 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "pcov" + php-version: "${{ matrix.php-version }}" + ini-file: development + extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb + tools: infection:0.31.4 + + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - uses: "actions/download-artifact@v4" + with: + name: "result-cache-${{ matrix.php-version }}" + path: "tmp/" + + - name: "Run infection" + run: | + git fetch --depth=1 origin $GITHUB_BASE_REF + infection --git-diff-base=origin/$GITHUB_BASE_REF --git-diff-lines --ignore-msi-with-no-mutations --min-msi=100 --min-covered-msi=100 --log-verbosity=all --debug + + - uses: "actions/upload-artifact@v4" + if: always() + with: + name: "infection-log-${{ matrix.php-version }}" + path: "tmp/infection.log" + static-analysis: name: "PHPStan" runs-on: "ubuntu-latest" @@ -189,3 +238,9 @@ jobs: - name: "PHPStan" run: "make phpstan" + + - uses: "actions/upload-artifact@v4" + with: + # "update-packages" is not relevant for the download-artifact counterpart, but we need it here to get unique artifact names across all jobs + name: "result-cache-${{ matrix.php-version }}${{ matrix.update-packages && '-packages-updated' || '' }}" + path: "tmp/resultCache.php" diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 00000000..7afd7230 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,17 @@ +{ + "$schema": "vendor/infection/infection/resources/schema.json", + "timeout": 30, + "source": { + "directories": [ + "src" + ] + }, + "staticAnalysisTool": "phpstan", + "logs": { + "text": "tmp/infection.log" + }, + "mutators": { + "@default": false, + "PHPStan\\Infection\\TrinaryLogicMutator": true + } +} diff --git a/phpstan.neon b/phpstan.neon index eabc5225..385b9690 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -18,11 +18,14 @@ parameters: - src - tests + resultCachePath: tmp/resultCache.php + excludePaths: - tests/*/data/* - tests/*/data-attributes/* - tests/*/data-php-*/* - tests/Rules/Doctrine/ORM/entity-manager.php + - tests/Infection/ reportUnmatchedIgnoredErrors: false diff --git a/tests/Infection/TrinaryLogicMutator.php b/tests/Infection/TrinaryLogicMutator.php new file mode 100644 index 00000000..67e9b530 --- /dev/null +++ b/tests/Infection/TrinaryLogicMutator.php @@ -0,0 +1,69 @@ + + */ +final class TrinaryLogicMutator implements Mutator +{ + + public static function getDefinition(): Definition + { + return new Definition( + <<<'TXT' + Replaces TrinaryLogic->yes() with !TrinaryLogic->no() and vice versa. + TXT + , + MutatorCategory::ORTHOGONAL_REPLACEMENT, + null, + <<<'DIFF' + - $type->isBoolean()->yes(); + + !$type->isBoolean()->no(); + DIFF, + ); + } + + public function getName(): string + { + return 'TrinaryLogicMutator'; + } + + public function canMutate(Node $node): bool + { + if (!$node instanceof Node\Expr\MethodCall) { + return false; + } + + if (!$node->name instanceof Node\Identifier) { + return false; + } + + if (!in_array($node->name->name, ['yes', 'no'], true)) { + return false; + } + + return true; + } + + public function mutate(Node $node): iterable + { + if (!$node->name instanceof Node\Identifier) { + throw new LogicException(); + } + + if ($node->name->name === 'yes') { + yield new Node\Expr\BooleanNot(new Node\Expr\MethodCall($node->var, 'no')); + } else { + yield new Node\Expr\BooleanNot(new Node\Expr\MethodCall($node->var, 'yes')); + } + } + +} From 31a924b443a3c15a851c3a8bd0a9f672d3aedc35 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 10 Oct 2025 14:49:29 +0200 Subject: [PATCH 152/160] Print infection output instead of uploading the log as artifact --- .github/workflows/build.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de64c74b..991d22ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -169,7 +169,7 @@ jobs: php-version: "${{ matrix.php-version }}" ini-file: development extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb - tools: infection:0.31.4 + tools: infection:0.31.6 - name: "Allow installing on PHP 8.4" if: matrix.php-version == '8.4' @@ -186,13 +186,15 @@ jobs: - name: "Run infection" run: | git fetch --depth=1 origin $GITHUB_BASE_REF - infection --git-diff-base=origin/$GITHUB_BASE_REF --git-diff-lines --ignore-msi-with-no-mutations --min-msi=100 --min-covered-msi=100 --log-verbosity=all --debug - - - uses: "actions/upload-artifact@v4" - if: always() - with: - name: "infection-log-${{ matrix.php-version }}" - path: "tmp/infection.log" + infection \ + --git-diff-base=origin/$GITHUB_BASE_REF \ + --git-diff-lines \ + --ignore-msi-with-no-mutations \ + --min-msi=100 \ + --min-covered-msi=100 \ + --log-verbosity=all \ + --debug \ + --logger-text=php://stdout static-analysis: name: "PHPStan" From 8fe637e22f0a4a7a81f68858b4c6b7b995ef7c92 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 26 Oct 2025 09:12:28 +0100 Subject: [PATCH 153/160] Utilize build-infection Thank you! Now we can do it on more repos :) --- .github/workflows/build.yml | 38 ++++++++++---- infection.json5 | 17 ------ phpstan.neon | 1 - tests/Infection/TrinaryLogicMutator.php | 69 ------------------------- 4 files changed, 28 insertions(+), 97 deletions(-) delete mode 100644 infection.json5 delete mode 100644 tests/Infection/TrinaryLogicMutator.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 991d22ed..635d82da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,10 @@ on: branches: - "2.0.x" +concurrency: + group: build-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + jobs: lint: name: "Lint" @@ -169,7 +173,7 @@ jobs: php-version: "${{ matrix.php-version }}" ini-file: development extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb - tools: infection:0.31.6 + tools: infection:0.31.7 - name: "Allow installing on PHP 8.4" if: matrix.php-version == '8.4' @@ -178,10 +182,30 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - uses: "actions/download-artifact@v4" + - name: "Checkout build-infection" + uses: actions/checkout@v5 + with: + repository: "phpstan/build-infection" + path: "build-infection" + ref: "1.x" + + - name: "Install build-infection dependencies" + working-directory: "build-infection" + run: "composer install --no-interaction --no-progress" + + - name: "Configure infection" + run: | + php build-infection/bin/infection-config.php \ + > infection.json5 + cat infection.json5 | jq + + - name: "Cache Result cache" + uses: actions/cache@v4 with: - name: "result-cache-${{ matrix.php-version }}" - path: "tmp/" + path: ./tmp + key: "result-cache-v1-${{ matrix.php-version }}-${{ github.run_id }}" + restore-keys: | + result-cache-v1-${{ matrix.php-version }}- - name: "Run infection" run: | @@ -240,9 +264,3 @@ jobs: - name: "PHPStan" run: "make phpstan" - - - uses: "actions/upload-artifact@v4" - with: - # "update-packages" is not relevant for the download-artifact counterpart, but we need it here to get unique artifact names across all jobs - name: "result-cache-${{ matrix.php-version }}${{ matrix.update-packages && '-packages-updated' || '' }}" - path: "tmp/resultCache.php" diff --git a/infection.json5 b/infection.json5 deleted file mode 100644 index 7afd7230..00000000 --- a/infection.json5 +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "vendor/infection/infection/resources/schema.json", - "timeout": 30, - "source": { - "directories": [ - "src" - ] - }, - "staticAnalysisTool": "phpstan", - "logs": { - "text": "tmp/infection.log" - }, - "mutators": { - "@default": false, - "PHPStan\\Infection\\TrinaryLogicMutator": true - } -} diff --git a/phpstan.neon b/phpstan.neon index 385b9690..b5f35ecc 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -25,7 +25,6 @@ parameters: - tests/*/data-attributes/* - tests/*/data-php-*/* - tests/Rules/Doctrine/ORM/entity-manager.php - - tests/Infection/ reportUnmatchedIgnoredErrors: false diff --git a/tests/Infection/TrinaryLogicMutator.php b/tests/Infection/TrinaryLogicMutator.php deleted file mode 100644 index 67e9b530..00000000 --- a/tests/Infection/TrinaryLogicMutator.php +++ /dev/null @@ -1,69 +0,0 @@ - - */ -final class TrinaryLogicMutator implements Mutator -{ - - public static function getDefinition(): Definition - { - return new Definition( - <<<'TXT' - Replaces TrinaryLogic->yes() with !TrinaryLogic->no() and vice versa. - TXT - , - MutatorCategory::ORTHOGONAL_REPLACEMENT, - null, - <<<'DIFF' - - $type->isBoolean()->yes(); - + !$type->isBoolean()->no(); - DIFF, - ); - } - - public function getName(): string - { - return 'TrinaryLogicMutator'; - } - - public function canMutate(Node $node): bool - { - if (!$node instanceof Node\Expr\MethodCall) { - return false; - } - - if (!$node->name instanceof Node\Identifier) { - return false; - } - - if (!in_array($node->name->name, ['yes', 'no'], true)) { - return false; - } - - return true; - } - - public function mutate(Node $node): iterable - { - if (!$node->name instanceof Node\Identifier) { - throw new LogicException(); - } - - if ($node->name->name === 'yes') { - yield new Node\Expr\BooleanNot(new Node\Expr\MethodCall($node->var, 'no')); - } else { - yield new Node\Expr\BooleanNot(new Node\Expr\MethodCall($node->var, 'yes')); - } - } - -} From 5e5fc80f3fdcea4ef554f188a9f41629fafcdb21 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 26 Oct 2025 21:00:26 +0100 Subject: [PATCH 154/160] Setup PHP Infection composite action --- .github/workflows/build.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 635d82da..0dfbd37c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -166,14 +166,17 @@ jobs: - name: "Checkout" uses: actions/checkout@v5 - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + - name: "Checkout build-infection" + uses: actions/checkout@v5 + with: + repository: "phpstan/build-infection" + path: "build-infection" + ref: "1.x" + + - uses: ./build-infection/.github/actions/setup-php with: - coverage: "pcov" php-version: "${{ matrix.php-version }}" - ini-file: development - extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb - tools: infection:0.31.7 + php-extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb - name: "Allow installing on PHP 8.4" if: matrix.php-version == '8.4' @@ -182,13 +185,6 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Checkout build-infection" - uses: actions/checkout@v5 - with: - repository: "phpstan/build-infection" - path: "build-infection" - ref: "1.x" - - name: "Install build-infection dependencies" working-directory: "build-infection" run: "composer install --no-interaction --no-progress" From ca50bd1278af04151f83a64cdb822d256e448fc6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Oct 2025 09:30:49 +0100 Subject: [PATCH 155/160] Skip DocumentManagerTypeInferenceTest on PHP8+ --- .../ODM/DocumentManagerTypeInferenceTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/DoctrineIntegration/ODM/DocumentManagerTypeInferenceTest.php b/tests/DoctrineIntegration/ODM/DocumentManagerTypeInferenceTest.php index 8fe0c036..050e92f0 100644 --- a/tests/DoctrineIntegration/ODM/DocumentManagerTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ODM/DocumentManagerTypeInferenceTest.php @@ -5,7 +5,7 @@ use PHPStan\Testing\TypeInferenceTestCase; use const PHP_VERSION_ID; -class DocumentManagerTypeInferenceTest extends TypeInferenceTestCase +final class DocumentManagerTypeInferenceTest extends TypeInferenceTestCase { /** @@ -13,10 +13,6 @@ class DocumentManagerTypeInferenceTest extends TypeInferenceTestCase */ public function dataFileAsserts(): iterable { - if (PHP_VERSION_ID >= 80000) { - return []; - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/documentManagerDynamicReturn.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/documentRepositoryDynamicReturn.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/documentManagerMergeReturn.php'); @@ -33,6 +29,10 @@ public function testFileAsserts( ...$args ): void { + if (PHP_VERSION_ID >= 80000) { + self::markTestSkipped('Test requires PHP 7.'); + } + $this->assertFileAsserts($assertType, $file, ...$args); } From 197d47cb06b5124edd7cdf055606be50405ebb7e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Oct 2025 13:50:51 +0100 Subject: [PATCH 156/160] Update phpstan-phpunit to 2.0.8 (#706) --- composer.json | 2 +- .../ODM/DocumentManagerTypeInferenceTest.php | 3 - .../ORM/EntityManagerTypeInferenceTest.php | 3 - ...utObjectManagerLoaderTypeInferenceTest.php | 3 - .../ManagerRegistryTypeInferenceTest.php | 3 - .../DoctrineIntegration/TypeInferenceTest.php | 3 - ...eryResultTypeWalkerFetchTypeMatrixTest.php | 5214 ++++++++--------- tests/Type/Doctrine/NewExprTest.php | 3 - ...ueryBuilderTypeSpecifyingExtensionTest.php | 3 - 9 files changed, 2608 insertions(+), 2629 deletions(-) diff --git a/composer.json b/composer.json index 9185e8b9..8a54ff34 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "nesbot/carbon": "^2.49", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-deprecation-rules": "^2.0.2", - "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-phpunit": "^2.0.8", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", diff --git a/tests/DoctrineIntegration/ODM/DocumentManagerTypeInferenceTest.php b/tests/DoctrineIntegration/ODM/DocumentManagerTypeInferenceTest.php index 050e92f0..a5ff018d 100644 --- a/tests/DoctrineIntegration/ODM/DocumentManagerTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ODM/DocumentManagerTypeInferenceTest.php @@ -8,9 +8,6 @@ final class DocumentManagerTypeInferenceTest extends TypeInferenceTestCase { - /** - * @return iterable - */ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/documentManagerDynamicReturn.php'); diff --git a/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php b/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php index 086a6e0e..52898ad2 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php @@ -9,9 +9,6 @@ class EntityManagerTypeInferenceTest extends TypeInferenceTestCase { - /** - * @return iterable - */ public function dataFileAsserts(): iterable { $ormVersion = InstalledVersions::getVersion('doctrine/orm'); diff --git a/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php b/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php index 25308635..56c53fc4 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php @@ -9,9 +9,6 @@ class EntityManagerWithoutObjectManagerLoaderTypeInferenceTest extends TypeInferenceTestCase { - /** - * @return iterable - */ public function dataFileAsserts(): iterable { $ormVersion = InstalledVersions::getVersion('doctrine/orm'); diff --git a/tests/DoctrineIntegration/Persistence/ManagerRegistryTypeInferenceTest.php b/tests/DoctrineIntegration/Persistence/ManagerRegistryTypeInferenceTest.php index d35df4a2..a25ff2cf 100644 --- a/tests/DoctrineIntegration/Persistence/ManagerRegistryTypeInferenceTest.php +++ b/tests/DoctrineIntegration/Persistence/ManagerRegistryTypeInferenceTest.php @@ -7,9 +7,6 @@ class ManagerRegistryTypeInferenceTest extends TypeInferenceTestCase { - /** - * @return iterable - */ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/managerRegistryRepositoryDynamicReturn.php'); diff --git a/tests/DoctrineIntegration/TypeInferenceTest.php b/tests/DoctrineIntegration/TypeInferenceTest.php index ba2c4fd6..0382118b 100644 --- a/tests/DoctrineIntegration/TypeInferenceTest.php +++ b/tests/DoctrineIntegration/TypeInferenceTest.php @@ -7,9 +7,6 @@ class TypeInferenceTest extends TypeInferenceTestCase { - /** - * @return iterable - */ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/getRepository.php'); diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 04cbb552..28a127c3 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -630,3803 +630,3803 @@ public static function provideCases(): iterable { yield ' -1' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT -1 FROM %s t', - 'mysql' => new ConstantIntegerType(-1), - 'sqlite' => new ConstantIntegerType(-1), - 'pdo_pgsql' => new ConstantIntegerType(-1), - 'pgsql' => new ConstantIntegerType(-1), - 'mssql' => self::mixed(), - 'mysqlResult' => -1, - 'sqliteResult' => -1, - 'pdoPgsqlResult' => -1, - 'pgsqlResult' => -1, - 'mssqlResult' => -1, + 'dqlTemplate' => 'SELECT -1 FROM %s t', + 'mysqlExpectedType' => new ConstantIntegerType(-1), + 'sqliteExpectedType' => new ConstantIntegerType(-1), + 'pdoPgsqlExpectedType' => new ConstantIntegerType(-1), + 'pgsqlExpectedType' => new ConstantIntegerType(-1), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => -1, + 'sqliteExpectedResult' => -1, + 'pdoPgsqlExpectedResult' => -1, + 'pgsqlExpectedResult' => -1, + 'mssqlExpectedResult' => -1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield ' 1' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT 1 FROM %s t', - 'mysql' => new ConstantIntegerType(1), - 'sqlite' => new ConstantIntegerType(1), - 'pdo_pgsql' => new ConstantIntegerType(1), - 'pgsql' => new ConstantIntegerType(1), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT 1 FROM %s t', + 'mysqlExpectedType' => new ConstantIntegerType(1), + 'sqliteExpectedType' => new ConstantIntegerType(1), + 'pdoPgsqlExpectedType' => new ConstantIntegerType(1), + 'pgsqlExpectedType' => new ConstantIntegerType(1), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield ' 1.0' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT 1.0 FROM %s t', - 'mysql' => new ConstantStringType('1.0'), - 'sqlite' => new ConstantFloatType(1.0), - 'pdo_pgsql' => new ConstantStringType('1.0'), - 'pgsql' => new ConstantStringType('1.0'), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT 1.0 FROM %s t', + 'mysqlExpectedType' => new ConstantStringType('1.0'), + 'sqliteExpectedType' => new ConstantFloatType(1.0), + 'pdoPgsqlExpectedType' => new ConstantStringType('1.0'), + 'pgsqlExpectedType' => new ConstantStringType('1.0'), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield ' 1.00' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT 1.00 FROM %s t', - 'mysql' => new ConstantStringType('1.00'), - 'sqlite' => new ConstantFloatType(1.0), - 'pdo_pgsql' => new ConstantStringType('1.00'), - 'pgsql' => new ConstantStringType('1.00'), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.00', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00', - 'pgsqlResult' => '1.00', - 'mssqlResult' => '1.00', + 'dqlTemplate' => 'SELECT 1.00 FROM %s t', + 'mysqlExpectedType' => new ConstantStringType('1.00'), + 'sqliteExpectedType' => new ConstantFloatType(1.0), + 'pdoPgsqlExpectedType' => new ConstantStringType('1.00'), + 'pgsqlExpectedType' => new ConstantStringType('1.00'), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.00', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00', + 'pgsqlExpectedResult' => '1.00', + 'mssqlExpectedResult' => '1.00', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield ' 0.1' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT 0.1 FROM %s t', - 'mysql' => new ConstantStringType('0.1'), - 'sqlite' => new ConstantFloatType(0.1), - 'pdo_pgsql' => new ConstantStringType('0.1'), - 'pgsql' => new ConstantStringType('0.1'), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.1', - 'sqliteResult' => 0.1, - 'pdoPgsqlResult' => '0.1', - 'pgsqlResult' => '0.1', - 'mssqlResult' => '.1', + 'dqlTemplate' => 'SELECT 0.1 FROM %s t', + 'mysqlExpectedType' => new ConstantStringType('0.1'), + 'sqliteExpectedType' => new ConstantFloatType(0.1), + 'pdoPgsqlExpectedType' => new ConstantStringType('0.1'), + 'pgsqlExpectedType' => new ConstantStringType('0.1'), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.1', + 'sqliteExpectedResult' => 0.1, + 'pdoPgsqlExpectedResult' => '0.1', + 'pgsqlExpectedResult' => '0.1', + 'mssqlExpectedResult' => '.1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield ' 0.10' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT 0.10 FROM %s t', - 'mysql' => new ConstantStringType('0.10'), - 'sqlite' => new ConstantFloatType(0.1), - 'pdo_pgsql' => new ConstantStringType('0.10'), - 'pgsql' => new ConstantStringType('0.10'), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.10', - 'sqliteResult' => 0.1, - 'pdoPgsqlResult' => '0.10', - 'pgsqlResult' => '0.10', - 'mssqlResult' => '.10', + 'dqlTemplate' => 'SELECT 0.10 FROM %s t', + 'mysqlExpectedType' => new ConstantStringType('0.10'), + 'sqliteExpectedType' => new ConstantFloatType(0.1), + 'pdoPgsqlExpectedType' => new ConstantStringType('0.10'), + 'pgsqlExpectedType' => new ConstantStringType('0.10'), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.10', + 'sqliteExpectedResult' => 0.1, + 'pdoPgsqlExpectedResult' => '0.10', + 'pgsqlExpectedResult' => '0.10', + 'mssqlExpectedResult' => '.10', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '0.125e0' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT 0.125e0 FROM %s t', - 'mysql' => new ConstantFloatType(0.125), - 'sqlite' => new ConstantFloatType(0.125), - 'pdo_pgsql' => new ConstantStringType('0.125'), - 'pgsql' => new ConstantStringType('0.125'), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', - 'pgsqlResult' => '0.125', - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT 0.125e0 FROM %s t', + 'mysqlExpectedType' => new ConstantFloatType(0.125), + 'sqliteExpectedType' => new ConstantFloatType(0.125), + 'pdoPgsqlExpectedType' => new ConstantStringType('0.125'), + 'pgsqlExpectedType' => new ConstantStringType('0.125'), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => '0.125', + 'pgsqlExpectedResult' => '0.125', + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield ' 1e0' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT 1e0 FROM %s t', - 'mysql' => new ConstantFloatType(1.0), - 'sqlite' => new ConstantFloatType(1.0), - 'pdo_pgsql' => new ConstantStringType('1'), - 'pgsql' => new ConstantStringType('1'), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT 1e0 FROM %s t', + 'mysqlExpectedType' => new ConstantFloatType(1.0), + 'sqliteExpectedType' => new ConstantFloatType(1.0), + 'pdoPgsqlExpectedType' => new ConstantStringType('1'), + 'pgsqlExpectedType' => new ConstantStringType('1'), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield " '1'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT '1' FROM %s t", - 'mysql' => new ConstantStringType('1'), - 'sqlite' => new ConstantStringType('1'), - 'pdo_pgsql' => new ConstantStringType('1'), - 'pgsql' => new ConstantStringType('1'), - 'mssql' => self::mixed(), - 'mysqlResult' => '1', - 'sqliteResult' => '1', - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => '1', + 'dqlTemplate' => "SELECT '1' FROM %s t", + 'mysqlExpectedType' => new ConstantStringType('1'), + 'sqliteExpectedType' => new ConstantStringType('1'), + 'pdoPgsqlExpectedType' => new ConstantStringType('1'), + 'pgsqlExpectedType' => new ConstantStringType('1'), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => '1', + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => '1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield " '1e0'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT '1e0' FROM %s t", - 'mysql' => new ConstantStringType('1e0'), - 'sqlite' => new ConstantStringType('1e0'), - 'pdo_pgsql' => new ConstantStringType('1e0'), - 'pgsql' => new ConstantStringType('1e0'), - 'mssql' => self::mixed(), - 'mysqlResult' => '1e0', - 'sqliteResult' => '1e0', - 'pdoPgsqlResult' => '1e0', - 'pgsqlResult' => '1e0', - 'mssqlResult' => '1e0', + 'dqlTemplate' => "SELECT '1e0' FROM %s t", + 'mysqlExpectedType' => new ConstantStringType('1e0'), + 'sqliteExpectedType' => new ConstantStringType('1e0'), + 'pdoPgsqlExpectedType' => new ConstantStringType('1e0'), + 'pgsqlExpectedType' => new ConstantStringType('1e0'), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1e0', + 'sqliteExpectedResult' => '1e0', + 'pdoPgsqlExpectedResult' => '1e0', + 'pgsqlExpectedResult' => '1e0', + 'mssqlExpectedResult' => '1e0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '1 + 1' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (1 + 1) FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 2, - 'sqliteResult' => 2, - 'pdoPgsqlResult' => 2, - 'pgsqlResult' => 2, - 'mssqlResult' => 2, + 'dqlTemplate' => 'SELECT (1 + 1) FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2, + 'sqliteExpectedResult' => 2, + 'pdoPgsqlExpectedResult' => 2, + 'pgsqlExpectedResult' => 2, + 'mssqlExpectedResult' => 2, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "1 + 'foo'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT (1 + 'foo') FROM %s t", - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Invalid text representation - 'pgsql' => null, // Invalid text representation - 'mssql' => null, // Conversion failed - 'mysqlResult' => 1.0, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT (1 + 'foo') FROM %s t", + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Invalid text representation + 'pgsqlExpectedType' => null, // Invalid text representation + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "1 + '1.0'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT (1 + '1.0') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => null, // Invalid text representation - 'pgsql' => null, // Invalid text representation - 'mssql' => null, // Conversion failed - 'mysqlResult' => 2.0, - 'sqliteResult' => 2.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT (1 + '1.0') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => null, // Invalid text representation + 'pgsqlExpectedType' => null, // Invalid text representation + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => 2.0, + 'sqliteExpectedResult' => 2.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "1 + '1'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT (1 + '1') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 2.0, - 'sqliteResult' => 2, - 'pdoPgsqlResult' => 2, - 'pgsqlResult' => 2, - 'mssqlResult' => 2, + 'dqlTemplate' => "SELECT (1 + '1') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2.0, + 'sqliteExpectedResult' => 2, + 'pdoPgsqlExpectedResult' => 2, + 'pgsqlExpectedResult' => 2, + 'mssqlExpectedResult' => 2, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "1 + '1e0'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT (1 + '1e0') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => null, // Invalid text representation - 'pgsql' => null, // Invalid text representation - 'mssql' => null, // Conversion failed - 'mysqlResult' => 2.0, - 'sqliteResult' => 2.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT (1 + '1e0') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => null, // Invalid text representation + 'pgsqlExpectedType' => null, // Invalid text representation + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => 2.0, + 'sqliteExpectedResult' => 2.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '1 + 1 * 1 - 1' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (1 + 1 * 1 - 1) FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT (1 + 1 * 1 - 1) FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '1 + 1 * 1 / 1 - 1' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (1 + 1 * 1 / 1 - 1) FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT (1 + 1 * 1 / 1 - 1) FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int + t.col_int' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int + t.col_int FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 18, - 'sqliteResult' => 18, - 'pdoPgsqlResult' => 18, - 'pgsqlResult' => 18, - 'mssqlResult' => 18, + 'dqlTemplate' => 'SELECT t.col_int + t.col_int FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 18, + 'sqliteExpectedResult' => 18, + 'pdoPgsqlExpectedResult' => 18, + 'pgsqlExpectedResult' => 18, + 'mssqlExpectedResult' => 18, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_bigint + t.col_bigint' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_bigint + t.col_bigint FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 4294967296, - 'sqliteResult' => 4294967296, - 'pdoPgsqlResult' => 4294967296, - 'pgsqlResult' => 4294967296, - 'mssqlResult' => '4294967296', + 'dqlTemplate' => 'SELECT t.col_bigint + t.col_bigint FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 4294967296, + 'sqliteExpectedResult' => 4294967296, + 'pdoPgsqlExpectedResult' => 4294967296, + 'pgsqlExpectedResult' => 4294967296, + 'mssqlExpectedResult' => '4294967296', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int + t.col_float' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int + t.col_float FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 9.125, - 'sqliteResult' => 9.125, - 'pdoPgsqlResult' => 9.125, - 'pgsqlResult' => 9.125, - 'mssqlResult' => 9.125, + 'dqlTemplate' => 'SELECT t.col_int + t.col_float FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 9.125, + 'sqliteExpectedResult' => 9.125, + 'pdoPgsqlExpectedResult' => 9.125, + 'pgsqlExpectedResult' => 9.125, + 'mssqlExpectedResult' => 9.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int + t.col_mixed' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int + t.col_mixed FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => 10, - 'sqliteResult' => 10, - 'pdoPgsqlResult' => 10, - 'pgsqlResult' => 10, - 'mssqlResult' => 10, + 'dqlTemplate' => 'SELECT t.col_int + t.col_mixed FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 10, + 'sqliteExpectedResult' => 10, + 'pdoPgsqlExpectedResult' => 10, + 'pgsqlExpectedResult' => 10, + 'mssqlExpectedResult' => 10, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_bigint + t.col_float' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_bigint + t.col_float FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 2147483648.125, - 'sqliteResult' => 2147483648.125, - 'pdoPgsqlResult' => 2147483648.125, - 'pgsqlResult' => 2147483648.125, - 'mssqlResult' => 2147483648.125, + 'dqlTemplate' => 'SELECT t.col_bigint + t.col_float FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2147483648.125, + 'sqliteExpectedResult' => 2147483648.125, + 'pdoPgsqlExpectedResult' => 2147483648.125, + 'pgsqlExpectedResult' => 2147483648.125, + 'mssqlExpectedResult' => 2147483648.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_bigint + t.col_float (int data)' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT t.col_bigint + t.col_float FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 2.0, - 'sqliteResult' => 2.0, - 'pdoPgsqlResult' => 2.0, - 'pgsqlResult' => 2.0, - 'mssqlResult' => 2.0, + 'dqlTemplate' => 'SELECT t.col_bigint + t.col_float FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2.0, + 'sqliteExpectedResult' => 2.0, + 'pdoPgsqlExpectedResult' => 2.0, + 'pgsqlExpectedResult' => 2.0, + 'mssqlExpectedResult' => 2.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_float + t.col_float' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_float + t.col_float FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.25, - 'sqliteResult' => 0.25, - 'pdoPgsqlResult' => 0.25, - 'pgsqlResult' => 0.25, - 'mssqlResult' => 0.25, + 'dqlTemplate' => 'SELECT t.col_float + t.col_float FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.25, + 'sqliteExpectedResult' => 0.25, + 'pdoPgsqlExpectedResult' => 0.25, + 'pgsqlExpectedResult' => 0.25, + 'mssqlExpectedResult' => 0.25, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int + t.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int + t.col_decimal FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '9.1', - 'sqliteResult' => 9.1, - 'pdoPgsqlResult' => '9.1', - 'pgsqlResult' => '9.1', - 'mssqlResult' => '9.1', + 'dqlTemplate' => 'SELECT t.col_int + t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '9.1', + 'sqliteExpectedResult' => 9.1, + 'pdoPgsqlExpectedResult' => '9.1', + 'pgsqlExpectedResult' => '9.1', + 'mssqlExpectedResult' => '9.1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int + t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT t.col_int + t.col_decimal FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '2.0', - 'sqliteResult' => 2, - 'pdoPgsqlResult' => '2.0', - 'pgsqlResult' => '2.0', - 'mssqlResult' => '2.0', + 'dqlTemplate' => 'SELECT t.col_int + t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '2.0', + 'sqliteExpectedResult' => 2, + 'pdoPgsqlExpectedResult' => '2.0', + 'pgsqlExpectedResult' => '2.0', + 'mssqlExpectedResult' => '2.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_float + t.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_float + t.col_decimal FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.225, - 'sqliteResult' => 0.225, - 'pdoPgsqlResult' => 0.225, - 'pgsqlResult' => 0.225, - 'mssqlResult' => 0.225, + 'dqlTemplate' => 'SELECT t.col_float + t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.225, + 'sqliteExpectedResult' => 0.225, + 'pdoPgsqlExpectedResult' => 0.225, + 'pgsqlExpectedResult' => 0.225, + 'mssqlExpectedResult' => 0.225, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_float + t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT t.col_float + t.col_decimal FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 2.0, - 'sqliteResult' => 2.0, - 'pdoPgsqlResult' => 2.0, - 'pgsqlResult' => 2.0, - 'mssqlResult' => 2.0, + 'dqlTemplate' => 'SELECT t.col_float + t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2.0, + 'sqliteExpectedResult' => 2.0, + 'pdoPgsqlExpectedResult' => 2.0, + 'pgsqlExpectedResult' => 2.0, + 'mssqlExpectedResult' => 2.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal + t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '2.0', - 'sqliteResult' => 2, - 'pdoPgsqlResult' => '2.0', - 'pgsqlResult' => '2.0', - 'mssqlResult' => '2.0', + 'dqlTemplate' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '2.0', + 'sqliteExpectedResult' => 2, + 'pdoPgsqlExpectedResult' => '2.0', + 'pgsqlExpectedResult' => '2.0', + 'mssqlExpectedResult' => '2.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int + t.col_float + t.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int + t.col_float + t.col_decimal FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 9.225, - 'sqliteResult' => 9.225, - 'pdoPgsqlResult' => 9.225, - 'pgsqlResult' => 9.225, - 'mssqlResult' => 9.225, + 'dqlTemplate' => 'SELECT t.col_int + t.col_float + t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 9.225, + 'sqliteExpectedResult' => 9.225, + 'pdoPgsqlExpectedResult' => 9.225, + 'pgsqlExpectedResult' => 9.225, + 'mssqlExpectedResult' => 9.225, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal + t.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.2', - 'sqliteResult' => 0.2, - 'pdoPgsqlResult' => '0.2', - 'pgsqlResult' => '0.2', - 'mssqlResult' => '.2', + 'dqlTemplate' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.2', + 'sqliteExpectedResult' => 0.2, + 'pdoPgsqlExpectedResult' => '0.2', + 'pgsqlExpectedResult' => '0.2', + 'mssqlExpectedResult' => '.2', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int + t.col_string' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int + t.col_string FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Error converting data type - 'mysqlResult' => 9.0, - 'sqliteResult' => 9, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_int + t.col_string FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Error converting data type + 'mysqlExpectedResult' => 9.0, + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int + t.col_string (int data)' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT t.col_int + t.col_string FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => self::mixed(), - 'mysqlResult' => 2.0, - 'sqliteResult' => 2, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 2, + 'dqlTemplate' => 'SELECT t.col_int + t.col_string FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2.0, + 'sqliteExpectedResult' => 2, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 2, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int + t.col_bool' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int + t.col_bool FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, - 'mssql' => self::mixed(), // Undefined function - 'mysqlResult' => 10, - 'sqliteResult' => 10, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 10, + 'dqlTemplate' => 'SELECT t.col_int + t.col_bool FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, + 'mssqlExpectedType' => self::mixed(), // Undefined function + 'mysqlExpectedResult' => 10, + 'sqliteExpectedResult' => 10, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 10, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_float + t.col_string' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_float + t.col_string FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Error converting data type - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_float + t.col_string FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Error converting data type + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_decimal + t.col_bool' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_decimal + t.col_bool FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => self::mixed(), - 'mysqlResult' => '1.1', - 'sqliteResult' => 1.1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => '1.1', + 'dqlTemplate' => 'SELECT t.col_decimal + t.col_bool FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.1', + 'sqliteExpectedResult' => 1.1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => '1.1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_decimal + t.col_string' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_decimal + t.col_string FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Error converting data type - 'mysqlResult' => 0.1, - 'sqliteResult' => 0.1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_decimal + t.col_string FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Error converting data type + 'mysqlExpectedResult' => 0.1, + 'sqliteExpectedResult' => 0.1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int + t.col_int_nullable' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int + t.col_int_nullable FROM %s t', - 'mysql' => self::intOrNull(), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => self::intOrNull(), - 'pgsql' => self::intOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_int + t.col_int_nullable FROM %s t', + 'mysqlExpectedType' => self::intOrNull(), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => self::intOrNull(), + 'pgsqlExpectedType' => self::intOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int / t.col_int' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int / t.col_int FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT t.col_int / t.col_int FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_bigint / t.col_bigint' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_bigint / t.col_bigint FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => '1', + 'dqlTemplate' => 'SELECT t.col_bigint / t.col_bigint FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => '1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int / t.col_float' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int / t.col_float FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 72.0, - 'sqliteResult' => 72.0, - 'pdoPgsqlResult' => 72.0, - 'pgsqlResult' => 72.0, - 'mssqlResult' => 72.0, + 'dqlTemplate' => 'SELECT t.col_int / t.col_float FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 72.0, + 'sqliteExpectedResult' => 72.0, + 'pdoPgsqlExpectedResult' => 72.0, + 'pgsqlExpectedResult' => 72.0, + 'mssqlExpectedResult' => 72.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int / t.col_float / t.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int / t.col_float / t.col_decimal FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 720.0, - 'sqliteResult' => 720.0, - 'pdoPgsqlResult' => 720.0, - 'pgsqlResult' => 720.0, - 'mssqlResult' => 720.0, + 'dqlTemplate' => 'SELECT t.col_int / t.col_float / t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 720.0, + 'sqliteExpectedResult' => 720.0, + 'pdoPgsqlExpectedResult' => 720.0, + 'pgsqlExpectedResult' => 720.0, + 'mssqlExpectedResult' => 720.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_bigint / t.col_float' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_bigint / t.col_float FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 17179869184.0, - 'sqliteResult' => 17179869184.0, - 'pdoPgsqlResult' => 17179869184.0, - 'pgsqlResult' => 17179869184.0, - 'mssqlResult' => 17179869184.0, + 'dqlTemplate' => 'SELECT t.col_bigint / t.col_float FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 17179869184.0, + 'sqliteExpectedResult' => 17179869184.0, + 'pdoPgsqlExpectedResult' => 17179869184.0, + 'pgsqlExpectedResult' => 17179869184.0, + 'mssqlExpectedResult' => 17179869184.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_float / t.col_float' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_float / t.col_float FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT t.col_float / t.col_float FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int / t.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int / t.col_decimal FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '90.0000', - 'sqliteResult' => 90.0, - 'pdoPgsqlResult' => '90.0000000000000000', - 'pgsqlResult' => '90.0000000000000000', - 'mssqlResult' => '90.000000', + 'dqlTemplate' => 'SELECT t.col_int / t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '90.0000', + 'sqliteExpectedResult' => 90.0, + 'pdoPgsqlExpectedResult' => '90.0000000000000000', + 'pgsqlExpectedResult' => '90.0000000000000000', + 'mssqlExpectedResult' => '90.000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int / t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT t.col_int / t.col_decimal FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => '1.000000', + 'dqlTemplate' => 'SELECT t.col_int / t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => '1.000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_float / t.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_float / t.col_decimal FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.25, - 'sqliteResult' => 1.25, - 'pdoPgsqlResult' => 1.25, - 'pgsqlResult' => 1.25, - 'mssqlResult' => 1.25, + 'dqlTemplate' => 'SELECT t.col_float / t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.25, + 'sqliteExpectedResult' => 1.25, + 'pdoPgsqlExpectedResult' => 1.25, + 'pgsqlExpectedResult' => 1.25, + 'mssqlExpectedResult' => 1.25, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal / t.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.00000', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => '1.000000', + 'dqlTemplate' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.00000', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => '1.000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_decimal / t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.00000', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => '1.000000', + 'dqlTemplate' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.00000', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => '1.000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_decimal / t.col_mixed' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_decimal / t.col_mixed FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.10000', - 'sqliteResult' => 0.1, - 'pdoPgsqlResult' => '0.10000000000000000000', - 'pgsqlResult' => '0.10000000000000000000', - 'mssqlResult' => '.100000000000', + 'dqlTemplate' => 'SELECT t.col_decimal / t.col_mixed FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.10000', + 'sqliteExpectedResult' => 0.1, + 'pdoPgsqlExpectedResult' => '0.10000000000000000000', + 'pgsqlExpectedResult' => '0.10000000000000000000', + 'mssqlExpectedResult' => '.100000000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int / t.col_string' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int / t.col_string FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Conversion failed - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_int / t.col_string FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int / t.col_string (int data)' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT t.col_int / t.col_string FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT t.col_int / t.col_string FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_string / t.col_int' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_string / t.col_int FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Conversion failed - 'mysqlResult' => 0.0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_string / t.col_int FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int / t.col_bool' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int / t.col_bool FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::int(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => self::mixed(), - 'mysqlResult' => '9.0000', - 'sqliteResult' => 9, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT t.col_int / t.col_bool FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '9.0000', + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_float / t.col_string' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_float / t.col_string FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Error converting data type - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_float / t.col_string FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Error converting data type + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_string / t.col_float' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_string / t.col_float FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Error converting data type - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_string / t.col_float FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Error converting data type + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_decimal / t.col_bool' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_decimal / t.col_bool FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => self::mixed(), - 'mysqlResult' => '0.10000', - 'sqliteResult' => 0.1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => '.100000', + 'dqlTemplate' => 'SELECT t.col_decimal / t.col_bool FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.10000', + 'sqliteExpectedResult' => 0.1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => '.100000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_decimal / t.col_bool (int data)' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT t.col_decimal / t.col_bool FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => self::mixed(), - 'mysqlResult' => '1.00000', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => '1.000000', + 'dqlTemplate' => 'SELECT t.col_decimal / t.col_bool FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.00000', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => '1.000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_decimal / t.col_string' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_decimal / t.col_string FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Error converting data type - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_decimal / t.col_string FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Error converting data type + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_string / t.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_string / t.col_decimal FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Error converting data type - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_string / t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Error converting data type + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 't.col_int / t.col_int_nullable' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int / t.col_int_nullable FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => self::intOrNull(), - 'pgsql' => self::intOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_int / t.col_int_nullable FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => self::intOrNull(), + 'pgsqlExpectedType' => self::intOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '1 - 1' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (1 - 1) FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT (1 - 1) FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '1 * 1' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (1 * 1) FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT (1 * 1) FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "1 * '1'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT (1 * '1') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => "SELECT (1 * '1') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "1 * '1.0'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT (1 * '1.0') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => null, // Invalid text representation - 'pgsql' => null, // Invalid text representation - 'mssql' => null, // Conversion failed - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT (1 * '1.0') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => null, // Invalid text representation + 'pgsqlExpectedType' => null, // Invalid text representation + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '1 / 1' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (1 / 1) FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT (1 / 1) FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '1 / 1.0' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (1 / 1.0) FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(true, true), - 'pgsql' => self::numericString(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => '1.000000', + 'dqlTemplate' => 'SELECT (1 / 1.0) FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::numericString(true, true), + 'pgsqlExpectedType' => self::numericString(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => '1.000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '1 / 1e0' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (1 / 1e0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(true, true), - 'pgsql' => self::numericString(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT (1 / 1e0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::numericString(true, true), + 'pgsqlExpectedType' => self::numericString(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "'foo' / 1" => [ 'data' => self::dataDefault(), - 'select' => "SELECT ('foo' / 1) FROM %s t", - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Invalid text representation - 'pgsql' => null, // Invalid text representation - 'mssql' => null, // Conversion failed - 'mysqlResult' => 0.0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT ('foo' / 1) FROM %s t", + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Invalid text representation + 'pgsqlExpectedType' => null, // Invalid text representation + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "1 / 'foo'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT (1 / 'foo') FROM %s t", - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Invalid text representation - 'pgsql' => null, // Invalid text representation - 'mssql' => null, // Conversion failed - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT (1 / 'foo') FROM %s t", + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Invalid text representation + 'pgsqlExpectedType' => null, // Invalid text representation + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "1 / '1'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT (1 / '1') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => "SELECT (1 / '1') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "'1' / 1" => [ 'data' => self::dataDefault(), - 'select' => "SELECT ('1' / 1) FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => "SELECT ('1' / 1) FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "1 / '1.0'" => [ 'data' => self::dataDefault(), - 'select' => "SELECT (1 / '1.0') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => null, // Invalid text representation - 'pgsql' => null, // Invalid text representation - 'mssql' => null, // Conversion failed - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT (1 / '1.0') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => null, // Invalid text representation + 'pgsqlExpectedType' => null, // Invalid text representation + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '2147483648 ' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT 2147483648 FROM %s t', - 'mysql' => new ConstantIntegerType(2147483648), - 'sqlite' => new ConstantIntegerType(2147483648), - 'pdo_pgsql' => new ConstantIntegerType(2147483648), - 'pgsql' => new ConstantIntegerType(2147483648), - 'mssql' => self::mixed(), - 'mysqlResult' => 2147483648, - 'sqliteResult' => 2147483648, - 'pdoPgsqlResult' => 2147483648, - 'pgsqlResult' => 2147483648, - 'mssqlResult' => '2147483648', + 'dqlTemplate' => 'SELECT 2147483648 FROM %s t', + 'mysqlExpectedType' => new ConstantIntegerType(2147483648), + 'sqliteExpectedType' => new ConstantIntegerType(2147483648), + 'pdoPgsqlExpectedType' => new ConstantIntegerType(2147483648), + 'pgsqlExpectedType' => new ConstantIntegerType(2147483648), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2147483648, + 'sqliteExpectedResult' => 2147483648, + 'pdoPgsqlExpectedResult' => 2147483648, + 'pgsqlExpectedResult' => 2147483648, + 'mssqlExpectedResult' => '2147483648', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "''" => [ 'data' => self::dataDefault(), - 'select' => 'SELECT \'\' FROM %s t', - 'mysql' => new ConstantStringType(''), - 'sqlite' => new ConstantStringType(''), - 'pdo_pgsql' => new ConstantStringType(''), - 'pgsql' => new ConstantStringType(''), - 'mssql' => self::mixed(), - 'mysqlResult' => '', - 'sqliteResult' => '', - 'pdoPgsqlResult' => '', - 'pgsqlResult' => '', - 'mssqlResult' => '', + 'dqlTemplate' => 'SELECT \'\' FROM %s t', + 'mysqlExpectedType' => new ConstantStringType(''), + 'sqliteExpectedType' => new ConstantStringType(''), + 'pdoPgsqlExpectedType' => new ConstantStringType(''), + 'pgsqlExpectedType' => new ConstantStringType(''), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '', + 'sqliteExpectedResult' => '', + 'pdoPgsqlExpectedResult' => '', + 'pgsqlExpectedResult' => '', + 'mssqlExpectedResult' => '', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '(TRUE)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (TRUE) FROM %s t', - 'mysql' => new ConstantIntegerType(1), - 'sqlite' => new ConstantIntegerType(1), - 'pdo_pgsql' => new ConstantBooleanType(true), - 'pgsql' => new ConstantBooleanType(true), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => true, - 'pgsqlResult' => true, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT (TRUE) FROM %s t', + 'mysqlExpectedType' => new ConstantIntegerType(1), + 'sqliteExpectedType' => new ConstantIntegerType(1), + 'pdoPgsqlExpectedType' => new ConstantBooleanType(true), + 'pgsqlExpectedType' => new ConstantBooleanType(true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => true, + 'pgsqlExpectedResult' => true, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_PG_BOOL, ]; yield '(FALSE)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT (FALSE) FROM %s t', - 'mysql' => new ConstantIntegerType(0), - 'sqlite' => new ConstantIntegerType(0), - 'pdo_pgsql' => new ConstantBooleanType(false), - 'pgsql' => new ConstantBooleanType(false), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => false, - 'pgsqlResult' => false, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT (FALSE) FROM %s t', + 'mysqlExpectedType' => new ConstantIntegerType(0), + 'sqliteExpectedType' => new ConstantIntegerType(0), + 'pdoPgsqlExpectedType' => new ConstantBooleanType(false), + 'pgsqlExpectedType' => new ConstantBooleanType(false), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => false, + 'pgsqlExpectedResult' => false, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_PG_BOOL, ]; yield 't.col_bool' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_bool FROM %s t', - 'mysql' => self::bool(), - 'sqlite' => self::bool(), - 'pdo_pgsql' => self::bool(), - 'pgsql' => self::bool(), - 'mssql' => self::bool(), - 'mysqlResult' => true, - 'sqliteResult' => true, - 'pdoPgsqlResult' => true, - 'pgsqlResult' => true, - 'mssqlResult' => true, + 'dqlTemplate' => 'SELECT t.col_bool FROM %s t', + 'mysqlExpectedType' => self::bool(), + 'sqliteExpectedType' => self::bool(), + 'pdoPgsqlExpectedType' => self::bool(), + 'pgsqlExpectedType' => self::bool(), + 'mssqlExpectedType' => self::bool(), + 'mysqlExpectedResult' => true, + 'sqliteExpectedResult' => true, + 'pdoPgsqlExpectedResult' => true, + 'pgsqlExpectedResult' => true, + 'mssqlExpectedResult' => true, 'stringify' => self::STRINGIFY_NONE, ]; yield 't.col_bool_nullable' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_bool_nullable FROM %s t', - 'mysql' => self::boolOrNull(), - 'sqlite' => self::boolOrNull(), - 'pdo_pgsql' => self::boolOrNull(), - 'pgsql' => self::boolOrNull(), - 'mssql' => self::boolOrNull(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT t.col_bool_nullable FROM %s t', + 'mysqlExpectedType' => self::boolOrNull(), + 'sqliteExpectedType' => self::boolOrNull(), + 'pdoPgsqlExpectedType' => self::boolOrNull(), + 'pgsqlExpectedType' => self::boolOrNull(), + 'mssqlExpectedType' => self::boolOrNull(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_NONE, ]; yield 'COALESCE(t.col_bool, t.col_bool)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_bool, t.col_bool) FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::bool(), - 'pgsql' => self::bool(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => true, - 'pgsqlResult' => true, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT COALESCE(t.col_bool, t.col_bool) FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::bool(), + 'pgsqlExpectedType' => self::bool(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => true, + 'pgsqlExpectedResult' => true, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_PG_BOOL, ]; yield 'COALESCE(t.col_decimal, t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT COALESCE(t.col_decimal, t.col_decimal) FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT COALESCE(t.col_decimal, t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_float, t.col_float)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_float, t.col_float) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => 0.125, - 'pgsqlResult' => 0.125, - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT COALESCE(t.col_float, t.col_float) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => 0.125, + 'pgsqlExpectedResult' => 0.125, + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_float, t.col_float) + int data' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT COALESCE(t.col_float, t.col_float) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT COALESCE(t.col_float, t.col_float) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_decimal FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::numericString(false, true), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::numericString(false, true), - 'mysqlResult' => '0.1', - 'sqliteResult' => '0.1', - 'pdoPgsqlResult' => '0.1', - 'pgsqlResult' => '0.1', - 'mssqlResult' => '.1', + 'dqlTemplate' => 'SELECT t.col_decimal FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::numericString(false, true), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::numericString(false, true), + 'mysqlExpectedResult' => '0.1', + 'sqliteExpectedResult' => '0.1', + 'pdoPgsqlExpectedResult' => '0.1', + 'pgsqlExpectedResult' => '0.1', + 'mssqlExpectedResult' => '.1', 'stringify' => self::STRINGIFY_NONE, ]; yield 't.col_int' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_int FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::int(), - 'mysqlResult' => 9, - 'sqliteResult' => 9, - 'pdoPgsqlResult' => 9, - 'pgsqlResult' => 9, - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT t.col_int FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::int(), + 'mysqlExpectedResult' => 9, + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => 9, + 'pgsqlExpectedResult' => 9, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_NONE, ]; yield 't.col_bigint' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_bigint FROM %s t', - 'mysql' => self::hasDbal4() ? self::int() : self::numericString(true, true), - 'sqlite' => self::hasDbal4() ? self::int() : self::numericString(true, true), - 'pdo_pgsql' => self::hasDbal4() ? self::int() : self::numericString(true, true), - 'pgsql' => self::hasDbal4() ? self::int() : self::numericString(true, true), - 'mssql' => self::hasDbal4() ? self::int() : self::numericString(true, true), - 'mysqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', - 'sqliteResult' => self::hasDbal4() ? 2147483648 : '2147483648', - 'pdoPgsqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', - 'pgsqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', - 'mssqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'dqlTemplate' => 'SELECT t.col_bigint FROM %s t', + 'mysqlExpectedType' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'sqliteExpectedType' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'pdoPgsqlExpectedType' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'pgsqlExpectedType' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'mssqlExpectedType' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'mysqlExpectedResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'sqliteExpectedResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'pdoPgsqlExpectedResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'pgsqlExpectedResult' => self::hasDbal4() ? 2147483648 : '2147483648', + 'mssqlExpectedResult' => self::hasDbal4() ? 2147483648 : '2147483648', 'stringify' => self::STRINGIFY_NONE, ]; yield 't.col_float' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_float FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::float(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => 0.125, - 'pgsqlResult' => 0.125, - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT t.col_float FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::float(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => 0.125, + 'pgsqlExpectedResult' => 0.125, + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_NONE, ]; yield 'AVG(t.col_float)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(t.col_float) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::floatOrNull(), - 'pgsql' => self::floatOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => 0.125, - 'pgsqlResult' => 0.125, - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT AVG(t.col_float) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::floatOrNull(), + 'pgsqlExpectedType' => self::floatOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => 0.125, + 'pgsqlExpectedResult' => 0.125, + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_float) + no data' => [ 'data' => self::dataNone(), - 'select' => 'SELECT AVG(t.col_float) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::floatOrNull(), - 'pgsql' => self::floatOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT AVG(t.col_float) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::floatOrNull(), + 'pgsqlExpectedType' => self::floatOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_float) + GROUP BY' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(t.col_float) FROM %s t GROUP BY t.col_int', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => 0.125, - 'pgsqlResult' => 0.125, - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT AVG(t.col_float) FROM %s t GROUP BY t.col_int', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => 0.125, + 'pgsqlExpectedResult' => 0.125, + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_float_nullable) + GROUP BY' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(t.col_float_nullable) FROM %s t GROUP BY t.col_int', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::floatOrNull(), - 'pgsql' => self::floatOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT AVG(t.col_float_nullable) FROM %s t GROUP BY t.col_int', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::floatOrNull(), + 'pgsqlExpectedType' => self::floatOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_decimal)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(false, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(false, true), - 'pgsql' => self::numericStringOrNull(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.10000', - 'sqliteResult' => 0.1, - 'pdoPgsqlResult' => '0.10000000000000000000', - 'pgsqlResult' => '0.10000000000000000000', - 'mssqlResult' => '.100000', + 'dqlTemplate' => 'SELECT AVG(t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(false, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(false, true), + 'pgsqlExpectedType' => self::numericStringOrNull(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.10000', + 'sqliteExpectedResult' => 0.1, + 'pdoPgsqlExpectedResult' => '0.10000000000000000000', + 'pgsqlExpectedResult' => '0.10000000000000000000', + 'mssqlExpectedResult' => '.100000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT AVG(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(false, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(false, true), - 'pgsql' => self::numericStringOrNull(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.00000', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => '1.000000', + 'dqlTemplate' => 'SELECT AVG(t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(false, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(false, true), + 'pgsqlExpectedType' => self::numericStringOrNull(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.00000', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => '1.000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(t.col_mixed)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(t.col_mixed) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::floatOrNull(), // always float|null, see https://www.sqlite.org/lang_aggfunc.html - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT AVG(t.col_mixed) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::floatOrNull(), // always float|null, see https://www.sqlite.org/lang_aggfunc.html + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '9.0000', - 'sqliteResult' => 9.0, - 'pdoPgsqlResult' => '9.0000000000000000', - 'pgsqlResult' => '9.0000000000000000', - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT AVG(t.col_int) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '9.0000', + 'sqliteExpectedResult' => 9.0, + 'pdoPgsqlExpectedResult' => '9.0000000000000000', + 'pgsqlExpectedResult' => '9.0000000000000000', + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(t.col_bool)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(t.col_bool) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // perand data type bit is invalid for avg operator. - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT AVG(t.col_bool) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // perand data type bit is invalid for avg operator. + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(t.col_string)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(t.col_string) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type nvarchar is invalid for avg operator - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT AVG(t.col_string) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type nvarchar is invalid for avg operator + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(1)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(1) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT AVG(1) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "AVG('1')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT AVG('1') FROM %s t", - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT AVG('1') FROM %s t", + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "AVG('1.0')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT AVG('1.0') FROM %s t", - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT AVG('1.0') FROM %s t", + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "AVG('1e0')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT AVG('1e0') FROM %s t", - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT AVG('1e0') FROM %s t", + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "AVG('foo')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT AVG('foo') FROM %s t", - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT AVG('foo') FROM %s t", + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(1) + GROUP BY' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(1) FROM %s t GROUP BY t.col_int', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(true, true), - 'pgsql' => self::numericString(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0000', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT AVG(1) FROM %s t GROUP BY t.col_int', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::numericString(true, true), + 'pgsqlExpectedType' => self::numericString(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0000', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(1.0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.00000', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => '1.000000', + 'dqlTemplate' => 'SELECT AVG(1.0) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.00000', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => '1.000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(1e0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.00000', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.00000000000000000000', - 'pgsqlResult' => '1.00000000000000000000', - 'mssqlResult' => '1.000000', + 'dqlTemplate' => 'SELECT AVG(1.0) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.00000', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.00000000000000000000', + 'pgsqlExpectedResult' => '1.00000000000000000000', + 'mssqlExpectedResult' => '1.000000', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'AVG(t.col_bigint)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT AVG(t.col_bigint) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '2147483648.0000', - 'sqliteResult' => 2147483648.0, - 'pdoPgsqlResult' => '2147483648.00000000', - 'pgsqlResult' => '2147483648.00000000', - 'mssqlResult' => '2147483648', + 'dqlTemplate' => 'SELECT AVG(t.col_bigint) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '2147483648.0000', + 'sqliteExpectedResult' => 2147483648.0, + 'pdoPgsqlExpectedResult' => '2147483648.00000000', + 'pgsqlExpectedResult' => '2147483648.00000000', + 'mssqlExpectedResult' => '2147483648', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(t.col_float)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(t.col_float) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::floatOrNull(), - 'pgsql' => self::floatOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => 0.125, - 'pgsqlResult' => 0.125, - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT SUM(t.col_float) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::floatOrNull(), + 'pgsqlExpectedType' => self::floatOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => 0.125, + 'pgsqlExpectedResult' => 0.125, + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SUM(t.col_float) + no data' => [ 'data' => self::dataNone(), - 'select' => 'SELECT SUM(t.col_float) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::floatOrNull(), - 'pgsql' => self::floatOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT SUM(t.col_float) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::floatOrNull(), + 'pgsqlExpectedType' => self::floatOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SUM(t.col_float) + GROUP BY' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(t.col_float) FROM %s t GROUP BY t.col_int', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => 0.125, - 'pgsqlResult' => 0.125, - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT SUM(t.col_float) FROM %s t GROUP BY t.col_int', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => 0.125, + 'pgsqlExpectedResult' => 0.125, + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield '1 + -(CASE WHEN MIN(t.col_float) = 0 THEN SUM(t.col_float) ELSE 0 END)' => [ // agg function (causing null) deeply inside AST 'data' => self::dataDefault(), - 'select' => 'SELECT 1 + -(CASE WHEN MIN(t.col_float) = 0 THEN SUM(t.col_float) ELSE 0 END) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::floatOrNull(), - 'pgsql' => self::floatOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT 1 + -(CASE WHEN MIN(t.col_float) = 0 THEN SUM(t.col_float) ELSE 0 END) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrIntOrNull(), + 'pdoPgsqlExpectedType' => self::floatOrNull(), + 'pgsqlExpectedType' => self::floatOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SUM(t.col_decimal)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(false, true), - 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(false, true), - 'pgsql' => self::numericStringOrNull(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.1', - 'sqliteResult' => 0.1, - 'pdoPgsqlResult' => '0.1', - 'pgsqlResult' => '0.1', - 'mssqlResult' => '.1', + 'dqlTemplate' => 'SELECT SUM(t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(false, true), + 'sqliteExpectedType' => self::floatOrIntOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(false, true), + 'pgsqlExpectedType' => self::numericStringOrNull(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.1', + 'sqliteExpectedResult' => 0.1, + 'pdoPgsqlExpectedResult' => '0.1', + 'pgsqlExpectedResult' => '0.1', + 'mssqlExpectedResult' => '.1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT SUM(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(false, true), - 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(false, true), - 'pgsql' => self::numericStringOrNull(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT SUM(t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(false, true), + 'sqliteExpectedType' => self::floatOrIntOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(false, true), + 'pgsqlExpectedType' => self::numericStringOrNull(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'mssql' => self::mixed(), - 'mysqlResult' => '9', - 'sqliteResult' => 9, - 'pdoPgsqlResult' => 9, - 'pgsqlResult' => 9, - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT SUM(t.col_int) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '9', + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => 9, + 'pgsqlExpectedResult' => 9, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '-SUM(t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT -SUM(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'mssql' => self::mixed(), - 'mysqlResult' => '-9', - 'sqliteResult' => -9, - 'pdoPgsqlResult' => -9, - 'pgsqlResult' => -9, - 'mssqlResult' => -9, + 'dqlTemplate' => 'SELECT -SUM(t.col_int) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '-9', + 'sqliteExpectedResult' => -9, + 'pdoPgsqlExpectedResult' => -9, + 'pgsqlExpectedResult' => -9, + 'mssqlExpectedResult' => -9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '-SUM(t.col_int) + no data' => [ 'data' => self::dataNone(), - 'select' => 'SELECT -SUM(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT -SUM(t.col_int) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(t.col_mixed)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(t.col_mixed) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => '1', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT SUM(t.col_mixed) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(t.col_bool)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(t.col_bool) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => '1', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT SUM(t.col_bool) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(t.col_string)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(t.col_string) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT SUM(t.col_string) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "SUM('foo')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT SUM('foo') FROM %s t", - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT SUM('foo') FROM %s t", + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "SUM('1')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT SUM('1') FROM %s t", - 'mysql' => self::floatOrNull(), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 1.0, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT SUM('1') FROM %s t", + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "SUM('1.0')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT SUM('1.0') FROM %s t", - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT SUM('1.0') FROM %s t", + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "SUM('1.1')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT SUM('1.1') FROM %s t", - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 1.1, - 'sqliteResult' => 1.1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT SUM('1.1') FROM %s t", + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 1.1, + 'sqliteExpectedResult' => 1.1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(1)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(1) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'mssql' => self::mixed(), - 'mysqlResult' => '1', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT SUM(1) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(1) + GROUP BY' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(1) FROM %s t GROUP BY t.col_int', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::int(), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), - 'pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), - 'mssql' => self::mixed(), - 'mysqlResult' => '1', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT SUM(1) FROM %s t GROUP BY t.col_int', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::numericString(true, true), self::int()), + 'pgsqlExpectedType' => TypeCombinator::union(self::numericString(true, true), self::int()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(1.0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT SUM(1.0) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(1e0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(1e0) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT SUM(1e0) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SUM(t.col_bigint)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT SUM(t.col_bigint) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), - 'mssql' => self::mixed(), - 'mysqlResult' => '2147483648', - 'sqliteResult' => 2147483648, - 'pdoPgsqlResult' => '2147483648', - 'pgsqlResult' => '2147483648', - 'mssqlResult' => '2147483648', + 'dqlTemplate' => 'SELECT SUM(t.col_bigint) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsqlExpectedType' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '2147483648', + 'sqliteExpectedResult' => 2147483648, + 'pdoPgsqlExpectedResult' => '2147483648', + 'pgsqlExpectedResult' => '2147483648', + 'mssqlExpectedResult' => '2147483648', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(t.col_float)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(t.col_float) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::floatOrNull(), - 'pgsql' => self::floatOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => 0.125, - 'pgsqlResult' => 0.125, - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT MAX(t.col_float) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::floatOrNull(), + 'pgsqlExpectedType' => self::floatOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => 0.125, + 'pgsqlExpectedResult' => 0.125, + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'MAX(t.col_float) + no data' => [ 'data' => self::dataNone(), - 'select' => 'SELECT MAX(t.col_float) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::floatOrNull(), - 'pgsql' => self::floatOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT MAX(t.col_float) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::floatOrNull(), + 'pgsqlExpectedType' => self::floatOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'MAX(t.col_float) + GROUP BY' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(t.col_float) FROM %s t GROUP BY t.col_int', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => 0.125, - 'pgsqlResult' => 0.125, - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT MAX(t.col_float) FROM %s t GROUP BY t.col_int', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => 0.125, + 'pgsqlExpectedResult' => 0.125, + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'MAX(t.col_decimal)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(false, true), - 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(false, true), - 'pgsql' => self::numericStringOrNull(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.1', - 'sqliteResult' => 0.1, - 'pdoPgsqlResult' => '0.1', - 'pgsqlResult' => '0.1', - 'mssqlResult' => '.1', + 'dqlTemplate' => 'SELECT MAX(t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(false, true), + 'sqliteExpectedType' => self::floatOrIntOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(false, true), + 'pgsqlExpectedType' => self::numericStringOrNull(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.1', + 'sqliteExpectedResult' => 0.1, + 'pdoPgsqlExpectedResult' => '0.1', + 'pgsqlExpectedResult' => '0.1', + 'mssqlExpectedResult' => '.1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT MAX(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(false, true), - 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(false, true), - 'pgsql' => self::numericStringOrNull(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT MAX(t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(false, true), + 'sqliteExpectedType' => self::floatOrIntOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(false, true), + 'pgsqlExpectedType' => self::numericStringOrNull(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(t.col_int) FROM %s t', - 'mysql' => self::intOrNull(), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => self::intOrNull(), - 'pgsql' => self::intOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => 9, - 'sqliteResult' => 9, - 'pdoPgsqlResult' => 9, - 'pgsqlResult' => 9, - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT MAX(t.col_int) FROM %s t', + 'mysqlExpectedType' => self::intOrNull(), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => self::intOrNull(), + 'pgsqlExpectedType' => self::intOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 9, + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => 9, + 'pgsqlExpectedResult' => 9, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(t.col_mixed)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(t.col_mixed) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT MAX(t.col_mixed) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(t.col_bool)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(t.col_bool) FROM %s t', - 'mysql' => self::intOrNull(), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT MAX(t.col_bool) FROM %s t', + 'mysqlExpectedType' => self::intOrNull(), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(t.col_string)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(t.col_string) FROM %s t', - 'mysql' => self::stringOrNull(), - 'sqlite' => self::stringOrNull(), - 'pdo_pgsql' => self::stringOrNull(), - 'pgsql' => self::stringOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => 'foobar', - 'sqliteResult' => 'foobar', - 'pdoPgsqlResult' => 'foobar', - 'pgsqlResult' => 'foobar', - 'mssqlResult' => 'foobar', + 'dqlTemplate' => 'SELECT MAX(t.col_string) FROM %s t', + 'mysqlExpectedType' => self::stringOrNull(), + 'sqliteExpectedType' => self::stringOrNull(), + 'pdoPgsqlExpectedType' => self::stringOrNull(), + 'pgsqlExpectedType' => self::stringOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 'foobar', + 'sqliteExpectedResult' => 'foobar', + 'pdoPgsqlExpectedResult' => 'foobar', + 'pgsqlExpectedResult' => 'foobar', + 'mssqlExpectedResult' => 'foobar', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "MAX('foobar')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT MAX('foobar') FROM %s t", - 'mysql' => TypeCombinator::addNull(self::string()), - 'sqlite' => TypeCombinator::addNull(self::string()), - 'pdo_pgsql' => TypeCombinator::addNull(self::string()), - 'pgsql' => TypeCombinator::addNull(self::string()), - 'mssql' => self::mixed(), - 'mysqlResult' => 'foobar', - 'sqliteResult' => 'foobar', - 'pdoPgsqlResult' => 'foobar', - 'pgsqlResult' => 'foobar', - 'mssqlResult' => 'foobar', + 'dqlTemplate' => "SELECT MAX('foobar') FROM %s t", + 'mysqlExpectedType' => TypeCombinator::addNull(self::string()), + 'sqliteExpectedType' => TypeCombinator::addNull(self::string()), + 'pdoPgsqlExpectedType' => TypeCombinator::addNull(self::string()), + 'pgsqlExpectedType' => TypeCombinator::addNull(self::string()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 'foobar', + 'sqliteExpectedResult' => 'foobar', + 'pdoPgsqlExpectedResult' => 'foobar', + 'pgsqlExpectedResult' => 'foobar', + 'mssqlExpectedResult' => 'foobar', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "MAX('1')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT MAX('1') FROM %s t", - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::numericStringOrNull(true, true), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1', - 'sqliteResult' => '1', - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => '1', + 'dqlTemplate' => "SELECT MAX('1') FROM %s t", + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::numericStringOrNull(true, true), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => '1', + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => '1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "MAX('1.0')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT MAX('1.0') FROM %s t", - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::numericStringOrNull(true, true), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => '1.0', - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => "SELECT MAX('1.0') FROM %s t", + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::numericStringOrNull(true, true), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => '1.0', + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(1)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(1) FROM %s t', - 'mysql' => self::intOrNull(), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => self::intOrNull(), - 'pgsql' => self::intOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT MAX(1) FROM %s t', + 'mysqlExpectedType' => self::intOrNull(), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => self::intOrNull(), + 'pgsqlExpectedType' => self::intOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(1) + GROUP BY' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(1) FROM %s t GROUP BY t.col_int', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT MAX(1) FROM %s t GROUP BY t.col_int', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(1.0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(true, true), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT MAX(1.0) FROM %s t', + 'mysqlExpectedType' => self::numericStringOrNull(true, true), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(1e0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(1e0) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(true, true), - 'pgsql' => self::numericStringOrNull(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT MAX(1e0) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericStringOrNull(true, true), + 'pgsqlExpectedType' => self::numericStringOrNull(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MAX(t.col_bigint)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MAX(t.col_bigint) FROM %s t', - 'mysql' => self::intOrNull(), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => self::intOrNull(), - 'pgsql' => self::intOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => 2147483648, - 'sqliteResult' => 2147483648, - 'pdoPgsqlResult' => 2147483648, - 'pgsqlResult' => 2147483648, - 'mssqlResult' => '2147483648', + 'dqlTemplate' => 'SELECT MAX(t.col_bigint) FROM %s t', + 'mysqlExpectedType' => self::intOrNull(), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => self::intOrNull(), + 'pgsqlExpectedType' => self::intOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2147483648, + 'sqliteExpectedResult' => 2147483648, + 'pdoPgsqlExpectedResult' => 2147483648, + 'pgsqlExpectedResult' => 2147483648, + 'mssqlExpectedResult' => '2147483648', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(t.col_float)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(t.col_float) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => 0.125, - 'pgsqlResult' => 0.125, - 'mssqlResult' => 0.125, + 'dqlTemplate' => 'SELECT ABS(t.col_float) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.125, + 'pdoPgsqlExpectedResult' => 0.125, + 'pgsqlExpectedResult' => 0.125, + 'mssqlExpectedResult' => 0.125, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'ABS(t.col_decimal)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(t.col_decimal) FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.1', - 'sqliteResult' => 0.1, - 'pdoPgsqlResult' => '0.1', - 'pgsqlResult' => '0.1', - 'mssqlResult' => '.1', + 'dqlTemplate' => 'SELECT ABS(t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.1', + 'sqliteExpectedResult' => 0.1, + 'pdoPgsqlExpectedResult' => '0.1', + 'pgsqlExpectedResult' => '0.1', + 'mssqlExpectedResult' => '.1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT ABS(t.col_decimal) FROM %s t', - 'mysql' => self::numericString(false, true), - 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT ABS(t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::numericString(false, true), + 'sqliteExpectedType' => self::floatOrInt(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(t.col_int) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 9, - 'sqliteResult' => 9, - 'pdoPgsqlResult' => 9, - 'pgsqlResult' => 9, - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT ABS(t.col_int) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 9, + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => 9, + 'pgsqlExpectedResult' => 9, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield '-ABS(t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT -ABS(t.col_int) FROM %s t', - 'mysql' => IntegerRangeType::fromInterval(null, 0), - 'sqlite' => IntegerRangeType::fromInterval(null, 0), - 'pdo_pgsql' => IntegerRangeType::fromInterval(null, 0), - 'pgsql' => IntegerRangeType::fromInterval(null, 0), - 'mssql' => self::mixed(), - 'mysqlResult' => -9, - 'sqliteResult' => -9, - 'pdoPgsqlResult' => -9, - 'pgsqlResult' => -9, - 'mssqlResult' => -9, + 'dqlTemplate' => 'SELECT -ABS(t.col_int) FROM %s t', + 'mysqlExpectedType' => IntegerRangeType::fromInterval(null, 0), + 'sqliteExpectedType' => IntegerRangeType::fromInterval(null, 0), + 'pdoPgsqlExpectedType' => IntegerRangeType::fromInterval(null, 0), + 'pgsqlExpectedType' => IntegerRangeType::fromInterval(null, 0), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => -9, + 'sqliteExpectedResult' => -9, + 'pdoPgsqlExpectedResult' => -9, + 'pgsqlExpectedResult' => -9, + 'mssqlExpectedResult' => -9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(t.col_int_nullable)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(t.col_int_nullable) FROM %s t', - 'mysql' => self::intNonNegativeOrNull(), - 'sqlite' => self::intNonNegativeOrNull(), - 'pdo_pgsql' => self::intNonNegativeOrNull(), - 'pgsql' => self::intNonNegativeOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT ABS(t.col_int_nullable) FROM %s t', + 'mysqlExpectedType' => self::intNonNegativeOrNull(), + 'sqliteExpectedType' => self::intNonNegativeOrNull(), + 'pdoPgsqlExpectedType' => self::intNonNegativeOrNull(), + 'pgsqlExpectedType' => self::intNonNegativeOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(t.col_string)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(t.col_string) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // Operand data type is invalid - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT ABS(t.col_string) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // Operand data type is invalid + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(t.col_string) + int data' => [ 'data' => self::dataAllIntLike(), - 'select' => 'SELECT ABS(t.col_string) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT ABS(t.col_string) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(t.col_bool)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(t.col_bool) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT ABS(t.col_bool) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(-1)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(-1) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT ABS(-1) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(1)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(1) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT ABS(1) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(1.0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(1.0) FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(true, true), - 'pgsql' => self::numericString(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT ABS(1.0) FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::numericString(true, true), + 'pgsqlExpectedType' => self::numericString(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(1e0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(1e0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(true, true), - 'pgsql' => self::numericString(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT ABS(1e0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::numericString(true, true), + 'pgsqlExpectedType' => self::numericString(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "ABS('1.0')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT ABS('1.0') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => "SELECT ABS('1.0') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "ABS('1')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT ABS('1') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => "SELECT ABS('1') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'ABS(t.col_bigint)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(t.col_bigint) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 2147483648, - 'sqliteResult' => 2147483648, - 'pdoPgsqlResult' => 2147483648, - 'pgsqlResult' => 2147483648, - 'mssqlResult' => '2147483648', + 'dqlTemplate' => 'SELECT ABS(t.col_bigint) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2147483648, + 'sqliteExpectedResult' => 2147483648, + 'pdoPgsqlExpectedResult' => 2147483648, + 'pgsqlExpectedResult' => 2147483648, + 'mssqlExpectedResult' => '2147483648', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'ABS(t.col_mixed)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT ABS(t.col_mixed) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT ABS(t.col_mixed) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_int, 0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_int, 0) FROM %s t', - 'mysql' => self::intNonNegativeOrNull(), - 'sqlite' => self::intNonNegativeOrNull(), - 'pdo_pgsql' => null, - 'pgsql' => null, - 'mssql' => null, // Divide by zero error encountered. - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT MOD(t.col_int, 0) FROM %s t', + 'mysqlExpectedType' => self::intNonNegativeOrNull(), + 'sqliteExpectedType' => self::intNonNegativeOrNull(), + 'pdoPgsqlExpectedType' => null, + 'pgsqlExpectedType' => null, + 'mssqlExpectedType' => null, // Divide by zero error encountered. + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_int, 1)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_int, 1) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT MOD(t.col_int, 1) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_mixed, 1)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_mixed, 1) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT MOD(t.col_mixed, 1) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "MOD(t.col_int, '1')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT MOD(t.col_int, '1') FROM %s t", - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => "SELECT MOD(t.col_int, '1') FROM %s t", + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "MOD(t.col_int, '1.0')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT MOD(t.col_int, '1') FROM %s t", - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => "SELECT MOD(t.col_int, '1') FROM %s t", + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_int, t.col_float)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_int, t.col_float) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // The data types are incompatible in the modulo operator. - 'mysqlResult' => 0.0, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT MOD(t.col_int, t.col_float) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // The data types are incompatible in the modulo operator. + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_int, t.col_decimal)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_int, t.col_decimal) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.0', - 'sqliteResult' => null, - 'pdoPgsqlResult' => '0.0', - 'pgsqlResult' => '0.0', - 'mssqlResult' => '.0', + 'dqlTemplate' => 'SELECT MOD(t.col_int, t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.0', + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => '0.0', + 'pgsqlExpectedResult' => '0.0', + 'mssqlExpectedResult' => '.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_float, t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_float, t.col_int) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // The data types are incompatible in the modulo operator. - 'mysqlResult' => 0.125, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT MOD(t.col_float, t.col_int) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // The data types are incompatible in the modulo operator. + 'mysqlExpectedResult' => 0.125, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_decimal, t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_decimal, t.col_int) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.1', - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => '0.1', - 'pgsqlResult' => '0.1', - 'mssqlResult' => '.1', + 'dqlTemplate' => 'SELECT MOD(t.col_decimal, t.col_int) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.1', + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => '0.1', + 'pgsqlExpectedResult' => '0.1', + 'mssqlExpectedResult' => '.1', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_string, t.col_string)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_string, t.col_string) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => null, // Undefined function - 'pgsql' => null, // Undefined function - 'mssql' => null, // The data types are incompatible in the modulo operator. - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT MOD(t.col_string, t.col_string) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => null, // Undefined function + 'pgsqlExpectedType' => null, // Undefined function + 'mssqlExpectedType' => null, // The data types are incompatible in the modulo operator. + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_int, t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_int, t.col_int) FROM %s t', - 'mysql' => self::intNonNegativeOrNull(), - 'sqlite' => self::intNonNegativeOrNull(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT MOD(t.col_int, t.col_int) FROM %s t', + 'mysqlExpectedType' => self::intNonNegativeOrNull(), + 'sqliteExpectedType' => self::intNonNegativeOrNull(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_int, t.col_int_nullable)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_int, t.col_int_nullable) FROM %s t', - 'mysql' => self::intNonNegativeOrNull(), - 'sqlite' => self::intNonNegativeOrNull(), - 'pdo_pgsql' => self::intNonNegativeOrNull(), - 'pgsql' => self::intNonNegativeOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT MOD(t.col_int, t.col_int_nullable) FROM %s t', + 'mysqlExpectedType' => self::intNonNegativeOrNull(), + 'sqliteExpectedType' => self::intNonNegativeOrNull(), + 'pdoPgsqlExpectedType' => self::intNonNegativeOrNull(), + 'pgsqlExpectedType' => self::intNonNegativeOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(10, 7)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(10, 7) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 3, - 'sqliteResult' => 3, - 'pdoPgsqlResult' => 3, - 'pgsqlResult' => 3, - 'mssqlResult' => 3, + 'dqlTemplate' => 'SELECT MOD(10, 7) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 3, + 'sqliteExpectedResult' => 3, + 'pdoPgsqlExpectedResult' => 3, + 'pgsqlExpectedResult' => 3, + 'mssqlExpectedResult' => 3, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(10, -7)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(10, -7) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 3, - 'sqliteResult' => 3, - 'pdoPgsqlResult' => 3, - 'pgsqlResult' => 3, - 'mssqlResult' => 3, + 'dqlTemplate' => 'SELECT MOD(10, -7) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 3, + 'sqliteExpectedResult' => 3, + 'pdoPgsqlExpectedResult' => 3, + 'pgsqlExpectedResult' => 3, + 'mssqlExpectedResult' => 3, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'MOD(t.col_bigint, t.col_bigint)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT MOD(t.col_bigint, t.col_bigint) FROM %s t', - 'mysql' => self::intNonNegativeOrNull(), - 'sqlite' => self::intNonNegativeOrNull(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => '0', + 'dqlTemplate' => 'SELECT MOD(t.col_bigint, t.col_bigint) FROM %s t', + 'mysqlExpectedType' => self::intNonNegativeOrNull(), + 'sqliteExpectedType' => self::intNonNegativeOrNull(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => '0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'BIT_AND(t.col_bigint, t.col_bigint)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT BIT_AND(t.col_bigint, t.col_bigint) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 2147483648, - 'sqliteResult' => 2147483648, - 'pdoPgsqlResult' => 2147483648, - 'pgsqlResult' => 2147483648, - 'mssqlResult' => '2147483648', + 'dqlTemplate' => 'SELECT BIT_AND(t.col_bigint, t.col_bigint) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 2147483648, + 'sqliteExpectedResult' => 2147483648, + 'pdoPgsqlExpectedResult' => 2147483648, + 'pgsqlExpectedResult' => 2147483648, + 'mssqlExpectedResult' => '2147483648', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'BIT_AND(t.col_int, t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT BIT_AND(t.col_int, t.col_int) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 9, - 'sqliteResult' => 9, - 'pdoPgsqlResult' => 9, - 'pgsqlResult' => 9, - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT BIT_AND(t.col_int, t.col_int) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 9, + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => 9, + 'pgsqlExpectedResult' => 9, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'BIT_AND(t.col_mixed, t.col_mixed)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT BIT_AND(t.col_mixed, t.col_mixed) FROM %s t', - 'mysql' => self::intNonNegativeOrNull(), - 'sqlite' => self::intNonNegativeOrNull(), - 'pdo_pgsql' => self::intNonNegativeOrNull(), - 'pgsql' => self::intNonNegativeOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT BIT_AND(t.col_mixed, t.col_mixed) FROM %s t', + 'mysqlExpectedType' => self::intNonNegativeOrNull(), + 'sqliteExpectedType' => self::intNonNegativeOrNull(), + 'pdoPgsqlExpectedType' => self::intNonNegativeOrNull(), + 'pgsqlExpectedType' => self::intNonNegativeOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'BIT_AND(t.col_int, t.col_int_nullable)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT BIT_AND(t.col_int, t.col_int_nullable) FROM %s t', - 'mysql' => self::intNonNegativeOrNull(), - 'sqlite' => self::intNonNegativeOrNull(), - 'pdo_pgsql' => self::intNonNegativeOrNull(), - 'pgsql' => self::intNonNegativeOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT BIT_AND(t.col_int, t.col_int_nullable) FROM %s t', + 'mysqlExpectedType' => self::intNonNegativeOrNull(), + 'sqliteExpectedType' => self::intNonNegativeOrNull(), + 'pdoPgsqlExpectedType' => self::intNonNegativeOrNull(), + 'pgsqlExpectedType' => self::intNonNegativeOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'BIT_AND(1, 0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT BIT_AND(1, 0) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT BIT_AND(1, 0) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'BIT_AND(t.col_string, t.col_string)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT BIT_AND(t.col_string, t.col_string) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => null, - 'pgsql' => null, - 'mssql' => null, - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT BIT_AND(t.col_string, t.col_string) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => null, + 'pgsqlExpectedType' => null, + 'mssqlExpectedType' => null, + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'DATE_DIFF(CURRENT_DATE(), CURRENT_DATE())' => [ 'data' => self::dataDefault(), - 'select' => "SELECT DATE_DIFF('2024-01-01 12:00', '2024-01-01 11:00') FROM %s t", - 'mysql' => self::int(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => "SELECT DATE_DIFF('2024-01-01 12:00', '2024-01-01 11:00') FROM %s t", + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'DATE_DIFF(CURRENT_DATE(), t.col_string_nullable)' => [ 'data' => self::dataDefault(), - 'select' => "SELECT DATE_DIFF('2024-01-01 12:00', t.col_string_nullable) FROM %s t", - 'mysql' => self::intOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::intOrNull(), - 'pgsql' => self::intOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT DATE_DIFF('2024-01-01 12:00', t.col_string_nullable) FROM %s t", + 'mysqlExpectedType' => self::intOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::intOrNull(), + 'pgsqlExpectedType' => self::intOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'DATE_DIFF(CURRENT_DATE(), t.col_mixed)' => [ 'data' => self::dataDefault(), - 'select' => "SELECT DATE_DIFF('2024-01-01 12:00', t.col_mixed) FROM %s t", - 'mysql' => self::intOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, - 'pgsql' => null, - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => 2460310.0, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 45289, + 'dqlTemplate' => "SELECT DATE_DIFF('2024-01-01 12:00', t.col_mixed) FROM %s t", + 'mysqlExpectedType' => self::intOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, + 'pgsqlExpectedType' => null, + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => 2460310.0, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 45289, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SQRT(t.col_float)' => [ 'data' => self::dataSqrt(), - 'select' => 'SELECT SQRT(t.col_float) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT SQRT(t.col_float) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(t.col_decimal)' => [ 'data' => self::dataSqrt(), - 'select' => 'SELECT SQRT(t.col_decimal) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericString(false, true), - 'pgsql' => self::numericString(false, true), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.000000000000000', - 'pgsqlResult' => '1.000000000000000', - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT SQRT(t.col_decimal) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::numericString(false, true), + 'pgsqlExpectedType' => self::numericString(false, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.000000000000000', + 'pgsqlExpectedResult' => '1.000000000000000', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SQRT(t.col_int)' => [ 'data' => self::dataSqrt(), - 'select' => 'SELECT SQRT(t.col_int) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 3.0, - 'sqliteResult' => 3.0, - 'pdoPgsqlResult' => 3.0, - 'pgsqlResult' => 3.0, - 'mssqlResult' => 3.0, + 'dqlTemplate' => 'SELECT SQRT(t.col_int) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 3.0, + 'sqliteExpectedResult' => 3.0, + 'pdoPgsqlExpectedResult' => 3.0, + 'pgsqlExpectedResult' => 3.0, + 'mssqlExpectedResult' => 3.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(t.col_mixed)' => [ 'data' => self::dataSqrt(), - 'select' => 'SELECT SQRT(t.col_mixed) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT SQRT(t.col_mixed) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(t.col_int_nullable)' => [ 'data' => self::dataSqrt(), - 'select' => 'SELECT SQRT(t.col_int_nullable) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => PHP_VERSION_ID >= 80100 && !self::hasDbal4() ? null : self::floatOrNull(), // fails in UDF since PHP 8.1: sqrt(): Passing null to parameter #1 ($num) of type float is deprecated - 'pdo_pgsql' => self::floatOrNull(), - 'pgsql' => self::floatOrNull(), - 'mssql' => self::mixed(), - 'mysqlResult' => null, - 'sqliteResult' => self::hasDbal4() ? null : 0.0, // 0.0 caused by UDF wired through PHP's sqrt() which returns 0.0 for null - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT SQRT(t.col_int_nullable) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => PHP_VERSION_ID >= 80100 && !self::hasDbal4() ? null : self::floatOrNull(), // fails in UDF since PHP 8.1: sqrt(): Passing null to parameter #1 ($num) of type float is deprecated + 'pdoPgsqlExpectedType' => self::floatOrNull(), + 'pgsqlExpectedType' => self::floatOrNull(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => self::hasDbal4() ? null : 0.0, // 0.0 caused by UDF wired through PHP's sqrt() which returns 0.0 for null + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(-1)' => [ 'data' => self::dataSqrt(), - 'select' => 'SELECT SQRT(-1) FROM %s t', - 'mysql' => self::floatOrNull(), - 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => null, // failure: cannot take square root of a negative number - 'pgsql' => null, // failure: cannot take square root of a negative number - 'mssql' => null, // An invalid floating point operation occurred. - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT SQRT(-1) FROM %s t', + 'mysqlExpectedType' => self::floatOrNull(), + 'sqliteExpectedType' => self::floatOrNull(), + 'pdoPgsqlExpectedType' => null, // failure: cannot take square root of a negative number + 'pgsqlExpectedType' => null, // failure: cannot take square root of a negative number + 'mssqlExpectedType' => null, // An invalid floating point operation occurred. + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SQRT(1)' => [ 'data' => self::dataSqrt(), - 'select' => 'SELECT SQRT(1) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT SQRT(1) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('1')" => [ 'data' => self::dataSqrt(), - 'select' => "SELECT SQRT('1') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => "SELECT SQRT('1') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('1.0')" => [ 'data' => self::dataSqrt(), - 'select' => "SELECT SQRT('1.0') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => "SELECT SQRT('1.0') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('1e0')" => [ 'data' => self::dataSqrt(), - 'select' => "SELECT SQRT('1e0') FROM %s t", - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::float(), - 'pgsql' => self::float(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => "SELECT SQRT('1e0') FROM %s t", + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::float(), + 'pgsqlExpectedType' => self::float(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('foo')" => [ 'data' => self::dataSqrt(), - 'select' => "SELECT SQRT('foo') FROM %s t", - 'mysql' => self::mixed(), - 'sqlite' => self::hasDbal4() ? self::mixed() : null, // fails in UDF: sqrt(): Argument #1 ($num) must be of type float, string given - 'pdo_pgsql' => null, // Invalid text representation - 'pgsql' => null, // Invalid text representation - 'mssql' => null, // Error converting data type - 'mysqlResult' => 0.0, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT SQRT('foo') FROM %s t", + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::hasDbal4() ? self::mixed() : null, // fails in UDF: sqrt(): Argument #1 ($num) must be of type float, string given + 'pdoPgsqlExpectedType' => null, // Invalid text representation + 'pgsqlExpectedType' => null, // Invalid text representation + 'mssqlExpectedType' => null, // Error converting data type + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SQRT(t.col_string)' => [ 'data' => self::dataSqrt(), - 'select' => 'SELECT SQRT(t.col_string) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::hasDbal4() ? self::mixed() : null, // fails in UDF: sqrt(): Argument #1 ($num) must be of type float, string given - 'pdo_pgsql' => null, // undefined function - 'pgsql' => null, // undefined function - 'mssql' => null, // Error converting data type - 'mysqlResult' => 0.0, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT SQRT(t.col_string) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::hasDbal4() ? self::mixed() : null, // fails in UDF: sqrt(): Argument #1 ($num) must be of type float, string given + 'pdoPgsqlExpectedType' => null, // undefined function + 'pgsqlExpectedType' => null, // undefined function + 'mssqlExpectedType' => null, // Error converting data type + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'SQRT(1.0)' => [ 'data' => self::dataSqrt(), - 'select' => 'SELECT SQRT(1.0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(true, true), - 'pgsql' => self::numericString(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.000000000000000', - 'pgsqlResult' => '1.000000000000000', - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT SQRT(1.0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::numericString(true, true), + 'pgsqlExpectedType' => self::numericString(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.000000000000000', + 'pgsqlExpectedResult' => '1.000000000000000', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COUNT(t)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COUNT(t) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::intNonNegative(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT COUNT(t) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::intNonNegative(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_NONE, ]; yield 'SUBSELECT' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t1.col_int, (SELECT COUNT(t2.col_int) FROM ' . PlatformEntity::class . ' t2) FROM %s t1', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::int(), - 'mysqlResult' => 9, - 'sqliteResult' => 9, - 'pdoPgsqlResult' => 9, - 'pgsqlResult' => 9, - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT t1.col_int, (SELECT COUNT(t2.col_int) FROM ' . PlatformEntity::class . ' t2) FROM %s t1', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::int(), + 'mysqlExpectedResult' => 9, + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => 9, + 'pgsqlExpectedResult' => 9, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_NONE, ]; yield 'COUNT(t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COUNT(t.col_int) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::intNonNegative(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT COUNT(t.col_int) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::intNonNegative(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_NONE, ]; yield 'COUNT(t.col_mixed)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COUNT(t.col_mixed) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::intNonNegative(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT COUNT(t.col_mixed) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::intNonNegative(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_NONE, ]; yield 'COUNT(1)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COUNT(1) FROM %s t', - 'mysql' => self::intNonNegative(), - 'sqlite' => self::intNonNegative(), - 'pdo_pgsql' => self::intNonNegative(), - 'pgsql' => self::intNonNegative(), - 'mssql' => self::intNonNegative(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT COUNT(1) FROM %s t', + 'mysqlExpectedType' => self::intNonNegative(), + 'sqliteExpectedType' => self::intNonNegative(), + 'pdoPgsqlExpectedType' => self::intNonNegative(), + 'pgsqlExpectedType' => self::intNonNegative(), + 'mssqlExpectedType' => self::intNonNegative(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_NONE, ]; yield 't.col_mixed' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT t.col_mixed FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT t.col_mixed FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_NONE, ]; yield 'INT_PI()' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT INT_PI() FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::int(), - 'mysqlResult' => 3, - 'sqliteResult' => 3, - 'pdoPgsqlResult' => 3, - 'pgsqlResult' => 3, - 'mssqlResult' => 3, + 'dqlTemplate' => 'SELECT INT_PI() FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::int(), + 'mysqlExpectedResult' => 3, + 'sqliteExpectedResult' => 3, + 'pdoPgsqlExpectedResult' => 3, + 'pgsqlExpectedResult' => 3, + 'mssqlExpectedResult' => 3, 'stringify' => self::STRINGIFY_NONE, ]; yield '-INT_PI()' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT -INT_PI() FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => '-3.14159', - 'sqliteResult' => -3.14159, - 'pdoPgsqlResult' => '-3.14159', - 'pgsqlResult' => '-3.14159', - 'mssqlResult' => '-3.14159', + 'dqlTemplate' => 'SELECT -INT_PI() FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '-3.14159', + 'sqliteExpectedResult' => -3.14159, + 'pdoPgsqlExpectedResult' => '-3.14159', + 'pgsqlExpectedResult' => '-3.14159', + 'mssqlExpectedResult' => '-3.14159', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'BOOL_PI()' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT BOOL_PI() FROM %s t', - 'mysql' => self::bool(), - 'sqlite' => self::bool(), - 'pdo_pgsql' => self::bool(), - 'pgsql' => self::bool(), - 'mssql' => self::bool(), - 'mysqlResult' => true, - 'sqliteResult' => true, - 'pdoPgsqlResult' => true, - 'pgsqlResult' => true, - 'mssqlResult' => true, + 'dqlTemplate' => 'SELECT BOOL_PI() FROM %s t', + 'mysqlExpectedType' => self::bool(), + 'sqliteExpectedType' => self::bool(), + 'pdoPgsqlExpectedType' => self::bool(), + 'pgsqlExpectedType' => self::bool(), + 'mssqlExpectedType' => self::bool(), + 'mysqlExpectedResult' => true, + 'sqliteExpectedResult' => true, + 'pdoPgsqlExpectedResult' => true, + 'pgsqlExpectedResult' => true, + 'mssqlExpectedResult' => true, 'stringify' => self::STRINGIFY_NONE, ]; yield 'STRING_PI()' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT STRING_PI() FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => '3.14159', - 'sqliteResult' => 3.14159, - 'pdoPgsqlResult' => '3.14159', - 'pgsqlResult' => '3.14159', - 'mssqlResult' => '3.14159', + 'dqlTemplate' => 'SELECT STRING_PI() FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '3.14159', + 'sqliteExpectedResult' => 3.14159, + 'pdoPgsqlExpectedResult' => '3.14159', + 'pgsqlExpectedResult' => '3.14159', + 'mssqlExpectedResult' => '3.14159', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'INT_WRAP(MIN(t.col_float)) + no data' => [ 'data' => self::dataNone(), - 'select' => 'SELECT INT_WRAP(MIN(t.col_float)) FROM %s t', - 'mysql' => self::intOrNull(), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => self::intOrNull(), - 'pgsql' => self::intOrNull(), - 'mssql' => self::intOrNull(), - 'mysqlResult' => null, - 'sqliteResult' => null, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT INT_WRAP(MIN(t.col_float)) FROM %s t', + 'mysqlExpectedType' => self::intOrNull(), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => self::intOrNull(), + 'pgsqlExpectedType' => self::intOrNull(), + 'mssqlExpectedType' => self::intOrNull(), + 'mysqlExpectedResult' => null, + 'sqliteExpectedResult' => null, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_NONE, ]; yield 'INT_WRAP(MIN(t.col_float))' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT INT_WRAP(MIN(t.col_float)) FROM %s t', - 'mysql' => self::intOrNull(), - 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => self::intOrNull(), - 'pgsql' => self::intOrNull(), - 'mssql' => self::intOrNull(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT INT_WRAP(MIN(t.col_float)) FROM %s t', + 'mysqlExpectedType' => self::intOrNull(), + 'sqliteExpectedType' => self::intOrNull(), + 'pdoPgsqlExpectedType' => self::intOrNull(), + 'pgsqlExpectedType' => self::intOrNull(), + 'mssqlExpectedType' => self::intOrNull(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_NONE, ]; yield 'COALESCE(t.col_datetime, t.col_datetime)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_datetime, t.col_datetime) FROM %s t', - 'mysql' => self::string(), - 'sqlite' => self::string(), - 'pdo_pgsql' => self::string(), - 'pgsql' => self::string(), - 'mssql' => self::mixed(), - 'mysqlResult' => '2024-01-31 12:59:59', - 'sqliteResult' => '2024-01-31 12:59:59', - 'pdoPgsqlResult' => '2024-01-31 12:59:59', - 'pgsqlResult' => '2024-01-31 12:59:59', - 'mssqlResult' => '2024-01-31 12:59:59.000000', // doctrine/dbal changes default ReturnDatesAsStrings to true + 'dqlTemplate' => 'SELECT COALESCE(t.col_datetime, t.col_datetime) FROM %s t', + 'mysqlExpectedType' => self::string(), + 'sqliteExpectedType' => self::string(), + 'pdoPgsqlExpectedType' => self::string(), + 'pgsqlExpectedType' => self::string(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '2024-01-31 12:59:59', + 'sqliteExpectedResult' => '2024-01-31 12:59:59', + 'pdoPgsqlExpectedResult' => '2024-01-31 12:59:59', + 'pgsqlExpectedResult' => '2024-01-31 12:59:59', + 'mssqlExpectedResult' => '2024-01-31 12:59:59.000000', // doctrine/dbal changes default ReturnDatesAsStrings to true 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(SUM(t.col_int_nullable), 0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(SUM(t.col_int_nullable), 0) FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::int(), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), - 'pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), - 'mssql' => self::mixed(), - 'mysqlResult' => '0', - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT COALESCE(SUM(t.col_int_nullable), 0) FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::numericString(true, true), self::int()), + 'pgsqlExpectedType' => TypeCombinator::union(self::numericString(true, true), self::int()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0', + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(SUM(ABS(t.col_int)), 0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(SUM(ABS(t.col_int)), 0) FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::int(), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'mssql' => self::mixed(), - 'mysqlResult' => '9', - 'sqliteResult' => 9, - 'pdoPgsqlResult' => 9, - 'pgsqlResult' => 9, - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT COALESCE(SUM(ABS(t.col_int)), 0) FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '9', + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => 9, + 'pgsqlExpectedResult' => 9, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "COALESCE(t.col_int_nullable, 'foo')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT COALESCE(t.col_int_nullable, 'foo') FROM %s t", - 'mysql' => self::string(), - 'sqlite' => TypeCombinator::union(self::int(), self::string()), - 'pdo_pgsql' => null, // COALESCE types cannot be matched - 'pgsql' => null, // COALESCE types cannot be matched - 'mssql' => null, // Conversion failed - 'mysqlResult' => 'foo', - 'sqliteResult' => 'foo', - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => "SELECT COALESCE(t.col_int_nullable, 'foo') FROM %s t", + 'mysqlExpectedType' => self::string(), + 'sqliteExpectedType' => TypeCombinator::union(self::int(), self::string()), + 'pdoPgsqlExpectedType' => null, // COALESCE types cannot be matched + 'pgsqlExpectedType' => null, // COALESCE types cannot be matched + 'mssqlExpectedType' => null, // Conversion failed + 'mysqlExpectedResult' => 'foo', + 'sqliteExpectedResult' => 'foo', + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "COALESCE(t.col_int, 'foo')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT COALESCE(t.col_int, 'foo') FROM %s t", - 'mysql' => self::string(), - 'sqlite' => TypeCombinator::union(self::int(), self::string()), - 'pdo_pgsql' => null, // COALESCE types cannot be matched - 'pgsql' => null, // COALESCE types cannot be matched - 'mssql' => self::mixed(), - 'mysqlResult' => '9', - 'sqliteResult' => 9, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 9, + 'dqlTemplate' => "SELECT COALESCE(t.col_int, 'foo') FROM %s t", + 'mysqlExpectedType' => self::string(), + 'sqliteExpectedType' => TypeCombinator::union(self::int(), self::string()), + 'pdoPgsqlExpectedType' => null, // COALESCE types cannot be matched + 'pgsqlExpectedType' => null, // COALESCE types cannot be matched + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '9', + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "COALESCE(t.col_bool, 'foo')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT COALESCE(t.col_bool, 'foo') FROM %s t", - 'mysql' => self::string(), - 'sqlite' => TypeCombinator::union(self::int(), self::string()), - 'pdo_pgsql' => null, // COALESCE types cannot be matched - 'pgsql' => null, // COALESCE types cannot be matched - 'mssql' => self::mixed(), - 'mysqlResult' => '1', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 1, + 'dqlTemplate' => "SELECT COALESCE(t.col_bool, 'foo') FROM %s t", + 'mysqlExpectedType' => self::string(), + 'sqliteExpectedType' => TypeCombinator::union(self::int(), self::string()), + 'pdoPgsqlExpectedType' => null, // COALESCE types cannot be matched + 'pgsqlExpectedType' => null, // COALESCE types cannot be matched + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "COALESCE(1, 'foo')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT COALESCE(1, 'foo') FROM %s t", - 'mysql' => self::string(), - 'sqlite' => TypeCombinator::union(self::int(), self::string()), - 'pdo_pgsql' => null, // COALESCE types cannot be matched - 'pgsql' => null, // COALESCE types cannot be matched - 'mssql' => self::mixed(), - 'mysqlResult' => '1', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 1, + 'dqlTemplate' => "SELECT COALESCE(1, 'foo') FROM %s t", + 'mysqlExpectedType' => self::string(), + 'sqliteExpectedType' => TypeCombinator::union(self::int(), self::string()), + 'pdoPgsqlExpectedType' => null, // COALESCE types cannot be matched + 'pgsqlExpectedType' => null, // COALESCE types cannot be matched + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "COALESCE(1, '1')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT COALESCE(1, '1') FROM %s t", - 'mysql' => self::numericString(true, true), - 'sqlite' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'mssql' => self::mixed(), - 'mysqlResult' => '1', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => "SELECT COALESCE(1, '1') FROM %s t", + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(1, 1.0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(1, 1.0) FROM %s t', - 'mysql' => self::numericString(), - 'sqlite' => TypeCombinator::union(self::int(), self::float()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT COALESCE(1, 1.0) FROM %s t', + 'mysqlExpectedType' => self::numericString(), + 'sqliteExpectedType' => TypeCombinator::union(self::int(), self::float()), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(0, 0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(0, 0) FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT COALESCE(0, 0) FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(1.0, 1.0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(1.0, 1.0) FROM %s t', - 'mysql' => self::numericString(true, true), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(true, true), - 'pgsql' => self::numericString(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => '1.0', - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1.0', - 'pgsqlResult' => '1.0', - 'mssqlResult' => '1.0', + 'dqlTemplate' => 'SELECT COALESCE(1.0, 1.0) FROM %s t', + 'mysqlExpectedType' => self::numericString(true, true), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::numericString(true, true), + 'pgsqlExpectedType' => self::numericString(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1.0', + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1.0', + 'pgsqlExpectedResult' => '1.0', + 'mssqlExpectedResult' => '1.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(1e0, 1.0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(1e0, 1.0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(true, true), - 'pgsql' => self::numericString(true, true), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT COALESCE(1e0, 1.0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => self::numericString(true, true), + 'pgsqlExpectedType' => self::numericString(true, true), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1.0, + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(1, 1.0, 1e0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(1, 1.0, 1e0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT COALESCE(1, 1.0, 1e0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => TypeCombinator::union(self::float(), self::int()), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "COALESCE(1, 1.0, 1e0, '1')" => [ 'data' => self::dataDefault(), - 'select' => "SELECT COALESCE(1, 1.0, 1e0, '1') FROM %s t", - 'mysql' => self::numericString(), - 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString(true, true)), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), - 'mssql' => self::mixed(), - 'mysqlResult' => '1', - 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1', - 'pgsqlResult' => '1', - 'mssqlResult' => 1.0, + 'dqlTemplate' => "SELECT COALESCE(1, 1.0, 1e0, '1') FROM %s t", + 'mysqlExpectedType' => self::numericString(), + 'sqliteExpectedType' => TypeCombinator::union(self::float(), self::int(), self::numericString(true, true)), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsqlExpectedType' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '1', + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => '1', + 'pgsqlExpectedResult' => '1', + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, 0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_int_nullable, 0) FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => 0, - 'pgsqlResult' => 0, - 'mssqlResult' => 0, + 'dqlTemplate' => 'SELECT COALESCE(t.col_int_nullable, 0) FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => 0, + 'pgsqlExpectedResult' => 0, + 'mssqlExpectedResult' => 0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, t.col_bool)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_bool) FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => null, // COALESCE types cannot be matched - 'pgsql' => null, // COALESCE types cannot be matched - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT COALESCE(t.col_int_nullable, t.col_bool) FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => null, // COALESCE types cannot be matched + 'pgsqlExpectedType' => null, // COALESCE types cannot be matched + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_float_nullable, 0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_float_nullable, 0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => PHP_VERSION_ID < 80400 + 'dqlTemplate' => 'SELECT COALESCE(t.col_float_nullable, 0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => TypeCombinator::union(self::float(), self::int()), + 'pdoPgsqlExpectedType' => PHP_VERSION_ID < 80400 ? TypeCombinator::union(self::numericString(), self::int()) : TypeCombinator::union(self::float(), self::int()), - 'pgsql' => TypeCombinator::union(self::float(), self::int()), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, - 'pgsqlResult' => 0.0, - 'mssqlResult' => 0.0, + 'pgsqlExpectedType' => TypeCombinator::union(self::float(), self::int()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, + 'pgsqlExpectedResult' => 0.0, + 'mssqlExpectedResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_float_nullable, 0.0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_float_nullable, 0.0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => self::float(), - 'pdo_pgsql' => TypeCombinator::union(self::float(), self::numericString(false, true)), - 'pgsql' => TypeCombinator::union(self::float(), self::numericString(false, true)), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => 0.0, - 'pgsqlResult' => 0.0, - 'mssqlResult' => 0.0, + 'dqlTemplate' => 'SELECT COALESCE(t.col_float_nullable, 0.0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => self::float(), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::float(), self::numericString(false, true)), + 'pgsqlExpectedType' => TypeCombinator::union(self::float(), self::numericString(false, true)), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => 0.0, + 'pgsqlExpectedResult' => 0.0, + 'mssqlExpectedResult' => 0.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, 0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, 0) FROM %s t', - 'mysql' => self::numericString(), - 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), - 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), - 'mssql' => self::mixed(), - 'mysqlResult' => '0.0', - 'sqliteResult' => 0, - 'pdoPgsqlResult' => '0', - 'pgsqlResult' => '0', - 'mssqlResult' => '.0', + 'dqlTemplate' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, 0) FROM %s t', + 'mysqlExpectedType' => self::numericString(), + 'sqliteExpectedType' => TypeCombinator::union(self::float(), self::int()), + 'pdoPgsqlExpectedType' => TypeCombinator::union(self::numericString(), self::int()), + 'pgsqlExpectedType' => TypeCombinator::union(self::numericString(), self::int()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0.0', + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => '0', + 'pgsqlExpectedResult' => '0', + 'mssqlExpectedResult' => '.0', 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => PHP_VERSION_ID < 80400 + 'dqlTemplate' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => TypeCombinator::union(self::float(), self::int()), + 'pdoPgsqlExpectedType' => PHP_VERSION_ID < 80400 ? TypeCombinator::union(self::numericString(), self::int()) : TypeCombinator::union(self::numericString(), self::int(), self::float()), - 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.0, - 'sqliteResult' => 0, - 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, - 'pgsqlResult' => 0.0, - 'mssqlResult' => 0.0, + 'pgsqlExpectedType' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0, + 'pdoPgsqlExpectedResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, + 'pgsqlExpectedResult' => 0.0, + 'mssqlExpectedResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => PHP_VERSION_ID < 80400 + 'dqlTemplate' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => TypeCombinator::union(self::float(), self::int()), + 'pdoPgsqlExpectedType' => PHP_VERSION_ID < 80400 ? TypeCombinator::union(self::numericString(), self::int()) : TypeCombinator::union(self::numericString(), self::int(), self::float()), - 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, - 'pgsqlResult' => 0.0, - 'mssqlResult' => 0.0, + 'pgsqlExpectedType' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, + 'pgsqlExpectedResult' => 0.0, + 'mssqlExpectedResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0) FROM %s t', - 'mysql' => self::float(), - 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => PHP_VERSION_ID < 80400 + 'dqlTemplate' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0) FROM %s t', + 'mysqlExpectedType' => self::float(), + 'sqliteExpectedType' => TypeCombinator::union(self::float(), self::int()), + 'pdoPgsqlExpectedType' => PHP_VERSION_ID < 80400 ? TypeCombinator::union(self::numericString(), self::int()) : TypeCombinator::union(self::numericString(), self::int(), self::float()), - 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), - 'mssql' => self::mixed(), - 'mysqlResult' => 0.0, - 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, - 'pgsqlResult' => 0.0, - 'mssqlResult' => 0.0, + 'pgsqlExpectedType' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 0.0, + 'sqliteExpectedResult' => 0.0, + 'pdoPgsqlExpectedResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, + 'pgsqlExpectedResult' => 0.0, + 'mssqlExpectedResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield "COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, '0')" => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, \'0\') FROM %s t', - 'mysql' => self::numericString(), - 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString()), - 'pdo_pgsql' => PHP_VERSION_ID < 80400 + 'dqlTemplate' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, \'0\') FROM %s t', + 'mysqlExpectedType' => self::numericString(), + 'sqliteExpectedType' => TypeCombinator::union(self::float(), self::int(), self::numericString()), + 'pdoPgsqlExpectedType' => PHP_VERSION_ID < 80400 ? TypeCombinator::union(self::numericString(), self::int()) : TypeCombinator::union(self::numericString(), self::int(), self::float()), - 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), - 'mssql' => self::mixed(), - 'mysqlResult' => '0', - 'sqliteResult' => '0', - 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, - 'pgsqlResult' => 0.0, - 'mssqlResult' => 0.0, + 'pgsqlExpectedType' => TypeCombinator::union(self::numericString(), self::int(), self::float()), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '0', + 'sqliteExpectedResult' => '0', + 'pdoPgsqlExpectedResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, + 'pgsqlExpectedResult' => 0.0, + 'mssqlExpectedResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_string)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_string) FROM %s t', - 'mysql' => self::string(), - 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::string()), - 'pdo_pgsql' => null, // COALESCE types cannot be matched - 'pgsql' => null, // COALESCE types cannot be matched - 'mssql' => null, // Error converting data - 'mysqlResult' => 'foobar', - 'sqliteResult' => 'foobar', - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => null, + 'dqlTemplate' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_string) FROM %s t', + 'mysqlExpectedType' => self::string(), + 'sqliteExpectedType' => TypeCombinator::union(self::float(), self::int(), self::string()), + 'pdoPgsqlExpectedType' => null, // COALESCE types cannot be matched + 'pgsqlExpectedType' => null, // COALESCE types cannot be matched + 'mssqlExpectedType' => null, // Error converting data + 'mysqlExpectedResult' => 'foobar', + 'sqliteExpectedResult' => 'foobar', + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => null, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_mixed)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_mixed) FROM %s t', - 'mysql' => self::mixed(), - 'sqlite' => self::mixed(), - 'pdo_pgsql' => self::mixed(), - 'pgsql' => self::mixed(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1.0, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1.0, - 'pgsqlResult' => 1.0, - 'mssqlResult' => 1.0, + 'dqlTemplate' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, t.col_mixed) FROM %s t', + 'mysqlExpectedType' => self::mixed(), + 'sqliteExpectedType' => self::mixed(), + 'pdoPgsqlExpectedType' => self::mixed(), + 'pgsqlExpectedType' => self::mixed(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1.0, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1.0, + 'pgsqlExpectedResult' => 1.0, + 'mssqlExpectedResult' => 1.0, 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_string_nullable, t.col_int)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT COALESCE(t.col_string_nullable, t.col_int) FROM %s t', - 'mysql' => self::string(), - 'sqlite' => TypeCombinator::union(self::int(), self::string()), - 'pdo_pgsql' => null, // COALESCE types cannot be matched - 'pgsql' => null, // COALESCE types cannot be matched - 'mssql' => self::mixed(), - 'mysqlResult' => '9', - 'sqliteResult' => 9, - 'pdoPgsqlResult' => null, - 'pgsqlResult' => null, - 'mssqlResult' => 9, + 'dqlTemplate' => 'SELECT COALESCE(t.col_string_nullable, t.col_int) FROM %s t', + 'mysqlExpectedType' => self::string(), + 'sqliteExpectedType' => TypeCombinator::union(self::int(), self::string()), + 'pdoPgsqlExpectedType' => null, // COALESCE types cannot be matched + 'pgsqlExpectedType' => null, // COALESCE types cannot be matched + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => '9', + 'sqliteExpectedResult' => 9, + 'pdoPgsqlExpectedResult' => null, + 'pgsqlExpectedResult' => null, + 'mssqlExpectedResult' => 9, 'stringify' => self::STRINGIFY_DEFAULT, ]; yield 'IDENTITY(t.related_entity)' => [ 'data' => self::dataDefault(), - 'select' => 'SELECT IDENTITY(t.related_entity) FROM %s t', - 'mysql' => self::int(), - 'sqlite' => self::int(), - 'pdo_pgsql' => self::int(), - 'pgsql' => self::int(), - 'mssql' => self::mixed(), - 'mysqlResult' => 1, - 'sqliteResult' => 1, - 'pdoPgsqlResult' => 1, - 'pgsqlResult' => 1, - 'mssqlResult' => 1, + 'dqlTemplate' => 'SELECT IDENTITY(t.related_entity) FROM %s t', + 'mysqlExpectedType' => self::int(), + 'sqliteExpectedType' => self::int(), + 'pdoPgsqlExpectedType' => self::int(), + 'pgsqlExpectedType' => self::int(), + 'mssqlExpectedType' => self::mixed(), + 'mysqlExpectedResult' => 1, + 'sqliteExpectedResult' => 1, + 'pdoPgsqlExpectedResult' => 1, + 'pgsqlExpectedResult' => 1, + 'mssqlExpectedResult' => 1, 'stringify' => self::STRINGIFY_DEFAULT, ]; } diff --git a/tests/Type/Doctrine/NewExprTest.php b/tests/Type/Doctrine/NewExprTest.php index ee3ff1d2..e1bb9dde 100644 --- a/tests/Type/Doctrine/NewExprTest.php +++ b/tests/Type/Doctrine/NewExprTest.php @@ -7,9 +7,6 @@ class NewExprTest extends TypeInferenceTestCase { - /** - * @return iterable - */ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-191.php'); diff --git a/tests/Type/Doctrine/QueryBuilderTypeSpecifyingExtensionTest.php b/tests/Type/Doctrine/QueryBuilderTypeSpecifyingExtensionTest.php index e0ebc471..9af21808 100644 --- a/tests/Type/Doctrine/QueryBuilderTypeSpecifyingExtensionTest.php +++ b/tests/Type/Doctrine/QueryBuilderTypeSpecifyingExtensionTest.php @@ -7,9 +7,6 @@ class QueryBuilderTypeSpecifyingExtensionTest extends TypeInferenceTestCase { - /** - * @return iterable - */ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-219.php'); From 95eff3003046be690cf1d1515b776ed89436a5bc Mon Sep 17 00:00:00 2001 From: Can Vural Date: Fri, 31 Oct 2025 11:37:54 +0100 Subject: [PATCH 157/160] use getIterableValueType --- tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 28a127c3..c9f07109 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -4714,7 +4714,7 @@ private function assertInferredResultMatchesExpected( $realFirstResult = var_export($firstResult, true); self::assertTrue($inferredType->isConstantArray()->yes()); - $inferredFirstItemType = $inferredType->getFirstIterableValueType(); + $inferredFirstItemType = $inferredType->getIterableValueType(); self::assertTrue( $expectedFirstItemType->accepts($inferredFirstItemType, true)->yes(), From 10636d983b0f89d1253619c02f931d03d9c010e7 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 4 Nov 2025 10:35:12 +0100 Subject: [PATCH 158/160] Remove 8.3.99 --- .github/workflows/build.yml | 16 ---------------- .github/workflows/platform-test.yml | 4 ---- 2 files changed, 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0dfbd37c..f8f49f83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,10 +43,6 @@ jobs: - name: "Validate Composer" run: "composer validate" - - name: "Allow installing on PHP 8.4" - if: matrix.php-version == '8.4' - run: "composer config platform.php 8.3.99" - - name: "Install dependencies" run: "composer install --no-interaction --no-progress" @@ -131,10 +127,6 @@ jobs: ini-file: development extensions: "mongodb" - - name: "Allow installing on PHP 8.4" - if: matrix.php-version == '8.4' - run: "composer config platform.php 8.3.99" - - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} run: "composer update --prefer-lowest --no-interaction --no-progress" @@ -178,10 +170,6 @@ jobs: php-version: "${{ matrix.php-version }}" php-extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb - - name: "Allow installing on PHP 8.4" - if: matrix.php-version == '8.4' - run: "composer config platform.php 8.3.99" - - name: "Install dependencies" run: "composer install --no-interaction --no-progress" @@ -248,10 +236,6 @@ jobs: extensions: "mongodb" ini-file: development - - name: "Allow installing on PHP 8.4" - if: matrix.php-version == '8.4' - run: "composer config platform.php 8.3.99" - - name: "Install dependencies" run: "composer update --no-interaction --no-progress" diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml index 21f0d515..09c85f45 100644 --- a/.github/workflows/platform-test.yml +++ b/.github/workflows/platform-test.yml @@ -50,10 +50,6 @@ jobs: ini-file: development extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb - - name: "Allow installing on PHP 8.4" - if: matrix.php-version == '8.4' - run: "composer config platform.php 8.3.99" - - name: "Install dependencies" run: "composer install --no-interaction --no-progress" From 368ad1c713a6d95763890bc2292694a603ece7c8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 4 Nov 2025 09:03:35 +0100 Subject: [PATCH 159/160] Declare getReflectionClass as covariant --- stubs/MongoClassMetadataInfo.stub | 2 +- stubs/ORM/Mapping/ClassMetadataInfo.stub | 2 +- stubs/Persistence/Mapping/ClassMetadata.stub | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stubs/MongoClassMetadataInfo.stub b/stubs/MongoClassMetadataInfo.stub index 65cfa40f..94c7bcd2 100644 --- a/stubs/MongoClassMetadataInfo.stub +++ b/stubs/MongoClassMetadataInfo.stub @@ -35,7 +35,7 @@ class ClassMetadata implements BaseClassMetadata public function getName(); /** - * @return ReflectionClass + * @return ReflectionClass */ public function getReflectionClass(); diff --git a/stubs/ORM/Mapping/ClassMetadataInfo.stub b/stubs/ORM/Mapping/ClassMetadataInfo.stub index 243ff1bb..339acea5 100644 --- a/stubs/ORM/Mapping/ClassMetadataInfo.stub +++ b/stubs/ORM/Mapping/ClassMetadataInfo.stub @@ -54,7 +54,7 @@ class ClassMetadataInfo implements ClassMetadata public function getName(); /** - * @return ReflectionClass + * @return ReflectionClass */ public function getReflectionClass(); diff --git a/stubs/Persistence/Mapping/ClassMetadata.stub b/stubs/Persistence/Mapping/ClassMetadata.stub index e16dc685..f64962f4 100644 --- a/stubs/Persistence/Mapping/ClassMetadata.stub +++ b/stubs/Persistence/Mapping/ClassMetadata.stub @@ -15,7 +15,7 @@ interface ClassMetadata public function getName(); /** - * @return ReflectionClass + * @return ReflectionClass */ public function getReflectionClass(); From d20ee0373d22735271f1eb4d631856b5f847d399 Mon Sep 17 00:00:00 2001 From: Jakub Janata Date: Mon, 1 Dec 2025 12:28:19 +0100 Subject: [PATCH 160/160] fix compatibility with doctrine/orm 3.5.8 --- src/Doctrine/Mapping/ClassMetadataFactory.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Doctrine/Mapping/ClassMetadataFactory.php b/src/Doctrine/Mapping/ClassMetadataFactory.php index 764268f1..96dec3bd 100644 --- a/src/Doctrine/Mapping/ClassMetadataFactory.php +++ b/src/Doctrine/Mapping/ClassMetadataFactory.php @@ -39,9 +39,16 @@ protected function initialize(): void $config = new Configuration(); $config->setMetadataDriverImpl(count($drivers) === 1 ? $drivers[0] : new MappingDriverChain($drivers)); - $config->setAutoGenerateProxyClasses(true); - $config->setProxyDir($this->tmpDir); - $config->setProxyNamespace('__PHPStanDoctrine__\\Proxy'); + + // @phpstan-ignore function.impossibleType (Available since Doctrine ORM 3.4) + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'enableNativeLazyObjects')) { + $config->enableNativeLazyObjects(true); + } else { + $config->setAutoGenerateProxyClasses(true); + $config->setProxyDir($this->tmpDir); + $config->setProxyNamespace('__PHPStanDoctrine__\\Proxy'); + } + $connection = DriverManager::getConnection([ 'driver' => 'pdo_sqlite', 'memory' => true,