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", "and", "any", "array", "as", "asc", "between", "by",
115 "case", "cast", "check", "column", "constraint", "create",
116 "cross", "current", "default", "delete", "desc", "distinct",
117 "drop", "else", "end", "exists", "false", "for", "foreign",
118 "from", "full", "group", "having", "in", "index", "inner",
119 "insert", "into", "is", "join", "key", "left", "like", "limit",
120 "not", "null", "offset", "on", "or", "order", "outer", "primary",
121 "references", "right", "select", "set", "table", "then", "to",
122 "true", "union", "unique", "update", "using", "values", "when",
123 "where", "with",
124 ];
125
126 KEYWORDS.contains(&s.to_lowercase().as_str())
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_escape_ident() {
135 assert_eq!(escape_ident("users"), "\"users\"");
136 assert_eq!(escape_ident("user_table"), "\"user_table\"");
137 assert_eq!(escape_ident("user\"name"), "\"user\"\"name\"");
138 assert_eq!(escape_ident("My Table"), "\"My Table\"");
139 assert_eq!(escape_ident(""), "\"\"");
140 }
141
142 #[test]
143 fn test_quote_literal() {
144 assert_eq!(quote_literal("hello"), "'hello'");
145 assert_eq!(quote_literal("it's"), "'it''s'");
146 assert_eq!(quote_literal(""), "''");
147 }
148
149 #[test]
150 fn test_from_qi() {
151 let qi = QualifiedIdentifier::new("public", "users");
152 assert_eq!(from_qi(&qi), "\"public\".\"users\"");
153
154 let qi = QualifiedIdentifier::unqualified("users");
155 assert_eq!(from_qi(&qi), "\"users\"");
156
157 let qi = QualifiedIdentifier::new("my schema", "my\"table");
158 assert_eq!(from_qi(&qi), "\"my schema\".\"my\"\"table\"");
159 }
160
161 #[test]
162 fn test_is_valid_identifier() {
163 assert!(is_valid_identifier("users"));
164 assert!(is_valid_identifier("_private"));
165 assert!(is_valid_identifier("user123"));
166 assert!(is_valid_identifier("user$table"));
167
168 assert!(!is_valid_identifier(""));
169 assert!(!is_valid_identifier("123users"));
170 assert!(!is_valid_identifier("my-table"));
171 assert!(!is_valid_identifier("my table"));
172 }
173
174 #[test]
175 fn test_is_keyword() {
176 assert!(is_keyword("select"));
177 assert!(is_keyword("SELECT"));
178 assert!(is_keyword("from"));
179 assert!(is_keyword("WHERE"));
180
181 assert!(!is_keyword("users"));
182 assert!(!is_keyword("my_column"));
183 }
184}