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}