Skip to content

Commit c18b1ae

Browse files
committed
PDO MySQL: Use native types for results
Previously, PDO MySQL only fetched data as native int/float if native prepared statements were used. This patch updates PDO to have the same behavior for emulated prepared statements, and thus removes the largest remaining discrepancy between these two modes. Note that PDO already has a ATTR_STRINGIFY_FETCHES option to control whether native types are desired or not. The previous output can be restored by enabling this option. Most of the tests make use of that option, because this allows the tests to work under libmysqlclient as well, which currently always returns string results (independently of whether native or emulated PS are used).
1 parent 33e9049 commit c18b1ae

28 files changed

+161
-61
lines changed

UPGRADING

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ PHP 8.1 UPGRADE NOTES
3131
. The mysqlnd.fetch_copy_data ini setting has been removed. However, this
3232
should not result in user-visible behavior changes.
3333

34+
- PDO MySQL:
35+
. Integers and floats in result sets will now be returned using native PHP
36+
types instead of strings when using emulated prepared statements. This
37+
matches the behavior of native prepared statements. You can restore the
38+
previous behavior by enabling the PDO::ATTR_STRINGIFY_FETCHES option.
39+
3440
- Standard:
3541
. version_compare() no longer accepts undocumented operator abbreviations.
3642

ext/mysqlnd/mysqlnd.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ PHPAPI void mysqlnd_debug(const char *mode);
100100
/* Query */
101101
#define mysqlnd_fetch_into(result, flags, ret_val) (result)->m.fetch_into((result), (flags), (ret_val) ZEND_FILE_LINE_CC)
102102
#define mysqlnd_fetch_row_c(result) (result)->m.fetch_row_c((result))
103+
#define mysqlnd_fetch_row_zval(result, row_ptr, fetched) \
104+
(result)->m.fetch_row((result), (row_ptr), 0, (fetched))
103105
#define mysqlnd_fetch_all(result, flags, return_value) (result)->m.fetch_all((result), (flags), (return_value) ZEND_FILE_LINE_CC)
104106
#define mysqlnd_get_connection_stats(conn, values) ((conn)->data)->m->get_statistics((conn)->data, (values) ZEND_FILE_LINE_CC)
105107
#define mysqlnd_get_client_stats(values) _mysqlnd_get_client_stats(mysqlnd_global_stats, (values) ZEND_FILE_LINE_CC)

ext/pdo_mysql/mysql_driver.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,14 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options)
826826
goto cleanup;
827827
}
828828

829+
#ifdef PDO_USE_MYSQLND
830+
bool int_and_float_native = true;
831+
if (mysql_options(H->server, MYSQLND_OPT_INT_AND_FLOAT_NATIVE, (const char *) &int_and_float_native)) {
832+
pdo_mysql_error(dbh);
833+
goto cleanup;
834+
}
835+
#endif
836+
829837
if (vars[0].optval && mysql_options(H->server, MYSQL_SET_CHARSET_NAME, vars[0].optval)) {
830838
pdo_mysql_error(dbh);
831839
goto cleanup;

ext/pdo_mysql/mysql_statement.c

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
# define pdo_mysql_stmt_execute_prepared(stmt) pdo_mysql_stmt_execute_prepared_libmysql(stmt)
3535
#endif
3636

37-
3837
static void pdo_mysql_free_result(pdo_mysql_stmt *S)
3938
{
4039
if (S->result) {
@@ -52,8 +51,16 @@ static void pdo_mysql_free_result(pdo_mysql_stmt *S)
5251
efree(S->out_length);
5352
S->bound_result = NULL;
5453
}
54+
#else
55+
if (S->current_row) {
56+
unsigned column_count = mysql_num_fields(S->result);
57+
for (unsigned i = 0; i < column_count; i++) {
58+
zval_ptr_dtor_nogc(&S->current_row[i]);
59+
}
60+
efree(S->current_row);
61+
S->current_row = NULL;
62+
}
5563
#endif
56-
5764
mysql_free_result(S->result);
5865
S->result = NULL;
5966
}
@@ -104,12 +111,6 @@ static int pdo_mysql_stmt_dtor(pdo_stmt_t *stmt) /* {{{ */
104111
}
105112
}
106113

