// NOSONAR - TAP test files do not need to follow the same rules as production code /** * @file pgsql-extended_query_protocol_test-t.cpp * @brief This TAP test suite verifies the correct handling of PostgreSQL's Extended Query Protocol * through ProxySQL. It includes comprehensive tests for Parse, Bind, Execute, Describe, and Close message flows, * ensuring compliance with protocol semantics and robustness under edge cases. */ #include #include #include #include #include #include #include #include "libpq-fe.h" #include "pg_lite_client.h" #include "command_line.h" #include "tap.h" #include "utils.h" CommandLine cl; int test_count = 1; using PGConnPtr = std::unique_ptr; using PGResultPtr = std::unique_ptr; enum ConnType { ADMIN, BACKEND }; PGConnPtr createNewConnection(ConnType conn_type, const std::string& options = "", bool with_ssl = false) { const char* host = (conn_type == BACKEND) ? cl.pgsql_host : cl.pgsql_admin_host; int port = (conn_type == BACKEND) ? cl.pgsql_port : cl.pgsql_admin_port; const char* username = (conn_type == BACKEND) ? cl.pgsql_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"); 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); } bool executeQueries(PGconn* conn, const std::vector& queries) { auto fnResultType = [](const char* query) -> int { const char* fs = strchr(query, ' '); // NOSONAR: strlen is safe here as we control the input size_t qtlen = strlen(query); // NOSONAR 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) { return PGRES_COPY_OUT; } 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\n", query.c_str(), PQerrorMessage(conn)); PQclear(res); return false; } PQclear(res); } return true; } 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() ? false : true; } std::shared_ptr create_connection() { auto conn = std::make_shared(5000); try { conn->connect(cl.pgsql_host, cl.pgsql_port, cl.pgsql_username, cl.pgsql_username, cl.pgsql_password); } catch (const PgException& e) { diag("Connection failed: %s", e.what()); return nullptr; } return conn; } bool has_immediate_response(int sock) { if (sock < 0) return false; // Save current socket flags int flags = fcntl(sock, F_GETFL, 0); if (flags == -1) return false; // Set non-blocking mode if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) { return false; } // Try to read one byte (without removing from buffer) char dummy; ssize_t n = recv(sock, &dummy, 1, MSG_PEEK | MSG_DONTWAIT); // Restore original flags fcntl(sock, F_SETFL, flags); if (n > 0) { return true; // Data available } else if (n == 0) { return true; // Connection closed } else { // Check if error was due to no data return (errno != EAGAIN && errno != EWOULDBLOCK); } } /*bool no_immediate_response(int sock) { fd_set read_fds; FD_ZERO(&read_fds); FD_SET(sock, &read_fds); timeval timeout{}; timeout.tv_sec = 0; timeout.tv_usec = 100000; // 100ms timeout return select(sock + 1, &read_fds, nullptr, nullptr, &timeout) == 0; }*/ void test_parse_without_sync() { diag("Test %d: Parse without sync should not respond", test_count++); auto conn = create_connection(); if (!conn) return; try { // Get raw socket for timeout check int sock = conn->getSocket(); // Prepare without sync conn->prepareStatement("test_stmt", "SELECT 1", false); // Check for immediate response (should timeout) ok(!has_immediate_response(sock), "No response after parse without sync"); // Now sync and verify completion conn->sendSync(); char type; bool got_ready = 0; int parse_count = 0; int other_count = 0; while (!got_ready) { std::vector buffer; conn->readMessage(type, buffer); if (type == PgConnection::PARSE_COMPLETE) { parse_count++; } else if (type == PgConnection::READY_FOR_QUERY) { got_ready = true; } else { other_count++; } } ok(parse_count == 1, "Received parse complete after sync (%d/1)", parse_count); ok(got_ready, "Received ready packet after sync"); ok(other_count == 0, "No other messages received after sync (%d)", other_count); } catch (const PgException& e) { ok(false, "Parse without sync test failed with errpr: %s", e.what()); } } void test_parse_with_sync() { diag("Test %d: Parse with sync should respond immediately", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("test_stmt", "SELECT 1", true); ok(true, "Parse completes with sync enabled"); } catch (const PgException& e) { ok(false, "Parse with sync test failed with error: %s", e.what()); } } void test_parse_use_same_stmt_name() { diag("Test %d: Parse using same statement name", test_count++); auto conn = create_connection(); if (!conn) return; try { char type; std::vector buffer; { conn->prepareStatement("test_stmt_multi", "SELECT $1::int", false); conn->describeStatement("test_stmt_multi", false); conn->sendSync(); // Read the parse complete for the first statement conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received parse complete for first parse"); // Read the describe complete for the first statement conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description for first parse"); // Read parameter description BufferReader reader(buffer); int num_params = reader.readInt16(); ok(num_params == 1, "Received 1 parameter description for first parse"); // Read parameter type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Parameter type OID is 23 (int)"); conn->waitForReady(); } { conn->prepareStatement("test_stmt_multi", "SELECT $1::text", false); conn->sendSync(); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for second parse with same name"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "42P05", "Received ERRCODE_DUPLICATE_PSTATEMENT Error:%s", errormsg.c_str()); // Now read the ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after multiple parse"); } { conn->describeStatement("test_stmt_multi", false); conn->sendSync(); // Read the describe complete for the second statement conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description for second parse"); // Read parameter description BufferReader reader(buffer); int num_params = reader.readInt16(); ok(num_params == 1, "Received 1 parameter description for second parse"); // Read parameter type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Parameter type OID is 23 (int)"); conn->waitForReady(); } { conn->closeStatement("test_stmt_multi", false); conn->sendSync(); // Read the close complete for the statement conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "Received close complete for statement"); // Now read the ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after close statement"); } { conn->prepareStatement("test_stmt_multi", "SELECT $1::text", false); conn->describeStatement("test_stmt_multi", false); conn->sendSync(); conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received parse complete for second parse with same name"); // Read the describe complete for the second statement conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description for second parse with same name"); // Read parameter description BufferReader reader(buffer); int num_params = reader.readInt16(); ok(num_params == 1, "Received 1 parameter description for second parse with same name"); // Read parameter type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 25, "Parameter type OID is 25 (text)"); conn->waitForReady(); } } catch (const PgException& e) { ok(false, "Parse using same statement name failed with error: %s", e.what()); } } void test_parse_use_unnamed_stmt() { diag("Test %d: Parse using unnamed statement", test_count++); auto conn = create_connection(); if (!conn) return; try { char type; std::vector buffer; { conn->prepareStatement("", "SELECT $1::int", false); conn->describeStatement("", false); conn->sendSync(); // Read the parse complete for the first statement conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received parse complete for first parse"); // Read the describe complete for the first statement conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description for first parse"); // Read parameter description BufferReader reader(buffer); int num_params = reader.readInt16(); ok(num_params == 1, "Received 1 parameter description for first parse"); // Read parameter type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Parameter type OID is 23 (int)"); conn->waitForReady(); } { conn->prepareStatement("", "SELECT $1::text", false); conn->describeStatement("", false); conn->sendSync(); // Read the parse complete for the first statement conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received parse complete for second parse"); // Read the describe complete for the first statement conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description for second parse"); // Read parameter description BufferReader reader(buffer); int num_params = reader.readInt16(); ok(num_params == 1, "Received 1 parameter description for first parse"); // Read parameter type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 25, "Parameter type OID is 25 (text)"); conn->waitForReady(); } } catch (const PgException& e) { ok(false, "Parse using unnamed statement with error: %s", e.what()); } } void test_malformed_packet() { diag("Test %d: Malformed parse packet", test_count++); auto conn = create_connection(); if (!conn) return; try { // Send garbage instead of parse message std::vector garbage{ 0xDE, 0xAD, 0xBE, 0xEF }; conn->sendMessage('P', garbage); // Should get error response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for malformed packet"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "08P01", "Received ERRCODE_PROTOCOL_VIOLATION Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(false, "Session should be terminated by server"); } catch (const PgException& e) { ok(true, "Session should be terminated error: %s", e.what()); } } void test_empty_query() { diag("Test %d: Empty query string", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("empty_stmt", "", true); ok(true, "Empty query should succeed"); } catch (const PgException& e) { ok(false, "Empty query string failed with error: %s", e.what()); } } void test_multiple_parse() { diag("Test %d: Multiple parse without sync", test_count++); auto conn = create_connection(); if (!conn) return; try { // Send multiple parse commands conn->prepareStatement("stmt1", "SELECT 1", false); conn->prepareStatement("stmt2", "SELECT 2", false); conn->prepareStatement("stmt3", "SELECT 3", false); // Send single sync conn->sendSync(); // Should get 3 parse complete messages char type; bool got_ready = false; int parse_count = 0; int other_count = 0; while (!got_ready) { std::vector buffer; conn->readMessage(type, buffer); if (type == PgConnection::PARSE_COMPLETE) { parse_count++; } else if (type == PgConnection::READY_FOR_QUERY) { got_ready = true; } else { other_count++; } } ok(parse_count == 3, "Received all parse completes (%d/3)", parse_count); ok(got_ready, "Received ready packet after multiple parse"); ok(other_count == 0, "No other messages received after multiple parse (%d)", other_count); } catch (const PgException& e) { ok(false, "Multiple parse test faile with error: %s", e.what()); } } void test_only_sync() { diag("Test %d: Sending only sync", test_count++); auto conn = create_connection(); if (!conn) return; try { // Send single sync conn->sendSync(); // Should get 3 parse complete messages char type; bool got_ready = false; int other_count = 0; while (!got_ready) { std::vector buffer; conn->readMessage(type, buffer); if (type == PgConnection::READY_FOR_QUERY) { got_ready = true; } else { other_count++; } } ok(got_ready, "Received ready packet after sync"); ok(other_count == 0, "No other messages received after sync (%d)", other_count); } catch (const PgException& e) { ok(false, "Sending only sync test failed with error: %s", e.what()); } } void test_empty_stmt() { diag("Test %d: Empty statement", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("", "SELECT 1", true); ok(true, "Empty statmement should succeed"); } catch (const PgException& e) { ok(false, "Empty stmt failed with error: %s", e.what()); } } void test_prepare_statement_mix() { diag("Test %d: Prepare statement + Simple Query", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("test_stmt_mix", "SELECT 1", false); conn->sendQuery("SELECT 2"); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received parse complete for prepared statement"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description for query"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received row data for query"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received command completion for query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); // Now send sync conn->sendSync(); // Should get ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after sync"); } catch (const PgException& e) { ok(false, "Prepare statement + Simple Query failed with error: %s", e.what()); } } void test_extended_query_prepared_describe_execute_simple_query_without_sync() { diag("Test %d: Extended Query (Prepare, Describe, Execute) + Simple Query without sync", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare without sync conn->prepareStatement("test_stmt_eq", "SELECT $1::int + 1", false); conn->describeStatement("test_stmt_eq", false); PgConnection::Param param = { "41", 0 }; conn->bindStatement("test_stmt_eq", "", { param }, { 0 }, false); conn->executePortal("", 0, false); // Now send simple query without sync conn->sendQuery("SELECT 2"); // Finally send sync conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received parse complete for prepared statement"); conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description for prepared statement"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description for prepared statement"); { BufferReader reader(buffer); // Read row description int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Row description has 1 field (%d/1)", fieldCount); // Read field name std::string fieldName = reader.readString(); ok(fieldName == "?column?", "Field name is '?column?'"); // Read field table OID unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); // Read field attribute number unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Parameter type OID is 23 (int)"); // Read field type size unsigned int typeSize = reader.readInt16(); ok(typeSize == 4, "Field type size is 4 (integer size)"); // Read field type modifier unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); // Read field format code unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); } conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received bind complete for prepared statement"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received data row for prepared statement"); { BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Received 1 field in data row for prepared statement"); int valueLen = reader.readInt32(); ok(valueLen == 2, "Field length is 2 bytes"); auto val = reader.readBytes(2); ok(val[0] == '4' && val[1] == '2' , "Received correct value 42 from prepared statement"); } conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received command complete for prepared statement"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description for simple query"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received data row for simple query"); { BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Received 1 field in data row for simple query"); int valueLen = reader.readInt32(); ok(valueLen == 1, "Field length is 1 byte"); uint8_t val = reader.readByte(); ok(val == '2', "Received correct value 2 from simple query"); } conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received command complete for simple query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after all commands"); } catch (const PgException& e) { ok(false, "Extended Query + Simple Query without sync failed with error: %s", e.what()); } } void test_invalid_query_parse_packet() { diag("Test %d: Invalid query in parse packet", test_count++); auto conn = create_connection(); if (!conn) return; try { // Send multiple parse commands conn->prepareStatement("invalid_stmt_test", "SELECT * FROM dummy_table", false); // Send single sync conn->sendSync(); // Should get 3 parse complete messages char type; int error_count = 0; bool got_ready = false; int other_count = 0; std::string errormsg; std::string errorcode; while (!got_ready) { std::vector buffer; conn->readMessage(type, buffer); if (type == PgConnection::ERROR_RESPONSE) { error_count++; BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') { errormsg = reader.readString(); } else if (field == 'C') { errorcode = reader.readString(); } else { reader.readString(); // Skip other fields } } } else if (type == PgConnection::READY_FOR_QUERY) { got_ready = true; } else { other_count++; } } ok(error_count == 1, "Received error response (%d)", error_count); ok(errorcode == "42P01", "Received undefined table error code: %s", errorcode.c_str()); ok(errormsg.find("relation \"dummy_table\" does not exist") != std::string::npos, "Received expected error message: %s", errormsg.c_str()); ok(got_ready, "Got ready for query packet"); ok(other_count == 0, "No other messages received (%d)", other_count); } catch (const PgException& e) { ok(false, "Invalid query in parse packet failed with error: %s", e.what()); } } bool test_text_binary_mix() { PGconn* conn = PQconnectdb("host=localhost dbname=postgres user=postgres password=postgres sslmode='disable'"); ok(PQstatus(conn) == CONNECTION_OK, "Connected to database"); PGresult* res; // Setup: ensure table exists res = PQexec(conn, "CREATE TEMP TABLE test_bin_text(id integer)"); PQclear(res); res = PQexec(conn, "INSERT INTO test_bin_text VALUES (42)"); PQclear(res); // 1. Prepare statement with declared parameter type as 'text' res = PQprepare(conn, "stmt1", "SELECT * FROM test_bin_text WHERE id = $1", 1, nullptr); // 25 = TEXTOID if (PQresultStatus(res) != PGRES_COMMAND_OK) { diag("Prepare failed: %s", PQerrorMessage(conn)); } ok(PQresultStatus(res) == PGRES_COMMAND_OK, "Prepared statement with param type text"); PQclear(res); res = PQdescribePrepared(conn, "stmt1"); PQclear(res); // 2. Attempt to bind binary-formatted int32 to text param int32_t intval = htonl(42); // Network byte order const char* paramValues[1] = { (char*)&intval }; int paramLengths[1] = { sizeof(intval) }; int paramFormats[1] = { 1 }; // Binary format Oid resultFormat = 0; res = PQexecPrepared(conn, "stmt1", 1, paramValues, paramLengths, paramFormats, resultFormat); if (PQresultStatus(res) == PGRES_TUPLES_OK) { diag("Unexpectedly succeeded: binary int bound to text param"); } else { diag("Expected failure: %s", PQerrorMessage(conn)); } ok(PQresultStatus(res) != PGRES_TUPLES_OK, "Binary format to text param fails as expected"); PQclear(res); res = PQdescribePrepared(conn, "stmt1"); PQclear(res); const char* paramValues1[1] = { "42" }; res = PQexecPrepared(conn, "stmt1", 1, paramValues1, 0, NULL, 0); PQclear(res); res = PQdescribePrepared(conn, "stmt1"); PQclear(res); PQfinish(conn); return 0; } bool test_text_binary_mix2() { PGconn* conn = PQconnectdb("host=localhost dbname=postgres user=postgres password=postgres sslmode='disable'"); ok(PQstatus(conn) == CONNECTION_OK, "Connected to database"); PGresult* res; // 1. Prepare statement with declared parameter type as 'text' res = PQprepare(conn, "stmt1", "SELECT $1", 1, nullptr); // 25 = TEXTOID if (PQresultStatus(res) != PGRES_COMMAND_OK) { diag("Prepare failed: %s", PQerrorMessage(conn)); } ok(PQresultStatus(res) == PGRES_COMMAND_OK, "Prepared statement with param type text"); PQclear(res); res = PQdescribePrepared(conn, "stmt1"); PQclear(res); // 2. Attempt to bind binary-formatted int32 to text param int32_t intval = htonl(42); // Network byte order const char* paramValues[1] = { (char*)&intval }; int paramLengths[1] = { sizeof(intval) }; int paramFormats[1] = { 1 }; // Binary format Oid resultFormat = 0; res = PQexecPrepared(conn, "stmt1", 1, paramValues, paramLengths, paramFormats, resultFormat); if (PQresultStatus(res) == PGRES_TUPLES_OK) { diag("Unexpectedly succeeded: binary int bound to text param"); } else { diag("Expected failure: %s", PQerrorMessage(conn)); } ok(PQresultStatus(res) != PGRES_TUPLES_OK, "Binary format to text param fails as expected"); PQclear(res); res = PQdescribePrepared(conn, "stmt1"); PQclear(res); const char* paramValues1[1] = { "42" }; res = PQexecPrepared(conn, "stmt1", 1, paramValues1, 0, NULL, 0); PQclear(res); res = PQdescribePrepared(conn, "stmt1"); PQclear(res); PQfinish(conn); return 0; } void test_describe_existing_statement() { diag("Test %d: Describe existing prepared statement", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare a valid statement conn->prepareStatement("valid_stmt", "SELECT 1", true); // Describe the prepared statement conn->describeStatement("valid_stmt", true); // Verify response char type; std::vector buffer; { conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description"); // Read parameter description BufferReader reader(buffer); int paramCount = reader.readInt16(); ok(paramCount == 0, "No parameters in prepared statement (%d/0)", paramCount); } { conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description"); BufferReader reader(buffer); // Read row description int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Row description has 1 field (%d/1)", fieldCount); // Read field name std::string fieldName = reader.readString(); ok(fieldName == "?column?", "Field name is '?column?'"); // Read field table OID unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); // Read field attribute number unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); // Read field type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Field type OID is 23 (integer)"); // Read field type size unsigned int typeSize = reader.readInt16(); ok(typeSize == 4, "Field type size is 4 (integer size)"); // Read field type modifier unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); // Read field format code unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); } // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after describe"); } catch (const PgException& e) { ok(false, "Describe existing prepared statement failed with error: %s", e.what()); } } void test_describe_nonexistent_statement() { diag("Test %d: Describe non-existent prepared statement", test_count++); auto conn = create_connection(); if (!conn) return; try { // Describe unknown statement conn->describeStatement("ghost_stmt", true); // Should get error response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for non-existent statement"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after describe non-existent statement"); } catch (const PgException& e) { ok(false, "Describe non-existent prepared statement failed with error: %s", e.what()); } } void test_describe_without_sync() { diag("Test %d: Describe without sync", test_count++); auto conn = create_connection(); if (!conn) return; try { int sock = conn->getSocket(); conn->prepareStatement("async_stmt", "SELECT 1", false); conn->describeStatement("async_stmt", false); // Shouldn't get immediate response ok(!has_immediate_response(sock), "No immediate response after describe without sync"); conn->sendSync(); char type; std::vector buffer; int parse_count = 0; int param_desc_count = 0; int row_desc_count = 0; bool gotReady = false; while (!gotReady) { conn->readMessage(type, buffer); if (type == PgConnection::PARSE_COMPLETE) { parse_count++; } else if (type == PgConnection::PARAMETER_DESCRIPTION) { param_desc_count++; } else if (type == PgConnection::ROW_DESCRIPTION) { row_desc_count++; } else if (type == PgConnection::READY_FOR_QUERY) { gotReady = true; } else if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); std::string errorMsg; char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errorMsg = reader.readString(); else reader.readString(); } throw PgException("Error: " + errorMsg); } } ok(parse_count == 1, "Received ParseComplete (%d/1)", parse_count); ok(param_desc_count == 1, "Received ParameterDescription (%d/1)", param_desc_count); ok(row_desc_count == 1, "Received RowDescription (%d/1)", row_desc_count); ok(gotReady, "Sync completed after describe"); } catch (const PgException& e) { ok(false, "Describe without sync failed with error:%s", e.what()); } } void test_describe_malformed_packet() { diag("Test %d: Malformed describe packet", test_count++); auto conn = create_connection(); if (!conn) return; try { // Send garbage describe message std::vector garbage{ 'X' }; conn->sendMessage('D', garbage); // Should get error response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for malformed packet"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "08P01", "Received ERRCODE_PROTOCOL_VIOLATION Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(false, "Session should be terminated by server"); } catch (const PgException& e) { ok(true, "Session should be terminated error: %s", e.what()); } } void test_describe_after_close_statement() { diag("Test %d: Describe after statement close", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("temp_stmt", "SELECT 1", true); conn->closeStatement("temp_stmt", true); conn->describeStatement("temp_stmt", true); // Should get error response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error for closed statement"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after closed statement"); } catch (const PgException& e) { ok(false, "Describe after statement close failed with error: %s", e.what()); } } void test_multiple_describe_calls() { diag("Test %d: Multiple describe calls", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("multi_desc", "SELECT 1", true); // First describe conn->describeStatement("multi_desc", false); // Second describe conn->describeStatement("multi_desc", false); conn->sendSync(); int param_desc_count = 0; int desc_count = 0; int other_count = 0; char type; bool got_ready = false; while (!got_ready) { std::vector buffer; conn->readMessage(type, buffer); if (type == PgConnection::PARAMETER_DESCRIPTION) { param_desc_count++; } else if (type == PgConnection::ROW_DESCRIPTION) { desc_count++; } else if (type == PgConnection::READY_FOR_QUERY) { got_ready = true; } else { other_count++; } } ok(param_desc_count == 2, "Received parameter description (%d/2)", param_desc_count); ok(desc_count == 2, "Received description packets (%d/2)", desc_count); ok(got_ready, "Received ready for query after multiple describes"); ok(other_count == 0, "No other messages received after multiple describes (%d)", other_count); } catch (const PgException& e) { ok(false, "Multiple describe calls failed with error: %s", e.what()); } } void test_describe_parameter_types() { diag("Test %d: Verify parameter type reporting", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare statement with multiple parameter types conn->prepareStatement("param_types", "SELECT $1::int, $2::text, $3::bool", true); // Describe prepared statement conn->describeStatement("param_types", true); // Verify parameter description char type; std::vector param_buffer; conn->readMessage(type, param_buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description"); // Parse parameter OIDs (format: [count] + [oids]) if (param_buffer.size() >= 2) { int16_t num_params = (param_buffer[0] << 8) | param_buffer[1]; ok(num_params == 3, "Three parameters reported"); // Verify OIDs (int=23, text=25, bool=16) if (num_params == 3 && param_buffer.size() >= 8) { uint32_t oid1 = (param_buffer[2] << 24) | (param_buffer[3] << 16) | (param_buffer[4] << 8) | param_buffer[5]; uint32_t oid2 = (param_buffer[6] << 24) | (param_buffer[7] << 16) | (param_buffer[8] << 8) | param_buffer[9]; uint32_t oid3 = (param_buffer[10] << 24) | (param_buffer[11] << 16) | (param_buffer[12] << 8) | param_buffer[13]; ok(oid1 == 23, "Parameter 1 type is int (OID: %u)", oid1); ok(oid2 == 25, "Parameter 2 type is text (OID: %u)", oid2); ok(oid3 == 16, "Parameter 3 type is bool (OID: %u)", oid3); } else { ok(false, "Invalid parameter description size"); } } else { ok(false, "Invalid parameter description size"); } // Read row description (should be empty for this query) std::vector row_buffer; conn->readMessage(type, row_buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description after parameter description"); // Verify no fields in row description BufferReader reader(row_buffer); int16_t num_fields = reader.readInt16(); ok(num_fields == 3, "No fields in row description (%d/3)", num_fields); // First field metadata // Read field name std::string fieldName = reader.readString(); ok(fieldName == "int4", "Field name is 'int4'"); // Read field table OID unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); // Read field attribute number unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); // Read field type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Field type OID is 23 (integer)"); // Read field type size unsigned int typeSize = reader.readInt16(); ok(typeSize == 4, "Field type size is 4 (integer size)"); // Read field type modifier unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); // Read field format code unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); // Second field metadata fieldName = reader.readString(); ok(fieldName == "text", "Field name is 'text'"); tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); typeOid = reader.readInt32(); ok(typeOid == 25, "Field type OID is 25 (text)"); typeSize = reader.readInt16(); ok(typeSize == -1, "Field type size is -1 (variable length)"); typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); // Third field metadata fieldName = reader.readString(); ok(fieldName == "bool", "Field name is 'bool'"); tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); typeOid = reader.readInt32(); ok(typeOid == 16, "Field type OID is 16 (boolean)"); typeSize = reader.readInt16(); ok(typeSize == 1, "Field type size is 1 (boolean size)"); typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); // Read ready for query conn->readMessage(type, row_buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after parameter description"); } catch (const PgException& e) { ok(false, "Parameter type verification failed with error:%s", e.what()); } } void test_describe_result_metadata() { diag("Test %d: Verify result metadata accuracy", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare complex query conn->prepareStatement("result_meta", "SELECT 1::int AS id, 'test'::text AS name, true::bool AS flag", true); // Describe prepared statement conn->describeStatement("result_meta", true); char type; std::vector buffer; // Read parameter description { conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description"); BufferReader reader(buffer); int16_t param_count = reader.readInt16(); for (int i = 0; i < param_count; i++) { reader.readInt32(); // Skip parameter type OID } } // Read row description int16_t num_fields = 0; std::vector> fields; conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description"); BufferReader reader(buffer); num_fields = reader.readInt16(); ok(num_fields == 3, "Three result columns"); for (int i = 0; i < num_fields; i++) { std::string name = reader.readString(); reader.readInt32(); // Skip table OID reader.readInt16(); // Skip column attr num uint32_t type_oid = reader.readInt32(); reader.readInt16(); // Skip type size reader.readInt32(); // Skip type modifier reader.readInt16(); // Skip format fields.emplace_back(name, type_oid); } // Verify metadata ok(fields.size() == 3, "Result has 3 fields (%zu/3)", fields.size()); ok(std::get<0>(fields[0]) == "id" && std::get<1>(fields[0]) == 23, "Field 1: id (OID: %u)", std::get<1>(fields[0])); ok(std::get<0>(fields[1]) == "name" && std::get<1>(fields[1]) == 25, "Field 2: name (OID: %u)", std::get<1>(fields[1])); ok(std::get<0>(fields[2]) == "flag" && std::get<1>(fields[2]) == 16, "Field 3: flag (OID: %u)", std::get<1>(fields[2])); // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after result metadata"); } catch (const PgException& e) { ok(false, "Result metadata verification failed with error:%s", e.what()); } } void test_describe_prepared_noname() { diag("Test %d: Describe prepared with noname statement", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare a statement without a name conn->prepareStatement("", "SELECT 1", true); // Describe the prepared statement conn->describeStatement("", true); // Verify response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description for unnamed statement"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description for unnamed statement"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after describe unnamed statement"); } catch (const PgException& e) { ok(false, "Describe prepared with noname failed with error:%s", e.what()); } } void test_close_existing_statement() { diag("Test %d: Close existing prepared statement", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare a valid statement conn->prepareStatement("existing_stmt", "SELECT 1", true); // Close the statement conn->closeStatement("existing_stmt", false); conn->sendSync(); // Verify response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "Received CloseComplete for existing statement"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after close existing statement"); // Verify statement is actually closed conn->describeStatement("existing_stmt", true); conn->readMessage(type, buffer); // Should get error ok(type == PgConnection::ERROR_RESPONSE, "Describe fails after close"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); } catch (const PgException& e) { ok(false, "Close existing statement failed with error:%s", e.what()); } } void test_close_nonexistent_statement() { diag("Test %d: Close non-existent statement", test_count++); auto conn = create_connection(); if (!conn) return; try { // Close unknown statement conn->closeStatement("ghost_stmt", false); conn->sendSync(); // Should still get CloseComplete char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "Received CloseComplete for non-existent statement"); } catch (const PgException& e) { ok(false, "Close non-existent failed with error:%s", e.what()); } } void test_close_unnamed_statement() { diag("Test %d: Close unnamed statement", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare unnamed statement conn->prepareStatement("", "SELECT 1", false); conn->sendSync(); conn->waitForMessage(PgConnection::PARSE_COMPLETE, "parse complete", true); // Close unnamed statement conn->closeStatement("", false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "Received CloseComplete for unnamed statement"); // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after close unnamed statement"); // Verify closed conn->describeStatement("", true); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Describe fails for closed unnamed statement"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); } catch (const PgException& e) { ok(false, "Close unnamed failed with error:%s", e.what()); } } void test_close_without_sync() { diag("Test %d: Close without sync", test_count++); auto conn = create_connection(); if (!conn) return; try { int sock = conn->getSocket(); conn->prepareStatement("async_close_stmt", "SELECT 1", false); // Close without sync conn->closeStatement("async_close_stmt", false); // Shouldn't get immediate response ok(!has_immediate_response(sock), "No immediate response after close without sync"); // Send sync and verify responses conn->sendSync(); char type; std::vector buffer; bool gotParseComplete = false; bool gotCloseComplete = false; bool gotReady = false; while (!gotReady) { conn->readMessage(type, buffer); if (type == PgConnection::PARSE_COMPLETE) gotParseComplete = true; else if (type == PgConnection::CLOSE_COMPLETE) gotCloseComplete = true; else if (type == PgConnection::READY_FOR_QUERY) gotReady = true; } ok(gotParseComplete, "Received ParseComplete"); ok(gotCloseComplete, "Received CloseComplete"); ok(gotReady, "Received ReadyForQuery"); } catch (const PgException& e) { ok(false, "Close without sync failed with error:%s", e.what()); } } void test_multiple_close_without_sync() { diag("Test %d: Multiple close without sync", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare multiple statements conn->prepareStatement("multi_close_1", "SELECT 1", false); conn->prepareStatement("multi_close_2", "SELECT 2", false); conn->prepareStatement("multi_close_3", "SELECT 3", false); // Close without sync conn->closeStatement("multi_close_1", false); conn->closeStatement("multi_close_2", false); conn->closeStatement("multi_close_3", false); // Send sync conn->sendSync(); // Verify responses char type; int close_count = 0; int parse_count = 0; bool got_ready = false; while (!got_ready) { std::vector buffer; conn->readMessage(type, buffer); if (type == PgConnection::PARSE_COMPLETE) parse_count++; else if (type == PgConnection::CLOSE_COMPLETE) close_count++; else if (type == PgConnection::READY_FOR_QUERY) got_ready = true; } ok(parse_count == 3, "Received 3 parse completes"); ok(close_count == 3, "Received 3 close completes"); ok(got_ready, "Received ReadyForQuery"); } catch (const PgException& e) { ok(false, "Multiple close failed with error:%s", e.what()); } } void test_close_malformed_packet() { diag("Test %d: Malformed close packet", test_count++); auto conn = create_connection(); if (!conn) return; try { // Send garbage close message (invalid target type) std::vector garbage; garbage.push_back('X'); // Invalid target type garbage.push_back(0); // Null-terminated empty name conn->sendMessage('C', garbage); // Should get error response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for malformed packet"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "08P01", "Received ERRCODE_PROTOCOL_VIOLATION Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(false, "Session should be terminated by server"); } catch (const PgException& e) { ok(true, "Session should be terminated error: %s", e.what()); } } void test_close_twice() { diag("Test %d: Close statement twice", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("dupe_close", "SELECT 1", true); // First close conn->closeStatement("dupe_close", false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "First close success"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "First close returns ReadyForQuery"); // Second close conn->closeStatement("dupe_close", false); conn->sendSync(); conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "Second close returns CloseComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Second close returns ReadyForQuery"); } catch (const PgException& e) { ok(false, "Close twice failed with error:%s", e.what()); } } void test_close_without_prepare() { diag("Test %d: Close without preparing", test_count++); auto conn = create_connection(); if (!conn) return; try { // Close without preparing first conn->closeStatement("never_prepared", false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "Close succeeds for non-prepared statement"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after close without prepare"); } catch (const PgException& e) { ok(false, "Close without prepare failed with error:%s", e.what()); } } void test_close_during_pending_ops() { diag("Test %d: Close during pending operations", test_count++); auto conn = create_connection(); if (!conn) return; try { int sock = conn->getSocket(); // Start parse without sync conn->prepareStatement("pending_stmt", "SELECT 1", false); // Close without sync conn->closeStatement("pending_stmt", false); // Shouldn't get immediate response ok(!has_immediate_response(sock), "No response during pending operations"); // Send sync and verify responses conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete after close during pending ops"); conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "Received CloseComplete after close during pending ops"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after close during pending ops"); } catch (const PgException& e) { ok(false, "Close during pending ops failed with error:%s", e.what()); } } void test_close_all_types() { diag("Test %d: Close all types of targets", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare statements conn->prepareStatement("named_stmt", "SELECT 1", false); conn->prepareStatement("", "SELECT 2", false); // Unnamed // Close named statement conn->closeStatement("named_stmt", false); // Close unnamed statement conn->closeStatement("", false); // Close non-existent (should still work) conn->closeStatement("ghost", false); // Send sync conn->sendSync(); // Verify responses char type; int close_count = 0; int parse_count = 0; bool got_ready = false; while (!got_ready) { std::vector buffer; conn->readMessage(type, buffer); if (type == PgConnection::PARSE_COMPLETE) parse_count++; else if (type == PgConnection::CLOSE_COMPLETE) close_count++; else if (type == PgConnection::READY_FOR_QUERY) got_ready = true; } ok(parse_count == 2, "Received 2 parse completes"); ok(close_count == 3, "Received 3 close completes"); ok(got_ready, "Received ReadyForQuery"); } catch (const PgException& e) { ok(false, "Close all types failed with error:%s", e.what()); } } void test_parse_execute_without_bind() { diag("Test %d: Unnamed Prepared and Execute", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare a statement conn->prepareStatement("", "SELECT 1", true); // Execute statement directly conn->executeStatement(0, false); conn->sendSync(); // Verify results char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "34000", "Received ERRCODE_UNDEFINED_CURSOR Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery"); } catch (const PgException& e) { ok(false, "Unnamed Prepared and Execute failed with error:%s", e.what()); } } void test_bind_basic() { diag("Test %d: Basic Bind and Execute", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare a statement conn->prepareStatement("basic_bind", "SELECT $1::int AS num", false); conn->describeStatement("basic_bind", false); // Bind parameters directly to statement PgConnection::Param param = { "42", 0 }; conn->bindStatement("basic_bind", "", { param }, {}, false); conn->describePortal("", false); // Execute statement directly conn->executeStatement(0, false); conn->sendSync(); // Verify results char type; std::vector buffer; // Read parse complete conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete"); // Read parameter description conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received ParameterDescription"); BufferReader reader(buffer); int16_t num_params = reader.readInt16(); ok(num_params == 1, "One parameter reported"); if (num_params == 1 && buffer.size() >= 4) { uint32_t oid = (buffer[2] << 24) | (buffer[3] << 16) | (buffer[4] << 8) | buffer[5]; ok(oid == 23, "Parameter type is int (OID: %u)", oid); } else { ok(false, "Invalid parameter description size"); } // Read row description conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); // Verify row description reader = BufferReader(buffer); int16_t num_fields = reader.readInt16(); ok(num_fields == 1, "One field in row description (%d/1)", num_fields); if (num_fields == 1 && buffer.size() >= 20) { // Read field metadata std::string fieldName = reader.readString(); ok(fieldName == "num", "Field name is 'num'"); unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Field type OID is 23 (integer)"); unsigned int typeSize = reader.readInt16(); ok(typeSize == 4, "Field type size is 4 (integer size)"); unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); } else { ok(false, "Invalid row description size"); } conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); // Verify row description conn->readMessage(type, buffer); reader = BufferReader(buffer); num_fields = reader.readInt16(); ok(num_fields == 1, "One field in row description (%d/1)", num_fields); if (num_fields == 1 && buffer.size() >= 20) { // Read field metadata std::string fieldName = reader.readString(); ok(fieldName == "num", "Field name is 'num'"); unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Field type OID is 23 (integer)"); unsigned int typeSize = reader.readInt16(); ok(typeSize == 4, "Field type size is 4 (integer size)"); unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); } else { ok(false, "Invalid row description size"); } // Read data row conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); // Verify data row reader = BufferReader(buffer); int16_t num_columns = reader.readInt16(); ok(num_columns == 1, "One column in data row (%d/1)", num_columns); if (num_columns == 1 && buffer.size() >= 8) { // Read column length int32_t column_length = reader.readInt32(); ok(column_length == 2, "Column length is 2"); // Read column data buffer = reader.readBytes(column_length); ok(buffer[0] == '4' && buffer[1] == '2', "Column value is 42 (expected)"); } else { ok(false, "Invalid data row size"); } // Read command complete conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery"); } catch (const PgException& e) { ok(false, "Basic Bind/Execute failed with error:%s", e.what()); } } void test_bind_without_sync() { diag("Test %d: Bind without Sync", test_count++); auto conn = create_connection(); if (!conn) return; try { int sock = conn->getSocket(); conn->prepareStatement("async_bind", "SELECT $1::int", false); // Bind without sync PgConnection::Param param = { "5", 0 }; conn->bindStatement("async_bind", "", { param }, {}, false); // Shouldn't get immediate response ok(!has_immediate_response(sock), "No immediate response after bind without sync"); // Execute without sync conn->executeStatement(0, false); // Send sync and verify responses conn->sendSync(); char type; int parse_count = 0; int bind_count = 0; int execute_count = 0; bool got_ready = false; while (!got_ready) { std::vector buffer; conn->readMessage(type, buffer); if (type == PgConnection::PARSE_COMPLETE) parse_count++; else if (type == PgConnection::BIND_COMPLETE) bind_count++; else if (type == PgConnection::DATA_ROW) execute_count++; else if (type == PgConnection::READY_FOR_QUERY) got_ready = true; } ok(parse_count == 1, "Received ParseComplete"); ok(bind_count == 1, "Received BindComplete"); ok(execute_count == 1, "Received DataRow"); ok(got_ready, "Received ReadyForQuery"); } catch (const PgException& e) { ok(false, "Bind without sync failed with error:%s", e.what()); } } void test_bind_nonexistent_statement() { diag("Test %d: Bind to non-existent statement", test_count++); auto conn = create_connection(); if (!conn) return; try { PgConnection::Param param = { "test", 1 }; conn->bindStatement("ghost_stmt", "", { param }, {}, false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse for non-existent statement"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after bind to non-existent statement"); } catch (const PgException& e) { ok(false, "Bind to non-existent statement failed with error:%s", e.what()); } } void test_bind_incorrect_parameters() { diag("Test %d: Bind with incorrect parameters", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("incorrect_params", "SELECT $1::int, $2::text", true); // Pass only one parameter instead of two PgConnection::Param param = { "42", 1 }; conn->bindStatement("incorrect_params", "", { param }, {}, false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for incorrect parameters"); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse for incorrect parameters"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "08P01", "Received ERRCODE_PROTOCOL_VIOLATION Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after bind with incorrect parameters"); } catch (const PgException& e) { ok(false, "Bind failed with incorrect parameters: %s", e.what()); } } void test_binary_parameters() { diag("Test %d: Bind binary parameters", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("binary_params", "SELECT $1::int", true); // Create binary representation of integer 42 (network byte order) int32_t bin_value = htonl(42); PgConnection::Param param = { std::string(reinterpret_cast(&bin_value), sizeof(bin_value)), 1 // Binary format }; conn->bindStatement("binary_params", "", { param }, { 1 }, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); // Verify we got a binary result char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); // Read row description conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); // Verify row description BufferReader reader(buffer); int16_t num_fields = reader.readInt16(); ok(num_fields == 1, "One field in row description (%d/1)", num_fields); if (num_fields == 1 && buffer.size() >= 20) { // Read field metadata std::string fieldName = reader.readString(); ok(fieldName == "int4", "Field name is 'int4'"); unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Field type OID is 23 (integer)"); unsigned int typeSize = reader.readInt16(); ok(typeSize == 4, "Field type size is 4 (integer size)"); unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); unsigned int formatCode = reader.readInt16(); ok(formatCode == 1, "Field format code is 1 (binary format)"); } else { ok(false, "Invalid row description size"); } // Read data row conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); // Verify data row reader = BufferReader(buffer); int16_t num_columns = reader.readInt16(); ok(num_columns == 1, "One column in data row (%d/1)", num_columns); if (num_columns == 1 && buffer.size() >= 8) { // Read column length int32_t column_length = reader.readInt32(); ok(column_length == 4, "Column length is 4 (int32 size)"); // Read column data int32_t val = reader.readInt32(); ok(val == 42, "Column value is 42 (expected)"); } // Read command complete conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery"); } catch (const PgException& e) { ok(false, "Binary parameters test failed with error:%s", e.what()); } } void test_bind_large_data() { diag("Test %d: Bind with large data", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("large_data", "SELECT length($1::text)", true); // Create 1MB string std::string large_data(1024 * 1024, 'X'); PgConnection::Param param = { large_data, 1 }; conn->bindStatement("large_data", "", { param }, {1}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); // Read row description conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); BufferReader reader(buffer); int16_t num_fields = reader.readInt16(); ok(num_fields == 1, "One field in row description (%d/1)", num_fields); if (num_fields == 1 && buffer.size() >= 20) { // Read field metadata std::string fieldName = reader.readString(); ok(fieldName == "length", "Field name is 'length'"); unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Field type OID is 23 (integer)"); unsigned int typeSize = reader.readInt16(); ok(typeSize == 4, "Field type size is 4 (integer size)"); unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); unsigned int formatCode = reader.readInt16(); ok(formatCode == 1, "Field format code is 1 (binary format)"); } else { ok(false, "Invalid row description size"); } conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); reader = BufferReader(buffer); num_fields = reader.readInt16(); ok(num_fields == 1, "One field in data row (%d/1)", num_fields); if (num_fields == 1) { int32_t len = reader.readInt32(); if (len == 4) { // Length of int32 int32_t val = reader.readInt32(); ok(val == 1024 * 1024, "Received correct length: %d", val); } } conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery"); } catch (const PgException& e) { ok(false, "Large data test failed with error:%s", e.what()); } } void test_bind_null_parameters() { diag("Test %d: Bind with NULL parameters", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("null_params", "SELECT $1::int IS NULL", true); // Bind NULL parameter std::vector params = { { {}, 1} }; // is_null = true conn->bindStatement("null_params", "", params, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); // Read row description conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); BufferReader reader(buffer); int16_t num_fields = reader.readInt16(); ok(num_fields == 1, "One field in row description (%d/1)", num_fields); if (num_fields == 1 && buffer.size() >= 20) { // Read field metadata std::string fieldName = reader.readString(); ok(fieldName == "?column?", "Field name is '?column?'"); unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); unsigned int typeOid = reader.readInt32(); ok(typeOid == 16, "Field type OID is 16 (boolean)"); unsigned int typeSize = reader.readInt16(); ok(typeSize == 1, "Field type size is 1 (boolean size)"); unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); } else { ok(false, "Invalid row description size"); } conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); reader = BufferReader(buffer); num_fields = reader.readInt16(); ok(num_fields == 1, "One field in data row (%d/1)", num_fields); if (num_fields == 1) { int32_t len = reader.readInt32(); if (len == 1) { char val = reader.readByte(); ok(val == 't', "Received correct NULL check: %c", val); } } conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery"); } catch (const PgException& e) { ok(false, "NULL parameter test failed with error:%s", e.what()); } } void test_malformed_bind_packet() { diag("Test %d: Malformed Bind packet", test_count++); auto conn = create_connection(); if (!conn) return; try { // Send garbage bind message std::vector garbage{ 'X'}; conn->sendMessage('B', garbage); // Should get error response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for malformed packet"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "08P01", "Received ERRCODE_PROTOCOL_VIOLATION Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(false, "Session should be terminated by server"); } catch (const PgException& e) { ok(true, "Session should be terminated error: %s", e.what()); } } void test_malformed_execute_packet() { diag("Test %d: Malformed Execute packet", test_count++); auto conn = create_connection(); if (!conn) return; try { // Send garbage execute message std::vector garbage{ 'X' }; conn->sendMessage('E', garbage); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for malformed packet"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "08P01", "Received ERRCODE_PROTOCOL_VIOLATION Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(false, "Session should be terminated by server"); } catch (const PgException& e) { ok(true, "Session should be terminated error: %s", e.what()); } } void test_bind_named_portal() { diag("Test %d: Bind with named portal (should fail)", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("stmt_portal", "SELECT $1", true); // Attempt to bind with named portal PgConnection::Param param = { "1", 0 }; conn->bindStatement("stmt_portal", "named_portal", { param }, {}, false); conn->sendSync(); // Should get error response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error for named portal bind"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "0A000", "Received ERRCODE_FEATURE_NOT_SUPPORTED for named portal: %s", errormsg.c_str()); } catch (const PgException& e) { ok(false, "Bind named portal failed with error: %s", e.what()); } } void test_describe_portal() { diag("Test %d: Describe portal", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("stmt_desc_portal", "SELECT $1 AS test", true); // Bind with unnamed portal PgConnection::Param param = { "1", 0 }; conn->bindStatement("stmt_desc_portal", "", { param }, {}, false); conn->describePortal("", false); conn->executePortal("", 0, false); conn->describePortal("", false); conn->sendSync(); // Verify response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received Bind complete response"); // Should get row description conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description for portal"); // Verify description content BufferReader reader(buffer); int16_t num_fields = reader.readInt16(); ok(num_fields == 1, "One field in description"); if (num_fields == 1) { std::string name = reader.readString(); ok(name == "test", "Field name is 'test'"); } conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received data row for portal"); // Verify data row reader = BufferReader(buffer); num_fields = reader.readInt16(); ok(num_fields == 1, "One column in data row (%d/1)", num_fields); if (num_fields == 1 && buffer.size() >= 5) { // Read column length int32_t column_length = reader.readInt32(); ok(column_length == 1, "Column length is 1"); // Read column data uint8_t val = reader.readByte(); ok(val == '1', "Column value is '1' (expected)"); } else { ok(false, "Invalid data row size"); } conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for describe portal"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "34000", "Received ERRCODE_UNDEFINED_CURSOR for describe portal: %s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after describe portal"); } catch (const PgException& e) { ok(false, "Describe portal failed with error: %s", e.what()); } } void test_close_portal() { diag("Test %d: Close portal", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("stmt_close_portal", "SELECT $1", true); // Bind and create portal PgConnection::Param param = { "1", 0 }; conn->bindStatement("stmt_close_portal", "", { param }, {}, false); // Close portal conn->closePortal("", false); // Should get close complete conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received bind complete for portal"); conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "Received close complete for portal"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after portal close"); // Verify portal is closed conn->executeStatement(0, true); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for closed portal"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "34000", "Received ERRCODE_INVALID_CURSOR_DEFINITION for closed portal: %s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after closed portal error"); } catch (const PgException& e) { ok(false, "Close portal failed with error: %s", e.what()); } } void test_portal_lifecycle() { diag("Test %d: Unnamed portal lifecycle", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("stmt_portal_life", "SELECT $1::int", true); // First bind PgConnection::Param param1 = { "10", 0 }; conn->bindStatement("stmt_portal_life", "", { param1 }, {}, false); param1 = { "42", 0 }; // Change value for next bind conn->bindStatement("stmt_portal_life", "", { param1 }, {}, false); conn->describePortal("", false); // Execute and verify conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; do { conn->readMessage(type, buffer); } while (type != PgConnection::DATA_ROW); BufferReader reader(buffer); int16_t num_fields = reader.readInt16(); ok(num_fields == 1, "One field in data row (%d/1)", num_fields); int32_t len = reader.readInt32(); ok(len == 2, "Data row length is 2 (int32 size)"); std::vector val = reader.readBytes(len); ok(val[0] == '4' && val[1] == '2', "First execution returns 42"); // Re-bind with new value (same statement) PgConnection::Param param2 = { "99", 0 }; conn->bindStatement("stmt_portal_life", "", { param2 }, {}, false); // Execute again conn->executeStatement(0, false); conn->sendSync(); // Skip to data row do { conn->readMessage(type, buffer); } while (type != PgConnection::DATA_ROW); reader = BufferReader(buffer); num_fields = reader.readInt16(); ok(num_fields == 1, "One field in data row (%d/1)", num_fields); len = reader.readInt32(); ok(len == 2, "Data row length is 2 (int32 size)"); val = reader.readBytes(len); ok(val[0] == '9' && val[1] == '9', "Second execution returns 99 (bind replaced)"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete after second execution"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after second execution"); // Close portal explicitly conn->closePortal("", false); conn->sendSync(); conn->readMessage(type, buffer); // Close complete ok(type == PgConnection::CLOSE_COMPLETE, "Portal closed successfully"); // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after portal close"); // Verify portal is closed conn->describePortal("", false); conn->sendSync(); // Should get error response conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error for describe closed portal"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "34000", "Received ERRCODE_INVALID_CURSOR_DEFINITION: %s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after portal lifecycle test"); // Auto-close on sync conn->bindStatement("stmt_portal_life", "", { param1 }, {}, false); conn->executeStatement(0, false); conn->sendSync(); do { conn->readMessage(type, buffer); } while (type != PgConnection::READY_FOR_QUERY); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after auto-close on sync"); conn->describePortal("", true); // Should get error response again conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error for describe closed portal after auto-close"); if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "34000", "Received ERRCODE_INVALID_CURSOR_DEFINITION after auto-close: %s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after describe closed portal auto-close"); } catch (const PgException& e) { ok(false, "Portal lifecycle test failed: %s", e.what()); } } void test_describe_closed_portal() { diag("Test %d: Describe closed portal", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("stmt_desc_closed", "SELECT 1", true); // Bind and create portal PgConnection::Param param = { "1", 0 }; conn->bindStatement("stmt_desc_closed", "", { param }, {}, false); // Close portal conn->closePortal("", false); conn->sendSync(); char type; std::vector buffer; // Read bind complete conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received bind complete for closed portal"); // Read close complete conn->readMessage(type, buffer); ok(type == PgConnection::CLOSE_COMPLETE, "Received close complete for portal"); // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after portal close"); // Describe closed portal conn->describePortal("", false); conn->sendSync(); // Should get error conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error for closed portal describe"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "34000", "Received ERRCODE_INVALID_CURSOR_DEFINITION: %s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after describe closed portal"); } catch (const PgException& e) { ok(false, "Describe closed portal failed: %s", e.what()); } } void test_libpq_style_execute() { diag("Test %d: libpq Style Execute", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("stmt_libpq_execute", "SELECT $1", true); // Bind and create portal PgConnection::Param param = { "1", 0 }; conn->bindStatement("stmt_libpq_execute", "", { param }, {}, false); // describe protal conn->describePortal("", false); conn->executePortal("", 0, false); // Should get close complete conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received bind complete for portal"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description"); BufferReader reader(buffer); // Read row description int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Row description has 1 field (%d/1)", fieldCount); // Read field name std::string fieldName = reader.readString(); ok(fieldName == "?column?", "Field name is '?column?'"); // Read field table OID unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); // Read field attribute number unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); // Read field type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 25, "Field type OID is 25 (text)"); // Read field type size unsigned int typeSize = reader.readInt16(); ok(typeSize == -1, "Field type size is -1 (text size)"); // Read field type modifier unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); // Read field format code unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received Data Row"); // Read data row reader = BufferReader(buffer); int16_t num_columns = reader.readInt16(); ok(num_columns == 1, "One column in data row (%d/1)", num_columns); if (num_columns == 1 && buffer.size() >= 5) { // Read column length int32_t column_length = reader.readInt32(); ok(column_length == 1, "Column length is 1 (text size)"); // Read column data uint8_t val = reader.readByte(); ok(val == '1', "Column value is '1' (expected)"); } else { ok(false, "Invalid data row size"); } conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after portal close"); } catch (const PgException& e) { ok(false, "libpq Style Execute failed with error: %s", e.what()); } } void test_multiple_execute_on_single_bind() { diag("Test %d: Multiple Execute On Single Bind", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("stmt_mul_execute", "SELECT $1", true); // Bind and create portal PgConnection::Param param = { "1", 0 }; conn->bindStatement("stmt_mul_execute", "", { param }, {}, false); conn->describePortal("", false); conn->executePortal("", 0, false); conn->describePortal("", false); conn->executePortal("", 0, false); // Should get close complete conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received bind complete for portal"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description"); BufferReader reader(buffer); // Read row description int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Row description has 1 field (%d/1)", fieldCount); // Read field name std::string fieldName = reader.readString(); ok(fieldName == "?column?", "Field name is '?column?'"); // Read field table OID unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); // Read field attribute number unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); // Read field type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 25, "Field type OID is 25 (text)"); // Read field type size unsigned int typeSize = reader.readInt16(); ok(typeSize == -1, "Field type size is -1 (text size)"); // Read field type modifier unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); // Read field format code unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received Data Row"); // Read data row reader = BufferReader(buffer); int16_t num_columns = reader.readInt16(); ok(num_columns == 1, "One column in data row (%d/1)", num_columns); if (num_columns == 1 && buffer.size() >= 5) { // Read column length int32_t column_length = reader.readInt32(); ok(column_length == 1, "Column length is 1 (text size)"); // Read column data uint8_t val = reader.readByte(); ok(val == '1', "Column value is '1' (expected)"); } else { ok(false, "Invalid data row size"); } conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received error response for malformed packet"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "34000", "Received ERRCODE_UNDEFINED_CURSOR Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after portal close"); } catch (const PgException& e) { ok(false, "libpq Style Execute failed with error: %s", e.what()); } } void test_insert_command_complete() { diag("Test %d: Extended Query INSERT and CommandComplete", test_count++); auto conn = create_connection(); if (!conn) return; try { // CREATE TEMP TABLE via Extended Query conn->prepareStatement("create_tmp", "CREATE TEMP TABLE tmp_test(id SERIAL PRIMARY KEY, txt TEXT, flg BOOLEAN)", false); conn->bindStatement("create_tmp", "", {}, {}, false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buf; conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for CREATE TEMP TABLE"); conn->readMessage(type, buf); ok(type == PgConnection::BIND_COMPLETE, "BindComplete for CREATE TEMP TABLE"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for CREATE TEMP TABLE"); auto tag = BufferReader(buf).readString(); ok(tag.rfind("CREATE TABLE", 0) == 0, "CommandComplete tag is 'CREATE TABLE': %s", tag.c_str()); conn->readMessage(type, buf); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery after CREATE TEMP TABLE"); // INSERT via Extended Query conn->prepareStatement("ins_stmt", "INSERT INTO tmp_test(txt, flg) VALUES('hello', true)", false); conn->bindStatement("ins_stmt", "", {}, {}, false); conn->executeStatement(0, false); conn->sendSync(); conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for INSERT"); conn->readMessage(type, buf); ok(type == PgConnection::BIND_COMPLETE, "BindComplete for INSERT"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for INSERT"); tag = BufferReader(buf).readString(); ok(tag.rfind("INSERT 0 1", 0) == 0, "CommandComplete tag is 'INSERT 0 1': %s", tag.c_str()); conn->readMessage(type, buf); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery after INSERT"); } catch (const PgException& e) { ok(false, "INSERT Extended Query test failed: %s", e.what()); } } void test_parse_with_param_type() { diag("Test %d: Parse with Param Types", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("test_param_type", "SELECT $1", false); conn->describeStatement("test_param_type", false); conn->sendSync(); // Verify response char type; std::vector buffer; { conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received parse complete for statement with parameter types"); conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description"); // Read parameter description BufferReader reader(buffer); int paramCount = reader.readInt16(); ok(paramCount == 1, "No parameters in prepared statement (%d/0)", paramCount); // Read parameter type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 25, "Parameter type OID is 25 (text)"); } { conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description"); BufferReader reader(buffer); // Read row description int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Row description has 1 field (%d/1)", fieldCount); // Read field name std::string fieldName = reader.readString(); ok(fieldName == "?column?", "Field name is '?column?'"); // Read field table OID unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); // Read field attribute number unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); // Read field type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 25, "Field type OID is 25 (text)"); // Read field type size unsigned int typeSize = reader.readInt16(); ok(typeSize == -1, "Field type size is -1 (text size)"); // Read field type modifier unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); // Read field format code unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); } // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after describe"); conn->prepareStatement("test_param_type2", "SELECT $1", false, { 23 }); conn->describeStatement("test_param_type2", false); conn->sendSync(); // Verify response { conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received parse complete for statement with parameter types"); conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "Received parameter description"); // Read parameter description BufferReader reader(buffer); int paramCount = reader.readInt16(); ok(paramCount == 1, "Parameters in prepared statement (%d/1)", paramCount); // Read parameter type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Parameter type OID is 23 (integer)"); } { conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received row description"); BufferReader reader(buffer); // Read row description int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Row description has 1 field (%d/1)", fieldCount); // Read field name std::string fieldName = reader.readString(); ok(fieldName == "?column?", "Field name is '?column?'"); // Read field table OID unsigned int tableOid = reader.readInt32(); ok(tableOid == 0, "Field table OID is 0 (no table)"); // Read field attribute number unsigned int attrNum = reader.readInt16(); ok(attrNum == 0, "Field attribute number is 0 (no specific column)"); // Read field type OID unsigned int typeOid = reader.readInt32(); ok(typeOid == 23, "Field type OID is 23 (integer)"); // Read field type size unsigned int typeSize = reader.readInt16(); ok(typeSize == 4, "Field type size is 4 (integer size)"); // Read field type modifier unsigned int typeModifier = reader.readInt32(); ok(typeModifier == -1, "Field type modifier is -1 (default)"); // Read field format code unsigned int formatCode = reader.readInt16(); ok(formatCode == 0, "Field format code is 0 (text format)"); } } catch (const PgException& e) { ok(false, "Parse with sync test failed with error: %s", e.what()); } } void test_set_statement_tracked() { diag("Test %d: Extended Query SET Statement Tracked", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("set_tracked_stmt", "SET client_min_messages TO 'error'", false); conn->bindStatement("set_tracked_stmt", "", {}, {}, false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buf; conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for SET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::BIND_COMPLETE, "BindComplete for SET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for SET EXECUTE"); conn->readMessage(type, buf); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery after SET STATEMENT"); usleep(1000); ok(check_logs_for_command(".*\\[WARNING\\] Unable to parse unknown SET query from client.*") == false, "Should not be locked on a hostgroup"); } catch (const PgException& e) { ok(false, "Extended Query SET Statement Tracked test failed: %s", e.what()); } } void test_set_statement_tracked_with_describe() { diag("Test %d: Extended Query SET Statement Tracked with Describe", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("set_tracked_stmt_2", "SET client_min_messages TO 'error'", false); conn->bindStatement("set_tracked_stmt_2", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buf; conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for SET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::BIND_COMPLETE, "BindComplete for SET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::NO_DATA, "NoData for SET DESCRIBE"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for SET EXECUTE"); conn->readMessage(type, buf); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery after SET STATEMENT"); usleep(1000); ok(check_logs_for_command(".*\\[WARNING\\] Unable to parse unknown SET query from client.*") == false, "Should not be locked on a hostgroup"); } catch (const PgException& e) { ok(false, "Extended Query SET Statement Tracked test failed: %s", e.what()); } } void test_set_statement_with_simple_query_mix() { diag("Test %d: Extended Query SET Statement with Simple Query Mix", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("set_stmt_mix", "SET client_min_messages TO 'error'", false); conn->bindStatement("set_stmt_mix", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->execute("SHOW client_min_messages"); char type; std::vector buf; conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for SET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::BIND_COMPLETE, "BindComplete for SET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::NO_DATA, "NoData for SET DESCRIBE"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for SET EXECUTE"); conn->readMessage(type, buf); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription for SHOW"); BufferReader buff(buf); int16_t fields = buff.readInt16(); ok(fields == 1, "One field in RowDescription for SHOW (%d/1)", fields); std::string field_name = buff.readString(); ok(field_name == "client_min_messages", "Field name is 'client_min_messages' (%s)", field_name.c_str()); unsigned int table_oid = buff.readInt32(); ok(table_oid == 0, "Field table OID is 0 (no table)"); unsigned int attr_num = buff.readInt16(); ok(attr_num == 0, "Field attribute number is 0 (no specific column)"); unsigned int type_oid = buff.readInt32(); ok(type_oid == 25, "Field type OID is 25 (text)"); unsigned int type_size = buff.readInt16(); ok(type_size == -1, "Field type size is -1 (text size)"); unsigned int type_modifier = buff.readInt32(); ok(type_modifier == -1, "Field type modifier is -1 (default)"); unsigned int format_code = buff.readInt16(); ok(format_code == 0, "Field format code is 0 (text format)"); conn->readMessage(type, buf); ok(type == PgConnection::DATA_ROW, "DataRow for SHOW"); buff = buf; int16_t cols = buff.readInt16(); ok(cols == 1, "One column in DataRow for SHOW (%d/1)", cols); // Read column length int32_t col_len = buff.readInt32(); ok(col_len == 5, "Column length is 5 (text size)"); std::vector val = buff.readBytes(col_len); ok(memcmp((char*)val.data(), "error", col_len) == 0, "SHOW returned 'error'"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for SHOW"); conn->readMessage(type, buf); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery after SET STATEMENT and SHOW"); usleep(1000); ok(check_logs_for_command(".*\\[WARNING\\] Unable to parse unknown SET query from client.*") == false, "Should not be locked on a hostgroup"); } catch (const PgException& e) { ok(false, "Extended Query SET Statement Tracked test failed: %s", e.what()); } } void test_reset_statement_with_simple_query_mix() { diag("Test %d: Extended Query RESET Statement with Simple Query Mix", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->execute("SET client_min_messages='notice'"); conn->waitForReady(); conn->prepareStatement("reset_stmt_mix", "RESET client_min_messages", false); conn->bindStatement("reset_stmt_mix", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->execute("SHOW client_min_messages"); char type; std::vector buf; conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for RESET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::BIND_COMPLETE, "BindComplete for RESET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::NO_DATA, "NoData for RESET DESCRIBE"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for RESET EXECUTE"); conn->readMessage(type, buf); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription for SHOW after RESET"); BufferReader buff(buf); int16_t fields = buff.readInt16(); ok(fields == 1, "One field in RowDescription for SHOW (%d/1)", fields); std::string field_name = buff.readString(); ok(field_name == "client_min_messages", "Field name is 'client_min_messages' (%s)", field_name.c_str()); buff.readInt32(); // table_oid buff.readInt16(); // attr_num unsigned int type_oid = buff.readInt32(); ok(type_oid == 25, "Field type OID is 25 (text)"); buff.readInt16(); // type_size buff.readInt32(); // type_modifier buff.readInt16(); // format_code conn->readMessage(type, buf); ok(type == PgConnection::DATA_ROW, "DataRow for SHOW after RESET"); buff = buf; int16_t cols = buff.readInt16(); ok(cols == 1, "One column in DataRow (%d/1)", cols); int32_t col_len = buff.readInt32(); std::vector val = buff.readBytes(col_len); ok(col_len == 6, "Column length is 6 (text size) after RESET"); ok(memcmp((char*)val.data(), "notice", col_len) == 0, "SHOW after RESET returned 'notice'"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for SHOW"); conn->readMessage(type, buf); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery after RESET and SHOW"); usleep(1000); ok(check_logs_for_command(".*\\[WARNING\\] Unable to parse unknown RESET query from client.*") == false, "Should not be locked on a hostgroup"); } catch (const PgException& e) { ok(false, "Extended Query RESET Statement test failed: %s", e.what()); } } void test_discard_statement_with_simple_query_mix() { diag("Test %d: Extended Query DISCARD Statement with Simple Query Mix", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("discard_stmt_mix", "DISCARD TEMP", false); conn->bindStatement("discard_stmt_mix", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->execute("SELECT 1"); char type; std::vector buf; conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for DISCARD STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::BIND_COMPLETE, "BindComplete for DISCARD STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::NO_DATA, "NoData for DISCARD DESCRIBE"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for DISCARD EXECUTE"); conn->readMessage(type, buf); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription for SELECT after DISCARD"); BufferReader buff(buf); int16_t fields = buff.readInt16(); ok(fields == 1, "One field in RowDescription for SELECT (%d/1)", fields); std::string field_name = buff.readString(); ok(field_name == "?column?", "Field name is '?column?' (%s)", field_name.c_str()); unsigned int table_oid = buff.readInt32(); ok(table_oid == 0, "Field table OID is 0 (no table)"); unsigned int attr_num = buff.readInt16(); ok(attr_num == 0, "Field attribute number is 0 (no specific column)"); unsigned int type_oid = buff.readInt32(); ok(type_oid == 23, "Field type OID is 23 (integer)"); unsigned int type_size = buff.readInt16(); ok(type_size == 4, "Field type size is 4 (integer size)"); unsigned int type_modifier = buff.readInt32(); ok(type_modifier == -1, "Field type modifier is -1 (default)"); unsigned int format_code = buff.readInt16(); ok(format_code == 0, "Field format code is 0 (text format)"); conn->readMessage(type, buf); ok(type == PgConnection::DATA_ROW, "DataRow for SELECT after DISCARD"); buff = buf; int16_t cols = buff.readInt16(); ok(cols == 1, "One column in DataRow for SELECT (%d/1)", cols); // Read column length int32_t col_len = buff.readInt32(); ok(col_len == 1, "Column length is 1 (text size)"); std::vector val = buff.readBytes(col_len); ok(memcmp((char*)val.data(), "1", col_len) == 0, "SELECT after DISCARD returned '1'"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for SELECT"); conn->readMessage(type, buf); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery after DISCARD and SELECT"); } catch (const PgException& e) { ok(false, "Extended Query DISCARD Statement with Simple Query Mix test failed: %s", e.what()); } } void test_deallocate_statement_with_simple_query_mix() { diag("Test %d: Extended Query DEALLOCATE Statement with Simple Query Mix", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare a dummy statement first conn->prepareStatement("dummy_stmt", "SELECT 1", false); // Now DEALLOCATE it conn->prepareStatement("dealloc_stmt_mix", "DEALLOCATE dummy_stmt", false); conn->bindStatement("dealloc_stmt_mix", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->execute("SELECT 1"); char type; std::vector buf; conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for DEALLOCATE STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for DEALLOCATE STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::BIND_COMPLETE, "BindComplete for DEALLOCATE STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::NO_DATA, "NoData for DEALLOCATE DESCRIBE"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for DEALLOCATE EXECUTE"); conn->readMessage(type, buf); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription for SELECT after DEALLOCATE"); BufferReader buff(buf); int16_t fields = buff.readInt16(); ok(fields == 1, "One field in RowDescription for SELECT (%d/1)", fields); std::string field_name = buff.readString(); ok(field_name == "?column?", "Field name is '?column?' (%s)", field_name.c_str()); unsigned int table_oid = buff.readInt32(); ok(table_oid == 0, "Field table OID is 0 (no table)"); unsigned int attr_num = buff.readInt16(); ok(attr_num == 0, "Field attribute number is 0 (no specific column)"); unsigned int type_oid = buff.readInt32(); ok(type_oid == 23, "Field type OID is 23 (integer)"); unsigned int type_size = buff.readInt16(); ok(type_size == 4, "Field type size is 4 (integer size)"); unsigned int type_modifier = buff.readInt32(); ok(type_modifier == -1, "Field type modifier is -1 (default)"); unsigned int format_code = buff.readInt16(); ok(format_code == 0, "Field format code is 0 (text format)"); conn->readMessage(type, buf); ok(type == PgConnection::DATA_ROW, "DataRow for SELECT after DEALLOCATE"); buff = buf; int16_t cols = buff.readInt16(); ok(cols == 1, "One column in DataRow for SELECT (%d/1)", cols); // Read column length int32_t col_len = buff.readInt32(); ok(col_len == 1, "Column length is 1 (text size)"); std::vector val = buff.readBytes(col_len); ok(memcmp((char*)val.data(), "1", col_len) == 0, "SELECT after DEALLOCATE returned '1'"); conn->readMessage(type, buf); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete for SELECT"); conn->readMessage(type, buf); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery after DEALLOCATE and SELECT"); } catch (const PgException& e) { ok(false, "Extended Query DEALLOCATE Statement with Simple Query Mix test failed: %s", e.what()); } } void test_set_statement_untracked() { diag("Test %d: Extended Query SET Statement UnTracked", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("set_untracked_stmt", "SET dummy TO 'dummy'", false); conn->bindStatement("set_untracked_stmt", "", {}, {}, false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buf; conn->readMessage(type, buf); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete for SET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::BIND_COMPLETE, "BindComplete for SET STATEMENT"); conn->readMessage(type, buf); ok(type == PgConnection::ERROR_RESPONSE, "ErrorResponse for SET EXECUTE"); conn->readMessage(type, buf); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery after SET STATEMENT"); ok(check_logs_for_command(".*\\[WARNING\\] Unable to parse unknown SET query from client.*") == true, "Should be locked on a hostgroup"); } catch (const PgException& e) { ok(false, "Extended Query SET Statement UnTracked test failed: %s", e.what()); } } void test_deallocate_having_stmt_name_via_simple_query() { diag("Test %d: Simple Query - DEALLOCATE named statement", test_count++); auto conn = create_connection(); if (!conn) return; try { // Prepare a valid statement conn->prepareStatement("deallocate_existing_stmt", "SELECT 1", true); // Close the statement conn->execute("DEALLOCATE PREPARE deallocate_existing_stmt"); // Verify response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received after DEALLOCATE"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "ReadyForQuery was received after DEALLOCATE"); // Verify statement is actually closed conn->describeStatement("deallocate_existing_stmt", true); conn->readMessage(type, buffer); // Should get error ok(type == PgConnection::ERROR_RESPONSE, "Describe failed as expected after DEALLOCATE"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); } catch (const PgException& e) { ok(false, "Simple Query - DEALLOCATE named statement failed with error:%s", e.what()); } } void test_deallocate_having_stmt_name_via_prepared() { diag("Test %d: Extended Query - DEALLOCATE named statement via prepared execution", test_count++); auto conn = create_connection(); if (!conn) return; if (!conn) return; try { // Prepare a valid statement conn->prepareStatement("deallocate_existing_stmt2", "DEALLOCATE deallocate_existing_stmt2", false); conn->sendSync(); // Verify response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete was received for DEALLOCATE"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); // should be present in prepared statements conn->describeStatement("deallocate_existing_stmt2", true); conn->readMessage(type, buffer); ok(type == PgConnection::PARAMETER_DESCRIPTION, "ParameterDescription was received for prepared DEALLOCATE"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); conn->bindStatement("deallocate_existing_stmt2", "", {}, {}, false); conn->executeStatement(0, false); conn->sendSync(); // Verify response conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "BindComplete was received for DEALLOCATE"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received after DEALLOCATE execution"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after deallocate existing statement"); // Verify statement is actually closed conn->describeStatement("deallocate_existing_stmt2", true); conn->readMessage(type, buffer); // Should get error ok(type == PgConnection::ERROR_RESPONSE, "Describe failed as expected after DEALLOCATE"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after deallocate existing statement"); } catch (const PgException& e) { ok(false, "Extended Query - DEALLOCATE named statement via prepared execution failed with error:%s", e.what()); } } void test_deallocate_all_via_simple_query() { diag("Test %d: Simple Query - DEALLOCATE ALL", test_count++); auto conn = create_connection(); if (!conn) return; if (!conn) return; try { // Prepare a valid statement conn->prepareStatement("deallocate_all_1", "SELECT 1", true); conn->prepareStatement("deallocate_all_2", "SELECT 2", true); conn->prepareStatement("deallocate_all_3", "SELECT 3", true); // Close the statement conn->execute("DEALLOCATE PREPARE ALL"); // Verify response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received after DEALLOCATE ALL"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); // Verify statement is actually closed conn->describeStatement("deallocate_all_1", true); conn->readMessage(type, buffer); // Should get error ok(type == PgConnection::ERROR_RESPONSE, "Describe failed for deallocate_all_1 as expected"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); // Verify statement is actually closed conn->describeStatement("deallocate_all_2", true); conn->readMessage(type, buffer); // Should get error ok(type == PgConnection::ERROR_RESPONSE, "Describe failed for deallocate_all_2 as expected"); if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); // Verify statement is actually closed conn->describeStatement("deallocate_all_3", true); conn->readMessage(type, buffer); // Should get error ok(type == PgConnection::ERROR_RESPONSE, "Describe failed for deallocate_all_3 as expected"); if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); } catch (const PgException& e) { ok(false, "Extended Query DEALLOCATE having Statement Name failed with error:%s", e.what()); } } void test_deallocate_all_via_prepared() { diag("Test %d: Extended Query - DEALLOCATE ALL via prepared execution", test_count++); auto conn = create_connection(); if (!conn) return; if (!conn) return; try { conn->execute("SET dummy TO 'test'"); // intentionally lock on hostgroup. DEALLOCATE should work regardless // Verify response char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Session Locked On Hostgroup as expected"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "42704", "Received ERRCODE_UNDEFINED_OBJECT Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after SET dummy"); // Prepare a valid statement conn->prepareStatement("deallocate_all_1", "SELECT 1", true); conn->prepareStatement("deallocate_all_2", "SELECT 2", true); conn->prepareStatement("deallocate_all_3", "SELECT 3", true); // Bind and execute DEALLOCATE ALL conn->prepareStatement("", "DEALLOCATE ALL", true); conn->bindStatement("", "", {}, {}, false); conn->executeStatement(0, false); conn->sendSync(); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "BindComplete was received for DEALLOCATE ALL"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for DEALLOCATE ALL"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); // Verify statement is actually closed conn->describeStatement("deallocate_all_1", true); conn->readMessage(type, buffer); // Should get error ok(type == PgConnection::ERROR_RESPONSE, "Describe failed for deallocate_all_1 as expected"); if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); // Verify statement is actually closed conn->describeStatement("deallocate_all_2", true); conn->readMessage(type, buffer); // Should get error ok(type == PgConnection::ERROR_RESPONSE, "Describe failed for deallocate_all_2 as expected"); if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); // Verify statement is actually closed conn->describeStatement("deallocate_all_3", true); conn->readMessage(type, buffer); // Should get error ok(type == PgConnection::ERROR_RESPONSE, "Describe failed for deallocate_all_3 as expected"); if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query"); } catch (const PgException& e) { ok(false, "Extended Query DEALLOCATE ALL via Prepared failed with error:%s", e.what()); } } void test_deallocate_non_existent_stmt() { diag("Test %d: Extended Query - DEALLOCATE non-existent statement", test_count++); auto conn = create_connection(); if (!conn) return; if (!conn) return; try { // Attempt to deallocate a non-existent statement conn->execute("DEALLOCATE PREPARE non_existent_stmt"); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "ErrorResponse was received for DEALLOCATE non-existent statement"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "26000", "Received ERRCODE_INVALID_SQL_STATEMENT_NAME Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after DEALLOCATE non-existent statement"); } catch (const PgException& e) { ok(false, "Extended Query DEALLOCATE non-existent statement failed with error:%s", e.what()); } } void test_describe_portal_returns_no_data() { diag("Test %d: Extended Query - Describe Returns No Data", test_count++); auto conn = create_connection(); if (!conn) return; if (!conn) return; try { conn->execute("CREATE TEMP TABLE test_describe (id integer)"); conn->waitForReady(); conn->prepareStatement("describe_no_data_stmt", "INSERT INTO test_describe VALUES (42)", false); conn->bindStatement("describe_no_data_stmt", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete was received for describe no data statement"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "BindComplete was received for describe no data statement"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "NoData was received for describe no data statement"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for describe no data statement"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after describe no data statement"); } catch (const PgException& e) { ok(false, "Extended Query Describe Returns No Data failed with error:%s", e.what()); } } void test_multiple_parse_bind_describe_execute_fail() { diag("Test %d: Extended Query - Multiple Parse/Bind/Describe/Execute Fail", test_count++); auto conn = create_connection(); if (!conn) return; if (!conn) return; try { // Prepare a valid statement conn->prepareStatement("multi_stmt", "SELECT 1", false); conn->bindStatement("multi_stmt", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("multi_stmt2", "SELECT 1/0", false); conn->bindStatement("multi_stmt2", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("multi_stmt3", "SELECT 2", false); conn->bindStatement("multi_stmt3", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete was received for multi_stmt"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "BindComplete was received for multi_stmt"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription was received for multi_stmt"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "DataRow was received for multi_stmt"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for multi_stmt"); conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete was received for multi_stmt2"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "BindComplete was received for multi_stmt2"); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "ErrorResponse was received for multi_stmt2"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "22012", "Received ERRCODE_DIVISION_BY_ZERO Error:%s", errormsg.c_str()); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after multi_stmt2 error"); } catch (const PgException& e) { ok(false, "Extended Query Multiple Parse/Bind/Describe/Execute Fail failed with error:%s", e.what()); } } void test_send_multiple_simple_query_without_waiting_for_response() { diag("Test %d: Simple Query - Send multiple queries without waiting for response", test_count++); auto conn = create_connection(); if (!conn) return; if (!conn) return; try { // Send multiple simple queries without waiting for response conn->execute("SELECT 1"); conn->execute("SELECT 2"); conn->execute("SELECT 3"); conn->execute("SELECT 4"); conn->execute("SELECT 5"); char type; std::vector buffer; for (int i = 1; i <= 5; i++) { conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription was received for SELECT %d", i); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "DataRow was received for SELECT %d", i); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "DataRow has 1 field for SELECT %d", i); int valueLen = reader.readInt32(); ok(valueLen == 1, "DataRow value length is 1 for SELECT %d", i); uint8_t value = reader.readByte(); ok(value == (i + '0'), "DataRow value is %c for SELECT %d", value, i); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for SELECT %d", i); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query for SELECT %d", i); } } catch (const PgException& e) { ok(false, "Simple Query - Send multiple queries without waiting for response failed with error:%s", e.what()); } } void test_send_multiple_simple_query_in_transaction_without_waiting_for_response() { diag("Test %d: Simple Query - Send multiple queries in transaction without waiting for response", test_count++); auto conn = create_connection(); if (!conn) return; if (!conn) return; try { // Send multiple simple queries without waiting for response conn->execute("BEGIN"); conn->execute("SELECT 1"); conn->execute("SELECT 2"); conn->execute("SELECT 3"); conn->execute("SELECT 4"); conn->execute("SELECT 5"); conn->execute("COMMIT"); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for BEGIN"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after BEGIN"); for (int i = 1; i <= 5; i++) { conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription was received for SELECT %d", i); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "DataRow was received for SELECT %d", i); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "DataRow has 1 field for SELECT %d", i); int valueLen = reader.readInt32(); ok(valueLen == 1, "DataRow value length is 1 for SELECT %d", i); uint8_t value = reader.readByte(); ok(value == (i + '0'), "DataRow value is %c for SELECT %d", value, i); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for SELECT %d", i); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query for SELECT %d", i); } conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for COMMIT"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query after all SELECTs and COMMIT"); } catch (const PgException& e) { ok(false, "Simple Query - Send multiple queries in transaction without waiting for response failed with error:%s", e.what()); } } void test_send_multiple_simple_query_without_waiting_for_response_proxysql_fast_forward_mode(PGconn* admin_conn) { diag("Test %d: Simple Query - Send multiple queries without waiting for response (ProxySQL Fast Forward Mode)", test_count++); // Enable ProxySQL Fast Forward Mode executeQueries(admin_conn, { "UPDATE pgsql_users SET fast_forward=1", "LOAD PGSQL USERS TO RUNTIME" }); test_send_multiple_simple_query_without_waiting_for_response(); // Restore ProxySQL Fast Forward Mode to default (disabled) executeQueries(admin_conn, { "UPDATE pgsql_users SET fast_forward=0", "LOAD PGSQL USERS TO RUNTIME" }); } void test_send_multiple_simple_query_in_transaction_without_waiting_for_response_proxysql_fast_forward_mode(PGconn* admin_conn) { diag("Test %d: Simple Query - Send multiple queries in transaction without waiting for response (ProxySQL Fast Forward Mode)", test_count++); // Enable ProxySQL Fast Forward Mode executeQueries(admin_conn, { "UPDATE pgsql_users SET fast_forward=1", "LOAD PGSQL USERS TO RUNTIME" }); test_send_multiple_simple_query_in_transaction_without_waiting_for_response(); // Restore ProxySQL Fast Forward Mode to default (disabled) executeQueries(admin_conn, { "UPDATE pgsql_users SET fast_forward=0", "LOAD PGSQL USERS TO RUNTIME" }); } void test_send_simple_query_and_extended_query_without_waiting_for_response() { diag("Test %d: Simple Query and Extended Query without waiting for response", test_count++); auto conn = create_connection(); if (!conn) return; if (!conn) return; try { // Send multiple simple queries without waiting for response conn->execute("SELECT 1"); conn->execute("SELECT 2"); conn->execute("SELECT 3"); conn->execute("SELECT 4"); conn->execute("SELECT 5"); conn->prepareStatement("ext_stmt", "SELECT $1", false); PgConnection::Param param = { "1", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); param = { "2", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); param = { "3", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); param = { "4", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); param = { "5", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); conn->sendSync(); param = { "1", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); param = { "2", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); param = { "3", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); param = { "4", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); param = { "5", 0 }; conn->bindStatement("ext_stmt", "", { param }, {}, false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; for (int i = 1; i <= 5; i++) { conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription was received for SELECT %d", i); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "DataRow was received for SELECT %d", i); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "DataRow has 1 field for SELECT %d", i); int valueLen = reader.readInt32(); ok(valueLen == 1, "DataRow value length is 1 for SELECT %d", i); uint8_t value = reader.readByte(); ok(value == (i + '0'), "DataRow value is %c for SELECT %d", value, i); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for SELECT %d", i); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query for SELECT %d", i); } conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "ParseComplete was received for extended query"); for (int i = 1; i <= 5; i++) { conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "BindComplete was received (extended query P1)"); //conn->readMessage(type, buffer); //ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription was received for SELECT %d (extended query P1)", i); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "DataRow was received for SELECT %d (extended query P1)", i); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "DataRow has 1 field for SELECT %d (extended query P1)", i); int valueLen = reader.readInt32(); ok(valueLen == 1, "DataRow value length is 1 for SELECT %d (extended query P1)", i); uint8_t value = reader.readByte(); ok(value == (i + '0'), "DataRow value is %c for SELECT %d (extended query P1)", value, i); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for SELECT %d (extended query P1)", i); } conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query (extended query P1)"); for (int i = 1; i <= 5; i++) { conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "BindComplete was received (extended query P2)"); //conn->readMessage(type, buffer); //ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription was received for SELECT %d (extended query P2)", i); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "DataRow was received for SELECT %d (extended query P2)", i); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "DataRow has 1 field for SELECT %d (extended query P2)", i); int valueLen = reader.readInt32(); ok(valueLen == 1, "DataRow value length is 1 for SELECT %d (extended query P2)", i); uint8_t value = reader.readByte(); ok(value == (i + '0'), "DataRow value is %c for SELECT %d (extended query P2)", value, i); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "CommandComplete was received for SELECT %d (extended query P2)", i); } conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ready for query (extended query P2)"); } catch (const PgException& e) { ok(false, "Simple Query and Extended Query without waiting for response failed with error:%s", e.what()); } } void test_send_simple_query_and_extended_query_without_waiting_for_response_proxysql_fast_forward_mode(PGconn* admin_conn) { diag("Test %d: Simple Query and Extended Query without waiting for response (ProxySQL Fast Forward Mode)", test_count++); // Enable ProxySQL Fast Forward Mode executeQueries(admin_conn, { "UPDATE pgsql_users SET fast_forward=1", "LOAD PGSQL USERS TO RUNTIME" }); test_send_simple_query_and_extended_query_without_waiting_for_response(); // Restore ProxySQL Fast Forward Mode to default (disabled) executeQueries(admin_conn, { "UPDATE pgsql_users SET fast_forward=0", "LOAD PGSQL USERS TO RUNTIME" }); } void test_pipeline_error() { diag("Test %d: Extended Query - Pipeline Error Handling", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("basic_bind", "SELECT 1/0", false); conn->bindStatement("basic_bind", "", { }, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("basic_bind2", "SELECT 2", false); conn->bindStatement("basic_bind2", "", { }, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); // Verify results char type; std::vector buffer; // Read parse complete conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse for division by zero"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "22012", "Received ERRCODE_DIVISION_BY_ZERO Error:%s", errormsg.c_str()); // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after error"); } catch (const PgException& e) { ok(false, "Extended Query Pipeline Error Handling failed with error:%s", e.what()); } } void test_pipeline_error_2() { diag("Test %d: Extended Query - Pipeline Error Handling 2", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("basic_bind", "SELECT 1/0", false); conn->bindStatement("basic_bind", "", { }, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); // Verify results char type; std::vector buffer; // Read parse complete conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse for division by zero"); std::string errormsg; std::string errorcode; if (type == PgConnection::ERROR_RESPONSE) { BufferReader reader(buffer); char field; while (reader.remaining() > 0 && (field = reader.readByte()) != 0) { if (field == 'M') errormsg = reader.readString(); else if (field == 'C') errorcode = reader.readString(); else reader.readString(); } } ok(errorcode == "22012", "Received ERRCODE_DIVISION_BY_ZERO Error:%s", errormsg.c_str()); // Read ready for query conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after error"); } catch (const PgException& e) { ok(false, "Extended Query Pipeline Error Handling 2 failed with error:%s", e.what()); } } void test_pipeline_exit() { diag("Test %d: Extended Query - Pipeline exit", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("basic_bind", "SELECT 1", false); conn->bindStatement("basic_bind", "", { }, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("basic_bind2", "SELECT 1", false); conn->execute("SELECT 2"); // Verify results char type; std::vector buffer; // Read parse complete conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for second statement"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription was received for simple query"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "DataRow was received for simple query"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for simple query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after all commands"); } catch (const PgException& e) { ok(false, "Extended Query Pipeline Error Handling failed with error:%s", e.what()); } } void test_pipeline_exit_2() { diag("Test %d: Extended Query - Pipeline exit 2", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("basic_bind", "SELECT 1", false); conn->bindStatement("basic_bind", "", { }, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); // Verify results char type; std::vector buffer; // Read parse complete conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after all commands"); } catch (const PgException& e) { ok(false, " Extended Query - Pipeline exit 2 failed with error:%s", e.what()); } } void test_pipeline_exit_3() { diag("Test %d: Extended Query - Pipeline exit 3", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("basic_bind", "SELECT 1", false); conn->bindStatement("basic_bind", "", { }, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("basic_bind2", "SELECT 1", false); conn->sendSync(); // Verify results char type; std::vector buffer; // Read parse complete conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for second statement"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after all commands"); } catch (const PgException& e) { ok(false, " Extended Query - Pipeline exit 3 failed with error:%s", e.what()); } } void test_pipeline_transaction_exit() { diag("Test %d: Extended Query - Pipeline exit in transaction", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->execute("BEGIN"); conn->prepareStatement("basic_bind", "SELECT 1", false); conn->bindStatement("basic_bind", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("basic_bind2", "SELECT 1", false); conn->execute("SELECT 2"); conn->execute("ROLLBACK"); // Verify results char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for BEGIN"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after BEGIN"); conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete"); conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for second statement"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "RowDescription was received for simple query"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "DataRow was received for simple query"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for simple query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after all commands"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ROLLBACK"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after all commands"); } catch (const PgException& e) { ok(false, "Extended Query Pipeline exit in transaction failed with error:%s", e.what()); } } void test_implicit_txn_error_first_insert() { diag("Test %d: Implicit transaction - error on first insert should rollback all", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->execute("CREATE TEMP TABLE temp_test(id INT)"); // First insert has error (text instead of int) conn->prepareStatement("ins1", "INSERT INTO temp_test VALUES('abc')", false); conn->bindStatement("ins1", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); // Second insert should not execute conn->prepareStatement("ins2", "INSERT INTO temp_test VALUES(2)", false); conn->bindStatement("ins2", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; // Consume until ReadyForQuery conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for CREATE TEMP TABLE"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after CREATE TEMP TABLE"); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse for first insert"); conn->readMessage(type, buffer); // ReadyForQuery ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after first insert error"); // Now check count conn->execute("SELECT count(*) FROM temp_test"); int rows = 0; conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); // DataRow ok(type == PgConnection::DATA_ROW, "Received RowData"); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Received 1 field in data row"); int valueLen = reader.readInt32(); ok(valueLen == 1, "Field length is 1 bytes"); uint8_t val = reader.readByte(); ok(val == '0', "Received correct value '0' for count query"); conn->readMessage(type, buffer); // CommandComplete ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for count query"); conn->readMessage(type, buffer); // ReadyForQuery ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after count query"); } catch (const PgException& e) { ok(false, "Implicit txn error-first-insert test failed: %s", e.what()); } } void test_implicit_txn_error_middle_insert() { diag("Test %d: Implicit transaction - error on middle insert should rollback all", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->execute("CREATE TEMP TABLE temp_test(id INT)"); // First insert valid conn->prepareStatement("ins1", "INSERT INTO temp_test VALUES(1)", false); conn->bindStatement("ins1", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); // Second insert invalid conn->prepareStatement("ins2", "INSERT INTO temp_test VALUES('oops')", false); conn->bindStatement("ins2", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); // Third insert valid (should be rolled back) conn->prepareStatement("ins3", "INSERT INTO temp_test VALUES(3)", false); conn->bindStatement("ins3", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; // CREATE TABLE conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for CREATE TEMP TABLE"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after CREATE TEMP TABLE"); // First insert conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for first insert"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for first insert"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for first insert"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for first insert"); // Second insert fails conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse for second insert error"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after error"); // Verify rollback (should have 0 rows) conn->execute("SELECT count(*) FROM temp_test"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Received 1 field in data row"); int valueLen = reader.readInt32(); ok(valueLen == 1, "Field length is 1 byte"); uint8_t val = reader.readByte(); ok(val == '0', "Received correct value '0' for count query"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for count query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after count query"); } catch (const PgException& e) { ok(false, "Implicit txn error-middle-insert test failed: %s", e.what()); } } void test_implicit_txn_error_last_insert() { diag("Test %d: Implicit transaction - error on last insert should rollback all", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->execute("CREATE TEMP TABLE temp_test(id INT)"); // First insert valid conn->prepareStatement("ins1", "INSERT INTO temp_test VALUES(1)", false); conn->bindStatement("ins1", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); // Second insert valid conn->prepareStatement("ins2", "INSERT INTO temp_test VALUES(2)", false); conn->bindStatement("ins2", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); // Third insert invalid conn->prepareStatement("ins3", "INSERT INTO temp_test VALUES('bad')", false); conn->bindStatement("ins3", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; // CREATE TABLE conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for CREATE TEMP TABLE"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after CREATE TEMP TABLE"); // First insert ok conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for first insert"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for first insert"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for first insert"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for first insert"); // Second insert ok conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for second insert"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for second insert"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for second insert"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for second insert"); // Third insert fails conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse for third insert error"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after error"); // Verify rollback (should have 0 rows) conn->execute("SELECT count(*) FROM temp_test"); int rows = 0; conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Received 1 field in data row"); int valueLen = reader.readInt32(); ok(valueLen == 1, "Field length is 1 byte"); uint8_t val = reader.readByte(); ok(val == '0', "Received correct value '0' for count query"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for count query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after count query"); } catch (const PgException& e) { ok(false, "Implicit txn error-last-insert test failed: %s", e.what()); } } void test_explicit_txn_error_with_rollback_extended() { diag("Test %d: Explicit transaction - extended query pipeline inside transaction, error -> ROLLBACK", test_count++); auto conn = create_connection(); if (!conn) return; try { // Create temp table and begin explicit transaction (simple commands; we verify their responses) conn->execute("CREATE TEMP TABLE temp_test(id INT)"); conn->execute("BEGIN"); char type; std::vector buffer; // CREATE TABLE response conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for CREATE TEMP TABLE"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after CREATE TEMP TABLE"); // BEGIN response conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for BEGIN"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after BEGIN"); // Now issue three INSERTs using extended query pipeline (two good, one bad) conn->prepareStatement("ins1", "INSERT INTO temp_test VALUES(1)", false); conn->bindStatement("ins1", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("ins2", "INSERT INTO temp_test VALUES(2)", false); conn->bindStatement("ins2", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("ins3", "INSERT INTO temp_test VALUES('bad')", false); conn->bindStatement("ins3", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); // Read extended-protocol responses in sequence // ins1 conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for ins1"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for ins1"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for ins1"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ins1"); // ins2 conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for ins2"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for ins2"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for ins2"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ins2"); // ins3 (error) conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE || type == PgConnection::ERROR_RESPONSE, "Received ParseComplete or ErrorResponse for ins3 (got %d)", (int)type); if (type == PgConnection::PARSE_COMPLETE) { conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for ins3"); conn->readMessage(type, buffer); } ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse for ins3 (bad value)"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after ins3 error"); // Now ROLLBACK the explicit transaction conn->execute("ROLLBACK"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ROLLBACK"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after ROLLBACK"); // Verify table is empty conn->execute("SELECT count(*) FROM temp_test"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Received 1 field in data row"); int valueLen = reader.readInt32(); ok(valueLen == 1, "Field length is 1 byte"); uint8_t val = reader.readByte(); ok(val == '0', "Received correct value '0' after rollback"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for count query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after count query"); } catch (const PgException& e) { ok(false, "Explicit txn (extended) error-with-rollback test failed: %s", e.what()); } } void test_explicit_txn_error_with_commit_extended() { diag("Test %d: Explicit transaction - extended query pipeline inside transaction, error -> COMMIT", test_count++); auto conn = create_connection(); if (!conn) return; try { // Create temp table and BEGIN conn->execute("CREATE TEMP TABLE temp_test(id INT)"); conn->execute("BEGIN"); char type; std::vector buffer; // CREATE TABLE response conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for CREATE TEMP TABLE"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after CREATE TEMP TABLE"); // BEGIN response conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for BEGIN"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after BEGIN"); // Extended pipeline: two good inserts and one bad conn->prepareStatement("ins1", "INSERT INTO temp_test VALUES(1)", false); conn->bindStatement("ins1", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("ins2", "INSERT INTO temp_test VALUES(2)", false); conn->bindStatement("ins2", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("ins3", "INSERT INTO temp_test VALUES('bad')", false); conn->bindStatement("ins3", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); // Read responses for ins1 conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for ins1"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for ins1"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for ins1"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ins1"); // ins2 conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for ins2"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for ins2"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for ins2"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ins2"); // ins3 error conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE || type == PgConnection::ERROR_RESPONSE, "Received ParseComplete or ErrorResponse for ins3 (got %d)", (int)type); if (type == PgConnection::PARSE_COMPLETE) { conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for ins3"); conn->readMessage(type, buffer); } ok(type == PgConnection::ERROR_RESPONSE, "Received ErrorResponse for ins3 (bad value)"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after ins3 error"); // Attempt COMMIT of aborted transaction conn->execute("COMMIT"); conn->readMessage(type, buffer); ok(type == PgConnection::ERROR_RESPONSE || type == PgConnection::COMMAND_COMPLETE, "Received response for COMMIT after error (either ErrorResponse or CommandComplete)"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after COMMIT"); // Verify that no rows were committed (transaction was aborted and commit did not persist inserts) conn->execute("SELECT count(*) FROM temp_test"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Received 1 field in data row"); int valueLen = reader.readInt32(); ok(valueLen == 1, "Field length is 1 byte"); uint8_t val = reader.readByte(); ok(val == '0', "Received correct value '0' after commit of aborted txn"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for count query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after count query"); } catch (const PgException& e) { ok(false, "Explicit txn (extended) error-with-commit test failed: %s", e.what()); } } void test_explicit_txn_success_then_rollback_extended() { diag("Test %d: Explicit transaction - extended inserts succeed but ROLLBACK discards them", test_count++); auto conn = create_connection(); if (!conn) return; try { // Create temp table and BEGIN conn->execute("CREATE TEMP TABLE temp_test(id INT)"); conn->execute("BEGIN"); char type; std::vector buffer; // CREATE TABLE response conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for CREATE TEMP TABLE"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after CREATE TEMP TABLE"); // BEGIN response conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for BEGIN"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after BEGIN"); // Extended pipeline: three valid inserts conn->prepareStatement("ins1", "INSERT INTO temp_test VALUES(1)", false); conn->bindStatement("ins1", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("ins2", "INSERT INTO temp_test VALUES(2)", false); conn->bindStatement("ins2", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->prepareStatement("ins3", "INSERT INTO temp_test VALUES(3)", false); conn->bindStatement("ins3", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); // Read responses for ins1 conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for ins1"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for ins1"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for ins1"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ins1"); // ins2 conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for ins2"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for ins2"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for ins2"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ins2"); // ins3 conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for ins3"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for ins3"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for ins3"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ins3"); // End of batch conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after all inserts"); // Now ROLLBACK explicitly conn->execute("ROLLBACK"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for ROLLBACK"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after ROLLBACK"); // Verify rollback (table should be empty) conn->execute("SELECT count(*) FROM temp_test"); conn->readMessage(type, buffer); ok(type == PgConnection::ROW_DESCRIPTION, "Received RowDescription"); conn->readMessage(type, buffer); ok(type == PgConnection::DATA_ROW, "Received DataRow"); BufferReader reader(buffer); int fieldCount = reader.readInt16(); ok(fieldCount == 1, "Received 1 field in data row"); int valueLen = reader.readInt32(); ok(valueLen == 1, "Field length is 1 byte"); uint8_t val = reader.readByte(); ok(val == '0', "Received correct value '0' after rollback of successful inserts"); conn->readMessage(type, buffer); ok(type == PgConnection::COMMAND_COMPLETE, "Received CommandComplete for count query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after count query"); } catch (const PgException& e) { ok(false, "Explicit txn (extended) success-then-rollback test failed: %s", e.what()); } } void test_empty_query_describe_portal_returns_no_data() { diag("Test %d: Describe Portal Returns No Data for empty query", test_count++); auto conn = create_connection(); if (!conn) return; try{ conn->prepareStatement("empty_query_stmt", "", false); conn->bindStatement("empty_query_stmt", "", {}, {}, false); conn->describePortal("", false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for empty query"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for empty query"); conn->readMessage(type, buffer); ok(type == PgConnection::NO_DATA, "Received NoData for empty query"); conn->readMessage(type, buffer); ok(type == PgConnection::EMPTY_QUERY_RESPONSE, "Received EmptyQueryResponse for empty query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after all commands"); } catch (const PgException& e) { ok(false, "Describe Portal Returns No Data for empty query failed with error:%s", e.what()); } } void test_empty_query_without_describe_portal() { diag("Test %d: Execute empty query without Describe Portal", test_count++); auto conn = create_connection(); if (!conn) return; try { conn->prepareStatement("empty_query2_stmt", "", false); conn->bindStatement("empty_query2_stmt", "", {}, {}, false); conn->executeStatement(0, false); conn->sendSync(); char type; std::vector buffer; conn->readMessage(type, buffer); ok(type == PgConnection::PARSE_COMPLETE, "Received ParseComplete for empty query"); conn->readMessage(type, buffer); ok(type == PgConnection::BIND_COMPLETE, "Received BindComplete for empty query"); conn->readMessage(type, buffer); ok(type == PgConnection::EMPTY_QUERY_RESPONSE, "Received EmptyQueryResponse for empty query"); conn->readMessage(type, buffer); ok(type == PgConnection::READY_FOR_QUERY, "Received ReadyForQuery after all commands"); } catch (const PgException& e) { ok(false, "Execute empty query without Describe Portal failed with error:%s", e.what()); } } int main(int argc, char** argv) { plan(1061); // Adjust based on number of tests if (cl.getEnv()) return exit_status(); 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(); } auto admin_conn = createNewConnection(ConnType::ADMIN, "", false); if (!admin_conn || PQstatus(admin_conn.get()) != CONNECTION_OK) { BAIL_OUT("Error: failed to connect to the database in file %s, line %d", __FILE__, __LINE__); return exit_status(); } if (executeQueries(admin_conn.get(), { "SET pgsql-authentication_method=1", "LOAD PGSQL VARIABLES TO RUNTIME" }) == false) { BAIL_OUT("Error: failed to set pgsql-authentication_method=1 in file %s, line %d", __FILE__, __LINE__); return exit_status(); } try { // Parse Prepared Statement test_parse_without_sync(); test_parse_with_sync(); test_malformed_packet(); test_empty_query(); test_multiple_parse(); test_only_sync(); test_empty_stmt(); test_prepare_statement_mix(); test_invalid_query_parse_packet(); test_parse_use_same_stmt_name(); test_parse_use_unnamed_stmt(); // Describe Prepared Statement test_describe_existing_statement(); test_describe_nonexistent_statement(); test_describe_without_sync(); test_describe_malformed_packet(); test_describe_after_close_statement(); test_multiple_describe_calls(); test_describe_parameter_types(); test_describe_result_metadata(); test_describe_prepared_noname(); // Close Statement test_close_existing_statement(); test_close_nonexistent_statement(); test_close_unnamed_statement(); test_close_without_sync(); test_multiple_close_without_sync(); test_close_malformed_packet(); test_close_twice(); test_close_without_prepare(); test_close_during_pending_ops(); test_close_all_types(); // Bind and Execute test_parse_execute_without_bind(); test_bind_basic(); test_bind_without_sync(); test_bind_nonexistent_statement(); test_bind_incorrect_parameters(); test_binary_parameters(); test_bind_large_data(); test_bind_null_parameters(); test_malformed_bind_packet(); test_malformed_execute_packet(); // Portals test_bind_named_portal(); test_describe_portal(); test_close_portal(); test_portal_lifecycle(); test_describe_closed_portal(); test_describe_portal_returns_no_data(); // random tests test_libpq_style_execute(); test_multiple_execute_on_single_bind(); test_multiple_parse_bind_describe_execute_fail(); test_insert_command_complete(); test_parse_with_param_type(); // SET statement tracking test_set_statement_tracked(); test_set_statement_tracked_with_describe(); test_set_statement_untracked(); test_set_statement_with_simple_query_mix(); test_reset_statement_with_simple_query_mix(); // DEALLOCATE statements test_deallocate_having_stmt_name_via_simple_query(); test_deallocate_having_stmt_name_via_prepared(); test_deallocate_all_via_simple_query(); test_deallocate_all_via_prepared(); test_deallocate_non_existent_stmt(); test_deallocate_statement_with_simple_query_mix(); // Tests for sending multiple simple queries and extended queries without waiting for response test_send_multiple_simple_query_without_waiting_for_response(); test_send_multiple_simple_query_in_transaction_without_waiting_for_response(); test_send_simple_query_and_extended_query_without_waiting_for_response(); test_send_multiple_simple_query_without_waiting_for_response_proxysql_fast_forward_mode(admin_conn.get()); test_send_multiple_simple_query_in_transaction_without_waiting_for_response_proxysql_fast_forward_mode(admin_conn.get()); test_send_simple_query_and_extended_query_without_waiting_for_response_proxysql_fast_forward_mode(admin_conn.get()); // Extended Query (without sync) + Simple Query test_extended_query_prepared_describe_execute_simple_query_without_sync(); test_prepare_statement_mix(); // DISCARD test_discard_statement_with_simple_query_mix(); // Pipeline tests test_pipeline_error(); test_pipeline_error_2(); test_pipeline_exit(); test_pipeline_exit_2(); test_pipeline_exit_3(); test_pipeline_transaction_exit(); // Implicit transaction tests test_implicit_txn_error_first_insert(); test_implicit_txn_error_middle_insert(); test_implicit_txn_error_last_insert(); test_explicit_txn_error_with_rollback_extended(); test_explicit_txn_error_with_commit_extended(); test_explicit_txn_success_then_rollback_extended(); // Empty query tests test_empty_query_describe_portal_returns_no_data(); test_empty_query_without_describe_portal(); } catch (const std::exception& e) { diag("Fatal error: %s",e.what()); } f_proxysql_log.close(); return exit_status(); }