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/pgsql-admin_metacmds_descri...

552 lines
19 KiB

/**
* @file pgsql-describe_aueries-t.cpp
* @brief Advanced testing for \d meta-command query sequence with data validation
*
* This test validates each of the 8 queries in the \d tablename sequence
* by executing them individually and checking the response format AND data accuracy.
*
* Query Sequence:
* 1. pg_class with c.relname OPERATOR - returns: oid, nspname, relname
* 2. pg_class with c.oid = - returns: relchecks, relkind, relhasindex, etc.
* 3. pg_attribute - returns: attname, format_type, pg_get_expr, attnotnull, etc.
* 4. pg_policy - returns: polname, polpermissive, polroles, etc.
* 5. pg_statistic_ext - returns: oid, stxrelid, nsp, stxname, columns, etc.
* 6. pg_publication - returns: pubname, prqual, prattrs
* 7. pg_inherits (parent) - returns: oid
* 8. pg_inherits (child) - returns: oid, relkind, inhdetachpending, pg_get_expr
*
* Data Validation:
* - Compares returned column names against actual SQLite table_info
* - Validates table type detection (table vs view)
* - Verifies column count and names match
*/
#include <string>
#include <sstream>
#include <vector>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <libpq-fe.h>
#include "command_line.h"
#include "tap.h"
#include "utils.h"
// Execute a SQL query and return result
PGresult* execute_query(PGconn* conn, const char* query) {
return PQexec(conn, query);
}
// Check if result has expected column
bool result_has_column(PGresult* res, const char* col_name) {
int ncols = PQnfields(res);
for (int i = 0; i < ncols; i++) {
if (strcmp(PQfname(res, i), col_name) == 0) {
return true;
}
}
return false;
}
// Get column value as string (handles NULL)
std::string get_column_value(PGresult* res, int row, int col) {
if (PQgetisnull(res, row, col)) {
return "";
}
return std::string(PQgetvalue(res, row, col));
}
// Build connection string
PGconn* connect_to_admin(const CommandLine& cl) {
std::stringstream conninfo;
conninfo << "host=" << cl.pgsql_admin_host;
conninfo << " port=" << cl.pgsql_admin_port;
conninfo << " user=" << cl.admin_username;
conninfo << " password=" << cl.admin_password;
conninfo << " dbname=postgres";
conninfo << " connect_timeout=10";
return PQconnectdb(conninfo.str().c_str());
}
// ==================== DATA VALIDATION HELPERS ====================
/**
* @brief Get expected columns from SQLite table_info
*
* Returns a vector of column names that should be present
* based on the actual SQLite admin table structure.
*/
std::vector<std::string> get_expected_columns_from_sqlite(const char* table_name) {
// Known columns for common admin tables
if (strcmp(table_name, "mysql_servers") == 0) {
return {"hostgroup_id", "hostname", "port", "gtid_port", "status",
"weight", "compression", "max_connections", "max_replication_lag",
"use_ssl", "max_latency_ms", "comment"};
} else if (strcmp(table_name, "mysql_users") == 0) {
return {"username", "password", "active", "use_ssl", "default_hostgroup",
"default_schema", "schema_locked", "transaction_persistent",
"fast_forward", "backend", "frontend", "max_connections", "attributes", "comment"};
} else if (strcmp(table_name, "pgsql_servers") == 0) {
return {"hostgroup_id", "hostname", "port", "status", "weight",
"compression", "max_connections", "max_replication_lag",
"use_ssl", "max_latency_ms", "comment"};
} else if (strcmp(table_name, "global_variables") == 0) {
return {"variable_name", "variable_value"};
}
// Default: return empty (will be validated differently)
return {};
}
// ==================== QUERY TESTS WITH DATA VALIDATION ====================
void test_query_1_pg_class_lookup(PGconn* conn) {
diag("Query 1: pg_class table lookup");
const char* query =
"SELECT c.oid, n.nspname, c.relname "
"FROM pg_catalog.pg_class c "
"LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace "
"WHERE c.relname OPERATOR(pg_catalog.~) '^(mysql_servers)$' "
"AND pg_catalog.pg_table_is_visible(c.oid) "
"ORDER BY 2, 3";
PGresult* res = execute_query(conn, query);
bool success = PQresultStatus(res) == PGRES_TUPLES_OK;
bool has_oid = result_has_column(res, "oid");
bool has_nspname = result_has_column(res, "nspname");
bool has_relname = result_has_column(res, "relname");
// DATA VALIDATION: Check that relname matches what we asked for
bool data_correct = true;
if (PQntuples(res) > 0) {
std::string relname = get_column_value(res, 0, 2); // c.relname is at index 2
if (relname != "mysql_servers") {
diag("ERROR: Expected relname='mysql_servers', got '%s'", relname.c_str());
data_correct = false;
}
std::string nspname = get_column_value(res, 0, 1); // n.nspname is at index 1
if (nspname != "public") {
diag("ERROR: Expected nspname='public', got '%s'", nspname.c_str());
data_correct = false;
}
}
ok(success && has_oid && has_nspname && has_relname && data_correct,
"Query 1: pg_class lookup returns correct structure and data");
PQclear(res);
}
void test_query_2_pg_class_attributes(PGconn* conn) {
diag("Query 2: pg_class table attributes with data validation");
const char* query =
"SELECT 0 AS relchecks, "
"CASE WHEN (SELECT type FROM sqlite_master WHERE name='mysql_servers' AND type='table' LIMIT 1) = 'table' THEN 'r' "
"WHEN (SELECT type FROM sqlite_master WHERE name='mysql_servers' AND type='view' LIMIT 1) = 'view' THEN 'v' "
"ELSE 'r' END AS relkind, "
"(SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='index' AND tbl_name='mysql_servers')) AS relhasindex, "
"0 AS relhasrules, "
"(SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='trigger' AND tbl_name='mysql_servers')) AS relhastriggers, "
"0 AS relrowsecurity, 0 AS relforcerowsecurity, 0 AS relhasoids, 0 AS relispartition, "
"'' AS empty1, 0 AS reltablespace, '' AS reloftype, "
"'p' AS relpersistence, 'd' AS relreplident, 'heap' AS amname";
PGresult* res = execute_query(conn, query);
bool success = PQresultStatus(res) == PGRES_TUPLES_OK;
// DATA VALIDATION: Check relkind is 'r' for regular table
bool relkind_correct = false;
if (PQntuples(res) > 0) {
std::string relkind = get_column_value(res, 0, 1); // relkind at index 1
relkind_correct = (relkind == "r");
if (!relkind_correct) {
diag("ERROR: Expected relkind='r' for table, got '%s'", relkind.c_str());
}
}
ok(success && relkind_correct,
"Query 2: pg_class attributes returns correct table type detection");
PQclear(res);
}
void test_query_3_pg_attribute_with_validation(PGconn* conn) {
diag("Query 3: pg_attribute column info with data validation");
const char* query =
"SELECT name AS attname, type AS format_type, dflt_value AS pg_get_expr, "
"'f' AS attnotnull, NULL AS attcollation, '' AS attidentity, '' AS attgenerated "
"FROM pragma_table_info('mysql_servers') ORDER BY cid";
PGresult* res = execute_query(conn, query);
bool success = PQresultStatus(res) == PGRES_TUPLES_OK;
// DATA VALIDATION: Check that returned columns match expected SQLite columns
std::vector<std::string> expected_cols = get_expected_columns_from_sqlite("mysql_servers");
std::vector<std::string> returned_cols;
int nrows = PQntuples(res);
for (int i = 0; i < nrows; i++) {
returned_cols.push_back(get_column_value(res, i, 0)); // attname at index 0
}
// Check that expected columns are present
bool columns_match = true;
for (const auto& expected : expected_cols) {
if (std::find(returned_cols.begin(), returned_cols.end(), expected) == returned_cols.end()) {
diag("ERROR: Expected column '%s' not found in result", expected.c_str());
columns_match = false;
}
}
// Check column count is reasonable (should have at least 10 columns for mysql_servers)
bool col_count_ok = nrows >= 10;
if (!col_count_ok) {
diag("ERROR: Expected at least 10 columns, got %d", nrows);
}
ok(success && columns_match && col_count_ok,
"Query 3: pg_attribute returns correct columns matching SQLite schema (%d columns)", nrows);
PQclear(res);
}
void test_query_4_pg_policy_empty(PGconn* conn) {
diag("Query 4: pg_policy RLS policies (should be empty)");
const char* query =
"SELECT '' AS polname, 't' AS polpermissive, NULL AS polroles, "
"NULL AS pg_get_expr_1, NULL AS pg_get_expr_2, '' AS cmd WHERE 0=1";
PGresult* res = execute_query(conn, query);
bool success = PQresultStatus(res) == PGRES_TUPLES_OK;
// DATA VALIDATION: Should return 0 rows (SQLite has no RLS)
int nrows = PQntuples(res);
bool is_empty = (nrows == 0);
ok(success && is_empty,
"Query 4: pg_policy correctly returns empty result (no RLS in SQLite)");
PQclear(res);
}
void test_query_5_pg_statistic_ext_empty(PGconn* conn) {
diag("Query 5: pg_statistic_ext (should be empty)");
const char* query =
"SELECT 0 AS oid, '' AS stxrelid, '' AS nsp, '' AS stxname, "
"NULL AS columns, 0 AS ndist_enabled, 0 AS deps_enabled, "
"0 AS mcv_enabled, 0 AS stxstattarget WHERE 0=1";
PGresult* res = execute_query(conn, query);
bool success = PQresultStatus(res) == PGRES_TUPLES_OK;
int nrows = PQntuples(res);
bool is_empty = (nrows == 0);
ok(success && is_empty,
"Query 5: pg_statistic_ext correctly returns empty result");
PQclear(res);
}
void test_query_6_pg_publication_empty(PGconn* conn) {
diag("Query 6: pg_publication (should be empty)");
const char* query =
"SELECT '' AS pubname, NULL AS prqual, NULL AS prattrs WHERE 0=1";
PGresult* res = execute_query(conn, query);
bool success = PQresultStatus(res) == PGRES_TUPLES_OK;
int nrows = PQntuples(res);
bool is_empty = (nrows == 0);
ok(success && is_empty,
"Query 6: pg_publication correctly returns empty result");
PQclear(res);
}
void test_query_7_pg_inherits_parent_empty(PGconn* conn) {
diag("Query 7: pg_inherits parent (should be empty - no inheritance)");
const char* query =
"SELECT '' AS oid WHERE 0=1";
PGresult* res = execute_query(conn, query);
bool success = PQresultStatus(res) == PGRES_TUPLES_OK;
int nrows = PQntuples(res);
bool is_empty = (nrows == 0);
ok(success && is_empty,
"Query 7: pg_inherits parent correctly returns empty result");
PQclear(res);
}
void test_query_8_pg_inherits_child_empty(PGconn* conn) {
diag("Query 8: pg_inherits child/partitions (should be empty)");
const char* query =
"SELECT '' AS oid, '' AS relkind, 0 AS inhdetachpending, NULL AS pg_get_expr WHERE 0=1";
PGresult* res = execute_query(conn, query);
bool success = PQresultStatus(res) == PGRES_TUPLES_OK;
int nrows = PQntuples(res);
bool is_empty = (nrows == 0);
ok(success && is_empty,
"Query 8: pg_inherits child correctly returns empty result");
PQclear(res);
}
// ==================== CROSS-TABLE VALIDATION ====================
void test_column_consistency_across_tables(PGconn* conn) {
diag("================================================================");
diag("Cross-table column consistency validation");
diag("================================================================");
struct TableExpectation {
const char* table_name;
int min_columns;
};
TableExpectation tables[] = {
{"mysql_servers", 10},
{"mysql_users", 10},
{"pgsql_servers", 10},
{"global_variables", 2},
{"mysql_query_rules", 5}
};
int passed = 0;
for (const auto& table : tables) {
std::stringstream query;
query << "SELECT name AS attname, type AS format_type, dflt_value AS pg_get_expr, "
<< "'f' AS attnotnull, NULL AS attcollation, '' AS attidentity, '' AS attgenerated "
<< "FROM pragma_table_info('" << table.table_name << "') ORDER BY cid";
PGresult* res = execute_query(conn, query.str().c_str());
bool success = PQresultStatus(res) == PGRES_TUPLES_OK;
int nrows = PQntuples(res);
bool has_min_cols = nrows >= table.min_columns;
if (success && has_min_cols) {
passed++;
diag(" %s: PASS (%d columns)", table.table_name, nrows);
} else {
diag(" %s: FAIL (%d columns, expected >= %d)", table.table_name, nrows, table.min_columns);
}
PQclear(res);
}
ok(passed == 5, "All 5 tables have expected minimum columns (%d/5 passed)", passed);
}
// ==================== DESCRIBE_MODE RESET TEST ====================
void test_describe_mode_reset(PGconn* conn) {
diag("================================================================");
diag("Testing describe_mode reset functionality");
diag("================================================================");
// Step 1: Execute first describe query (sets describe_mode=true, saves table name)
const char* query1 =
"SELECT c.oid, n.nspname, c.relname "
"FROM pg_catalog.pg_class c "
"LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace "
"WHERE c.relname OPERATOR(pg_catalog.~) '^(mysql_servers)$' "
"AND pg_catalog.pg_table_is_visible(c.oid) "
"ORDER BY 2, 3";
PGresult* res1 = execute_query(conn, query1);
bool q1_ok = PQresultStatus(res1) == PGRES_TUPLES_OK;
PQclear(res1);
ok(q1_ok, "Step 1: Describe query 1 executed (describe_mode should be true)");
// Step 2: Execute second describe query (uses saved table name)
const char* query2 =
"SELECT name AS attname, type AS format_type, dflt_value AS pg_get_expr, "
"'f' AS attnotnull, NULL AS attcollation, '' AS attidentity, '' AS attgenerated "
"FROM pragma_table_info('mysql_servers') ORDER BY cid";
PGresult* res2 = execute_query(conn, query2);
bool q2_ok = PQresultStatus(res2) == PGRES_TUPLES_OK;
int q2_rows = PQntuples(res2);
PQclear(res2);
ok(q2_ok && q2_rows > 0, "Step 2: Describe query 2 used saved table name (%d columns)", q2_rows);
// Step 3: Execute NON-DESCRIBE query (should reset describe_mode=false)
const char* query3 = "SELECT 1 AS test_column";
PGresult* res3 = execute_query(conn, query3);
bool q3_ok = PQresultStatus(res3) == PGRES_TUPLES_OK;
PQclear(res3);
ok(q3_ok, "Step 3: Non-describe query executed (describe_mode should be reset to false)");
// Step 4: Execute new describe query with different table
// If describe_mode was correctly reset, this should work with new table
const char* query4 =
"SELECT c.oid, n.nspname, c.relname "
"FROM pg_catalog.pg_class c "
"LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace "
"WHERE c.relname OPERATOR(pg_catalog.~) '^(pgsql_servers)$' "
"AND pg_catalog.pg_table_is_visible(c.oid) "
"ORDER BY 2, 3";
PGresult* res4 = execute_query(conn, query4);
bool q4_ok = PQresultStatus(res4) == PGRES_TUPLES_OK;
std::string q4_relname = PQntuples(res4) > 0 ? PQgetvalue(res4, 0, 2) : "";
bool q4_correct = q4_ok && q4_relname == "pgsql_servers";
PQclear(res4);
ok(q4_correct, "Step 4: New describe query uses 'pgsql_servers' (not old 'mysql_servers')");
// Step 5: Verify pg_attribute works with new table
const char* query5 =
"SELECT name AS attname, type AS format_type, dflt_value AS pg_get_expr, "
"'f' AS attnotnull, NULL AS attcollation, '' AS attidentity, '' AS attgenerated "
"FROM pragma_table_info('pgsql_servers') ORDER BY cid";
PGresult* res5 = execute_query(conn, query5);
bool q5_ok = PQresultStatus(res5) == PGRES_TUPLES_OK;
int q5_rows = PQntuples(res5);
PQclear(res5);
ok(q5_ok && q5_rows > 0, "Step 5: pg_attribute for 'pgsql_servers' returned %d columns", q5_rows);
}
// ==================== ERROR HANDLING TESTS ====================
void test_describe_error_handling(PGconn* conn) {
diag("================================================================");
diag("Testing describe mode error handling");
diag("================================================================");
// Test 1: Query 2 without Query 1 should return error
diag("Test: Query 2 (pg_class attributes) without prior Query 1");
const char* query2_no_q1 =
"SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, c.relhastriggers, "
"c.relrowsecurity, c.relforcerowsecurity, false AS relhasoids, c.relispartition, "
"'', c.reltablespace, '', c.relpersistence, c.relreplident, 'heap' AS amname "
"FROM pg_catalog.pg_class c "
"WHERE c.oid = 'test_table'";
PGresult* res1 = execute_query(conn, query2_no_q1);
bool q2_error = PQresultStatus(res1) == PGRES_FATAL_ERROR;
if (!q2_error && PQresultStatus(res1) == PGRES_TUPLES_OK) {
// Check if we got empty result (old behavior) or actual data
int rows = PQntuples(res1);
diag("Query 2 returned %d rows (expected error)", rows);
}
PQclear(res1);
ok(q2_error, "Query 2 without Query 1 returns error (not empty result)");
// Test 2: Query 3 without Query 1 should return error
diag("Test: Query 3 (pg_attribute) without prior Query 1");
const char* query3_no_q1 =
"SELECT a.attname, "
"pg_catalog.format_type(a.atttypid, a.atttypmod), "
"pg_catalog.pg_get_expr(d.adbin, d.adrelid), "
"a.attnotnull, '' AS attcollation, a.attidentity, a.attgenerated "
"FROM pg_catalog.pg_attribute a "
"WHERE a.attrelid = 'test_table'";
PGresult* res2 = execute_query(conn, query3_no_q1);
bool q3_error = PQresultStatus(res2) == PGRES_FATAL_ERROR;
PQclear(res2);
ok(q3_error, "Query 3 without Query 1 returns error (not empty result)");
// Test 3: Query 1 with unextractable pattern should return error
diag("Test: Query 1 with invalid/unextractable pattern");
const char* query1_invalid =
"SELECT c.oid, n.nspname, c.relname "
"FROM pg_catalog.pg_class c "
"LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace "
"WHERE c.relname OPERATOR(pg_catalog.~) '' " // Empty pattern
"AND pg_catalog.pg_table_is_visible(c.oid) "
"ORDER BY 2, 3";
PGresult* res3 = execute_query(conn, query1_invalid);
bool q1_error = PQresultStatus(res3) == PGRES_FATAL_ERROR;
PQclear(res3);
ok(q1_error, "Query 1 with invalid pattern returns error");
}
// ==================== MAIN ====================
int main(int argc, char** argv) {
CommandLine cl;
if (cl.getEnv()) {
diag("Failed to get the required environmental variables.");
return -1;
}
// Skip test if PostgreSQL admin port is not configured
if (cl.pgsql_admin_port == 0) {
plan(1);
skip(1, "PostgreSQL admin interface not configured");
return exit_status();
}
plan(18);
// Connect to admin interface
PGconn* conn = connect_to_admin(cl);
if (PQstatus(conn) != CONNECTION_OK) {
diag("Connection to admin interface failed: %s", PQerrorMessage(conn));
PQfinish(conn);
return -1;
}
ok(true, "Connected to PostgreSQL admin interface");
diag("================================================================");
diag("CONCERT TESTING: Full 8-Query \\d Sequence with Data Validation");
diag("================================================================");
// Run all 8 query tests with data validation
test_query_1_pg_class_lookup(conn);
test_query_2_pg_class_attributes(conn);
test_query_3_pg_attribute_with_validation(conn);
test_query_4_pg_policy_empty(conn);
test_query_5_pg_statistic_ext_empty(conn);
test_query_6_pg_publication_empty(conn);
test_query_7_pg_inherits_parent_empty(conn);
test_query_8_pg_inherits_child_empty(conn);
// Cross-table validation
test_column_consistency_across_tables(conn);
// Describe mode reset functionality test
test_describe_mode_reset(conn);
// Error handling tests
test_describe_error_handling(conn);
// Cleanup
PQfinish(conn);
return exit_status();
}