flowscope_core/analyzer/helpers/
naming.rs1use sqlparser::ast::{Ident, ObjectName};
2
3use crate::types::CanonicalName;
4
5fn object_name_part_value(part: &sqlparser::ast::ObjectNamePart) -> String {
14 part.as_ident()
15 .map(|ident| ident.value.clone())
16 .unwrap_or_else(|| part.to_string())
17}
18
19pub fn extract_simple_name_from_object_name(name: &ObjectName) -> String {
27 name.0
28 .last()
29 .map(object_name_part_value)
30 .unwrap_or_default()
31}
32
33pub fn canonical_name_from_object_name(name: &ObjectName) -> CanonicalName {
45 match name.0.len() {
47 0 => CanonicalName::table(None, None, String::new()),
48 1 => CanonicalName::table(None, None, object_name_part_value(&name.0[0])),
49 2 => CanonicalName::table(
50 None,
51 Some(object_name_part_value(&name.0[0])),
52 object_name_part_value(&name.0[1]),
53 ),
54 3 => CanonicalName::table(
55 Some(object_name_part_value(&name.0[0])),
56 Some(object_name_part_value(&name.0[1])),
57 object_name_part_value(&name.0[2]),
58 ),
59 _ => {
60 let joined = name
63 .0
64 .iter()
65 .map(object_name_part_value)
66 .collect::<Vec<_>>()
67 .join(".");
68 CanonicalName::table(None, None, joined)
69 }
70 }
71}
72
73pub fn ident_value(ident: &Ident) -> &str {
79 &ident.value
80}
81
82pub fn extract_simple_name(name: &str) -> String {
87 let mut parts = split_qualified_identifiers(name);
88 parts.pop().unwrap_or_else(|| name.to_string())
89}
90
91pub fn split_qualified_identifiers(name: &str) -> Vec<String> {
92 let mut parts = Vec::new();
93 let mut current = String::new();
94 let mut chars = name.chars().peekable();
95 let mut active_quote: Option<char> = None;
96
97 while let Some(ch) = chars.next() {
98 if let Some(q) = active_quote {
99 current.push(ch);
100 if ch == q {
101 if matches!(q, '"' | '\'' | '`') {
102 if let Some(next) = chars.peek() {
103 if *next == q {
104 current.push(chars.next().unwrap());
105 continue;
106 }
107 }
108 }
109 active_quote = None;
110 } else if q == ']' && ch == ']' {
111 active_quote = None;
112 }
113 continue;
114 }
115
116 match ch {
117 '"' | '\'' | '`' => {
118 active_quote = Some(ch);
119 current.push(ch);
120 }
121 '[' => {
122 active_quote = Some(']');
123 current.push(ch);
124 }
125 '.' => {
126 if !current.is_empty() {
127 parts.push(current.trim().to_string());
128 current.clear();
129 }
130 }
131 _ => current.push(ch),
132 }
133 }
134
135 if !current.is_empty() {
136 parts.push(current.trim().to_string());
137 }
138
139 if parts.is_empty() && !name.is_empty() {
140 vec![name.trim().to_string()]
141 } else {
142 parts
143 }
144}
145
146pub fn is_quoted_identifier(part: &str) -> bool {
147 let trimmed = part.trim();
148 if trimmed.len() < 2 {
149 return false;
150 }
151 let first = trimmed.chars().next().unwrap();
152 let last = trimmed.chars().last().unwrap();
153 matches!(
154 (first, last),
155 ('"', '"') | ('`', '`') | ('[', ']') | ('\'', '\'')
156 )
157}
158
159pub fn unquote_identifier(part: &str) -> String {
160 let trimmed = part.trim();
161 if trimmed.len() < 2 {
162 return trimmed.to_string();
163 }
164
165 if is_quoted_identifier(trimmed) {
166 trimmed[1..trimmed.len() - 1].to_string()
167 } else {
168 trimmed.to_string()
169 }
170}
171
172pub fn parse_canonical_name(name: &str) -> CanonicalName {
173 let parts = split_qualified_identifiers(name);
174 match parts.len() {
175 0 => CanonicalName::table(None, None, String::new()),
176 1 => CanonicalName::table(None, None, parts[0].clone()),
177 2 => CanonicalName::table(None, Some(parts[0].clone()), parts[1].clone()),
178 3 => CanonicalName::table(
179 Some(parts[0].clone()),
180 Some(parts[1].clone()),
181 parts[2].clone(),
182 ),
183 _ => CanonicalName::table(None, None, name.to_string()),
184 }
185}