1const SQLITE_RESERVED: &[&str] = &[
5 "abort",
6 "action",
7 "add",
8 "after",
9 "all",
10 "alter",
11 "always",
12 "analyze",
13 "and",
14 "as",
15 "asc",
16 "attach",
17 "autoincrement",
18 "before",
19 "begin",
20 "between",
21 "by",
22 "cascade",
23 "case",
24 "cast",
25 "check",
26 "collate",
27 "column",
28 "commit",
29 "conflict",
30 "constraint",
31 "create",
32 "cross",
33 "current",
34 "current_date",
35 "current_time",
36 "current_timestamp",
37 "database",
38 "default",
39 "deferrable",
40 "deferred",
41 "delete",
42 "desc",
43 "detach",
44 "distinct",
45 "do",
46 "drop",
47 "each",
48 "else",
49 "end",
50 "escape",
51 "except",
52 "exclude",
53 "exclusive",
54 "exists",
55 "explain",
56 "fail",
57 "filter",
58 "first",
59 "following",
60 "for",
61 "foreign",
62 "from",
63 "full",
64 "generated",
65 "glob",
66 "group",
67 "groups",
68 "having",
69 "if",
70 "ignore",
71 "immediate",
72 "in",
73 "index",
74 "indexed",
75 "initially",
76 "inner",
77 "insert",
78 "instead",
79 "intersect",
80 "into",
81 "is",
82 "isnull",
83 "join",
84 "key",
85 "last",
86 "left",
87 "like",
88 "limit",
89 "match",
90 "materialized",
91 "natural",
92 "no",
93 "not",
94 "nothing",
95 "notnull",
96 "null",
97 "nulls",
98 "of",
99 "offset",
100 "on",
101 "or",
102 "order",
103 "others",
104 "outer",
105 "over",
106 "partition",
107 "plan",
108 "pragma",
109 "preceding",
110 "primary",
111 "query",
112 "raise",
113 "range",
114 "recursive",
115 "references",
116 "regexp",
117 "reindex",
118 "release",
119 "rename",
120 "replace",
121 "restrict",
122 "returning",
123 "right",
124 "rollback",
125 "row",
126 "rows",
127 "savepoint",
128 "select",
129 "set",
130 "table",
131 "temp",
132 "temporary",
133 "then",
134 "ties",
135 "to",
136 "transaction",
137 "trigger",
138 "unbounded",
139 "union",
140 "unique",
141 "update",
142 "using",
143 "vacuum",
144 "values",
145 "view",
146 "virtual",
147 "when",
148 "where",
149 "window",
150 "with",
151 "without",
152];
153
154#[derive(Debug, Clone, PartialEq, Eq, Hash)]
156pub struct Ident {
157 pub raw: String,
159 pub normalized: String,
161}
162
163impl Ident {
164 pub fn new(name: &str) -> Self {
166 Self {
167 raw: name.to_string(),
168 normalized: name.to_lowercase(),
169 }
170 }
171
172 pub fn quoted(name: &str) -> Self {
174 Self {
175 raw: name.to_string(),
176 normalized: name.to_string(),
177 }
178 }
179
180 pub fn needs_quotes(&self) -> bool {
182 let n = &self.normalized;
183
184 if n.is_empty() {
186 return true;
187 }
188
189 if n.starts_with(|c: char| c.is_ascii_digit()) {
191 return true;
192 }
193
194 if n.chars()
196 .any(|c| c.is_ascii_uppercase() || c == ' ' || c == '-')
197 {
198 return true;
199 }
200
201 if n.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') {
203 return true;
204 }
205
206 if SQLITE_RESERVED.contains(&n.as_str()) {
208 return true;
209 }
210
211 false
212 }
213
214 pub fn to_sql(&self) -> String {
216 if self.needs_quotes() {
217 format!("\"{}\"", self.normalized.replace('"', "\"\""))
218 } else {
219 self.normalized.clone()
220 }
221 }
222}
223
224impl std::fmt::Display for Ident {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 write!(f, "{}", self.normalized)
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232pub struct QualifiedName {
233 pub schema: Option<Ident>,
234 pub name: Ident,
235}
236
237impl QualifiedName {
238 pub fn new(name: Ident) -> Self {
239 Self { schema: None, name }
240 }
241
242 pub fn with_schema(schema: Ident, name: Ident) -> Self {
243 Self {
244 schema: Some(schema),
245 name,
246 }
247 }
248
249 pub fn to_sql(&self) -> String {
251 self.name.to_sql()
252 }
253
254 pub fn name_eq(&self, other: &QualifiedName) -> bool {
259 self.name == other.name
260 }
261}
262
263impl std::fmt::Display for QualifiedName {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 if let Some(schema) = &self.schema {
266 write!(f, "{}.{}", schema, self.name)
267 } else {
268 write!(f, "{}", self.name)
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_ident_new_normalizes() {
279 let id = Ident::new("MyTable");
280 assert_eq!(id.normalized, "mytable");
281 assert_eq!(id.raw, "MyTable");
282 }
283
284 #[test]
285 fn test_ident_quoted_preserves() {
286 let id = Ident::quoted("MyTable");
287 assert_eq!(id.normalized, "MyTable");
288 }
289
290 #[test]
291 fn test_needs_quotes_reserved() {
292 assert!(Ident::new("select").needs_quotes());
293 assert!(Ident::new("table").needs_quotes());
294 }
295
296 #[test]
297 fn test_needs_quotes_special_chars() {
298 assert!(Ident::quoted("My-Column").needs_quotes());
299 assert!(Ident::quoted("Has Space").needs_quotes());
300 }
301
302 #[test]
303 fn test_no_quotes_simple() {
304 assert!(!Ident::new("users").needs_quotes());
305 assert!(!Ident::new("user_id").needs_quotes());
306 }
307
308 #[test]
309 fn test_to_sql() {
310 assert_eq!(Ident::new("users").to_sql(), "users");
311 assert_eq!(Ident::new("select").to_sql(), "\"select\"");
312 assert_eq!(Ident::quoted("MyTable").to_sql(), "\"MyTable\"");
313 }
314
315 #[test]
316 fn test_starts_with_digit() {
317 assert!(Ident::new("1col").needs_quotes());
318 }
319}