Skip to content

Commit c044164

Browse files
committed
Patch for bug #67839 (mysqli does not handle 4-byte floats correctly)
Before the patch, a value of 9.99 in a FLOAT column came out of mysqli as 9.9998998641968. This is because it would naively cast a 4-byte float into PHP's internal 8-byte double. To fix this, with GCC we use the built-in decimal support to "up-convert" the 4-byte float to a 8-byte double. When that is not available, we fall back to converting the float to a string and then converting the string to a double. This mimics what MySQL does.
1 parent aeb6335 commit c044164

File tree

3 files changed

+129
-4
lines changed

3 files changed

+129
-4
lines changed

ext/mysqli/tests/bug67839.phpt

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
--TEST--
2+
mysqli_float_handling - ensure 4 byte float is handled correctly
3+
--SKIPIF--
4+
<?php
5+
require_once('skipif.inc');
6+
require_once('skipifemb.inc');
7+
require_once('skipifconnectfailure.inc');
8+
?>
9+
--FILE--
10+
<?php
11+
require('connect.inc');
12+
if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
13+
printf("[001] [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
14+
die();
15+
}
16+
17+
18+
if (!mysqli_query($link, "DROP TABLE IF EXISTS test")) {
19+
printf("[002] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
20+
die();
21+
}
22+
23+
if (!mysqli_query($link, "CREATE TABLE test(id INT PRIMARY KEY, fp4 FLOAT, fp8 DOUBLE) ENGINE = InnoDB")) {
24+
printf("[003] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
25+
die();
26+
}
27+
28+
// Insert via string to make sure the real floating number gets to the DB
29+
if (!mysqli_query($link, "INSERT INTO test(id, fp4, fp8) VALUES (1, 9.9999, 9.9999)")) {
30+
printf("[004] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
31+
die();
32+
}
33+
34+
if (!($stmt = mysqli_prepare($link, "SELECT id, fp4, fp8 FROM test"))) {
35+
printf("[005] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
36+
die();
37+
}
38+
39+
if (!mysqli_stmt_execute($stmt)) {
40+
printf("[006] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
41+
die();
42+
}
43+
44+
45+
if (!($result = mysqli_stmt_get_result($stmt))) {
46+
printf("[007] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
47+
die();
48+
}
49+
50+
$data = mysqli_fetch_assoc($result);
51+
print $data['id'] . ": " . $data['fp4'] . ": " . $data['fp8'] . "\n";
52+
?>
53+
--CLEAN--
54+
<?php
55+
require_once("clean_table.inc");
56+
?>
57+
--EXPECTF--
58+
1: 9.9999: 9.9999

ext/mysqlnd/config9.m4

+26
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,29 @@ if test "$PHP_MYSQLND" != "no" || test "$PHP_MYSQLND_ENABLED" = "yes" || test "$
5151
#endif
5252
])
5353
fi
54+
55+
dnl
56+
dnl Check if the compiler supports Decimal32/64/128 types from the IEEE-754 2008 version
57+
dnl References: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1657.pdf
58+
dnl http://speleotrove.com/decimal/
59+
dnl
60+
AC_CACHE_CHECK([whether whether compiler supports Decimal32/64/128 types], ac_cv_decimal_fp_supported,[
61+
AC_TRY_RUN( [
62+
#include <stdio.h>
63+
64+
int main(int argc, char **argv) {
65+
typedef float dec32 __attribute__((mode(SD)));
66+
dec32 k = 99.49f;
67+
double d2 = (double)k;
68+
return 0;
69+
}
70+
],[
71+
ac_cv_decimal_fp_supported=yes
72+
],[
73+
ac_cv_decimal_fp_supported=no
74+
],[
75+
ac_cv_decimal_fp_supported=no
76+
])])
77+
if test "$ac_cv_decimal_fp_supported" = "yes"; then
78+
AC_DEFINE(HAVE_DECIMAL_FP_SUPPORT, 1, [Define if the compiler supports Decimal32/64/128 types.])
79+
fi

ext/mysqlnd/mysqlnd_ps_codec.c

+45-4
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,53 @@ void ps_fetch_float(zval *zv, const MYSQLND_FIELD * const field,
195195
unsigned int pack_len, zend_uchar **row,
196196
zend_bool as_unicode TSRMLS_DC)
197197
{
198-
float value;
198+
float fval;
199+
double dval;
199200
DBG_ENTER("ps_fetch_float");
200-
float4get(value, *row);
201-
ZVAL_DOUBLE(zv, value);
201+
float4get(fval, *row);
202202
(*row)+= 4;
203-
DBG_INF_FMT("value=%f", value);
203+
DBG_INF_FMT("value=%f", fval);
204+
205+
/*
206+
* The following is needed to correctly support 4-byte floats.
207+
* Otherwise, a value of 9.99 in a FLOAT column comes out of mysqli
208+
* as 9.9998998641968.
209+
*
210+
* For GCC, we use the built-in decimal support to "up-convert" a
211+
* 4-byte float to a 8-byte double.
212+
* When that is not available, we fall back to converting the float
213+
* to a string and then converting the string to a double. This mimics
214+
* what MySQL does.
215+
*/
216+
#ifdef HAVE_DECIMAL_FP_SUPPORT
217+
{
218+
typedef float dec32 __attribute__((mode(SD)));
219+
dec32 d32val = fval;
220+
221+
/* The following cast is guaranteed to do the right thing */
222+
dval = (double) d32val;
223+
}
224+
#else
225+
{
226+
char num_buf[2048]; /* Over allocated */
227+
char *s;
228+
229+
/* Convert to string. Ignoring localization, etc.
230+
* Following MySQL's rules. If precision is undefined (NOT_FIXED_DEC i.e. 31)
231+
* or larger than 31, the value is limited to 6 (FLT_DIG).
232+
*/
233+
s = php_gcvt(fval,
234+
field->decimals >= 31 ? 6 : field->decimals,
235+
'.',
236+
'e',
237+
num_buf);
238+
239+
/* And now convert back to double */
240+
dval = zend_strtod(s, NULL);
241+
}
242+
#endif
243+
244+
ZVAL_DOUBLE(zv, dval);
204245
DBG_VOID_RETURN;
205246
}
206247
/* }}} */

0 commit comments

Comments
 (0)