From a565fdb23a446b6fdc28fc881e3bdf7cdce09d63 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 26 Jun 2024 10:52:16 +0200 Subject: [PATCH] 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(); + } + }