diff --git a/composer.json b/composer.json index c061b640..d6f9eeb5 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "doctrine/persistence": "^1.3.8 || ^2.2.1", "nesbot/carbon": "^2.49", "nikic/php-parser": "^4.13.2", + "ocramius/package-versions": "*", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", diff --git a/phpstan.neon b/phpstan.neon index 93aed167..d5904d83 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -17,6 +17,10 @@ parameters: reportUnmatchedIgnoredErrors: false + bootstrapFiles: + - stubs/runtime/Enum/UnitEnum.php + - stubs/runtime/Enum/BackedEnum.php + ignoreErrors: - message: '~^Variable method call on Doctrine\\ORM\\QueryBuilder~' diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 18a6d41e..5551dff0 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -2,8 +2,10 @@ namespace PHPStan\Type\Doctrine\Query; +use BackedEnum; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Query; use Doctrine\ORM\Query\AST; use Doctrine\ORM\Query\AST\TypedExpression; @@ -15,6 +17,7 @@ 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\FloatType; @@ -31,6 +34,7 @@ use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use function array_map; use function assert; use function class_exists; use function count; @@ -42,6 +46,7 @@ use function is_numeric; use function is_object; use function is_string; +use function is_subclass_of; use function serialize; use function sprintf; use function strtolower; @@ -231,15 +236,13 @@ public function walkPathExpression($pathExpr) switch ($pathExpr->type) { case AST\PathExpression::TYPE_STATE_FIELD: - $typeName = $class->getTypeOfField($fieldName); - - assert(is_string($typeName)); + [$typeName, $enumType] = $this->getTypeOfField($class, $fieldName); $nullable = $this->isQueryComponentNullable($dqlAlias) || $class->isNullable($fieldName) || $this->hasAggregateWithoutGroupBy(); - $fieldType = $this->resolveDatabaseInternalType($typeName, $nullable); + $fieldType = $this->resolveDatabaseInternalType($typeName, $enumType, $nullable); return $this->marshalType($fieldType); @@ -273,14 +276,12 @@ public function walkPathExpression($pathExpr) } $targetFieldName = $identifierFieldNames[0]; - $typeName = $targetClass->getTypeOfField($targetFieldName); - - assert(is_string($typeName)); + [$typeName] = $this->getTypeOfField($targetClass, $targetFieldName); $nullable = (bool) ($joinColumn['nullable'] ?? true) || $this->hasAggregateWithoutGroupBy(); - $fieldType = $this->resolveDatabaseInternalType($typeName, $nullable); + $fieldType = $this->resolveDatabaseInternalType($typeName, null, $nullable); return $this->marshalType($fieldType); @@ -556,7 +557,7 @@ public function walkFunction($function) $nullable = (bool) ($joinColumn['nullable'] ?? true) || $this->hasAggregateWithoutGroupBy(); - $fieldType = $this->resolveDatabaseInternalType($typeName, $nullable); + $fieldType = $this->resolveDatabaseInternalType($typeName, null, $nullable); return $this->marshalType($fieldType); @@ -783,15 +784,13 @@ public function walkSelectExpression($selectExpression) $qComp = $this->queryComponents[$dqlAlias]; $class = $qComp['metadata']; - $typeName = $class->getTypeOfField($fieldName); - - assert(is_string($typeName)); + [$typeName, $enumType] = $this->getTypeOfField($class, $fieldName); $nullable = $this->isQueryComponentNullable($dqlAlias) || $class->isNullable($fieldName) || $this->hasAggregateWithoutGroupBy(); - $type = $this->resolveDoctrineType($typeName, $nullable); + $type = $this->resolveDoctrineType($typeName, $enumType, $nullable); $this->typeBuilder->addScalar($resultAlias, $type); @@ -1295,14 +1294,37 @@ private function isQueryComponentNullable(string $dqlAlias): bool return $this->nullableQueryComponents[$dqlAlias] ?? false; } - private function resolveDoctrineType(string $typeName, bool $nullable = false): Type + /** @return array{string, ?class-string} Doctrine type name and enum type of field */ + private function getTypeOfField(ClassMetadataInfo $class, string $fieldName): array { - try { - $type = $this->descriptorRegistry - ->get($typeName) - ->getWritableToPropertyType(); - } catch (DescriptorNotRegisteredException $e) { - $type = new MixedType(); + assert(isset($class->fieldMappings[$fieldName])); + + /** @var array{type: string, enumType?: ?string} $metadata */ + $metadata = $class->fieldMappings[$fieldName]; + + $type = $metadata['type']; + $enumType = $metadata['enumType'] ?? null; + + if (!is_string($enumType) || !class_exists($enumType) || !is_subclass_of($enumType, BackedEnum::class)) { + $enumType = null; + } + + return [$type, $enumType]; + } + + /** @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(); + } catch (DescriptorNotRegisteredException $e) { + $type = new MixedType(); + } } if ($nullable) { @@ -1312,7 +1334,8 @@ private function resolveDoctrineType(string $typeName, bool $nullable = false): return $type; } - private function resolveDatabaseInternalType(string $typeName, bool $nullable = false): Type + /** @param ?class-string $enumType */ + private function resolveDatabaseInternalType(string $typeName, ?string $enumType = null, bool $nullable = false): Type { try { $type = $this->descriptorRegistry @@ -1322,6 +1345,15 @@ private function resolveDatabaseInternalType(string $typeName, bool $nullable = $type = new MixedType(); } + if ($enumType !== null) { + $enumTypes = array_map(static function ($enumType) { + return ConstantTypeHelper::getTypeFromValue($enumType->value); + }, $enumType::cases()); + $enumType = TypeCombinator::union(...$enumTypes); + $enumType = TypeCombinator::union($enumType, $enumType->toString()); + $type = TypeCombinator::intersect($enumType, $type); + } + if ($nullable) { $type = TypeCombinator::addNull($type); } diff --git a/stubs/runtime/Enum/BackedEnum.php b/stubs/runtime/Enum/BackedEnum.php new file mode 100644 index 00000000..b0333af0 --- /dev/null +++ b/stubs/runtime/Enum/BackedEnum.php @@ -0,0 +1,22 @@ +persist($child); } + if (property_exists(Column::class, 'enumType') && PHP_VERSION_ID >= 80100) { + assert(class_exists(StringEnum::class)); + assert(class_exists(IntEnum::class)); + + $entityWithEnum = new EntityWithEnum(); + $entityWithEnum->id = '1'; + $entityWithEnum->stringEnumColumn = StringEnum::A; + $entityWithEnum->intEnumColumn = IntEnum::A; + $entityWithEnum->intEnumOnStringColumn = IntEnum::A; + $em->persist($entityWithEnum); + } + $em->flush(); } @@ -173,6 +194,11 @@ public function test(Type $expectedType, string $dql, ?string $expectedException $typeBuilder = new QueryResultTypeBuilder(); + if ($expectedExceptionMessage !== null) { + $this->expectException(Throwable::class); + $this->expectExceptionMessage($expectedExceptionMessage); + } + QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry); $type = $typeBuilder->getResultType(); @@ -186,11 +212,6 @@ public function test(Type $expectedType, string $dql, ?string $expectedException $query = $em->createQuery($dql); - if ($expectedExceptionMessage !== null) { - $this->expectException(Throwable::class); - $this->expectExceptionMessage($expectedExceptionMessage); - } - $result = $query->getResult(); self::assertGreaterThan(0, count($result)); @@ -199,7 +220,7 @@ public function test(Type $expectedType, string $dql, ?string $expectedException self::assertTrue( $type->accepts($rowType, true)->yes(), sprintf( - "%s\nshould accept\n%s", + "The inferred type\n%s\nshould accept actual type\n%s", $type->describe(VerbosityLevel::precise()), $rowType->describe(VerbosityLevel::precise()) ) @@ -208,1126 +229,1247 @@ public function test(Type $expectedType, string $dql, ?string $expectedException } /** - * @return array + * @return iterable */ - public function getTestData(): array + public function getTestData(): iterable { - return [ - 'just root entity' => [ - new ObjectType(One::class), - ' - SELECT o - FROM QueryResult\Entities\One o - ', - ], - 'just root entity as alias' => [ - $this->constantArray([ - [new ConstantStringType('one'), new ObjectType(One::class)], - ]), - ' - SELECT o AS one - FROM QueryResult\Entities\One o - ', - ], - 'arbitrary left join, not selected' => [ - new ObjectType(Many::class), - ' - SELECT m - FROM QueryResult\Entities\Many m - LEFT JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - // The result of arbitrary joins with multiple selected entities - // is an alternation of all selected entities - 'arbitrary left join, selected' => [ - TypeCombinator::union( - new ObjectType(Many::class), - TypeCombinator::addNull(new ObjectType(One::class)) - ), - ' - SELECT m, o - FROM QueryResult\Entities\Many m - LEFT JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - 'arbitrary inner join, selected' => [ - TypeCombinator::union( - new ObjectType(Many::class), - new ObjectType(One::class) - ), - ' - SELECT m, o - FROM QueryResult\Entities\Many m - JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - 'arbitrary left join, selected, some as alias' => [ - TypeCombinator::union( - $this->constantArray([ - [new ConstantStringType('many'), new ObjectType(Many::class)], - ]), - $this->constantArray([ - [new ConstantIntegerType(0), TypeCombinator::addNull(new ObjectType(One::class))], - ]) - ), - ' - SELECT m AS many, o - FROM QueryResult\Entities\Many m - LEFT JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - 'arbitrary left join, selected as alias' => [ - TypeCombinator::union( - $this->constantArray([ - [new ConstantStringType('many'), new ObjectType(Many::class)], - ]), - $this->constantArray([ - [new ConstantStringType('one'), TypeCombinator::addNull(new ObjectType(One::class))], - ]) - ), - ' - SELECT m AS many, o AS one - FROM QueryResult\Entities\Many m - LEFT JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - 'arbitrary inner join, selected as alias' => [ - TypeCombinator::union( - $this->constantArray([ - [new ConstantStringType('many'), new ObjectType(Many::class)], - ]), - $this->constantArray([ - [new ConstantStringType('one'), new ObjectType(One::class)], - ]) - ), - ' - SELECT m AS many, o AS one - FROM QueryResult\Entities\Many m - JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - // In arbitrary joins all non-entity results are returned only with - // the last declared entity (in FROM/JOIN order) - 'arbitrary inner join selected, and scalars' => [ - TypeCombinator::union( - $this->constantArray([ - [new ConstantIntegerType(0), new ObjectType(Many::class)], - ]), - $this->constantArray([ - [new ConstantIntegerType(0), new ObjectType(One::class)], - [new ConstantStringType('id'), new StringType()], - [new ConstantStringType('intColumn'), new IntegerType()], - ]) - ), - ' - SELECT m, o, m.id, o.intColumn - FROM QueryResult\Entities\Many m - JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - 'arbitrary inner join selected, and scalars (selection order variation)' => [ - TypeCombinator::union( - $this->constantArray([ - [new ConstantIntegerType(0), new ObjectType(Many::class)], - ]), - $this->constantArray([ - [new ConstantIntegerType(0), new ObjectType(One::class)], - [new ConstantStringType('id'), new StringType()], - [new ConstantStringType('intColumn'), new IntegerType()], - ]) - ), - ' - SELECT o, m2, m, m.id, o.intColumn - FROM QueryResult\Entities\Many m - JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - JOIN QueryResult\Entities\Many m2 - WITH IDENTITY(m2.one) = o.id - ', - ], - 'arbitrary inner join selected as alias, and scalars' => [ - TypeCombinator::union( - $this->constantArray([ - [new ConstantStringType('many'), new ObjectType(Many::class)], - ]), - $this->constantArray([ - [new ConstantStringType('one'), new ObjectType(One::class)], - [new ConstantStringType('id'), new StringType()], - [new ConstantStringType('intColumn'), new IntegerType()], - ]) - ), - ' - SELECT m AS many, o AS one, m.id, o.intColumn - FROM QueryResult\Entities\Many m - JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - 'join' => [ + yield 'just root entity' => [ + new ObjectType(One::class), + ' + SELECT o + FROM QueryResult\Entities\One o + ', + ]; + + yield 'just root entity as alias' => [ + $this->constantArray([ + [new ConstantStringType('one'), new ObjectType(One::class)], + ]), + ' + SELECT o AS one + FROM QueryResult\Entities\One o + ', + ]; + + yield 'arbitrary left join, not selected' => [ + new ObjectType(Many::class), + ' + SELECT m + FROM QueryResult\Entities\Many m + LEFT JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + // The result of arbitrary joins with multiple selected entities + // is an alternation of all selected entities + yield 'arbitrary left join, selected' => [ + TypeCombinator::union( new ObjectType(Many::class), - ' - SELECT m - FROM QueryResult\Entities\Many m - JOIN m.one o - ', - ], - 'fetch-join' => [ + TypeCombinator::addNull(new ObjectType(One::class)) + ), + ' + SELECT m, o + FROM QueryResult\Entities\Many m + LEFT JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + yield 'arbitrary inner join, selected' => [ + TypeCombinator::union( new ObjectType(Many::class), - ' - SELECT m, o - FROM QueryResult\Entities\Many m - JOIN m.one o - ', - ], - 'scalar' => [ + new ObjectType(One::class) + ), + ' + SELECT m, o + FROM QueryResult\Entities\Many m + JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + yield 'arbitrary left join, selected, some as alias' => [ + TypeCombinator::union( $this->constantArray([ - [new ConstantStringType('intColumn'), new IntegerType()], - [new ConstantStringType('stringColumn'), new StringType()], - [new ConstantStringType('stringNullColumn'), TypeCombinator::addNull(new StringType())], - [new ConstantStringType('datetimeColumn'), new ObjectType(DateTime::class)], - [new ConstantStringType('datetimeImmutableColumn'), new ObjectType(DateTimeImmutable::class)], + [new ConstantStringType('many'), new ObjectType(Many::class)], ]), - ' - SELECT m.intColumn, m.stringColumn, m.stringNullColumn, - m.datetimeColumn, m.datetimeImmutableColumn - FROM QueryResult\Entities\Many m - ', - ], - 'scalar with alias' => [ $this->constantArray([ - [new ConstantStringType('i'), new IntegerType()], - [new ConstantStringType('s'), new StringType()], - [new ConstantStringType('sn'), TypeCombinator::addNull(new StringType())], - ]), - ' - SELECT m.intColumn AS i, m.stringColumn AS s, m.stringNullColumn AS sn - FROM QueryResult\Entities\Many m - ', - ], - 'scalar from join' => [ + [new ConstantIntegerType(0), TypeCombinator::addNull(new ObjectType(One::class))], + ]) + ), + ' + SELECT m AS many, o + FROM QueryResult\Entities\Many m + LEFT JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + yield 'arbitrary left join, selected as alias' => [ + TypeCombinator::union( $this->constantArray([ - [new ConstantStringType('intColumn'), new IntegerType()], - [new ConstantStringType('stringNullColumn'), TypeCombinator::addNull(new StringType())], + [new ConstantStringType('many'), new ObjectType(Many::class)], ]), - ' - SELECT o.intColumn, o.stringNullColumn - FROM QueryResult\Entities\Many m - JOIN m.one o - ', - ], - 'scalar from left join' => [ $this->constantArray([ - [new ConstantStringType('intColumn'), TypeCombinator::addNull(new IntegerType())], - [new ConstantStringType('stringNullColumn'), TypeCombinator::addNull(new StringType())], - ]), - ' - SELECT o.intColumn, o.stringNullColumn - FROM QueryResult\Entities\Many m - LEFT JOIN m.one o - ', - ], - 'scalar from arbitrary join' => [ + [new ConstantStringType('one'), TypeCombinator::addNull(new ObjectType(One::class))], + ]) + ), + ' + SELECT m AS many, o AS one + FROM QueryResult\Entities\Many m + LEFT JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + yield 'arbitrary inner join, selected as alias' => [ + TypeCombinator::union( $this->constantArray([ - [new ConstantStringType('intColumn'), new IntegerType()], - [new ConstantStringType('stringNullColumn'), TypeCombinator::addNull(new StringType())], + [new ConstantStringType('many'), new ObjectType(Many::class)], ]), - ' - SELECT o.intColumn, o.stringNullColumn - FROM QueryResult\Entities\Many m - JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - 'scalar from arbitrary left join' => [ $this->constantArray([ - [new ConstantStringType('intColumn'), TypeCombinator::addNull(new IntegerType())], - [new ConstantStringType('stringNullColumn'), TypeCombinator::addNull(new StringType())], + [new ConstantStringType('one'), new ObjectType(One::class)], + ]) + ), + ' + SELECT m AS many, o AS one + FROM QueryResult\Entities\Many m + JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + // In arbitrary joins all non-entity results are returned only with + // the last declared entity (in FROM/JOIN order) + yield 'arbitrary inner join selected, and scalars' => [ + TypeCombinator::union( + $this->constantArray([ + [new ConstantIntegerType(0), new ObjectType(Many::class)], ]), - ' - SELECT o.intColumn, o.stringNullColumn - FROM QueryResult\Entities\Many m - LEFT JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - 'just root entity and scalars' => [ $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(One::class)], [new ConstantStringType('id'), new StringType()], - ]), - ' - SELECT o, o.id - FROM QueryResult\Entities\One o - ', - ], - 'hidden' => [ - $this->constantArray([ [new ConstantStringType('intColumn'), new IntegerType()], - ]), - ' - SELECT m.intColumn, m.stringColumn AS HIDDEN sc - FROM QueryResult\Entities\Many m - ', - ], - 'sub query are not support yet' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new MixedType()], - ]), - ' - SELECT (SELECT m.intColumn FROM QueryResult\Entities\Many m) - FROM QueryResult\Entities\Many m2 - ', - ], - 'aggregate' => [ + ]) + ), + ' + SELECT m, o, m.id, o.intColumn + FROM QueryResult\Entities\Many m + JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + yield 'arbitrary inner join selected, and scalars (selection order variation)' => [ + TypeCombinator::union( $this->constantArray([ - [ - new ConstantStringType('many'), - TypeCombinator::addNull(new ObjectType(Many::class)), - ], - [ - new ConstantIntegerType(1), - TypeCombinator::addNull($this->intStringified()), - ], - [ - new ConstantIntegerType(2), - TypeCombinator::addNull(new StringType()), - ], - [ - new ConstantIntegerType(3), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), - ], - [ - new ConstantIntegerType(4), - TypeCombinator::addNull($this->intStringified()), - ], - [ - new ConstantIntegerType(5), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), - ], - [ - new ConstantIntegerType(6), - TypeCombinator::addNull($this->intStringified()), - ], - [ - new ConstantIntegerType(7), - $this->intStringified(), - ], + [new ConstantIntegerType(0), new ObjectType(Many::class)], ]), - ' - SELECT m AS many, - MAX(m.intColumn), MAX(m.stringNullColumn), COUNT(m.stringNullColumn), - MAX(o.intColumn), COUNT(o.stringNullColumn), - MAX(m.intColumn+1), - COALESCE(MAX(m.intColumn), 0) - FROM QueryResult\Entities\Many m - LEFT JOIN m.one o - ', - ], - 'aggregate with group by' => [ $this->constantArray([ - [ - new ConstantStringType('intColumn'), - new IntegerType(), - ], - [ - new ConstantStringType('max'), - TypeCombinator::addNull($this->intStringified()), - ], - [ - new ConstantStringType('arithmetic'), - $this->intStringified(), - ], - [ - new ConstantStringType('coalesce'), - $this->intStringified(), - ], - [ - new ConstantStringType('count'), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), - ], - ]), - ' - SELECT m.intColumn, - MAX(m.intColumn) AS max, - m.intColumn+1 AS arithmetic, - COALESCE(m.intColumn, m.intColumn) AS coalesce, - COUNT(m.intColumn) AS count - FROM QueryResult\Entities\Many m - GROUP BY m.intColumn - ', - ], - 'literal' => [ + [new ConstantIntegerType(0), new ObjectType(One::class)], + [new ConstantStringType('id'), new StringType()], + [new ConstantStringType('intColumn'), new IntegerType()], + ]) + ), + ' + SELECT o, m2, m, m.id, o.intColumn + FROM QueryResult\Entities\Many m + JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + JOIN QueryResult\Entities\Many m2 + WITH IDENTITY(m2.one) = o.id + ', + ]; + + yield 'arbitrary inner join selected as alias, and scalars' => [ + TypeCombinator::union( $this->constantArray([ - [ - new ConstantIntegerType(1), - TypeCombinator::union( - new ConstantStringType('1'), - new ConstantIntegerType(1) - ), - ], - [new ConstantIntegerType(2), new ConstantStringType('hello')], + [new ConstantStringType('many'), new ObjectType(Many::class)], ]), - ' - SELECT 1, \'hello\' - FROM QueryResult\Entities\Many m - ', - ], - 'nullif' => [ $this->constantArray([ - [ - new ConstantIntegerType(1), - TypeCombinator::union( - new ConstantIntegerType(1), - new ConstantStringType('1'), - new NullType() - ), - ], - ]), - ' - SELECT NULLIF(true, m.id) - FROM QueryResult\Entities\Many m - ', - ], - 'coalesce' => [ + [new ConstantStringType('one'), new ObjectType(One::class)], + [new ConstantStringType('id'), new StringType()], + [new ConstantStringType('intColumn'), new IntegerType()], + ]) + ), + ' + SELECT m AS many, o AS one, m.id, o.intColumn + FROM QueryResult\Entities\Many m + JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + yield 'join' => [ + new ObjectType(Many::class), + ' + SELECT m + FROM QueryResult\Entities\Many m + JOIN m.one o + ', + ]; + + yield 'fetch-join' => [ + new ObjectType(Many::class), + ' + SELECT m, o + FROM QueryResult\Entities\Many m + JOIN m.one o + ', + ]; + + yield 'scalar' => [ + $this->constantArray([ + [new ConstantStringType('intColumn'), new IntegerType()], + [new ConstantStringType('stringColumn'), new StringType()], + [new ConstantStringType('stringNullColumn'), 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 + ', + ]; + + yield 'scalar with alias' => [ + $this->constantArray([ + [new ConstantStringType('i'), new IntegerType()], + [new ConstantStringType('s'), new StringType()], + [new ConstantStringType('sn'), TypeCombinator::addNull(new StringType())], + ]), + ' + SELECT m.intColumn AS i, m.stringColumn AS s, m.stringNullColumn AS sn + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'scalar from join' => [ + $this->constantArray([ + [new ConstantStringType('intColumn'), new IntegerType()], + [new ConstantStringType('stringNullColumn'), TypeCombinator::addNull(new StringType())], + ]), + ' + SELECT o.intColumn, o.stringNullColumn + FROM QueryResult\Entities\Many m + JOIN m.one o + ', + ]; + + yield 'scalar from left join' => [ + $this->constantArray([ + [new ConstantStringType('intColumn'), TypeCombinator::addNull(new IntegerType())], + [new ConstantStringType('stringNullColumn'), TypeCombinator::addNull(new StringType())], + ]), + ' + SELECT o.intColumn, o.stringNullColumn + FROM QueryResult\Entities\Many m + LEFT JOIN m.one o + ', + ]; + + yield 'scalar from arbitrary join' => [ + $this->constantArray([ + [new ConstantStringType('intColumn'), new IntegerType()], + [new ConstantStringType('stringNullColumn'), TypeCombinator::addNull(new StringType())], + ]), + ' + SELECT o.intColumn, o.stringNullColumn + FROM QueryResult\Entities\Many m + JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + yield 'scalar from arbitrary left join' => [ + $this->constantArray([ + [new ConstantStringType('intColumn'), TypeCombinator::addNull(new IntegerType())], + [new ConstantStringType('stringNullColumn'), TypeCombinator::addNull(new StringType())], + ]), + ' + SELECT o.intColumn, o.stringNullColumn + FROM QueryResult\Entities\Many m + LEFT JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + yield 'just root entity and scalars' => [ + $this->constantArray([ + [new ConstantIntegerType(0), new ObjectType(One::class)], + [new ConstantStringType('id'), new StringType()], + ]), + ' + SELECT o, o.id + FROM QueryResult\Entities\One o + ', + ]; + + 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), TypeCombinator::union( - new StringType(), - new IntegerType() + new ConstantStringType('a'), + new ConstantStringType('b') ), ], [ new ConstantIntegerType(2), TypeCombinator::union( - new StringType(), - new NullType() + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantStringType('1'), + new ConstantStringType('2') ), ], [ new ConstantIntegerType(3), - $this->intStringified(), - ], - ]), - ' - SELECT COALESCE(m.stringNullColumn, m.intColumn, false), - COALESCE(m.stringNullColumn, m.stringNullColumn), - COALESCE(NULLIF(m.intColumn, 1), 0) - FROM QueryResult\Entities\Many m - ', - ], - 'general case' => [ - $this->constantArray([ - [ - new ConstantIntegerType(1), TypeCombinator::union( - new StringType(), - new ConstantIntegerType(0) + new ConstantStringType('1'), + new ConstantStringType('2') ), ], ]), ' - SELECT CASE - WHEN m.intColumn < 10 THEN m.stringColumn - WHEN m.intColumn < 20 THEN \'b\' - ELSE false - END - FROM QueryResult\Entities\Many m + SELECT COALESCE(e.stringEnumColumn, e.stringEnumColumn), + COALESCE(e.intEnumColumn, e.intEnumColumn), + COALESCE(e.intEnumOnStringColumn, e.intEnumOnStringColumn) + FROM QueryResult\EntitiesEnum\EntityWithEnum e ', - ], - 'simple case' => [ - $this->constantArray([ - [ + ]; + } + + yield 'hidden' => [ + $this->constantArray([ + [new ConstantStringType('intColumn'), new IntegerType()], + ]), + ' + SELECT m.intColumn, m.stringColumn AS HIDDEN sc + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'sub query are not support yet' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new MixedType()], + ]), + ' + SELECT (SELECT m.intColumn FROM QueryResult\Entities\Many m) + FROM QueryResult\Entities\Many m2 + ', + ]; + + yield 'aggregate' => [ + $this->constantArray([ + [ + new ConstantStringType('many'), + TypeCombinator::addNull(new ObjectType(Many::class)), + ], + [ + new ConstantIntegerType(1), + TypeCombinator::addNull($this->intStringified()), + ], + [ + new ConstantIntegerType(2), + TypeCombinator::addNull(new StringType()), + ], + [ + new ConstantIntegerType(3), + $this->hasTypedExpressions() + ? $this->uint() + : $this->uintStringified(), + ], + [ + new ConstantIntegerType(4), + TypeCombinator::addNull($this->intStringified()), + ], + [ + new ConstantIntegerType(5), + $this->hasTypedExpressions() + ? $this->uint() + : $this->uintStringified(), + ], + [ + new ConstantIntegerType(6), + TypeCombinator::addNull($this->intStringified()), + ], + [ + new ConstantIntegerType(7), + $this->intStringified(), + ], + ]), + ' + SELECT m AS many, + MAX(m.intColumn), MAX(m.stringNullColumn), COUNT(m.stringNullColumn), + MAX(o.intColumn), COUNT(o.stringNullColumn), + MAX(m.intColumn+1), + COALESCE(MAX(m.intColumn), 0) + FROM QueryResult\Entities\Many m + LEFT JOIN m.one o + ', + ]; + + yield 'aggregate with group by' => [ + $this->constantArray([ + [ + new ConstantStringType('intColumn'), + new IntegerType(), + ], + [ + new ConstantStringType('max'), + TypeCombinator::addNull($this->intStringified()), + ], + [ + new ConstantStringType('arithmetic'), + $this->intStringified(), + ], + [ + new ConstantStringType('coalesce'), + $this->intStringified(), + ], + [ + new ConstantStringType('count'), + $this->hasTypedExpressions() + ? $this->uint() + : $this->uintStringified(), + ], + ]), + ' + SELECT m.intColumn, + MAX(m.intColumn) AS max, + m.intColumn+1 AS arithmetic, + COALESCE(m.intColumn, m.intColumn) AS coalesce, + COUNT(m.intColumn) AS count + FROM QueryResult\Entities\Many m + GROUP BY m.intColumn + ', + ]; + + yield 'literal' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + TypeCombinator::union( + new ConstantStringType('1'), + new ConstantIntegerType(1) + ), + ], + [new ConstantIntegerType(2), new ConstantStringType('hello')], + ]), + ' + SELECT 1, \'hello\' + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'nullif' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + TypeCombinator::union( new ConstantIntegerType(1), - TypeCombinator::union( - new StringType(), - new ConstantIntegerType(0) - ), - ], - ]), - ' - SELECT CASE m.intColumn - WHEN 10 THEN m.stringColumn - WHEN 20 THEN \'b\' - ELSE false - END - FROM QueryResult\Entities\Many m - ', - ], - 'Issue 311' => [ - $this->constantArray([ - [ + new ConstantStringType('1'), + new NullType() + ), + ], + ]), + ' + SELECT NULLIF(true, m.id) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'coalesce' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + TypeCombinator::union( + new StringType(), + new IntegerType() + ), + ], + [ + new ConstantIntegerType(2), + TypeCombinator::union( + new StringType(), + new NullType() + ), + ], + [ + new ConstantIntegerType(3), + $this->intStringified(), + ], + ]), + ' + SELECT COALESCE(m.stringNullColumn, m.intColumn, false), + COALESCE(m.stringNullColumn, m.stringNullColumn), + COALESCE(NULLIF(m.intColumn, 1), 0) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'general case' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + TypeCombinator::union( + new StringType(), + new ConstantIntegerType(0) + ), + ], + ]), + ' + SELECT CASE + WHEN m.intColumn < 10 THEN m.stringColumn + WHEN m.intColumn < 20 THEN \'b\' + ELSE false + END + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'simple case' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + TypeCombinator::union( + new StringType(), + new ConstantIntegerType(0) + ), + ], + ]), + ' + SELECT CASE m.intColumn + WHEN 10 THEN m.stringColumn + WHEN 20 THEN \'b\' + ELSE false + END + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'Issue 311' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + TypeCombinator::union( + new ConstantIntegerType(0), new ConstantIntegerType(1), - TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantIntegerType(1), - new ConstantStringType('0'), - new ConstantStringType('1') - ), - ], - ]), - ' - SELECT CASE - WHEN m.intColumn < 10 THEN true - ELSE false - END - FROM QueryResult\Entities\Many m - ', - ], - 'Issue 311 - 2' => [ - $this->constantArray([ - [ + new ConstantStringType('0'), + new ConstantStringType('1') + ), + ], + ]), + ' + SELECT CASE + WHEN m.intColumn < 10 THEN true + ELSE false + END + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'Issue 311 - 2' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + TypeCombinator::union( + new ConstantIntegerType(0), new ConstantIntegerType(1), - TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantIntegerType(1), - new ConstantStringType('0'), - new ConstantStringType('1') - ), - ], - ]), - ' - SELECT CASE - WHEN m.intColumn < 10 THEN TRUE - ELSE FALSE - END - FROM QueryResult\Entities\Many m - ', - ], - 'Issue 311 - 3' => [ - $this->constantArray([ - [ + new ConstantStringType('0'), + new ConstantStringType('1') + ), + ], + ]), + ' + SELECT CASE + WHEN m.intColumn < 10 THEN TRUE + ELSE FALSE + END + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'Issue 311 - 3' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + TypeCombinator::union( new ConstantIntegerType(1), - TypeCombinator::union( - new ConstantIntegerType(1), - new ConstantStringType('1') - ), - ], - [ - new ConstantIntegerType(2), - TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantStringType('0') - ), - ], - [ - new ConstantIntegerType(3), - TypeCombinator::union( - new ConstantIntegerType(1), - new ConstantStringType('1') - ), - ], - [ - new ConstantIntegerType(4), - TypeCombinator::union( - new ConstantIntegerType(0), - new ConstantStringType('0') - ), - ], - ]), - ' - SELECT (TRUE), (FALSE), (true), (false) - FROM QueryResult\Entities\Many m - ', - ], - 'new' => [ - new ObjectType(ManyId::class), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id) - FROM QueryResult\Entities\Many m - ', - ], - 'news' => [ - $this->constantArray([ - [new ConstantIntegerType(0), new ObjectType(ManyId::class)], - [new ConstantIntegerType(1), new ObjectType(OneId::class)], - ]), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id), - NEW QueryResult\Entities\OneId(m.id) - FROM QueryResult\Entities\Many m - ', - ], - // Alias on NEW is ignored when there is only no scalars and a - // single NEW - 'new as alias' => [ - new ObjectType(ManyId::class), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id) AS id - FROM QueryResult\Entities\Many m - ', - ], - 'news as alias' => [ - $this->constantArray([ - [new ConstantStringType('id'), new ObjectType(ManyId::class)], - [new ConstantStringType('id2'), new ObjectType(OneId::class)], - ]), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, - NEW QueryResult\Entities\OneId(m.id) as id2 - FROM QueryResult\Entities\Many m - ', - ], - 'new and scalars' => [ - $this->constantArray([ - [ - new ConstantStringType('intColumn'), - new IntegerType(), - ], - [ - new ConstantStringType('id'), - new ObjectType(ManyId::class), - ], - ]), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, - m.intColumn - FROM QueryResult\Entities\Many m - ', - ], - 'new and entity' => [ - new ObjectType(ManyId::class), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, - m - FROM QueryResult\Entities\Many m - ', - ], - 'news and entity' => [ - $this->constantArray([ - [new ConstantIntegerType(0), new ObjectType(Many::class)], - [new ConstantStringType('id'), new ObjectType(ManyId::class)], - [new ConstantStringType('id2'), new ObjectType(OneId::class)], - ]), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, - NEW QueryResult\Entities\OneId(m.id) as id2, - m - FROM QueryResult\Entities\Many m - ', - ], - 'new, scalars, and entity' => [ + new ConstantStringType('1') + ), + ], + [ + new ConstantIntegerType(2), + TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantStringType('0') + ), + ], + [ + new ConstantIntegerType(3), + TypeCombinator::union( + new ConstantIntegerType(1), + new ConstantStringType('1') + ), + ], + [ + new ConstantIntegerType(4), + TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantStringType('0') + ), + ], + ]), + ' + SELECT (TRUE), (FALSE), (true), (false) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'new' => [ + new ObjectType(ManyId::class), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'news' => [ + $this->constantArray([ + [new ConstantIntegerType(0), new ObjectType(ManyId::class)], + [new ConstantIntegerType(1), new ObjectType(OneId::class)], + ]), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id), + NEW QueryResult\Entities\OneId(m.id) + FROM QueryResult\Entities\Many m + ', + ]; + + // Alias on NEW is ignored when there is only no scalars and a + // single NEW + yield 'new as alias' => [ + new ObjectType(ManyId::class), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id) AS id + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'news as alias' => [ + $this->constantArray([ + [new ConstantStringType('id'), new ObjectType(ManyId::class)], + [new ConstantStringType('id2'), new ObjectType(OneId::class)], + ]), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, + NEW QueryResult\Entities\OneId(m.id) as id2 + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'new and scalars' => [ + $this->constantArray([ + [ + new ConstantStringType('intColumn'), + new IntegerType(), + ], + [ + new ConstantStringType('id'), + new ObjectType(ManyId::class), + ], + ]), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, + m.intColumn + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'new and entity' => [ + new ObjectType(ManyId::class), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, + m + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'news and entity' => [ + $this->constantArray([ + [new ConstantIntegerType(0), new ObjectType(Many::class)], + [new ConstantStringType('id'), new ObjectType(ManyId::class)], + [new ConstantStringType('id2'), new ObjectType(OneId::class)], + ]), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, + NEW QueryResult\Entities\OneId(m.id) as id2, + m + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'new, scalars, and entity' => [ + $this->constantArray([ + [ + new ConstantIntegerType(0), + new ObjectType(ManyId::class), + ], + [ + new ConstantStringType('intColumn'), + new IntegerType(), + ], + ]), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id), + m.intColumn, + m + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'new as alias, scalars, and entity' => [ + $this->constantArray([ + [ + new ConstantIntegerType(0), + new ObjectType(Many::class), + ], + [ + new ConstantStringType('intColumn'), + new IntegerType(), + ], + [ + new ConstantStringType('id'), + new ObjectType(ManyId::class), + ], + ]), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, + m.intColumn, + m + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'new as alias, scalars, and entity as alias' => [ + $this->constantArray([ + [ + new ConstantStringType('many'), + new ObjectType(Many::class), + ], + [ + new ConstantStringType('intColumn'), + new IntegerType(), + ], + [ + new ConstantStringType('id'), + new ObjectType(ManyId::class), + ], + ]), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, + m.intColumn, + m AS many + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'news, scalars, and entities as alias' => [ + TypeCombinator::union( $this->constantArray([ [ - new ConstantIntegerType(0), - new ObjectType(ManyId::class), - ], - [ - new ConstantStringType('intColumn'), - new IntegerType(), + new ConstantStringType('many'), + new ObjectType(Many::class), ], ]), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id), - m.intColumn, - m - FROM QueryResult\Entities\Many m - ', - ], - 'new as alias, scalars, and entity' => [ $this->constantArray([ [ - new ConstantIntegerType(0), - new ObjectType(Many::class), - ], - [ - new ConstantStringType('intColumn'), - new IntegerType(), - ], - [ - new ConstantStringType('id'), - new ObjectType(ManyId::class), + new ConstantStringType('one'), + new ObjectType(One::class), ], - ]), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, - m.intColumn, - m - FROM QueryResult\Entities\Many m - ', - ], - 'new as alias, scalars, and entity as alias' => [ - $this->constantArray([ [ - new ConstantStringType('many'), - new ObjectType(Many::class), + new ConstantIntegerType(2), + TypeCombinator::union( + new ConstantIntegerType(1), + new ConstantStringType('1') + ), ], [ new ConstantStringType('intColumn'), new IntegerType(), ], [ - new ConstantStringType('id'), + new ConstantIntegerType(0), new ObjectType(ManyId::class), ], - ]), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id) AS id, - m.intColumn, - m AS many - FROM QueryResult\Entities\Many m - ', - ], - 'news, scalars, and entities as alias' => [ - TypeCombinator::union( - $this->constantArray([ - [ - new ConstantStringType('many'), - new ObjectType(Many::class), - ], - ]), - $this->constantArray([ - [ - new ConstantStringType('one'), - new ObjectType(One::class), - ], - [ - new ConstantIntegerType(2), - TypeCombinator::union( - new ConstantIntegerType(1), - new ConstantStringType('1') - ), - ], - [ - new ConstantStringType('intColumn'), - new IntegerType(), - ], - [ - new ConstantIntegerType(0), - new ObjectType(ManyId::class), - ], - [ - new ConstantIntegerType(1), - new ObjectType(OneId::class), - ], - ]) - ), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id), - COALESCE(1,1), - NEW QueryResult\Entities\OneId(m.id), - m.intColumn, - m AS many, - o AS one - FROM QueryResult\Entities\Many m - JOIN QueryResult\Entities\One o - WITH o.id = IDENTITY(m.one) - ', - ], - 'news shadown scalars' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new ObjectType(OneId::class)], - [new ConstantIntegerType(0), new ObjectType(ManyId::class)], - ]), - ' - SELECT NULLIF(m.intColumn, 1), - NEW QueryResult\Entities\ManyId(m.id), - NEW QueryResult\Entities\OneId(m.id) - FROM QueryResult\Entities\Many m - ', - ], - 'new arguments affect scalar counter' => [ - $this->constantArray([ - [new ConstantIntegerType(5), TypeCombinator::addNull($this->intStringified())], - [new ConstantIntegerType(0), new ObjectType(ManyId::class)], - [new ConstantIntegerType(1), new ObjectType(OneId::class)], - ]), - ' - SELECT NEW QueryResult\Entities\ManyId(m.id), - NEW QueryResult\Entities\OneId(m.id, m.id, m.id), - NULLIF(m.intColumn, 1) - FROM QueryResult\Entities\Many m - ', - ], - 'arithmetic' => [ - $this->constantArray([ - [new ConstantStringType('intColumn'), new IntegerType()], - [new ConstantIntegerType(1), $this->intStringified()], - [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()], - ]), - ' - SELECT m.intColumn, - +1, - 1+1, - 1+nullif(1,1), - m.intColumn*2+m.intColumn+3, - (1+1), - \'foo\' + \'bar\', - \'foo\' * \'bar\' - FROM QueryResult\Entities\Many m - ', - ], - '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())], - ]), - ' - SELECT ABS(m.intColumn), - ABS(NULLIF(m.intColumn, 1)), - ABS(1), - ABS(\'foo\') - FROM QueryResult\Entities\Many m - ', - ], - 'bit_and function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), $this->uintStringified()], - ]), - ' - SELECT BIT_AND(m.intColumn, 1), - BIT_AND(m.intColumn, NULLIF(1,1)), - BIT_AND(1, 2) - FROM QueryResult\Entities\Many m - ', - ], - 'bit_or function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), $this->uintStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], - [new ConstantIntegerType(3), $this->uintStringified()], - ]), - ' - SELECT BIT_OR(m.intColumn, 1), - BIT_OR(m.intColumn, NULLIF(1,1)), - BIT_OR(1, 2) - FROM QueryResult\Entities\Many m - ', - ], - 'concat 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 CONCAT(m.stringColumn, m.stringColumn), - CONCAT(m.stringColumn, m.stringNullColumn), - CONCAT(m.stringColumn, m.stringColumn, m.stringNullColumn), - CONCAT(\'foo\', \'bar\') - FROM QueryResult\Entities\Many m - ', - ], - 'current_ functions' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new StringType()], - [new ConstantIntegerType(2), new StringType()], - [new ConstantIntegerType(3), new StringType()], - ]), - ' - SELECT CURRENT_DATE(), - CURRENT_TIME(), - CURRENT_TIMESTAMP() - FROM QueryResult\Entities\Many m - ', - ], - '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 - ', - ], - '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 - ', - ], - '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()], - ]), - ' - SELECT DATE_DIFF(m.datetimeColumn, m.datetimeColumn), - DATE_DIFF(m.stringNullColumn, m.datetimeColumn), - DATE_DIFF(m.datetimeColumn, m.stringNullColumn), - DATE_DIFF(\'2020-01-01\', \'2019-01-01\') - FROM QueryResult\Entities\Many m - ', - ], - '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 - ', - ], - 'length function' => [ - $this->constantArray([ [ new ConstantIntegerType(1), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), - ], - [ - new ConstantIntegerType(2), - TypeCombinator::addNull( - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified() - ), - ], - [ - new ConstantIntegerType(3), - $this->hasTypedExpressions() - ? $this->uint() - : $this->uintStringified(), + new ObjectType(OneId::class), ], - ]), - ' - SELECT LENGTH(m.stringColumn), - LENGTH(m.stringNullColumn), - LENGTH(\'foo\') - FROM QueryResult\Entities\Many m - ', - ], - '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()], - ]), - ' - 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 - ', - ], - 'lower function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new StringType()], - [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(3), new StringType()], - ]), - ' - SELECT LOWER(m.stringColumn), - LOWER(m.stringNullColumn), - LOWER(\'foo\') - FROM QueryResult\Entities\Many m - ', - ], - '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()], - ]), - ' - SELECT MOD(m.intColumn, 1), - MOD(10, m.intColumn), - MOD(NULLIF(m.intColumn, 10), 2), - MOD(10, 4) - FROM QueryResult\Entities\Many m - ', - ], - 'mod function error' => [ - $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->uintStringified())], - ]), - ' - SELECT MOD(10, NULLIF(m.intColumn, m.intColumn)) - FROM QueryResult\Entities\Many m - ', - 'Modulo by zero', - ], - 'substring 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), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(5), new StringType()], - ]), - ' - SELECT SUBSTRING(m.stringColumn, m.intColumn, m.intColumn), - SUBSTRING(m.stringNullColumn, m.intColumn, m.intColumn), - SUBSTRING(m.stringColumn, NULLIF(m.intColumn, 1), m.intColumn), - SUBSTRING(m.stringColumn, m.intColumn, NULLIF(m.intColumn, 1)), - SUBSTRING(\'foo\', 1, 2) - FROM QueryResult\Entities\Many m - ', - ], - 'trim function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new StringType()], - [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(3), new StringType()], - ]), - ' - SELECT TRIM(LEADING \' \' FROM m.stringColumn), - TRIM(LEADING \' \' FROM m.stringNullColumn), - TRIM(LEADING \' \' FROM \'foo\') - FROM QueryResult\Entities\Many m - ', - ], - 'upper function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new StringType()], - [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(3), new StringType()], - ]), - ' - SELECT UPPER(m.stringColumn), - UPPER(m.stringNullColumn), - UPPER(\'foo\') - FROM QueryResult\Entities\Many m - ', - ], - '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(4), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(5), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(6), TypeCombinator::addNull($this->numericStringOrInt())], - [new ConstantIntegerType(7), TypeCombinator::addNull(new MixedType())], - [new ConstantIntegerType(8), TypeCombinator::addNull($this->numericStringOrInt())], - ]), - ' - SELECT IDENTITY(m.oneNull), - IDENTITY(m.one), - IDENTITY(m.oneDefaultNullability), - IDENTITY(m.compoundPk), - IDENTITY(m.compoundPk, \'id\'), - IDENTITY(m.compoundPk, \'version\'), - IDENTITY(m.compoundPkAssoc), - IDENTITY(m.compoundPkAssoc, \'version\') - FROM QueryResult\Entities\Many m - ', - ], - 'select nullable association' => [ - $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], - ]), - ' - SELECT DISTINCT(m.oneNull) - FROM QueryResult\Entities\Many m - ', - ], - 'select non null association' => [ - $this->constantArray([ - [new ConstantIntegerType(1), $this->numericStringOrInt()], - ]), - ' - SELECT DISTINCT(m.one) - FROM QueryResult\Entities\Many m - ', - ], - 'select default nullability association' => [ - $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], - ]), - ' - SELECT DISTINCT(m.oneDefaultNullability) - FROM QueryResult\Entities\Many m - ', - ], - 'select non null association in aggregated query' => [ - $this->constantArray([ - [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], - [ - new ConstantIntegerType(2), + ]) + ), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id), + COALESCE(1,1), + NEW QueryResult\Entities\OneId(m.id), + m.intColumn, + m AS many, + o AS one + FROM QueryResult\Entities\Many m + JOIN QueryResult\Entities\One o + WITH o.id = IDENTITY(m.one) + ', + ]; + + yield 'news shadown scalars' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new ObjectType(OneId::class)], + [new ConstantIntegerType(0), new ObjectType(ManyId::class)], + ]), + ' + SELECT NULLIF(m.intColumn, 1), + NEW QueryResult\Entities\ManyId(m.id), + NEW QueryResult\Entities\OneId(m.id) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'new arguments affect scalar counter' => [ + $this->constantArray([ + [new ConstantIntegerType(5), TypeCombinator::addNull($this->intStringified())], + [new ConstantIntegerType(0), new ObjectType(ManyId::class)], + [new ConstantIntegerType(1), new ObjectType(OneId::class)], + ]), + ' + SELECT NEW QueryResult\Entities\ManyId(m.id), + NEW QueryResult\Entities\OneId(m.id, m.id, m.id), + NULLIF(m.intColumn, 1) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'arithmetic' => [ + $this->constantArray([ + [new ConstantStringType('intColumn'), new IntegerType()], + [new ConstantIntegerType(1), $this->intStringified()], + [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()], + ]), + ' + SELECT m.intColumn, + +1, + 1+1, + 1+nullif(1,1), + m.intColumn*2+m.intColumn+3, + (1+1), + \'foo\' + \'bar\', + \'foo\' * \'bar\' + FROM QueryResult\Entities\Many m + ', + ]; + + 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())], + ]), + ' + SELECT ABS(m.intColumn), + ABS(NULLIF(m.intColumn, 1)), + ABS(1), + ABS(\'foo\') + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'bit_and function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), $this->uintStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(3), $this->uintStringified()], + ]), + ' + SELECT BIT_AND(m.intColumn, 1), + BIT_AND(m.intColumn, NULLIF(1,1)), + BIT_AND(1, 2) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'bit_or function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), $this->uintStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintStringified())], + [new ConstantIntegerType(3), $this->uintStringified()], + ]), + ' + SELECT BIT_OR(m.intColumn, 1), + BIT_OR(m.intColumn, NULLIF(1,1)), + BIT_OR(1, 2) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'concat 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 CONCAT(m.stringColumn, m.stringColumn), + CONCAT(m.stringColumn, m.stringNullColumn), + CONCAT(m.stringColumn, m.stringColumn, m.stringNullColumn), + CONCAT(\'foo\', \'bar\') + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'current_ functions' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new StringType()], + [new ConstantIntegerType(2), new StringType()], + [new ConstantIntegerType(3), new StringType()], + ]), + ' + SELECT CURRENT_DATE(), + CURRENT_TIME(), + CURRENT_TIMESTAMP() + FROM QueryResult\Entities\Many m + ', + ]; + + 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->numericStringified()], + [new ConstantIntegerType(2), TypeCombinator::addNull($this->numericStringified())], + [new ConstantIntegerType(3), TypeCombinator::addNull($this->numericStringified())], + [new ConstantIntegerType(4), $this->numericStringified()], + ]), + ' + SELECT DATE_DIFF(m.datetimeColumn, m.datetimeColumn), + DATE_DIFF(m.stringNullColumn, m.datetimeColumn), + DATE_DIFF(m.datetimeColumn, m.stringNullColumn), + DATE_DIFF(\'2020-01-01\', \'2019-01-01\') + FROM QueryResult\Entities\Many m + ', + ]; + + 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 + ', + ]; + + yield 'length function' => [ + $this->constantArray([ + [ + new ConstantIntegerType(1), + $this->hasTypedExpressions() + ? $this->uint() + : $this->uintStringified(), + ], + [ + new ConstantIntegerType(2), + TypeCombinator::addNull( $this->hasTypedExpressions() ? $this->uint() - : $this->uintStringified(), - ], - ]), - ' - SELECT DISTINCT(m.one), COUNT(m.one) - FROM QueryResult\Entities\Many m - ', - ], - 'joined inheritance' => [ - $this->constantArray([ - [new ConstantStringType('parentColumn'), new IntegerType()], - [new ConstantStringType('childColumn'), new IntegerType()], - ]), - ' - SELECT c.parentColumn, c.childColumn - FROM QueryResult\Entities\JoinedChild c - ', - ], - 'single table inheritance' => [ - $this->constantArray([ - [new ConstantStringType('parentColumn'), new IntegerType()], - [new ConstantStringType('childNullColumn'), TypeCombinator::addNull(new IntegerType())], - ]), - ' - SELECT c.parentColumn, c.childNullColumn - FROM QueryResult\Entities\SingleTableChild c - ', - ], - 'embedded' => [ - $this->constantArray([ - [new ConstantStringType('embedded.intColumn'), new IntegerType()], - [new ConstantStringType('embedded.stringNullColumn'), TypeCombinator::addNull(new StringType())], - [new ConstantStringType('embedded.nestedEmbedded.intColumn'), new IntegerType()], - [new ConstantStringType('embedded.nestedEmbedded.stringNullColumn'), TypeCombinator::addNull(new StringType())], - ]), - ' - SELECT o.embedded.intColumn, - o.embedded.stringNullColumn, - o.embedded.nestedEmbedded.intColumn, - o.embedded.nestedEmbedded.stringNullColumn - FROM QueryResult\Entities\One o - ', - ], + : $this->uintStringified() + ), + ], + [ + new ConstantIntegerType(3), + $this->hasTypedExpressions() + ? $this->uint() + : $this->uintStringified(), + ], + ]), + ' + SELECT LENGTH(m.stringColumn), + LENGTH(m.stringNullColumn), + LENGTH(\'foo\') + FROM QueryResult\Entities\Many m + ', + ]; + + 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()], + ]), + ' + 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 + ', + ]; + + yield 'lower function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new StringType()], + [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(3), new StringType()], + ]), + ' + SELECT LOWER(m.stringColumn), + LOWER(m.stringNullColumn), + LOWER(\'foo\') + FROM QueryResult\Entities\Many m + ', + ]; + + 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()], + ]), + ' + SELECT MOD(m.intColumn, 1), + MOD(10, m.intColumn), + MOD(NULLIF(m.intColumn, 10), 2), + MOD(10, 4) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'mod function error' => [ + $this->constantArray([ + [new ConstantIntegerType(1), TypeCombinator::addNull($this->uintStringified())], + ]), + ' + SELECT MOD(10, NULLIF(m.intColumn, m.intColumn)) + FROM QueryResult\Entities\Many m + ', + 'Modulo by zero', + ]; + + yield 'substring 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), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(5), new StringType()], + ]), + ' + SELECT SUBSTRING(m.stringColumn, m.intColumn, m.intColumn), + SUBSTRING(m.stringNullColumn, m.intColumn, m.intColumn), + SUBSTRING(m.stringColumn, NULLIF(m.intColumn, 1), m.intColumn), + SUBSTRING(m.stringColumn, m.intColumn, NULLIF(m.intColumn, 1)), + SUBSTRING(\'foo\', 1, 2) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'trim function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new StringType()], + [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(3), new StringType()], + ]), + ' + SELECT TRIM(LEADING \' \' FROM m.stringColumn), + TRIM(LEADING \' \' FROM m.stringNullColumn), + TRIM(LEADING \' \' FROM \'foo\') + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'upper function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new StringType()], + [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(3), new StringType()], + ]), + ' + SELECT UPPER(m.stringColumn), + UPPER(m.stringNullColumn), + UPPER(\'foo\') + FROM QueryResult\Entities\Many m + ', + ]; + + 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(4), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(5), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(6), TypeCombinator::addNull($this->numericStringOrInt())], + [new ConstantIntegerType(7), TypeCombinator::addNull(new MixedType())], + [new ConstantIntegerType(8), TypeCombinator::addNull($this->numericStringOrInt())], + ]), + ' + SELECT IDENTITY(m.oneNull), + IDENTITY(m.one), + IDENTITY(m.oneDefaultNullability), + IDENTITY(m.compoundPk), + IDENTITY(m.compoundPk, \'id\'), + IDENTITY(m.compoundPk, \'version\'), + IDENTITY(m.compoundPkAssoc), + IDENTITY(m.compoundPkAssoc, \'version\') + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'select nullable association' => [ + $this->constantArray([ + [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + ]), + ' + SELECT DISTINCT(m.oneNull) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'select non null association' => [ + $this->constantArray([ + [new ConstantIntegerType(1), $this->numericStringOrInt()], + ]), + ' + SELECT DISTINCT(m.one) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'select default nullability association' => [ + $this->constantArray([ + [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + ]), + ' + SELECT DISTINCT(m.oneDefaultNullability) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'select non null association in aggregated query' => [ + $this->constantArray([ + [new ConstantIntegerType(1), TypeCombinator::addNull($this->numericStringOrInt())], + [ + new ConstantIntegerType(2), + $this->hasTypedExpressions() + ? $this->uint() + : $this->uintStringified(), + ], + ]), + ' + SELECT DISTINCT(m.one), COUNT(m.one) + FROM QueryResult\Entities\Many m + ', + ]; + + yield 'joined inheritance' => [ + $this->constantArray([ + [new ConstantStringType('parentColumn'), new IntegerType()], + [new ConstantStringType('childColumn'), new IntegerType()], + ]), + ' + SELECT c.parentColumn, c.childColumn + FROM QueryResult\Entities\JoinedChild c + ', + ]; + + yield 'single table inheritance' => [ + $this->constantArray([ + [new ConstantStringType('parentColumn'), new IntegerType()], + [new ConstantStringType('childNullColumn'), TypeCombinator::addNull(new IntegerType())], + ]), + ' + SELECT c.parentColumn, c.childNullColumn + FROM QueryResult\Entities\SingleTableChild c + ', + ]; + + yield 'embedded' => [ + $this->constantArray([ + [new ConstantStringType('embedded.intColumn'), new IntegerType()], + [new ConstantStringType('embedded.stringNullColumn'), TypeCombinator::addNull(new StringType())], + [new ConstantStringType('embedded.nestedEmbedded.intColumn'), new IntegerType()], + [new ConstantStringType('embedded.nestedEmbedded.stringNullColumn'), TypeCombinator::addNull(new StringType())], + ]), + ' + SELECT o.embedded.intColumn, + o.embedded.stringNullColumn, + o.embedded.nestedEmbedded.intColumn, + o.embedded.nestedEmbedded.stringNullColumn + FROM QueryResult\Entities\One o + ', ]; } @@ -1436,4 +1578,12 @@ private static function combinations(array $arrays): iterable } } + private function isDoctrine211(): bool + { + $version = Versions::getVersion('doctrine/orm'); + + return version_compare($version, '2.11', '>=') + && version_compare($version, '2.12', '<'); + } + } diff --git a/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php new file mode 100644 index 00000000..c22acd45 --- /dev/null +++ b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php @@ -0,0 +1,41 @@ += 8.1 + +namespace QueryResult\EntitiesEnum; + +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Embedded as ORMEmbedded; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\JoinColumn; +use Doctrine\ORM\Mapping\ManyToOne; +use Doctrine\ORM\Mapping\OneToMany; + +enum IntEnum: int +{ + case A = 1; + case B = 2; +} diff --git a/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/StringEnum.php b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/StringEnum.php new file mode 100644 index 00000000..e9b7ebb1 --- /dev/null +++ b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/StringEnum.php @@ -0,0 +1,18 @@ += 8.1 + +namespace QueryResult\EntitiesEnum; + +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Embedded as ORMEmbedded; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\JoinColumn; +use Doctrine\ORM\Mapping\ManyToOne; +use Doctrine\ORM\Mapping\OneToMany; + +enum StringEnum: string +{ + case A = 'a'; + case B = 'b'; +} diff --git a/tests/Type/Doctrine/data/QueryResult/entity-manager.php b/tests/Type/Doctrine/data/QueryResult/entity-manager.php index 6a6a4025..91537c73 100644 --- a/tests/Type/Doctrine/data/QueryResult/entity-manager.php +++ b/tests/Type/Doctrine/data/QueryResult/entity-manager.php @@ -3,6 +3,7 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; @@ -15,11 +16,19 @@ $config->setMetadataCacheImpl(new DoctrineProvider(new ArrayAdapter())); $metadataDriver = new MappingDriverChain(); + $metadataDriver->addDriver(new AnnotationDriver( new AnnotationReader(), [__DIR__ . '/Entities'] ), 'QueryResult\Entities\\'); +if (property_exists(Column::class, 'enumType') && PHP_VERSION_ID >= 80100) { + $metadataDriver->addDriver(new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/EntitiesEnum'] + ), 'QueryResult\EntitiesEnum\\'); +} + $config->setMetadataDriverImpl($metadataDriver); return EntityManager::create(