1use crate::SyntaxNode;
2use crate::generated::keywords::RESERVED_KEYWORDS;
3
4pub fn quote_string_literal(text: &str) -> String {
5 format!("'{}'", text.replace('\'', "''"))
6}
7
8pub fn quote_column_alias(text: &str) -> String {
9 if needs_quoting(text) {
10 format!(r#""{}""#, text.replace('"', r#""""#))
11 } else {
12 text.to_string()
13 }
14}
15
16pub fn unquote_ident(node: &SyntaxNode) -> Option<String> {
17 let text = node.text().to_string();
18
19 if !text.starts_with('"') || !text.ends_with('"') {
20 return None;
21 }
22
23 let text = &text[1..text.len() - 1];
24
25 if is_reserved_word(text) {
26 return None;
27 }
28
29 if text.is_empty() {
30 return None;
31 }
32
33 let mut chars = text.chars();
34
35 match chars.next() {
37 Some(c) if c.is_lowercase() || c == '_' => {}
38 _ => return None,
39 }
40
41 for c in chars {
42 if c.is_lowercase() || c.is_ascii_digit() || c == '_' || c == '$' {
43 continue;
44 }
45 return None;
46 }
47
48 Some(text.to_string())
49}
50
51pub fn needs_quoting(text: &str) -> bool {
52 if text.is_empty() {
53 return true;
54 }
55
56 let mut chars = text.chars();
61
62 match chars.next() {
63 Some(c) if c.is_lowercase() || c == '_' => {}
64 _ => return true,
65 }
66
67 for c in chars {
68 if c.is_lowercase() || c.is_ascii_digit() || c == '_' || c == '$' {
69 continue;
70 }
71 return true;
72 }
73
74 false
75}
76
77pub fn is_reserved_word(text: &str) -> bool {
78 RESERVED_KEYWORDS
79 .binary_search(&text.to_ascii_lowercase().as_str())
80 .is_ok()
81}
82
83pub fn strip_quotes(text: &str) -> Option<&str> {
84 text.strip_prefix('\'')?.strip_suffix('\'')
85}
86
87pub fn strip_prefixed_quotes(text: &str, prefix: [char; 2]) -> Option<&str> {
88 strip_quotes(text.strip_prefix(prefix)?)
89}
90
91pub fn strip_unicode_esc_prefix(text: &str) -> Option<&str> {
92 strip_quotes(text.strip_prefix(['u', 'U'])?.strip_prefix('&')?)
93}
94
95pub fn dollar_quote_tag(text: &str) -> Option<&str> {
96 text.strip_prefix('$')?.split_once('$').map(|(tag, _)| tag)
97}
98
99pub fn strip_dollar_quotes(text: &str) -> Option<&str> {
100 let tag = dollar_quote_tag(text)?;
101 let body = &text[tag.len() + 2..];
102 let closing = format!("${tag}$");
103 body.strip_suffix(&closing)
104}
105
106#[cfg(test)]
107mod tests {
108 use insta::assert_snapshot;
109
110 use super::*;
111
112 #[test]
113 fn quote_string_literal_escapes_apostrophes() {
114 assert_snapshot!(quote_string_literal("it's"), @"'it''s'");
115 }
116
117 #[test]
118 fn quote_column_alias_handles_embedded_quotes() {
119 assert_snapshot!(quote_column_alias(r#"foo"bar"#), @r#""foo""bar""#);
120 }
121
122 #[test]
123 fn quote_column_alias_doesnt_quote_reserved_words() {
124 assert_snapshot!(quote_column_alias("case"), @"case");
126 assert_snapshot!(quote_column_alias("array"), @"array");
127 }
128
129 #[test]
130 fn quote_column_alias_doesnt_quote_simple_identifiers() {
131 assert_snapshot!(quote_column_alias("col_name"), @"col_name");
132 }
133
134 #[test]
135 fn quote_column_alias_handles_special_column_name() {
136 assert_snapshot!(quote_column_alias("?column?"), @r#""?column?""#);
137 }
138}