Skip to content

Commit 311a77d

Browse files
committed
Handle changing column count in mysqlnd result binding
If the count changes from prepare to execute and result_bind is alreadly allocated, reallocate it there. This is something of a hack. It would be cleaner to require that result bindings are registered only after execute, when the final result set fields are known. But mysqli at least directly exposes this to the user, so we have no guarantee.
1 parent 2df09b9 commit 311a77d

File tree

2 files changed

+25
-50
lines changed

2 files changed

+25
-50
lines changed

ext/mysqlnd/mysqlnd_ps.c

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ enum_func_status mysqlnd_stmt_execute_generate_request(MYSQLND_STMT * const s, z
3636
enum_func_status mysqlnd_stmt_execute_batch_generate_request(MYSQLND_STMT * const s, zend_uchar ** request, size_t *request_len, zend_bool * free_buffer);
3737

3838
static void mysqlnd_stmt_separate_result_bind(MYSQLND_STMT * const stmt);
39-
static void mysqlnd_stmt_separate_one_result_bind(MYSQLND_STMT * const stmt, const unsigned int param_no);
4039

4140
/* {{{ mysqlnd_stmt::store_result */
4241
static MYSQLND_RES *
@@ -542,7 +541,27 @@ mysqlnd_stmt_execute_parse_response(MYSQLND_STMT * const s, enum_mysqlnd_parse_e
542541
stmt->result->conn = conn->m->get_reference(conn);
543542
}
544543

545-
/* Update stmt->field_count as SHOW sets it to 0 at prepare */
544+
/* If the field count changed, update the result_bind structure. Ideally result_bind
545+
* would only ever be created after execute, in which case the size cannot change anymore,
546+
* but at least in mysqli this does not seem enforceable. */
547+
if (stmt->result_bind && conn->field_count != stmt->field_count) {
548+
if (conn->field_count < stmt->field_count) {
549+
/* Number of columns decreased, free bindings. */
550+
for (unsigned i = conn->field_count; i < stmt->field_count; i++) {
551+
zval_ptr_dtor(&stmt->result_bind[i].zv);
552+
}
553+
}
554+
stmt->result_bind =
555+
mnd_erealloc(stmt->result_bind, conn->field_count * sizeof(MYSQLND_RESULT_BIND));
556+
if (conn->field_count > stmt->field_count) {
557+
/* Number of columns increase, initialize new ones. */
558+
for (unsigned i = stmt->field_count; i < conn->field_count; i++) {
559+
ZVAL_UNDEF(&stmt->result_bind[i].zv);
560+
stmt->result_bind[i].bound = false;
561+
}
562+
}
563+
}
564+
546565
stmt->field_count = stmt->result->field_count = conn->field_count;
547566
if (stmt->result->stored_data) {
548567
stmt->result->stored_data->lengths = NULL;
@@ -1577,22 +1596,13 @@ MYSQLND_METHOD(mysqlnd_stmt, bind_one_result)(MYSQLND_STMT * const s, unsigned i
15771596
SET_EMPTY_ERROR(conn->error_info);
15781597

15791598
if (stmt->field_count) {
1580-
mysqlnd_stmt_separate_one_result_bind(s, param_no);
1581-
/* Guaranteed is that stmt->result_bind is NULL */
15821599
if (!stmt->result_bind) {
15831600
stmt->result_bind = mnd_ecalloc(stmt->field_count, sizeof(MYSQLND_RESULT_BIND));
1584-
} else {
1585-
stmt->result_bind = mnd_erealloc(stmt->result_bind, stmt->field_count * sizeof(MYSQLND_RESULT_BIND));
15861601
}
1587-
if (!stmt->result_bind) {
1588-
DBG_RETURN(FAIL);
1602+
if (stmt->result_bind[param_no].bound) {
1603+
zval_ptr_dtor(&stmt->result_bind[param_no].zv);
15891604
}
15901605
ZVAL_NULL(&stmt->result_bind[param_no].zv);
1591-
/*
1592-
Don't update is_ref !!! it's not our job
1593-
Otherwise either 009.phpt or mysqli_stmt_bind_result.phpt
1594-
will fail.
1595-
*/
15961606
stmt->result_bind[param_no].bound = TRUE;
15971607
}
15981608
DBG_INF("PASS");
@@ -1968,37 +1978,6 @@ mysqlnd_stmt_separate_result_bind(MYSQLND_STMT * const s)
19681978
/* }}} */
19691979

19701980

1971-
/* {{{ mysqlnd_stmt_separate_one_result_bind */
1972-
static void
1973-
mysqlnd_stmt_separate_one_result_bind(MYSQLND_STMT * const s, const unsigned int param_no)
1974-
{
1975-
MYSQLND_STMT_DATA * stmt = s? s->data : NULL;
1976-
DBG_ENTER("mysqlnd_stmt_separate_one_result_bind");
1977-
if (!stmt) {
1978-
DBG_VOID_RETURN;
1979-
}
1980-
DBG_INF_FMT("stmt=%lu result_bind=%p field_count=%u param_no=%u", stmt->stmt_id, stmt->result_bind, stmt->field_count, param_no);
1981-
1982-
if (!stmt->result_bind) {
1983-
DBG_VOID_RETURN;
1984-
}
1985-
1986-
/*
1987-
Because only the bound variables can point to our internal buffers, then
1988-
separate or free only them. Free is possible because the user could have
1989-
lost reference.
1990-
*/
1991-
/* Let's try with no cache */
1992-
if (stmt->result_bind[param_no].bound == TRUE) {
1993-
DBG_INF_FMT("%u has refcount=%u", param_no, Z_REFCOUNTED(stmt->result_bind[param_no].zv)? Z_REFCOUNT(stmt->result_bind[param_no].zv) : 0);
1994-
zval_ptr_dtor(&stmt->result_bind[param_no].zv);
1995-
}
1996-
1997-
DBG_VOID_RETURN;
1998-
}
1999-
/* }}} */
2000-
2001-
20021981
/* {{{ mysqlnd_stmt::free_stmt_result */
20031982
static void
20041983
MYSQLND_METHOD(mysqlnd_stmt, free_stmt_result)(MYSQLND_STMT * const s)

ext/pdo_mysql/tests/pdo_mysql_stmt_variable_columncount.phpt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
--TEST--
22
MySQL Prepared Statements and different column counts
3-
--XFAIL--
4-
nextRowset() problem with stored proc & emulation mode & mysqlnd
53
--SKIPIF--
64
<?php
75
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc');
@@ -25,10 +23,8 @@ if ($version < 50000)
2523
$db = MySQLPDOTest::factory();
2624

2725
function check_result($offset, $stmt, $columns) {
28-
29-
do {
30-
$row = $stmt->fetch(PDO::FETCH_ASSOC);
31-
} while ($stmt->nextRowSet());
26+
$row = $stmt->fetch(PDO::FETCH_ASSOC);
27+
$stmt->nextRowSet();
3228

3329
if (!isset($row['one']) || ($row['one'] != 1)) {
3430
printf("[%03d + 1] Expecting array('one' => 1), got %s\n", $offset, var_export($row, true));

0 commit comments

Comments
 (0)