flowscope_core/analyzer/helpers/
naming.rs

1use sqlparser::ast::{Ident, ObjectName};
2
3use crate::types::CanonicalName;
4
5// =============================================================================
6// ObjectName-based helpers (work directly with AST types)
7// =============================================================================
8
9/// Extract the identifier value from an ObjectName part.
10///
11/// Returns the string value for Identifier parts, or the Display representation
12/// for Function parts.
13fn 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
19/// Extract the simple (unqualified) name from an ObjectName.
20///
21/// Works directly with the AST to avoid string parsing.
22///
23/// # Examples
24/// - `schema.table` → `"table"`
25/// - `catalog.schema.table` → `"table"`
26pub 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
33/// Build a CanonicalName from an ObjectName without string round-trips.
34///
35/// This is more efficient than converting to string and parsing, as it
36/// works directly with the already-parsed identifiers. For names that have
37/// more than three segments we follow the legacy string-based helper and
38/// treat the entire identifier as a single `name` so callers do not lose
39/// the leading qualifiers (e.g. SQL Server's `server.database.schema.table`).
40///
41/// All name parts are stored unquoted for consistency. For example,
42/// `"my.schema"."my.table"` becomes `my.schema.my.table` in the name field
43/// when there are more than 3 parts.
44pub fn canonical_name_from_object_name(name: &ObjectName) -> CanonicalName {
45    // Match on length first to avoid intermediate Vec allocation for common cases
46    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            // For >3 parts, join all unquoted values with dots to maintain
61            // consistency with the 1-3 part branches (which use unquoted values)
62            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
73/// Get the unquoted value from an Ident.
74///
75/// sqlparser's Ident already stores the unquoted value in `.value`,
76/// so this is just a convenience accessor that mirrors the string-based
77/// `unquote_identifier` function.
78pub fn ident_value(ident: &Ident) -> &str {
79    &ident.value
80}
81
82// =============================================================================
83// String-based helpers (for user input and backward compatibility)
84// =============================================================================
85
86pub 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}