Skip to main content

database_mcp_sql/
identifier.rs

1//! Shared identifier validation for all database backends.
2
3use database_mcp_server::AppError;
4
5/// Wraps `name` in `quote_char` for safe use in SQL statements.
6///
7/// Escapes internal occurrences of `quote_char` by doubling them.
8#[must_use]
9pub fn quote_identifier(name: &str, quote_char: char) -> String {
10    let doubled: String = std::iter::repeat_n(quote_char, 2).collect();
11    let escaped = name.replace(quote_char, &doubled);
12    format!("{quote_char}{escaped}{quote_char}")
13}
14
15/// Validates that `name` is a non-empty identifier without control characters.
16///
17/// # Errors
18///
19/// Returns [`AppError::InvalidIdentifier`] if the name is empty,
20/// whitespace-only, or contains control characters.
21pub fn validate_identifier(name: &str) -> Result<(), AppError> {
22    if name.is_empty() || name.chars().all(char::is_whitespace) {
23        return Err(AppError::InvalidIdentifier(name.to_string()));
24    }
25    if name.chars().any(char::is_control) {
26        return Err(AppError::InvalidIdentifier(name.to_string()));
27    }
28    Ok(())
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn accepts_standard_names() {
37        assert!(validate_identifier("users").is_ok());
38        assert!(validate_identifier("my_table").is_ok());
39        assert!(validate_identifier("DB_123").is_ok());
40    }
41
42    #[test]
43    fn accepts_hyphenated_names() {
44        assert!(validate_identifier("eu-docker").is_ok());
45        assert!(validate_identifier("access-logs").is_ok());
46    }
47
48    #[test]
49    fn accepts_special_chars() {
50        assert!(validate_identifier("my.db").is_ok());
51        assert!(validate_identifier("123db").is_ok());
52        assert!(validate_identifier("café").is_ok());
53        assert!(validate_identifier("a b").is_ok());
54    }
55
56    #[test]
57    fn rejects_empty() {
58        assert!(validate_identifier("").is_err());
59    }
60
61    #[test]
62    fn rejects_whitespace_only() {
63        assert!(validate_identifier("   ").is_err());
64        assert!(validate_identifier("\t").is_err());
65    }
66
67    #[test]
68    fn rejects_control_chars() {
69        assert!(validate_identifier("test\x00db").is_err());
70        assert!(validate_identifier("test\ndb").is_err());
71        assert!(validate_identifier("test\x1Fdb").is_err());
72    }
73
74    #[test]
75    fn quote_with_double_quotes() {
76        assert_eq!(quote_identifier("users", '"'), "\"users\"");
77        assert_eq!(quote_identifier("eu-docker", '"'), "\"eu-docker\"");
78        assert_eq!(quote_identifier("test\"db", '"'), "\"test\"\"db\"");
79    }
80
81    #[test]
82    fn quote_with_backticks() {
83        assert_eq!(quote_identifier("users", '`'), "`users`");
84        assert_eq!(quote_identifier("test`db", '`'), "`test``db`");
85    }
86}