Skip to main content

rigsql_rules/rigsql/
rg05.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// RG05: Subqueries in FROM clause should have an alias.
7///
8/// A subquery used as a table source must be given an explicit alias
9/// so that its columns can be referenced unambiguously.
10#[derive(Debug, Default)]
11pub struct RuleRG05;
12
13impl Rule for RuleRG05 {
14    fn code(&self) -> &'static str {
15        "RG05"
16    }
17    fn name(&self) -> &'static str {
18        "rigsql.subquery_alias"
19    }
20    fn description(&self) -> &'static str {
21        "Subqueries in FROM clause should have an alias."
22    }
23    fn explanation(&self) -> &'static str {
24        "When a subquery is used as a table source in a FROM or JOIN clause, \
25         it must be given an explicit alias. Without an alias, columns from the \
26         subquery cannot be referenced clearly in the outer query."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Aliasing]
30    }
31    fn is_fixable(&self) -> bool {
32        false
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::Segment(vec![SegmentType::FromClause, SegmentType::JoinClause])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let mut violations = Vec::new();
41        check_subqueries_have_alias(ctx.segment, &mut violations, self.code());
42        violations
43    }
44}
45
46fn check_subqueries_have_alias(
47    segment: &Segment,
48    violations: &mut Vec<LintViolation>,
49    code: &'static str,
50) {
51    let children = segment.children();
52
53    for child in children {
54        let st = child.segment_type();
55
56        // A bare Subquery (not wrapped in AliasExpression) lacks an alias
57        if st == SegmentType::Subquery {
58            violations.push(LintViolation::new(
59                code,
60                "Subquery in FROM/JOIN clause should have an alias.",
61                child.span(),
62            ));
63            continue;
64        }
65
66        // AliasExpression wraps aliased subqueries -- skip (already aliased)
67        if st == SegmentType::AliasExpression {
68            continue;
69        }
70
71        // Recurse into other nodes (e.g., JoinClause inside FromClause)
72        check_subqueries_have_alias(child, violations, code);
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::test_utils::lint_sql;
80
81    #[test]
82    fn test_rg05_flags_bare_subquery() {
83        let violations = lint_sql("SELECT * FROM (SELECT 1)", RuleRG05);
84        assert_eq!(violations.len(), 1);
85    }
86
87    #[test]
88    fn test_rg05_accepts_aliased_subquery() {
89        let violations = lint_sql("SELECT * FROM (SELECT 1) AS sub", RuleRG05);
90        assert_eq!(violations.len(), 0);
91    }
92}