Skip to content

Commit

Permalink
PDO MySQL: Use native types for results
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
nikic committed Dec 17, 2020
1 parent 33e9049 commit c18b1ae
Show file tree
Hide file tree
Showing 28 changed files with 161 additions and 61 deletions.
6 changes: 6 additions & 0 deletions UPGRADING
Expand Up @@ -31,6 +31,12 @@ PHP 8.1 UPGRADE NOTES
. The mysqlnd.fetch_copy_data ini setting has been removed. However, this
should not result in user-visible behavior changes.

- PDO MySQL:
. Integers and floats in result sets will now be returned using native PHP
types instead of strings when using emulated prepared statements. This
matches the behavior of native prepared statements. You can restore the
previous behavior by enabling the PDO::ATTR_STRINGIFY_FETCHES option.

- Standard:
. version_compare() no longer accepts undocumented operator abbreviations.

Expand Down
2 changes: 2 additions & 0 deletions ext/mysqlnd/mysqlnd.h
Expand Up @@ -100,6 +100,8 @@ PHPAPI void mysqlnd_debug(const char *mode);
/* Query */
#define mysqlnd_fetch_into(result, flags, ret_val) (result)->m.fetch_into((result), (flags), (ret_val) ZEND_FILE_LINE_CC)
#define mysqlnd_fetch_row_c(result) (result)->m.fetch_row_c((result))
#define mysqlnd_fetch_row_zval(result, row_ptr, fetched) \
(result)->m.fetch_row((result), (row_ptr), 0, (fetched))
#define mysqlnd_fetch_all(result, flags, return_value) (result)->m.fetch_all((result), (flags), (return_value) ZEND_FILE_LINE_CC)
#define mysqlnd_get_connection_stats(conn, values) ((conn)->data)->m->get_statistics((conn)->data, (values) ZEND_FILE_LINE_CC)
#define mysqlnd_get_client_stats(values) _mysqlnd_get_client_stats(mysqlnd_global_stats, (values) ZEND_FILE_LINE_CC)
Expand Down
8 changes: 8 additions & 0 deletions ext/pdo_mysql/mysql_driver.c
Expand Up @@ -826,6 +826,14 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options)
goto cleanup;
}

#ifdef PDO_USE_MYSQLND
bool int_and_float_native = true;
if (mysql_options(H->server, MYSQLND_OPT_INT_AND_FLOAT_NATIVE, (const char *) &int_and_float_native)) {
pdo_mysql_error(dbh);
goto cleanup;
}
#endif

if (vars[0].optval && mysql_options(H->server, MYSQL_SET_CHARSET_NAME, vars[0].optval)) {
pdo_mysql_error(dbh);
goto cleanup;
Expand Down
67 changes: 40 additions & 27 deletions ext/pdo_mysql/mysql_statement.c
Expand Up @@ -34,7 +34,6 @@
# define pdo_mysql_stmt_execute_prepared(stmt) pdo_mysql_stmt_execute_prepared_libmysql(stmt)
#endif


static void pdo_mysql_free_result(pdo_mysql_stmt *S)
{
if (S->result) {
Expand All @@ -52,8 +51,16 @@ static void pdo_mysql_free_result(pdo_mysql_stmt *S)
efree(S->out_length);
S->bound_result = NULL;
}
#else
if (S->current_row) {
unsigned column_count = mysql_num_fields(S->result);
for (unsigned i = 0; i < column_count; i++) {
zval_ptr_dtor_nogc(&S->current_row[i]);
}
efree(S->current_row);
S->current_row = NULL;
}
#endif

mysql_free_result(S->result);
S->result = NULL;
}
Expand Down Expand Up @@ -104,12 +111,6 @@ static int pdo_mysql_stmt_dtor(pdo_stmt_t *stmt) /* {{{ */
}
}

#ifdef PDO_USE_MYSQLND
if (!S->stmt && S->current_data) {
mnd_efree(S->current_data);
}
#endif /* PDO_USE_MYSQLND */

efree(S);
PDO_DBG_RETURN(1);
}
Expand Down Expand Up @@ -553,9 +554,24 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori
PDO_DBG_RETURN(1);
}

