Skip to main content

rigsql_rules/convention/
cv02.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::utils::first_non_trivia;
5use crate::violation::{LintViolation, SourceEdit};
6
7/// CV02: Use COALESCE instead of IFNULL or NVL.
8///
9/// COALESCE is ANSI standard and portable across databases.
10#[derive(Debug, Default)]
11pub struct RuleCV02;
12
13impl Rule for RuleCV02 {
14    fn code(&self) -> &'static str {
15        "CV02"
16    }
17    fn name(&self) -> &'static str {
18        "convention.coalesce"
19    }
20    fn description(&self) -> &'static str {
21        "Use COALESCE instead of IFNULL or NVL."
22    }
23    fn explanation(&self) -> &'static str {
24        "COALESCE is the ANSI SQL standard function for handling NULL values. \
25         IFNULL (MySQL) and NVL (Oracle) are database-specific alternatives. \
26         Using COALESCE improves portability and consistency."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Convention]
30    }
31    fn is_fixable(&self) -> bool {
32        true
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::Segment(vec![SegmentType::FunctionCall])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let children = ctx.segment.children();
41
42        // First non-trivia child should be the function name
43        let func_name = first_non_trivia(children);
44
45        if let Some(Segment::Token(t)) = func_name {
46            let name = t.token.text.as_str();
47            if name.eq_ignore_ascii_case("IFNULL") || name.eq_ignore_ascii_case("NVL") {
48                return vec![LintViolation::with_fix_and_msg_key(
49                    self.code(),
50                    format!("Use COALESCE instead of '{}'.", name),
51                    t.token.span,
52                    vec![SourceEdit::replace(t.token.span, "COALESCE")],
53                    "rules.CV02.msg",
54                    vec![("name".to_string(), name.to_string())],
55                )];
56            }
57        }
58
59        vec![]
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::test_utils::lint_sql;
67
68    #[test]
69    fn test_cv02_flags_ifnull() {
70        let violations = lint_sql("SELECT IFNULL(a, 0) FROM t", RuleCV02);
71        assert_eq!(violations.len(), 1);
72        assert_eq!(violations[0].fixes.len(), 1);
73        assert_eq!(violations[0].fixes[0].new_text, "COALESCE");
74    }
75
76    #[test]
77    fn test_cv02_flags_nvl() {
78        let violations = lint_sql("SELECT NVL(a, 0) FROM t", RuleCV02);
79        assert_eq!(violations.len(), 1);
80        assert_eq!(violations[0].fixes[0].new_text, "COALESCE");
81    }
82
83    #[test]
84    fn test_cv02_accepts_coalesce() {
85        let violations = lint_sql("SELECT COALESCE(a, 0) FROM t", RuleCV02);
86        assert_eq!(violations.len(), 0);
87    }
88}