1use crate::SyntaxNode;
2use crate::generated::keywords::RESERVED_KEYWORDS;
3
4pub fn quote_column_alias(text: &str) -> String {
5 if needs_quoting(text) {
6 format!(r#""{}""#, text.replace('"', r#""""#))
7 } else {
8 text.to_string()
9 }
10}
11
12pub fn unquote_ident(node: &SyntaxNode) -> Option<String> {
13 let text = node.text().to_string();
14
15 if !text.starts_with('"') || !text.ends_with('"') {
16 return None;
17 }
18
19 let text = &text[1..text.len() - 1];
20
21 if is_reserved_word(text) {
22 return None;
23 }
24
25 if text.is_empty() {
26 return None;
27 }
28
29 let mut chars = text.chars();
30
31 match chars.next() {
33 Some(c) if c.is_lowercase() || c == '_' => {}
34 _ => return None,
35 }
36
37 for c in chars {
38 if c.is_lowercase() || c.is_ascii_digit() || c == '_' || c == '$' {
39 continue;
40 }
41 return None;
42 }
43
44 Some(text.to_string())
45}
46
47fn needs_quoting(text: &str) -> bool {
48 if text.is_empty() {
49 return true;
50 }
51
52 let mut chars = text.chars();
57
58 match chars.next() {
59 Some(c) if c.is_lowercase() || c == '_' => {}
60 _ => return true,
61 }
62
63 for c in chars {
64 if c.is_lowercase() || c.is_ascii_digit() || c == '_' || c == '$' {
65 continue;
66 }
67 return true;
68 }
69
70 false
71}
72
73pub fn is_reserved_word(text: &str) -> bool {
74 RESERVED_KEYWORDS
75 .binary_search(&text.to_lowercase().as_str())
76 .is_ok()
77}
78
79pub fn normalize_identifier(text: &str) -> String {
80 text.strip_prefix('"')
82 .and_then(|t| t.strip_suffix('"'))
83 .map(|x| x.replace(r#""""#, "\""))
84 .unwrap_or_else(|| text.to_ascii_lowercase())
85}
86
87#[cfg(test)]
88mod tests {
89 use insta::assert_snapshot;
90
91 use super::*;
92
93 #[test]
94 fn quote_column_alias_handles_embedded_quotes() {
95 assert_snapshot!(quote_column_alias(r#"foo"bar"#), @r#""foo""bar""#);
96 }
97
98 #[test]
99 fn quote_column_alias_doesnt_quote_reserved_words() {
100 assert_snapshot!(quote_column_alias("case"), @"case");
102 assert_snapshot!(quote_column_alias("array"), @"array");
103 }
104
105 #[test]
106 fn quote_column_alias_doesnt_quote_simple_identifiers() {
107 assert_snapshot!(quote_column_alias("col_name"), @"col_name");
108 }
109
110 #[test]
111 fn quote_column_alias_handles_special_column_name() {
112 assert_snapshot!(quote_column_alias("?column?"), @r#""?column?""#);
113 }
114}