You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/test/tap/tests/pgsql-set_statement_test-t.cpp

2120 lines
76 KiB

/**
* @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 <unistd.h>
#include <string>
#include <sstream>
#include <chrono>
#include <thread>
#include <vector>
#include <map>
#include <ctime>
#include <sys/select.h>
#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<PGconn, decltype(&PQfinish)>;
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 << "'";
}
PGconn* conn = PQconnectdb(ss.str().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<PipelineTestVariable> 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<std::string, std::string> 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<PipelineTestVariable> 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<std::string, std::string> 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<PipelineTestVariable> 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<std::string, std::string> 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: RESET single variable should work in pipeline mode
bool test_reset_single_var_pipeline() {
diag("=== Test: RESET single variable in pipeline mode ===");
PGConnPtr conn = createNewConnection(BACKEND);
if (!conn) return false;
// First set a non-default value via simple query
if (!set_variable_simple(conn.get(), "DateStyle", "Postgres, DMY")) {
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());
// 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);
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_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());
// Verify the value changed from what we SET
bool value_changed = (reset_result != set_value);
diag("Value changed from '%s' to '%s': %s",
set_value.c_str(), reset_result.c_str(), value_changed ? "yes" : "no");
return (cmd_count >= 1) && (show_count >= 1) && value_changed;
}
// 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();
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<TestCase> 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 (19 total: 16 pipeline + 3 simple query RESET/DISCARD)
const int num_pipeline_tests = 19;
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");
// 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<PipelineTestVariable> 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<std::string, std::string> 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;
}