107-
#ifdef PDO_USE_MYSQLND
108-
if (!S->stmt && S->current_data) {
109-
mnd_efree(S->current_data);
110-
}
111-
#endif /* PDO_USE_MYSQLND */
112-
113114
efree(S);
114115
PDO_DBG_RETURN(1);
115116
}
@@ -553,9 +554,24 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori
553554
PDO_DBG_RETURN(1);
554555
}
555556

556-
if (!S->stmt && S->current_data) {
557-
mnd_efree(S->current_data);
557+
zval *row_data;
558+
if (mysqlnd_fetch_row_zval(S->result, &row_data, &fetched_anything) == FAIL) {
559+
pdo_mysql_error_stmt(stmt);
560+
PDO_DBG_RETURN(0);
561+
}
562+
563+
if (!fetched_anything) {
564+
PDO_DBG_RETURN(0);
565+
}
566+
567+
if (!S->current_row) {
568+
S->current_row = ecalloc(sizeof(zval), stmt->column_count);
558569
}
570+
for (unsigned i = 0; i < stmt->column_count; i++) {
571+
zval_ptr_dtor_nogc(&S->current_row[i]);
572+
ZVAL_COPY_VALUE(&S->current_row[i], &row_data[i]);
573+
}
574+
PDO_DBG_RETURN(1);
559575
#else
560576
int ret;
561577

@@ -577,7 +593,6 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori
577593

578594
PDO_DBG_RETURN(1);
579595
}
580-
#endif /* PDO_USE_MYSQLND */
581596

582597
if ((S->current_data = mysql_fetch_row(S->result)) == NULL) {
583598
if (!S->H->buffered && mysql_errno(S->H->server)) {
@@ -588,6 +603,7 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori
588603

589604
S->current_lengths = mysql_fetch_lengths(S->result);
590605
PDO_DBG_RETURN(1);
606+
#endif /* PDO_USE_MYSQLND */
591607
}
592608
/* }}} */
593609

@@ -630,13 +646,10 @@ static int pdo_mysql_stmt_describe(pdo_stmt_t *stmt, int colno) /* {{{ */
630646
cols[i].maxlen = S->fields[i].length;
631647

632648
#ifdef PDO_USE_MYSQLND
633-
if (S->stmt) {
634-
cols[i].param_type = PDO_PARAM_ZVAL;
635-
} else
649+
cols[i].param_type = PDO_PARAM_ZVAL;
650+
#else
651+
cols[i].param_type = PDO_PARAM_STR;
636652
#endif
637-
{
638-
cols[i].param_type = PDO_PARAM_STR;
639-
}
640653
}
641654
PDO_DBG_RETURN(1);
642655
}
@@ -652,13 +665,6 @@ static int pdo_mysql_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, size_
652665
PDO_DBG_RETURN(0);
653666
}
654667

