diff --git a/include/sqlite3db.h b/include/sqlite3db.h index ad9d0107b..bdd01fc9b 100644 --- a/include/sqlite3db.h +++ b/include/sqlite3db.h @@ -44,6 +44,7 @@ extern void (*proxy_sqlite3_free)(void*); extern int (*proxy_sqlite3_status)(int op, int *pCurrent, int *pHighwater, int resetFlag); extern int (*proxy_sqlite3_status64)(int op, long long *pCurrent, long long *pHighwater, int resetFlag); extern int (*proxy_sqlite3_changes)(sqlite3*); +extern long long (*proxy_sqlite3_total_changes64)(sqlite3*); extern int (*proxy_sqlite3_step)(sqlite3_stmt*); extern int (*proxy_sqlite3_config)(int, ...); extern int (*proxy_sqlite3_shutdown)(void); @@ -93,6 +94,7 @@ int (*proxy_sqlite3_status)(int op, int *pCurrent, int *pHighwater, int resetFla int (*proxy_sqlite3_status64)(int op, long long *pCurrent, long long *pHighwater, int resetFlag); int (*proxy_sqlite3_changes)(sqlite3*); +long long (*proxy_sqlite3_total_changes64)(sqlite3*); int (*proxy_sqlite3_step)(sqlite3_stmt*); int (*proxy_sqlite3_config)(int, ...); int (*proxy_sqlite3_shutdown)(void); diff --git a/lib/sqlite3db.cpp b/lib/sqlite3db.cpp index 3acae23c0..37d7f3cb1 100644 --- a/lib/sqlite3db.cpp +++ b/lib/sqlite3db.cpp @@ -17,7 +17,6 @@ #define USLEEP_SQLITE_LOCKED 100 - /** * @brief Constructor for the SQLite3_column class. * @@ -342,6 +341,8 @@ bool SQLite3DB::execute_statement(const char *str, char **error, int *cols, int *cols = (*proxy_sqlite3_column_count)(statement); if (*cols==0) { // not a SELECT *resultset=NULL; + // Get total changes before executing the statement + long long total_changes_before = (*proxy_sqlite3_total_changes64)(db); do { rc=(*proxy_sqlite3_step)(statement); if (rc==SQLITE_LOCKED || rc==SQLITE_BUSY) { // the execution of the prepared statement failed because locked @@ -353,7 +354,9 @@ bool SQLite3DB::execute_statement(const char *str, char **error, int *cols, int } } while (rc==SQLITE_LOCKED || rc==SQLITE_BUSY); if (rc==SQLITE_DONE) { - *affected_rows=(*proxy_sqlite3_changes)(db); + // Calculate affected rows as the difference in total changes + long long total_changes_after = (*proxy_sqlite3_total_changes64)(db); + *affected_rows = (int)(total_changes_after - total_changes_before); ret=true; } else { *error=strdup((*proxy_sqlite3_errmsg)(db)); @@ -372,7 +375,7 @@ bool SQLite3DB::execute_statement(const char *str, char **error, int *cols, int /** * @brief Executes a SQL statement and returns the result set without parsing it. - * + * * @param str The SQL statement to execute. * @param error Pointer to a variable to store the error message. * @param cols Pointer to a variable to store the number of columns. @@ -394,6 +397,8 @@ bool SQLite3DB::execute_statement_raw(const char *str, char **error, int *cols, *cols = (*proxy_sqlite3_column_count)(*statement); if (*cols==0) { // not a SELECT //*resultset=NULL; + // Get total changes before executing the statement + long long total_changes_before = (*proxy_sqlite3_total_changes64)(db); do { rc=(*proxy_sqlite3_step)(*statement); if (rc==SQLITE_LOCKED || rc==SQLITE_BUSY) { // the execution of the prepared statement failed because locked @@ -401,7 +406,9 @@ bool SQLite3DB::execute_statement_raw(const char *str, char **error, int *cols, } } while (rc==SQLITE_LOCKED || rc==SQLITE_BUSY); if (rc==SQLITE_DONE) { - *affected_rows=(*proxy_sqlite3_changes)(db); + // Calculate affected rows as the difference in total changes + long long total_changes_after = (*proxy_sqlite3_total_changes64)(db); + *affected_rows = (int)(total_changes_after - total_changes_before); ret=true; } else { *error=strdup((*proxy_sqlite3_errmsg)(db)); @@ -1010,6 +1017,7 @@ void SQLite3DB::LoadPlugin(const char *plugin_name) { proxy_sqlite3_status = NULL; proxy_sqlite3_status64 = NULL; proxy_sqlite3_changes = NULL; + proxy_sqlite3_total_changes64 = NULL; proxy_sqlite3_step = NULL; proxy_sqlite3_shutdown = NULL; proxy_sqlite3_prepare_v2 = NULL; @@ -1089,6 +1097,7 @@ void SQLite3DB::LoadPlugin(const char *plugin_name) { proxy_sqlite3_status = sqlite3_status; proxy_sqlite3_status64 = sqlite3_status64; proxy_sqlite3_changes = sqlite3_changes; + proxy_sqlite3_total_changes64 = sqlite3_total_changes64; proxy_sqlite3_step = sqlite3_step; proxy_sqlite3_shutdown = sqlite3_shutdown; proxy_sqlite3_prepare_v2 = sqlite3_prepare_v2; @@ -1118,6 +1127,7 @@ void SQLite3DB::LoadPlugin(const char *plugin_name) { assert(proxy_sqlite3_status); assert(proxy_sqlite3_status64); assert(proxy_sqlite3_changes); + assert(proxy_sqlite3_total_changes64); assert(proxy_sqlite3_step); assert(proxy_sqlite3_shutdown); assert(proxy_sqlite3_prepare_v2); diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index b95ebb3f8..7a4f904d6 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -31,6 +31,7 @@ "mysql-reg_test_4716_single_semicolon-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "mysql-reg_test_4723_query_cache_stores_empty_result-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "mysql-reg_test_4867_query_rules-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], + "reg_test_4855_affected_rows_ddl-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "reg_test_5212_tcp_keepalive_warnings-t" : [ "default-g1" ], "mysql-set_transaction-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], "mysql-sql_log_bin-error-t" : [ "default-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1" ], diff --git a/test/tap/tests/reg_test_4855_affected_rows_ddl-t.cpp b/test/tap/tests/reg_test_4855_affected_rows_ddl-t.cpp new file mode 100644 index 000000000..f2baab5f9 --- /dev/null +++ b/test/tap/tests/reg_test_4855_affected_rows_ddl-t.cpp @@ -0,0 +1,195 @@ +/* + Copyright (c) 2025 ProxySQL + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +#include +#include +#include +#include +#include +#include "tap.h" +#include "command_line.h" +#include "utils.h" +#include "mysql.h" + +int main(int argc, char** argv) { + plan(14); + + CommandLine cl; + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return exit_status(); + } + + MYSQL *admin = mysql_init(NULL); + ok(admin != NULL, "mysql_init() succeeded"); + if (!admin) { + return exit_status(); + } + + // Connect to ProxySQL Admin using command line arguments + if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + diag("Failed to connect to ProxySQL Admin: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + ok(true, "Connected to ProxySQL Admin"); + + // Clean up any existing test tables + mysql_query(admin, "DROP TABLE IF EXISTS test_table_4855"); + mysql_query(admin, "DROP TABLE IF EXISTS test_table_4855_2"); + + // Test 1: Run a DDL query - should return 0 affected rows (this was the bug) + if (mysql_query(admin, "CREATE TABLE test_table_4855 (id INT PRIMARY KEY, name VARCHAR(255))")) { + diag("Failed to execute CREATE TABLE query: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + my_ulonglong affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 0, "CREATE TABLE returns 0 affected rows (bug fix verified): %llu", affected_rows); + diag("CREATE TABLE query executed successfully"); + + // Test 2: Run a DML query that affects rows + if (mysql_query(admin, "INSERT INTO test_table_4855 (id, name) VALUES (1, 'test1')")) { + diag("Failed to execute INSERT query: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 1, "INSERT query returns 1 affected row: %llu", affected_rows); + diag("INSERT query executed successfully"); + + // Test 3: Run another DML query + if (mysql_query(admin, "INSERT INTO test_table_4855 (id, name) VALUES (2, 'test2')")) { + diag("Failed to execute second INSERT query: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 1, "Second INSERT query returns 1 affected row: %llu", affected_rows); + diag("Second INSERT query executed successfully"); + + // Test 4: Run another DDL query - should return 0 affected rows + if (mysql_query(admin, "CREATE TABLE test_table_4855_2 (id INT PRIMARY KEY, value TEXT)")) { + diag("Failed to execute second CREATE TABLE query: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 0, "Second CREATE TABLE returns 0 affected rows: %llu", affected_rows); + diag("Second CREATE TABLE query executed successfully"); + + // Test 5: Run an UPDATE query - should return correct affected rows + if (mysql_query(admin, "UPDATE test_table_4855 SET name = 'updated' WHERE id = 1")) { + diag("Failed to execute UPDATE query: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 1, "UPDATE query returns 1 affected row: %llu", affected_rows); + diag("UPDATE query executed successfully"); + + // Test 6: Run a DELETE query - should return correct affected rows + if (mysql_query(admin, "DELETE FROM test_table_4855 WHERE id IN (1, 2)")) { + diag("Failed to execute DELETE query: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 2, "DELETE query returns 2 affected rows: %llu", affected_rows); + diag("DELETE query executed successfully"); + + // Test 7: Run DROP TABLE - should return 0 affected rows + if (mysql_query(admin, "DROP TABLE test_table_4855")) { + diag("Failed to execute DROP TABLE query: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 0, "DROP TABLE returns 0 affected rows: %llu", affected_rows); + diag("DROP TABLE query executed successfully"); + + // Test 8: Run another DDL to verify the fix again (ALTER TABLE) + if (mysql_query(admin, "ALTER TABLE test_table_4855_2 ADD COLUMN extra INTEGER")) { + diag("Failed to execute ALTER TABLE query: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 0, "ALTER TABLE returns 0 affected rows: %llu", affected_rows); + diag("ALTER TABLE query executed successfully"); + + // Test 9: Test with comments followed by DDL (this was mentioned in the issue) + if (mysql_query(admin, "/* This is a comment */ CREATE TABLE test_table_4855 (id INT PRIMARY KEY)")) { + diag("Failed to execute CREATE TABLE with comment: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 0, "CREATE TABLE with comment returns 0 affected rows: %llu", affected_rows); + diag("CREATE TABLE with comment executed successfully"); + + // Test 10: Insert data and run SELECT to verify normal operation + if (mysql_query(admin, "INSERT INTO test_table_4855 (id) VALUES (1), (2), (3)")) { + diag("Failed to execute INSERT with multiple values: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 3, "INSERT with multiple values returns 3 affected rows: %llu", affected_rows); + diag("INSERT with multiple values executed successfully"); + + // Test 11: Run VACUUM - should return 0 affected rows + if (mysql_query(admin, "VACUUM")) { + diag("Failed to execute VACUUM query: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 0, "VACUUM returns 0 affected rows: %llu", affected_rows); + diag("VACUUM query executed successfully"); + + // Test 12: Clean up - DROP remaining tables individually + if (mysql_query(admin, "DROP TABLE IF EXISTS test_table_4855")) { + diag("Failed to execute final DROP TABLE: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + + affected_rows = mysql_affected_rows(admin); + ok(affected_rows == 0, "Final DROP TABLE returns 0 affected rows: %llu", affected_rows); + diag("Final DROP TABLE query executed successfully"); + + // Additional cleanup without test assertion + if (mysql_query(admin, "DROP TABLE IF EXISTS test_table_4855_2")) { + diag("Failed to execute second DROP TABLE: %s", mysql_error(admin)); + mysql_close(admin); + return exit_status(); + } + diag("Additional cleanup DROP TABLE executed"); + mysql_close(admin); + return exit_status(); +} \ No newline at end of file