1use std::collections::HashMap;
2
3#[derive(Debug)]
5pub struct IdentifierQuoter {
6 keywords: HashMap<String, AllowedKeywordUsage>,
8}
9
10#[derive(Debug, Copy, Clone)]
12pub struct AllowedKeywordUsage {
13 pub column_name: bool,
14 pub type_or_function_name: bool,
15}
16
17#[derive(Debug, Copy, Clone, Eq, PartialEq)]
19pub enum AttemptedKeywordUsage {
20 ColumnName,
21 TypeOrFunctionName,
22 Other,
23}
24
25impl IdentifierQuoter {
26 pub fn new(keywords: HashMap<String, AllowedKeywordUsage>) -> Self {
28 Self { keywords }
29 }
30
31 pub fn empty() -> Self {
35 Self {
36 keywords: HashMap::new(),
37 }
38 }
39
40 pub fn quote(&self, identifier: impl AsRef<str>, usage: AttemptedKeywordUsage) -> String {
44 let identifier = identifier.as_ref();
45
46 if identifier.is_empty() {
47 return "\"\"".to_string();
48 }
49
50 let mut chars = identifier.chars();
51
52 let safe = if let Some(allowed) = self.keywords.get(identifier) {
53 match usage {
54 AttemptedKeywordUsage::ColumnName => allowed.column_name,
55 AttemptedKeywordUsage::TypeOrFunctionName => allowed.type_or_function_name,
56 AttemptedKeywordUsage::Other => false,
57 }
58 } else {
59 matches!(chars.next(), Some('a'..='z' | '_'))
60 && chars.all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_'))
61 };
62
63 if safe {
64 identifier.to_string()
65 } else {
66 let escaped = identifier.replace('"', r#""""#);
67
68 format!("\"{escaped}\"")
69 }
70 }
71
72 pub fn quote_iter<'a, 's, S: AsRef<str>, I: IntoIterator<Item = S>>(
74 &'a self,
75 identifiers: I,
76 usage: AttemptedKeywordUsage,
77 ) -> impl Iterator<Item = String> + 'a
78 where
79 <I as IntoIterator>::IntoIter: 'a,
80 {
81 identifiers.into_iter().map(move |i| self.quote(i, usage))
82 }
83}
84
85pub(crate) trait Quotable {
87 fn quote(&self, quoter: &IdentifierQuoter, usage: AttemptedKeywordUsage) -> String;
89}
90
91impl<S> Quotable for S
92where
93 S: AsRef<str>,
94{
95 fn quote(&self, quoter: &IdentifierQuoter, usage: AttemptedKeywordUsage) -> String {
96 quoter.quote(self, usage)
97 }
98}
99
100pub(crate) trait QuotableIter: Sized {
102 fn quote(
103 self,
104 quoter: &IdentifierQuoter,
105 usage: AttemptedKeywordUsage,
106 ) -> IteratorQuoter<'_, Self>;
107}
108
109impl<I> QuotableIter for I
110where
111 I: Iterator,
112 I::Item: AsRef<str>,
113{
114 fn quote(
115 self,
116 quoter: &IdentifierQuoter,
117 usage: AttemptedKeywordUsage,
118 ) -> IteratorQuoter<'_, Self> {
119 IteratorQuoter {
120 quoter,
121 usage,
122 iter: self,
123 }
124 }
125}
126
127pub(crate) struct IteratorQuoter<'q, I> {
129 quoter: &'q IdentifierQuoter,
130 usage: AttemptedKeywordUsage,
131 iter: I,
132}
133
134impl<I> Iterator for IteratorQuoter<'_, I>
135where
136 I: Iterator,
137 I::Item: AsRef<str>,
138{
139 type Item = String;
140
141 fn next(&mut self) -> Option<Self::Item> {
142 self.iter.next().map(|i| self.quoter.quote(i, self.usage))
143 }
144}
145
146pub(crate) fn quote_value_string(s: &str) -> String {
148 format!("'{}'", s.replace('\'', "''"))
149}
150
151#[cfg(test)]
152mod tests {
153 use crate::quoting::{AllowedKeywordUsage, AttemptedKeywordUsage};
154 use std::collections::HashMap;
155
156 #[test]
157 fn quoting() {
158 let quoter = super::IdentifierQuoter::new(HashMap::from([(
159 "table".to_string(),
160 AllowedKeywordUsage {
161 type_or_function_name: false,
162 column_name: false,
163 },
164 )]));
165
166 macro_rules! test_quote {
167 ($identifier:literal, $expected:literal) => {
168 let quoted = quoter.quote($identifier, AttemptedKeywordUsage::Other);
169 assert_eq!(quoted, $expected);
170 };
171 }
172
173 test_quote!("table", "\"table\"");
174 test_quote!("table1", "table1");
175 test_quote!("table_1", "table_1");
176 test_quote!("table-1", "\"table-1\"");
177 test_quote!("table 1", "\"table 1\"");
178 test_quote!("1table", "\"1table\"");
179 test_quote!("my_table", "my_table");
180 test_quote!("MyTable", "\"MyTable\"");
181 test_quote!("my\"table", "\"my\"\"table\"");
182 test_quote!("", "\"\"");
183 }
184
185 #[test]
186 fn quotes_keywords_based_on_usage() {
187 let quoter = super::IdentifierQuoter::new(HashMap::from([
190 (
191 "between".to_string(),
192 AllowedKeywordUsage {
193 column_name: true,
194 type_or_function_name: false,
195 },
196 ),
197 (
198 "left".to_string(),
199 AllowedKeywordUsage {
200 column_name: false,
201 type_or_function_name: true,
202 },
203 ),
204 ]));
205
206 assert_eq!(
207 quoter.quote("between", AttemptedKeywordUsage::ColumnName),
208 "between"
209 );
210 assert_eq!(
211 quoter.quote("between", AttemptedKeywordUsage::TypeOrFunctionName),
212 "\"between\""
213 );
214
215 assert_eq!(
216 quoter.quote("left", AttemptedKeywordUsage::ColumnName),
217 "\"left\""
218 );
219 assert_eq!(
220 quoter.quote("left", AttemptedKeywordUsage::TypeOrFunctionName),
221 "left"
222 );
223
224 assert_eq!(
226 quoter.quote("left", AttemptedKeywordUsage::Other),
227 "\"left\""
228 );
229 }
230}