Skip to main content

rigsql_rules/
utils.rs

1use rigsql_core::{Segment, SegmentType};
2
3/// Check if an AliasExpression's children contain an explicit AS keyword.
4pub fn has_as_keyword(children: &[Segment]) -> bool {
5    children.iter().any(|child| {
6        if let Segment::Token(t) = child {
7            t.segment_type == SegmentType::Keyword && t.token.text.eq_ignore_ascii_case("AS")
8        } else {
9            false
10        }
11    })
12}
13
14/// Return the first non-trivia child segment.
15pub fn first_non_trivia(children: &[Segment]) -> Option<&Segment> {
16    children.iter().find(|c| !c.segment_type().is_trivia())
17}
18
19/// Return the last non-trivia child segment.
20pub fn last_non_trivia(children: &[Segment]) -> Option<&Segment> {
21    children
22        .iter()
23        .rev()
24        .find(|c| !c.segment_type().is_trivia())
25}
26
27/// Keywords that should NOT be treated as alias names.
28/// Sorted alphabetically for binary_search.
29const NOT_ALIAS_KEYWORDS: &[&str] = &[
30    "ALTER",
31    "AND",
32    "BEGIN",
33    "BREAK",
34    "CATCH",
35    "CLOSE",
36    "COMMIT",
37    "CONTINUE",
38    "CREATE",
39    "CROSS",
40    "CURSOR",
41    "DEALLOCATE",
42    "DECLARE",
43    "DELETE",
44    "DROP",
45    "ELSE",
46    "END",
47    "EXCEPT",
48    "EXEC",
49    "EXECUTE",
50    "FETCH",
51    "FOR",
52    "FROM",
53    "FULL",
54    "GO",
55    "GOTO",
56    "GROUP",
57    "HAVING",
58    "IF",
59    "INNER",
60    "INSERT",
61    "INTERSECT",
62    "INTO",
63    "JOIN",
64    "LEFT",
65    "LIMIT",
66    "MERGE",
67    "NATURAL",
68    "NEXT",
69    "OFFSET",
70    "ON",
71    "OPEN",
72    "OR",
73    "ORDER",
74    "OUTPUT",
75    "OVER",
76    "PRINT",
77    "RAISERROR",
78    "RETURN",
79    "RETURNING",
80    "RIGHT",
81    "ROLLBACK",
82    "SELECT",
83    "SET",
84    "TABLE",
85    "THEN",
86    "THROW",
87    "TRUNCATE",
88    "TRY",
89    "UNION",
90    "UPDATE",
91    "VALUES",
92    "WHEN",
93    "WHERE",
94    "WHILE",
95    "WITH",
96];
97
98/// Check if the "alias name" in an AliasExpression is actually a misidentified
99/// SQL keyword (e.g. OVER in window functions). Returns true if the alias
100/// looks like a false positive.
101pub fn is_false_alias(children: &[Segment]) -> bool {
102    // The alias name is the last non-trivia child
103    if let Some(Segment::Token(t)) = last_non_trivia(children) {
104        let upper = t.token.text.to_ascii_uppercase();
105        return NOT_ALIAS_KEYWORDS.binary_search(&upper.as_str()).is_ok();
106    }
107    false
108}
109
110/// Extract the alias name from an AliasExpression.
111/// The alias name is the last Identifier or QuotedIdentifier before any
112/// non-trivia, non-keyword segment (scanning from the end).
113pub fn extract_alias_name(children: &[Segment]) -> Option<String> {
114    for child in children.iter().rev() {
115        let st = child.segment_type();
116        if st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier {
117            if let Segment::Token(t) = child {
118                return Some(t.token.text.to_string());
119            }
120        }
121        if st.is_trivia() {
122            continue;
123        }
124        if st != SegmentType::Keyword {
125            break;
126        }
127    }
128    None
129}
130
131/// Find a keyword by case-insensitive name in children. Returns (index, segment).
132pub fn find_keyword_in_children<'a>(
133    children: &'a [Segment],
134    name: &str,
135) -> Option<(usize, &'a Segment)> {
136    children.iter().enumerate().find(|(_, c)| {
137        if let Segment::Token(t) = c {
138            t.segment_type == SegmentType::Keyword && t.token.text.eq_ignore_ascii_case(name)
139        } else {
140            false
141        }
142    })
143}