diff --git a/UPGRADE.md b/UPGRADE.md index 213738383b1..c2d0ea98cc5 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -8,6 +8,11 @@ awareness about deprecated code. # Upgrade to 4.0 +## BC BREAK: BIGINT vales are cast to int if possible + +`BigIntType` casts values retrieved from the database to int if they're inside +the integer range of PHP. Previously, those values were always cast to string. + ## BC BREAK: Stricter `DateTime` types The following types don't accept or return `DateTimeImmutable` instances anymore: diff --git a/docs/en/reference/types.rst b/docs/en/reference/types.rst index 4fe6dc3c833..8a14613b357 100644 --- a/docs/en/reference/types.rst +++ b/docs/en/reference/types.rst @@ -83,22 +83,22 @@ bigint ++++++ Maps and converts 8-byte integer values. -Unsigned integer values have a range of **0** to **18446744073709551615** while signed +Unsigned integer values have a range of **0** to **18446744073709551615**, while signed integer values have a range of **−9223372036854775808** to **9223372036854775807**. If you know the integer data you want to store always fits into one of these ranges you should consider using this type. -Values retrieved from the database are always converted to PHP's ``string`` type -or ``null`` if no data is present. +Values retrieved from the database are always converted to PHP's ``integer`` type +if they are within PHP's integer range or ``string`` if they aren't. +Otherwise, returns ``null`` if no data is present. .. note:: - For compatibility reasons this type is not converted to an integer - as PHP can only represent big integer values as real integers on - systems with a 64-bit architecture and would fall back to approximated - float values otherwise which could lead to false assumptions in applications. - - Not all of the database vendors support unsigned integers, so such an assumption - might not be propagated to the database. + Due to architectural differences, 32-bit PHP systems have a smaller + integer range than their 64-bit counterparts. On 32-bit systems, + values exceeding this range will be represented as strings instead + of integers. Bear in mind that not all database vendors + support unsigned integers, so schema configuration cannot be + enforced. Decimal types ^^^^^^^^^^^^^ diff --git a/src/Types/BigIntType.php b/src/Types/BigIntType.php index 1d068a42c2d..0cb14c5b44c 100644 --- a/src/Types/BigIntType.php +++ b/src/Types/BigIntType.php @@ -7,8 +7,17 @@ use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; +use function assert; +use function is_int; +use function is_string; + +use const PHP_INT_MAX; +use const PHP_INT_MIN; + /** - * Type that maps a database BIGINT to a PHP string. + * Type that attempts to map a database BIGINT to a PHP int. + * + * If the presented value is outside of PHP's integer range, the value is returned as-is (usually a string). */ class BigIntType extends Type implements PhpIntegerMappingType { @@ -28,12 +37,25 @@ public function getBindingType(): ParameterType /** * @param T $value * - * @return (T is null ? null : string) + * @return (T is null ? null : int|string) * * @template T */ - public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?string + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): int|string|null { - return $value === null ? null : (string) $value; + if ($value === null || is_int($value)) { + return $value; + } + + if ($value > PHP_INT_MIN && $value < PHP_INT_MAX) { + return (int) $value; + } + + assert( + is_string($value), + 'DBAL assumes values outside of the integer range to be returned as string by the database driver.', + ); + + return $value; } } diff --git a/tests/Functional/Types/BigIntTypeTest.php b/tests/Functional/Types/BigIntTypeTest.php new file mode 100644 index 00000000000..65dbdcfcdb9 --- /dev/null +++ b/tests/Functional/Types/BigIntTypeTest.php @@ -0,0 +1,118 @@ +addColumn('id', Types::SMALLINT, ['notnull' => true]); + $table->addColumn('my_integer', Types::BIGINT, ['notnull' => false]); + $table->setPrimaryKey(['id']); + $this->dropAndCreateTable($table); + + $this->connection->executeStatement(<<connection->convertToPHPValue( + $this->connection->fetchOne('SELECT my_integer from bigint_type_test WHERE id = 42'), + Types::BIGINT, + ), + ); + } + + /** @return Generator */ + public static function provideBigIntLiterals(): Generator + { + yield 'zero' => ['0', 0]; + yield 'null' => ['null', null]; + yield 'positive number' => ['42', 42]; + yield 'negative number' => ['-42', -42]; + + if (PHP_INT_SIZE < 8) { + // The following tests only work on 64bit systems. + return; + } + + yield 'large positive number' => ['9223372036854775806', PHP_INT_MAX - 1]; + yield 'large negative number' => ['-9223372036854775807', PHP_INT_MIN + 1]; + } + + #[DataProvider('provideBigIntEdgeLiterals')] + public function testSelectBigIntEdge(int $value): void + { + $table = new Table('bigint_type_test'); + $table->addColumn('id', Types::SMALLINT, ['notnull' => true]); + $table->addColumn('my_integer', Types::BIGINT, ['notnull' => false]); + $table->setPrimaryKey(['id']); + $this->dropAndCreateTable($table); + + $this->connection->executeStatement(<<connection->convertToPHPValue( + $this->connection->fetchOne('SELECT my_integer from bigint_type_test WHERE id = 42'), + Types::BIGINT, + ), + LogicalOr::fromConstraints(new IsIdentical($value), new IsIdentical((string) $value)), + ); + } + + /** @return Generator */ + public static function provideBigIntEdgeLiterals(): Generator + { + yield 'max int' => [PHP_INT_MAX]; + yield 'min int' => [PHP_INT_MIN]; + } + + public function testUnsignedBigIntOnMySQL(): void + { + if (! TestUtil::isDriverOneOf('mysqli', 'pdo_mysql')) { + self::markTestSkipped('This test only works on MySQL/MariaDB.'); + } + + $table = new Table('bigint_type_test'); + $table->addColumn('id', Types::SMALLINT, ['notnull' => true]); + $table->addColumn('my_integer', Types::BIGINT, ['notnull' => false, 'unsigned' => true]); + $table->setPrimaryKey(['id']); + $this->dropAndCreateTable($table); + + // Insert (2 ** 64) - 1 + $this->connection->executeStatement(<<<'SQL' + INSERT INTO bigint_type_test (id, my_integer) + VALUES (42, 0xFFFFFFFFFFFFFFFF) + SQL); + + self::assertSame( + '18446744073709551615', + $this->connection->convertToPHPValue( + $this->connection->fetchOne('SELECT my_integer from bigint_type_test WHERE id = 42'), + Types::BIGINT, + ), + ); + } +}