Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/functions/system_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub fn register_system_functions(conn: &Connection) -> Result<()> {
|_ctx| {
// Return a PostgreSQL-compatible version string
// This format is what SQLAlchemy expects to parse
Ok(format!("PostgreSQL 15.0 (pgsqlite {}) on x86_64-pc-linux-gnu, compiled by rustc, 64-bit",
Ok(format!("PostgreSQL 16.0 (pgsqlite {}) on x86_64-pc-linux-gnu, compiled by rustc, 64-bit",
env!("CARGO_PKG_VERSION")))
},
)?;
Expand Down
128 changes: 128 additions & 0 deletions src/query/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ use once_cell::sync::Lazy;
use uuid::Uuid;
use regex::Regex;

static PG_SHOW_ALL_SETTINGS_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)pg_show_all_settings\(\s*\)").unwrap()
});

static SET_CONFIG_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)set_config\(\s*'([^']+)'\s*,\s*'([^']*)'\s*,\s*(true|false)\s*\)").unwrap()
});

fn preprocess_query(query: &str) -> String {
if PG_SHOW_ALL_SETTINGS_PATTERN.is_match(query) {
PG_SHOW_ALL_SETTINGS_PATTERN.replace_all(query, "pg_settings").to_string()
} else {
query.to_string()
}
}

/// Combined schema information for a table
#[derive(Clone)]
struct TableSchemaInfo {
Expand Down Expand Up @@ -261,6 +277,47 @@ impl QueryExecutor {
));
}
}
// Preprocess query: rewrite pg_show_all_settings() → pg_settings
let query = preprocess_query(query);
let query: &str = query.as_str();

// Handle set_config() function calls
if let Some(caps) = SET_CONFIG_PATTERN.captures(query) {
let param_name = caps[1].to_string();
let param_value = caps[2].to_string();
// is_local (caps[3]) is ignored — pgsqlite doesn't support transaction-scoped settings

debug!("Handling set_config('{}', '{}', ...)", param_name, param_value);

// Set the parameter in the session
let mut params = session.parameters.write().await;
params.insert(param_name.to_uppercase(), param_value.clone());
drop(params);

// Send synthetic response: RowDescription + DataRow + CommandComplete
let field = FieldDescription {
name: "set_config".to_string(),
table_oid: 0,
column_id: 1,
type_oid: PgType::Text.to_oid(),
type_size: -1,
type_modifier: -1,
format: 0,
};
framed.send(BackendMessage::RowDescription(vec![field])).await
.map_err(PgSqliteError::Io)?;

let row = vec![Some(param_value.as_bytes().to_vec())];
framed.send(BackendMessage::DataRow(row)).await
.map_err(PgSqliteError::Io)?;

framed.send(BackendMessage::CommandComplete {
tag: "SELECT 1".to_string()
}).await.map_err(PgSqliteError::Io)?;

return Ok(());
}

// Ultra-fast path: Skip all translation if query is simple enough
let is_ultra_simple = crate::query::simple_query_detector::is_ultra_simple_query(query);
// Checking if query is ultra-simple
Expand Down Expand Up @@ -2784,4 +2841,75 @@ mod tests {
let result_str = String::from_utf8_lossy(result_data);
assert_eq!(result_str, r#"{"a","b","c"}"#);
}

#[test]
fn test_pg_show_all_settings_rewrite() {
let query = "SELECT set_config('bytea_output','hex',false) FROM pg_show_all_settings() WHERE name = 'bytea_output'";
let rewritten = preprocess_query(query);
assert!(rewritten.contains("pg_settings"));
assert!(!rewritten.contains("pg_show_all_settings()"));
}

#[test]
fn test_pg_show_all_settings_case_insensitive() {
let query = "SELECT * FROM PG_SHOW_ALL_SETTINGS() WHERE name = 'timezone'";
let rewritten = preprocess_query(query);
assert!(rewritten.contains("pg_settings"));
}

#[test]
fn test_no_rewrite_when_not_present() {
let query = "SELECT * FROM pg_settings WHERE name = 'timezone'";
let rewritten = preprocess_query(query);
assert_eq!(rewritten, query);
}

#[test]
fn test_set_config_detection() {
let query = "SELECT set_config('bytea_output','hex',false) FROM pg_settings WHERE name = 'bytea_output'";
assert!(SET_CONFIG_PATTERN.is_match(query));
}

#[test]
fn test_set_config_captures() {
let query = "SELECT set_config('bytea_output','hex',false)";
let caps = SET_CONFIG_PATTERN.captures(query).unwrap();
assert_eq!(&caps[1], "bytea_output");
assert_eq!(&caps[2], "hex");
assert_eq!(&caps[3], "false");
}

#[test]
fn test_set_config_empty_value() {
let query = "SELECT set_config('application_name','',false)";
let caps = SET_CONFIG_PATTERN.captures(query).unwrap();
assert_eq!(&caps[1], "application_name");
assert_eq!(&caps[2], "");
assert_eq!(&caps[3], "false");
}

#[test]
fn test_set_config_with_spaces() {
let query = "SELECT set_config( 'timezone' , 'UTC' , true )";
let caps = SET_CONFIG_PATTERN.captures(query).unwrap();
assert_eq!(&caps[1], "timezone");
assert_eq!(&caps[2], "UTC");
assert_eq!(&caps[3], "true");
}

#[test]
fn test_pgadmin4_full_query_preprocessing() {
let query = "SELECT set_config('bytea_output','hex',false) FROM pg_show_all_settings() WHERE name = 'bytea_output'";

let rewritten = preprocess_query(query);
assert_eq!(
rewritten,
"SELECT set_config('bytea_output','hex',false) FROM pg_settings WHERE name = 'bytea_output'"
);

let caps = SET_CONFIG_PATTERN.captures(&rewritten).unwrap();
assert_eq!(&caps[1], "bytea_output");
assert_eq!(&caps[2], "hex");
assert_eq!(&caps[3], "false");
}
}
43 changes: 39 additions & 4 deletions src/query/set_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ static SET_TIMEZONE_PATTERN: Lazy<Regex> = Lazy::new(|| {
});

