Skip to content

Commit

Permalink
Cast BIGINT values to int if possible
Browse files Browse the repository at this point in the history
Co-authored-by: cizordj <32869222+cizordj@users.noreply.github.com>
  • Loading branch information
derrabus and cizordj committed Oct 9, 2023
1 parent 81c9688 commit 65896da
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 14 deletions.
5 changes: 5 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 10 additions & 10 deletions docs/en/reference/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
or ``string`` if the data exceeds PHP's integer safe limits.
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
^^^^^^^^^^^^^
Expand Down
21 changes: 17 additions & 4 deletions src/Types/BigIntType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use function is_int;

use const PHP_INT_MAX;
use const PHP_INT_MIN;

/**
* Type that maps a database BIGINT to a PHP string.
* Type that maps a database BIGINT to a PHP int.
*/
class BigIntType extends Type implements PhpIntegerMappingType
{
Expand All @@ -28,12 +33,20 @@ 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;
}

return (string) $value;
}
}
118 changes: 118 additions & 0 deletions tests/Functional/Types/BigIntTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Types;

use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Tests\TestUtil;
use Doctrine\DBAL\Types\Types;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Constraint\IsIdentical;
use PHPUnit\Framework\Constraint\LogicalXor;

use const PHP_INT_MAX;
use const PHP_INT_MIN;
use const PHP_INT_SIZE;

class BigIntTypeTest extends FunctionalTestCase
{
#[DataProvider('provideBigIntLiterals')]
public function testSelectBigInt(string $sqlLiteral, int|string|null $expectedValue): 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(<<<SQL
INSERT INTO bigint_type_test (id, my_integer)
VALUES (42, $sqlLiteral)
SQL);

self::assertSame(
$expectedValue,
$this->connection->convertToPHPValue(
$this->connection->fetchOne('SELECT my_integer from bigint_type_test WHERE id = 42'),
Types::BIGINT,
),
);
}

/** @return Generator<string, array{string, int|string|null}> */
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(<<<SQL
INSERT INTO bigint_type_test (id, my_integer)
VALUES (42, $value)
SQL);

self::assertThat(
$this->connection->convertToPHPValue(
$this->connection->fetchOne('SELECT my_integer from bigint_type_test WHERE id = 42'),
Types::BIGINT,
),
LogicalXor::fromConstraints(new IsIdentical($value), new IsIdentical((string) $value)),
);
}

/** @return Generator<string, array{int}> */
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,
),
);
}
}

0 comments on commit 65896da

Please sign in to comment.