Skip to main content

rigsql_rules/structure/
st08.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// ST08: DISTINCT used with parentheses.
7///
8/// DISTINCT applies to the entire row, not a single column. Using
9/// parentheses after DISTINCT makes it look like a function call.
10#[derive(Debug, Default)]
11pub struct RuleST08;
12
13impl Rule for RuleST08 {
14    fn code(&self) -> &'static str {
15        "ST08"
16    }
17    fn name(&self) -> &'static str {
18        "structure.distinct"
19    }
20    fn description(&self) -> &'static str {
21        "DISTINCT used with parentheses is misleading."
22    }
23    fn explanation(&self) -> &'static str {
24        "DISTINCT is a keyword that applies to the entire SELECT result set, not a function. \
25         Writing SELECT DISTINCT(col) suggests DISTINCT operates on a single column like a \
26         function, which is misleading. Use SELECT DISTINCT col instead."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Structure]
30    }
31    fn is_fixable(&self) -> bool {
32        false
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::Segment(vec![SegmentType::SelectClause])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let children = ctx.segment.children();
41
42        // Look for DISTINCT keyword followed by a ParenExpression
43        let mut found_distinct = false;
44        for child in children {
45            if child.segment_type().is_trivia() {
46                continue;
47            }
48
49            if found_distinct {
50                // The next non-trivia after DISTINCT
51                if child.segment_type() == SegmentType::ParenExpression {
52                    return vec![LintViolation::new(
53                        self.code(),
54                        "DISTINCT used with parentheses is misleading. DISTINCT is not a function.",
55                        child.span(),
56                    )];
57                }
58                // If it's not a paren expression, stop looking
59                found_distinct = false;
60            }
61
62            if let Segment::Token(t) = child {
63                if t.segment_type == SegmentType::Keyword
64                    && t.token.text.eq_ignore_ascii_case("DISTINCT")
65                {
66                    found_distinct = true;
67                }
68            }
69        }
70
71        vec![]
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::test_utils::lint_sql;
79
80    #[test]
81    fn test_st08_accepts_distinct_without_parens() {
82        let violations = lint_sql("SELECT DISTINCT a, b FROM t;", RuleST08);
83        assert_eq!(violations.len(), 0);
84    }
85
86    #[test]
87    fn test_st08_accepts_no_distinct() {
88        let violations = lint_sql("SELECT a FROM t;", RuleST08);
89        assert_eq!(violations.len(), 0);
90    }
91}