Skip to main content

fraiseql_db/
identifier.rs

1//! Database identifier quoting utilities.
2//!
3//! This module provides database-specific identifier quoting functions that handle
4//! schema-qualified identifiers (e.g., `schema.table`, `catalog.schema.table`).
5//!
6//! Each function splits on `.` and quotes each component with the appropriate syntax
7//! for the target database.
8
9/// Quote a PostgreSQL identifier.
10///
11/// PostgreSQL uses double quotes for identifiers. Schema-qualified names
12/// (e.g., `schema.table`) are split and quoted per component.
13///
14/// # Examples
15///
16/// ```rust
17/// use fraiseql_db::quote_postgres_identifier;
18/// assert_eq!(quote_postgres_identifier("v_user"), "\"v_user\"");
19/// assert_eq!(quote_postgres_identifier("benchmark.v_user"), "\"benchmark\".\"v_user\"");
20/// assert_eq!(
21///     quote_postgres_identifier("catalog.schema.table"),
22///     "\"catalog\".\"schema\".\"table\""
23/// );
24/// ```
25#[inline]
26#[must_use]
27pub fn quote_postgres_identifier(identifier: &str) -> String {
28    identifier
29        .split('.')
30        .map(|part| format!("\"{}\"", part.replace('"', "\"\"")))
31        .collect::<Vec<_>>()
32        .join(".")
33}
34
35/// Quote a MySQL identifier.
36///
37/// MySQL uses backticks for identifiers. Schema-qualified names
38/// (e.g., `database.table`) are split and quoted per component.
39///
40/// # Examples
41///
42/// ```rust
43/// use fraiseql_db::quote_mysql_identifier;
44/// assert_eq!(quote_mysql_identifier("v_user"), "`v_user`");
45/// assert_eq!(quote_mysql_identifier("mydb.v_user"), "`mydb`.`v_user`");
46/// assert_eq!(
47///     quote_mysql_identifier("catalog.schema.table"),
48///     "`catalog`.`schema`.`table`"
49/// );
50/// ```
51#[inline]
52#[must_use]
53pub fn quote_mysql_identifier(identifier: &str) -> String {
54    identifier
55        .split('.')
56        .map(|part| format!("`{}`", part.replace('`', "``")))
57        .collect::<Vec<_>>()
58        .join(".")
59}
60
61/// Quote a SQLite identifier.
62///
63/// SQLite uses double quotes for identifiers. Schema-qualified names
64/// (e.g., `schema.table`) are split and quoted per component.
65///
66/// # Examples
67///
68/// ```rust
69/// use fraiseql_db::quote_sqlite_identifier;
70/// assert_eq!(quote_sqlite_identifier("v_user"), "\"v_user\"");
71/// assert_eq!(quote_sqlite_identifier("main.v_user"), "\"main\".\"v_user\"");
72/// assert_eq!(
73///     quote_sqlite_identifier("catalog.schema.table"),
74///     "\"catalog\".\"schema\".\"table\""
75/// );
76/// ```
77#[inline]
78#[must_use]
79pub fn quote_sqlite_identifier(identifier: &str) -> String {
80    identifier
81        .split('.')
82        .map(|part| format!("\"{}\"", part.replace('"', "\"\"")))
83        .collect::<Vec<_>>()
84        .join(".")
85}
86
87/// Quote a SQL Server identifier.
88///
89/// SQL Server uses square brackets for identifiers. Schema-qualified names
90/// (e.g., `schema.table`) are split and quoted per component.
91///
92/// # Examples
93///
94/// ```rust
95/// use fraiseql_db::quote_sqlserver_identifier;
96/// assert_eq!(quote_sqlserver_identifier("v_user"), "[v_user]");
97/// assert_eq!(quote_sqlserver_identifier("dbo.v_user"), "[dbo].[v_user]");
98/// assert_eq!(
99///     quote_sqlserver_identifier("catalog.schema.table"),
100///     "[catalog].[schema].[table]"
101/// );
102/// ```
103#[inline]
104#[must_use]
105pub fn quote_sqlserver_identifier(identifier: &str) -> String {
106    identifier
107        .split('.')
108        .map(|part| format!("[{}]", part.replace(']', "]]")))
109        .collect::<Vec<_>>()
110        .join(".")
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_postgres_simple_identifier() {
119        assert_eq!(quote_postgres_identifier("v_user"), "\"v_user\"");
120    }
121
122    #[test]
123    fn test_postgres_schema_qualified() {
124        assert_eq!(quote_postgres_identifier("benchmark.v_user"), "\"benchmark\".\"v_user\"");
125    }
126
127    #[test]
128    fn test_postgres_three_part_name() {
129        assert_eq!(
130            quote_postgres_identifier("catalog.schema.table"),
131            "\"catalog\".\"schema\".\"table\""
132        );
133    }
134
135    #[test]
136    fn test_mysql_simple_identifier() {
137        assert_eq!(quote_mysql_identifier("v_user"), "`v_user`");
138    }
139
140    #[test]
141    fn test_mysql_schema_qualified() {
142        assert_eq!(quote_mysql_identifier("mydb.v_user"), "`mydb`.`v_user`");
143    }
144
145    #[test]
146    fn test_mysql_three_part_name() {
147        assert_eq!(quote_mysql_identifier("catalog.schema.table"), "`catalog`.`schema`.`table`");
148    }
149
150    #[test]
151    fn test_sqlite_simple_identifier() {
152        assert_eq!(quote_sqlite_identifier("v_user"), "\"v_user\"");
153    }
154
155    #[test]
156    fn test_sqlite_schema_qualified() {
157        assert_eq!(quote_sqlite_identifier("main.v_user"), "\"main\".\"v_user\"");
158    }
159
160    #[test]
161    fn test_sqlite_three_part_name() {
162        assert_eq!(
163            quote_sqlite_identifier("catalog.schema.table"),
164            "\"catalog\".\"schema\".\"table\""
165        );
166    }
167
168    #[test]
169    fn test_sqlserver_simple_identifier() {
170        assert_eq!(quote_sqlserver_identifier("v_user"), "[v_user]");
171    }
172
173    #[test]
174    fn test_sqlserver_schema_qualified() {
175        assert_eq!(quote_sqlserver_identifier("dbo.v_user"), "[dbo].[v_user]");
176    }
177
178    #[test]
179    fn test_sqlserver_three_part_name() {
180        assert_eq!(
181            quote_sqlserver_identifier("catalog.schema.table"),
182            "[catalog].[schema].[table]"
183        );
184    }
185
186    // Delimiter-escape tests — the delimiter character must be doubled inside the quoted name.
187
188    #[test]
189    fn test_postgres_escapes_embedded_double_quote() {
190        // A double-quote inside a PostgreSQL quoted identifier must be doubled ("").
191        assert_eq!(quote_postgres_identifier("evil\"inject"), "\"evil\"\"inject\"");
192    }
193
194    #[test]
195    fn test_sqlite_escapes_embedded_double_quote() {
196        assert_eq!(quote_sqlite_identifier("evil\"inject"), "\"evil\"\"inject\"");
197    }
198
199    #[test]
200    fn test_mysql_escapes_embedded_backtick() {
201        // A backtick inside a MySQL quoted identifier must be doubled (``).
202        assert_eq!(quote_mysql_identifier("evil`inject"), "`evil``inject`");
203    }
204
205    #[test]
206    fn test_sqlserver_escapes_embedded_bracket() {
207        // A closing bracket ']' inside a SQL Server quoted identifier must be doubled ']]'.
208        // Single identifier component containing ']':
209        assert_eq!(quote_sqlserver_identifier("evil]inject"), "[evil]]inject]");
210        // Schema-qualified name where each part escapes its own ']':
211        assert_eq!(quote_sqlserver_identifier("dbo.evil]inject"), "[dbo].[evil]]inject]");
212    }
213}