Add support for PostgreSQL psql meta-commands in admin interface

Implement handling for psql meta-commands (\dt, \di, \dv, \d, \l) in ProxySQL's PostgreSQL admin interface by intercepting expanded queries and converting them to appropriate SQLite queries.

Supported commands:
- \l, \l+: List databases (maps to PRAGMA DATABASE_LIST)
- \dt [pattern]: List tables with optional pattern matching
- \di [pattern]: List indexes with optional pattern matching
- \dv [pattern]: List views with optional pattern matching
- \d: List all relations (tables, views, indexes, triggers)

Pattern matching supports psql regex syntax like '\dt mytest*' by converting PostgreSQL regex patterns (^(pattern).*$) to SQLite LIKE patterns (pattern%).
pull/5367/head
Rahim Kanji 2 months ago
parent 80195edb82
commit 331b0d6bce

@ -310,6 +310,151 @@ const std::vector<std::string> LOAD_COREDUMP_FROM_MEMORY = {
extern unordered_map<string,std::tuple<string, vector<string>, vector<string>>> load_save_disk_commands;
// Helper function: Escape single quotes in a string for SQL safety
// Returns number of chars written to dst (excluding null terminator)
static size_t escape_sql_string(char* dst, const char* src, size_t dst_size) {
if (!dst || !src || dst_size == 0) return 0;
size_t i = 0, j = 0;
while (src[i] && j < dst_size - 1) {
if (src[i] == '\'') {
// Escape single quote by doubling it
if (j < dst_size - 2) {
dst[j++] = '\'';
dst[j++] = '\'';
}
}
else {
dst[j++] = src[i];
}
i++;
}
dst[j] = '\0';
return j;
}
// Helper function: Extract pattern from c.relname OPERATOR or LIKE clause
// Returns true if pattern was found, false otherwise
// pattern_buf must be at least 128 bytes
static bool extract_psql_pattern(const char* query, char* pattern_buf, size_t buf_size) {
if (!query || !pattern_buf || buf_size < 64) return false;
pattern_buf[0] = '\0';
// Look for pattern in c.relname
char* relname_pos = strcasestr((char*)query, "c.relname");
if (!relname_pos) return false;
// Skip to the operator or LIKE keyword
char* value_pos = relname_pos + strlen("c.relname");
while (*value_pos && *value_pos == ' ') value_pos++;
// Safety check: ensure we haven't gone past end of string
if (!*value_pos) return false;
char* pattern = nullptr;
// Check for LIKE operator first
char* like_pos = strcasestr(value_pos, "LIKE");
if (like_pos) {
like_pos += 4; // Skip "LIKE"
while (*like_pos && *like_pos == ' ') like_pos++;
pattern = like_pos;
}
// Check for OPERATOR operator
else if (strcasestr(value_pos, "OPERATOR")) {
char* op_pos = strcasestr(value_pos, "OPERATOR");
char* value_start = strchr(op_pos, '\'');
if (value_start) {
value_start++;
pattern = value_start;
}
}
if (!pattern) return false;
// Skip leading spaces
while (*pattern && *pattern == ' ') pattern++;
// Find end of pattern (first quote or newline)
char* end = pattern;
size_t max_len = buf_size / 2 - 1; // Leave room for escaping
while (*end && *end != '\'' && *end != '\n' && (end - pattern) < (int)max_len) end++;
// Copy and escape to safe buffer
size_t len = end - pattern;
if (len > 0) {
escape_sql_string(pattern_buf, pattern, buf_size);
return true;
}
return false;
}
// Helper function: Convert PostgreSQL regex pattern to SQLite LIKE pattern
// Handles: ^, $, (), and .* to % conversion
// dst_size should be at least 128 (twice src size for escaping)
static void convert_regex_to_like(char* dst, const char* src, size_t dst_size) {
if (!dst || !src || dst_size < 64) return;
char* dst_end = dst + dst_size - 1;
// Skip ^ at start
if (*src == '^') src++;
// Copy pattern, converting regex to LIKE
while (*src && dst < dst_end - 1) { // Reserve space for potential escape
if (*src == '.' && *(src + 1) == '*') {
*dst++ = '%';
src += 2;
}
else if (*src == '$' && (*(src + 1) == '\0' || *(src + 1) == '\'')) {
// Skip $ at end
break;
}
else if (*src == '(' || *src == ')') {
// Skip regex grouping parentheses
src++;
}
else {
// Escape single quotes for SQL safety
if (*src == '\'' && dst < dst_end - 1) {
*dst++ = '\'';
}
*dst++ = *src++;
}
}
*dst = '\0';
}
// Unified handler for \dt, \di, \dv commands
// relkind_char: 'r' for tables, 'i' for indexes, 'v' for views
// sqlite_type: "table", "index", or "view"
// columns: columns to SELECT (e.g., "name" or "name, tbl_name")
static void handle_psql_list_command(const char* query_no_space, char** query, unsigned int* query_length,
char relkind_char, const char* sqlite_type, const char* columns) {
// Check for pattern in WHERE clause
const char* where_clause = strcasestr(query_no_space, "WHERE");
char pattern_buf[128] = { 0 };
char converted_pattern[128] = { 0 };
char buf[512] = { 0 };
if (where_clause && extract_psql_pattern(where_clause, pattern_buf, sizeof(pattern_buf))) {
convert_regex_to_like(converted_pattern, pattern_buf, sizeof(converted_pattern));
// Build query with pattern - note: converted_pattern is already escaped
snprintf(buf, sizeof(buf), "SELECT %s FROM sqlite_master WHERE type='%s' AND name NOT LIKE 'sqlite_%%' AND name LIKE '%s' ORDER BY name",
columns, sqlite_type, converted_pattern);
}
else {
// Build query without pattern
snprintf(buf, sizeof(buf), "SELECT %s FROM sqlite_master WHERE type='%s' AND name NOT LIKE 'sqlite_%%' ORDER BY name",
columns, sqlite_type);
}
*query = l_strdup(buf);
*query_length = strlen(*query) + 1;
}
bool is_admin_command_or_alias(const std::vector<std::string>& cmds, char *query_no_space, int query_no_space_length) {
for (std::vector<std::string>::const_iterator it=cmds.begin(); it!=cmds.end(); ++it) {
if ((unsigned int)query_no_space_length==it->length() && !strncasecmp(it->c_str(), query_no_space, query_no_space_length)) {
@ -4106,6 +4251,67 @@ void admin_session_handler(S* sess, void *_pa, PtrSize_t *pkt) {
goto __run_query;
}
// Handle PostgreSQL meta commands expanded by psql client
// These commands are intercepted and converted to appropriate SQLite queries
if constexpr (std::is_same_v<S, PgSQL_Session>) {
if (query_no_space_length >= strlen("SELECT") && !strncasecmp("SELECT", query_no_space, strlen("SELECT"))) {
// \l, \l+ : List databases
// psql: SELECT ... FROM pg_catalog.pg_database ...
// sqlite: PRAGMA DATABASE_LIST
if (strcasestr(query_no_space, "FROM pg_catalog.pg_database") != nullptr ||
strcasestr(query_no_space, "FROM pg_database") != nullptr) {
l_free(query_length, query);
query = l_strdup("PRAGMA DATABASE_LIST");
query_length = strlen(query) + 1;
goto __run_query;
}
// \d : List all relations (without table name)
// psql: SELECT ... FROM pg_catalog.pg_class c ... WHERE c.relkind IN ('r','p','v','m','S','f','')
// sqlite: SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view', 'index', 'trigger') ...
// Note: \d includes 'v' (views) in relkind, \dt does not
if ((strcasestr(query_no_space, "FROM pg_catalog.pg_class c") != nullptr ||
strcasestr(query_no_space, "FROM pg_class c") != nullptr) &&
strcasestr(query_no_space, "c.relkind IN ('r','p','v'") != nullptr) {
l_free(query_length, query);
// List all relations
query = l_strdup("SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view', 'index', 'trigger') AND name NOT LIKE 'sqlite_%' ORDER BY type, name");
query_length = strlen(query) + 1;
goto __run_query;
}
// \dt [pattern] : List tables (with optional pattern)
// psql: SELECT ... FROM pg_catalog.pg_class c ... WHERE c.relkind IN ('r','p', ...)
if ((strcasestr(query_no_space, "FROM pg_catalog.pg_class c") != nullptr ||
strcasestr(query_no_space, "FROM pg_class c") != nullptr) &&
strcasestr(query_no_space, "c.relkind IN ('r'") != nullptr) {
l_free(query_length, query);
handle_psql_list_command(query_no_space, &query, &query_length, 'r', "table", "name");
goto __run_query;
}
// \di [pattern] : List indexes (with optional pattern)
if ((strcasestr(query_no_space, "FROM pg_catalog.pg_class c") != nullptr ||
strcasestr(query_no_space, "FROM pg_class c") != nullptr) &&
strcasestr(query_no_space, "c.relkind IN ('i'") != nullptr) {
l_free(query_length, query);
handle_psql_list_command(query_no_space, &query, &query_length, 'i', "index", "name, tbl_name");
goto __run_query;
}
// \dv [pattern] : List views (with optional pattern)
if ((strcasestr(query_no_space, "FROM pg_catalog.pg_class c") != nullptr ||
strcasestr(query_no_space, "FROM pg_class c") != nullptr) &&
strcasestr(query_no_space, "c.relkind IN ('v'") != nullptr) {
l_free(query_length, query);
handle_psql_list_command(query_no_space, &query, &query_length, 'v', "view", "name");
goto __run_query;
}
}
}
if (strncasecmp("SHOW ", query_no_space, 5)) {
goto __end_show_commands; // in the next block there are only SHOW commands
}

Loading…
Cancel
Save