Skip to main content

rigsql_rules/rigsql/
rg04.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// RG04: Use of HAVING without GROUP BY.
7///
8/// A HAVING clause without a corresponding GROUP BY is likely a mistake;
9/// use WHERE instead, or add the missing GROUP BY.
10#[derive(Debug, Default)]
11pub struct RuleRG04;
12
13impl Rule for RuleRG04 {
14    fn code(&self) -> &'static str {
15        "RG04"
16    }
17    fn name(&self) -> &'static str {
18        "rigsql.having_without_group_by"
19    }
20    fn description(&self) -> &'static str {
21        "Use of HAVING without GROUP BY."
22    }
23    fn explanation(&self) -> &'static str {
24        "HAVING is designed to filter grouped results. Using HAVING without GROUP BY \
25         treats the entire result set as a single group, which is almost always a mistake. \
26         Use WHERE for filtering ungrouped rows, or add the missing GROUP BY clause."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Convention]
30    }
31    fn is_fixable(&self) -> bool {
32        false
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::Segment(vec![SegmentType::SelectStatement])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let children = ctx.segment.children();
41
42        let has_having = children
43            .iter()
44            .any(|c| c.segment_type() == SegmentType::HavingClause);
45        let has_group_by = children
46            .iter()
47            .any(|c| c.segment_type() == SegmentType::GroupByClause);
48
49        if has_having && !has_group_by {
50            let having_span = children
51                .iter()
52                .find(|c| c.segment_type() == SegmentType::HavingClause)
53                .map(|c| c.span())
54                .unwrap_or(ctx.segment.span());
55
56            return vec![LintViolation::with_msg_key(
57                self.code(),
58                "HAVING clause without GROUP BY. Use WHERE for ungrouped filtering.",
59                having_span,
60                "rules.RG04.msg",
61                vec![],
62            )];
63        }
64
65        vec![]
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::test_utils::lint_sql;
73
74    #[test]
75    fn test_rg04_flags_having_without_group_by() {
76        let violations = lint_sql("SELECT COUNT(*) FROM t HAVING COUNT(*) > 1", RuleRG04);
77        assert_eq!(violations.len(), 1);
78    }
79
80    #[test]
81    fn test_rg04_accepts_having_with_group_by() {
82        let violations = lint_sql(
83            "SELECT a, COUNT(*) FROM t GROUP BY a HAVING COUNT(*) > 1",
84            RuleRG04,
85        );
86        assert_eq!(violations.len(), 0);
87    }
88}