const LIST_QUOTE_PARAMETERS: [&str; 5] = [
"local_preload_libraries",
"search_path",
"session_preload_libraries",
"shared_preload_libraries",
"temp_tablespaces",
];
pub fn is_list_quote_parameter(parameter: &str) -> bool {
LIST_QUOTE_PARAMETERS.contains(¶meter)
}
pub fn split_guc_list(value: &str) -> Option<Vec<String>> {
let mut elements = Vec::new();
let mut chars = value.chars().peekable();
loop {
while matches!(chars.peek(), Some(c) if c.is_whitespace() || *c == ',') {
chars.next();
}
let Some(&first) = chars.peek() else {
return Some(elements);
};
let mut element = String::new();
if first == '"' {
chars.next();
loop {
match chars.next() {
None => return None, Some('"') => {
if chars.peek() == Some(&'"') {
chars.next();
element.push('"');
} else {
break;
}
}
Some(c) => element.push(c),
}
}
} else {
while matches!(chars.peek(), Some(c) if !c.is_whitespace() && *c != ',') {
element.push(chars.next().expect("peeked"));
}
}
elements.push(element);
}
}
fn quote_element_if_needed(element: &str) -> String {
let mut chars = element.chars();
let simple = matches!(chars.next(), Some(c) if c.is_ascii_lowercase() || c == '_')
&& chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
if simple {
element.to_string()
} else {
format!("\"{}\"", element.replace('"', "\"\""))
}
}
pub fn canonicalize_list_guc_value(value: &str) -> String {
match split_guc_list(value) {
Some(elements) => elements
.iter()
.map(|element| quote_element_if_needed(element))
.collect::<Vec<_>>()
.join(", "),
None => value.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_list_parameters() {
assert!(is_list_quote_parameter("search_path"));
assert!(is_list_quote_parameter("temp_tablespaces"));
assert!(!is_list_quote_parameter("role"));
assert!(!is_list_quote_parameter("statement_timeout"));
assert!(!is_list_quote_parameter("app.tenant"));
}
#[test]
fn split_handles_bare_quoted_and_escaped_elements() {
assert_eq!(
split_guc_list(r#""$user", public"#),
Some(vec!["$user".to_string(), "public".to_string()])
);
assert_eq!(
split_guc_list("a,b , c"),
Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
);
assert_eq!(
split_guc_list(r#""has, comma", plain"#),
Some(vec!["has, comma".to_string(), "plain".to_string()])
);
assert_eq!(
split_guc_list(r#""say ""hi""""#),
Some(vec![r#"say "hi""#.to_string()])
);
assert_eq!(split_guc_list(""), Some(vec![]));
assert_eq!(split_guc_list(r#""unterminated"#), None);
}
#[test]
fn canonicalization_is_quoting_and_spacing_insensitive() {
for spelling in [
r#""$user", public"#,
r#""$user",public"#,
r#" "$user" , public "#,
r#"$user, public"#, ] {
assert_eq!(
canonicalize_list_guc_value(spelling),
r#""$user", public"#,
"spelling: {spelling}"
);
}
assert_eq!(canonicalize_list_guc_value("app, public"), "app, public");
assert_eq!(
canonicalize_list_guc_value(r#""app, public""#),
r#""app, public""#
);
assert_eq!(
canonicalize_list_guc_value("MySchema, public"),
r#""MySchema", public"#
);
}
}