Skip to content

Commit 65896da

Browse files
derrabuscizordj
andcommitted
Cast BIGINT values to int if possible
Co-authored-by: cizordj <[email protected]>
1 parent 81c9688 commit 65896da

File tree

4 files changed

+150
-14
lines changed

4 files changed

+150
-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+
or ``string`` if the data exceeds PHP's integer safe limits.
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: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
use Doctrine\DBAL\ParameterType;
88
use Doctrine\DBAL\Platforms\AbstractPlatform;
99

10+
use function is_int;
11+
12+
use const PHP_INT_MAX;
13+
use const PHP_INT_MIN;
14+
1015
/**
11-
* Type that maps a database BIGINT to a PHP string.
16+
* Type that maps a database BIGINT to a PHP int.
1217
*/
1318
class BigIntType extends Type implements PhpIntegerMappingType
1419
{
@@ -28,12 +33,20 @@ public function getBindingType(): ParameterType
2833
/**
2934
* @param T $value
3035
*
31-
* @return (T is null ? null : string)
36+
* @return (T is null ? null : int|string)
3237
*
3338
* @template T
3439
*/
35-
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?string
40+
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): int|string|null
3641
{
37-
return $value === null ? null : (string) $value;
42+
if ($value === null || is_int($value)) {
43+
return $value;
44+
}
45+
46+
if ($value > PHP_INT_MIN && $value < PHP_INT_MAX) {
47+
return (int) $value;
48+
}
49+
50+
return (string) $value;
3851
}
3952
}
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\LogicalXor;
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+
LogicalXor::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)