-
Notifications
You must be signed in to change notification settings - Fork 8k
Fix for bug #76386: Prepared Statement formatter truncates fractional seconds from date/time column #3257
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
Conversation
..that is also a duplicate of #67122
|
It should be targeted to |
|
I'd be glad to "cover it with tests", but facts are the following:
My questions are:
If anyone objects, and there is not a C level testing framework, then in order to create a separate "tests" folder here with the mysqlnd driver component, I'd need to duplicate quite a few files - like "connect.inc" or "skipif.inc" (that someone will possibly have to maintain in the future) to no apparent benefit, so that is possibly unwanted. Please someone help me deciding with this. |
|
It's fine to create tests in either mysqli or PDO mysql (or both). That's how mysqlnd is usually tested. |
ext/mysqlnd/mysqlnd_ps_codec.c
Outdated
| length = mnd_sprintf(&value, 0, "%s%02u:%02u:%02u", (t.neg ? "-" : ""), t.hour, t.minute, t.second); | ||
| if (field->decimals > 0) { | ||
| char * format; | ||
| mnd_sprintf(&format, 0, "%%s%%02u:%%02u:%%02u.%%%02uu", field->decimals); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of creating a dynamic format string, you should be able to use something like %0*u to specify the width dynamically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks.
Guess I could use some more libc knowledge. :)
Will do.
|
I added a test case - that covers all necessary changes - and also switched to the asterisk syntax. |
ext/mysqli/tests/bug76386.phpt
Outdated
| 'CREATE TABLE IF NOT EXISTS ts_test (id bigint unsigned auto_increment ' . | ||
| 'primary key, ts timestamp default 0, ts2 timestamp(2) default 0, ' . | ||
| 'ts4 timestamp(4) default 0, ts6 timestamp(6) default 0) character ' . | ||
| 'set utf8 collate utf8_general_ci;' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MySQL 5.7 might disallow zero for the default. IMO it were more robust not using a default value but inserting some explicit timestamp. As an option also using CURRENT_TIMESTAMP.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for that, I did not know that they have such plans.
The MySQL 5.7 that I use, as well as MariaDB 10.3, do not have such issues.
Updated the test according to your recommendations anyway.
ext/mysqlnd/mysqlnd_ps_codec.c
Outdated
|
|
||
| length = mnd_sprintf(&value, 0, "%s%02u:%02u:%02u", (t.neg ? "-" : ""), t.hour, t.minute, t.second); | ||
| if (field->decimals > 0) { | ||
| length = mnd_sprintf(&value, 0, "%s%02u:%02u:%02u.%0*u", (t.neg ? "-" : ""), t.hour, t.minute, t.second, field->decimals, t.second_part); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second_part member is zend_ulong. It would be cleaner to use ZEND_ULONG_FMT_SPEC instead of u. Please see zend_long.h.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course, skipping that could cause trouble with e.g. future 16bit platforms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After checking your recommendations in practice, I am fairly sure that using ZEND_ULONG_FMT_SPEC would not work.
What is needed here is to format the same ulong as an unsigned zero-padded decimal having any number of digits between 1-6.
While ZEND_ULONG_FMT_SPEC resolves - at least here - to PRIu64, we do not want an unsigned long here but a predetermined amount of digits, zero padded.
..um, excuse me, the new test (or, rather, the new defaults) fail in the same way when I used ZEND_ULONG_FMT_SPEC.
Let me check what else we have here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
zend_ulong is dynamic, it's 64- or 32-bit depending on the platform used. Using the macro is trivial. 16-bit platforms are not supported at all, so nothing to care. Principally, one could also leave u as the fractional part won't be that big anyway. It's just a formal thing - the format to match the datatype. It doesn't affect the field width, as the decimal places number is still being passed. I'm going to check once more tomorrow.
Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, after recompiling PHP with --enable-debug, and using mysql_debug, and adding some extra DBG_INF_FMT macros, it became apparent that while the decimals member is always right (2, 4, 6, the precision that is configured), the second_part member has its value always set to microseconds internally in mysql.
Sigh.
As such, I think we are not heading towards the long format, since we want to get "77" from "770000" with a decimals value of 2, and even "9" from a long value of "900000" with a decimals value of 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also means the "second_part" member name is a misnomer, since it is always microseconds, no matter the actual precision configured.
ext/mysqli/tests/bug76386.phpt
Outdated
| $stmt->fetch(); | ||
| var_dump($ts, $ts2, $ts4, $ts6); | ||
| } finally { | ||
| $link->query('DROP TABLE IF EXISTS ts_test;'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would make sense to drop that table right before creating it. Otherwise, the --CLEAN-- section should be used for the cleanup.
|
This is what I came up with. |
| } | ||
|
|
||
| length = mnd_sprintf(&value, 0, "%s%02u:%02u:%02u", (t.neg ? "-" : ""), t.hour, t.minute, t.second); | ||
| if (field->decimals > 0 && field->decimals < 7) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why ignoring fraction with higher precision?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently it is declared that the precision is microseconds at most; according to the manual, higher precision is not possible.
This is only to safeguard against possible future hard-to-find issues should any behavioral changes go unnoticed.
ext/mysqlnd/mysqlnd_ps_codec.c
Outdated
| t.minute, | ||
| t.second, | ||
| field->decimals, | ||
| (int) (t.second_part / pow(10, 6 - field->decimals)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem right, what was wrong with just left padding it up to the number of decimals?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the passed field width is smaller than the number of digits, all the digits would be included.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was concerned about a field the same size as the number of digits, but smaller than 6. Consider TIME(3) with value of 123 - wouldn't that become 123 / pow(10, 3) = 123 / 1000 ~= 0? Or am I missing sth and second_part always comes with a fixed number of digits?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems it always delivers 6 digits, the irrelevant places are filled with zeroes. Like with TIMESTAMP(3), a number would be like 123000. At least those DB servers I was able to test do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is exactly the case.
|
@strongholdmedia thanks for checking the comments. As for me, the trailing zeroes should be cut off. Another option to do that would be to put Then append The MySQL manual says
So it might be ok to limit the fractional digits hardcoded to 6. Not sure we have this information already somewhere in the driver. It might be not future proof, however it's unlikely that limit to change in the near future, if at all. MySQL 8.0 documents it that way, too. Perhaps more thoughts are needed about this point. The other place with I would rather suggest to be reluctant to target stable branches, that's it. It is of course a bug fix, but the change of the delivered date format might be sudden for some apps, fe when the timestamp columns was declared with the precision already. Thus, IMHO it would be better to get it stable and see if it's in time for 7.3, otherwise to go by 7.4. Thanks. |
ext/mysqlnd/mysqlnd_ps_codec.c
Outdated
| t.minute, | ||
| t.second, | ||
| field->decimals, | ||
| (int) (t.second_part / pow(10, 6 - field->decimals)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For int the placeholder would be %d ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. :D Sure enough. I should cast to UINT32_T, given that it can only take a value between 0-999999.
But that is theoretical only, as now I have the problem that my last commit failed the test, whilst it ran without failure or being skipped locally.
Apart from versions, etc, the only obvious difference is that I use "127.0.0.1" as MYSQL_TEST_HOST without using mysqld.sock (that seems to be the default for "localhost" for years now).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As for the target branch.. most people (like me) are forced to use emulated_prepares with PDO (that is on by default anyway, but many of us would have reasons to turn it off).
Now having a delta of 1 second was totally acceptable like, um, 10 years ago, but in a HA system, many things could and should happen in one second.
There are some bug reports for stuff like PHP frameworks and so on, with people having no idea what and why is this happening.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The support for actually working microseconds is in since MySQL 5.6.4 (milestone 7, released on 2011-12-20; stable as 5.6.10 since 2013-02-05; neither seems yesterday).
ext/mysqlnd/mysqlnd_ps_codec.c
Outdated
| length = mnd_sprintf( | ||
| &value, | ||
| 0, | ||
| "%s%02u:%02u:%02u.%0*" ZEND_ULONG_FMT_SPEC, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field width is not needed anymore, as the argument was truncated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I missing something? I thought it still needed to be zero-padded when smaller than 10^(decimals-1).
The leading zero though seems superfluous indeed.
|
If the test fails again on the Travis instance, may I be able to obtain a dockerfile or such to reproduce the exact build environment to debug the issue further please. |
|
Now "my" build failed with This CI stuff doesn't seem very robust, now does it. |
ext/mysqlnd/mysqlnd_ps_codec.c
Outdated
| length = mnd_sprintf( | ||
| &value, | ||
| 0, | ||
| "%s%02u:%02u:%02u.%*u", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The asterisk is also unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry but you seem to have missed my previous reply.
When you have 9 as a long, with a decimals value of 3, that is represented as a decimal 9000 (microseconds), but you still want to return 009, not 9.
That being said, I am unsure as of what to add in the place of asterisks if I remove them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. Forgot about the opposite case. Then it looks like it's missing a zero, should be %0*u so a small number is padded on the left. Please lets test this case by inserting the appropriate data, too.
|
Yeah, those two are sporadic and thus unrelated. To the actual failure is shown iin the job log https://travis-ci.org/php/php-src/jobs/388499682#L2072 . It could be same about the strictness mode, like it was before about the default zero timestamp. It would help to add some error checks, so they're shown in the log output as well. Most likely it's a test issue. Either the table creation or the insert fails. Separating into two different files would also help to isolate, as the previous test didn't fail. Thanks. |
ext/mysqli/tests/bug76386.phpt
Outdated
| $link->query('DROP TABLE IF EXISTS t_test;'); | ||
| $link->query( | ||
| 'CREATE TABLE t_test (id bigint unsigned auto_increment primary key, ' . | ||
| 't time default current_time(), t2 time(2) default current_time(2), t4 ' . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess default current_time is what it doesn't like. It only initalizes timestamp/datetime automatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, surely enough. I am a total idiot and thought that was IMPOSSIBLE.
It is just that I am using MariaDB, that DOES support this, so I assumed MySQL does as well.
It is the second time that I run into exactly this kind of fail recently.
Anyway that's why I asked about the "official" dockerfile earlier.
|
So to add, MariaDB 10.3 does support "CURRENT_TIME(n)" as initializer, while MySQL, at least the version used in the CI environment, doesn't. |
|
The current solution seems fine, as for me. Lets see, if there are further comments. It should suit for 7.3 alpha, if everything goes ok. Thanks. |
|
/cc @cmb69 |
|
I can't comment on technical details here, but considering that this apparently would fix a bug, it's fine for me to put it into 7.3. |
|
@strongholdmedia looks like it's fine to merge. Could you add an UPGRADING note please? Thanks. |
|
@weltling sure enough, I did. |
|
Merged as 71c0432 and also added NEWS. Thanks for all your work on this! |
|
Thank you for the great job guys! |
..that is also a duplicate of #67122.
URLs for bugs that have been fixed with this PR:
https://bugs.php.net/bug.php?id=76386
https://bugs.php.net/bug.php?id=76386
There is also a pull request #3245 that is intended to address this issue, but that is not acceptable since it ignores precision and only supports DATETIME(6) or TIMESTAMP(6).This fix supports arbitrary precision from the driver.
I ran all previous tests, it does not break mysqli neither.
I tried to adapt to naming && coding conventions, unsure if this was successful.