Skip to content

Commit 16c850f

Browse files
derrabuscizordj
andauthored
Cast BIGINT values to int if possible (doctrine#6177)
| Q | A |------------- | ----------- | Type | improvement | Fixed issues | Replaces doctrine#6143, closes doctrine#6126 #### Summary `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. This PR continues the work done by @cizordj in doctrine#6143. Co-authored-by: cizordj <[email protected]>
1 parent 3112306 commit 16c850f

File tree

4 files changed

+159
-14
lines changed

4 files changed

+159
-14
lines changed

UPGRADE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ awareness about deprecated code.
88

99
# Upgrade to 4.0
1010

11+
## BC BREAK: BIGINT vales are cast to int if possible
12+
13+
`BigIntType` casts values retrieved from the database to int if they're inside
14+
the integer range of PHP. Previously, those values were always cast to string.
15+
1116
## BC BREAK: Stricter `DateTime` types
1217

1318
The following types don't accept or return `DateTimeImmutable` instances anymore:

docs/en/reference/types.rst

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,22 @@ bigint
8383
++++++
8484

8585
Maps and converts 8-byte integer values.
86-
Unsigned integer values have a range of **0** to **18446744073709551615** while signed
86+
Unsigned integer values have a range of **0** to **18446744073709551615**, while signed
8787
integer values have a range of **−9223372036854775808** to **9223372036854775807**.
8888
If you know the integer data you want to store always fits into one of these ranges
8989
you should consider using this type.
90-
Values retrieved from the database are always converted to PHP's ``string`` type
91-
or ``null`` if no data is present.
90+
Values retrieved from the database are always converted to PHP's ``integer`` type
91+
if they are within PHP's integer range or ``string`` if they aren't.
92+
Otherwise, returns ``null`` if no data is present.
9293

9394
.. note::
9495

95-
For compatibility reasons this type is not converted to an integer
96-
as PHP can only represent big integer values as real integers on
97-
systems with a 64-bit architecture and would fall back to approximated
98-
float values otherwise which could lead to false assumptions in applications.
99-
100-
Not all of the database vendors support unsigned integers, so such an assumption
101-
might not be propagated to the database.
96+
Due to architectural differences, 32-bit PHP systems have a smaller
97+
integer range than their 64-bit counterparts. On 32-bit systems,
98+
values exceeding this range will be represented as strings instead
99+
of integers. Bear in mind that not all database vendors
100+
support unsigned integers, so schema configuration cannot be
101+
enforced.
102102

103103
Decimal types
104104
^^^^^^^^^^^^^

src/Types/BigIntType.php

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@
77
use Doctrine\DBAL\ParameterType;
88
use Doctrine\DBAL\Platforms\AbstractPlatform;
99

10+
use function assert;
11+
use function is_int;
12+
use function is_string;
13+
14+
use const PHP_INT_MAX;
15+
use const PHP_INT_MIN;
16+
1017
/**
11-
* Type that maps a database BIGINT to a PHP string.
18+
* Type that attempts to map a database BIGINT to a PHP int.
19+
*
20+
* If the presented value is outside of PHP's integer range, the value is returned as-is (usually a string).
1221
*/
1322
class BigIntType extends Type implements PhpIntegerMappingType
1423
{
@@ -28,12 +37,25 @@ public function getBindingType(): ParameterType
2837
/**
2938
* @param T $value
3039
*
31-
* @return (T is null ? null : string)
40+
* @return (T is null ? null : int|string)
3241
*
3342
* @template T
3443
*/
35-
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?string
44+
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): int|string|null
3645
{
37-
return $value === null ? null : (string) $value;
46+
if ($value === null || is_int($value)) {
47+
return $value;
48+
}
49+
50+
if ($value > PHP_INT_MIN && $value < PHP_INT_MAX) {
51+
return (int) $value;
52+
}
53+
54+
assert(
55+
is_string($value),
56+
'DBAL assumes values outside of the integer range to be returned as string by the database driver.',
57+
);
58+
59+
return $value;
3860
}
3961
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Functional\Types;
6+
7+
use Doctrine\DBAL\Schema\Table;
8+
use Doctrine\DBAL\Tests\FunctionalTestCase;
9+
use Doctrine\DBAL\Tests\TestUtil;
10+
use Doctrine\DBAL\Types\Types;
11+
use Generator;
12+
use PHPUnit\Framework\Attributes\DataProvider;
13+
use PHPUnit\Framework\Constraint\IsIdentical;
14+
use PHPUnit\Framework\Constraint\LogicalOr;
15+
16+
use const PHP_INT_MAX;
17+
use const PHP_INT_MIN;
18+
use const PHP_INT_SIZE;
19+
20+
class BigIntTypeTest extends FunctionalTestCase
21+
{
22+
#[DataProvider('provideBigIntLiterals')]
23+
public function testSelectBigInt(string $sqlLiteral, int|string|null $expectedValue): void
24+
{
25+
$table = new Table('bigint_type_test');
26+
$table->addColumn('id', Types::SMALLINT, ['notnull' => true]);
27+
$table->addColumn('my_integer', Types::BIGINT, ['notnull' => false]);
28+
$table->setPrimaryKey(['id']);
29+
$this->dropAndCreateTable($table);
30+
31+
$this->connection->executeStatement(<<<SQL
32+
INSERT INTO bigint_type_test (id, my_integer)
33+
VALUES (42, $sqlLiteral)
34+
SQL);
35+
36+
self::assertSame(
37+
$expectedValue,
38+
$this->connection->convertToPHPValue(
39+
$this->connection->fetchOne('SELECT my_integer from bigint_type_test WHERE id = 42'),
40+
Types::BIGINT,
41+
),
42+
);
43+
}
44+
45+
/** @return Generator<string, array{string, int|string|null}> */
46+
public static function provideBigIntLiterals(): Generator
47+
{
48+
yield 'zero' => ['0', 0];
49+
yield 'null' => ['null', null];
50+
yield 'positive number' => ['42', 42];
51+
yield 'negative number' => ['-42', -42];
52+
53+
if (PHP_INT_SIZE < 8) {
54+
// The following tests only work on 64bit systems.
55+
return;
56+
}
57+
58+
yield 'large positive number' => ['9223372036854775806', PHP_INT_MAX - 1];
59+
yield 'large negative number' => ['-9223372036854775807', PHP_INT_MIN + 1];
60+
}
61+
62+
#[DataProvider('provideBigIntEdgeLiterals')]
63+
public function testSelectBigIntEdge(int $value): void
64+
{
65+
$table = new Table('bigint_type_test');
66+
$table->addColumn('id', Types::SMALLINT, ['notnull' => true]);
67+
$table->addColumn('my_integer', Types::BIGINT, ['notnull' => false]);
68+
$table->setPrimaryKey(['id']);
69+
$this->dropAndCreateTable($table);
70+
71+
$this->connection->executeStatement(<<<SQL
72+
INSERT INTO bigint_type_test (id, my_integer)
73+
VALUES (42, $value)
74+
SQL);
75+
76+
self::assertThat(
77+
$this->connection->convertToPHPValue(
78+
$this->connection->fetchOne('SELECT my_integer from bigint_type_test WHERE id = 42'),
79+
Types::BIGINT,
80+
),
81+
LogicalOr::fromConstraints(new IsIdentical($value), new IsIdentical((string) $value)),
82+
);
83+
}
84+
85+
/** @return Generator<string, array{int}> */
86+
public static function provideBigIntEdgeLiterals(): Generator
87+
{
88+
yield 'max int' => [PHP_INT_MAX];
89+
yield 'min int' => [PHP_INT_MIN];
90+
}
91+
92+
public function testUnsignedBigIntOnMySQL(): void
93+
{
94+
if (! TestUtil::isDriverOneOf('mysqli', 'pdo_mysql')) {
95+
self::markTestSkipped('This test only works on MySQL/MariaDB.');
96+
}
97+
98+
$table = new Table('bigint_type_test');
99+
$table->addColumn('id', Types::SMALLINT, ['notnull' => true]);
100+
$table->addColumn('my_integer', Types::BIGINT, ['notnull' => false, 'unsigned' => true]);
101+
$table->setPrimaryKey(['id']);
102+
$this->dropAndCreateTable($table);
103+
104+
// Insert (2 ** 64) - 1
105+
$this->connection->executeStatement(<<<'SQL'
106+
INSERT INTO bigint_type_test (id, my_integer)
107+
VALUES (42, 0xFFFFFFFFFFFFFFFF)
108+
SQL);
109+
110+
self::assertSame(
111+
'18446744073709551615',
112+
$this->connection->convertToPHPValue(
113+
$this->connection->fetchOne('SELECT my_integer from bigint_type_test WHERE id = 42'),
114+
Types::BIGINT,
115+
),
116+
);
117+
}
118+
}

0 commit comments

Comments
 (0)