diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index f8ff90ba9b4..d4c60866904 100644 --- a/ext/pdo/pdo_stmt.c +++ b/ext/pdo/pdo_stmt.c @@ -173,6 +173,45 @@ int pdo_stmt_describe_columns(pdo_stmt_t *stmt) /* {{{ */ } /* }}} */ +static void pdo_stmt_reset_columns(pdo_stmt_t *stmt) { + if (stmt->columns) { + int i; + struct pdo_column_data *cols = stmt->columns; + + for (i = 0; i < stmt->column_count; i++) { + if (cols[i].name) { + zend_string_release_ex(cols[i].name, 0); + } + } + efree(stmt->columns); + } + stmt->columns = NULL; + stmt->column_count = 0; +} + +/** + * Change the column count on the statement. If it differs from the previous one, + * discard existing columns information. + */ +PDO_API void php_pdo_stmt_set_column_count(pdo_stmt_t *stmt, int new_count) +{ + /* Columns not yet "described". */ + if (!stmt->columns) { + stmt->column_count = new_count; + return; + } + + /* The column count has not changed: No need to reload columns description. + * Note: Do not handle attribute name change, without column count change. */ + if (new_count == stmt->column_count) { + return; + } + + /* Free previous columns to force reload description. */ + pdo_stmt_reset_columns(stmt); + stmt->column_count = new_count; +} + static void get_lazy_object(pdo_stmt_t *stmt, zval *return_value) /* {{{ */ { if (Z_ISUNDEF(stmt->lazy_object_ref)) { @@ -1910,20 +1949,7 @@ PHP_METHOD(PDOStatement, setFetchMode) static bool pdo_stmt_do_next_rowset(pdo_stmt_t *stmt) { - /* un-describe */ - if (stmt->columns) { - int i; - struct pdo_column_data *cols = stmt->columns; - - for (i = 0; i < stmt->column_count; i++) { - if (cols[i].name) { - zend_string_release_ex(cols[i].name, 0); - } - } - efree(stmt->columns); - stmt->columns = NULL; - stmt->column_count = 0; - } + pdo_stmt_reset_columns(stmt); if (!stmt->methods->next_rowset(stmt)) { /* Set the executed flag to 0 to reallocate columns on next execute */ @@ -2156,19 +2182,7 @@ PDO_API void php_pdo_free_statement(pdo_stmt_t *stmt) efree(stmt->query_string); } - if (stmt->columns) { - int i; - struct pdo_column_data *cols = stmt->columns; - - for (i = 0; i < stmt->column_count; i++) { - if (cols[i].name) { - zend_string_release_ex(cols[i].name, 0); - cols[i].name = NULL; - } - } - efree(stmt->columns); - stmt->columns = NULL; - } + pdo_stmt_reset_columns(stmt); if (!Z_ISUNDEF(stmt->fetch.into) && stmt->default_fetch_type == PDO_FETCH_INTO) { zval_ptr_dtor(&stmt->fetch.into); diff --git a/ext/pdo/php_pdo_driver.h b/ext/pdo/php_pdo_driver.h index 974eb0bad02..2c6bd96600a 100644 --- a/ext/pdo/php_pdo_driver.h +++ b/ext/pdo/php_pdo_driver.h @@ -695,7 +695,7 @@ PDO_API void php_pdo_dbh_addref(pdo_dbh_t *dbh); PDO_API void php_pdo_dbh_delref(pdo_dbh_t *dbh); PDO_API void php_pdo_free_statement(pdo_stmt_t *stmt); - +PDO_API void php_pdo_stmt_set_column_count(pdo_stmt_t *stmt, int new_count); PDO_API void pdo_throw_exception(unsigned int driver_errcode, char *driver_errmsg, pdo_error_type *pdo_error); #endif /* PHP_PDO_DRIVER_H */ diff --git a/ext/pdo_mysql/mysql_statement.c b/ext/pdo_mysql/mysql_statement.c index 0d1cd348c7a..c8a6a218abe 100644 --- a/ext/pdo_mysql/mysql_statement.c +++ b/ext/pdo_mysql/mysql_statement.c @@ -144,7 +144,7 @@ static int pdo_mysql_fill_stmt_from_result(pdo_stmt_t *stmt) /* {{{ */ } stmt->row_count = (zend_long) mysql_num_rows(S->result); - stmt->column_count = (int) mysql_num_fields(S->result); + php_pdo_stmt_set_column_count(stmt, (int) mysql_num_fields(S->result)); S->fields = mysql_fetch_fields(S->result); } else { /* this was a DML or DDL query (INSERT, UPDATE, DELETE, ... */ @@ -194,7 +194,7 @@ static int pdo_mysql_stmt_execute_prepared_libmysql(pdo_stmt_t *stmt) /* {{{ */ efree(S->out_length); } - stmt->column_count = (int)mysql_num_fields(S->result); + php_pdo_stmt_set_column_count(stmt, (int)mysql_num_fields(S->result)); S->bound_result = ecalloc(stmt->column_count, sizeof(MYSQL_BIND)); S->out_null = ecalloc(stmt->column_count, sizeof(my_bool)); S->out_length = ecalloc(stmt->column_count, sizeof(zend_ulong)); @@ -290,7 +290,7 @@ static int pdo_mysql_stmt_execute_prepared_mysqlnd(pdo_stmt_t *stmt) /* {{{ */ } /* for SHOW/DESCRIBE and others the column/field count is not available before execute */ - stmt->column_count = mysql_stmt_field_count(S->stmt); + php_pdo_stmt_set_column_count(stmt, mysql_stmt_field_count(S->stmt)); for (i = 0; i < stmt->column_count; i++) { mysqlnd_stmt_bind_one_result(S->stmt, i); } @@ -378,7 +378,7 @@ static int pdo_mysql_stmt_next_rowset(pdo_stmt_t *stmt) /* {{{ */ /* for SHOW/DESCRIBE and others the column/field count is not available before execute */ int i; - stmt->column_count = mysql_stmt_field_count(S->stmt); + php_pdo_stmt_set_column_count(stmt, mysql_stmt_field_count(S->stmt)); for (i = 0; i < stmt->column_count; i++) { mysqlnd_stmt_bind_one_result(S->stmt, i); } @@ -407,9 +407,6 @@ static int pdo_mysql_stmt_next_rowset(pdo_stmt_t *stmt) /* {{{ */ /* ensure that we free any previous unfetched results */ #ifndef PDO_USE_MYSQLND if (S->stmt) { - if (S->result) { - stmt->column_count = (int)mysql_num_fields(S->result); - } mysql_stmt_free_result(S->stmt); } #endif diff --git a/ext/pdo_mysql/tests/change_column_count.phpt b/ext/pdo_mysql/tests/change_column_count.phpt new file mode 100644 index 00000000000..5bb521d300c --- /dev/null +++ b/ext/pdo_mysql/tests/change_column_count.phpt @@ -0,0 +1,60 @@ +--TEST-- +Change column count after statement has been prepared +--SKIPIF-- + +--FILE-- +setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + +$db->exec('DROP TABLE IF EXISTS test'); +$db->exec('CREATE TABLE test (id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(255) NOT NULL)'); + +$stmt = $db->prepare('INSERT INTO test (id, name) VALUES(:id, :name)'); +$stmt->execute([ + 'id' => 10, + 'name' => 'test', +]); + +$stmt = $db->prepare('SELECT * FROM test WHERE id = :id'); +$stmt->execute(['id' => 10]); +var_dump($stmt->fetchAll(\PDO::FETCH_ASSOC)); + +$db->exec('ALTER TABLE test ADD new_col VARCHAR(255)'); +$stmt->execute(['id' => 10]); +var_dump($stmt->fetchAll(\PDO::FETCH_ASSOC)); + +?> +--CLEAN-- + +--EXPECT-- +array(1) { + [0]=> + array(2) { + ["id"]=> + string(2) "10" + ["name"]=> + string(4) "test" + } +} +array(1) { + [0]=> + array(3) { + ["id"]=> + string(2) "10" + ["name"]=> + string(4) "test" + ["new_col"]=> + NULL + } +} diff --git a/ext/pdo_sqlite/sqlite_statement.c b/ext/pdo_sqlite/sqlite_statement.c index 64a90e0edea..3769b1e0496 100644 --- a/ext/pdo_sqlite/sqlite_statement.c +++ b/ext/pdo_sqlite/sqlite_statement.c @@ -39,46 +39,6 @@ static int pdo_sqlite_stmt_dtor(pdo_stmt_t *stmt) return 1; } -/** - * Change the column count on the statement. - * - * Since PHP 7.2 sqlite3_prepare_v2 is used which auto recompile prepared statement on schema change. - * Instead of raise an error on schema change, the result set will change, and the statement's columns must be updated. - * - * See bug #78192 - */ -static void pdo_sqlite_stmt_set_column_count(pdo_stmt_t *stmt, int new_count) -{ - /* Columns not yet "described" */ - if (!stmt->columns) { - stmt->column_count = new_count; - - return; - } - - /* - * The column count has not changed : no need to reload columns description - * Note: Do not handle attribute name change, without column count change - */ - if (new_count == stmt->column_count) { - return; - } - - /* Free previous columns to force reload description */ - int i; - - for (i = 0; i < stmt->column_count; i++) { - if (stmt->columns[i].name) { - zend_string_release(stmt->columns[i].name); - stmt->columns[i].name = NULL; - } - } - - efree(stmt->columns); - stmt->columns = NULL; - stmt->column_count = new_count; -} - static int pdo_sqlite_stmt_execute(pdo_stmt_t *stmt) { pdo_sqlite_stmt *S = (pdo_sqlite_stmt*)stmt->driver_data; @@ -91,11 +51,11 @@ static int pdo_sqlite_stmt_execute(pdo_stmt_t *stmt) switch (sqlite3_step(S->stmt)) { case SQLITE_ROW: S->pre_fetched = 1; - pdo_sqlite_stmt_set_column_count(stmt, sqlite3_data_count(S->stmt)); + php_pdo_stmt_set_column_count(stmt, sqlite3_data_count(S->stmt)); return 1; case SQLITE_DONE: - pdo_sqlite_stmt_set_column_count(stmt, sqlite3_column_count(S->stmt)); + php_pdo_stmt_set_column_count(stmt, sqlite3_column_count(S->stmt)); stmt->row_count = sqlite3_changes(S->H->db); sqlite3_reset(S->stmt); S->done = 1;