1pub fn escape_ident(name: &str) -> String {
21 format!("\"{}\"", name.replace('"', "\"\""))
22}
23
24pub fn quote_literal(s: &str) -> String {
45 format!("'{}'", s.replace('\'', "''"))
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, Hash)]
50pub struct QualifiedIdentifier {
51 pub schema: String,
54 pub name: String,
56}
57
58impl QualifiedIdentifier {
59 pub fn new(schema: impl Into<String>, name: impl Into<String>) -> Self {
61 Self {
62 schema: schema.into(),
63 name: name.into(),
64 }
65 }
66
67 pub fn unqualified(name: impl Into<String>) -> Self {
72 Self {
73 schema: String::new(),
74 name: name.into(),
75 }
76 }
77}
78
79pub fn from_qi(qi: &QualifiedIdentifier) -> String {
93 if qi.schema.is_empty() {
94 escape_ident(&qi.name)
95 } else {
96 format!("{}.{}", escape_ident(&qi.schema), escape_ident(&qi.name))
97 }
98}
99
100pub fn is_valid_identifier(s: &str) -> bool {
105 if s.is_empty() {
106 return false;
107 }
108
109 let mut chars = s.chars();
110 let first = chars.next().unwrap();
111
112 if !first.is_ascii_alphabetic() && first != '_' {
113 return false;
114 }
115
116 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
117}
118
119pub fn is_keyword(s: &str) -> bool {
121 const KEYWORDS: &[&str] = &[
122 "all",
123 "and",
124 "any",
125 "array",
126 "as",
127 "asc",
128 "between",
129 "by",
130 "case",
131 "cast",
132 "check",
133 "column",
134 "constraint",
135 "create",
136 "cross",
137 "current",
138 "default",
139 "delete",
140 "desc",
141 "distinct",
142 "drop",
143 "else",
144 "end",
145 "exists",
146 "false",
147 "for",
148 "foreign",
149 "from",
150 "full",
151 "group",
152 "having",
153 "in",
154 "index",
155 "inner",
156 "insert",
157 "into",
158 "is",
159 "join",
160 "key",
161 "left",
162 "like",
163 "limit",
164 "not",
165 "null",
166 "offset",
167 "on",
168 "or",
169 "order",
170 "outer",
171 "primary",
172 "references",
173 "right",
174 "select",
175 "set",
176 "table",
177 "then",
178 "to",
179 "true",
180 "union",
181 "unique",
182 "update",
183 "using",
184 "values",
185 "when",
186 "where",
187 "with",
188 ];
189
190 KEYWORDS.contains(&s.to_lowercase().as_str())
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_escape_ident() {
199 assert_eq!(escape_ident("users"), "\"users\"");
200 assert_eq!(escape_ident("user_table"), "\"user_table\"");
201 assert_eq!(escape_ident("user\"name"), "\"user\"\"name\"");
202 assert_eq!(escape_ident("My Table"), "\"My Table\"");
203 assert_eq!(escape_ident(""), "\"\"");
204 }
205
206 #[test]
207 fn test_quote_literal() {
208 assert_eq!(quote_literal("hello"), "'hello'");
209 assert_eq!(quote_literal("it's"), "'it''s'");
210 assert_eq!(quote_literal(""), "''");
211 }
212
213 #[test]
214 fn test_from_qi() {
215 let qi = QualifiedIdentifier::new("public", "users");
216 assert_eq!(from_qi(&qi), "\"public\".\"users\"");
217
218 let qi = QualifiedIdentifier::unqualified("users");
219 assert_eq!(from_qi(&qi), "\"users\"");
220
221 let qi = QualifiedIdentifier::new("my schema", "my\"table");
222 assert_eq!(from_qi(&qi), "\"my schema\".\"my\"\"table\"");
223 }
224
225 #[test]
226 fn test_is_valid_identifier() {
227 assert!(is_valid_identifier("users"));
228 assert!(is_valid_identifier("_private"));
229 assert!(is_valid_identifier("user123"));
230 assert!(is_valid_identifier("user$table"));
231
232 assert!(!is_valid_identifier(""));
233 assert!(!is_valid_identifier("123users"));
234 assert!(!is_valid_identifier("my-table"));
235 assert!(!is_valid_identifier("my table"));
236 }
237
238 #[test]
239 fn test_is_keyword() {
240 assert!(is_keyword("select"));
241 assert!(is_keyword("SELECT"));
242 assert!(is_keyword("from"));
243 assert!(is_keyword("WHERE"));
244
245 assert!(!is_keyword("users"));
246 assert!(!is_keyword("my_column"));
247 }
248}