static SET_PARAMETER_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^\s*SET\s+(\w+)\s+(?:TO|=)\s+(.+)$").unwrap()
Regex::new(r"(?i)^\s*SET\s+(\w+)(?:\s*=\s*|\s+TO\s+)(.+)$").unwrap()
});

static SHOW_PARAMETER_PATTERN: Lazy<Regex> = Lazy::new(|| {
Expand Down Expand Up @@ -107,8 +107,8 @@ impl SetHandler {
"TRANSACTION ISOLATION LEVEL" => "read committed".to_string(),
"DEFAULT_TRANSACTION_ISOLATION" => "read committed".to_string(),
"TRANSACTION_ISOLATION" => "read committed".to_string(),
"SERVER_VERSION" => "15.0".to_string(),
"SERVER_VERSION_NUM" => "150000".to_string(),
"SERVER_VERSION" => "16.0".to_string(),
"SERVER_VERSION_NUM" => "160000".to_string(),
"IS_SUPERUSER" => "on".to_string(),
"SESSION_AUTHORIZATION" => "postgres".to_string(),
"STANDARD_CONFORMING_STRINGS" => "on".to_string(),
Expand Down Expand Up @@ -221,8 +221,43 @@ mod tests {
fn test_show_parameter_pattern() {
let query = "SHOW TimeZone";
assert!(SHOW_PARAMETER_PATTERN.is_match(query));

let query = "show search_path";
assert!(SHOW_PARAMETER_PATTERN.is_match(query));
}

#[test]
fn test_set_parameter_pattern_equals_no_spaces() {
// Issue #71: pgAdmin4 sends SET DateStyle=ISO
assert!(SET_PARAMETER_PATTERN.is_match("SET DateStyle=ISO"));
assert!(SET_PARAMETER_PATTERN.is_match("SET client_min_messages=notice"));
assert!(SET_PARAMETER_PATTERN.is_match("SET client_encoding='utf-8'"));
}

#[test]
fn test_set_parameter_pattern_equals_with_spaces() {
assert!(SET_PARAMETER_PATTERN.is_match("SET DateStyle = ISO"));
assert!(SET_PARAMETER_PATTERN.is_match("SET client_encoding = 'UTF8'"));
}

#[test]
fn test_set_parameter_pattern_to_keyword() {
assert!(SET_PARAMETER_PATTERN.is_match("SET search_path TO public"));
assert!(SET_PARAMETER_PATTERN.is_match("SET client_encoding TO 'UTF8'"));
}

#[test]
fn test_set_parameter_pattern_captures() {
let caps = SET_PARAMETER_PATTERN.captures("SET DateStyle=ISO").unwrap();
assert_eq!(&caps[1], "DateStyle");
assert_eq!(&caps[2], "ISO");

let caps = SET_PARAMETER_PATTERN.captures("SET client_encoding = 'UTF8'").unwrap();
assert_eq!(&caps[1], "client_encoding");
assert_eq!(&caps[2], "'UTF8'");

let caps = SET_PARAMETER_PATTERN.captures("SET search_path TO public").unwrap();
assert_eq!(&caps[1], "search_path");
assert_eq!(&caps[2], "public");
}
}
2 changes: 1 addition & 1 deletion src/session/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub struct Portal {
impl SessionState {
pub fn new(database: String, user: String) -> Self {
let mut parameters = HashMap::new();
parameters.insert("server_version".to_string(), "14.0 (SQLite wrapper)".to_string());
parameters.insert("server_version".to_string(), "16.0".to_string());
parameters.insert("server_encoding".to_string(), "UTF8".to_string());
parameters.insert("client_encoding".to_string(), "UTF8".to_string());
parameters.insert("DateStyle".to_string(), "ISO, MDY".to_string());
Expand Down
Loading