/** * @file reg_test_5233_set_warning-t.cpp * @brief Test for issue #5233: "Unable to parse multi-statements command with SET statement" * @details Tests that UPDATE statements with SET clauses don't trigger false SET statement warnings * * The issue is that queries like: * UPDATE setting SET value = '3.5' WHERE setting_id = 'foo'; SELECT ROW_COUNT(); * Trigger warning: "Unable to parse multi-statements command with SET statement" * * This happens because the code checks if digest_text starts with "SET ", but for * UPDATE statements, the digest text might start with "SET " after normalization. */ #include #include #include #include #include #include #include #include #include "mysql.h" #include "tap.h" #include "command_line.h" #include "utils.h" using namespace std; /** * @brief Get the digest text for a query from stats_mysql_query_digest * @param admin MySQL connection to admin interface * @param digest_text_to_find The digest text to look for * @return The actual digest text found, or empty string if not found */ string get_digest_text_from_stats(MYSQL* admin, const string& partial_digest) { string query = "SELECT digest_text FROM stats_mysql_query_digest WHERE digest_text LIKE '%" + partial_digest + "%'"; if (mysql_query(admin, query.c_str())) { diag("Failed to query stats_mysql_query_digest: %s", mysql_error(admin)); return ""; } MYSQL_RES* res = mysql_store_result(admin); if (!res) { diag("Failed to store result for digest text query"); return ""; } string found_digest = ""; MYSQL_ROW row; // consume all rows until NULL is returned while ((row = mysql_fetch_row(res))) { found_digest = row[0] ? row[0] : ""; } mysql_free_result(res); return found_digest; } int main(int argc, char** argv) { // Plan: 3 tests // 1. Test that warning appears (confirming bug exists) // 2. Check digest text in stats table // 3. Test with actual SET statement for comparison plan(3); CommandLine cl; if (cl.getEnv()) return exit_status(); // Get connections MYSQL* admin = mysql_init(NULL); if (!admin) { diag("Failed to initialize admin connection"); return exit_status(); } 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(); } // Connect to MySQL proxy interface with multi-statements enabled MYSQL* proxy = mysql_init(NULL); if (!proxy) { diag("Failed to initialize proxy connection"); mysql_close(admin); return exit_status(); } // Enable multi-statements if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, "test", cl.port, NULL, CLIENT_MULTI_STATEMENTS)) { diag("Failed to connect to ProxySQL proxy: %s", mysql_error(proxy)); mysql_close(admin); return exit_status(); } // Get the log file path const string log_path { get_env("REGULAR_INFRA_DATADIR") + "/proxysql.log" }; diag("Using log file: %s", log_path.c_str()); // Create test database and table MYSQL_QUERY(proxy, "CREATE DATABASE IF NOT EXISTS test"); MYSQL_QUERY(proxy, "USE test"); MYSQL_QUERY(proxy, "CREATE TABLE IF NOT EXISTS setting (setting_id VARCHAR(100) PRIMARY KEY, value VARCHAR(100))"); MYSQL_QUERY(proxy, "INSERT IGNORE INTO setting (setting_id, value) VALUES ('foo', '1.0')"); // Clear stats to start fresh MYSQL_QUERY(admin, "SELECT * FROM stats_mysql_query_digest_reset"); MYSQL_RES* result = mysql_store_result(admin); if (result) { mysql_free_result(result); } // Test 1: Execute the problematic UPDATE statement with multi-statement diag("Test 1: Executing problematic UPDATE statement with multi-statement"); const string test_query = "UPDATE setting SET value = '3.5' WHERE setting_id = 'foo'; SELECT ROW_COUNT()"; int rc = mysql_query(proxy, test_query.c_str()); bool query_executed = (rc == 0); if (!query_executed) { diag("Query execution failed: %s", mysql_error(proxy)); // Continue anyway - warning might still be logged during parsing } else { // Consume all results do { MYSQL_RES* result = mysql_store_result(proxy); if (result) { mysql_free_result(result); } } while (mysql_next_result(proxy) == 0); } // Wait for warning to be written to log usleep(500000); // 500ms // Check for the warning in the log const string warning_regex { ".*Unable to parse multi-statements command with SET statement.*" }; const auto& [match_count, warning_lines] = get_matching_lines_from_filename(log_path, warning_regex, true, 50); // This is the bug: warning should NOT appear for UPDATE statements // But currently it does, so we expect match_count > 0 bool warning_found = (match_count > 0); ok(!warning_found, "UPDATE statement does NOT trigger SET warning - match_count: %zu", match_count); if (warning_found && match_count > 0) { for (const auto& match : warning_lines) { const string& line = get<1>(match); diag("Found warning: %s", line.c_str()); } } // Test 2: Check digest text in stats table diag("Test 2: Checking digest text in stats table"); // Wait a bit for stats to be updated usleep(200000); // 200ms // Get digest text for the UPDATE query string digest_text = get_digest_text_from_stats(admin, "setting"); if (warning_found) { if (!digest_text.empty()) { diag("Found digest text: %s", digest_text.c_str()); // Check if digest text starts with "SET " bool starts_with_set = (digest_text.size() >= 4 && (digest_text[0] == 'S' || digest_text[0] == 's') && (digest_text[1] == 'E' || digest_text[1] == 'e') && (digest_text[2] == 'T' || digest_text[2] == 't') && digest_text[3] == ' '); ok(!starts_with_set, "Digest text does NOT starts with 'SET '"); if (starts_with_set) { diag("BUG CONFIRMED: UPDATE statement digest text starts with 'SET ': '%s'", digest_text.c_str()); } else { diag("Digest text doesn't start with 'SET ', something else is causing the warning"); } } else { diag("Could not find digest text in stats table"); ok(0, "Could not find digest text in stats table"); } } else { diag("UPDATE statement did NOT trigger SET warning, not checking digest"); ok(1, "UPDATE statement did NOT trigger SET warning, not checking digest"); } // Test 3: Execute actual SET statement for comparison diag("Test 3: Executing actual SET statement for comparison"); // Execute a real multi-statement SET command const string set_query = "SET @test_var = 1; SELECT 1"; rc = mysql_query(proxy, set_query.c_str()); if (rc == 0) { // Consume results do { MYSQL_RES* result = mysql_store_result(proxy); if (result) { mysql_free_result(result); } } while (mysql_next_result(proxy) == 0); } // Wait for warning usleep(500000); // Check for warning again const auto& [set_match_count, set_warning_lines] = get_matching_lines_from_filename(log_path, warning_regex, true, 50); bool set_warning_found = (set_match_count > match_count); // Should have new warnings ok(set_warning_found, "Actual SET statement also triggers warning (expected) - new matches: %zu", set_match_count - match_count); // Cleanup MYSQL_QUERY(proxy, "DROP TABLE IF EXISTS test.setting"); mysql_close(proxy); mysql_close(admin); return exit_status(); }