Skip to content

pdo_mysql: Fix boolean parameter binds failing when not using emulation. #3921

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 6 commits into from

Conversation

camporter
Copy link
Contributor

@camporter camporter commented Mar 6, 2019

Attempts to fix bug 38546.

@nikic
Copy link
Member

nikic commented Mar 6, 2019

Could you please add a test for this change?

@camporter
Copy link
Contributor Author

camporter commented Mar 6, 2019

There seems to be a disagreement with the test for Bug 44707 about how to handle a difference in data types for booleans.

@camporter
Copy link
Contributor Author

camporter commented Mar 6, 2019

The last commit changes the test for that bug, because not modifying the variable that the boolean came from and enforcing boolean parameters seem to be separate concerns.

@@ -552,6 +552,8 @@ static int pdo_mysql_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_da
mysqlnd_stmt_bind_one_param(S->stmt, param->paramno, parameter, MYSQL_TYPE_VAR_STRING);
break;
case IS_LONG:
case IS_TRUE:
case IS_FALSE:
Copy link
Member

Choose a reason for hiding this comment

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

It seems like this is okay due to the code in https://github.com/php/php-src/blob/master/ext/mysqlnd/mysqlnd_ps_codec.c#L615. This will end up casting the booleans to double and then to integer, ending up with the expected value. Might be good to implement that more explicitly, as right now it seems like this working is a lucky accident rather than how this API was intended to be used.

I'd also appreciate someone who understands MySQL take a look at this. Maybe @johannes?

Copy link

Choose a reason for hiding this comment

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

@nikic Disclaimer, I don't know a lot about C but do know a bit about MySQL.

I would not expect it to be casted to a double and then to an integer. What I would expect is that it is casted to an integer directly.

As per MySQL documentation the BOOL, BOOLEAN field is just a synonym for a TINYINT(1) field so I would expect this field type to also be used in the php mysql client. If I'm right there is already a type for this:
https://github.com/php/php-src/blob/master/ext/mysqlnd/mysqlnd_ps_codec.c#L405

Documentation of MySQL Bool type: https://dev.mysql.com/doc/refman/8.0/en/numeric-type-overview.html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So perhaps it would be better to use MYSQL_TYPE_TINY for IS_TRUE and IS_FALSE instead?

Copy link
Member

Choose a reason for hiding this comment

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

The cast to double is just a simple trick to see if the value might be bigger than maxint. Imagine you submit a large value on a platform where long is 32 bit, or an extremely huge value into an unsigned 64bit field. So there is no problem with applying this patch.

The whole type juggling might certainly be optimized ... but having a good test for this case is a good step towards making a refactoring there more possible with less risk of breaking anything :-)

Copy link

@rjd22 rjd22 Mar 21, 2019

Choose a reason for hiding this comment

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

@camporter @johannes In that case I would still use the MYSQL_TYPE_TINY type but make a expicit implementation that casts the value to double and then to int and not piggyback on the LONG, LONGLONG type functionality for the reason that they can to be separately maintained IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trying to use MYSQL_TYPE_TINY for IS_TRUE and IS_FALSE results in NULL values being sent to mysql. In the test on this PR:

Fatal error: Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'some_bool_1' cannot be null in /home/cmporter/php-src/test.php:75

I'm possibly misunderstanding what the suggested change is.

Copy link

@rjd22 rjd22 Mar 23, 2019

Choose a reason for hiding this comment

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

@camporter most likely you will need to add supporting code to the mysqlnd_stmt_execute_prepare_param_types method to cast the booleans to double and then to integer when the type is MYSQL_TYPE_TINY .

In mysql_statement.c it would be something like the following:

case IS_TRUE:
case IS_FALSE:
	mysqlnd_stmt_bind_one_param(S->stmt, param->paramno, parameter, MYSQL_TYPE_TINY);
	break;

@nikic I think that is what you mean with making it explicit, or did you mean something else? Or would you rather see a MYSQL_TYPE_BOOL added?

Copy link
Member

Choose a reason for hiding this comment

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

For tiny the hack with checking double first isn't needed. A tiny integer will in all cases fit into a long (PHP's int type) The double hack is just just to see whether the value is bigger than maxint in which case it is passed along as string (which is the only generic way to transport large values without loosing precision until PHP has a built-in bigint support)

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, so adding

case IS_TRUE:
case IS_FALSE:
	mysqlnd_stmt_bind_one_param(S->stmt, param->paramno, parameter, MYSQL_TYPE_TINY);
	break;

requires mysqlnd_stmt_execute_store_param_values to handle MYSQL_TYPE_TINY specifically now, otherwise TRUE/FALSE values become NULL (default value stored). Storing 4 bytes the same as a long appears to work, but seems incorrect. Should it be storing 1 byte, and how would we go about handling that correctly? eg:

case MYSQL_TYPE_TINY:
  if (Z_TYPE_P(data) == IS_STRING) {
    goto send_string;
  }

  int4store(*p, Z_LVAL_P(data)); //int1store(?)
  (*p) += 4; // += 1
  break;

We probably want to avoid making assumptions that only TRUE/FALSE values will use the TINY data type?

Is there any harm in having TRUE/FALSE be dealt with the same as LONG values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So since it seems like we want to use MYSQL_TYPE_TINY so that TRUE/FALSE can use it from PDO, I made changes to support it in mysqlnd. Please check the new changes.

Pass MYSQL_TYPE_TINY if the bind value is TRUE/FALSE for pdo_mysql using
mysqlnd. Store a single byte rather than 4 or 8.
@nikic
Copy link
Member

nikic commented Apr 23, 2019

Looks good to me. @johannes Do you also want to take a look?

@rjd22
Copy link

rjd22 commented Apr 24, 2019

@camporter looks really good, thx for the effort.

@johannes
Copy link
Member

Looks good to me. Only small comment: If this is supported (supposed o be supported) by all drivers it might be a good idea to move the test to PDO's test suite as redirect test so it's run on all drivers.

@camporter
Copy link
Contributor Author

I can try moving and running the test for PDO in general. Not sure how much will work, it will be a few days until I can complete checking that.

@camporter
Copy link
Contributor Author

camporter commented Apr 29, 2019

There seems to be quite a bit of variance between various drivers about how and whether they handle a boolean type, which doesn't exactly match what is being fixed on this specific pull request.

I'm up for additional changes to get consistency between the drivers as far as handling boolean values, but I'm not sure what the expected behavior is supposed to be under every supported driver.

Adding confusion is that boolean values fetched from (for example) mysql are returned as integers, instead of true/false specifically because they are represented as numbers. It's not obvious to me how much PDO can or should 'abstract' in various boolean type cases.

@johannes
Copy link
Member

Result types we can't unify further - he database tells a type and that's all info we have (till we know table schemas and have an sql engine or user requests a specific type - which both are out of scope for PDO)

If writing a generic est is too hard then I wouldn't bother now but suggest to push the change.

@camporter
Copy link
Contributor Author

I was not able to come up with a generic test that would make sense for more than the mysql driver in PDO due to driver and database differences.

@nikic nikic self-assigned this Jun 6, 2019
@nikic
Copy link
Member

nikic commented Jun 7, 2019

Merged as 7d1aa75 into PHP 7.2 and up. Thanks a lot for working on this!

@nikic nikic closed this Jun 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants