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

1521 lines
59 KiB

/**
* @file query_processor_unit-t.cpp
* @brief Unit tests for MySQL_Query_Processor and PgSQL_Query_Processor.
*
* Tests the query processor rule management in isolation without a
* running ProxySQL instance. Covers:
* - Rule creation via new_query_rule() factory method
* - Rule field storage and retrieval
* - Rule insertion, sorting by rule_id, and commit
* - Rule retrieval via get_current_query_rules() (SQLite3 result)
* - Regex modifier parsing (CASELESS, GLOBAL)
* - Active/inactive rule filtering
* - PgSQL rule parity
*
* @note Full process_query() testing requires a MySQL_Session with
* populated connection data (username, schema, client address),
* which is beyond the scope of isolated unit tests. Those
* scenarios are covered by the existing E2E TAP tests.
*
* @see Phase 2.4 of the Unit Testing Framework (GitHub issue #5476)
*/
#include "tap.h"
#include "test_globals.h"
#include "test_init.h"
#include "proxysql.h"
#include "MySQL_Query_Processor.h"
#include "PgSQL_Query_Processor.h"
#include <cstring>
// Extern declarations for Glo* pointers (defined in test_globals.cpp)
extern MySQL_Query_Processor *GloMyQPro;
extern PgSQL_Query_Processor *GloPgQPro;
// ============================================================================
// Helpers
// ============================================================================
/**
* @brief Create a simple MySQL query rule with common defaults.
*
* Most fields default to -1/NULL/false (no match / no action).
* Only rule_id, active, and explicitly passed fields are set.
*/
static MySQL_Query_Processor_Rule_t *mysql_simple_rule(
int rule_id, bool active,
const char *match_pattern = nullptr,
int destination_hostgroup = -1,
bool apply = false,
const char *username = nullptr,
int flagIN = 0, int flagOUT = -1)
{
return MySQL_Query_Processor::new_query_rule(
rule_id, active,
username, // username
nullptr, // schemaname
flagIN, // flagIN
nullptr, // client_addr
nullptr, // proxy_addr
-1, // proxy_port
nullptr, // digest
nullptr, // match_digest
match_pattern, // match_pattern
false, // negate_match_pattern
nullptr, // re_modifiers
flagOUT, // flagOUT
nullptr, // replace_pattern
destination_hostgroup, // destination_hostgroup
-1, // cache_ttl
-1, // cache_empty_result
-1, // cache_timeout
-1, // reconnect
-1, // timeout
-1, // retries
-1, // delay
-1, // next_query_flagIN
-1, // mirror_flagOUT
-1, // mirror_hostgroup
nullptr, // error_msg
nullptr, // OK_msg
-1, // sticky_conn
-1, // multiplex
-1, // gtid_from_hostgroup
-1, // log
apply, // apply
nullptr, // attributes
nullptr // comment
);
}
/**
* @brief Create a simple PgSQL query rule with common defaults.
*/
static PgSQL_Query_Processor_Rule_t *pgsql_simple_rule(
int rule_id, bool active,
const char *match_pattern = nullptr,
int destination_hostgroup = -1,
bool apply = false)
{
return PgSQL_Query_Processor::new_query_rule(
rule_id, active,
nullptr, nullptr, // username, schemaname
0, // flagIN
nullptr, nullptr, -1, // client_addr, proxy_addr, proxy_port
nullptr, // digest
nullptr, // match_digest
match_pattern, // match_pattern
false, // negate_match_pattern
nullptr, // re_modifiers
-1, // flagOUT
nullptr, // replace_pattern
destination_hostgroup, // destination_hostgroup
-1, -1, -1, // cache_ttl, cache_empty_result, cache_timeout
-1, -1, -1, -1, // reconnect, timeout, retries, delay
-1, -1, -1, // next_query_flagIN, mirror_flagOUT, mirror_hostgroup
nullptr, nullptr, // error_msg, OK_msg
-1, -1, // sticky_conn, multiplex
-1, // log
apply, // apply
nullptr, nullptr // attributes, comment
);
}
// ============================================================================
// 1. Rule creation via new_query_rule()
// ============================================================================
/**
* @brief Test that new_query_rule() allocates and populates a rule.
*/
static void test_mysql_rule_creation() {
auto *rule = MySQL_Query_Processor::new_query_rule(
100, // rule_id
true, // active
"testuser", // username
"testdb", // schemaname
0, // flagIN
"192.168.1.%", // client_addr
nullptr, // proxy_addr
-1, // proxy_port
nullptr, // digest
"^SELECT", // match_digest
"SELECT.*FROM users", // match_pattern
false, // negate_match_pattern
"CASELESS", // re_modifiers
-1, // flagOUT
nullptr, // replace_pattern
5, // destination_hostgroup
3000, // cache_ttl
1, // cache_empty_result
-1, // cache_timeout
-1, // reconnect
5000, // timeout
3, // retries
-1, // delay
-1, // next_query_flagIN
-1, // mirror_flagOUT
-1, // mirror_hostgroup
nullptr, // error_msg
nullptr, // OK_msg
1, // sticky_conn
0, // multiplex
-1, // gtid_from_hostgroup
1, // log
true, // apply
nullptr, // attributes
"route reads to HG 5" // comment
);
ok(rule != nullptr, "MySQL QP: new_query_rule() returns non-null");
ok(rule->rule_id == 100, "MySQL QP: rule_id is correct");
ok(rule->active == true, "MySQL QP: active is correct");
ok(rule->username != nullptr && strcmp(rule->username, "testuser") == 0,
"MySQL QP: username is stored correctly");
ok(rule->schemaname != nullptr && strcmp(rule->schemaname, "testdb") == 0,
"MySQL QP: schemaname is stored correctly");
ok(rule->match_digest != nullptr && strcmp(rule->match_digest, "^SELECT") == 0,
"MySQL QP: match_digest is stored correctly");
ok(rule->match_pattern != nullptr && strcmp(rule->match_pattern, "SELECT.*FROM users") == 0,
"MySQL QP: match_pattern is stored correctly");
ok(rule->destination_hostgroup == 5,
"MySQL QP: destination_hostgroup is correct");
ok(rule->cache_ttl == 3000,
"MySQL QP: cache_ttl is correct");
ok(rule->timeout == 5000,
"MySQL QP: timeout is correct");
ok(rule->retries == 3,
"MySQL QP: retries is correct");
ok(rule->sticky_conn == 1,
"MySQL QP: sticky_conn is correct");
ok(rule->multiplex == 0,
"MySQL QP: multiplex is correct");
ok(rule->apply == true,
"MySQL QP: apply flag is correct");
ok(rule->comment != nullptr && strcmp(rule->comment, "route reads to HG 5") == 0,
"MySQL QP: comment is stored correctly");
ok(rule->log == 1,
"MySQL QP: log is correct");
ok(rule->client_addr != nullptr && strcmp(rule->client_addr, "192.168.1.%") == 0,
"MySQL QP: client_addr is stored correctly");
// Verify re_modifiers parsed correctly
ok(rule->re_modifiers & QP_RE_MOD_CASELESS,
"MySQL QP: CASELESS re_modifier is set");
// This rule is not inserted into the QP, so we free it manually.
free(rule->username); free(rule->schemaname);
free(rule->match_digest); free(rule->match_pattern);
free(rule->client_addr); free(rule->comment);
free(rule);
}
// ============================================================================
// 2. Rule insertion and retrieval via get_current_query_rules()
// ============================================================================
/**
* @brief Test inserting rules and retrieving them via SQL result set.
*/
static void test_mysql_insert_and_retrieve() {
// Create and insert rules
auto *r1 = mysql_simple_rule(10, true, "^SELECT", 1, true);
auto *r2 = mysql_simple_rule(20, true, "^INSERT", 2, true);
auto *r3 = mysql_simple_rule(30, false, "^DELETE", 3, true); // inactive
GloMyQPro->insert((QP_rule_t *)r1);
GloMyQPro->insert((QP_rule_t *)r2);
GloMyQPro->insert((QP_rule_t *)r3);
GloMyQPro->sort();
GloMyQPro->commit();
SQLite3_result *result = GloMyQPro->get_current_query_rules();
ok(result != nullptr, "MySQL QP: get_current_query_rules() returns non-null");
if (result != nullptr) {
ok(result->rows_count == 3,
"MySQL QP: get_current_query_rules() returns 3 rules");
delete result;
} else {
ok(0, "MySQL QP: get_current_query_rules() returns 3 rules (skipped)");
}
}
/**
* @brief Test that rules are sorted by rule_id after sort().
*/
static void test_mysql_rule_sorting() {
// Reset rules
GloMyQPro->reset_all(true);
// Insert in reverse order
auto *r3 = mysql_simple_rule(300, true, nullptr, 3);
auto *r1 = mysql_simple_rule(100, true, nullptr, 1);
auto *r2 = mysql_simple_rule(200, true, nullptr, 2);
GloMyQPro->insert((QP_rule_t *)r3);
GloMyQPro->insert((QP_rule_t *)r1);
GloMyQPro->insert((QP_rule_t *)r2);
GloMyQPro->sort();
GloMyQPro->commit();
SQLite3_result *result = GloMyQPro->get_current_query_rules();
ok(result != nullptr && result->rows_count == 3,
"MySQL QP: 3 rules after insert in reverse order");
if (result != nullptr && result->rows_count == 3) {
// First row should be rule_id=100 (sorted ascending)
auto it = result->rows.begin();
ok(strcmp((*it)->fields[0], "100") == 0,
"MySQL QP: first rule after sort has rule_id=100");
++it;
ok(strcmp((*it)->fields[0], "200") == 0,
"MySQL QP: second rule after sort has rule_id=200");
++it;
ok(strcmp((*it)->fields[0], "300") == 0,
"MySQL QP: third rule after sort has rule_id=300");
delete result;
} else {
ok(0, "MySQL QP: first rule sorted (skipped)");
ok(0, "MySQL QP: second rule sorted (skipped)");
ok(0, "MySQL QP: third rule sorted (skipped)");
if (result) {
delete result;
}
}
}
// ============================================================================
// 3. Regex modifier parsing
// ============================================================================
/**
* @brief Test re_modifiers parsing for CASELESS, GLOBAL, and combined.
*/
static void test_regex_modifiers() {
auto *r1 = mysql_simple_rule(1, true);
ok((r1->re_modifiers & QP_RE_MOD_CASELESS) == 0,
"MySQL QP: no modifiers when re_modifiers is null");
free(r1);
// Create rule with CASELESS
auto *r2 = MySQL_Query_Processor::new_query_rule(
2, true, nullptr, nullptr, 0, nullptr, nullptr, -1,
nullptr, nullptr, "test", false, "CASELESS",
-1, nullptr, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
nullptr, nullptr, -1, -1, -1, -1, false, nullptr, nullptr);
ok((r2->re_modifiers & QP_RE_MOD_CASELESS) != 0,
"MySQL QP: CASELESS modifier parsed");
ok((r2->re_modifiers & QP_RE_MOD_GLOBAL) == 0,
"MySQL QP: GLOBAL not set when only CASELESS specified");
free(r2->match_pattern);
free(r2);
// Create rule with CASELESS,GLOBAL
auto *r3 = MySQL_Query_Processor::new_query_rule(
3, true, nullptr, nullptr, 0, nullptr, nullptr, -1,
nullptr, nullptr, "test", false, "CASELESS,GLOBAL",
-1, nullptr, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
nullptr, nullptr, -1, -1, -1, -1, false, nullptr, nullptr);
ok((r3->re_modifiers & QP_RE_MOD_CASELESS) != 0,
"MySQL QP: CASELESS set in combined modifiers");
ok((r3->re_modifiers & QP_RE_MOD_GLOBAL) != 0,
"MySQL QP: GLOBAL set in combined modifiers");
free(r3->match_pattern);
free(r3);
}
// ============================================================================
// 4. Rule with all match fields populated
// ============================================================================
/**
* @brief Test rule with error_msg, OK_msg, and replace_pattern.
*/
static void test_mysql_rule_special_fields() {
auto *rule = MySQL_Query_Processor::new_query_rule(
50, true,
nullptr, nullptr, // username, schemaname
0, // flagIN
nullptr, nullptr, -1, // client_addr, proxy_addr, proxy_port
nullptr, // digest
nullptr, // match_digest
"^BLOCKED", // match_pattern
false, // negate_match_pattern
nullptr, // re_modifiers
-1, // flagOUT
"REWRITTEN", // replace_pattern
-1, // destination_hostgroup
-1, -1, -1, // cache_ttl, cache_empty_result, cache_timeout
-1, -1, -1, -1, // reconnect, timeout, retries, delay
-1, -1, -1, // next_query_flagIN, mirror_flagOUT, mirror_hostgroup
"Access denied", // error_msg
"Query OK", // OK_msg
-1, -1, -1, -1, // sticky_conn, multiplex, gtid_from_hostgroup, log
true, nullptr, nullptr // apply, attributes, comment
);
ok(rule->error_msg != nullptr && strcmp(rule->error_msg, "Access denied") == 0,
"MySQL QP: error_msg stored correctly");
ok(rule->OK_msg != nullptr && strcmp(rule->OK_msg, "Query OK") == 0,
"MySQL QP: OK_msg stored correctly");
ok(rule->replace_pattern != nullptr && strcmp(rule->replace_pattern, "REWRITTEN") == 0,
"MySQL QP: replace_pattern stored correctly");
free(rule->match_pattern); free(rule->replace_pattern);
free(rule->error_msg); free(rule->OK_msg);
free(rule);
}
// ============================================================================
// 5. flagIN/flagOUT chaining
// ============================================================================
/**
* @brief Test rule creation with flagIN/flagOUT for chain matching.
*/
static void test_mysql_flag_chaining() {
GloMyQPro->reset_all(true);
// Rule 1: flagIN=0, flagOUT=1 (passes to next stage)
auto *r1 = mysql_simple_rule(10, true, nullptr, -1, false,
nullptr, 0, 1);
// Rule 2: flagIN=1, applies with destination
auto *r2 = mysql_simple_rule(20, true, nullptr, 5, true,
nullptr, 1, -1);
GloMyQPro->insert((QP_rule_t *)r1);
GloMyQPro->insert((QP_rule_t *)r2);
GloMyQPro->sort();
GloMyQPro->commit();
SQLite3_result *result = GloMyQPro->get_current_query_rules();
ok(result != nullptr && result->rows_count == 2,
"MySQL QP: 2 chained rules inserted correctly");
if (result) {
delete result;
}
}
// ============================================================================
// 6. Rule with username filter
// ============================================================================
/**
* @brief Test rule creation with username filter.
*/
static void test_mysql_rule_with_username() {
GloMyQPro->reset_all(true);
auto *r1 = mysql_simple_rule(10, true, "^SELECT", 1, true, "admin");
auto *r2 = mysql_simple_rule(20, true, "^SELECT", 2, true, "readonly");
GloMyQPro->insert((QP_rule_t *)r1);
GloMyQPro->insert((QP_rule_t *)r2);
GloMyQPro->sort();
GloMyQPro->commit();
SQLite3_result *result = GloMyQPro->get_current_query_rules();
ok(result != nullptr && result->rows_count == 2,
"MySQL QP: 2 rules with username filters inserted");
if (result) {
delete result;
}
}
// ============================================================================
// 7. Reset all rules
// ============================================================================
/**
* @brief Test reset_all() clears all rules.
*/
static void test_mysql_reset_all() {
GloMyQPro->reset_all(true);
GloMyQPro->insert((QP_rule_t *)mysql_simple_rule(10, true));
GloMyQPro->insert((QP_rule_t *)mysql_simple_rule(20, true));
GloMyQPro->commit();
GloMyQPro->reset_all(true);
SQLite3_result *result = GloMyQPro->get_current_query_rules();
ok(result != nullptr && result->rows_count == 0,
"MySQL QP: no rules after reset_all()");
if (result) {
delete result;
}
}
// ============================================================================
// 8. Stats commands counters
// ============================================================================
/**
* @brief Test get_stats_commands_counters() returns a valid result.
*/
static void test_mysql_stats_counters() {
SQLite3_result *result = GloMyQPro->get_stats_commands_counters();
ok(result != nullptr,
"MySQL QP: get_stats_commands_counters() returns non-null");
if (result != nullptr) {
ok(result->columns > 0,
"MySQL QP: stats counters result has columns");
delete result;
} else {
ok(0, "MySQL QP: stats counters result has columns (skipped)");
}
}
// ============================================================================
// 9. PgSQL Query Processor: Basic operations
// ============================================================================
/**
* @brief Test PgSQL rule creation and insertion.
*/
static void test_pgsql_rule_creation_and_insert() {
auto *rule = pgsql_simple_rule(10, true, "^SELECT", 1, true);
ok(rule != nullptr, "PgSQL QP: new_query_rule() returns non-null");
ok(rule->rule_id == 10, "PgSQL QP: rule_id is correct");
ok(rule->destination_hostgroup == 1,
"PgSQL QP: destination_hostgroup is correct");
GloPgQPro->insert((QP_rule_t *)rule);
GloPgQPro->sort();
GloPgQPro->commit();
SQLite3_result *result = GloPgQPro->get_current_query_rules();
ok(result != nullptr && result->rows_count >= 1,
"PgSQL QP: rule appears in get_current_query_rules()");
if (result) {
delete result;
}
}
/**
* @brief Test PgSQL reset and stats.
*/
static void test_pgsql_reset_and_stats() {
GloPgQPro->reset_all(true);
SQLite3_result *result = GloPgQPro->get_current_query_rules();
ok(result != nullptr && result->rows_count == 0,
"PgSQL QP: no rules after reset_all()");
if (result) {
delete result;
}
}
// ============================================================================
// 10. PgSQL: Comprehensive rule creation with all fields
// ============================================================================
/**
* @brief Test PgSQL rule creation with all fields populated.
*
* Exercises PgSQL_Query_Processor::new_query_rule() with every
* parameter set, verifying correct storage including PgSQL-specific
* fields like log (which PgSQL exposes without gtid_from_hostgroup).
*/
static void test_pgsql_rule_creation_all_fields() {
auto *rule = PgSQL_Query_Processor::new_query_rule(
200, // rule_id
true, // active
"pguser", // username
"pgdb", // schemaname
0, // flagIN
"10.0.0.%", // client_addr (wildcard)
"127.0.0.1", // proxy_addr
6432, // proxy_port
"0x1234ABCD00000000", // digest
"^SELECT", // match_digest
"SELECT.*FROM orders", // match_pattern
true, // negate_match_pattern
"CASELESS,GLOBAL", // re_modifiers
5, // flagOUT
"REWRITTEN", // replace_pattern
10, // destination_hostgroup
5000, // cache_ttl
1, // cache_empty_result
2000, // cache_timeout
1, // reconnect
10000, // timeout
5, // retries
100, // delay
3, // next_query_flagIN
7, // mirror_flagOUT
20, // mirror_hostgroup
"blocked", // error_msg
"done", // OK_msg
1, // sticky_conn
2, // multiplex
1, // log
true, // apply
nullptr, // attributes
"PgSQL routing rule" // comment
);
ok(rule != nullptr, "PgSQL QP: all-fields rule is non-null");
ok(rule->rule_id == 200, "PgSQL QP: rule_id stored correctly");
ok(rule->active == true, "PgSQL QP: active stored correctly");
ok(rule->username != nullptr && strcmp(rule->username, "pguser") == 0,
"PgSQL QP: username stored correctly");
ok(rule->schemaname != nullptr && strcmp(rule->schemaname, "pgdb") == 0,
"PgSQL QP: schemaname stored correctly");
ok(rule->flagIN == 0, "PgSQL QP: flagIN stored correctly");
ok(rule->client_addr != nullptr && strcmp(rule->client_addr, "10.0.0.%") == 0,
"PgSQL QP: client_addr stored correctly");
ok(rule->client_addr_wildcard_position == 7,
"PgSQL QP: client_addr wildcard position computed correctly");
ok(rule->proxy_addr != nullptr && strcmp(rule->proxy_addr, "127.0.0.1") == 0,
"PgSQL QP: proxy_addr stored correctly");
ok(rule->proxy_port == 6432, "PgSQL QP: proxy_port stored correctly");
ok(rule->digest != 0, "PgSQL QP: digest parsed from hex string");
ok(rule->match_digest != nullptr && strcmp(rule->match_digest, "^SELECT") == 0,
"PgSQL QP: match_digest stored correctly");
ok(rule->match_pattern != nullptr && strcmp(rule->match_pattern, "SELECT.*FROM orders") == 0,
"PgSQL QP: match_pattern stored correctly");
ok(rule->negate_match_pattern == true,
"PgSQL QP: negate_match_pattern stored correctly");
ok((rule->re_modifiers & QP_RE_MOD_CASELESS) != 0,
"PgSQL QP: CASELESS re_modifier set");
ok((rule->re_modifiers & QP_RE_MOD_GLOBAL) != 0,
"PgSQL QP: GLOBAL re_modifier set");
ok(rule->flagOUT == 5, "PgSQL QP: flagOUT stored correctly");
ok(rule->replace_pattern != nullptr && strcmp(rule->replace_pattern, "REWRITTEN") == 0,
"PgSQL QP: replace_pattern stored correctly");
ok(rule->destination_hostgroup == 10,
"PgSQL QP: destination_hostgroup stored correctly");
ok(rule->cache_ttl == 5000, "PgSQL QP: cache_ttl stored correctly");
ok(rule->cache_empty_result == 1, "PgSQL QP: cache_empty_result stored correctly");
ok(rule->cache_timeout == 2000, "PgSQL QP: cache_timeout stored correctly");
ok(rule->timeout == 10000, "PgSQL QP: timeout stored correctly");
ok(rule->retries == 5, "PgSQL QP: retries stored correctly");
ok(rule->delay == 100, "PgSQL QP: delay stored correctly");
ok(rule->next_query_flagIN == 3, "PgSQL QP: next_query_flagIN stored correctly");
ok(rule->mirror_flagOUT == 7, "PgSQL QP: mirror_flagOUT stored correctly");
ok(rule->mirror_hostgroup == 20, "PgSQL QP: mirror_hostgroup stored correctly");
ok(rule->error_msg != nullptr && strcmp(rule->error_msg, "blocked") == 0,
"PgSQL QP: error_msg stored correctly");
ok(rule->OK_msg != nullptr && strcmp(rule->OK_msg, "done") == 0,
"PgSQL QP: OK_msg stored correctly");
ok(rule->sticky_conn == 1, "PgSQL QP: sticky_conn stored correctly");
ok(rule->multiplex == 2, "PgSQL QP: multiplex stored correctly");
ok(rule->log == 1, "PgSQL QP: log stored correctly");
ok(rule->apply == true, "PgSQL QP: apply stored correctly");
ok(rule->comment != nullptr && strcmp(rule->comment, "PgSQL routing rule") == 0,
"PgSQL QP: comment stored correctly");
ok(rule->hits == 0, "PgSQL QP: hits initialized to 0");
// Free manually since not inserted into QP
free(rule->username); free(rule->schemaname);
free(rule->client_addr); free(rule->proxy_addr);
free(rule->match_digest); free(rule->match_pattern);
free(rule->replace_pattern); free(rule->error_msg);
free(rule->OK_msg); free(rule->comment);
free(rule);
}
// ============================================================================
// 11. PgSQL: client_addr wildcard at position 0 (catch-all '%')
// ============================================================================
/**
* @brief Test client_addr wildcard position computation edge cases.
*/
static void test_pgsql_client_addr_wildcard() {
// Catch-all: client_addr = "%"
auto *r1 = PgSQL_Query_Processor::new_query_rule(
1, true, nullptr, nullptr, 0,
"%", // client_addr = catch-all
nullptr, -1, nullptr, nullptr, nullptr, false, nullptr,
-1, nullptr, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
nullptr, nullptr, -1, -1, -1, false, nullptr, nullptr);
ok(r1->client_addr_wildcard_position == 0,
"PgSQL QP: client_addr catch-all sets wildcard position to 0");
free(r1->client_addr);
free(r1);
// No wildcard
auto *r2 = PgSQL_Query_Processor::new_query_rule(
2, true, nullptr, nullptr, 0,
"192.168.1.1", // no wildcard
nullptr, -1, nullptr, nullptr, nullptr, false, nullptr,
-1, nullptr, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
nullptr, nullptr, -1, -1, -1, false, nullptr, nullptr);
ok(r2->client_addr_wildcard_position == -1,
"PgSQL QP: client_addr without wildcard has position -1");
free(r2->client_addr);
free(r2);
// No client_addr
auto *r3 = PgSQL_Query_Processor::new_query_rule(
3, true, nullptr, nullptr, 0,
nullptr, // no client_addr
nullptr, -1, nullptr, nullptr, nullptr, false, nullptr,
-1, nullptr, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
nullptr, nullptr, -1, -1, -1, false, nullptr, nullptr);
ok(r3->client_addr == nullptr,
"PgSQL QP: null client_addr is stored as null");
ok(r3->client_addr_wildcard_position == -1,
"PgSQL QP: null client_addr has wildcard position -1");
free(r3);
}
// ============================================================================
// 12. PgSQL: Rule with JSON attributes (flagOUTs)
// ============================================================================
/**
* @brief Test rule creation with flagOUTs in attributes JSON.
*
* Exercises the JSON attributes parsing path in new_query_rule(),
* which populates flagOUT_ids, flagOUT_weights, and flagOUT_weights_total.
*/
static void test_pgsql_rule_attributes_flagouts() {
const char *attrs = R"({"flagOUTs":[{"id":1,"weight":10},{"id":2,"weight":20}]})";
auto *rule = PgSQL_Query_Processor::new_query_rule(
50, true, nullptr, nullptr, 0,
nullptr, nullptr, -1, nullptr, nullptr, nullptr, false, nullptr,
-1, nullptr, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
nullptr, nullptr, -1, -1, -1, false, attrs, nullptr);
ok(rule->flagOUT_ids != nullptr,
"PgSQL QP: flagOUT_ids parsed from attributes");
ok(rule->flagOUT_weights != nullptr,
"PgSQL QP: flagOUT_weights parsed from attributes");
if (rule->flagOUT_ids && rule->flagOUT_weights) {
ok(rule->flagOUT_ids->size() == 2,
"PgSQL QP: flagOUT_ids has 2 entries");
ok(rule->flagOUT_weights->size() == 2,
"PgSQL QP: flagOUT_weights has 2 entries");
ok(rule->flagOUT_weights_total == 30,
"PgSQL QP: flagOUT_weights_total is sum of weights (30)");
ok((*rule->flagOUT_ids)[0] == 1 && (*rule->flagOUT_ids)[1] == 2,
"PgSQL QP: flagOUT_ids values are correct");
ok((*rule->flagOUT_weights)[0] == 10 && (*rule->flagOUT_weights)[1] == 20,
"PgSQL QP: flagOUT_weights values are correct");
} else {
// Skip tests if parsing failed
ok(0, "PgSQL QP: flagOUT_ids has 2 entries (skipped)");
ok(0, "PgSQL QP: flagOUT_weights has 2 entries (skipped)");
ok(0, "PgSQL QP: flagOUT_weights_total is sum of weights (skipped)");
ok(0, "PgSQL QP: flagOUT_ids values are correct (skipped)");
ok(0, "PgSQL QP: flagOUT_weights values are correct (skipped)");
}
if (rule->flagOUT_ids) {
delete rule->flagOUT_ids;
}
if (rule->flagOUT_weights) {
delete rule->flagOUT_weights;
}
free(rule->attributes);
free(rule);
}
// ============================================================================
// 13. PgSQL: get_stats_commands_counters validation
// ============================================================================
/**
* @brief Test PgSQL-specific command counters structure.
*
* Validates that get_stats_commands_counters() returns the expected
* number of columns (15) and rows covering PgSQL-specific command types
* (VACUUM, ANALYZE, LISTEN, NOTIFY, etc. up to PGSQL_QUERY__UNINITIALIZED).
*/
static void test_pgsql_stats_commands_counters() {
SQLite3_result *result = GloPgQPro->get_stats_commands_counters();
ok(result != nullptr,
"PgSQL QP: get_stats_commands_counters() returns non-null");
if (result != nullptr) {
ok(result->columns == 15,
"PgSQL QP: stats counters has 15 columns");
// PGSQL_QUERY__UNINITIALIZED is the count of actual command types
ok(result->rows_count == (unsigned int)PGSQL_QUERY__UNINITIALIZED,
"PgSQL QP: stats counters row count matches PGSQL_QUERY__UNINITIALIZED");
// Verify first row is "SELECT"
if (result->rows_count > 0) {
auto it = result->rows.begin();
ok((*it)->fields[0] != nullptr && strcmp((*it)->fields[0], "SELECT") == 0,
"PgSQL QP: first command counter is SELECT");
} else {
ok(0, "PgSQL QP: first command counter is SELECT (skipped)");
}
delete result;
} else {
ok(0, "PgSQL QP: stats counters has 15 columns (skipped)");
ok(0, "PgSQL QP: stats counters row count matches (skipped)");
ok(0, "PgSQL QP: first command counter is SELECT (skipped)");
}
}
// ============================================================================
// 14. PgSQL: get_stats_query_rules
// ============================================================================
/**
* @brief Test PgSQL stats query rules retrieval.
*/
static void test_pgsql_stats_query_rules() {
GloPgQPro->reset_all(true);
// Insert two active rules and one inactive
auto *r1 = pgsql_simple_rule(10, true, "^SELECT", 1, true);
auto *r2 = pgsql_simple_rule(20, true, "^INSERT", 2, true);
auto *r3 = pgsql_simple_rule(30, false, "^DELETE", 3, true);
GloPgQPro->insert((QP_rule_t *)r1);
GloPgQPro->insert((QP_rule_t *)r2);
GloPgQPro->insert((QP_rule_t *)r3);
GloPgQPro->sort();
GloPgQPro->commit();
SQLite3_result *result = GloPgQPro->get_stats_query_rules();
ok(result != nullptr,
"PgSQL QP: get_stats_query_rules() returns non-null");
if (result != nullptr) {
// Only active rules appear in stats
ok(result->rows_count == 2,
"PgSQL QP: stats shows 2 active rules (inactive excluded)");
ok(result->columns == 2,
"PgSQL QP: stats has 2 columns (rule_id, hits)");
delete result;
} else {
ok(0, "PgSQL QP: stats shows 2 active rules (skipped)");
ok(0, "PgSQL QP: stats has 2 columns (skipped)");
}
}
// ============================================================================
// 15. PgSQL: Rule field retrieval via get_current_query_rules() result
// ============================================================================
/**
* @brief Verify that get_current_query_rules() result columns match PgSQL schema.
*
* PgSQL has 35 columns in its rule text (no gtid_from_hostgroup but
* includes log). This test verifies the column count and names.
*/
static void test_pgsql_get_current_query_rules_columns() {
GloPgQPro->reset_all(true);
// Insert a rule with known values to verify result text serialization
auto *rule = PgSQL_Query_Processor::new_query_rule(
42, true, "testuser", "testdb", 0,
nullptr, nullptr, -1, nullptr, nullptr, "^SELECT", false,
"CASELESS,GLOBAL",
-1, nullptr, 7, -1, -1, -1, -1, 3000, -1, -1, -1, -1, -1,
nullptr, nullptr, -1, -1, 1, true, nullptr, "test comment");
GloPgQPro->insert((QP_rule_t *)rule);
GloPgQPro->sort();
GloPgQPro->commit();
SQLite3_result *result = GloPgQPro->get_current_query_rules();
ok(result != nullptr, "PgSQL QP: get_current_query_rules returns non-null");
if (result != nullptr) {
ok(result->columns == 35,
"PgSQL QP: result has 35 columns");
ok(result->rows_count == 1,
"PgSQL QP: result has 1 row");
if (result->rows_count == 1) {
auto it = result->rows.begin();
char **fields = (*it)->fields;
// Verify key field positions in PgSQL rule text:
// [0]=rule_id, [1]=active, [2]=username, [3]=database
ok(strcmp(fields[0], "42") == 0,
"PgSQL QP: rule_id field is '42'");
ok(strcmp(fields[1], "1") == 0,
"PgSQL QP: active field is '1'");
ok(fields[2] != nullptr && strcmp(fields[2], "testuser") == 0,
"PgSQL QP: username field is 'testuser'");
ok(fields[3] != nullptr && strcmp(fields[3], "testdb") == 0,
"PgSQL QP: database field is 'testdb'");
// [10]=match_pattern, [12]=re_modifiers
ok(fields[10] != nullptr && strcmp(fields[10], "^SELECT") == 0,
"PgSQL QP: match_pattern field is '^SELECT'");
ok(fields[12] != nullptr && strcmp(fields[12], "CASELESS,GLOBAL") == 0,
"PgSQL QP: re_modifiers field is 'CASELESS,GLOBAL'");
// [15]=destination_hostgroup, [20]=timeout, [30]=log, [31]=apply
ok(strcmp(fields[15], "7") == 0,
"PgSQL QP: destination_hostgroup field is '7'");
ok(strcmp(fields[20], "3000") == 0,
"PgSQL QP: timeout field is '3000'");
ok(strcmp(fields[30], "1") == 0,
"PgSQL QP: log field is '1'");
ok(strcmp(fields[31], "1") == 0,
"PgSQL QP: apply field is '1'");
ok(fields[33] != nullptr && strcmp(fields[33], "test comment") == 0,
"PgSQL QP: comment field is 'test comment'");
} else {
for (int i = 0; i < 13; i++) {
ok(0, "PgSQL QP: field check skipped (no rows)");
}
}
delete result;
} else {
for (int i = 0; i < 15; i++) {
ok(0, "PgSQL QP: get_current_query_rules column check (skipped)");
}
}
}
// ============================================================================
// 16. PgSQL: Sorting with reverse-ordered rules
// ============================================================================
/**
* @brief Test PgSQL rule sorting by rule_id.
*/
static void test_pgsql_rule_sorting() {
GloPgQPro->reset_all(true);
auto *r3 = pgsql_simple_rule(300, true, nullptr, 3);
auto *r1 = pgsql_simple_rule(100, true, nullptr, 1);
auto *r2 = pgsql_simple_rule(200, true, nullptr, 2);
GloPgQPro->insert((QP_rule_t *)r3);
GloPgQPro->insert((QP_rule_t *)r1);
GloPgQPro->insert((QP_rule_t *)r2);
GloPgQPro->sort();
GloPgQPro->commit();
SQLite3_result *result = GloPgQPro->get_current_query_rules();
ok(result != nullptr && result->rows_count == 3,
"PgSQL QP: 3 rules after reverse-order insert");
if (result != nullptr && result->rows_count == 3) {
auto it = result->rows.begin();
ok(strcmp((*it)->fields[0], "100") == 0,
"PgSQL QP: first rule after sort has rule_id=100");
++it;
ok(strcmp((*it)->fields[0], "200") == 0,
"PgSQL QP: second rule after sort has rule_id=200");
++it;
ok(strcmp((*it)->fields[0], "300") == 0,
"PgSQL QP: third rule after sort has rule_id=300");
delete result;
} else {
ok(0, "PgSQL QP: first rule sorted (skipped)");
ok(0, "PgSQL QP: second rule sorted (skipped)");
ok(0, "PgSQL QP: third rule sorted (skipped)");
if (result) {
delete result;
}
}
}
// ============================================================================
// 17. PgSQL: flagIN/flagOUT chaining
// ============================================================================
/**
* @brief Test PgSQL rule chaining via flagIN/flagOUT.
*/
static void test_pgsql_flag_chaining() {
GloPgQPro->reset_all(true);
auto *r1 = PgSQL_Query_Processor::new_query_rule(
10, true, nullptr, nullptr,
0, // flagIN
nullptr, nullptr, -1, nullptr, nullptr, nullptr, false, nullptr,
1, // flagOUT
nullptr, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
nullptr, nullptr, -1, -1, -1, false, nullptr, nullptr);
auto *r2 = PgSQL_Query_Processor::new_query_rule(
20, true, nullptr, nullptr,
1, // flagIN = prev flagOUT
nullptr, nullptr, -1, nullptr, nullptr, nullptr, false, nullptr,
-1, // flagOUT (terminal)
nullptr, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
nullptr, nullptr, -1, -1, -1, true, nullptr, nullptr);
GloPgQPro->insert((QP_rule_t *)r1);
GloPgQPro->insert((QP_rule_t *)r2);
GloPgQPro->sort();
GloPgQPro->commit();
SQLite3_result *result = GloPgQPro->get_current_query_rules();
ok(result != nullptr && result->rows_count == 2,
"PgSQL QP: 2 chained rules inserted correctly");
if (result) {
delete result;
}
}
// ============================================================================
// 18. PgSQL: query_parser_command_type — comprehensive command parsing
// ============================================================================
/**
* @brief Helper: call query_parser_command_type with a SQL string.
*
* Sets up an SQP_par_t with query_prefix pointing to a strdup of
* the input (since the function frees query_prefix).
*/
static enum PGSQL_QUERY_command pgsql_parse_cmd(const char *sql) {
SQP_par_t qp;
memset(&qp, 0, sizeof(qp));
qp.digest_text = nullptr;
qp.query_prefix = strdup(sql);
return PgSQL_Query_Processor::query_parser_command_type(&qp);
}
/**
* @brief Test PgSQL query_parser_command_type for all major command types.
*
* This is the largest uncovered area in PgSQL_Query_Processor.cpp:
* the giant switch statement classifying PgSQL commands. Each sub-test
* exercises a different branch.
*/
static void test_pgsql_query_parser_command_type() {
// Basic DML
ok(pgsql_parse_cmd("SELECT 1") == PGSQL_QUERY_SELECT,
"PgSQL cmd: SELECT");
ok(pgsql_parse_cmd("INSERT INTO t VALUES(1)") == PGSQL_QUERY_INSERT,
"PgSQL cmd: INSERT");
ok(pgsql_parse_cmd("UPDATE t SET x=1") == PGSQL_QUERY_UPDATE,
"PgSQL cmd: UPDATE");
ok(pgsql_parse_cmd("DELETE FROM t") == PGSQL_QUERY_DELETE,
"PgSQL cmd: DELETE");
ok(pgsql_parse_cmd("MERGE INTO t USING s ON t.id=s.id") == PGSQL_QUERY_MERGE,
"PgSQL cmd: MERGE");
ok(pgsql_parse_cmd("TRUNCATE TABLE t") == PGSQL_QUERY_TRUNCATE,
"PgSQL cmd: TRUNCATE");
ok(pgsql_parse_cmd("COPY t FROM '/tmp/data.csv'") == PGSQL_QUERY_COPY,
"PgSQL cmd: COPY");
// DDL: CREATE/ALTER/DROP TABLE
ok(pgsql_parse_cmd("CREATE TABLE t (id int)") == PGSQL_QUERY_CREATE_TABLE,
"PgSQL cmd: CREATE TABLE");
ok(pgsql_parse_cmd("ALTER TABLE t ADD COLUMN x int") == PGSQL_QUERY_ALTER_TABLE,
"PgSQL cmd: ALTER TABLE");
ok(pgsql_parse_cmd("DROP TABLE t") == PGSQL_QUERY_DROP_TABLE,
"PgSQL cmd: DROP TABLE");
// INDEX
ok(pgsql_parse_cmd("CREATE INDEX idx ON t(id)") == PGSQL_QUERY_CREATE_INDEX,
"PgSQL cmd: CREATE INDEX");
ok(pgsql_parse_cmd("ALTER INDEX idx RENAME") == PGSQL_QUERY_ALTER_INDEX,
"PgSQL cmd: ALTER INDEX");
ok(pgsql_parse_cmd("DROP INDEX idx") == PGSQL_QUERY_DROP_INDEX,
"PgSQL cmd: DROP INDEX");
// VIEW
ok(pgsql_parse_cmd("CREATE VIEW v AS SELECT 1") == PGSQL_QUERY_CREATE_VIEW,
"PgSQL cmd: CREATE VIEW");
ok(pgsql_parse_cmd("ALTER VIEW v RENAME") == PGSQL_QUERY_ALTER_VIEW,
"PgSQL cmd: ALTER VIEW");
ok(pgsql_parse_cmd("DROP VIEW v") == PGSQL_QUERY_DROP_VIEW,
"PgSQL cmd: DROP VIEW");
// MATERIALIZED VIEW
ok(pgsql_parse_cmd("CREATE MATERIALIZED VIEW mv AS SELECT 1") == PGSQL_QUERY_CREATE_MATERIALIZED_VIEW,
"PgSQL cmd: CREATE MATERIALIZED VIEW");
ok(pgsql_parse_cmd("ALTER MATERIALIZED VIEW mv RENAME") == PGSQL_QUERY_ALTER_MATERIALIZED_VIEW,
"PgSQL cmd: ALTER MATERIALIZED VIEW");
ok(pgsql_parse_cmd("DROP MATERIALIZED VIEW mv") == PGSQL_QUERY_DROP_MATERIALIZED_VIEW,
"PgSQL cmd: DROP MATERIALIZED VIEW");
ok(pgsql_parse_cmd("REFRESH MATERIALIZED VIEW mv") == PGSQL_QUERY_REFRESH_MATERIALIZED_VIEW,
"PgSQL cmd: REFRESH MATERIALIZED VIEW");
// SEQUENCE
ok(pgsql_parse_cmd("CREATE SEQUENCE seq") == PGSQL_QUERY_CREATE_SEQUENCE,
"PgSQL cmd: CREATE SEQUENCE");
ok(pgsql_parse_cmd("ALTER SEQUENCE seq RESTART") == PGSQL_QUERY_ALTER_SEQUENCE,
"PgSQL cmd: ALTER SEQUENCE");
ok(pgsql_parse_cmd("DROP SEQUENCE seq") == PGSQL_QUERY_DROP_SEQUENCE,
"PgSQL cmd: DROP SEQUENCE");
// SCHEMA
ok(pgsql_parse_cmd("CREATE SCHEMA myschema") == PGSQL_QUERY_CREATE_SCHEMA,
"PgSQL cmd: CREATE SCHEMA");
ok(pgsql_parse_cmd("ALTER SCHEMA myschema OWNER TO u") == PGSQL_QUERY_ALTER_SCHEMA,
"PgSQL cmd: ALTER SCHEMA");
ok(pgsql_parse_cmd("DROP SCHEMA myschema") == PGSQL_QUERY_DROP_SCHEMA,
"PgSQL cmd: DROP SCHEMA");
// FUNCTION / PROCEDURE
ok(pgsql_parse_cmd("CREATE FUNCTION f() RETURNS void") == PGSQL_QUERY_CREATE_FUNCTION,
"PgSQL cmd: CREATE FUNCTION");
ok(pgsql_parse_cmd("ALTER FUNCTION f() OWNER TO u") == PGSQL_QUERY_ALTER_FUNCTION,
"PgSQL cmd: ALTER FUNCTION");
ok(pgsql_parse_cmd("DROP FUNCTION f()") == PGSQL_QUERY_DROP_FUNCTION,
"PgSQL cmd: DROP FUNCTION");
ok(pgsql_parse_cmd("CREATE PROCEDURE p() LANGUAGE plpgsql") == PGSQL_QUERY_CREATE_PROCEDURE,
"PgSQL cmd: CREATE PROCEDURE");
ok(pgsql_parse_cmd("ALTER PROCEDURE p() OWNER TO u") == PGSQL_QUERY_ALTER_PROCEDURE,
"PgSQL cmd: ALTER PROCEDURE");
ok(pgsql_parse_cmd("DROP PROCEDURE p()") == PGSQL_QUERY_DROP_PROCEDURE,
"PgSQL cmd: DROP PROCEDURE");
ok(pgsql_parse_cmd("CALL my_procedure()") == PGSQL_QUERY_CALL,
"PgSQL cmd: CALL");
// AGGREGATE / OPERATOR / TYPE / DOMAIN
ok(pgsql_parse_cmd("CREATE AGGREGATE agg(int) (sfunc=f, stype=int)") == PGSQL_QUERY_CREATE_AGGREGATE,
"PgSQL cmd: CREATE AGGREGATE");
ok(pgsql_parse_cmd("DROP AGGREGATE agg(int)") == PGSQL_QUERY_DROP_AGGREGATE,
"PgSQL cmd: DROP AGGREGATE");
ok(pgsql_parse_cmd("CREATE OPERATOR +(int,int)") == PGSQL_QUERY_CREATE_OPERATOR,
"PgSQL cmd: CREATE OPERATOR");
ok(pgsql_parse_cmd("DROP OPERATOR +(int,int)") == PGSQL_QUERY_DROP_OPERATOR,
"PgSQL cmd: DROP OPERATOR");
ok(pgsql_parse_cmd("CREATE TYPE mytype AS (x int)") == PGSQL_QUERY_CREATE_TYPE,
"PgSQL cmd: CREATE TYPE");
ok(pgsql_parse_cmd("DROP TYPE mytype") == PGSQL_QUERY_DROP_TYPE,
"PgSQL cmd: DROP TYPE");
ok(pgsql_parse_cmd("CREATE DOMAIN mydomain AS int") == PGSQL_QUERY_CREATE_DOMAIN,
"PgSQL cmd: CREATE DOMAIN");
ok(pgsql_parse_cmd("DROP DOMAIN mydomain") == PGSQL_QUERY_DROP_DOMAIN,
"PgSQL cmd: DROP DOMAIN");
// TRIGGER / RULE / EXTENSION / POLICY
ok(pgsql_parse_cmd("CREATE TRIGGER trig AFTER INSERT ON t") == PGSQL_QUERY_CREATE_TRIGGER,
"PgSQL cmd: CREATE TRIGGER");
ok(pgsql_parse_cmd("DROP TRIGGER trig ON t") == PGSQL_QUERY_DROP_TRIGGER,
"PgSQL cmd: DROP TRIGGER");
ok(pgsql_parse_cmd("CREATE RULE r AS ON INSERT TO t DO NOTHING") == PGSQL_QUERY_CREATE_RULE,
"PgSQL cmd: CREATE RULE");
ok(pgsql_parse_cmd("DROP RULE r ON t") == PGSQL_QUERY_DROP_RULE,
"PgSQL cmd: DROP RULE");
ok(pgsql_parse_cmd("CREATE EXTENSION hstore") == PGSQL_QUERY_CREATE_EXTENSION,
"PgSQL cmd: CREATE EXTENSION");
ok(pgsql_parse_cmd("DROP EXTENSION hstore") == PGSQL_QUERY_DROP_EXTENSION,
"PgSQL cmd: DROP EXTENSION");
ok(pgsql_parse_cmd("CREATE POLICY pol ON t") == PGSQL_QUERY_CREATE_POLICY,
"PgSQL cmd: CREATE POLICY");
ok(pgsql_parse_cmd("DROP POLICY pol ON t") == PGSQL_QUERY_DROP_POLICY,
"PgSQL cmd: DROP POLICY");
// ROLE / USER
ok(pgsql_parse_cmd("CREATE ROLE myrole") == PGSQL_QUERY_CREATE_ROLE,
"PgSQL cmd: CREATE ROLE");
ok(pgsql_parse_cmd("ALTER ROLE myrole LOGIN") == PGSQL_QUERY_ALTER_ROLE,
"PgSQL cmd: ALTER ROLE");
ok(pgsql_parse_cmd("DROP ROLE myrole") == PGSQL_QUERY_DROP_ROLE,
"PgSQL cmd: DROP ROLE");
ok(pgsql_parse_cmd("CREATE USER myuser") == PGSQL_QUERY_CREATE_USER,
"PgSQL cmd: CREATE USER");
ok(pgsql_parse_cmd("ALTER USER myuser PASSWORD 'x'") == PGSQL_QUERY_ALTER_USER,
"PgSQL cmd: ALTER USER");
ok(pgsql_parse_cmd("DROP USER myuser") == PGSQL_QUERY_DROP_USER,
"PgSQL cmd: DROP USER");
// GRANT / REVOKE
ok(pgsql_parse_cmd("GRANT SELECT ON t TO u") == PGSQL_QUERY_GRANT,
"PgSQL cmd: GRANT");
ok(pgsql_parse_cmd("REVOKE SELECT ON t FROM u") == PGSQL_QUERY_REVOKE,
"PgSQL cmd: REVOKE");
// Transaction control
ok(pgsql_parse_cmd("BEGIN") == PGSQL_QUERY_BEGIN,
"PgSQL cmd: BEGIN");
ok(pgsql_parse_cmd("START TRANSACTION") == PGSQL_QUERY_BEGIN,
"PgSQL cmd: START TRANSACTION -> BEGIN");
ok(pgsql_parse_cmd("COMMIT") == PGSQL_QUERY_COMMIT,
"PgSQL cmd: COMMIT");
ok(pgsql_parse_cmd("ROLLBACK") == PGSQL_QUERY_ROLLBACK,
"PgSQL cmd: ROLLBACK");
ok(pgsql_parse_cmd("ABORT") == PGSQL_QUERY_ROLLBACK,
"PgSQL cmd: ABORT -> ROLLBACK");
ok(pgsql_parse_cmd("SAVEPOINT sp1") == PGSQL_QUERY_SAVEPOINT,
"PgSQL cmd: SAVEPOINT");
ok(pgsql_parse_cmd("ROLLBACK TO SAVEPOINT sp1") == PGSQL_QUERY_ROLLBACK_TO_SAVEPOINT,
"PgSQL cmd: ROLLBACK TO SAVEPOINT");
ok(pgsql_parse_cmd("RELEASE SAVEPOINT sp1") == PGSQL_QUERY_RELEASE_SAVEPOINT,
"PgSQL cmd: RELEASE SAVEPOINT");
// Session / utility
ok(pgsql_parse_cmd("SET search_path TO public") == PGSQL_QUERY_SET,
"PgSQL cmd: SET");
ok(pgsql_parse_cmd("SHOW search_path") == PGSQL_QUERY_SHOW,
"PgSQL cmd: SHOW");
ok(pgsql_parse_cmd("RESET search_path") == PGSQL_QUERY_RESET,
"PgSQL cmd: RESET");
ok(pgsql_parse_cmd("DISCARD ALL") == PGSQL_QUERY_DISCARD,
"PgSQL cmd: DISCARD");
// PgSQL-specific
ok(pgsql_parse_cmd("LISTEN channel1") == PGSQL_QUERY_LISTEN,
"PgSQL cmd: LISTEN");
ok(pgsql_parse_cmd("UNLISTEN channel1") == PGSQL_QUERY_UNLISTEN,
"PgSQL cmd: UNLISTEN");
ok(pgsql_parse_cmd("NOTIFY channel1") == PGSQL_QUERY_NOTIFY,
"PgSQL cmd: NOTIFY");
ok(pgsql_parse_cmd("COMMENT ON TABLE t IS 'desc'") == PGSQL_QUERY_COMMENT,
"PgSQL cmd: COMMENT");
ok(pgsql_parse_cmd("LOCK TABLE t") == PGSQL_QUERY_LOCK,
"PgSQL cmd: LOCK");
ok(pgsql_parse_cmd("CHECKPOINT") == PGSQL_QUERY_CHECKPOINT,
"PgSQL cmd: CHECKPOINT");
ok(pgsql_parse_cmd("REINDEX TABLE t") == PGSQL_QUERY_REINDEX,
"PgSQL cmd: REINDEX");
ok(pgsql_parse_cmd("VACUUM t") == PGSQL_QUERY_VACUUM,
"PgSQL cmd: VACUUM");
ok(pgsql_parse_cmd("ANALYZE t") == PGSQL_QUERY_ANALYZE,
"PgSQL cmd: ANALYZE");
ok(pgsql_parse_cmd("EXPLAIN SELECT 1") == PGSQL_QUERY_EXPLAIN,
"PgSQL cmd: EXPLAIN");
ok(pgsql_parse_cmd("EXECUTE stmt1") == PGSQL_QUERY_EXECUTE,
"PgSQL cmd: EXECUTE");
ok(pgsql_parse_cmd("PREPARE stmt1 AS SELECT 1") == PGSQL_QUERY_PREPARE,
"PgSQL cmd: PREPARE");
ok(pgsql_parse_cmd("DEALLOCATE stmt1") == PGSQL_QUERY_DEALLOCATE,
"PgSQL cmd: DEALLOCATE");
ok(pgsql_parse_cmd("FETCH NEXT FROM cursor1") == PGSQL_QUERY_FETCH,
"PgSQL cmd: FETCH");
ok(pgsql_parse_cmd("MOVE NEXT IN cursor1") == PGSQL_QUERY_MOVE,
"PgSQL cmd: MOVE");
ok(pgsql_parse_cmd("CLUSTER t") == PGSQL_QUERY_CLUSTER,
"PgSQL cmd: CLUSTER");
// DATABASE
ok(pgsql_parse_cmd("CREATE DATABASE mydb") == PGSQL_QUERY_CREATE_DATABASE,
"PgSQL cmd: CREATE DATABASE");
ok(pgsql_parse_cmd("ALTER DATABASE mydb SET x=1") == PGSQL_QUERY_ALTER_DATABASE,
"PgSQL cmd: ALTER DATABASE");
ok(pgsql_parse_cmd("DROP DATABASE mydb") == PGSQL_QUERY_DROP_DATABASE,
"PgSQL cmd: DROP DATABASE");
// COLLATION
ok(pgsql_parse_cmd("CREATE COLLATION mycoll (locale='en_US.utf8')") == PGSQL_QUERY_CREATE_COLLATION,
"PgSQL cmd: CREATE COLLATION");
ok(pgsql_parse_cmd("DROP COLLATION mycoll") == PGSQL_QUERY_DROP_COLLATION,
"PgSQL cmd: DROP COLLATION");
// TEXT SEARCH objects
ok(pgsql_parse_cmd("CREATE TEXT SEARCH CONFIGURATION tsc (parser=p)") == PGSQL_QUERY_CREATE_TEXT_SEARCH_CONFIGURATION,
"PgSQL cmd: CREATE TEXT SEARCH CONFIGURATION");
ok(pgsql_parse_cmd("CREATE TEXT SEARCH DICTIONARY tsd (template=t)") == PGSQL_QUERY_CREATE_TEXT_SEARCH_DICTIONARY,
"PgSQL cmd: CREATE TEXT SEARCH DICTIONARY");
ok(pgsql_parse_cmd("CREATE TEXT SEARCH TEMPLATE tst (init=f)") == PGSQL_QUERY_CREATE_TEXT_SEARCH_TEMPLATE,
"PgSQL cmd: CREATE TEXT SEARCH TEMPLATE");
ok(pgsql_parse_cmd("CREATE TEXT SEARCH PARSER tsp (start=f)") == PGSQL_QUERY_CREATE_TEXT_SEARCH_PARSER,
"PgSQL cmd: CREATE TEXT SEARCH PARSER");
ok(pgsql_parse_cmd("DROP TEXT SEARCH CONFIGURATION tsc") == PGSQL_QUERY_DROP_TEXT_SEARCH_CONFIGURATION,
"PgSQL cmd: DROP TEXT SEARCH CONFIGURATION");
ok(pgsql_parse_cmd("DROP TEXT SEARCH DICTIONARY tsd") == PGSQL_QUERY_DROP_TEXT_SEARCH_DICTIONARY,
"PgSQL cmd: DROP TEXT SEARCH DICTIONARY");
// FOREIGN TABLE / SERVER / USER MAPPING
ok(pgsql_parse_cmd("CREATE FOREIGN TABLE ft (id int) SERVER srv") == PGSQL_QUERY_CREATE_FOREIGN_TABLE,
"PgSQL cmd: CREATE FOREIGN TABLE");
ok(pgsql_parse_cmd("DROP FOREIGN TABLE ft") == PGSQL_QUERY_DROP_FOREIGN_TABLE,
"PgSQL cmd: DROP FOREIGN TABLE");
ok(pgsql_parse_cmd("CREATE SERVER srv FOREIGN DATA WRAPPER fdw") == PGSQL_QUERY_CREATE_SERVER,
"PgSQL cmd: CREATE SERVER");
ok(pgsql_parse_cmd("DROP SERVER srv") == PGSQL_QUERY_DROP_SERVER,
"PgSQL cmd: DROP SERVER");
ok(pgsql_parse_cmd("IMPORT FOREIGN SCHEMA public FROM SERVER srv INTO local") == PGSQL_QUERY_IMPORT_FOREIGN_SCHEMA,
"PgSQL cmd: IMPORT FOREIGN SCHEMA");
// PUBLICATION / SUBSCRIPTION
ok(pgsql_parse_cmd("CREATE PUBLICATION pub FOR ALL TABLES") == PGSQL_QUERY_CREATE_PUBLICATION,
"PgSQL cmd: CREATE PUBLICATION");
ok(pgsql_parse_cmd("DROP PUBLICATION pub") == PGSQL_QUERY_DROP_PUBLICATION,
"PgSQL cmd: DROP PUBLICATION");
ok(pgsql_parse_cmd("CREATE SUBSCRIPTION sub CONNECTION '' PUBLICATION pub") == PGSQL_QUERY_CREATE_SUBSCRIPTION,
"PgSQL cmd: CREATE SUBSCRIPTION");
ok(pgsql_parse_cmd("DROP SUBSCRIPTION sub") == PGSQL_QUERY_DROP_SUBSCRIPTION,
"PgSQL cmd: DROP SUBSCRIPTION");
// EVENT TRIGGER / TRANSFORM / CAST
ok(pgsql_parse_cmd("CREATE EVENT TRIGGER et ON ddl_command_start") == PGSQL_QUERY_CREATE_EVENT_TRIGGER,
"PgSQL cmd: CREATE EVENT TRIGGER");
ok(pgsql_parse_cmd("DROP EVENT TRIGGER et") == PGSQL_QUERY_DROP_EVENT_TRIGGER,
"PgSQL cmd: DROP EVENT TRIGGER");
ok(pgsql_parse_cmd("CREATE TRANSFORM FOR mytype LANGUAGE plpgsql") == PGSQL_QUERY_CREATE_TRANSFORM,
"PgSQL cmd: CREATE TRANSFORM");
ok(pgsql_parse_cmd("DROP TRANSFORM FOR mytype LANGUAGE plpgsql") == PGSQL_QUERY_DROP_TRANSFORM,
"PgSQL cmd: DROP TRANSFORM");
ok(pgsql_parse_cmd("CREATE CAST (int AS text) WITH FUNCTION f") == PGSQL_QUERY_CREATE_CAST,
"PgSQL cmd: CREATE CAST");
ok(pgsql_parse_cmd("DROP CAST (int AS text)") == PGSQL_QUERY_DROP_CAST,
"PgSQL cmd: DROP CAST");
// OPERATOR CLASS / FAMILY — Note: the parser's else-if chain matches
// "OPERATOR" before reaching the "OPERATOR CLASS/FAMILY" branches,
// so all OPERATOR variants return the base OPERATOR command type.
ok(pgsql_parse_cmd("ALTER OPERATOR CLASS oc USING btree RENAME") == PGSQL_QUERY_ALTER_OPERATOR,
"PgSQL cmd: ALTER OPERATOR CLASS returns ALTER_OPERATOR");
ok(pgsql_parse_cmd("ALTER OPERATOR FAMILY ofam USING btree ADD") == PGSQL_QUERY_ALTER_OPERATOR,
"PgSQL cmd: ALTER OPERATOR FAMILY returns ALTER_OPERATOR");
ok(pgsql_parse_cmd("CREATE OPERATOR CLASS oc FOR TYPE int") == PGSQL_QUERY_CREATE_OPERATOR,
"PgSQL cmd: CREATE OPERATOR CLASS returns CREATE_OPERATOR");
ok(pgsql_parse_cmd("DROP OPERATOR CLASS oc USING btree") == PGSQL_QUERY_DROP_OPERATOR,
"PgSQL cmd: DROP OPERATOR CLASS returns DROP_OPERATOR");
// TABLESPACE
ok(pgsql_parse_cmd("CREATE TABLESPACE ts LOCATION '/data'") == PGSQL_QUERY_CREATE_TABLESPACE,
"PgSQL cmd: CREATE TABLESPACE");
ok(pgsql_parse_cmd("DROP TABLESPACE ts") == PGSQL_QUERY_DROP_TABLESPACE,
"PgSQL cmd: DROP TABLESPACE");
// ACCESS METHOD
ok(pgsql_parse_cmd("CREATE ACCESS METHOD am TYPE INDEX HANDLER f") == PGSQL_QUERY_CREATE_ACCESS_METHOD,
"PgSQL cmd: CREATE ACCESS METHOD");
ok(pgsql_parse_cmd("DROP ACCESS METHOD am") == PGSQL_QUERY_DROP_ACCESS_METHOD,
"PgSQL cmd: DROP ACCESS METHOD");
// pg_cancel_backend / pg_terminate_backend
ok(pgsql_parse_cmd("SELECT pg_cancel_backend(1234)") == PGSQL_QUERY_CANCEL_BACKEND,
"PgSQL cmd: SELECT pg_cancel_backend");
ok(pgsql_parse_cmd("SELECT pg_terminate_backend(1234)") == PGSQL_QUERY_TERMINATE_BACKEND,
"PgSQL cmd: SELECT pg_terminate_backend");
// DECLARE CURSOR / CLOSE CURSOR
ok(pgsql_parse_cmd("DECLARE CURSOR c FOR SELECT 1") == PGSQL_QUERY_DECLARE_CURSOR,
"PgSQL cmd: DECLARE CURSOR");
// Edge case: parenthesized subquery
ok(pgsql_parse_cmd("(SELECT 1)") == PGSQL_QUERY_SELECT,
"PgSQL cmd: (SELECT 1) parses as SELECT");
// Unknown command
ok(pgsql_parse_cmd("XYZZY") == PGSQL_QUERY_UNKNOWN,
"PgSQL cmd: unknown command returns PGSQL_QUERY_UNKNOWN");
// Case insensitivity
ok(pgsql_parse_cmd("select 1") == PGSQL_QUERY_SELECT,
"PgSQL cmd: lowercase 'select' recognized");
ok(pgsql_parse_cmd("Vacuum t") == PGSQL_QUERY_VACUUM,
"PgSQL cmd: mixed case 'Vacuum' recognized");
}
// ============================================================================
// 19. PgSQL: Fast routing hashmap
// ============================================================================
/**
* @brief Test PgSQL fast routing hashmap creation and lookup.
*/
static void test_pgsql_fast_routing() {
GloPgQPro->reset_all(true);
// Build a resultset with 4 columns: username, schemaname, flagIN, dest_hg
SQLite3_result *fr_resultset = new SQLite3_result(4);
fr_resultset->add_column_definition(SQLITE_TEXT, "username");
fr_resultset->add_column_definition(SQLITE_TEXT, "schemaname");
fr_resultset->add_column_definition(SQLITE_TEXT, "flagIN");
fr_resultset->add_column_definition(SQLITE_TEXT, "destination_hostgroup");
{
char *row1[] = { (char*)"user1", (char*)"db1", (char*)"0", (char*)"10" };
fr_resultset->add_row(row1);
char *row2[] = { (char*)"user2", (char*)"db2", (char*)"0", (char*)"20" };
fr_resultset->add_row(row2);
char *row3[] = { (char*)"user1", (char*)"db1", (char*)"1", (char*)"30" };
fr_resultset->add_row(row3);
}
fast_routing_hashmap_t hm = GloPgQPro->create_fast_routing_hashmap(fr_resultset);
ok(hm.rules_fast_routing != nullptr,
"PgSQL QP: fast routing hashmap created successfully");
GloPgQPro->wrlock();
SQLite3_result *old = GloPgQPro->load_fast_routing(hm);
GloPgQPro->wrunlock();
// Test lookups
int hg = GloPgQPro->testing___find_HG_in_mysql_query_rules_fast_routing(
(char*)"user1", (char*)"db1", 0);
ok(hg == 10, "PgSQL QP: fast routing finds user1/db1/flagIN=0 -> HG 10");
hg = GloPgQPro->testing___find_HG_in_mysql_query_rules_fast_routing(
(char*)"user2", (char*)"db2", 0);
ok(hg == 20, "PgSQL QP: fast routing finds user2/db2/flagIN=0 -> HG 20");
hg = GloPgQPro->testing___find_HG_in_mysql_query_rules_fast_routing(
(char*)"user1", (char*)"db1", 1);
ok(hg == 30, "PgSQL QP: fast routing finds user1/db1/flagIN=1 -> HG 30");
// Miss
hg = GloPgQPro->testing___find_HG_in_mysql_query_rules_fast_routing(
(char*)"nouser", (char*)"nodb", 0);
ok(hg == -1, "PgSQL QP: fast routing returns -1 for missing entry");
// Verify count
int count = GloPgQPro->get_current_query_rules_fast_routing_count();
ok(count == 3, "PgSQL QP: fast routing count is 3");
if (old) {
delete old;
}
}
// ============================================================================
// 20. PgSQL: Firewall whitelist
// ============================================================================
/**
* @brief Test PgSQL firewall whitelist user/rule loading and lookup.
*/
static void test_pgsql_firewall_whitelist() {
// Build user resultset: active, username, client_address, mode
SQLite3_result *users = new SQLite3_result(4);
users->add_column_definition(SQLITE_TEXT, "active");
users->add_column_definition(SQLITE_TEXT, "username");
users->add_column_definition(SQLITE_TEXT, "client_address");
users->add_column_definition(SQLITE_TEXT, "mode");
{
char *row1[] = { (char*)"1", (char*)"pguser", (char*)"10.0.0.1", (char*)"PROTECTING" };
users->add_row(row1);
char *row2[] = { (char*)"1", (char*)"pgadmin", (char*)"10.0.0.2", (char*)"DETECTING" };
users->add_row(row2);
char *row3[] = { (char*)"0", (char*)"inactive", (char*)"10.0.0.3", (char*)"PROTECTING" };
users->add_row(row3);
}
// Build rules resultset: active, username, client_address, schemaname, flagIN, digest
SQLite3_result *rules = new SQLite3_result(6);
rules->add_column_definition(SQLITE_TEXT, "active");
rules->add_column_definition(SQLITE_TEXT, "username");
rules->add_column_definition(SQLITE_TEXT, "client_address");
rules->add_column_definition(SQLITE_TEXT, "schemaname");
rules->add_column_definition(SQLITE_TEXT, "flagIN");
rules->add_column_definition(SQLITE_TEXT, "digest");
{
char *row1[] = { (char*)"1", (char*)"pguser", (char*)"10.0.0.1", (char*)"public", (char*)"0", (char*)"0xABCD" };
rules->add_row(row1);
}
// Empty sqli fingerprints
SQLite3_result *sqli = new SQLite3_result(1);
sqli->add_column_definition(SQLITE_TEXT, "fingerprint");
GloPgQPro->load_firewall(users, rules, sqli);
// Test user lookup
int mode = GloPgQPro->find_firewall_whitelist_user((char*)"pguser", (char*)"10.0.0.1");
ok(mode == WUS_PROTECTING,
"PgSQL QP: firewall user pguser/10.0.0.1 found in PROTECTING mode");
mode = GloPgQPro->find_firewall_whitelist_user((char*)"pgadmin", (char*)"10.0.0.2");
ok(mode == WUS_DETECTING,
"PgSQL QP: firewall user pgadmin/10.0.0.2 found in DETECTING mode");
mode = GloPgQPro->find_firewall_whitelist_user((char*)"unknown", (char*)"10.0.0.99");
ok(mode == WUS_NOT_FOUND,
"PgSQL QP: firewall returns NOT_FOUND for unknown user");
// Test rule lookup
bool found = GloPgQPro->find_firewall_whitelist_rule(
(char*)"pguser", (char*)"10.0.0.1", (char*)"public", 0, 0xABCD);
ok(found == true,
"PgSQL QP: firewall rule found for matching digest");
found = GloPgQPro->find_firewall_whitelist_rule(
(char*)"pguser", (char*)"10.0.0.1", (char*)"public", 0, 0x9999);
ok(found == false,
"PgSQL QP: firewall rule not found for non-matching digest");
// Verify memory tracking
unsigned long long mem_users = GloPgQPro->get_firewall_memory_users_table();
ok(mem_users > 0, "PgSQL QP: firewall users memory tracked (> 0)");
unsigned long long mem_rules = GloPgQPro->get_firewall_memory_rules_table();
ok(mem_rules > 0, "PgSQL QP: firewall rules memory tracked (> 0)");
}
// ============================================================================
// 21. PgSQL: Query digests
// ============================================================================
/**
* @brief Test PgSQL query digest operations.
*/
static void test_pgsql_query_digests() {
// get_query_digests on empty state should return valid result
SQLite3_result *result = GloPgQPro->get_query_digests();
ok(result != nullptr, "PgSQL QP: get_query_digests returns non-null on empty state");
if (result) {
ok(result->rows_count == 0,
"PgSQL QP: get_query_digests has 0 rows on empty state");
delete result;
} else {
ok(0, "PgSQL QP: get_query_digests has 0 rows (skipped)");
}
// Test digest total size
unsigned long long digest_size = GloPgQPro->get_query_digests_total_size();
ok(digest_size == 0,
"PgSQL QP: digest total size is 0 on empty state");
// Test purge on empty state
char *msg = nullptr;
unsigned long long purged = GloPgQPro->purge_query_digests(false, false, &msg);
ok(purged == 0, "PgSQL QP: purge_query_digests returns 0 on empty state");
}
// ============================================================================
// 22. PgSQL: Memory usage tracking
// ============================================================================
/**
* @brief Test PgSQL memory usage tracking for rules.
*/
static void test_pgsql_memory_tracking() {
GloPgQPro->reset_all(true);
unsigned long long mem_empty = GloPgQPro->get_rules_mem_used();
// Insert several rules
for (int i = 1; i <= 5; i++) {
auto *r = pgsql_simple_rule(i * 10, true, "^SELECT.*", i, true);
GloPgQPro->insert((QP_rule_t *)r);
}
GloPgQPro->sort();
GloPgQPro->commit();
unsigned long long mem_with_rules = GloPgQPro->get_rules_mem_used();
ok(mem_with_rules >= mem_empty,
"PgSQL QP: rules memory usage increases after inserting rules");
}
// ============================================================================
// Main
// ============================================================================
int main() {
plan(259);
test_init_minimal();
test_init_query_processor();
// MySQL tests
test_mysql_rule_creation(); // 18 tests
test_mysql_insert_and_retrieve(); // 2 tests
test_mysql_rule_sorting(); // 4 tests
test_regex_modifiers(); // 5 tests
test_mysql_rule_special_fields(); // 3 tests
test_mysql_flag_chaining(); // 1 test
test_mysql_rule_with_username(); // 1 test
test_mysql_reset_all(); // 1 test
test_mysql_stats_counters(); // 2 tests
// PgSQL tests — existing
test_pgsql_rule_creation_and_insert(); // 4 tests
test_pgsql_reset_and_stats(); // 1 test
// PgSQL tests — new
test_pgsql_rule_creation_all_fields(); // 36 tests
test_pgsql_client_addr_wildcard(); // 4 tests
test_pgsql_rule_attributes_flagouts(); // 7 tests
test_pgsql_stats_commands_counters(); // 4 tests
test_pgsql_stats_query_rules(); // 3 tests
test_pgsql_get_current_query_rules_columns(); // 14 tests
test_pgsql_rule_sorting(); // 4 tests
test_pgsql_flag_chaining(); // 1 test
test_pgsql_query_parser_command_type(); // 126 tests (bulk coverage of switch)
test_pgsql_fast_routing(); // 6 tests
test_pgsql_firewall_whitelist(); // 7 tests
test_pgsql_query_digests(); // 4 tests
test_pgsql_memory_tracking(); // 1 test
test_cleanup_query_processor();
test_cleanup_minimal();
return exit_status();
}