/** * @file pgsql-set_statement_test-t.cpp * @brief Intention: not to test every PostgreSQL variable, but to ensure different forms of SET statements * are parsed correctly and not wrongly locked on a hostgroup. Covers common syntaxes (`=`, TO, * multi-word params, aliases) and checks that unsupported forms like LOCAL or function-style values * trigger the expected hostgroup lock behavior. */ #include #include #include #include #include #include #include #include #include #include "libpq-fe.h" #include "command_line.h" #include "noise_utils.h" #include "tap.h" #include "utils.h" CommandLine cl; using PGConnPtr = std::unique_ptr; enum ConnType { ADMIN, BACKEND }; PGConnPtr createNewConnection(ConnType conn_type, const std::string& options = "", bool with_ssl = false) { const char* host = (conn_type == BACKEND) ? cl.pgsql_host : cl.pgsql_admin_host; int port = (conn_type == BACKEND) ? cl.pgsql_port : cl.pgsql_admin_port; const char* username = (conn_type == BACKEND) ? cl.pgsql_root_username : cl.admin_username; const char* password = (conn_type == BACKEND) ? cl.pgsql_root_password : cl.admin_password; std::stringstream ss; ss << "host=" << host << " port=" << port; ss << " user=" << username << " password=" << password; ss << (with_ssl ? " sslmode=require" : " sslmode=disable"); if (options.empty() == false) { ss << " options='" << options << "'"; } std::string conninfo = ss.str(); // Mask password for logging std::string conninfo_display = conninfo; size_t pwd_pos = conninfo_display.find("password="); if (pwd_pos != std::string::npos) { size_t pwd_end = conninfo_display.find(" ", pwd_pos); if (pwd_end == std::string::npos) pwd_end = conninfo_display.length(); conninfo_display.replace(pwd_pos + 9, pwd_end - (pwd_pos + 9), "***"); } diag("Connection string: %s", conninfo_display.c_str()); PGconn* conn = PQconnectdb(conninfo.c_str()); if (PQstatus(conn) != CONNECTION_OK) { fprintf(stderr, "Connection failed to '%s': %s", (conn_type == BACKEND ? "Backend" : "Admin"), PQerrorMessage(conn)); PQfinish(conn); return PGConnPtr(nullptr, &PQfinish); } return PGConnPtr(conn, &PQfinish); } struct TestCase { std::string sql; bool should_not_lock_on_hostgroup; std::string description; }; std::fstream f_proxysql_log{}; bool check_logs_for_command(const std::string& command_regex) { const auto& [_, cmd_lines] { get_matching_lines(f_proxysql_log, command_regex) }; return !cmd_lines.empty(); } bool run_set_statement(const std::string& stmt, ConnType type = BACKEND) { PGConnPtr conn = createNewConnection(type); if (!conn) return false; PGresult* res = PQexec(conn.get(), stmt.c_str()); if (PQresultStatus(res) != PGRES_COMMAND_OK) { PQclear(res); return false; } PQclear(res); return check_logs_for_command(".*\\[WARNING\\] Unable to parse unknown SET query from client.*") == false; } // Simple command execution that just checks if query succeeded bool run_command(const std::string& stmt, ConnType type = BACKEND) { PGConnPtr conn = createNewConnection(type); if (!conn) return false; PGresult* res = PQexec(conn.get(), stmt.c_str()); bool ok = (PQresultStatus(res) == PGRES_COMMAND_OK); PQclear(res); return ok; } // Structure to hold variable test data struct PipelineTestVariable { std::string name; std::string test_value; std::string initial_value; }; // Helper function to get variable value via simple query std::string get_variable_simple(PGconn* conn, const std::string& var_name) { std::string query = "SHOW " + var_name; PGresult* res = PQexec(conn, query.c_str()); if (!res || PQresultStatus(res) != PGRES_TUPLES_OK) { PQclear(res); return ""; } char* val = PQgetvalue(res, 0, 0); std::string result = val ? val : ""; PQclear(res); return result; } // Helper function to set variable via simple query (no verification) bool set_variable_simple(PGconn* conn, const std::string& var_name, const std::string& value) { std::string query = "SET " + var_name + " = '" + value + "'"; PGresult* res = PQexec(conn, query.c_str()); bool ok = (res && PQresultStatus(res) == PGRES_COMMAND_OK); PQclear(res); return ok; } // Test: SET variables in simple query, verify in pipeline mode bool test_set_simple_verify_pipeline() { diag("=== Test: SET in simple query, verify in pipeline ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) { diag("Failed to create connection"); return false; } // Test variables: mix of critical and non-critical // Values will be set to DIFFERENT from original std::vector test_vars = { {"DateStyle", "", ""}, {"TimeZone", "", ""}, {"bytea_output", "", ""}, {"extra_float_digits", "", ""} }; // Get original values and choose DIFFERENT test values for (auto& var : test_vars) { var.initial_value = get_variable_simple(conn.get(), var.name); // Choose value DIFFERENT from original if (var.name == "DateStyle") { var.test_value = (var.initial_value.find("ISO") != std::string::npos) ? "Postgres, DMY" : "ISO, MDY"; } else if (var.name == "TimeZone") { var.test_value = (var.initial_value.find("UTC") != std::string::npos) ? "PST8PDT" : "UTC"; } else if (var.name == "bytea_output") { var.test_value = (var.initial_value == "hex") ? "escape" : "hex"; } else if (var.name == "extra_float_digits") { var.test_value = (var.initial_value == "0") ? "3" : "0"; } diag("%s: original='%s', will SET to='%s'", var.name.c_str(), var.initial_value.c_str(), var.test_value.c_str()); } // Phase 1: Get initial values and SET new values via simple query for (auto& var : test_vars) { var.initial_value = get_variable_simple(conn.get(), var.name); diag("Initial %s: %s", var.name.c_str(), var.initial_value.c_str()); if (!set_variable_simple(conn.get(), var.name, var.test_value)) { diag("Failed to SET %s", var.name.c_str()); return false; } diag("SET %s = '%s'", var.name.c_str(), var.test_value.c_str()); } // Phase 2: Enter pipeline and SHOW variables if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } for (const auto& var : test_vars) { std::string query = "SHOW " + var.name; if (PQsendQueryParams(conn.get(), query.c_str(), 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SHOW %s in pipeline", var.name.c_str()); return false; } } PQpipelineSync(conn.get()); PQflush(conn.get()); // Phase 3: Consume results and verify // Use a longer timeout and more robust result consumption int expected_results = test_vars.size(); std::map results; int count = 0; int result_idx = 0; int sock = PQsocket(conn.get()); if (sock < 0) { diag("Invalid socket descriptor from PQsocket"); return false; } PGresult* res; time_t start_time = time(NULL); const int max_wait_seconds = 30; // Increased timeout while (count < expected_results + 1) { if (PQconsumeInput(conn.get()) == 0) { diag("PQconsumeInput failed: %s", PQerrorMessage(conn.get())); break; } bool got_result = false; while ((res = PQgetResult(conn.get())) != NULL) { got_result = true; ExecStatusType status = PQresultStatus(res); if (status == PGRES_TUPLES_OK && PQntuples(res) > 0) { char* val = PQgetvalue(res, 0, 0); if (val && result_idx < (int)test_vars.size()) { results[test_vars[result_idx].name] = val; result_idx++; diag("Got result %d/%d for %s: %s", result_idx, (int)test_vars.size(), test_vars[result_idx-1].name.c_str(), val); } } if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; // Keep consuming results, don't break } PQclear(res); count++; } if (count >= expected_results + 1) break; // Check for timeout if (time(NULL) - start_time > max_wait_seconds) { diag("Timeout waiting for results after %d seconds", max_wait_seconds); break; } // If we got results, continue immediately; otherwise wait for more data if (got_result) continue; // Only wait if libpq reports it's busy waiting for more data if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {1, 0}; // 1 second poll int sel = select(sock + 1, &input_mask, NULL, NULL, &timeout); if (sel < 0) { diag("select() failed"); break; } // Continue loop to call PQconsumeInput even on timeout } PQexitPipelineMode(conn.get()); // Verify results bool success = true; for (const auto& var : test_vars) { auto it = results.find(var.name); if (it == results.end()) { diag("No result for %s", var.name.c_str()); success = false; continue; } std::string pipeline_value = it->second; bool matches = (pipeline_value.find(var.test_value) != std::string::npos) || (var.test_value.find(pipeline_value) != std::string::npos); if (!matches) { diag("MISMATCH for %s: expected '%s', got '%s'", var.name.c_str(), var.test_value.c_str(), pipeline_value.c_str()); success = false; } else { diag("MATCH for %s: '%s' == '%s'", var.name.c_str(), var.test_value.c_str(), pipeline_value.c_str()); } } return success; } // Test: SET multiple critical variables, verify in pipeline bool test_multiple_critical_vars_pipeline() { diag("=== Test: Multiple critical variables in pipeline ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Critical variables - will use values DIFFERENT from original std::vector critical_vars = { {"client_encoding", "", ""}, {"DateStyle", "", ""}, {"IntervalStyle", "", ""}, {"standard_conforming_strings", "", ""} }; // Get original values and choose DIFFERENT test values for (auto& var : critical_vars) { var.initial_value = get_variable_simple(conn.get(), var.name); // Choose value DIFFERENT from original if (var.name == "client_encoding") { var.test_value = (var.initial_value == "UTF8") ? "LATIN1" : "UTF8"; } else if (var.name == "DateStyle") { var.test_value = (var.initial_value.find("ISO") != std::string::npos) ? "Postgres, MDY" : "ISO, DMY"; } else if (var.name == "IntervalStyle") { var.test_value = (var.initial_value == "postgres") ? "iso_8601" : "postgres"; } else if (var.name == "standard_conforming_strings") { var.test_value = (var.initial_value == "on") ? "off" : "on"; } diag("%s: original='%s', will SET to='%s'", var.name.c_str(), var.initial_value.c_str(), var.test_value.c_str()); } // Phase 1: SET all via simple query for (auto& var : critical_vars) { var.initial_value = get_variable_simple(conn.get(), var.name); if (!set_variable_simple(conn.get(), var.name, var.test_value)) { diag("Failed to SET %s", var.name.c_str()); return false; } } // Phase 2: Verify all in pipeline if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } for (const auto& var : critical_vars) { std::string query = "SHOW " + var.name; PQsendQueryParams(conn.get(), query.c_str(), 0, NULL, NULL, NULL, NULL, 0); } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int expected = critical_vars.size(); std::map results; int count = 0; int idx = 0; int sock = PQsocket(conn.get()); PGresult* res; while (count < expected + 1) { if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { if (PQresultStatus(res) == PGRES_TUPLES_OK && PQntuples(res) > 0) { char* val = PQgetvalue(res, 0, 0); if (val && idx < (int)critical_vars.size()) { results[critical_vars[idx].name] = val; idx++; } } if (PQresultStatus(res) == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; // Keep consuming results, don't break } PQclear(res); count++; } if (count >= expected + 1) break; // Only wait if libpq reports it's busy waiting for more data if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify bool success = true; for (const auto& var : critical_vars) { auto it = results.find(var.name); if (it == results.end()) { success = false; continue; } std::string val = it->second; // For standard_conforming_strings, check contains bool matches = (val.find(var.test_value) != std::string::npos) || (var.test_value.find(val) != std::string::npos) || (val == var.test_value); if (!matches) { diag("MISMATCH for %s: expected '%s', got '%s'", var.name.c_str(), var.test_value.c_str(), val.c_str()); success = false; } } return success; } // Phase 3: SET in Pipeline Mode - Send SET commands via extended query in pipeline bool test_set_in_pipeline_mode() { diag("=== Test: SET commands in pipeline mode ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Get initial values and choose DIFFERENT test values std::vector test_vars = { {"DateStyle", "", ""}, {"TimeZone", "", ""}, {"bytea_output", "", ""} }; for (auto& var : test_vars) { var.initial_value = get_variable_simple(conn.get(), var.name); // Choose value DIFFERENT from original if (var.name == "DateStyle") { var.test_value = (var.initial_value.find("ISO") != std::string::npos) ? "SQL, DMY" : "ISO, MDY"; } else if (var.name == "TimeZone") { var.test_value = (var.initial_value.find("UTC") != std::string::npos) ? "EST5EDT" : "UTC"; } else if (var.name == "bytea_output") { var.test_value = (var.initial_value == "hex") ? "escape" : "hex"; } diag("%s: original='%s', will SET to='%s'", var.name.c_str(), var.initial_value.c_str(), var.test_value.c_str()); } // Enter pipeline mode FIRST if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send SET commands in pipeline for (const auto& var : test_vars) { std::string query = "SET " + var.name + " = '" + var.test_value + "'"; if (PQsendQueryParams(conn.get(), query.c_str(), 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET %s in pipeline", var.name.c_str()); return false; } } // Send SHOW commands to verify in same pipeline for (const auto& var : test_vars) { std::string query = "SHOW " + var.name; if (PQsendQueryParams(conn.get(), query.c_str(), 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SHOW %s in pipeline", var.name.c_str()); return false; } } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int set_count = 0; int show_count = 0; int expected = test_vars.size() * 2; // SET + SHOW for each int count = 0; int sock = PQsocket(conn.get()); PGresult* res; std::map results; int result_idx = 0; while (count < expected + 1) { if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); if (status == PGRES_COMMAND_OK) { set_count++; } else if (status == PGRES_TUPLES_OK && PQntuples(res) > 0) { char* val = PQgetvalue(res, 0, 0); if (val && result_idx < (int)test_vars.size()) { results[test_vars[result_idx].name] = val; result_idx++; } show_count++; } else if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; // Keep consuming results, don't break } PQclear(res); count++; } if (count >= expected + 1) break; // Only wait if libpq reports it's busy waiting for more data if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify SET worked within pipeline bool success = true; for (const auto& var : test_vars) { auto it = results.find(var.name); if (it == results.end()) { diag("No result for %s", var.name.c_str()); success = false; continue; } std::string pipeline_value = it->second; bool matches = (pipeline_value.find(var.test_value) != std::string::npos) || (var.test_value.find(pipeline_value) != std::string::npos); if (!matches) { diag("MISMATCH for %s: expected '%s', got '%s'", var.name.c_str(), var.test_value.c_str(), pipeline_value.c_str()); success = false; } } diag("SET commands completed: %d, SHOW commands completed: %d", set_count, show_count); return success && (set_count >= (int)test_vars.size()) && (show_count >= (int)test_vars.size()); } // Phase 5: SET then query using variable - verify query respects SET value bool test_set_then_query_pipeline() { diag("=== Test: SET DateStyle then query in pipeline ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Get initial DateStyle and choose DIFFERENT value std::string orig_datestyle = get_variable_simple(conn.get(), "DateStyle"); // Choose SQL, DMY if current is ISO, otherwise choose ISO std::string new_datestyle = (orig_datestyle.find("ISO") != std::string::npos) ? "SQL, DMY" : "ISO, MDY"; diag("DateStyle: original='%s', will SET to='%s'", orig_datestyle.c_str(), new_datestyle.c_str()); // Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send SET DateStyle with NEW value (different from original) std::string set_query = "SET DateStyle = '" + new_datestyle + "'"; if (PQsendQueryParams(conn.get(), set_query.c_str(), 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET DateStyle"); return false; } // Send a query that uses dates - the output format should respect DateStyle if (PQsendQueryParams(conn.get(), "SELECT '2024-03-15'::date", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SELECT"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int count = 0; int cmd_count = 0; std::string date_result; int sock = PQsocket(conn.get()); PGresult* res; while (count < 3) { // 2 commands + 1 sync if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); if (status == PGRES_COMMAND_OK) { cmd_count++; } else if (status == PGRES_TUPLES_OK && PQntuples(res) > 0) { char* val = PQgetvalue(res, 0, 0); if (val) date_result = val; cmd_count++; } else if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; // Keep consuming results, don't break } PQclear(res); count++; } if (count >= 3) break; // Only wait if libpq reports it's busy waiting for more data if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); diag("Date result: '%s'", date_result.c_str()); // Verify format changed based on what we SET bool format_changed = false; if (new_datestyle.find("SQL") != std::string::npos) { // SQL, DMY format should be like "15/03/2024" (day/month/year) format_changed = (date_result.find("/") != std::string::npos) || (date_result.find("15") == 0); // Starts with day } else { // ISO format would be "2024-03-15" format_changed = (date_result.find("-") != std::string::npos) || (date_result.find("2024") == 0); // Starts with year } // Cleanup - restore original set_variable_simple(conn.get(), "DateStyle", orig_datestyle); return (cmd_count >= 2) && format_changed; } // Test: RESET ALL should fail in pipeline mode (not supported) bool test_reset_all_failure_pipeline() { diag("=== Test: RESET ALL failure in pipeline mode ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send RESET ALL if (PQsendQueryParams(conn.get(), "RESET ALL", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send RESET ALL"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int count = 0; bool got_error = false; std::string error_msg; int sock = PQsocket(conn.get()); PGresult* res; while (count < 2) { // 1 command + 1 sync if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); if (status == PGRES_FATAL_ERROR) { got_error = true; error_msg = PQresultErrorMessage(res); diag("Got expected error: %s", error_msg.c_str()); } if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; } PQclear(res); count++; } if (count >= 2) break; if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify error message mentions pipeline mode bool correct_error = (error_msg.find("pipeline") != std::string::npos) || (error_msg.find("not supported") != std::string::npos); return got_error && correct_error; } // Test: DISCARD ALL should fail in pipeline mode (not supported) bool test_discard_all_failure_pipeline() { diag("=== Test: DISCARD ALL failure in pipeline mode ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send DISCARD ALL if (PQsendQueryParams(conn.get(), "DISCARD ALL", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send DISCARD ALL"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int count = 0; bool got_error = false; std::string error_msg; int sock = PQsocket(conn.get()); PGresult* res; while (count < 2) { // 1 command + 1 sync if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); if (status == PGRES_FATAL_ERROR) { got_error = true; error_msg = PQresultErrorMessage(res); diag("Got expected error: %s", error_msg.c_str()); } if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; } PQclear(res); count++; } if (count >= 2) break; if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify error message mentions pipeline mode bool correct_error = (error_msg.find("pipeline") != std::string::npos) || (error_msg.find("not supported") != std::string::npos); return got_error && correct_error; } // Test: Verify startup parameters are applied bool test_startup_parameters() { diag("=== Test: Verify startup parameters ==="); // Create connection with explicit startup parameter (space must be escaped with double backslash) const std::string startup_value = "SQL,\\\\ DMY"; // C++: \\ -> actual: \ -> libpq sees: escaped space PGConnPtr conn = createNewConnection(BACKEND, "-c DateStyle=" + startup_value); if (!conn) { diag("Failed to create connection"); return false; } // Check connection status if (PQstatus(conn.get()) != CONNECTION_OK) { diag("Connection not OK: %s", PQerrorMessage(conn.get())); return false; } // Get the actual value from backend PGresult* res = PQexec(conn.get(), "SHOW DateStyle"); if (PQresultStatus(res) != PGRES_TUPLES_OK) { diag("SHOW failed: %s", PQresultErrorMessage(res)); PQclear(res); return false; } if (PQntuples(res) == 0) { diag("SHOW returned no rows"); PQclear(res); return false; } char* val = PQgetvalue(res, 0, 0); std::string actual_value = val ? val : ""; diag("Startup parameter set to: '%s'", startup_value.c_str()); diag("Actual DateStyle from backend: '%s'", actual_value.c_str()); PQclear(res); // In ProxySQL, startup parameters might not be forwarded to backend // This test documents the current behavior bool startup_applied = (actual_value.find("SQL") != std::string::npos); diag("Startup parameter applied: %s", startup_applied ? "yes" : "no (pooled connection used)"); // For this test, we just verify we can read the value, not that startup params work return !actual_value.empty(); } // Test: RESET single variable should work in pipeline mode bool test_reset_single_var_pipeline() { diag("=== Test: RESET single variable in pipeline mode ==="); // Note: Connection pooling may interfere with explicit startup parameters. // We test RESET behavior by: // 1. Get current value // 2. SET to a different value // 3. RESET in pipeline mode // 4. Verify value changed (not necessarily to original) PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Get current value (whatever the pooled connection has) const std::string initial_value = get_variable_simple(conn.get(), "DateStyle"); diag("Initial DateStyle: '%s'", initial_value.c_str()); // Choose a test value different from current std::string test_value = (initial_value.find("Postgres") != std::string::npos) ? "SQL, DMY" : "Postgres, DMY"; if (!set_variable_simple(conn.get(), "DateStyle", test_value)) { diag("Failed to SET DateStyle"); return false; } std::string set_value = get_variable_simple(conn.get(), "DateStyle"); diag("After SET: DateStyle = '%s'", set_value.c_str()); // Verify value was actually changed if (set_value == initial_value) { diag("SET did not change the value - test cannot proceed"); return false; } // Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send RESET DateStyle if (PQsendQueryParams(conn.get(), "RESET DateStyle", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send RESET DateStyle"); return false; } // Send SHOW DateStyle to verify reset if (PQsendQueryParams(conn.get(), "SHOW DateStyle", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SHOW DateStyle"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int cmd_count = 0; int show_count = 0; std::string reset_result; int count = 0; int sock = PQsocket(conn.get()); PGresult* res; while (count < 3) { // 2 commands + 1 sync if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); diag("Got result: status=%d (%s), ntuples=%d", status, PQresStatus(status), PQntuples(res)); if (status == PGRES_COMMAND_OK) { cmd_count++; diag("RESET command succeeded"); } else if (status == PGRES_TUPLES_OK && PQntuples(res) > 0) { char* val = PQgetvalue(res, 0, 0); if (val) { reset_result = val; diag("After RESET: DateStyle = '%s'", reset_result.c_str()); } show_count++; } else if (status == PGRES_TUPLES_OK && PQntuples(res) == 0) { diag("SHOW returned 0 tuples"); } else if (status == PGRES_FATAL_ERROR) { diag("Error: %s", PQresultErrorMessage(res)); } else if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; } PQclear(res); count++; } if (count >= 3) break; if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Debug output diag("Results summary: cmd_count=%d, show_count=%d, total_count=%d", cmd_count, show_count, count); diag("reset_result='%s', initial_value='%s'", reset_result.c_str(), initial_value.c_str()); // Verify RESET worked - value should be different from what we SET // Note: Due to connection pooling, it may not exactly match initial_value, // but it should be different from set_value (proving RESET executed) bool reset_executed = (!reset_result.empty() && reset_result != set_value); diag("Value reset from '%s' to '%s': %s", set_value.c_str(), reset_result.c_str(), reset_executed ? "success (value changed)" : "failed"); return (cmd_count >= 1) && (show_count >= 1) && reset_executed; } // Test: RESET reverts to startup parameter value bool test_reset_reverts_to_startup_param() { diag("=== Test: RESET reverts to startup parameter value ==="); // Create connection with explicit startup parameter (space must be escaped with double backslash) const std::string startup_value = "SQL,\\\\ DMY"; // C++: \\ -> actual: \ -> libpq sees: escaped space PGConnPtr conn = createNewConnection(BACKEND, "-c DateStyle=" + startup_value); if (!conn) { diag("Failed to create connection with startup parameter"); return false; } // Get startup value (what we set in connection string) std::string actual_startup = get_variable_simple(conn.get(), "DateStyle"); diag("Startup parameter value: '%s'", startup_value.c_str()); diag("Actual DateStyle after connect: '%s'", actual_startup.c_str()); // Choose a different value for SET std::string new_value = (actual_startup.find("Postgres") != std::string::npos) ? "ISO, MDY" : "Postgres, DMY"; // SET to a different value if (!set_variable_simple(conn.get(), "DateStyle", new_value)) { diag("Failed to SET DateStyle to '%s'", new_value.c_str()); return false; } std::string after_set = get_variable_simple(conn.get(), "DateStyle"); diag("After SET: DateStyle = '%s'", after_set.c_str()); // Verify SET worked if (after_set == actual_startup) { diag("SET did not change the value"); return false; } // RESET the variable PGresult* res = PQexec(conn.get(), "RESET DateStyle"); if (PQresultStatus(res) != PGRES_COMMAND_OK) { diag("RESET failed: %s", PQresultErrorMessage(res)); PQclear(res); return false; } PQclear(res); // Get value after RESET std::string after_reset = get_variable_simple(conn.get(), "DateStyle"); diag("After RESET: DateStyle = '%s'", after_reset.c_str()); // Verify RESET reverted to startup value bool reverted = (after_reset == actual_startup); diag("RESET reverted to startup value: %s", reverted ? "yes" : "no"); return reverted; } // Forward declarations for pipeline mode tests bool test_set_simple_verify_pipeline(); bool test_multiple_critical_vars_pipeline(); bool test_set_in_pipeline_mode(); bool test_set_then_query_pipeline(); bool test_set_failure_invalid_value_pipeline(); bool test_set_failure_invalid_encoding_pipeline(); bool test_set_failure_syntax_error_pipeline(); bool test_set_failure_multiple_set_one_fails(); bool test_set_different_values_from_original(); bool test_reset_all_failure_pipeline(); bool test_discard_all_failure_pipeline(); bool test_reset_single_var_pipeline(); bool test_reset_simple_query(); bool test_reset_all_simple_query(); bool test_discard_all_simple_query(); bool test_multiple_vars_out_of_sync_pipeline(); bool test_pipeline_with_locked_hostgroup(); bool test_reset_all_locked_hostgroup_pipeline(); bool test_discard_all_locked_hostgroup_pipeline(); bool test_reset_reverts_to_startup_param(); int main(int argc, char** argv) { if (cl.getEnv()) return exit_status(); spawn_internal_noise(cl, internal_noise_mysql_traffic_v2, {{"num_connections", "100"}, {"reconnect_interval", "100"}, {"avg_delay_ms", "300"}}); spawn_internal_noise(cl, internal_noise_prometheus_poller); spawn_internal_noise(cl, internal_noise_rest_prometheus_poller, {{"enable_rest_api", "true"}}); std::vector tests = { // Standard param/value {"SET datestyle = 'ISO, MDY';", true, "datestyle with ="}, {"SET datestyle TO 'ISO,MDY';", true, "datestyle with TO"}, {"SET standard_conforming_strings TO on;", true, "boolean ON"}, {"SET enable_seqscan = off;", true, "boolean OFF"}, {"SET SESSION datestyle = 'ISO, DMY';", true, "SESSION prefix"}, // TIME ZONE {"SET TIME ZONE 'UTC';", true, "TIME ZONE UTC"}, {"SET TIME ZONE DEFAULT;", true, "TIME ZONE DEFAULT"}, {"SET TIME ZONE -7;", true, "TIME ZONE numeric offset"}, {"SET TIME ZONE INTERVAL '+02:30' HOUR TO MINUTE;", true, "TIME ZONE interval"}, // TRANSACTION ISOLATION LEVEL {"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;", false, "TX ISOLATION READ UNCOMMITTED"}, {"SET TRANSACTION ISOLATION LEVEL READ COMMITTED;", false, "TX ISOLATION READ COMMITTED"}, {"SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;", false, "TX ISOLATION REPEATABLE READ"}, {"SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;", false, "TX ISOLATION SERIALIZABLE"}, // XML OPTION {"SET XML OPTION DOCUMENT;", false, "XML OPTION DOCUMENT"}, {"SET XML OPTION CONTENT;", false, "XML OPTION CONTENT"}, // SESSION AUTHORIZATION {"SET SESSION AUTHORIZATION DEFAULT;", false, "SESSION AUTHORIZATION DEFAULT"}, // ROLE {"SET ROLE NONE;", false, "ROLE NONE"}, // SCHEMA {"SET SCHEMA 'pg_catalog';", false, "SCHEMA valid"}, // NAMES {"SET NAMES SQL_ASCII;", true, "NAMES SQL_ASCII"}, {"SET NAMES UTF8;", true, "NAMES UTF8"}, // SEARCH_PATH {"SET search_path TO 'pg_catalog';", true, "search_path single schema"}, {"SET search_path TO 'schema1, schema2';", true, "search_path multiple schemas"}, {"SET search_path TO '\"MySchema\"';", true, "search_path quoted identifier"}, {"SET search_path TO 'schema1, \"MySchema\"';", true, "search_path mixed identifiers"}, {"SET search_path TO 'schema1, pg_catalog';", true, "search_path with pg_catalog"}, {"SET search_path TO '$user, public';", true, "search_path with $user"}, {"SET search_path TO 'public, $user';", true, "search_path with $user at end"}, {"SET search_path TO 'public, $user, pg_catalog';", true, "search_path with $user and pg_catalog"}, {"SET search_path TO '\"$user\"';", true, "search_path with quoted $user"}, {"SET search_path TO '\"$user\", pg_catalog';", true, "search_path with quoted $user and pg_catalog"}, {"SET search_path TO '\"$user\", public';", true, "search_path with quoted $user and public"}, {"SET search_path TO 'public, \"$user\"';", true, "search_path with public and quoted $user"}, {"SET search_path TO '\"$user\", public, pg_catalog';", true, "search_path with quoted $user, public and pg_catalog"}, {"SET search_path = 'public, \"$user\", pg_catalog';", true, "search_path with public, quoted $user and pg_catalog"}, {"SET search_path = '\"MySchema\", pg_catalog';", true, "search_path with quoted identifier and pg_catalog"}, {"SET search_path = '\"MySchema\", public';", true, "search_path with quoted identifier and public"}, {"SET search_path = 'public, \"MySchema\"';", true, "search_path with public and quoted identifier"}, {"SET search_path = '\"MySchema\", public, pg_catalog';", true, "search_path with quoted identifier, public and pg_catalog"}, {"SET search_path = 'public, \"MySchema\", pg_catalog';", true, "search_path with public, quoted identifier and pg_catalog"}, {"SET search_path = 'schema1, \"MySchema\", schema2';", true, "search_path multiple mixed identifiers"}, {"SET search_path = ''; ", true, "search_path empty string"}, {"SET search_path = ' , , '; ", true, "search_path only commas and spaces"}, {"SET search_path = ',public,'; ", true, "search_path leading and trailing commas"}, // SEED {"SET SEED 0.5;", false, "SEED 0.5"}, {"SET SEED 0;", false, "SEED 0"}, {"SET SEED 1;", false, "SEED 1"}, {"SET SEED 1.5;", false, "SEED out of range"}, // Failure cases {"SET ALL TO DEFAULT;", false, "ALL should fail"}, {"SET LOCAL datestyle TO 'ISO,MDY';", false, "LOCAL should fail"}, {"SET search_path TO current_schemas(true);", false, "function value should fail"}, {"SET datestyle = ;", false, "missing value"} }; // Add pipeline tests to the plan (20 total: 16 pipeline + 4 simple query RESET/DISCARD) const int num_pipeline_tests = 20; if (cl.use_noise) { plan(tests.size() + num_pipeline_tests + 3); } else { plan(tests.size() + num_pipeline_tests); } std::string f_path{ get_env("REGULAR_INFRA_DATADIR") + "/proxysql.log" }; int of_err = open_file_and_seek_end(f_path, f_proxysql_log); if (of_err != EXIT_SUCCESS) { return exit_status(); } // Run existing simple query tests for (const auto& t : tests) { f_proxysql_log.clear(f_proxysql_log.rdstate() & ~std::ios_base::failbit); f_proxysql_log.seekg(f_proxysql_log.tellg()); bool result = run_set_statement(t.sql); ok(result == t.should_not_lock_on_hostgroup, "%s", t.description.c_str()); usleep(10000); } f_proxysql_log.close(); // Run RESET and DISCARD tests in simple query mode ok(test_reset_simple_query(), "RESET single variable in simple query mode"); ok(test_reset_all_simple_query(), "RESET ALL in simple query mode"); ok(test_discard_all_simple_query(), "DISCARD ALL in simple query mode"); ok(test_reset_reverts_to_startup_param(), "RESET reverts to startup parameter value"); // Run pipeline tests ok(test_set_simple_verify_pipeline(), "SET in simple query, verify in pipeline mode"); ok(test_multiple_critical_vars_pipeline(), "Multiple critical variables in pipeline mode"); ok(test_set_in_pipeline_mode(), "SET commands in pipeline mode"); ok(test_set_then_query_pipeline(), "SET DateStyle then query in pipeline mode"); // Run SET failure tests in pipeline mode ok(test_set_failure_invalid_value_pipeline(), "SET failure with invalid value in pipeline"); ok(test_set_failure_invalid_encoding_pipeline(), "SET failure with invalid encoding in pipeline"); ok(test_set_failure_syntax_error_pipeline(), "SET failure with syntax error in pipeline"); ok(test_set_failure_multiple_set_one_fails(), "Multiple SETs where one fails in pipeline"); ok(test_set_different_values_from_original(), "SET values different from original in pipeline"); // Run RESET and DISCARD tests in pipeline mode ok(test_reset_all_failure_pipeline(), "RESET ALL fails in pipeline mode"); ok(test_discard_all_failure_pipeline(), "DISCARD ALL fails in pipeline mode"); ok(test_reset_single_var_pipeline(), "RESET single variable works in pipeline mode"); ok(test_multiple_vars_out_of_sync_pipeline(), "Multiple variables out of sync in pipeline mode"); ok(test_pipeline_with_locked_hostgroup(), "SET/RESET/DISCARD with locked hostgroup in pipeline mode"); ok(test_reset_all_locked_hostgroup_pipeline(), "RESET ALL with locked hostgroup in pipeline mode"); ok(test_discard_all_locked_hostgroup_pipeline(), "DISCARD ALL with locked hostgroup in pipeline mode"); return exit_status(); } // Test: SET with invalid value should fail gracefully in pipeline bool test_set_failure_invalid_value_pipeline() { diag("=== Test: SET failure with invalid value in pipeline ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send SET with invalid DateStyle value if (PQsendQueryParams(conn.get(), "SET DateStyle = 'INVALID_STYLE'", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int count = 0; bool got_error = false; int sock = PQsocket(conn.get()); PGresult* res; while (count < 2) { // 1 command + 1 sync if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); if (status == PGRES_FATAL_ERROR) { got_error = true; diag("Got expected error: %s", PQresultErrorMessage(res)); } if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; // Keep consuming results, don't break } PQclear(res); count++; } if (count >= 2) break; // Only wait if libpq reports it's busy waiting for more data if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify connection is still usable after error PGresult* res2 = PQexec(conn.get(), "SELECT 1"); bool connection_ok = (PQresultStatus(res2) == PGRES_TUPLES_OK); PQclear(res2); return got_error && connection_ok; } // Test: SET invalid client_encoding should fail bool test_set_failure_invalid_encoding_pipeline() { diag("=== Test: SET failure with invalid encoding in pipeline ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Get original encoding first std::string orig_encoding = get_variable_simple(conn.get(), "client_encoding"); diag("Original client_encoding: %s", orig_encoding.c_str()); // Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send SET with invalid encoding if (PQsendQueryParams(conn.get(), "SET client_encoding = 'INVALID_ENCODING'", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int count = 0; bool got_error = false; int sock = PQsocket(conn.get()); PGresult* res; while (count < 2) { if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); if (status == PGRES_FATAL_ERROR) { got_error = true; diag("Got expected error for invalid encoding"); } if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; // Keep consuming results, don't break } PQclear(res); count++; } if (count >= 2) break; // Only wait if libpq reports it's busy waiting for more data if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify encoding is still original (not changed) std::string after_encoding = get_variable_simple(conn.get(), "client_encoding"); bool encoding_unchanged = (after_encoding == orig_encoding); diag("After error client_encoding: %s (unchanged: %s)", after_encoding.c_str(), encoding_unchanged ? "yes" : "no"); return got_error && encoding_unchanged; } // Test: SET syntax error should fail bool test_set_failure_syntax_error_pipeline() { diag("=== Test: SET failure with syntax error in pipeline ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send SET with syntax error (missing value) if (PQsendQueryParams(conn.get(), "SET DateStyle =", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int count = 0; bool got_error = false; int sock = PQsocket(conn.get()); PGresult* res; while (count < 2) { if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); if (status == PGRES_FATAL_ERROR) { got_error = true; diag("Got expected syntax error"); } if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; // Keep consuming results, don't break } PQclear(res); count++; } if (count >= 2) break; // Only wait if libpq reports it's busy waiting for more data if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); return got_error; } // Test: Multiple SETs where one fails - verify state consistency bool test_set_failure_multiple_set_one_fails() { diag("=== Test: Multiple SETs where one fails - state consistency ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Get original values std::string orig_datestyle = get_variable_simple(conn.get(), "DateStyle"); std::string orig_timezone = get_variable_simple(conn.get(), "TimeZone"); diag("Original DateStyle: %s, TimeZone: %s", orig_datestyle.c_str(), orig_timezone.c_str()); // Ensure we're using DIFFERENT values std::string new_datestyle = (orig_datestyle.find("ISO") != std::string::npos) ? "Postgres, DMY" : "ISO, MDY"; std::string new_timezone = (orig_timezone.find("UTC") != std::string::npos) ? "PST8PDT" : "UTC"; // Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send: valid SET, invalid SET, valid SET std::string set1 = "SET DateStyle = '" + new_datestyle + "'"; if (PQsendQueryParams(conn.get(), set1.c_str(), 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET 1"); return false; } if (PQsendQueryParams(conn.get(), "SET client_encoding = 'INVALID'", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET 2 (invalid)"); return false; } std::string set3 = "SET TimeZone = '" + new_timezone + "'"; if (PQsendQueryParams(conn.get(), set3.c_str(), 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET 3"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int count = 0; int error_count = 0; int success_count = 0; int sock = PQsocket(conn.get()); PGresult* res; while (count < 4) { // 3 commands + 1 sync if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); if (status == PGRES_COMMAND_OK) { success_count++; } else if (status == PGRES_FATAL_ERROR) { error_count++; diag("Got error: %s", PQresultErrorMessage(res)); } else if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; // Keep consuming results, don't break } PQclear(res); count++; } if (count >= 4) break; // Only wait if libpq reports it's busy waiting for more data if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Check results std::string final_datestyle = get_variable_simple(conn.get(), "DateStyle"); std::string final_timezone = get_variable_simple(conn.get(), "TimeZone"); diag("Final DateStyle: %s (expected: %s, original: %s)", final_datestyle.c_str(), new_datestyle.c_str(), orig_datestyle.c_str()); diag("Final TimeZone: %s (expected: %s, original: %s)", final_timezone.c_str(), new_timezone.c_str(), orig_timezone.c_str()); diag("Success count: %d, Error count: %d", success_count, error_count); // PostgreSQL behavior: When a command fails in pipeline, the entire pipeline aborts // This means first SET may be rolled back along with subsequent commands // We verify: // 1. At least one error occurred (the invalid SET) // 2. Connection is still usable // 3. Either first SET succeeded (if no abort) or was rolled back (if abort) bool got_expected_error = (error_count >= 1); bool first_set_succeeded = (final_datestyle.find(new_datestyle) != std::string::npos); bool first_set_rolled_back = (final_datestyle == orig_datestyle); // Connection should still be usable PGresult* test_res = PQexec(conn.get(), "SELECT 1"); bool connection_ok = (PQresultStatus(test_res) == PGRES_TUPLES_OK); PQclear(test_res); diag("Results: error=%s, first_set_succeeded=%s, first_set_rolled_back=%s, connection_ok=%s", got_expected_error ? "yes" : "no", first_set_succeeded ? "yes" : "no", first_set_rolled_back ? "yes" : "no", connection_ok ? "yes" : "no"); // Accept either behavior: // - If no abort: first SET succeeded // - If abort: first SET was rolled back (back to original) return got_expected_error && connection_ok && (first_set_succeeded || first_set_rolled_back); } // Test: Verify SET values are different from original bool test_set_different_values_from_original() { diag("=== Test: SET values different from original in pipeline ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Get original values std::vector vars = { {"DateStyle", "", ""}, {"TimeZone", "", ""}, {"bytea_output", "", ""}, {"standard_conforming_strings", "", ""} }; // Get originals and determine DIFFERENT test values for (auto& var : vars) { var.initial_value = get_variable_simple(conn.get(), var.name); diag("Original %s: %s", var.name.c_str(), var.initial_value.c_str()); // Choose value DIFFERENT from original if (var.name == "DateStyle") { var.test_value = (var.initial_value.find("ISO") != std::string::npos) ? "Postgres, DMY" : "ISO, MDY"; } else if (var.name == "TimeZone") { var.test_value = (var.initial_value.find("UTC") != std::string::npos) ? "PST8PDT" : "UTC"; } else if (var.name == "bytea_output") { var.test_value = (var.initial_value == "hex") ? "escape" : "hex"; } else if (var.name == "standard_conforming_strings") { var.test_value = (var.initial_value == "on") ? "off" : "on"; } diag("Will SET %s to: %s", var.name.c_str(), var.test_value.c_str()); } // Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send all SETs for (const auto& var : vars) { std::string query = "SET " + var.name + " = '" + var.test_value + "'"; if (PQsendQueryParams(conn.get(), query.c_str(), 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET %s", var.name.c_str()); return false; } } // Send all SHOWs for (const auto& var : vars) { std::string query = "SHOW " + var.name; if (PQsendQueryParams(conn.get(), query.c_str(), 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SHOW %s", var.name.c_str()); return false; } } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int set_count = 0; int show_count = 0; std::map results; int count = 0; int result_idx = 0; int sock = PQsocket(conn.get()); PGresult* res; while (count < vars.size() * 2 + 1) { if (PQconsumeInput(conn.get()) == 0) break; while ((res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(res); if (status == PGRES_COMMAND_OK) { set_count++; } else if (status == PGRES_TUPLES_OK && PQntuples(res) > 0) { char* val = PQgetvalue(res, 0, 0); if (val && result_idx < (int)vars.size()) { results[vars[result_idx].name] = val; result_idx++; } show_count++; } else if (status == PGRES_PIPELINE_SYNC) { PQclear(res); count++; continue; // Keep consuming results, don't break } PQclear(res); count++; } if (count >= (int)vars.size() * 2 + 1) break; // Only wait if libpq reports it's busy waiting for more data if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify all values changed bool all_changed = true; for (const auto& var : vars) { auto it = results.find(var.name); if (it == results.end()) { diag("No result for %s", var.name.c_str()); all_changed = false; continue; } std::string final_val = it->second; // Use exact equality to avoid false positives from substring matching bool changed = (final_val == var.test_value); // Also verify it's NOT the original bool is_original = (final_val == var.initial_value); diag("%s: original='%s', test='%s', final='%s', changed=%s, is_original=%s", var.name.c_str(), var.initial_value.c_str(), var.test_value.c_str(), final_val.c_str(), changed ? "yes" : "no", is_original ? "yes" : "no"); if (!changed || is_original) { diag("FAIL: %s did not change or still original!", var.name.c_str()); all_changed = false; } } return all_changed && (set_count >= (int)vars.size()) && (show_count >= (int)vars.size()); } // Test: RESET single variable in simple query mode bool test_reset_simple_query() { diag("=== Test: RESET single variable in simple query mode ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Get initial value std::string initial = get_variable_simple(conn.get(), "DateStyle"); diag("Initial DateStyle: %s", initial.c_str()); // SET to different value if (!set_variable_simple(conn.get(), "DateStyle", "Postgres, DMY")) { diag("Failed to SET DateStyle"); return false; } std::string after_set = get_variable_simple(conn.get(), "DateStyle"); diag("After SET: DateStyle = %s", after_set.c_str()); // RESET PGresult* res = PQexec(conn.get(), "RESET DateStyle"); if (PQresultStatus(res) != PGRES_COMMAND_OK) { diag("RESET failed: %s", PQresultErrorMessage(res)); PQclear(res); return false; } PQclear(res); // Verify it's back to initial std::string after_reset = get_variable_simple(conn.get(), "DateStyle"); diag("After RESET: DateStyle = %s", after_reset.c_str()); bool success = (after_reset == initial); diag("RESET %s: '%s' == '%s'", success ? "succeeded" : "failed", after_reset.c_str(), initial.c_str()); return success; } // Test: RESET ALL in simple query mode bool test_reset_all_simple_query() { diag("=== Test: RESET ALL in simple query mode ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Get initial values std::string initial_datestyle = get_variable_simple(conn.get(), "DateStyle"); std::string initial_timezone = get_variable_simple(conn.get(), "TimeZone"); diag("Initial DateStyle: %s, TimeZone: %s", initial_datestyle.c_str(), initial_timezone.c_str()); // SET to different values set_variable_simple(conn.get(), "DateStyle", "Postgres, DMY"); set_variable_simple(conn.get(), "TimeZone", "PST8PDT"); diag("After SET: DateStyle = %s, TimeZone = %s", get_variable_simple(conn.get(), "DateStyle").c_str(), get_variable_simple(conn.get(), "TimeZone").c_str()); // RESET ALL PGresult* res = PQexec(conn.get(), "RESET ALL"); if (PQresultStatus(res) != PGRES_COMMAND_OK) { diag("RESET ALL failed: %s", PQresultErrorMessage(res)); PQclear(res); return false; } PQclear(res); // Verify values are back to initial std::string after_reset_datestyle = get_variable_simple(conn.get(), "DateStyle"); std::string after_reset_timezone = get_variable_simple(conn.get(), "TimeZone"); diag("After RESET ALL: DateStyle = %s, TimeZone = %s", after_reset_datestyle.c_str(), after_reset_timezone.c_str()); bool success = (after_reset_datestyle == initial_datestyle) && (after_reset_timezone == initial_timezone); diag("RESET ALL %s", success ? "succeeded" : "failed"); return success; } // Test: DISCARD ALL in simple query mode bool test_discard_all_simple_query() { diag("=== Test: DISCARD ALL in simple query mode ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Execute DISCARD ALL PGresult* res = PQexec(conn.get(), "DISCARD ALL"); if (PQresultStatus(res) != PGRES_COMMAND_OK) { diag("DISCARD ALL failed: %s", PQresultErrorMessage(res)); PQclear(res); return false; } PQclear(res); // Verify connection still works PGresult* res2 = PQexec(conn.get(), "SELECT 1"); bool connection_ok = (PQresultStatus(res2) == PGRES_TUPLES_OK); PQclear(res2); diag("DISCARD ALL %s", connection_ok ? "succeeded" : "failed"); return connection_ok; } // Test: Multiple variables out of sync in pipeline mode // This tests the edge case where a pooled backend connection has different // parameter values than the client's current session, triggering the // "multiple parameters need syncing" error and session termination bool test_multiple_vars_out_of_sync_pipeline() { diag("=== Test: Multiple variables out of sync in pipeline mode ==="); // Step 1: Create first connection and set multiple variables PGConnPtr conn1 = createNewConnection(BACKEND); if (!conn1) return false; // Set multiple variables to non-default values PGresult* res; res = PQexec(conn1.get(), "SET DateStyle = 'Postgres, DMY'"); PQclear(res); res = PQexec(conn1.get(), "SET TimeZone = 'PST8PDT'"); PQclear(res); res = PQexec(conn1.get(), "SET bytea_output = 'escape'"); PQclear(res); // Verify values are set std::string ds1 = get_variable_simple(conn1.get(), "DateStyle"); std::string tz1 = get_variable_simple(conn1.get(), "TimeZone"); std::string bo1 = get_variable_simple(conn1.get(), "bytea_output"); // Close connection (returns to pool with these values) conn1.reset(); diag("Connection 1 closed - returned to pool with DateStyle='Postgres, MDY', TimeZone='PST8PDT', bytea_output='escape'"); // Wait for connection to be returned to pool with polling (max 5 seconds) // Fixed delay replaced with polling to handle slow CI systems bool conn_in_pool = false; for (int retry = 0; retry < 50; retry++) { usleep(100000); // 100ms * 50 = 5 seconds max // Check if we can create a new connection (indicates pool has capacity) PGConnPtr test_conn = createNewConnection(BACKEND); if (test_conn) { conn_in_pool = true; break; } } if (!conn_in_pool) { diag("Warning: Connection may not have returned to pool yet, continuing anyway"); } // Step 2: Create new connection with DIFFERENT variable values (simple query mode) PGConnPtr conn2 = createNewConnection(BACKEND); if (!conn2) return false; // Set different values (these become client-side hashes) res = PQexec(conn2.get(), "SET DateStyle = 'SQL, DMY'"); PQclear(res); res = PQexec(conn2.get(), "SET TimeZone = 'UTC'"); PQclear(res); res = PQexec(conn2.get(), "SET bytea_output = 'hex'"); PQclear(res); diag("Connection 2: SET DateStyle='SQL, DMY', TimeZone='UTC', bytea_output='hex'"); // Step 3: Directly enter pipeline mode and execute a query // This will pull the pooled connection from conn1 which has DIFFERENT values // All 3 variables will be out of sync, triggering multiple variable sync error if (PQenterPipelineMode(conn2.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Send a query - this will use a pooled connection with different parameters if (PQsendQueryParams(conn2.get(), "SELECT 1", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send query"); return false; } PQpipelineSync(conn2.get()); PQflush(conn2.get()); // Step 4: Check results int count = 0; bool got_result = false; bool got_error = false; PGresult* result_res; int sock = PQsocket(conn2.get()); while (count < 2) { if (PQconsumeInput(conn2.get()) == 0) break; while ((result_res = PQgetResult(conn2.get())) != NULL) { ExecStatusType status = PQresultStatus(result_res); if (status == PGRES_TUPLES_OK) { got_result = true; diag("Got query result (variables synced via pipeline)"); } else if (status == PGRES_FATAL_ERROR) { got_error = true; diag("Got error (multiple variables out of sync, session terminated): %s", PQresultErrorMessage(result_res)); } else if (status == PGRES_PIPELINE_SYNC) { PQclear(result_res); count++; continue; } PQclear(result_res); count++; } if (count >= 2) break; if (!PQisBusy(conn2.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } // Try to exit pipeline mode PQexitPipelineMode(conn2.get()); // Verify connection still works PGresult* test_res = PQexec(conn2.get(), "SELECT 1"); bool connection_ok = (PQresultStatus(test_res) == PGRES_TUPLES_OK); PQclear(test_res); diag("Test result: got_result=%s, got_error=%s, connection_ok=%s", got_result ? "yes" : "no", got_error ? "yes" : "no", connection_ok ? "yes" : "no"); // The test passes if either: // 1. Query succeeded (variables were synced via new SET handling), OR // 2. Connection was terminated but can be re-established return got_result || connection_ok; } // Test: SET/RESET/DISCARD with locked hostgroup in pipeline mode // This tests that SET/RESET/DISCARD work correctly when hostgroup is locked // Hostgroup locking is triggered by SET with unknown parameter bool test_pipeline_with_locked_hostgroup() { diag("=== Test: SET/RESET/DISCARD with locked hostgroup in pipeline mode ==="); // Open ProxySQL log file to check for hostgroup lock messages std::string f_path{ get_env("REGULAR_INFRA_DATADIR") + "/proxysql.log" }; std::fstream log_file{}; int of_err = open_file_and_seek_end(f_path, log_file); if (of_err != EXIT_SUCCESS) { diag("Failed to open ProxySQL log file"); return false; } PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Step 1: Lock hostgroup by setting a user-defined variable // User variables with dotted names are valid in PostgreSQL and will lock hostgroup PGresult* res = PQexec(conn.get(), "SET myapp.test_var = 'test_value'"); bool set_ok = (PQresultStatus(res) == PGRES_COMMAND_OK); PQclear(res); diag("SET myapp.test_var %s (hostgroup should be locked)", set_ok ? "succeeded" : "failed"); // Check logs for hostgroup lock warning usleep(50000); // Give time for log to be written log_file.clear(log_file.rdstate() & ~std::ios_base::failbit); log_file.seekg(log_file.tellg()); const auto& [_, cmd_lines] { get_matching_lines(log_file, ".*\\[WARNING\\] Unable to parse unknown SET query from client.*") }; bool hostgroup_locked = !cmd_lines.empty(); diag("Hostgroup lock triggered (log check): %s", hostgroup_locked ? "yes" : "no"); // Step 2: Change some variables via simple query res = PQexec(conn.get(), "SET DateStyle = 'Postgres, DMY'"); PQclear(res); res = PQexec(conn.get(), "SET TimeZone = 'PST8PDT'"); PQclear(res); diag("SET DateStyle='Postgres, DMY', TimeZone='PST8PDT' on locked hostgroup"); // Step 3: Enter pipeline mode and SHOW variables to verify they were set if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } if (PQsendQueryParams(conn.get(), "SHOW DateStyle", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SHOW DateStyle"); return false; } if (PQsendQueryParams(conn.get(), "SHOW TimeZone", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SHOW TimeZone"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume SHOW results int count = 0; std::string datestyle_val, timezone_val; PGresult* result_res; int sock = PQsocket(conn.get()); while (count < 3) { // 2 SHOW + 1 sync if (PQconsumeInput(conn.get()) == 0) break; while ((result_res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(result_res); if (status == PGRES_TUPLES_OK && PQntuples(result_res) > 0) { char* val = PQgetvalue(result_res, 0, 0); if (datestyle_val.empty()) { datestyle_val = val ? val : ""; diag("SHOW DateStyle in pipeline: %s", datestyle_val.c_str()); } else if (timezone_val.empty()) { timezone_val = val ? val : ""; diag("SHOW TimeZone in pipeline: %s", timezone_val.c_str()); } } else if (status == PGRES_PIPELINE_SYNC) { PQclear(result_res); count++; continue; } PQclear(result_res); count++; } if (count >= 3) break; if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify values were set correctly bool values_correct = (datestyle_val.find("Postgres") != std::string::npos) && (timezone_val.find("PST") != std::string::npos); diag("Values set correctly: %s", values_correct ? "yes" : "no"); // Step 4: Create new connection and SET parameters in pipeline mode // Also SET dummy to test hostgroup lock handling PGConnPtr conn2 = createNewConnection(BACKEND); if (!conn2) return false; if (PQenterPipelineMode(conn2.get()) != 1) { diag("Failed to enter pipeline mode on conn2"); return false; } // SET real parameters in pipeline mode if (PQsendQueryParams(conn2.get(), "SET DateStyle = 'SQL, DMY'", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET DateStyle in pipeline"); return false; } if (PQsendQueryParams(conn2.get(), "SET TimeZone = 'UTC'", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET TimeZone in pipeline"); return false; } // SET user variable to trigger hostgroup lock if (PQsendQueryParams(conn2.get(), "SET myapp.test_var2 = 'test_value2'", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET user variable in pipeline"); return false; } // SHOW to verify real parameters were set if (PQsendQueryParams(conn2.get(), "SHOW DateStyle", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SHOW DateStyle"); return false; } if (PQsendQueryParams(conn2.get(), "SHOW TimeZone", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SHOW TimeZone"); return false; } PQpipelineSync(conn2.get()); PQflush(conn2.get()); // Consume results count = 0; int cmd_ok_count = 0; int show_count = 0; std::string ds2_val, tz2_val; sock = PQsocket(conn2.get()); while (count < 6) { // 3 SET + 2 SHOW + 1 sync if (PQconsumeInput(conn2.get()) == 0) break; while ((result_res = PQgetResult(conn2.get())) != NULL) { ExecStatusType status = PQresultStatus(result_res); if (status == PGRES_COMMAND_OK) { cmd_ok_count++; diag("SET command OK in pipeline mode (conn2)"); } else if (status == PGRES_FATAL_ERROR) { // SET myapp.test_var2 may fail if custom variables not configured diag("SET user variable returned error (may be expected): %s", PQresultErrorMessage(result_res) ? PQresultErrorMessage(result_res) : "unknown"); } else if (status == PGRES_TUPLES_OK && PQntuples(result_res) > 0) { char* val = PQgetvalue(result_res, 0, 0); if (ds2_val.empty()) { ds2_val = val ? val : ""; diag("SHOW DateStyle in pipeline (conn2): %s", ds2_val.c_str()); } else { tz2_val = val ? val : ""; diag("SHOW TimeZone in pipeline (conn2): %s", tz2_val.c_str()); } show_count++; } else if (status == PGRES_PIPELINE_SYNC) { PQclear(result_res); count++; continue; } PQclear(result_res); count++; } if (count >= 6) break; if (!PQisBusy(conn2.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn2.get()); // Verify conn2 values bool conn2_values_correct = (ds2_val.find("SQL") != std::string::npos) && (tz2_val.find("UTC") != std::string::npos); diag("Conn2 values set correctly in pipeline: %s", conn2_values_correct ? "yes" : "no"); diag("Conn2 SET commands OK: %d, SHOW commands: %d", cmd_ok_count, show_count); // Verify both connections still work PGresult* test_res = PQexec(conn.get(), "SELECT 1"); bool conn1_ok = (PQresultStatus(test_res) == PGRES_TUPLES_OK); PQclear(test_res); test_res = PQexec(conn2.get(), "SELECT 1"); bool conn2_ok = (PQresultStatus(test_res) == PGRES_TUPLES_OK); PQclear(test_res); diag("Connections still usable: conn1=%s, conn2=%s", conn1_ok ? "yes" : "no", conn2_ok ? "yes" : "no"); log_file.close(); // Expected: SET commands OK (DateStyle, TimeZone), SHOW results // Also verify hostgroup was locked via log check return values_correct && conn2_values_correct && (cmd_ok_count >= 2) && conn1_ok && conn2_ok && hostgroup_locked; } // Test: RESET ALL with locked hostgroup in pipeline mode - startup values MATCH // This tests that RESET ALL works when hostgroup is locked and startup values match bool test_reset_all_locked_hostgroup_pipeline() { diag("=== Test: RESET ALL with locked hostgroup when startup values match ==="); // Note: In this test, we rely on the fact that a fresh connection has // matching startup values between client and backend (both use defaults) PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Step 1: Enter pipeline mode (fresh connection, startup values match) if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Step 2: Set user variable to lock hostgroup (in pipeline) if (PQsendQueryParams(conn.get(), "SET myapp.lock_var = 'lock_value'", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET lock_var"); return false; } // Step 3: Send RESET ALL in pipeline mode with locked hostgroup // Since startup values match (fresh connection), this should succeed if (PQsendQueryParams(conn.get(), "RESET ALL", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send RESET ALL"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int count = 0; bool lock_ok = false; bool reset_ok = false; int sock = PQsocket(conn.get()); PGresult* result_res; while (count < 3) { // 2 commands + 1 sync if (PQconsumeInput(conn.get()) == 0) break; while ((result_res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(result_res); if (status == PGRES_COMMAND_OK) { if (!lock_ok) { lock_ok = true; diag("SET lock_var succeeded (hostgroup locked)"); } else { reset_ok = true; diag("RESET ALL succeeded in pipeline with locked hostgroup"); } } else if (status == PGRES_FATAL_ERROR) { diag("Command failed: %s", PQresultErrorMessage(result_res)); } else if (status == PGRES_PIPELINE_SYNC) { PQclear(result_res); count++; continue; } PQclear(result_res); count++; } if (count >= 3) break; if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify connection still works PGresult* res = PQexec(conn.get(), "SELECT 1"); bool conn_ok = (PQresultStatus(res) == PGRES_TUPLES_OK); PQclear(res); return lock_ok && reset_ok && conn_ok; } // Test: DISCARD ALL with locked hostgroup in pipeline mode // This tests that DISCARD ALL correctly FAILS in pipeline mode even with locked hostgroup // DISCARD ALL is more destructive than RESET ALL (resets prepared statements, temp tables, etc.) // so it is blocked in pipeline mode regardless of hostgroup lock status bool test_discard_all_locked_hostgroup_pipeline() { diag("=== Test: DISCARD ALL with locked hostgroup in pipeline mode ==="); PGConnPtr conn = createNewConnection(BACKEND); if (!conn) return false; // Step 1: Enter pipeline mode if (PQenterPipelineMode(conn.get()) != 1) { diag("Failed to enter pipeline mode"); return false; } // Step 2: Set user variable to lock hostgroup (in pipeline) if (PQsendQueryParams(conn.get(), "SET myapp.lock_var = 'lock_value'", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send SET lock_var"); return false; } // Step 3: Send DISCARD ALL in pipeline mode with locked hostgroup if (PQsendQueryParams(conn.get(), "DISCARD ALL", 0, NULL, NULL, NULL, NULL, 0) != 1) { diag("Failed to send DISCARD ALL"); return false; } PQpipelineSync(conn.get()); PQflush(conn.get()); // Consume results int count = 0; bool lock_ok = false; bool got_discard_error = false; std::string error_msg; int sock = PQsocket(conn.get()); PGresult* result_res; while (count < 3) { // 2 commands + 1 sync if (PQconsumeInput(conn.get()) == 0) break; while ((result_res = PQgetResult(conn.get())) != NULL) { ExecStatusType status = PQresultStatus(result_res); if (status == PGRES_COMMAND_OK) { lock_ok = true; diag("SET lock_var succeeded (hostgroup locked)"); } else if (status == PGRES_FATAL_ERROR) { char* err = PQresultErrorMessage(result_res); if (err && strstr(err, "DISCARD ALL")) { got_discard_error = true; error_msg = err; diag("DISCARD ALL correctly failed in pipeline: %s", err); } } else if (status == PGRES_PIPELINE_SYNC) { PQclear(result_res); count++; continue; } PQclear(result_res); count++; } if (count >= 3) break; if (!PQisBusy(conn.get())) continue; fd_set input_mask; FD_ZERO(&input_mask); FD_SET(sock, &input_mask); struct timeval timeout = {5, 0}; select(sock + 1, &input_mask, NULL, NULL, &timeout); } PQexitPipelineMode(conn.get()); // Verify connection still works after expected error PGresult* res = PQexec(conn.get(), "SELECT 1"); bool conn_ok = (PQresultStatus(res) == PGRES_TUPLES_OK); PQclear(res); // Test passes if: // 1. Hostgroup was locked (SET myapp.lock_var succeeded) // 2. DISCARD ALL failed with error (as expected) // 3. Connection is still usable return lock_ok && got_discard_error && conn_ok; }