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.
911 lines
34 KiB
911 lines
34 KiB
/**
|
|
* @file pgsql-copy_from_test-t.cpp
|
|
* @brief Tests COPY FROM functionality in ProxySQL
|
|
*/
|
|
|
|
#include <unistd.h>
|
|
#include <arpa/inet.h>
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include "libpq-fe.h"
|
|
#include "command_line.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, bool with_ssl) {
|
|
|
|
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_username : cl.admin_username;
|
|
const char* password = (conn_type == BACKEND) ? cl.pgsql_password : cl.admin_password;
|
|
|
|
|
|
std::stringstream ss;
|
|
|
|
ss << "host=" << host << " port=" << port;
|
|
ss << " user=" << username << " password=" << password;
|
|
ss << (with_ssl ? " sslmode=require" : " sslmode=disable");
|
|
|
|
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);
|
|
}
|
|
|
|
bool executeQueries(PGconn* conn, const std::vector<std::string>& queries) {
|
|
auto fnResultType = [](const char* query) -> int {
|
|
const char* fs = strchr(query, ' ');
|
|
size_t qtlen = strlen(query);
|
|
if (fs != NULL) {
|
|
qtlen = (fs - query) + 1;
|
|
}
|
|
char buf[qtlen];
|
|
memcpy(buf, query, qtlen - 1);
|
|
buf[qtlen - 1] = 0;
|
|
|
|
if (strncasecmp(buf, "SELECT", sizeof("SELECT") - 1) == 0) {
|
|
return PGRES_TUPLES_OK;
|
|
}
|
|
else if (strncasecmp(buf, "COPY", sizeof("COPY") - 1) == 0) {
|
|
if (strstr(query, "FROM") && (strstr(query, "STDIN") || strstr(query, "STDOUT"))) {
|
|
return PGRES_COPY_IN;
|
|
}
|
|
}
|
|
|
|
return PGRES_COMMAND_OK;
|
|
};
|
|
|
|
|
|
for (const auto& query : queries) {
|
|
diag("Running: %s", query.c_str());
|
|
PGresult* res = PQexec(conn, query.c_str());
|
|
bool success = PQresultStatus(res) == fnResultType(query.c_str());
|
|
if (!success) {
|
|
fprintf(stderr, "Failed to execute query '%s': %s",
|
|
query.c_str(), PQerrorMessage(conn));
|
|
PQclear(res);
|
|
return false;
|
|
}
|
|
PQclear(res);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool sendCopyData(PGconn* conn, const char* data, int size, bool last) {
|
|
|
|
if (data != nullptr && size > 0) {
|
|
if (PQputCopyData(conn, data, size) != 1) {
|
|
fprintf(stderr, "Failed to send data: %s", PQerrorMessage(conn));
|
|
return false;
|
|
}
|
|
}
|
|
if (last) {
|
|
if (PQputCopyEnd(conn, NULL) != 1) {
|
|
fprintf(stderr, "Failed to send end of data: %s", PQerrorMessage(conn));
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void splitString(std::vector<std::string>& split_str, const std::string& str) {
|
|
std::stringstream ss(str);
|
|
std::string token;
|
|
|
|
while (std::getline(ss, token, '\t')) {
|
|
// Remove the newline character at the end if present
|
|
if (!token.empty() && token.back() == '\n') {
|
|
token.pop_back();
|
|
}
|
|
split_str.push_back(token);
|
|
}
|
|
}
|
|
|
|
// Helper function to convert a 32-bit integer to network byte order
|
|
void write_int32(uint8_t* dest, int32_t value) {
|
|
dest[0] = (value >> 24) & 0xFF;
|
|
dest[1] = (value >> 16) & 0xFF;
|
|
dest[2] = (value >> 8) & 0xFF;
|
|
dest[3] = value & 0xFF;
|
|
}
|
|
|
|
// Helper function to convert a 16-bit integer to network byte order
|
|
void write_int16(uint8_t* dest, int16_t value) {
|
|
dest[0] = (value >> 8) & 0xFF;
|
|
dest[1] = value & 0xFF;
|
|
}
|
|
|
|
bool encodeNumericBinary(uint8_t* out, const char* numStr) {
|
|
int16_t numDigits = 0, weight = 0, sign = 0x0000, scale = 0;
|
|
int16_t digits[64] = { 0 }; // Temporary storage for up to 64 4-digit groups
|
|
size_t digitCount = 0;
|
|
|
|
// Handle negative numbers
|
|
const char* numericPart = numStr;
|
|
if (numStr[0] == '-') {
|
|
sign = 0x4000; // Negative sign
|
|
numericPart++; // Skip the negative sign
|
|
}
|
|
|
|
// Split the number into integer and fractional parts
|
|
const char* dotPos = strchr(numericPart, '.');
|
|
size_t intPartLen = dotPos ? (size_t)(dotPos - numericPart) : strlen(numericPart);
|
|
size_t fracPartLen = dotPos ? strlen(dotPos + 1) : 0;
|
|
|
|
// Combine integer and fractional parts into a single string of digits
|
|
char combined[128] = { 0 };
|
|
strncpy(combined, numericPart, intPartLen);
|
|
if (fracPartLen > 0) {
|
|
strncat(combined, dotPos + 1, fracPartLen);
|
|
}
|
|
|
|
// Remove leading zeros
|
|
while (combined[0] == '0' && combined[1] != '\0') {
|
|
memmove(combined, combined + 1, strlen(combined));
|
|
}
|
|
|
|
// Pad the combined string length to a multiple of 4 for grouping
|
|
size_t combinedLen = strlen(combined);
|
|
size_t paddedLen = (combinedLen + 3) & ~3; // Round up to next multiple of 4
|
|
for (size_t i = combinedLen; i < paddedLen; ++i) {
|
|
combined[i] = '0'; // Pad with zeros
|
|
}
|
|
|
|
// Parse the padded string into 4-digit groups
|
|
for (size_t i = 0; i < paddedLen; i += 4) {
|
|
char group[5] = { 0 }; // Temporary buffer for a group of up to 4 digits
|
|
strncpy(group, combined + i, 4);
|
|
digits[digitCount++] = htons((int16_t)atoi(group)); // Convert group to 16-bit integer
|
|
}
|
|
|
|
|
|
numDigits = (int16_t)(digitCount == 1 && combined[0] == '0') ? 0 : digitCount;
|
|
|
|
// Calculate weight
|
|
weight = (int16_t)((intPartLen + 3) / 4 - 1);
|
|
|
|
// Scale (number of fractional digits)
|
|
scale = (int16_t)fracPartLen;
|
|
|
|
// Pack the binary data
|
|
write_int16(out, numDigits); // numDigits
|
|
out += sizeof(int16_t);
|
|
write_int16(out, weight); // weight
|
|
out += sizeof(int16_t);
|
|
write_int16(out, htons(sign)); // sign (converted to network byte order)
|
|
out += sizeof(int16_t);
|
|
write_int16(out, scale); // scale
|
|
out += sizeof(int16_t);
|
|
memcpy(out, digits, numDigits * sizeof(int16_t)); // digit groups
|
|
|
|
return numDigits;
|
|
}
|
|
|
|
// Helper function to check if a year is a leap year
|
|
int isLeapYear(int year) {
|
|
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
|
}
|
|
|
|
// Function to calculate days from 2000-01-01
|
|
int calculateDaysFromEpoch(int year, int month, int day) {
|
|
// Days in each month (non-leap year)
|
|
const int daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
|
|
|
// Calculate days for previous years
|
|
int days = 0;
|
|
for (int y = 2000; y < year; ++y) {
|
|
days += isLeapYear(y) ? 366 : 365;
|
|
}
|
|
for (int y = 1999; y > year; --y) {
|
|
days -= isLeapYear(y) ? 366 : 365;
|
|
}
|
|
|
|
// Add days for previous months in the current year
|
|
for (int m = 1; m < month; ++m) {
|
|
days += daysInMonth[m - 1];
|
|
if (m == 2 && isLeapYear(year)) {
|
|
days += 1; // February in a leap year
|
|
}
|
|
}
|
|
|
|
// Add/subtract the current month's days
|
|
if (year >= 2000) {
|
|
days += day - 1;
|
|
}
|
|
else {
|
|
days -= daysInMonth[month - 1] - day + 1;
|
|
if (month == 2 && isLeapYear(year)) {
|
|
days -= 1; // Adjust for leap year February
|
|
}
|
|
}
|
|
|
|
return days;
|
|
}
|
|
|
|
// Function to encode a date into PostgreSQL binary format
|
|
uint32_t encodeDateBinary(const char* dateStr) {
|
|
int year, month, day;
|
|
|
|
// Parse the date string (expected format: YYYY-MM-DD)
|
|
if (sscanf(dateStr, "%d-%d-%d", &year, &month, &day) != 3) {
|
|
fprintf(stderr, "Invalid date format. Use YYYY-MM-DD.\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// Calculate the number of days since 2000-01-01
|
|
int days = calculateDaysFromEpoch(year, month, day);
|
|
|
|
// Convert to big-endian (network byte order)
|
|
return htonl(days);
|
|
}
|
|
|
|
int is_string_in_result(PGresult* result, const char* target_str) {
|
|
int rows = PQntuples(result);
|
|
int cols = PQnfields(result);
|
|
|
|
// Iterate through all rows and columns
|
|
for (int i = 0; i < rows; i++) {
|
|
int match_count = 0;
|
|
char full_row_str[1024] = { 0 }; // Buffer to reconstruct full row string
|
|
|
|
// Reconstruct the row string (with tab and newline separators)
|
|
for (int j = 0; j < cols; j++) {
|
|
char* val = PQgetvalue(result, i, j);
|
|
strcat(full_row_str, val);
|
|
if (j < cols - 1) {
|
|
strcat(full_row_str, "\t");
|
|
}
|
|
}
|
|
strcat(full_row_str, "\n");
|
|
|
|
// Compare reconstructed row string with target
|
|
if (strcmp(full_row_str, target_str) == 0) {
|
|
return 1; // Found a match
|
|
}
|
|
}
|
|
return 0; // No match found
|
|
}
|
|
|
|
bool check_logs_for_command(std::fstream& f_proxysql_log, const std::string& command_regex) {
|
|
const auto& [_, cmd_lines] { get_matching_lines(f_proxysql_log, command_regex) };
|
|
return cmd_lines.empty() ? false : true;
|
|
}
|
|
|
|
bool setupTestTable(PGconn* conn) {
|
|
return executeQueries(conn, {
|
|
"DROP TABLE IF EXISTS copy_in_test",
|
|
"CREATE TABLE copy_in_test (column1 INT,column2 TEXT,column3 NUMERIC(10, 2),column4 BOOLEAN,column5 DATE)"
|
|
});
|
|
}
|
|
|
|
std::vector<const char*> test_data = { "1\tHello\t123.45\tt\t2024-01-01\n",
|
|
"2\tWorld\t678.90\tf\t2024-02-15\n",
|
|
"3\tTest\t0.00\tt\t2023-12-25\n",
|
|
//"4\tSample\t-42.42\tf\t2024-11-27\n"
|
|
"4\tSample\t142.42\tf\t2024-11-27\n"
|
|
};
|
|
|
|
typedef enum {
|
|
INT,
|
|
TEXT,
|
|
NUMERIC,
|
|
BOOLEAN,
|
|
DATE
|
|
} column_type_t;
|
|
|
|
const column_type_t columns_type[] = {
|
|
INT,
|
|
TEXT,
|
|
NUMERIC,
|
|
BOOLEAN,
|
|
DATE
|
|
};
|
|
|
|
/**
|
|
* @brief Tests the COPY IN functionality using STDIN in TEXT format.
|
|
*
|
|
* This function executes a COPY IN command to insert data into a PostgreSQL table
|
|
* using the STDIN method in TEXT format. It verifies the success of the data transmission
|
|
* and checks the logs for specific commands to ensure the session mode switches correctly.
|
|
*
|
|
* @param admin_conn A pointer to the admin PGconn connection.
|
|
* @param conn A pointer to the PGconn connection used for the COPY IN operation.
|
|
* @param f_proxysql_log A reference to the fstream object for ProxySQL logs.
|
|
*/
|
|
void testSTDIN_TEXT_FORMAT(PGconn* admin_conn, PGconn* conn, std::fstream& f_proxysql_log) {
|
|
if (!executeQueries(conn, {"COPY /*dummy comment*/ copy_in_test(column1,column2,column3,column4,column5) /*dummy comment*/ FROM /*dummy comment*/ STDIN /*dummy comment*/ (FORMAT TEXT) /*dummy comment*/ "}))
|
|
return;
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)"), "Session Switched to fast forward mode");
|
|
|
|
bool success = true;
|
|
|
|
for (unsigned int i = 0; i < test_data.size(); i++) {
|
|
const char* data = test_data[i];
|
|
bool last = (i == (test_data.size() - 1));
|
|
if (!sendCopyData(conn, data, strlen(data), last)) {
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ok(success, "Copy data transmission should be successful");
|
|
|
|
PGresult* res = PQgetResult(conn);
|
|
|
|
ok((PQresultStatus(res) == PGRES_COMMAND_OK), "Rows successfully inserted. %s", PQerrorMessage(conn));
|
|
|
|
const char* row_count_str = PQcmdTuples(res);
|
|
const int row_count = atoi(row_count_str);
|
|
|
|
ok(row_count == test_data.size(), "Total rows inserted: %d. Expected: %ld", row_count, test_data.size());
|
|
PQclear(res);
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\] Switching back to Normal mode \\(Session Type:0x06\\).*"), "Switching back to Normal mode");
|
|
}
|
|
|
|
/**
|
|
* @brief Tests the COPY IN functionality using BINARY formats.
|
|
*
|
|
* This function performs the following steps:
|
|
* 1. Executes a COPY command to start the COPY IN process.
|
|
* 2. Checks the logs to ensure the session has switched to fast forward mode.
|
|
* 3. Sends the binary header for the COPY IN process.
|
|
* 4. Iterates over the test data, encoding each row according to its column type.
|
|
* 5. Sends the encoded row data to the PostgreSQL server.
|
|
* 6. Verifies that the data transmission was successful.
|
|
* 7. Checks the result to ensure rows were successfully inserted.
|
|
* 8. Verifies the number of rows inserted matches the expected count.
|
|
* 9. Checks the logs to ensure the session has switched back to normal mode.
|
|
*
|
|
* @param admin_conn The connection to the admin database.
|
|
* @param conn The connection to the target database.
|
|
* @param f_proxysql_log The log file stream for ProxySQL logs.
|
|
*/
|
|
void testSTDIN_TEXT_BINARY(PGconn* admin_conn, PGconn* conn, std::fstream& f_proxysql_log) {
|
|
if (!executeQueries(conn, { "COPY copy_in_test(column1,column2,column3,column4,column5) FROM STDIN (FORMAT BINARY)" }))
|
|
return;
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)"), "Session Switched to fast forward mode");
|
|
|
|
bool success = true;
|
|
|
|
// Send binary header
|
|
const char binary_signature[] = "PGCOPY\n\377\r\n\0";
|
|
int32_t flags = 0;
|
|
int32_t header_extension_length = 0;
|
|
|
|
char header[19];
|
|
memcpy(header, binary_signature, sizeof(binary_signature) - 1);
|
|
memcpy(header + 11, &flags, sizeof(flags));
|
|
memcpy(header + 15, &header_extension_length, sizeof(header_extension_length));
|
|
|
|
uint8_t row[1024];
|
|
int offset = 0;
|
|
|
|
for (unsigned int i = 0; i < test_data.size(); i++) {
|
|
if (i == 0) {
|
|
memcpy(row, header, sizeof(header));
|
|
offset = sizeof(header);
|
|
} else {
|
|
offset = 0;
|
|
}
|
|
|
|
std::vector<std::string> row_data;
|
|
splitString(row_data, test_data[i]);
|
|
|
|
const int16_t num_fields = row_data.size();
|
|
// write column count
|
|
write_int16(row + offset, num_fields);
|
|
offset += sizeof(num_fields);
|
|
|
|
for (unsigned int j = 0; j < row_data.size(); j++) {
|
|
const std::string& data = row_data[j];
|
|
if (columns_type[j] == INT) {
|
|
write_int32(row + offset, sizeof(int32_t));
|
|
offset += sizeof(int32_t);
|
|
|
|
int32_t value = atoi(data.c_str());
|
|
// write actual data
|
|
memcpy(row + offset, &value, sizeof(value));
|
|
offset += sizeof(value);
|
|
} else if (columns_type[j] == DATE) {
|
|
write_int32(row + offset, sizeof(int32_t));
|
|
offset += sizeof(int32_t);
|
|
|
|
uint32_t date = encodeDateBinary(data.c_str());
|
|
// write actual data
|
|
memcpy(row + offset, &date, sizeof(date));
|
|
offset += sizeof(date);
|
|
} else if (columns_type[j] == TEXT || columns_type[j] == BOOLEAN) {
|
|
// write field length
|
|
write_int32(row + offset, data.size());
|
|
offset += sizeof(int32_t);
|
|
|
|
// write actual data
|
|
memcpy(row + offset, data.c_str(), data.size());
|
|
offset += data.size();
|
|
} else if (columns_type[j] == NUMERIC) {
|
|
uint8_t* prev_pos = (row + offset);
|
|
offset += sizeof(int32_t);
|
|
bool has_digits = encodeNumericBinary(row + offset, data.c_str());
|
|
if (has_digits) {
|
|
write_int32(prev_pos, 12);
|
|
offset += 12;
|
|
} else {
|
|
write_int32(prev_pos, 8);
|
|
offset += 8;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool last = (i == (test_data.size() - 1));
|
|
|
|
if (last) {
|
|
write_int16(row + offset, -1);
|
|
offset += sizeof(int16_t);
|
|
}
|
|
if (!sendCopyData(conn, reinterpret_cast<const char*>(row), offset, last)) {
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ok(success, "Copy data transmission should be successful");
|
|
|
|
PGresult* res = PQgetResult(conn);
|
|
|
|
ok((PQresultStatus(res) == PGRES_COMMAND_OK), "Rows successfully inserted. %s", PQerrorMessage(conn));
|
|
|
|
const char* row_count_str = PQcmdTuples(res);
|
|
const int row_count = atoi(row_count_str);
|
|
|
|
ok(row_count == test_data.size(), "Total rows inserted: %d. Expected: %ld", row_count, test_data.size());
|
|
PQclear(res);
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\] Switching back to Normal mode \\(Session Type:0x06\\).*"), "Switching back to Normal mode");
|
|
}
|
|
|
|
/**
|
|
* @brief Tests the behavior of COPY FROM STDIN with an error scenario.
|
|
*
|
|
* This function attempts to execute a COPY FROM STDIN command on a non-existent table,
|
|
* expecting it to fail. It then checks the ProxySQL logs to ensure that the session
|
|
* switches to fast forward mode and then back to normal mode.
|
|
*
|
|
* @param admin_conn A pointer to the admin PGconn connection.
|
|
* @param conn A pointer to the PGconn connection.
|
|
* @param f_proxysql_log A reference to the fstream object for ProxySQL logs.
|
|
*/
|
|
void testSTDIN_ERROR(PGconn* admin_conn, PGconn* conn, std::fstream& f_proxysql_log) {
|
|
|
|
ok(executeQueries(conn, { "COPY non_existent_table FROM STDIN (FORMAT TEXT)" }) == false, "Query should fail. %s", PQerrorMessage(conn));
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)"), "Session Switched to fast forward mode");
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\] Switching back to Normal mode \\(Session Type:0x06\\).*"), "Switching back to Normal mode");
|
|
}
|
|
|
|
/**
|
|
* @brief Tests the COPY IN functionality within a transaction.
|
|
*
|
|
* This function initiates a transaction, performs a COPY IN operation to insert data into the
|
|
* 'copy_in_test' table, and verifies the success of the operation. It also checks the session
|
|
* mode transitions and ensures the connection remains in the transaction state throughout the process.
|
|
*
|
|
* @param admin_conn Pointer to the admin connection.
|
|
* @param conn Pointer to the backend connection.
|
|
* @param f_proxysql_log Reference to the ProxySQL log file stream.
|
|
*/
|
|
void testSTDIN_TRANSACTION(PGconn* admin_conn, PGconn* conn, std::fstream& f_proxysql_log) {
|
|
|
|
if (!executeQueries(conn, { "BEGIN;" }))
|
|
return;
|
|
|
|
ok(PQtransactionStatus(conn) == PQTRANS_INTRANS, "Connection should be in Transaction State");
|
|
|
|
if (!executeQueries(conn, { "COPY copy_in_test(column1,column2,column3,column4,column5) FROM STDIN (FORMAT TEXT)" }))
|
|
return;
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)"), "Session Switched to fast forward mode");
|
|
|
|
bool success = true;
|
|
|
|
for (unsigned int i = 0; i < test_data.size(); i++) {
|
|
const char* data = test_data[i];
|
|
bool last = (i == (test_data.size() - 1));
|
|
if (sendCopyData(conn, data, strlen(data), last) == false) {
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ok(success, "Copy data transmission should be successful");
|
|
|
|
PGresult* res = PQgetResult(conn);
|
|
|
|
ok((PQresultStatus(res) == PGRES_COMMAND_OK), "Rows successfully inserted. %s", PQerrorMessage(conn));
|
|
|
|
const char* row_count_str = PQcmdTuples(res);
|
|
const int row_count = atoi(row_count_str);
|
|
|
|
ok(row_count == test_data.size(), "Total rows inserted: %d. Expected: %ld", row_count, test_data.size());
|
|
PQclear(res);
|
|
|
|
PQclear(PQgetResult(conn));
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\] Switching back to Normal mode \\(Session Type:0x06\\).*"), "Switching back to Normal mode");
|
|
ok(PQtransactionStatus(conn) == PQTRANS_INTRANS, "Connection should be in Transaction State");
|
|
|
|
if (!executeQueries(conn, { "ROLLBACK;" }))
|
|
return;
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief Tests the behavior of a transaction when a COPY FROM STDIN command fails.
|
|
*
|
|
* This function initiates a transaction, attempts to execute a COPY FROM STDIN command
|
|
* on a non-existent table, and verifies that the connection transitions to an error state.
|
|
* It also checks the ProxySQL logs to ensure that the session switches to fast forward mode
|
|
* and then back to normal mode.
|
|
*
|
|
* @param admin_conn Pointer to the admin connection.
|
|
* @param conn Pointer to the backend connection.
|
|
* @param f_proxysql_log Reference to the ProxySQL log file stream.
|
|
*/
|
|
void testSTDIN_TRANSACTION_ERROR(PGconn* admin_conn, PGconn* conn, std::fstream& f_proxysql_log) {
|
|
|
|
if (!executeQueries(conn, { "BEGIN;" }))
|
|
return;
|
|
|
|
ok(PQtransactionStatus(conn) == PQTRANS_INTRANS, "Connection should be in Transaction State");
|
|
ok(executeQueries(conn, { "COPY non_existent_table FROM STDIN (FORMAT TEXT)" }) == false, "Query should fail. %s", PQerrorMessage(conn));
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)"), "Session Switched to fast forward mode");
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\] Switching back to Normal mode \\(Session Type:0x06\\).*"), "Switching back to Normal mode");
|
|
ok(PQtransactionStatus(conn) == PQTRANS_INERROR, "Connection should be in Error Transaction State");
|
|
|
|
if (!executeQueries(conn, { "ROLLBACK;" }))
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* @brief Tests the COPY IN and COPY OUT functionality using a file.
|
|
*
|
|
* This function first tests the COPY IN functionality using text format.
|
|
* It then performs a COPY OUT operation to a file, verifies that the session
|
|
* does not switch to fast forward mode, truncates the table, and performs a
|
|
* COPY IN operation from the file. Finally, it verifies that all test data
|
|
* entries are successfully copied into the database.
|
|
*
|
|
* @param admin_conn The connection to the admin database.
|
|
* @param conn The connection to the target database.
|
|
* @param f_proxysql_log The log file stream for ProxySQL logs.
|
|
*/
|
|
void testSTDIN_FILE(PGconn* admin_conn, PGconn* conn, std::fstream& f_proxysql_log) {
|
|
testSTDIN_TEXT_FORMAT(admin_conn, conn, f_proxysql_log);
|
|
|
|
if (!executeQueries(conn, { "COPY copy_in_test(column1,column2,column3,column4,column5) TO '/tmp/copy_in_test.txt' (FORMAT TEXT)" }))
|
|
return;
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)") == false, "Session should NOT Switch to fast forward mode");
|
|
|
|
if (!executeQueries(conn, { "TRUNCATE TABLE copy_in_test" }))
|
|
return;
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)") == false, "Session should NOT Switch to fast forward mode");
|
|
|
|
if (!executeQueries(conn, { "COPY copy_in_test(column1,column2,column3,column4,column5) FROM '/tmp/copy_in_test.txt' (FORMAT TEXT)" }))
|
|
return;
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)") == false, "Session should NOT Switch to fast forward mode");
|
|
|
|
PGresult* res = PQexec(conn, "SELECT column1,column2,column3,column4,column5 FROM copy_in_test");
|
|
|
|
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
|
|
fprintf(stderr, "Query failed: %s", PQerrorMessage(conn));
|
|
PQclear(res);
|
|
return;
|
|
}
|
|
|
|
// Verify each test data entry
|
|
bool all_found = true;
|
|
for (const char* data : test_data) {
|
|
if (!is_string_in_result(res, data)) {
|
|
all_found = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ok(all_found == true, "All test data successfully verified in the database!");
|
|
|
|
// Cleanup
|
|
PQclear(res);
|
|
}
|
|
|
|
/**
|
|
* @brief Test COPY FROM STDIN functionality with a multistatement query.
|
|
*
|
|
* This function sends a multistatement query to the PostgreSQL server, which includes a SELECT statement
|
|
* followed by a COPY FROM STDIN statement. It then verifies the results of the SELECT statement, sends
|
|
* data to be copied into the table, and verifies that the data was successfully inserted.
|
|
*
|
|
* @param admin_conn Pointer to the admin connection.
|
|
* @param conn Pointer to the PostgreSQL connection.
|
|
* @param f_proxysql_log Reference to the ProxySQL log file stream.
|
|
*/
|
|
void testSTDIN_MULTISTATEMENT(PGconn* admin_conn, PGconn* conn, std::fstream& f_proxysql_log) {
|
|
// Multistatement query: First a SELECT, then COPY TO STDIN
|
|
const char* query = "SELECT 1; COPY copy_in_test(column1,column2,column3,column4,column5) FROM STDIN (FORMAT TEXT);";
|
|
if (PQsendQuery(conn, query) == 0) {
|
|
fprintf(stderr, "Error sending query: %s\n", PQerrorMessage(conn));
|
|
return;
|
|
}
|
|
|
|
PQconsumeInput(conn);
|
|
while (PQisBusy(conn)) {
|
|
PQconsumeInput(conn);
|
|
}
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)"), "Session Switched to fast forward mode");
|
|
|
|
// Check first result (SELECT statement)
|
|
PGresult* res = PQgetResult(conn);
|
|
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
|
|
fprintf(stderr, "SELECT failed: %s\n", PQerrorMessage(conn));
|
|
PQclear(res);
|
|
return;
|
|
}
|
|
|
|
int rows = PQntuples(res);
|
|
ok(rows == 1, "Expected 1 row from SELECT. Actual: %d", rows);
|
|
|
|
// Check the data returned by SELECT
|
|
char* value = PQgetvalue(res, 0, 0);
|
|
ok(atoi(value) == 1, "Expected value 1 in first row");
|
|
PQclear(res); // Clear result
|
|
|
|
// Check second result (COPY FROM STDIN)
|
|
res = PQgetResult(conn);
|
|
if (PQresultStatus(res) != PGRES_COPY_IN) {
|
|
fprintf(stderr, "COPY IN failed: %s\n", PQerrorMessage(conn));
|
|
PQclear(res);
|
|
return;
|
|
}
|
|
|
|
bool success = true;
|
|
for (unsigned int i = 0; i < test_data.size(); i++) {
|
|
const char* data = test_data[i];
|
|
bool last = (i == (test_data.size() - 1));
|
|
if (!sendCopyData(conn, data, strlen(data), last)) {
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ok(success, "Copy data transmission should be successful");
|
|
PQclear(res); // Clear result
|
|
|
|
res = PQgetResult(conn);
|
|
ok((PQresultStatus(res) == PGRES_COMMAND_OK), "Rows successfully inserted. %s", PQerrorMessage(conn));
|
|
|
|
const char* row_count_str = PQcmdTuples(res);
|
|
int row_count = atoi(row_count_str);
|
|
ok(row_count == test_data.size(), "Total rows inserted: %d. Expected: %ld", row_count, test_data.size());
|
|
PQclear(res);
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\] Switching back to Normal mode \\(Session Type:0x06\\).*"), "Switching back to Normal mode");
|
|
|
|
// Cleanup
|
|
PQclear(PQgetResult(conn));
|
|
}
|
|
|
|
/**
|
|
* @brief Tests the COPY IN functionality with permanent fast forward mode.
|
|
*
|
|
* This function performs the following steps:
|
|
* 1. Updates the pgsql_users table to enable fast forward mode.
|
|
* 2. Loads the updated pgsql_users to runtime.
|
|
* 3. Creates a new backend connection.
|
|
* 4. Executes a COPY command to start copying data from STDIN.
|
|
* 5. Verifies that the session does not switch to fast forward mode.
|
|
* 6. Sends the test data to the backend connection.
|
|
* 7. Verifies that the data transmission is successful.
|
|
* 8. Checks that the rows are successfully inserted.
|
|
* 9. Verifies that the session does not switch back to normal mode.
|
|
* 10. Cleans up the result.
|
|
*
|
|
* @param admin_conn The connection to the admin database.
|
|
* @param conn The connection to the backend database.
|
|
* @param f_proxysql_log The log file stream for ProxySQL logs.
|
|
*/
|
|
void testSTDIN_PERMANENT_FAST_FORWARD(PGconn* admin_conn, PGconn* conn, std::fstream& f_proxysql_log) {
|
|
if (!executeQueries(admin_conn, {
|
|
"UPDATE pgsql_users SET fast_forward = 1",
|
|
"LOAD PGSQL USERS TO RUNTIME"
|
|
})) {
|
|
return;
|
|
}
|
|
|
|
PGConnPtr backend_conn = createNewConnection(ConnType::BACKEND, false);
|
|
|
|
if (!executeQueries(backend_conn.get(), {"COPY copy_in_test(column1,column2,column3,column4,column5) FROM STDIN (FORMAT TEXT)"})) {
|
|
return;
|
|
}
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)") == false, "Session should NOT Switch to fast forward mode");
|
|
|
|
bool success = true;
|
|
|
|
for (unsigned int i = 0; i < test_data.size(); i++) {
|
|
const char* data = test_data[i];
|
|
bool last = (i == (test_data.size() - 1));
|
|
if (sendCopyData(backend_conn.get(), data, strlen(data), last) == false) {
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ok(success == true, "Copy data transmission should be successful");
|
|
|
|
PGresult* res = PQgetResult(backend_conn.get());
|
|
|
|
ok((PQresultStatus(res) == PGRES_COMMAND_OK), "Rows successfully inserted. %s", PQerrorMessage(backend_conn.get()));
|
|
|
|
const char* row_count_str = PQcmdTuples(res);
|
|
const int row_count = atoi(row_count_str);
|
|
|
|
ok(row_count == test_data.size(), "Total rows inserted: %d. Expected: %ld", row_count, test_data.size());
|
|
PQclear(res);
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\] Switching back to Normal mode \\(Session Type:0x06\\).*") == false, "Should NOT Switch back to Normal mode");
|
|
|
|
// Cleanup
|
|
PQclear(PQgetResult(backend_conn.get()));
|
|
}
|
|
|
|
/**
|
|
* @brief Tests the COPY IN functionality using STDOUT in TEXT format.
|
|
*
|
|
* This function executes a COPY IN command to insert data into a PostgreSQL table
|
|
* using the STDOUT method in TEXT format. It verifies the success of the data transmission
|
|
* and checks the logs for specific commands to ensure the session mode switches correctly.
|
|
*
|
|
* @param admin_conn A pointer to the admin PGconn connection.
|
|
* @param conn A pointer to the PGconn connection used for the COPY IN operation.
|
|
* @param f_proxysql_log A reference to the fstream object for ProxySQL logs.
|
|
*/
|
|
void testSTDOUT_TEXT_FORMAT(PGconn* admin_conn, PGconn* conn, std::fstream& f_proxysql_log) {
|
|
if (!executeQueries(conn, { "COPY copy_in_test(column1,column2,column3,column4,column5) FROM STDOUT" }))
|
|
return;
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\].* Switching to Fast Forward mode \\(Session Type:0x06\\)"), "Session Switched to fast forward mode");
|
|
|
|
bool success = true;
|
|
|
|
for (unsigned int i = 0; i < test_data.size(); i++) {
|
|
const char* data = test_data[i];
|
|
bool last = (i == (test_data.size() - 1));
|
|
if (!sendCopyData(conn, data, strlen(data), last)) {
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ok(success, "Copy data transmission should be successful");
|
|
|
|
PGresult* res = PQgetResult(conn);
|
|
|
|
ok((PQresultStatus(res) == PGRES_COMMAND_OK), "Rows successfully inserted. %s", PQerrorMessage(conn));
|
|
|
|
const char* row_count_str = PQcmdTuples(res);
|
|
const int row_count = atoi(row_count_str);
|
|
|
|
ok(row_count == test_data.size(), "Total rows inserted: %d. Expected: %ld", row_count, test_data.size());
|
|
PQclear(res);
|
|
|
|
ok(check_logs_for_command(f_proxysql_log, ".*\\[INFO\\] Switching back to Normal mode \\(Session Type:0x06\\).*"), "Switching back to Normal mode");
|
|
}
|
|
|
|
std::vector<std::pair<std::string, void (*)(PGconn*, PGconn*, std::fstream& f_proxysql_log)>> tests = {
|
|
{ "COPY ... FROM STDIN Text Format", testSTDIN_TEXT_FORMAT },
|
|
{ "COPY ... FROM STDIN Binary Format", testSTDIN_TEXT_BINARY },
|
|
{ "COPY ... FROM STDIN Error", testSTDIN_ERROR },
|
|
{ "COPY ... FROM STDIN Transaction", testSTDIN_TRANSACTION },
|
|
{ "COPY ... FROM STDIN Transaction Error", testSTDIN_TRANSACTION_ERROR },
|
|
{ "COPY ... FROM STDIN File", testSTDIN_FILE },
|
|
{ "COPY ... FROM STDIN Multistatement", testSTDIN_MULTISTATEMENT },
|
|
{ "COPY ... FROM STDOUT Text Format", testSTDOUT_TEXT_FORMAT },
|
|
{ "COPY ... FROM STDIN Permanent Fast Forward", testSTDIN_PERMANENT_FAST_FORWARD }
|
|
};
|
|
|
|
void execute_tests(bool with_ssl, bool diff_conn) {
|
|
|
|
PGConnPtr admin_conn_1 = createNewConnection(ConnType::ADMIN, with_ssl);
|
|
|
|
if (!executeQueries(admin_conn_1.get(), {
|
|
"DELETE FROM pgsql_query_rules",
|
|
"LOAD PGSQL QUERY RULES TO RUNTIME",
|
|
"UPDATE pgsql_users SET fast_forward=0" ,
|
|
"LOAD PGSQL USERS TO RUNTIME"
|
|
}))
|
|
return;
|
|
|
|
std::string f_path{ get_env("REGULAR_INFRA_DATADIR") + "/proxysql.log" };
|
|
std::fstream f_proxysql_log{};
|
|
|
|
int of_err = open_file_and_seek_end(f_path, f_proxysql_log);
|
|
if (of_err != EXIT_SUCCESS) {
|
|
return;
|
|
}
|
|
|
|
if (diff_conn == false) {
|
|
PGConnPtr admin_conn = createNewConnection(ConnType::ADMIN, with_ssl);
|
|
PGConnPtr backend_conn = createNewConnection(ConnType::BACKEND, with_ssl);
|
|
|
|
if (!admin_conn || !backend_conn) {
|
|
BAIL_OUT("Error: failed to connect to the database in file %s, line %d\n", __FILE__, __LINE__);
|
|
return;
|
|
}
|
|
|
|
for (const auto& test : tests) {
|
|
if (!setupTestTable(backend_conn.get()))
|
|
return;
|
|
|
|
diag(">>>> Running %s - Shared Connection: %s <<<<", test.first.c_str(), !diff_conn ? "True" : "False");
|
|
test.second(admin_conn.get(), backend_conn.get(), f_proxysql_log);
|
|
f_proxysql_log.clear(f_proxysql_log.rdstate() & ~std::ios_base::failbit);
|
|
f_proxysql_log.seekg(f_proxysql_log.tellg());
|
|
diag(">>>> Done <<<<");
|
|
}
|
|
}
|
|
else {
|
|
for (const auto& test : tests) {
|
|
diag(">>>> Running %s - Shared Connection: %s <<<<", test.first.c_str(), diff_conn ? "False" : "True");
|
|
|
|
PGConnPtr admin_conn = createNewConnection(ConnType::ADMIN, with_ssl);
|
|
PGConnPtr backend_conn = createNewConnection(ConnType::BACKEND, with_ssl);
|
|
|
|
if (!admin_conn || !backend_conn) {
|
|
BAIL_OUT("Error: failed to connect to the database in file %s, line %d\n", __FILE__, __LINE__);
|
|
return;
|
|
}
|
|
|
|
if (!setupTestTable(backend_conn.get()))
|
|
return;
|
|
|
|
test.second(admin_conn.get(), backend_conn.get(), f_proxysql_log);
|
|
f_proxysql_log.clear(f_proxysql_log.rdstate() & ~std::ios_base::failbit);
|
|
f_proxysql_log.seekg(f_proxysql_log.tellg());
|
|
diag(">>>> Done <<<<");
|
|
}
|
|
}
|
|
|
|
f_proxysql_log.close();
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
|
|
plan(51 * 2); // Total number of tests planned
|
|
|
|
if (cl.getEnv())
|
|
return exit_status();
|
|
|
|
execute_tests(true, false);
|
|
execute_tests(false, false);
|
|
|
|
return exit_status();
|
|
}
|