655-
/* With mysqlnd data is stored inside mysqlnd, not S->current_data */
656-
if (!S->stmt) {
657-
if (S->current_data == NULL || !S->result) {
658-
PDO_DBG_RETURN(0);
659-
}
660-
}
661-
662668
if (colno >= stmt->column_count) {
663669
/* error invalid column */
664670
PDO_DBG_RETURN(0);
@@ -667,9 +673,12 @@ static int pdo_mysql_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, size_
667673
if (S->stmt) {
668674
Z_TRY_ADDREF(S->stmt->data->result_bind[colno].zv);
669675
*ptr = (char*)&S->stmt->data->result_bind[colno].zv;
670-
*len = sizeof(zval);
671-
PDO_DBG_RETURN(1);
676+
} else {
677+
Z_TRY_ADDREF(S->current_row[colno]);
678+
*ptr = (char*)&S->current_row[colno];
672679
}
680+
*len = sizeof(zval);
681+
PDO_DBG_RETURN(1);
673682
#else
674683
if (S->stmt) {
675684
if (S->out_null[colno]) {
@@ -688,10 +697,14 @@ static int pdo_mysql_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, size_
688697
*len = S->out_length[colno];
689698
PDO_DBG_RETURN(1);
690699
}
691-
#endif
700+
701+
if (S->current_data == NULL) {
702+
PDO_DBG_RETURN(0);
703+
}
692704
*ptr = S->current_data[colno];
693705
*len = S->current_lengths[colno];
694706
PDO_DBG_RETURN(1);
707+
#endif
695708
} /* }}} */
696709

697710
static char *type_to_name_native(int type) /* {{{ */

ext/pdo_mysql/php_pdo_mysql_int.h

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,6 @@ typedef struct {
120120
pdo_mysql_db_handle *H;
121121
MYSQL_RES *result;
122122
const MYSQL_FIELD *fields;
123-
MYSQL_ROW current_data;
124-
#ifdef PDO_USE_MYSQLND
125-
const size_t *current_lengths;
126-
#else
127-
unsigned long *current_lengths;
128-
#endif
129123
pdo_mysql_error_info einfo;
130124
#ifdef PDO_USE_MYSQLND
131125
MYSQLND_STMT *stmt;
@@ -137,10 +131,14 @@ typedef struct {
137131
#ifndef PDO_USE_MYSQLND
138132
my_bool *in_null;
139133
zend_ulong *in_length;
140-
#endif
141134
PDO_MYSQL_PARAM_BIND *bound_result;
142135
my_bool *out_null;
143136
zend_ulong *out_length;
137+
MYSQL_ROW current_data;
138+
unsigned long *current_lengths;
139+
#else
140+
zval *current_row;
141+
#endif
144142
unsigned max_length:1;
145143
/* Whether all result sets have been fully consumed.
146144
* If this flag is not set, they need to be consumed during destruction. */

ext/pdo_mysql/tests/bug44327.phpt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ $db = MySQLPDOTest::factory();
1111
<?php
1212
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
1313
$db = MySQLPDOTest::factory();
14+
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);
1415

1516
$stmt = $db->prepare("SELECT 1 AS \"one\"");
1617
$stmt->execute();
@@ -51,7 +52,7 @@ string(1) "1"
5152
string(1) "1"
5253
string(17) "SELECT 1 AS "one""
5354
----------------------------------
54-
object(PDORow)#%d (2) {
55+
object(PDORow)#5 (2) {
5556
["queryString"]=>
5657
string(19) "SELECT id FROM test"
5758
["id"]=>

ext/pdo_mysql/tests/bug71145.phpt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
1414
$attr = array(
1515
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
1616
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; SET SESSION sql_mode=traditional',
17+
PDO::ATTR_STRINGIFY_FETCHES => true,
1718
);
1819
putenv('PDOTEST_ATTR=' . serialize($attr));
1920

ext/pdo_mysql/tests/bug75177.phpt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ PDO MySQL Bug #75177 Type 'bit' is fetched as unexpected string
55
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc');
66
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
77
MySQLPDOTest::skip();
8+
if (!MySQLPDOTest::isPDOMySQLnd()) die('skip only for mysqlnd');
89
?>
910
--FILE--
1011
<?php
@@ -18,14 +19,23 @@ $pdo->query("INSERT INTO $tbl (`bit`) VALUES (1)");
1819
$pdo->query("INSERT INTO $tbl (`bit`) VALUES (0b011)");
1920
$pdo->query("INSERT INTO $tbl (`bit`) VALUES (0b01100)");
2021

22+
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
2123
$ret = $pdo->query("SELECT * FROM $tbl")->fetchAll();
24+
foreach ($ret as $i) {
25+
var_dump($i["bit"]);
26+
}
2227

28+
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
29+
$ret = $pdo->query("SELECT * FROM $tbl")->fetchAll();
2330
foreach ($ret as $i) {
2431
var_dump($i["bit"]);
2532
}
2633

2734
?>
2835
--EXPECT--
29-
string(1) "1"
30-
string(1) "3"
31-
string(2) "12"
36+
int(1)
37+
int(3)
38+
int(12)
39+
int(1)
40+
int(3)
41+
int(12)

ext/pdo_mysql/tests/bug80458.phpt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
1414
$db = MySQLPDOTest::factory();
1515
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
1616
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
17+
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);
1718

1819
$db->query('DROP TABLE IF EXISTS test');
1920
$db->query('CREATE TABLE test (first int) ENGINE = InnoDB');
@@ -127,9 +128,9 @@ array(1) {
127128
[0]=>
128129
array(2) {
129130
["first"]=>
130-
int(5)
131+
string(1) "5"
131132
[0]=>
132-
int(5)
133+
string(1) "5"
133134
}
134135
}
135136
array(0) {
@@ -138,9 +139,9 @@ array(1) {
138139
[0]=>
139140
array(2) {
140141
["first"]=>
141-
int(7)
142+
string(1) "7"
142143
[0]=>
143-
int(7)
144+
string(1) "7"
144145
}
145146
}
146147
array(0) {
@@ -179,8 +180,8 @@ array(1) {
179180
[0]=>
180181
array(2) {
181182
["first"]=>
182-
int(16)
183+
string(2) "16"
183184
[0]=>
184-
int(16)
185+
string(2) "16"
185186
}
186187
}

ext/pdo_mysql/tests/bug_33689.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ $stmt->execute();
2727
$tmp = $stmt->getColumnMeta(0);
2828

2929
// libmysql and mysqlnd will show the pdo_type entry at a different position in the hash
30-
if (!isset($tmp['pdo_type']) || (isset($tmp['pdo_type']) && $tmp['pdo_type'] != 2))
31-
printf("Expecting pdo_type = 2 got %s\n", $tmp['pdo_type']);
30+
if (!isset($tmp['pdo_type']) || (isset($tmp['pdo_type']) && $tmp['pdo_type'] != 1))
31+
printf("Expecting pdo_type = 1 got %s\n", $tmp['pdo_type']);
3232
else
3333
unset($tmp['pdo_type']);
3434

ext/pdo_mysql/tests/bug_41125.phpt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ if ($version < 40100)
2121
<?php
2222
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
2323
$db = MySQLPDOTest::factory();
24+
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);
2425
$db->exec("DROP TABLE IF EXISTS test");
2526

2627
// And now allow the evil to do his work

ext/pdo_mysql/tests/bug_41997.phpt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ if ($version < 50000)
2121
<?php
2222
require __DIR__ . '/mysql_pdo_test.inc';
2323
$db = MySQLPDOTest::factory();
24+
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);
2425

2526
$db->exec('DROP PROCEDURE IF EXISTS p');
2627
$db->exec('CREATE PROCEDURE p() BEGIN SELECT 1 AS "one"; END');

ext/pdo_mysql/tests/bug_61411.phpt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ if (!$attr) {
3030
}
3131
$attr[PDO::ATTR_PERSISTENT] = true;
3232
$attr[PDO::ATTR_EMULATE_PREPARES] = false;
33+
$attr[PDO::ATTR_STRINGIFY_FETCHES] = true;
3334
putenv('PDOTEST_ATTR='.serialize($attr));
3435

3536
$db = MySQLPDOTest::factory();
@@ -46,8 +47,8 @@ print "done!";
4647
--EXPECT--
4748
array(2) {
4849
[1]=>
49-
int(1)
50+
string(1) "1"
5051
[0]=>
51-
int(1)
52+
string(1) "1"
5253
}
5354
done!

ext/pdo_mysql/tests/change_column_count.phpt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ MySQLPDOTest::skip();
1212
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
1313

1414
$db = MySQLPDOTest::factory();
15-
$db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
15+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
16+
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);
1617

1718
$db->exec('DROP TABLE IF EXISTS test');
1819
$db->exec('CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(255) NOT NULL)');

0 commit comments

Comments
 (0)