if (!S->stmt && S->current_data) {
mnd_efree(S->current_data);
zval *row_data;
if (mysqlnd_fetch_row_zval(S->result, &row_data, &fetched_anything) == FAIL) {
pdo_mysql_error_stmt(stmt);
PDO_DBG_RETURN(0);
}

if (!fetched_anything) {
PDO_DBG_RETURN(0);
}

if (!S->current_row) {
S->current_row = ecalloc(sizeof(zval), stmt->column_count);
}
for (unsigned i = 0; i < stmt->column_count; i++) {
zval_ptr_dtor_nogc(&S->current_row[i]);
ZVAL_COPY_VALUE(&S->current_row[i], &row_data[i]);
}
PDO_DBG_RETURN(1);
#else
int ret;

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

PDO_DBG_RETURN(1);
}
#endif /* PDO_USE_MYSQLND */

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

S->current_lengths = mysql_fetch_lengths(S->result);
PDO_DBG_RETURN(1);
#endif /* PDO_USE_MYSQLND */
}
/* }}} */

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

#ifdef PDO_USE_MYSQLND
if (S->stmt) {
cols[i].param_type = PDO_PARAM_ZVAL;
} else
cols[i].param_type = PDO_PARAM_ZVAL;
#else
cols[i].param_type = PDO_PARAM_STR;
#endif
{
cols[i].param_type = PDO_PARAM_STR;
}
}
PDO_DBG_RETURN(1);
}
Expand All @@ -652,13 +665,6 @@ static int pdo_mysql_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, size_
PDO_DBG_RETURN(0);
}

/* With mysqlnd data is stored inside mysqlnd, not S->current_data */
if (!S->stmt) {
if (S->current_data == NULL || !S->result) {
PDO_DBG_RETURN(0);
}
}

if (colno >= stmt->column_count) {
/* error invalid column */
PDO_DBG_RETURN(0);
Expand All @@ -667,9 +673,12 @@ static int pdo_mysql_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, size_
if (S->stmt) {
Z_TRY_ADDREF(S->stmt->data->result_bind[colno].zv);
*ptr = (char*)&S->stmt->data->result_bind[colno].zv;
*len = sizeof(zval);
PDO_DBG_RETURN(1);
} else {
Z_TRY_ADDREF(S->current_row[colno]);
*ptr = (char*)&S->current_row[colno];
}
*len = sizeof(zval);
PDO_DBG_RETURN(1);
#else
if (S->stmt) {
if (S->out_null[colno]) {
Expand All @@ -688,10 +697,14 @@ static int pdo_mysql_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, size_
*len = S->out_length[colno];
PDO_DBG_RETURN(1);
}
#endif

if (S->current_data == NULL) {
PDO_DBG_RETURN(0);
}
*ptr = S->current_data[colno];
*len = S->current_lengths[colno];
PDO_DBG_RETURN(1);
#endif
} /* }}} */

static char *type_to_name_native(int type) /* {{{ */
Expand Down
12 changes: 5 additions & 7 deletions ext/pdo_mysql/php_pdo_mysql_int.h
Expand Up @@ -120,12 +120,6 @@ typedef struct {
pdo_mysql_db_handle *H;
MYSQL_RES *result;
const MYSQL_FIELD *fields;
MYSQL_ROW current_data;
#ifdef PDO_USE_MYSQLND
const size_t *current_lengths;
#else
unsigned long *current_lengths;
#endif
pdo_mysql_error_info einfo;
#ifdef PDO_USE_MYSQLND
MYSQLND_STMT *stmt;
Expand All @@ -137,10 +131,14 @@ typedef struct {
#ifndef PDO_USE_MYSQLND
my_bool *in_null;
zend_ulong *in_length;
#endif
PDO_MYSQL_PARAM_BIND *bound_result;
my_bool *out_null;
zend_ulong *out_length;
MYSQL_ROW current_data;
unsigned long *current_lengths;
#else
zval *current_row;
#endif
unsigned max_length:1;
/* Whether all result sets have been fully consumed.
* If this flag is not set, they need to be consumed during destruction. */
Expand Down
3 changes: 2 additions & 1 deletion ext/pdo_mysql/tests/bug44327.phpt
Expand Up @@ -11,6 +11,7 @@ $db = MySQLPDOTest::factory();
<?php
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
$db = MySQLPDOTest::factory();
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);

$stmt = $db->prepare("SELECT 1 AS \"one\"");
$stmt->execute();
Expand Down Expand Up @@ -51,7 +52,7 @@ string(1) "1"
string(1) "1"
string(17) "SELECT 1 AS "one""
----------------------------------
object(PDORow)#%d (2) {
object(PDORow)#5 (2) {
["queryString"]=>
string(19) "SELECT id FROM test"
["id"]=>
Expand Down
1 change: 1 addition & 0 deletions ext/pdo_mysql/tests/bug71145.phpt
Expand Up @@ -14,6 +14,7 @@ require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
$attr = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; SET SESSION sql_mode=traditional',
PDO::ATTR_STRINGIFY_FETCHES => true,
);
putenv('PDOTEST_ATTR=' . serialize($attr));

Expand Down
16 changes: 13 additions & 3 deletions ext/pdo_mysql/tests/bug75177.phpt
Expand Up @@ -5,6 +5,7 @@ PDO MySQL Bug #75177 Type 'bit' is fetched as unexpected string
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc');
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
MySQLPDOTest::skip();
if (!MySQLPDOTest::isPDOMySQLnd()) die('skip only for mysqlnd');
?>
--FILE--
<?php
Expand All @@ -18,14 +19,23 @@ $pdo->query("INSERT INTO $tbl (`bit`) VALUES (1)");
$pdo->query("INSERT INTO $tbl (`bit`) VALUES (0b011)");
$pdo->query("INSERT INTO $tbl (`bit`) VALUES (0b01100)");

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

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

?>
--EXPECT--
string(1) "1"
string(1) "3"
string(2) "12"
int(1)
int(3)
int(12)
int(1)
int(3)
int(12)
13 changes: 7 additions & 6 deletions ext/pdo_mysql/tests/bug80458.phpt
Expand Up @@ -14,6 +14,7 @@ require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
$db = MySQLPDOTest::factory();
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);

