Skip to main content

sqlmodel_core/
identifiers.rs

1//! SQL identifier quoting and sanitization utilities.
2//!
3//! This module provides functions for safely quoting SQL identifiers
4//! (table names, column names, etc.) to prevent SQL injection and
5//! handle special characters.
6
7/// Quote a SQL identifier using ANSI double-quoting.
8///
9/// Embedded double-quotes are escaped by doubling them (`"` → `""`).
10/// This function is safe against SQL injection for any input string.
11///
12/// # Examples
13///
14/// ```
15/// use sqlmodel_core::quote_ident;
16///
17/// assert_eq!(quote_ident("users"), "\"users\"");
18/// assert_eq!(quote_ident("user\"name"), "\"user\"\"name\"");
19/// assert_eq!(quote_ident("select"), "\"select\""); // SQL keyword
20/// ```
21#[inline]
22pub fn quote_ident(name: &str) -> String {
23    format!("\"{}\"", name.replace('"', "\"\""))
24}
25
26/// Quote a SQL identifier using MySQL backtick quoting.
27///
28/// Embedded backticks are escaped by doubling them (`` ` `` → ``` `` ```).
29/// This function is safe against SQL injection for any input string.
30///
31/// # Examples
32///
33/// ```
34/// use sqlmodel_core::quote_ident_mysql;
35///
36/// assert_eq!(quote_ident_mysql("users"), "`users`");
37/// assert_eq!(quote_ident_mysql("user`name"), "`user``name`");
38/// ```
39#[inline]
40pub fn quote_ident_mysql(name: &str) -> String {
41    format!("`{}`", name.replace('`', "``"))
42}
43
44/// Sanitize a SQL identifier by removing non-alphanumeric/underscore characters.
45///
46/// Use this when quoting is not possible (e.g., PRAGMA commands, SHOW commands).
47/// This is a more restrictive approach that only allows safe characters.
48///
49/// **Note:** This function strips characters rather than erroring. If the input
50/// contains only invalid characters, the result will be an empty string.
51///
52/// # Examples
53///
54/// ```
55/// use sqlmodel_core::sanitize_identifier;
56///
57/// assert_eq!(sanitize_identifier("users"), "users");
58/// assert_eq!(sanitize_identifier("user_name"), "user_name");
59/// assert_eq!(sanitize_identifier("user\"name"), "username");
60/// assert_eq!(sanitize_identifier("user;DROP TABLE--"), "userDROPTABLE");
61/// ```
62#[inline]
63pub fn sanitize_identifier(name: &str) -> String {
64    name.chars()
65        .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
66        .collect()
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    // ==================== ANSI Double-Quote Tests ====================
74
75    #[test]
76    fn test_quote_ident_simple() {
77        assert_eq!(quote_ident("users"), "\"users\"");
78    }
79
80    #[test]
81    fn test_quote_ident_empty() {
82        // Empty string is valid SQL (though unusual)
83        assert_eq!(quote_ident(""), "\"\"");
84    }
85
86    #[test]
87    fn test_quote_ident_embedded_double_quote() {
88        assert_eq!(quote_ident("user\"name"), "\"user\"\"name\"");
89    }
90
91    #[test]
92    fn test_quote_ident_multiple_quotes() {
93        assert_eq!(quote_ident("a\"b\"c"), "\"a\"\"b\"\"c\"");
94    }
95
96    #[test]
97    fn test_quote_ident_sql_keyword() {
98        // Quoting makes SQL keywords safe to use as identifiers
99        assert_eq!(quote_ident("select"), "\"select\"");
100        assert_eq!(quote_ident("from"), "\"from\"");
101        assert_eq!(quote_ident("where"), "\"where\"");
102    }
103
104    #[test]
105    fn test_quote_ident_spaces() {
106        assert_eq!(quote_ident("first name"), "\"first name\"");
107    }
108
109    #[test]
110    fn test_quote_ident_unicode() {
111        assert_eq!(quote_ident("用户"), "\"用户\"");
112        assert_eq!(quote_ident("naïve"), "\"naïve\"");
113    }
114
115    #[test]
116    fn test_quote_ident_semicolon() {
117        // Semicolons inside quoted identifiers are safe
118        assert_eq!(quote_ident("a;b"), "\"a;b\"");
119    }
120
121    #[test]
122    fn test_quote_ident_newline() {
123        // Newlines inside quoted identifiers are safe
124        assert_eq!(quote_ident("a\nb"), "\"a\nb\"");
125    }
126
127    #[test]
128    fn test_quote_ident_null_byte() {
129        // Null bytes are preserved (database will handle or reject)
130        assert_eq!(quote_ident("a\0b"), "\"a\0b\"");
131    }
132
133    #[test]
134    fn test_quote_ident_backslash() {
135        // Backslash is literal in ANSI SQL
136        assert_eq!(quote_ident("a\\b"), "\"a\\b\"");
137    }
138
139    #[test]
140    fn test_quote_ident_only_quotes() {
141        // Edge case: identifier consisting only of quotes
142        assert_eq!(quote_ident("\"\""), "\"\"\"\"\"\"");
143    }
144
145    #[test]
146    fn test_quote_ident_sql_injection_attempt() {
147        // SQL injection attempt is safely quoted
148        let malicious = "users\"; DROP TABLE secrets; --";
149        let quoted = quote_ident(malicious);
150        assert_eq!(quoted, "\"users\"\"; DROP TABLE secrets; --\"");
151        // The result is a valid identifier, not executable SQL
152    }
153
154    // ==================== MySQL Backtick Tests ====================
155
156    #[test]
157    fn test_quote_ident_mysql_simple() {
158        assert_eq!(quote_ident_mysql("users"), "`users`");
159    }
160
161    #[test]
162    fn test_quote_ident_mysql_embedded_backtick() {
163        assert_eq!(quote_ident_mysql("user`name"), "`user``name`");
164    }
165
166    #[test]
167    fn test_quote_ident_mysql_keyword() {
168        assert_eq!(quote_ident_mysql("select"), "`select`");
169    }
170
171    #[test]
172    fn test_quote_ident_mysql_multiple_backticks() {
173        assert_eq!(quote_ident_mysql("a`b`c"), "`a``b``c`");
174    }
175
176    #[test]
177    fn test_quote_ident_mysql_empty() {
178        assert_eq!(quote_ident_mysql(""), "``");
179    }
180
181    // ==================== Sanitize Identifier Tests ====================
182
183    #[test]
184    fn test_sanitize_simple() {
185        assert_eq!(sanitize_identifier("users"), "users");
186    }
187
188    #[test]
189    fn test_sanitize_strips_quotes() {
190        assert_eq!(sanitize_identifier("user\"name"), "username");
191    }
192
193    #[test]
194    fn test_sanitize_strips_semicolons() {
195        assert_eq!(sanitize_identifier("a;b"), "ab");
196    }
197
198    #[test]
199    fn test_sanitize_preserves_underscore() {
200        assert_eq!(sanitize_identifier("user_name"), "user_name");
201    }
202
203    #[test]
204    fn test_sanitize_strips_spaces() {
205        assert_eq!(sanitize_identifier("user name"), "username");
206    }
207
208    #[test]
209    fn test_sanitize_empty_input() {
210        assert_eq!(sanitize_identifier(""), "");
211    }
212
213    #[test]
214    fn test_sanitize_only_invalid_chars() {
215        assert_eq!(sanitize_identifier("!@#$%"), "");
216    }
217
218    #[test]
219    fn test_sanitize_sql_injection_attempt() {
220        assert_eq!(
221            sanitize_identifier("users; DROP TABLE secrets; --"),
222            "usersDROPTABLEsecrets"
223        );
224    }
225
226    #[test]
227    fn test_sanitize_unicode_stripped() {
228        // Unicode is stripped (only ASCII alphanumeric + underscore allowed)
229        assert_eq!(sanitize_identifier("用户"), "");
230        assert_eq!(sanitize_identifier("naïve"), "nave");
231    }
232
233    #[test]
234    fn test_sanitize_numbers_preserved() {
235        assert_eq!(sanitize_identifier("table123"), "table123");
236        assert_eq!(sanitize_identifier("123table"), "123table");
237    }
238}