Skip to content

Change BigIntType Casting #6143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed

Change BigIntType Casting #6143

wants to merge 2 commits into from

Conversation

cizordj
Copy link
Contributor

@cizordj cizordj commented Sep 3, 2023

Q A
Type improvement
Fixed issues #6126

Summary

  • Cast to int within safe range, else to string.

Copy link
Member

@derrabus derrabus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Please add tests.

@derrabus
Copy link
Member

derrabus commented Sep 3, 2023

Please also document this change in UPGRADE.md because this is a breaking change.

@cizordj
Copy link
Contributor Author

cizordj commented Sep 3, 2023

I'll fix the issues with the phpcs

Copy link
Member

@derrabus derrabus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this calls for a functional test on MySQL with a BIGINT UNSIGNED field.

]
];

$safeInt = filter_var($value, FILTER_VALIDATE_INT, $options);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use ext-filter here. Your previous code that simply compared the value to PHP_INT_MIN and PHP_INT_MAX was just fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I was creating the unit tests I was getting a strange behavior.

If the value is a string and exceeds the PHP_INT_MAX then it passes the comparison.

See this PHP shell example:

php > var_dump(PHP_INT_MAX);
int(9223372036854775807)
php > var_dump(('9223372036854775808' <= PHP_INT_MAX));
bool(true)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is the <= operator. If you have a string with an integer lager than PHP_INT_MAX, an integer cast gives you PHP_INT_MAX in PHP.

I'm afraid the ext-filter methods are to slow to put them on a hot execution path. Let's stick to < and >. I realize that this creates an edge case for PHP_INT_MAX itself because we cannot tell apart PHP_INT_MAX from an integer overflow, but I think we can live with that.


public function testShouldConvertPhpIntMinToInteger(): void
{
self::assertIsInt($this->type->convertToPHPValue(PHP_INT_MIN, $this->platform));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertIsInt() is too weak. An implementation that always returns 42 would pass this test. Why not assertSame()? We pretty much know that value to expect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok then, I'll change that.


public function testShouldConvertSafeNegativeValueToInteger(): void
{
self::assertIsInt($this->type->convertToPHPValue(PHP_INT_MIN + 1, $this->platform));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is bogus. PHP_INT_MIN + 1 is a float and a database would never return a float for a BIGINT field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No according to PHP shell.

php > var_dump(PHP_INT_MIN + 1);
int(-9223372036854775807)

The same goes for the other test

php > var_dump(PHP_INT_MAX - 1);
int(9223372036854775806)

The idea is to test scenarios around the limits of PHP.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@derrabus PHP_INT_MIN + 1 is not a float. You confused with PHP_INT_MAX + 1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof You're right.


public function testShouldConvertSafeNegativeStringValueToInteger(): void
{
self::assertIsInt($this->type->convertToPHPValue((string) (PHP_INT_MIN + 1), $this->platform));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's also not a valid scenario.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I'll remove these kinds of tests.

@cizordj
Copy link
Contributor Author

cizordj commented Sep 3, 2023

Could you tell me how to run integration tests? When I do this most tests are skipped.

php vendor/bin/phpunit --testdox tests/Functional

@greg0ire
Copy link
Member

greg0ire commented Sep 5, 2023

@cizordj by default, tests are run with SQLite. You can add the -vvv flag to see how this impacts skipped tests. You can take a look at phpunit.xml for instructions on how to use another database.

@cizordj
Copy link
Contributor Author

cizordj commented Sep 8, 2023

@derrabus Hi, can you please take a look at this test?

https://github.com/doctrine/dbal/blob/2264b78385c6961df9caecce6e9fb56f607317be/tests/Functional/BigIntUnsignedConversionTest.php#L89C5-L98C6

It's failing on my machine. Does that mean we should change our implementation?

protected function setUp(): void
{
if (
! $this->connection->getDatabasePlatform() instanceof MySQLPlatform
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the sqlite platform also support unsigned integer fields

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I didn't know that 😁


$this->connection->executeStatement(
<<<'SQL'
CREATE TABLE IF NOT EXISTS doctrine_tests.mysql_unsigned_test (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use the schema API so that it does not hardcode support for mysql only


Not all of the database vendors support unsigned integers, so such an assumption
might not be propagated to the database.
Values retrieved from the database are always converted to PHP's ``integer`` type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be worth mentioning that getting a string can only happen on PHP 32-bit architecture or for unsigned integers, as PHP 64-bit has an integer type that corresponds to the range of signed bigint

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I had no idea on how to express this in the documentation.

- Cast to int within safe range, else to string.
- Update the documentation regarding the BigIntType
- Add unit tests using assertSame
- Add integration test with both signed and unsigned integers
- Add the breaking change to the UPGRADE.md file
Comment on lines +40 to +42
if ($value === null) {
return null;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ($value === null) {
return null;
}
if ($value === null || is_int($value)) {
return $value;
}

Let's exit here already if we already hold an integer in our hand. This prevents accidental int-to-string casts. This should also help us getting the tests to pass.

@morozov morozov added this to the 4.0.0-RC1 milestone Oct 9, 2023
@morozov
Copy link
Member

morozov commented Oct 9, 2023

Closing in favor of #6177.

@morozov morozov closed this Oct 9, 2023
@morozov morozov removed this from the 4.0.0-RC1 milestone Oct 9, 2023
derrabus added a commit to derrabus/dbal that referenced this pull request Oct 9, 2023
|      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]>
@cizordj cizordj deleted the cizordj-patch-1 branch October 10, 2023 20:03
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 10, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants