pub fn escape_identifier(ident: &str, quote: char) -> String {
let trimmed = ident.trim();
if trimmed.is_empty() {
return String::new();
}
let mut out = String::with_capacity(trimmed.len() + 4);
for (i, part) in trimmed.split('.').enumerate() {
if i > 0 {
out.push('.');
}
let part = part.trim();
if part == "*" {
out.push('*');
continue;
}
out.push(quote);
for ch in part.chars() {
if ch == quote {
out.push(quote);
}
out.push(ch);
}
out.push(quote);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const BACKTICK: char = '\u{60}';
const DQUOTE: char = '"';
#[test]
fn plain_identifier_mysql() {
assert_eq!(escape_identifier("name", BACKTICK), "`name`");
}
#[test]
fn qualified_identifier_mysql() {
assert_eq!(escape_identifier("users.name", BACKTICK), "`users`.`name`");
}
#[test]
fn wildcard_passthrough() {
assert_eq!(escape_identifier("*", BACKTICK), "*");
assert_eq!(escape_identifier("users.*", BACKTICK), "`users`.*");
assert_eq!(escape_identifier("t.*", BACKTICK), "`t`.*");
}
#[test]
fn sqlite_uses_double_quotes() {
assert_eq!(escape_identifier("name", DQUOTE), "\"name\"");
}
#[test]
fn injection_attempt_is_neutralized_mysql() {
assert_eq!(
escape_identifier("name` = 1; DROP TABLE users; -- ", BACKTICK),
"`name`` = 1; DROP TABLE users; --`"
);
}
#[test]
fn injection_attempt_is_neutralized_double_quote() {
assert_eq!(
escape_identifier("name\" OR \"1\"=\"1", DQUOTE),
"\"name\"\" OR \"\"1\"\"=\"\"1\""
);
}
#[test]
fn empty_input_yields_empty() {
assert_eq!(escape_identifier(" ", BACKTICK), "");
}
}