feat(ffto): add pgsql_parse_error_response() helper with unit tests

feat/ffto-error-recording
Rene Cannao 4 weeks ago
parent 1c559e96d8
commit fe701d29aa

@ -0,0 +1,39 @@
/**
* @file PgSQLErrorFields.h
* @brief Parser for PostgreSQL ErrorResponse message fields.
*
* Extracted for unit testability. Scans ErrorResponse payload for
* SQLSTATE ('C') and message ('M') fields.
*
* @see PostgreSQL Protocol: ErrorResponse message format
*/
#ifndef PGSQL_ERROR_FIELDS_H
#define PGSQL_ERROR_FIELDS_H
#include <cstdint>
#include <cstddef>
/**
* @brief Result of parsing a PostgreSQL ErrorResponse payload.
*/
struct PgSQLErrorResult {
bool parsed; ///< True if payload was non-null and scanned.
char sqlstate[6]; ///< 5-char SQLSTATE + null terminator (empty if not found).
const char* message; ///< Pointer into payload at 'M' field value (null if not found).
size_t message_len; ///< Length of message string.
};
/**
* @brief Parse a PostgreSQL ErrorResponse payload to extract SQLSTATE and message.
*
* Scans the field-type/value pairs in the ErrorResponse payload.
* Field format: type_byte + null_terminated_string, repeated, ending with '\0'.
*
* @param payload ErrorResponse message payload (after the 5-byte header).
* @param len Length of the payload.
* @return PgSQLErrorResult with parsed fields.
*/
PgSQLErrorResult pgsql_parse_error_response(const unsigned char* payload, size_t len);
#endif // PGSQL_ERROR_FIELDS_H

@ -110,6 +110,7 @@ _OBJ_CXX := ProxySQL_GloVars.oo network.oo debug.oo configfile.oo Query_Cache.oo
TransactionState.oo \
HostgroupRouting.oo \
PgSQLCommandComplete.oo \
PgSQLErrorFields.oo \
MySQLProtocolUtils.oo \
PgSQLErrorClassifier.oo \
PgSQLMonitorDecision.oo \

@ -0,0 +1,37 @@
#include "PgSQLErrorFields.h"
#include <cstring>
PgSQLErrorResult pgsql_parse_error_response(const unsigned char* payload, size_t len) {
PgSQLErrorResult result {};
result.parsed = false;
result.sqlstate[0] = '\0';
result.message = nullptr;
result.message_len = 0;
if (!payload || len == 0) return result;
result.parsed = true;
size_t pos = 0;
while (pos < len) {
char field_type = static_cast<char>(payload[pos]);
if (field_type == '\0') break; // end of fields
pos++; // skip field type byte
// Find the null terminator for this field's value
const char* value = reinterpret_cast<const char*>(payload + pos);
size_t value_len = strnlen(value, len - pos);
if (pos + value_len >= len) break; // truncated
if (field_type == 'C' && value_len <= 5) {
memcpy(result.sqlstate, value, value_len);
result.sqlstate[value_len] = '\0';
} else if (field_type == 'M') {
result.message = value;
result.message_len = value_len;
}
pos += value_len + 1; // skip value + null terminator
}
return result;
}

@ -16,6 +16,7 @@
#include "proxysql.h"
#include "MySQLProtocolUtils.h"
#include "PgSQLCommandComplete.h"
#include "PgSQLErrorFields.h"
#include <cstring>
#include <vector>
@ -309,12 +310,56 @@ static void test_mysql_err_packet_empty_message() {
ok(errmsg_len == 0, "ERR parse empty msg: empty message");
}
// ============================================================================
// 7. PgSQL: ErrorResponse field parsing
// ============================================================================
static void test_pgsql_error_fields_basic() {
unsigned char payload[] = {
'S','E','R','R','O','R','\0',
'C','4','2','6','0','1','\0',
'M','s','y','n','t','a','x',' ','e','r','r','o','r','\0',
'\0'
};
PgSQLErrorResult r = pgsql_parse_error_response(payload, sizeof(payload));
ok(r.parsed == true, "PgSQL error parse: basic parsed");
ok(strcmp(r.sqlstate, "42601") == 0, "PgSQL error parse: sqlstate = 42601");
ok(r.message != nullptr && strncmp(r.message, "syntax error", 12) == 0,
"PgSQL error parse: message starts with 'syntax error'");
}
static void test_pgsql_error_fields_missing_code() {
unsigned char payload[] = {
'S','E','R','R','O','R','\0',
'M','s','o','m','e',' ','e','r','r','\0',
'\0'
};
PgSQLErrorResult r = pgsql_parse_error_response(payload, sizeof(payload));
ok(r.parsed == true, "PgSQL error no-code: parsed");
ok(strlen(r.sqlstate) == 0, "PgSQL error no-code: empty sqlstate");
ok(r.message != nullptr && strncmp(r.message, "some err", 8) == 0,
"PgSQL error no-code: message correct");
}
static void test_pgsql_error_fields_empty() {
unsigned char payload[] = {'\0'};
PgSQLErrorResult r = pgsql_parse_error_response(payload, 1);
ok(r.parsed == true, "PgSQL error empty: parsed (no fields)");
ok(strlen(r.sqlstate) == 0, "PgSQL error empty: empty sqlstate");
ok(r.message == nullptr, "PgSQL error empty: null message");
}
static void test_pgsql_error_fields_zero_length() {
PgSQLErrorResult r = pgsql_parse_error_response(nullptr, 0);
ok(r.parsed == false, "PgSQL error null: returns false");
}
// ============================================================================
// Main
// ============================================================================
int main() {
plan(46);
plan(56);
int rc = test_init_minimal();
ok(rc == 0, "test_init_minimal() succeeds");
@ -355,6 +400,12 @@ int main() {
test_mysql_err_packet_truncated(); // 1
test_mysql_err_packet_empty_message(); // 3
// PgSQL ErrorResponse parsing
test_pgsql_error_fields_basic(); // 3
test_pgsql_error_fields_missing_code(); // 3
test_pgsql_error_fields_empty(); // 3
test_pgsql_error_fields_zero_length(); // 1
test_cleanup_minimal();
return exit_status();
}

Loading…
Cancel
Save