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(self, quoter: &IdentifierQuoter, usage: AttemptedKeywordUsage)
103 -> IteratorQuoter<Self>;
104}
105
106impl<I> QuotableIter for I
107where
108 I: Iterator,
109 I::Item: AsRef<str>,
110{
111 fn quote(
112 self,
113 quoter: &IdentifierQuoter,
114 usage: AttemptedKeywordUsage,
115 ) -> IteratorQuoter<Self> {
116 IteratorQuoter {
117 quoter,
118 usage,
119 iter: self,
120 }
121 }
122}
123
124pub(crate) struct IteratorQuoter<'q, I> {
126 quoter: &'q IdentifierQuoter,
127 usage: AttemptedKeywordUsage,
128 iter: I,
129}
130
131impl<I> Iterator for IteratorQuoter<'_, I>
132where
133 I: Iterator,
134 I::Item: AsRef<str>,
135{
136 type Item = String;
137
138 fn next(&mut self) -> Option<Self::Item> {
139 self.iter.next().map(|i| self.quoter.quote(i, self.usage))
140 }
141}
142
143pub(crate) fn quote_value_string(s: &str) -> String {
145 format!("'{}'", s.replace('\'', "''"))
146}
147
148#[cfg(test)]
149mod tests {
150 use crate::quoting::{AllowedKeywordUsage, AttemptedKeywordUsage};
151 use std::collections::HashMap;
152
153 #[test]
154 fn quoting() {
155 let quoter = super::IdentifierQuoter::new(HashMap::from([(
156 "table".to_string(),
157 AllowedKeywordUsage {
158 type_or_function_name: false,
159 column_name: false,
160 },
161 )]));
162
163 macro_rules! test_quote {
164 ($identifier:literal, $expected:literal) => {
165 let quoted = quoter.quote($identifier, AttemptedKeywordUsage::Other);
166 assert_eq!(quoted, $expected);
167 };
168 }
169
170 test_quote!("table", "\"table\"");
171 test_quote!("table1", "table1");
172 test_quote!("table_1", "table_1");
173 test_quote!("table-1", "\"table-1\"");
174 test_quote!("table 1", "\"table 1\"");
175 test_quote!("1table", "\"1table\"");
176 test_quote!("my_table", "my_table");
177 test_quote!("MyTable", "\"MyTable\"");
178 test_quote!("my\"table", "\"my\"\"table\"");
179 test_quote!("", "\"\"");
180 }
181}