Skip to main content

rigsql_rules/ambiguous/
am09.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// AM09: LIMIT without ORDER BY gives non-deterministic results.
7///
8/// Using LIMIT without ORDER BY means the returned rows are unpredictable.
9#[derive(Debug, Default)]
10pub struct RuleAM09;
11
12impl Rule for RuleAM09 {
13    fn code(&self) -> &'static str {
14        "AM09"
15    }
16    fn name(&self) -> &'static str {
17        "ambiguous.order_by_limit"
18    }
19    fn description(&self) -> &'static str {
20        "LIMIT without ORDER BY."
21    }
22    fn explanation(&self) -> &'static str {
23        "Using LIMIT without ORDER BY produces non-deterministic results because the \
24         database is free to return rows in any order. Always pair LIMIT with ORDER BY \
25         to get predictable, reproducible result sets."
26    }
27    fn groups(&self) -> &[RuleGroup] {
28        &[RuleGroup::Ambiguous]
29    }
30    fn is_fixable(&self) -> bool {
31        false
32    }
33
34    fn crawl_type(&self) -> CrawlType {
35        CrawlType::Segment(vec![SegmentType::SelectStatement])
36    }
37
38    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
39        let children = ctx.segment.children();
40
41        let limit_clause = children
42            .iter()
43            .find(|c| c.segment_type() == SegmentType::LimitClause);
44
45        let has_order_by = children
46            .iter()
47            .any(|c| c.segment_type() == SegmentType::OrderByClause);
48
49        if let Some(limit) = limit_clause {
50            if !has_order_by {
51                return vec![LintViolation::new(
52                    self.code(),
53                    "LIMIT without ORDER BY gives non-deterministic results.",
54                    limit.span(),
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_am09_flags_limit_without_order_by() {
70        let violations = lint_sql("SELECT a FROM t LIMIT 10", RuleAM09);
71        assert_eq!(violations.len(), 1);
72    }
73
74    #[test]
75    fn test_am09_accepts_limit_with_order_by() {
76        let violations = lint_sql("SELECT a FROM t ORDER BY a LIMIT 10", RuleAM09);
77        assert_eq!(violations.len(), 0);
78    }
79
80    #[test]
81    fn test_am09_accepts_no_limit() {
82        let violations = lint_sql("SELECT a FROM t", RuleAM09);
83        assert_eq!(violations.len(), 0);
84    }
85}