Skip to main content

rigsql_rules/ambiguous/
am04.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// AM04: SELECT * should list columns explicitly.
7///
8/// Using SELECT * is ambiguous because the column set depends on the table
9/// definition and may change unexpectedly.
10#[derive(Debug, Default)]
11pub struct RuleAM04;
12
13impl Rule for RuleAM04 {
14    fn code(&self) -> &'static str {
15        "AM04"
16    }
17    fn name(&self) -> &'static str {
18        "ambiguous.column_count"
19    }
20    fn description(&self) -> &'static str {
21        "SELECT * should list columns explicitly."
22    }
23    fn explanation(&self) -> &'static str {
24        "Using SELECT * makes the query's column set depend on the table schema, which \
25         can change over time. Listing columns explicitly makes the query self-documenting \
26         and prevents unexpected changes when columns are added or removed."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Ambiguous]
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 mut violations = Vec::new();
41        find_bare_stars(ctx.segment, false, &mut violations);
42        violations
43    }
44}
45
46/// Find Star segments that are NOT inside a FunctionCall (e.g. COUNT(*) is ok).
47fn find_bare_stars(segment: &Segment, in_function: bool, violations: &mut Vec<LintViolation>) {
48    if segment.segment_type() == SegmentType::Star && !in_function {
49        violations.push(LintViolation::with_msg_key(
50            "AM04",
51            "SELECT * used. List columns explicitly.",
52            segment.span(),
53            "rules.AM04.msg",
54            vec![],
55        ));
56        return;
57    }
58
59    let entering_function = segment.segment_type() == SegmentType::FunctionCall;
60
61    for child in segment.children() {
62        find_bare_stars(child, in_function || entering_function, violations);
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::test_utils::lint_sql;
70
71    #[test]
72    fn test_am04_flags_select_star() {
73        let violations = lint_sql("SELECT * FROM t", RuleAM04);
74        assert_eq!(violations.len(), 1);
75    }
76
77    #[test]
78    fn test_am04_accepts_explicit_columns() {
79        let violations = lint_sql("SELECT a, b FROM t", RuleAM04);
80        assert_eq!(violations.len(), 0);
81    }
82
83    #[test]
84    fn test_am04_accepts_count_star() {
85        let violations = lint_sql("SELECT COUNT(*) FROM t", RuleAM04);
86        assert_eq!(violations.len(), 0);
87    }
88}