From 05960b5ddb026763719a37bd66539a350fcd2fad Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Thu, 27 Nov 2025 18:05:05 +0000 Subject: [PATCH] Fix issue 4855: Reset affected_rows to 0 for DDL queries in Admin interface Problem: When executing DDL queries (CREATE TABLE, DROP TABLE, VACUUM, etc.) in the ProxySQL Admin interface after DML operations, the affected rows count from the previous DML operation was incorrectly reported instead of 0. This is because SQLite's sqlite3_changes() function doesn't reset the counter for DDL statements. Root Cause: SQLite's sqlite3_changes() returns the number of rows affected by the most recent INSERT, UPDATE, or DELETE statement. For DDL statements that don't modify rows, SQLite doesn't reset this counter, so it continues to return the value from the last DML operation. Solution: - Added is_ddl_query_without_row_changes() function to identify DDL queries that don't affect row counts - Modified both execute_statement() and execute_statement_raw() in SQLite3DB to return 0 for affected_rows when executing DDL queries - The fix ensures that affected_rows is reset to 0 for: CREATE, DROP, ALTER, TRUNCATE, VACUUM, REINDEX, ANALYZE, CHECKPOINT, PRAGMA, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE, EXPLAIN Testing: - Created and ran comprehensive tests for DDL detection function - Verified build completes successfully - Confirmed the fix correctly identifies DDL vs DML queries Impact: This fix resolves the issue where Admin interface incorrectly shows affected rows for DDL operations, improving the accuracy and reliability of the ProxySQL Admin interface. Fixes #4855 --- lib/sqlite3db.cpp | 61 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/lib/sqlite3db.cpp b/lib/sqlite3db.cpp index 3acae23c0..7ba83cc8c 100644 --- a/lib/sqlite3db.cpp +++ b/lib/sqlite3db.cpp @@ -17,6 +17,13 @@ #define USLEEP_SQLITE_LOCKED 100 +/** + * @brief Check if a SQL statement is a DDL query that doesn't affect rows + * + * @param query The SQL statement to check + * @return True if the statement is a DDL query that doesn't affect rows + */ +static bool is_ddl_query_without_row_changes(const char *query); /** * @brief Constructor for the SQLite3_column class. @@ -353,7 +360,14 @@ 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); + int changes = (*proxy_sqlite3_changes)(db); + // For DDL queries that don't affect rows, reset affected_rows to 0 + // to prevent incorrect reporting of previous DML affected rows + if (is_ddl_query_without_row_changes(str)) { + *affected_rows = 0; + } else { + *affected_rows = changes; + } ret=true; } else { *error=strdup((*proxy_sqlite3_errmsg)(db)); @@ -370,9 +384,43 @@ __exit_execute_statement: return ret; } +/** + * @brief Check if a SQL statement is a DDL query that doesn't affect rows + * + * @param query The SQL statement to check + * @return True if the statement is a DDL query that doesn't affect rows + */ +static bool is_ddl_query_without_row_changes(const char *query) { + if (!query) return false; + + // Skip leading whitespace + while (isspace(*query)) { + query++; + } + + // Check for DDL statements that don't affect row counts + return ( + (strncasecmp(query, "CREATE", 6) == 0) || + (strncasecmp(query, "DROP", 4) == 0) || + (strncasecmp(query, "ALTER", 5) == 0) || + (strncasecmp(query, "TRUNCATE", 8) == 0) || + (strncasecmp(query, "VACUUM", 6) == 0) || + (strncasecmp(query, "REINDEX", 7) == 0) || + (strncasecmp(query, "ANALYZE", 7) == 0) || + (strncasecmp(query, "CHECKPOINT", 10) == 0) || + (strncasecmp(query, "PRAGMA", 6) == 0) || + (strncasecmp(query, "BEGIN", 5) == 0) || + (strncasecmp(query, "COMMIT", 6) == 0) || + (strncasecmp(query, "ROLLBACK", 8) == 0) || + (strncasecmp(query, "SAVEPOINT", 9) == 0) || + (strncasecmp(query, "RELEASE", 7) == 0) || + (strncasecmp(query, "EXPLAIN", 7) == 0) + ); +} + /** * @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. @@ -401,7 +449,14 @@ 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); + int changes = (*proxy_sqlite3_changes)(db); + // For DDL queries that don't affect rows, reset affected_rows to 0 + // to prevent incorrect reporting of previous DML affected rows + if (is_ddl_query_without_row_changes(str)) { + *affected_rows = 0; + } else { + *affected_rows = changes; + } ret=true; } else { *error=strdup((*proxy_sqlite3_errmsg)(db));