rigsql-rules 0.7.0

Lint rules (sqlfluff-compatible) for the rigsql SQL linter
Documentation
use rigsql_core::SegmentType;

use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
use crate::violation::LintViolation;

/// RG04: Use of HAVING without GROUP BY.
///
/// A HAVING clause without a corresponding GROUP BY is likely a mistake;
/// use WHERE instead, or add the missing GROUP BY.
#[derive(Debug, Default)]
pub struct RuleRG04;

impl Rule for RuleRG04 {
    fn code(&self) -> &'static str {
        "RG04"
    }
    fn name(&self) -> &'static str {
        "rigsql.having_without_group_by"
    }
    fn description(&self) -> &'static str {
        "Use of HAVING without GROUP BY."
    }
    fn explanation(&self) -> &'static str {
        "HAVING is designed to filter grouped results. Using HAVING without GROUP BY \
         treats the entire result set as a single group, which is almost always a mistake. \
         Use WHERE for filtering ungrouped rows, or add the missing GROUP BY clause."
    }
    fn groups(&self) -> &[RuleGroup] {
        &[RuleGroup::Convention]
    }
    fn is_fixable(&self) -> bool {
        false
    }

    fn crawl_type(&self) -> CrawlType {
        CrawlType::Segment(vec![SegmentType::SelectStatement])
    }

    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
        let children = ctx.segment.children();

        let has_having = children
            .iter()
            .any(|c| c.segment_type() == SegmentType::HavingClause);
        let has_group_by = children
            .iter()
            .any(|c| c.segment_type() == SegmentType::GroupByClause);

        if has_having && !has_group_by {
            let having_span = children
                .iter()
                .find(|c| c.segment_type() == SegmentType::HavingClause)
                .map(|c| c.span())
                .unwrap_or(ctx.segment.span());

            return vec![LintViolation::with_msg_key(
                self.code(),
                "HAVING clause without GROUP BY. Use WHERE for ungrouped filtering.",
                having_span,
                "rules.RG04.msg",
                vec![],
            )];
        }

        vec![]
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_utils::lint_sql;

    #[test]
    fn test_rg04_flags_having_without_group_by() {
        let violations = lint_sql("SELECT COUNT(*) FROM t HAVING COUNT(*) > 1", RuleRG04);
        assert_eq!(violations.len(), 1);
    }

    #[test]
    fn test_rg04_accepts_having_with_group_by() {
        let violations = lint_sql(
            "SELECT a, COUNT(*) FROM t GROUP BY a HAVING COUNT(*) > 1",
            RuleRG04,
        );
        assert_eq!(violations.len(), 0);
    }
}