mirror of https://github.com/sysown/proxysql
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.
2120 lines
76 KiB
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;
|
|
} |