$db->query('DROP TABLE IF EXISTS test');
$db->query('CREATE TABLE test (first int) ENGINE = InnoDB');
Expand Down Expand Up @@ -127,9 +128,9 @@ array(1) {
[0]=>
array(2) {
["first"]=>
int(5)
string(1) "5"
[0]=>
int(5)
string(1) "5"
}
}
array(0) {
Expand All @@ -138,9 +139,9 @@ array(1) {
[0]=>
array(2) {
["first"]=>
int(7)
string(1) "7"
[0]=>
int(7)
string(1) "7"
}
}
array(0) {
Expand Down Expand Up @@ -179,8 +180,8 @@ array(1) {
[0]=>
array(2) {
["first"]=>
int(16)
string(2) "16"
[0]=>
int(16)
string(2) "16"
}
}
4 changes: 2 additions & 2 deletions ext/pdo_mysql/tests/bug_33689.phpt
Expand Up @@ -27,8 +27,8 @@ $stmt->execute();
$tmp = $stmt->getColumnMeta(0);

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

Expand Down
1 change: 1 addition & 0 deletions ext/pdo_mysql/tests/bug_41125.phpt
Expand Up @@ -21,6 +21,7 @@ if ($version < 40100)
<?php
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
$db = MySQLPDOTest::factory();
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);
$db->exec("DROP TABLE IF EXISTS test");

// And now allow the evil to do his work
Expand Down
1 change: 1 addition & 0 deletions ext/pdo_mysql/tests/bug_41997.phpt
Expand Up @@ -21,6 +21,7 @@ if ($version < 50000)
<?php
require __DIR__ . '/mysql_pdo_test.inc';
$db = MySQLPDOTest::factory();
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);

$db->exec('DROP PROCEDURE IF EXISTS p');
$db->exec('CREATE PROCEDURE p() BEGIN SELECT 1 AS "one"; END');
Expand Down
5 changes: 3 additions & 2 deletions ext/pdo_mysql/tests/bug_61411.phpt
Expand Up @@ -30,6 +30,7 @@ if (!$attr) {
}
$attr[PDO::ATTR_PERSISTENT] = true;
$attr[PDO::ATTR_EMULATE_PREPARES] = false;
$attr[PDO::ATTR_STRINGIFY_FETCHES] = true;
putenv('PDOTEST_ATTR='.serialize($attr));

$db = MySQLPDOTest::factory();
Expand All @@ -46,8 +47,8 @@ print "done!";
--EXPECT--
array(2) {
[1]=>
int(1)
string(1) "1"
[0]=>
int(1)
string(1) "1"
}
done!
3 changes: 2 additions & 1 deletion ext/pdo_mysql/tests/change_column_count.phpt
Expand Up @@ -12,7 +12,8 @@ MySQLPDOTest::skip();
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');

$db = MySQLPDOTest::factory();
$db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true);

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

0 comments on commit c18b1ae

Please sign in to comment.