From d861d309754a7e3c64b0f81eb0bbe73d87f4371d Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 30 May 2024 13:01:22 +0200 Subject: [PATCH 1/8] Platform test: compare inferred types with real SQL engine results --- .github/workflows/platform-matrix-test.yml | 58 +++ .gitignore | 1 + Makefile | 2 +- phpstan.neon | 4 + src/Type/Doctrine/Descriptors/BooleanType.php | 3 +- tests/Platform/MatrixEntity/TestEntity.php | 56 +++ ...eryResultTypeWalkerFetchTypeMatrixTest.php | 416 ++++++++++++++++++ tests/Platform/README.md | 19 + tests/Platform/data/config.neon | 5 + tests/Platform/docker/Dockerfile80 | 7 + tests/Platform/docker/Dockerfile81 | 7 + tests/Platform/docker/docker-compose.yml | 40 ++ tests/Platform/docker/docker-setup.sh | 7 + 13 files changed, 623 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/platform-matrix-test.yml create mode 100644 tests/Platform/MatrixEntity/TestEntity.php create mode 100644 tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php create mode 100644 tests/Platform/README.md create mode 100644 tests/Platform/data/config.neon create mode 100644 tests/Platform/docker/Dockerfile80 create mode 100644 tests/Platform/docker/Dockerfile81 create mode 100644 tests/Platform/docker/docker-compose.yml create mode 100755 tests/Platform/docker/docker-setup.sh diff --git a/.github/workflows/platform-matrix-test.yml b/.github/workflows/platform-matrix-test.yml new file mode 100644 index 00000000..edad3371 --- /dev/null +++ b/.github/workflows/platform-matrix-test.yml @@ -0,0 +1,58 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Platform matrix test" + +on: + pull_request: + push: + branches: + - "1.4.x" + +jobs: + tests: + name: "Platform matrix test" + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: + php-version: + - "8.0" + - "8.1" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + ini-file: development + extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Run platform matrix test" + run: vendor/bin/phpunit --group=platform + + services: + postgres: + image: "postgres:latest" + env: + POSTGRES_PASSWORD: "secret" + POSTGRES_USER: root + POSTGRES_DB: foo + ports: + - "5432:5432" + + mysql: + image: "mysql:latest" + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: foo + ports: + - "3306:3306" diff --git a/.gitignore b/.gitignore index 7de9f3c5..e3d740a7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /build-cs /vendor /composer.lock +/.env .phpunit.result.cache diff --git a/Makefile b/Makefile index 2ec6452c..382dfa5c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ check: lint cs tests phpstan .PHONY: tests tests: - php vendor/bin/phpunit + php vendor/bin/phpunit --exclude-group=platform .PHONY: lint lint: diff --git a/phpstan.neon b/phpstan.neon index b3c5d642..429cf149 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -43,3 +43,7 @@ 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 + - + message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + reportUnmatched: false diff --git a/src/Type/Doctrine/Descriptors/BooleanType.php b/src/Type/Doctrine/Descriptors/BooleanType.php index 0e6980c0..955883a8 100644 --- a/src/Type/Doctrine/Descriptors/BooleanType.php +++ b/src/Type/Doctrine/Descriptors/BooleanType.php @@ -28,7 +28,8 @@ public function getDatabaseInternalType(): Type { return TypeCombinator::union( new ConstantIntegerType(0), - new ConstantIntegerType(1) + new ConstantIntegerType(1), + new \PHPStan\Type\BooleanType() ); } diff --git a/tests/Platform/MatrixEntity/TestEntity.php b/tests/Platform/MatrixEntity/TestEntity.php new file mode 100644 index 00000000..d6ac4a26 --- /dev/null +++ b/tests/Platform/MatrixEntity/TestEntity.php @@ -0,0 +1,56 @@ + $connectionParams + * @param array $expectedOnPhp80AndBelow + * @param array $expectedOnPhp81AndAbove + * @param array $connectionAttributes + * + * @dataProvider provideCases + */ + public function testFetchedTypes( + array $connectionParams, + array $expectedOnPhp80AndBelow, + array $expectedOnPhp81AndAbove, + array $connectionAttributes + ): void + { + $phpVersion = PHP_MAJOR_VERSION * 10 + PHP_MINOR_VERSION; + + try { + $connection = DriverManager::getConnection($connectionParams + [ + 'user' => 'root', + 'password' => 'secret', + 'dbname' => 'foo', + ]); + + $nativeConnection = $this->getNativeConnection($connection); + $this->setupAttributes($nativeConnection, $connectionAttributes); + + $config = new Configuration(); + $config->setProxyNamespace('PHPstan\Doctrine\OrmMatrixProxies'); + $config->setProxyDir('/tmp/doctrine'); + $config->setAutoGenerateProxyClasses(false); + $config->setSecondLevelCacheEnabled(false); + $config->setMetadataCache(new ArrayCachePool()); + $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), [__DIR__ . '/MatrixEntity'])); + $entityManager = new EntityManager($connection, $config); + + } catch (DbalException $e) { + if (strpos($e->getMessage(), 'Doctrine currently supports only the following drivers') !== false) { + self::markTestSkipped($e->getMessage()); // older doctrine versions, needed for old PHP versions + } + throw $e; + } + + $schemaTool = new SchemaTool($entityManager); + $classes = $entityManager->getMetadataFactory()->getAllMetadata(); + $schemaTool->dropSchema($classes); + $schemaTool->createSchema($classes); + + $entity = new TestEntity(); + $entity->col_bool = true; + $entity->col_float = 0.125; + $entity->col_decimal = '0.1'; + $entity->col_int = 9; + $entity->col_bigint = 2147483648; + $entity->col_string = 'foobar'; + + $entityManager->persist($entity); + $entityManager->flush(); + + $columnsQueryTemplate = 'SELECT %s FROM MatrixEntity\TestEntity t GROUP BY t.col_int, t.col_float, t.col_decimal, t.col_bigint, t.col_bool, t.col_string'; + + $expected = $phpVersion >= 81 + ? $expectedOnPhp81AndAbove + : $expectedOnPhp80AndBelow; + + foreach ($expected as $select => $expectedType) { + if ($expectedType === null) { + continue; // e.g. no such function + } + $dql = sprintf($columnsQueryTemplate, $select); + + $query = $entityManager->createQuery($dql); + $result = $query->getSingleResult(); + + $typeBuilder = new QueryResultTypeBuilder(); + QueryResultTypeWalker::walk($query, $typeBuilder, self::getContainer()->getByType(DescriptorRegistry::class)); + + $inferredPhpStanType = $typeBuilder->getResultType(); + $realRowPhpStanType = ConstantTypeHelper::getTypeFromValue($result); + + $firstResult = reset($result); + $resultType = gettype($firstResult); + $resultExported = var_export($firstResult, true); + + self::assertTrue( + $inferredPhpStanType->accepts($realRowPhpStanType, true)->yes(), + sprintf( + "Result of 'SELECT %s' for '%s' and PHP %s was inferred as %s, but the real result was %s", + $select, + $this->dataName(), + $phpVersion, + $inferredPhpStanType->describe(VerbosityLevel::precise()), + $realRowPhpStanType->describe(VerbosityLevel::precise()) + ) + ); + + self::assertThat( + $firstResult, + new IsType($expectedType), + sprintf( + "Result of 'SELECT %s' for '%s' and PHP %s is expected to be %s, but %s returned (%s).", + $select, + $this->dataName(), + $phpVersion, + $expectedType, + $resultType, + $resultExported + ) + ); + } + } + + /** + * @return iterable + + */ + public function provideCases(): iterable + { + // Preserve space-driven formatting for better readability + // phpcs:disable Squiz.WhiteSpace.OperatorSpacing.SpacingBefore + // phpcs:disable Squiz.WhiteSpace.OperatorSpacing.SpacingAfter + + // Notes: + // - Any direct column fetch uses the type declared in entity, but when passed to a function, the driver decides the type + + $testData = [ // mysql, sqlite, pdo_pgsql, pgsql, stringified, stringifiedOldPostgre + // bool-ish + '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'], + 'ABS(t.col_string)' => ['float', 'float', null, null, null, null], // postgre: function abs(character varying) does not exist + // TODO fix inferring 'MOD(t.col_float, 2)' => ['float', null, null, null, null, null,], // postgre: function mod(double precision, integer) does not exist + // sqlite: Implicit conversion from float 0.125 to int loses precision in \Doctrine\DBAL\Driver\API\SQLite\UserDefinedFunctions:46 + + // 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'], + // TODO fix inferrring 'SQRT(-1)' => ['null', 'null', null, null, null, null,], // postgre: cannot take square root of a negative number + + 'MOD(t.col_decimal, 2)' => ['string', null, null, null, null, null], // postgre: function mod(double precision, integer) does not exist + // sqlite: Implicit conversion from float 0.125 to int loses precision in \Doctrine\DBAL\Driver\API\SQLite\UserDefinedFunctions:46 + + // int-ish + '1' => ['int', 'int', 'int', 'int', 'string', 'string'], + '2147483648' => ['int', 'int', 'int', 'int', 'string', 'string'], + 't.col_int' => ['int', 'int', 'int', 'int', 'int', 'int'], + 't.col_bigint' => ['string', 'string', 'string', 'string', 'string', 'string'], + 'SUM(t.col_int)' => ['string', 'int', 'int', 'int', 'string', 'string'], + 'SUM(t.col_bigint)' => ['string', 'int', 'string', 'string', 'string', 'string'], + "LENGTH('')" => ['int', 'int', 'int', 'int', 'int', 'int'], + 'COUNT(t)' => ['int', 'int', 'int', 'int', 'int', 'int'], + 'COUNT(1)' => ['int', 'int', 'int', 'int', 'int', 'int'], + 'COUNT(t.col_int)' => ['int', 'int', 'int', 'int', 'int', 'int'], + 'MIN(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MIN(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MAX(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MAX(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MOD(t.col_int, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MOD(t.col_bigint, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'ABS(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'ABS(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], + + // string + 't.col_string' => ['string', 'string', 'string', 'string', 'string', 'string'], + 'LOWER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], + 'UPPER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], + 'TRIM(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], + ]; + + $selects = array_keys($testData); + + $nativeMysql = array_combine($selects, array_column($testData, 0)); + $nativeSqlite = array_combine($selects, array_column($testData, 1)); + $nativePdoPg = array_combine($selects, array_column($testData, 2)); + $nativePg = array_combine($selects, array_column($testData, 3)); + + $stringified = array_combine($selects, array_column($testData, 4)); + $stringifiedOldPostgre = array_combine($selects, array_column($testData, 5)); + + yield 'sqlite3' => [ + 'connection' => ['driver' => 'sqlite3', 'memory' => true], + 'php80-' => $nativeSqlite, + 'php81+' => $nativeSqlite, + 'setup' => [], + ]; + + yield 'pdo_sqlite, no stringify' => [ + 'connection' => ['driver' => 'pdo_sqlite', 'memory' => true], + 'php80-' => $stringified, + 'php81+' => $nativeSqlite, + 'setup' => [], + ]; + + yield 'pdo_sqlite, stringify' => [ + 'connection' => ['driver' => 'pdo_sqlite', 'memory' => true], + 'php80-' => $stringified, + 'php81+' => $stringified, + 'setup' => [PDO::ATTR_STRINGIFY_FETCHES => true], + ]; + + yield 'mysqli, no native numbers' => [ + 'connection' => ['driver' => 'mysqli', 'host' => 'mysql'], + 'php80-' => $nativeMysql, + 'php81+' => $nativeMysql, + 'setup' => [ + // This has no effect when using prepared statements (which is what doctrine/dbal uses) + // - prepared statements => always native types + // - non-prepared statements => stringified by default, can be changed by MYSQLI_OPT_INT_AND_FLOAT_NATIVE = true + // documented here: https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php#example-4303 + MYSQLI_OPT_INT_AND_FLOAT_NATIVE => false, + ], + ]; + + yield 'mysqli, native numbers' => [ + 'connection' => ['driver' => 'mysqli', 'host' => 'mysql'], + 'php80-' => $nativeMysql, + 'php81+' => $nativeMysql, + 'setup' => [MYSQLI_OPT_INT_AND_FLOAT_NATIVE => true], + ]; + + yield 'pdo_mysql, stringify, no emulate' => [ + 'connection' => ['driver' => 'pdo_mysql', 'host' => 'mysql'], + 'php80-' => $stringified, + 'php81+' => $stringified, + 'setup' => [ + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_STRINGIFY_FETCHES => true, + ], + ]; + + yield 'pdo_mysql, no stringify, no emulate' => [ + 'connection' => ['driver' => 'pdo_mysql', 'host' => 'mysql'], + 'php80-' => $nativeMysql, + 'php81+' => $nativeMysql, + 'setup' => [PDO::ATTR_EMULATE_PREPARES => false], + ]; + + yield 'pdo_mysql, no stringify, emulate' => [ + 'connection' => ['driver' => 'pdo_mysql', 'host' => 'mysql'], + 'php80-' => $stringified, + 'php81+' => $nativeMysql, + 'setup' => [], // defaults + ]; + + yield 'pdo_mysql, stringify, emulate' => [ + 'connection' => ['driver' => 'pdo_mysql', 'host' => 'mysql'], + 'php80-' => $stringified, + 'php81+' => $stringified, + 'setup' => [ + PDO::ATTR_STRINGIFY_FETCHES => true, + ], + ]; + + yield 'pdo_pgsql, stringify' => [ + 'connection' => ['driver' => 'pdo_pgsql', 'host' => 'pgsql'], + + 'php80-' => $stringifiedOldPostgre, + 'php81+' => $stringified, + 'setup' => [PDO::ATTR_STRINGIFY_FETCHES => true], + ]; + + yield 'pdo_pgsql, no stringify' => [ + 'connection' => ['driver' => 'pdo_pgsql', 'host' => 'pgsql'], + 'php80-' => $nativePdoPg, + 'php81+' => $nativePdoPg, + 'setup' => [], + ]; + + yield 'pgsql' => [ + 'connection' => ['driver' => 'pgsql', 'host' => 'pgsql'], + 'php80-' => $nativePg, + 'php81+' => $nativePg, + 'setup' => [], + ]; + } + + /** + * @param mixed $nativeConnection + * @param array $attributes + */ + private function setupAttributes($nativeConnection, array $attributes): void + { + if ($nativeConnection instanceof PDO) { + foreach ($attributes as $attribute => $value) { + $set = $nativeConnection->setAttribute($attribute, $value); + if (!$set) { + throw new LogicException(sprintf('Failed to set attribute %s to %s', $attribute, $value)); + } + } + + } elseif ($nativeConnection instanceof mysqli) { + foreach ($attributes as $attribute => $value) { + $set = $nativeConnection->options($attribute, $value); + if (!$set) { + throw new LogicException(sprintf('Failed to set attribute %s to %s', $attribute, $value)); + } + } + + } elseif ($nativeConnection instanceof NativePgsqlConnection) { + if ($attributes !== []) { + throw new LogicException('Cannot set attributes for ' . NativePgsqlConnection::class . ' driver'); + } + + } elseif ($nativeConnection instanceof SQLite3) { + if ($attributes !== []) { + throw new LogicException('Cannot set attributes for ' . NativePgsqlConnection::class . ' driver'); + } + + } elseif (is_resource($nativeConnection)) { // e.g. `resource (pgsql link)` on PHP < 8.1 with pgsql driver + if ($attributes !== []) { + throw new LogicException('Cannot set attributes for this resource'); + } + + } else { + throw new LogicException('Unexpected connection: ' . (function_exists('get_debug_type') ? get_debug_type($nativeConnection) : gettype($nativeConnection))); + } + } + + /** + * @return mixed + */ + private function getNativeConnection(Connection $connection) + { + if (method_exists($connection, 'getNativeConnection')) { + return $connection->getNativeConnection(); + } + + if ($connection->getWrappedConnection() instanceof PDO) { + return $connection->getWrappedConnection(); + } + + if (get_class($connection->getWrappedConnection()) === 'Doctrine\DBAL\Driver\Mysqli\MysqliConnection' && method_exists($connection->getWrappedConnection(), 'getWrappedResourceHandle')) { + return $connection->getWrappedConnection()->getWrappedResourceHandle(); + } + + throw new LogicException('Unable to get native connection'); + } + +} diff --git a/tests/Platform/README.md b/tests/Platform/README.md new file mode 100644 index 00000000..b9c07d6a --- /dev/null +++ b/tests/Platform/README.md @@ -0,0 +1,19 @@ + +## How to run platform tests in docker + +Set current working directory to project root. + +```sh +# Init services & dependencies +- `printf "UID=$(id -u)\nGID=$(id -g)" > .env` +- `docker-compose -f tests/Platform/docker/docker-compose.yml up -d` +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 composer install` + +# Test behaviour with old stringification +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php80 php -d memory_limit=1G vendor/bin/phpunit --group=platform` + +# Test behaviour with new stringification +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform` +``` + +You can also run utilize those containers for PHPStorm PHPUnit configuration. diff --git a/tests/Platform/data/config.neon b/tests/Platform/data/config.neon new file mode 100644 index 00000000..38f26ed7 --- /dev/null +++ b/tests/Platform/data/config.neon @@ -0,0 +1,5 @@ +includes: + - ../../../extension.neon +parameters: + featureToggles: + listType: true diff --git a/tests/Platform/docker/Dockerfile80 b/tests/Platform/docker/Dockerfile80 new file mode 100644 index 00000000..37b6694c --- /dev/null +++ b/tests/Platform/docker/Dockerfile80 @@ -0,0 +1,7 @@ +FROM php:8.0-cli + +COPY ./docker-setup.sh /opt/src/scripts/setup.sh +RUN /opt/src/scripts/setup.sh + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + diff --git a/tests/Platform/docker/Dockerfile81 b/tests/Platform/docker/Dockerfile81 new file mode 100644 index 00000000..4ef5c3df --- /dev/null +++ b/tests/Platform/docker/Dockerfile81 @@ -0,0 +1,7 @@ +FROM php:8.1-cli + +COPY ./docker-setup.sh /opt/src/scripts/setup.sh +RUN /opt/src/scripts/setup.sh + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + diff --git a/tests/Platform/docker/docker-compose.yml b/tests/Platform/docker/docker-compose.yml new file mode 100644 index 00000000..c7431b6a --- /dev/null +++ b/tests/Platform/docker/docker-compose.yml @@ -0,0 +1,40 @@ +# the setup here should be in sync with GitHub CI services, see platform-matrix-test.yml + +services: + mysql: + image: mysql:8.0 + command: mysqld --default-authentication-plugin=mysql_native_password + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: foo + + pgsql: + image: postgres:13 + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: secret + POSTGRES_USER: root + POSTGRES_DB: foo + + php80: + depends_on: [mysql, pgsql] + build: + context: . + dockerfile: ./Dockerfile80 + working_dir: /app + user: ${UID:-1000}:${GID:-1000} + volumes: + - ../../../:/app + + php81: + depends_on: [mysql, pgsql] + build: + context: . + dockerfile: ./Dockerfile81 + working_dir: /app + user: ${UID:-1000}:${GID:-1000} + volumes: + - ../../../:/app diff --git a/tests/Platform/docker/docker-setup.sh b/tests/Platform/docker/docker-setup.sh new file mode 100755 index 00000000..341c88c2 --- /dev/null +++ b/tests/Platform/docker/docker-setup.sh @@ -0,0 +1,7 @@ +set -ex \ + && apt update \ + && apt install -y bash zip libpq-dev libsqlite3-dev \ + && pecl install xdebug mongodb \ + && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ + && docker-php-ext-install pdo mysqli pgsql pdo_mysql pdo_pgsql pdo_sqlite \ + && docker-php-ext-enable xdebug mongodb From d3f892452d417f3bb98b4b801334ae11c46ffd71 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 30 May 2024 13:17:33 +0200 Subject: [PATCH 2/8] Fix entity name and hostnames --- .github/workflows/platform-matrix-test.yml | 2 ++ tests/Platform/MatrixEntity/TestEntity.php | 2 +- tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/platform-matrix-test.yml b/.github/workflows/platform-matrix-test.yml index edad3371..f428cf08 100644 --- a/.github/workflows/platform-matrix-test.yml +++ b/.github/workflows/platform-matrix-test.yml @@ -41,6 +41,7 @@ jobs: services: postgres: image: "postgres:latest" + options: --hostname pgsql env: POSTGRES_PASSWORD: "secret" POSTGRES_USER: root @@ -50,6 +51,7 @@ jobs: mysql: image: "mysql:latest" + options: --hostname mysql env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ROOT_PASSWORD: secret diff --git a/tests/Platform/MatrixEntity/TestEntity.php b/tests/Platform/MatrixEntity/TestEntity.php index d6ac4a26..74f2ae57 100644 --- a/tests/Platform/MatrixEntity/TestEntity.php +++ b/tests/Platform/MatrixEntity/TestEntity.php @@ -49,7 +49,7 @@ class TestEntity /** * @ORM\Id * @ORM\Column(type="bigint", name="col_bigint", nullable=false) - * @var int + * @var string */ public $col_bigint; diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index f0b7da34..5b5fd942 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -109,13 +109,13 @@ public function testFetchedTypes( $entity->col_float = 0.125; $entity->col_decimal = '0.1'; $entity->col_int = 9; - $entity->col_bigint = 2147483648; + $entity->col_bigint = '2147483648'; $entity->col_string = 'foobar'; $entityManager->persist($entity); $entityManager->flush(); - $columnsQueryTemplate = 'SELECT %s FROM MatrixEntity\TestEntity t GROUP BY t.col_int, t.col_float, t.col_decimal, t.col_bigint, t.col_bool, t.col_string'; + $columnsQueryTemplate = 'SELECT %s FROM %s t GROUP BY t.col_int, t.col_float, t.col_decimal, t.col_bigint, t.col_bool, t.col_string'; $expected = $phpVersion >= 81 ? $expectedOnPhp81AndAbove @@ -125,7 +125,7 @@ public function testFetchedTypes( if ($expectedType === null) { continue; // e.g. no such function } - $dql = sprintf($columnsQueryTemplate, $select); + $dql = sprintf($columnsQueryTemplate, $select, TestEntity::class); $query = $entityManager->createQuery($dql); $result = $query->getSingleResult(); From 2eb40822d09dae6a50dc65864ddc115fceebeaa2 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 30 May 2024 13:34:29 +0200 Subject: [PATCH 3/8] configurable host --- .github/workflows/platform-matrix-test.yml | 5 +++-- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 19 ++++++++++--------- tests/Platform/docker/docker-compose.yml | 6 ++++++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.github/workflows/platform-matrix-test.yml b/.github/workflows/platform-matrix-test.yml index f428cf08..1516b332 100644 --- a/.github/workflows/platform-matrix-test.yml +++ b/.github/workflows/platform-matrix-test.yml @@ -12,6 +12,9 @@ jobs: tests: name: "Platform matrix test" runs-on: "ubuntu-latest" + env: + MYSQL_HOST: localhost + PGSQL_HOST: localhost strategy: fail-fast: false @@ -41,7 +44,6 @@ jobs: services: postgres: image: "postgres:latest" - options: --hostname pgsql env: POSTGRES_PASSWORD: "secret" POSTGRES_USER: root @@ -51,7 +53,6 @@ jobs: mysql: image: "mysql:latest" - options: --hostname mysql env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ROOT_PASSWORD: secret diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 5b5fd942..52020836 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -30,6 +30,7 @@ use function function_exists; use function get_class; use function get_debug_type; +use function getenv; use function gettype; use function is_resource; use function method_exists; @@ -276,7 +277,7 @@ public function provideCases(): iterable ]; yield 'mysqli, no native numbers' => [ - 'connection' => ['driver' => 'mysqli', 'host' => 'mysql'], + 'connection' => ['driver' => 'mysqli', 'host' => getenv('MYSQL_HOST')], 'php80-' => $nativeMysql, 'php81+' => $nativeMysql, 'setup' => [ @@ -289,14 +290,14 @@ public function provideCases(): iterable ]; yield 'mysqli, native numbers' => [ - 'connection' => ['driver' => 'mysqli', 'host' => 'mysql'], + 'connection' => ['driver' => 'mysqli', 'host' => getenv('MYSQL_HOST')], 'php80-' => $nativeMysql, 'php81+' => $nativeMysql, 'setup' => [MYSQLI_OPT_INT_AND_FLOAT_NATIVE => true], ]; yield 'pdo_mysql, stringify, no emulate' => [ - 'connection' => ['driver' => 'pdo_mysql', 'host' => 'mysql'], + 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], 'php80-' => $stringified, 'php81+' => $stringified, 'setup' => [ @@ -306,21 +307,21 @@ public function provideCases(): iterable ]; yield 'pdo_mysql, no stringify, no emulate' => [ - 'connection' => ['driver' => 'pdo_mysql', 'host' => 'mysql'], + 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], 'php80-' => $nativeMysql, 'php81+' => $nativeMysql, 'setup' => [PDO::ATTR_EMULATE_PREPARES => false], ]; yield 'pdo_mysql, no stringify, emulate' => [ - 'connection' => ['driver' => 'pdo_mysql', 'host' => 'mysql'], + 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], 'php80-' => $stringified, 'php81+' => $nativeMysql, 'setup' => [], // defaults ]; yield 'pdo_mysql, stringify, emulate' => [ - 'connection' => ['driver' => 'pdo_mysql', 'host' => 'mysql'], + 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')], 'php80-' => $stringified, 'php81+' => $stringified, 'setup' => [ @@ -329,7 +330,7 @@ public function provideCases(): iterable ]; yield 'pdo_pgsql, stringify' => [ - 'connection' => ['driver' => 'pdo_pgsql', 'host' => 'pgsql'], + 'connection' => ['driver' => 'pdo_pgsql', 'host' => getenv('PGSQL_HOST')], 'php80-' => $stringifiedOldPostgre, 'php81+' => $stringified, @@ -337,14 +338,14 @@ public function provideCases(): iterable ]; yield 'pdo_pgsql, no stringify' => [ - 'connection' => ['driver' => 'pdo_pgsql', 'host' => 'pgsql'], + 'connection' => ['driver' => 'pdo_pgsql', 'host' => getenv('PGSQL_HOST')], 'php80-' => $nativePdoPg, 'php81+' => $nativePdoPg, 'setup' => [], ]; yield 'pgsql' => [ - 'connection' => ['driver' => 'pgsql', 'host' => 'pgsql'], + 'connection' => ['driver' => 'pgsql', 'host' => getenv('PGSQL_HOST')], 'php80-' => $nativePg, 'php81+' => $nativePg, 'setup' => [], diff --git a/tests/Platform/docker/docker-compose.yml b/tests/Platform/docker/docker-compose.yml index c7431b6a..5ff6fbb8 100644 --- a/tests/Platform/docker/docker-compose.yml +++ b/tests/Platform/docker/docker-compose.yml @@ -24,6 +24,9 @@ services: build: context: . dockerfile: ./Dockerfile80 + environment: + MYSQL_HOST: mysql + PGSQL_HOST: pgsql working_dir: /app user: ${UID:-1000}:${GID:-1000} volumes: @@ -34,6 +37,9 @@ services: build: context: . dockerfile: ./Dockerfile81 + environment: + MYSQL_HOST: mysql + PGSQL_HOST: pgsql working_dir: /app user: ${UID:-1000}:${GID:-1000} volumes: From 5ddaed5382435020c15300f1d5f05160e60a991c Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 30 May 2024 13:45:51 +0200 Subject: [PATCH 4/8] Try 127.0.0.1 --- .github/workflows/platform-matrix-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/platform-matrix-test.yml b/.github/workflows/platform-matrix-test.yml index 1516b332..a74ec83c 100644 --- a/.github/workflows/platform-matrix-test.yml +++ b/.github/workflows/platform-matrix-test.yml @@ -13,8 +13,8 @@ jobs: name: "Platform matrix test" runs-on: "ubuntu-latest" env: - MYSQL_HOST: localhost - PGSQL_HOST: localhost + MYSQL_HOST: '127.0.0.1' + PGSQL_HOST: '127.0.0.1' strategy: fail-fast: false From 486992bb2c89918b75931fab4bcca02b42b1b095 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 30 May 2024 13:53:42 +0200 Subject: [PATCH 5/8] PHPStan fixes --- phpstan.neon | 4 +++- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 21 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 429cf149..8dfe69fa 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -44,6 +44,8 @@ parameters: path: src/Doctrine/Mapping/ClassMetadataFactory.php reportUnmatched: false - - message: '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' + messages: + - '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#' + - '#^Cannot call method getWrappedResourceHandle\(\) on class\-string\|object\.$#' path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php reportUnmatched: false diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 52020836..77f3c22e 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -14,7 +14,6 @@ use LogicException; use mysqli; use PDO; -use PgSql\Connection as NativePgsqlConnection; use PHPStan\Platform\MatrixEntity\TestEntity; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ConstantTypeHelper; @@ -28,10 +27,10 @@ use function array_combine; use function array_keys; use function function_exists; -use function get_class; use function get_debug_type; use function getenv; use function gettype; +use function is_a; use function is_resource; use function method_exists; use function reset; @@ -374,14 +373,14 @@ private function setupAttributes($nativeConnection, array $attributes): void } } - } elseif ($nativeConnection instanceof NativePgsqlConnection) { + } elseif (is_a($nativeConnection, 'PgSql\Connection', true)) { if ($attributes !== []) { - throw new LogicException('Cannot set attributes for ' . NativePgsqlConnection::class . ' driver'); + throw new LogicException('Cannot set attributes for PgSql\Connection driver'); } } elseif ($nativeConnection instanceof SQLite3) { if ($attributes !== []) { - throw new LogicException('Cannot set attributes for ' . NativePgsqlConnection::class . ' driver'); + throw new LogicException('Cannot set attributes for ' . SQLite3::class . ' driver'); } } elseif (is_resource($nativeConnection)) { // e.g. `resource (pgsql link)` on PHP < 8.1 with pgsql driver @@ -403,12 +402,14 @@ private function getNativeConnection(Connection $connection) return $connection->getNativeConnection(); } - if ($connection->getWrappedConnection() instanceof PDO) { - return $connection->getWrappedConnection(); - } + if (method_exists($connection, 'getWrappedConnection')) { + if ($connection->getWrappedConnection() instanceof PDO) { + return $connection->getWrappedConnection(); + } - if (get_class($connection->getWrappedConnection()) === 'Doctrine\DBAL\Driver\Mysqli\MysqliConnection' && method_exists($connection->getWrappedConnection(), 'getWrappedResourceHandle')) { - return $connection->getWrappedConnection()->getWrappedResourceHandle(); + if (method_exists($connection->getWrappedConnection(), 'getWrappedResourceHandle')) { + return $connection->getWrappedConnection()->getWrappedResourceHandle(); + } } throw new LogicException('Unable to get native connection'); From fffa94f8d65c4ddb22873cbfc6e175d36bdac894 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 30 May 2024 14:44:06 +0200 Subject: [PATCH 6/8] Remove problematic testcases for now, those will be solved with proper inferring --- .../Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 77f3c22e..6a61d26e 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -170,7 +170,6 @@ public function testFetchedTypes( /** * @return iterable - */ public function provideCases(): iterable { @@ -194,9 +193,6 @@ public function provideCases(): iterable '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'], - 'ABS(t.col_string)' => ['float', 'float', null, null, null, null], // postgre: function abs(character varying) does not exist - // TODO fix inferring 'MOD(t.col_float, 2)' => ['float', null, null, null, null, null,], // postgre: function mod(double precision, integer) does not exist - // sqlite: Implicit conversion from float 0.125 to int loses precision in \Doctrine\DBAL\Driver\API\SQLite\UserDefinedFunctions:46 // decimal-ish 't.col_decimal' => ['string', 'string', 'string', 'string', 'string', 'string'], @@ -212,10 +208,6 @@ public function provideCases(): iterable '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'], - // TODO fix inferrring 'SQRT(-1)' => ['null', 'null', null, null, null, null,], // postgre: cannot take square root of a negative number - - 'MOD(t.col_decimal, 2)' => ['string', null, null, null, null, null], // postgre: function mod(double precision, integer) does not exist - // sqlite: Implicit conversion from float 0.125 to int loses precision in \Doctrine\DBAL\Driver\API\SQLite\UserDefinedFunctions:46 // int-ish '1' => ['int', 'int', 'int', 'int', 'string', 'string'], From 5dfa0ca16d104b806c64c75fc08915b94cc02d21 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 30 May 2024 16:40:25 +0200 Subject: [PATCH 7/8] Postgre: fix even literal inferring --- .../Doctrine/Query/QueryResultTypeWalker.php | 8 +- ...eryResultTypeWalkerFetchTypeMatrixTest.php | 91 ++++++++++--------- .../Query/QueryResultTypeWalkerTest.php | 28 ++++-- 3 files changed, 72 insertions(+), 55 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index d68bcdb8..55375c5f 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -14,6 +14,7 @@ use Doctrine\ORM\Query\SqlWalker; use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -1113,8 +1114,11 @@ public function walkLiteral($literal): string break; case AST\Literal::BOOLEAN: - $value = strtolower($literal->value) === 'true' ? 1 : 0; - $type = new ConstantIntegerType($value); + $value = strtolower($literal->value) === 'true'; + $type = TypeCombinator::union( + new ConstantIntegerType($value ? 1 : 0), + new ConstantBooleanType($value) + ); break; case AST\Literal::NUMERIC: diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 6a61d26e..11123c4b 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -180,60 +180,61 @@ public function provideCases(): iterable // 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 + $testData = [ // mysql, sqlite, pdo_pgsql, pgsql, stringified, stringifiedOldPostgre // bool-ish - 't.col_bool' => ['bool', 'bool', 'bool', 'bool', 'bool', 'bool'], - 'COALESCE(t.col_bool, TRUE)' => ['int', 'int', 'bool', 'bool', 'string', 'bool'], + '(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'], + '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'], + 't.col_decimal' => ['string', 'string', 'string', 'string', 'string', 'string'], + '0.1' => ['string', 'float', 'string', 'string', 'string', 'string'], + '0.125e0' => ['float', 'float', 'string', 'string', 'string', 'string'], + 'AVG(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'AVG(t.col_int)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'AVG(t.col_bigint)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'SUM(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'MIN(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'MAX(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], + 'SQRT(t.col_decimal)' => ['float', 'float', 'string', 'string', 'string', 'string'], + 'SQRT(t.col_int)' => ['float', 'float', 'string', 'float', 'string', 'string'], + 'SQRT(t.col_bigint)' => ['float', null, 'string', 'float', null, null], // sqlite3 returns float, but pdo_sqlite returns NULL + 'ABS(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'], // int-ish - '1' => ['int', 'int', 'int', 'int', 'string', 'string'], - '2147483648' => ['int', 'int', 'int', 'int', 'string', 'string'], - 't.col_int' => ['int', 'int', 'int', 'int', 'int', 'int'], - 't.col_bigint' => ['string', 'string', 'string', 'string', 'string', 'string'], - 'SUM(t.col_int)' => ['string', 'int', 'int', 'int', 'string', 'string'], - 'SUM(t.col_bigint)' => ['string', 'int', 'string', 'string', 'string', 'string'], - "LENGTH('')" => ['int', 'int', 'int', 'int', 'int', 'int'], - 'COUNT(t)' => ['int', 'int', 'int', 'int', 'int', 'int'], - 'COUNT(1)' => ['int', 'int', 'int', 'int', 'int', 'int'], - 'COUNT(t.col_int)' => ['int', 'int', 'int', 'int', 'int', 'int'], - 'MIN(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MIN(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MAX(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MAX(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MOD(t.col_int, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'MOD(t.col_bigint, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'ABS(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], - 'ABS(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], + '1' => ['int', 'int', 'int', 'int', 'string', 'string'], + '2147483648' => ['int', 'int', 'int', 'int', 'string', 'string'], + 't.col_int' => ['int', 'int', 'int', 'int', 'int', 'int'], + 't.col_bigint' => ['string', 'string', 'string', 'string', 'string', 'string'], + 'SUM(t.col_int)' => ['string', 'int', 'int', 'int', 'string', 'string'], + 'SUM(t.col_bigint)' => ['string', 'int', 'string', 'string', 'string', 'string'], + "LENGTH('')" => ['int', 'int', 'int', 'int', 'int', 'int'], + 'COUNT(t)' => ['int', 'int', 'int', 'int', 'int', 'int'], + 'COUNT(1)' => ['int', 'int', 'int', 'int', 'int', 'int'], + 'COUNT(t.col_int)' => ['int', 'int', 'int', 'int', 'int', 'int'], + 'MIN(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MIN(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MAX(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MAX(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MOD(t.col_int, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'MOD(t.col_bigint, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'ABS(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'], + 'ABS(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'], // string - 't.col_string' => ['string', 'string', 'string', 'string', 'string', 'string'], - 'LOWER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], - 'UPPER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], - 'TRIM(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'], + '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); diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 983362c2..f2f349ed 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -13,7 +13,9 @@ use Doctrine\ORM\Tools\SchemaTool; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; @@ -794,7 +796,8 @@ public function getTestData(): iterable TypeCombinator::union( new ConstantIntegerType(1), new ConstantStringType('1'), - new NullType() + new NullType(), + new ConstantBooleanType(true) ), ], ]), @@ -810,7 +813,8 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - new IntegerType() + new IntegerType(), + new ConstantBooleanType(false) ), ], [ @@ -839,6 +843,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), + new ConstantBooleanType(false), new ConstantIntegerType(0) ), ], @@ -859,6 +864,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), + new ConstantBooleanType(false), new ConstantIntegerType(0) ), ], @@ -881,7 +887,8 @@ public function getTestData(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), - new ConstantStringType('1') + new ConstantStringType('1'), + new BooleanType() ), ], ]), @@ -902,7 +909,8 @@ public function getTestData(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantStringType('0'), - new ConstantStringType('1') + new ConstantStringType('1'), + new BooleanType() ), ], ]), @@ -921,28 +929,32 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new ConstantIntegerType(1), - new ConstantStringType('1') + new ConstantStringType('1'), + new ConstantBooleanType(true) ), ], [ new ConstantIntegerType(2), TypeCombinator::union( new ConstantIntegerType(0), - new ConstantStringType('0') + new ConstantStringType('0'), + new ConstantBooleanType(false) ), ], [ new ConstantIntegerType(3), TypeCombinator::union( new ConstantIntegerType(1), - new ConstantStringType('1') + new ConstantStringType('1'), + new ConstantBooleanType(true) ), ], [ new ConstantIntegerType(4), TypeCombinator::union( new ConstantIntegerType(0), - new ConstantStringType('0') + new ConstantStringType('0'), + new ConstantBooleanType(false) ), ], ]), From 5d6bd049c73e766db0cb275c6eca422c301a5517 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 31 May 2024 16:00:17 +0200 Subject: [PATCH 8/8] PHPUnit: exclude platform group by default --- Makefile | 2 +- phpunit.xml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 382dfa5c..2ec6452c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ check: lint cs tests phpstan .PHONY: tests tests: - php vendor/bin/phpunit --exclude-group=platform + php vendor/bin/phpunit .PHONY: lint lint: diff --git a/phpunit.xml b/phpunit.xml index 6d69639a..f4beeb21 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -36,5 +36,11 @@ + + + platform + + +