1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct Identifier(String);
24
25#[derive(Debug, Error, PartialEq, Eq)]
27pub enum IdentifierError {
28 #[error("identifier is empty")]
30 Empty,
31 #[error("identifier exceeds 63 bytes: got {0}")]
33 TooLong(usize),
34 #[error("unquoted identifier contains invalid characters: {0:?}")]
36 InvalidUnquotedChars(String),
37}
38
39impl Identifier {
40 pub fn from_unquoted(s: &str) -> Result<Self, IdentifierError> {
42 if s.is_empty() {
43 return Err(IdentifierError::Empty);
44 }
45 if s.len() > 63 {
46 return Err(IdentifierError::TooLong(s.len()));
47 }
48 let mut chars = s.chars();
49 let Some(first) = chars.next() else {
51 unreachable!("non-empty string has at least one char")
52 };
53 if !(first.is_ascii_alphabetic() || first == '_') {
54 return Err(IdentifierError::InvalidUnquotedChars(s.to_string()));
55 }
56 for c in chars {
57 if !(c.is_ascii_alphanumeric() || c == '_' || c == '$') {
58 return Err(IdentifierError::InvalidUnquotedChars(s.to_string()));
59 }
60 }
61 Ok(Self(s.to_ascii_lowercase()))
62 }
63
64 pub fn from_quoted(s: &str) -> Result<Self, IdentifierError> {
66 if s.is_empty() {
67 return Err(IdentifierError::Empty);
68 }
69 if s.len() > 63 {
70 return Err(IdentifierError::TooLong(s.len()));
71 }
72 Ok(Self(s.to_string()))
73 }
74
75 pub fn as_str(&self) -> &str {
77 &self.0
78 }
79
80 pub fn render_sql(&self) -> String {
82 if needs_quoting(&self.0) {
83 format!("\"{}\"", self.0.replace('"', "\"\""))
84 } else {
85 self.0.clone()
86 }
87 }
88}
89
90fn needs_quoting(s: &str) -> bool {
91 if s.is_empty() {
92 return true;
93 }
94 if RESERVED_KEYWORDS.binary_search(&s).is_ok() {
95 return true;
96 }
97 let mut chars = s.chars();
98 let Some(first) = chars.next() else {
100 unreachable!("non-empty string has at least one char")
101 };
102 if !(first.is_ascii_lowercase() || first == '_') {
103 return true;
104 }
105 for c in chars {
106 if !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') {
107 return true;
108 }
109 }
110 false
111}
112
113static RESERVED_KEYWORDS: &[&str] = &[
116 "all",
117 "analyse",
118 "analyze",
119 "and",
120 "any",
121 "array",
122 "as",
123 "asc",
124 "asymmetric",
125 "both",
126 "case",
127 "cast",
128 "check",
129 "collate",
130 "column",
131 "constraint",
132 "create",
133 "current_catalog",
134 "current_date",
135 "current_role",
136 "current_time",
137 "current_timestamp",
138 "current_user",
139 "default",
140 "deferrable",
141 "desc",
142 "distinct",
143 "do",
144 "else",
145 "end",
146 "except",
147 "false",
148 "fetch",
149 "for",
150 "foreign",
151 "from",
152 "grant",
153 "group",
154 "having",
155 "in",
156 "initially",
157 "intersect",
158 "into",
159 "lateral",
160 "leading",
161 "limit",
162 "localtime",
163 "localtimestamp",
164 "not",
165 "null",
166 "offset",
167 "on",
168 "only",
169 "or",
170 "order",
171 "placing",
172 "primary",
173 "references",
174 "returning",
175 "select",
176 "session_user",
177 "some",
178 "symmetric",
179 "table",
180 "then",
181 "to",
182 "trailing",
183 "true",
184 "union",
185 "unique",
186 "user",
187 "using",
188 "variadic",
189 "when",
190 "where",
191 "window",
192 "with",
193];
194
195impl fmt::Display for Identifier {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 f.write_str(&self.0)
198 }
199}
200
201impl FromStr for Identifier {
202 type Err = IdentifierError;
203 fn from_str(s: &str) -> Result<Self, Self::Err> {
205 Self::from_unquoted(s)
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
211pub struct QualifiedName {
212 pub schema: Identifier,
214 pub name: Identifier,
216}
217
218impl QualifiedName {
219 pub const fn new(schema: Identifier, name: Identifier) -> Self {
221 Self { schema, name }
222 }
223
224 pub fn render_sql(&self) -> String {
226 format!("{}.{}", self.schema.render_sql(), self.name.render_sql())
227 }
228}
229
230impl fmt::Display for QualifiedName {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 write!(f, "{}.{}", self.schema, self.name)
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use pretty_assertions::assert_eq;
240
241 #[test]
242 fn unquoted_lowercases() {
243 let id = Identifier::from_unquoted("Users").unwrap();
244 assert_eq!(id.as_str(), "users");
245 }
246
247 #[test]
248 fn quoted_preserves_case() {
249 let id = Identifier::from_quoted("Users").unwrap();
250 assert_eq!(id.as_str(), "Users");
251 }
252
253 #[test]
254 fn rejects_empty() {
255 assert_eq!(Identifier::from_unquoted(""), Err(IdentifierError::Empty));
256 assert_eq!(Identifier::from_quoted(""), Err(IdentifierError::Empty));
257 }
258
259 #[test]
260 fn rejects_overlong() {
261 let long = "a".repeat(64);
262 assert!(matches!(
263 Identifier::from_unquoted(&long),
264 Err(IdentifierError::TooLong(64))
265 ));
266 }
267
268 #[test]
269 fn rejects_unquoted_starting_with_digit() {
270 assert!(matches!(
271 Identifier::from_unquoted("1foo"),
272 Err(IdentifierError::InvalidUnquotedChars(_))
273 ));
274 }
275
276 #[test]
277 fn quoted_allows_special_chars() {
278 let id = Identifier::from_quoted("foo bar").unwrap();
279 assert_eq!(id.as_str(), "foo bar");
280 }
281
282 #[test]
283 fn render_sql_quotes_when_necessary() {
284 assert_eq!(
285 Identifier::from_unquoted("users").unwrap().render_sql(),
286 "users"
287 );
288 assert_eq!(
289 Identifier::from_quoted("Users").unwrap().render_sql(),
290 "\"Users\""
291 );
292 assert_eq!(
293 Identifier::from_quoted("select").unwrap().render_sql(),
294 "\"select\""
295 );
296 }
297
298 #[test]
299 fn render_sql_escapes_embedded_quotes() {
300 let id = Identifier::from_quoted("a\"b").unwrap();
301 assert_eq!(id.render_sql(), "\"a\"\"b\"");
302 }
303
304 #[test]
305 fn qualified_name_renders() {
306 let qn = QualifiedName::new(
307 Identifier::from_unquoted("app").unwrap(),
308 Identifier::from_unquoted("users").unwrap(),
309 );
310 assert_eq!(qn.render_sql(), "app.users");
311 assert_eq!(qn.to_string(), "app.users");
312 }
313
314 #[test]
315 fn from_str_uses_unquoted_rules() {
316 let id: Identifier = "Foo".parse().unwrap();
317 assert_eq!(id.as_str(), "foo");
318 }
319}