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
255impl std::fmt::Display for QualifiedName {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 if let Some(schema) = &self.schema {
258 write!(f, "{}.{}", schema, self.name)
259 } else {
260 write!(f, "{}", self.name)
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_ident_new_normalizes() {
271 let id = Ident::new("MyTable");
272 assert_eq!(id.normalized, "mytable");
273 assert_eq!(id.raw, "MyTable");
274 }
275
276 #[test]
277 fn test_ident_quoted_preserves() {
278 let id = Ident::quoted("MyTable");
279 assert_eq!(id.normalized, "MyTable");
280 }
281
282 #[test]
283 fn test_needs_quotes_reserved() {
284 assert!(Ident::new("select").needs_quotes());
285 assert!(Ident::new("table").needs_quotes());
286 }
287
288 #[test]
289 fn test_needs_quotes_special_chars() {
290 assert!(Ident::quoted("My-Column").needs_quotes());
291 assert!(Ident::quoted("Has Space").needs_quotes());
292 }
293
294 #[test]
295 fn test_no_quotes_simple() {
296 assert!(!Ident::new("users").needs_quotes());
297 assert!(!Ident::new("user_id").needs_quotes());
298 }
299
300 #[test]
301 fn test_to_sql() {
302 assert_eq!(Ident::new("users").to_sql(), "users");
303 assert_eq!(Ident::new("select").to_sql(), "\"select\"");
304 assert_eq!(Ident::quoted("MyTable").to_sql(), "\"MyTable\"");
305 }
306
307 #[test]
308 fn test_starts_with_digit() {
309 assert!(Ident::new("1col").needs_quotes());
310 }
311}