]> granicus.if.org Git - php/commitdiff
Handle column count change in PDO MySQL
authorNikita Popov <nikita.ppv@gmail.com>
Tue, 8 Dec 2020 15:37:00 +0000 (16:37 +0100)
committerNikita Popov <nikita.ppv@gmail.com>
Tue, 8 Dec 2020 15:41:35 +0000 (16:41 +0100)
This has been fixed for PDO SQlite by GH-4313, however the same
issue also applied to PDO MySQL.

Move the column count setting function into the main PDO layer
(and export it) and then use it in both PDO SQLite and PDO MySQL.

ext/pdo/pdo_stmt.c
ext/pdo/php_pdo_driver.h
ext/pdo_mysql/mysql_statement.c
ext/pdo_mysql/tests/change_column_count.phpt [new file with mode: 0644]
ext/pdo_sqlite/sqlite_statement.c

index f8ff90ba9b425f0746091668ed7b4205c9c741eb..d4c60866904ae4e628fd45e6da1c2cf8df10ebe0 100644 (file)
@@ -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);
index 960ddec4efe089b8e22a439e353550448742fa3c..2ee8acdd561c1c784bb5082157f5cfedd8f94c50 100644 (file)
@@ -690,7 +690,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 */
index 0d1cd348c7a592a5243c7664e5d239a73bae90b2..c8a6a218abe65dbae64ce536af8ee8f39ca0fec3 100644 (file)
@@ -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 (file)
index 0000000..5bb521d
--- /dev/null
@@ -0,0 +1,60 @@
+--TEST--
+Change column count after statement has been prepared
+--SKIPIF--
+<?php
+if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) die('skip not loaded');
+require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc');
+require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+MySQLPDOTest::skip();
+?>
+--FILE--
+<?php
+require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+
+$db = MySQLPDOTest::factory();
+$db->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--
+<?php
+require __DIR__ . '/mysql_pdo_test.inc';
+MySQLPDOTest::dropTestTable();
+?>
+--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
+  }
+}
index 64a90e0edea359972f7728049b51533d12cd7740..3769b1e049613358a6a52d0ff414960a6c66666f 100644 (file)
@@ -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;