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