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 dollar_quote_tag(text: &str) -> Option<&str> {
92 text.strip_prefix('$')?.split_once('$').map(|(tag, _)| tag)
93}
94
95pub fn strip_dollar_quotes(text: &str) -> Option<&str> {
96 let tag = dollar_quote_tag(text)?;
97 let body = &text[tag.len() + 2..];
98 let closing = format!("${tag}$");
99 body.strip_suffix(&closing)
100}
101
102#[cfg(test)]
103mod tests {
104 use insta::assert_snapshot;
105
106 use super::*;
107
108 #[test]
109 fn quote_column_alias_handles_embedded_quotes() {
110 assert_snapshot!(quote_column_alias(r#"foo"bar"#), @r#""foo""bar""#);
111 }
112
113 #[test]
114 fn quote_column_alias_doesnt_quote_reserved_words() {
115 assert_snapshot!(quote_column_alias("case"), @"case");
117 assert_snapshot!(quote_column_alias("array"), @"array");
118 }
119
120 #[test]
121 fn quote_column_alias_doesnt_quote_simple_identifiers() {
122 assert_snapshot!(quote_column_alias("col_name"), @"col_name");
123 }
124
125 #[test]
126 fn quote_column_alias_handles_special_column_name() {
127 assert_snapshot!(quote_column_alias("?column?"), @r#""?column?""#);
128 }
129}