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
47pub fn 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_ascii_lowercase().as_str())
76 .is_ok()
77}
78
79pub fn strip_quotes(text: &str) -> Option<&str> {
80 text.strip_prefix('\'')?.strip_suffix('\'')
81}
82
83pub fn strip_prefixed_quotes(text: &str, prefix: [char; 2]) -> Option<&str> {
84 strip_quotes(text.strip_prefix(prefix)?)
85}
86
87pub fn strip_unicode_esc_prefix(text: &str) -> Option<&str> {
88 strip_quotes(text.strip_prefix(['u', 'U'])?.strip_prefix('&')?)
89}
90
91pub fn strip_dollar_quotes(text: &str) -> Option<&str> {
92 let after_first = text.strip_prefix('$')?;
93 let tag_end = after_first.find('$')?;
94 let tag = &after_first[..tag_end];
95 let body = &after_first[tag_end + 1..];
96 let closing = format!("${tag}$");
97 body.strip_suffix(&closing)
98}
99
100#[cfg(test)]
101mod tests {
102 use insta::assert_snapshot;
103
104 use super::*;
105
106 #[test]
107 fn quote_column_alias_handles_embedded_quotes() {
108 assert_snapshot!(quote_column_alias(r#"foo"bar"#), @r#""foo""bar""#);
109 }
110
111 #[test]
112 fn quote_column_alias_doesnt_quote_reserved_words() {
113 assert_snapshot!(quote_column_alias("case"), @"case");
115 assert_snapshot!(quote_column_alias("array"), @"array");
116 }
117
118 #[test]
119 fn quote_column_alias_doesnt_quote_simple_identifiers() {
120 assert_snapshot!(quote_column_alias("col_name"), @"col_name");
121 }
122
123 #[test]
124 fn quote_column_alias_handles_special_column_name() {
125 assert_snapshot!(quote_column_alias("?column?"), @r#""?column?""#);
126 }
127}