/** * @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 // 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(); }