diff --git a/src/functions/system_functions.rs b/src/functions/system_functions.rs index 84499cc7..7cb99486 100644 --- a/src/functions/system_functions.rs +++ b/src/functions/system_functions.rs @@ -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"))) }, )?; diff --git a/src/query/executor.rs b/src/query/executor.rs index 84ed0c62..a9c71ab4 100644 --- a/src/query/executor.rs +++ b/src/query/executor.rs @@ -22,6 +22,22 @@ use once_cell::sync::Lazy; use uuid::Uuid; use regex::Regex; +static PG_SHOW_ALL_SETTINGS_PATTERN: Lazy = Lazy::new(|| { + Regex::new(r"(?i)pg_show_all_settings\(\s*\)").unwrap() +}); + +static SET_CONFIG_PATTERN: Lazy = 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 { @@ -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 @@ -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"); + } } \ No newline at end of file diff --git a/src/query/set_handler.rs b/src/query/set_handler.rs index cbf8b55c..597d0c24 100644 --- a/src/query/set_handler.rs +++ b/src/query/set_handler.rs @@ -13,7 +13,7 @@ static SET_TIMEZONE_PATTERN: Lazy = Lazy::new(|| { }); static SET_PARAMETER_PATTERN: Lazy = 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 = Lazy::new(|| { @@ -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(), @@ -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"); + } } \ No newline at end of file diff --git a/src/session/state.rs b/src/session/state.rs index 06dfcae7..05887af9 100644 --- a/src/session/state.rs +++ b/src/session/state.rs @@ -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());