Skip to main content

rigsql_rules/aliasing/
al08.rs

1use std::collections::HashMap;
2
3use rigsql_core::SegmentType;
4
5use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
6use crate::utils::extract_alias_name;
7use crate::violation::LintViolation;
8
9/// AL08: Column aliases should be unique within each SELECT clause.
10///
11/// Duplicate column aliases create ambiguity in the result set.
12#[derive(Debug, Default)]
13pub struct RuleAL08;
14
15impl Rule for RuleAL08 {
16    fn code(&self) -> &'static str {
17        "AL08"
18    }
19    fn name(&self) -> &'static str {
20        "aliasing.unique.column"
21    }
22    fn description(&self) -> &'static str {
23        "Column aliases should be unique within each statement."
24    }
25    fn explanation(&self) -> &'static str {
26        "When the same alias is used for multiple columns in a single SELECT clause, \
27         the result set becomes ambiguous. Each column alias must be unique within \
28         its containing SELECT clause."
29    }
30    fn groups(&self) -> &[RuleGroup] {
31        &[RuleGroup::Aliasing]
32    }
33    fn is_fixable(&self) -> bool {
34        false
35    }
36
37    fn crawl_type(&self) -> CrawlType {
38        CrawlType::Segment(vec![SegmentType::SelectClause])
39    }
40
41    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
42        let mut violations = Vec::new();
43        let mut seen: HashMap<String, rigsql_core::Span> = HashMap::new();
44
45        for child in ctx.segment.children() {
46            if child.segment_type() == SegmentType::AliasExpression {
47                if let Some(name) = extract_alias_name(child.children()) {
48                    let span = child.span();
49                    let lower = name.to_lowercase();
50                    if let Some(first_span) = seen.get(&lower) {
51                        violations.push(LintViolation::with_msg_key(
52                            self.code(),
53                            format!(
54                                "Duplicate column alias '{}'. First used at offset {}.",
55                                name, first_span.start,
56                            ),
57                            span,
58                            "rules.AL08.msg",
59                            vec![
60                                ("name".to_string(), name.to_string()),
61                                ("offset".to_string(), first_span.start.to_string()),
62                            ],
63                        ));
64                    } else {
65                        seen.insert(lower, span);
66                    }
67                }
68            }
69        }
70
71        violations
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::test_utils::lint_sql;
79
80    #[test]
81    fn test_al08_flags_duplicate_column_alias() {
82        let violations = lint_sql("SELECT a AS x, b AS x FROM t", RuleAL08);
83        assert_eq!(violations.len(), 1);
84    }
85
86    #[test]
87    fn test_al08_accepts_unique_column_aliases() {
88        let violations = lint_sql("SELECT a AS x, b AS y FROM t", RuleAL08);
89        assert_eq!(violations.len(), 0);
90    }
91}