Skip to main content

sqruff_lib/rules/ambiguous/
am09.rs

1use hashbrown::HashMap;
2use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
3
4use crate::core::config::Value;
5use crate::core::rules::context::RuleContext;
6use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
7use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
8
9#[derive(Debug, Clone)]
10pub struct RuleAM09;
11
12impl Rule for RuleAM09 {
13    fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
14        Ok(RuleAM09.erased())
15    }
16
17    fn name(&self) -> &'static str {
18        "ambiguous.order_by_limit"
19    }
20
21    fn description(&self) -> &'static str {
22        "LIMIT/OFFSET without ORDER BY."
23    }
24
25    fn long_description(&self) -> &'static str {
26        r#"
27**Anti-pattern**
28
29Using `LIMIT` or `OFFSET` without `ORDER BY` leads to non-deterministic results, as the database may return different rows on successive executions.
30
31```sql
32SELECT *
33FROM foo
34LIMIT 10;
35```
36
37**Best practice**
38
39Always use `ORDER BY` when using `LIMIT` or `OFFSET`.
40
41```sql
42SELECT *
43FROM foo
44ORDER BY id
45LIMIT 10;
46```
47"#
48    }
49
50    fn groups(&self) -> &'static [RuleGroups] {
51        &[RuleGroups::All, RuleGroups::Ambiguous]
52    }
53
54    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
55        assert!(context.segment.is_type(SyntaxKind::SelectStatement));
56
57        let children = context.segment.segments();
58
59        // Check if this select has a LIMIT or OFFSET clause.
60        let limit_or_offset = children
61            .iter()
62            .find(|s| s.is_type(SyntaxKind::LimitClause) || s.is_type(SyntaxKind::OffsetClause));
63
64        let Some(anchor) = limit_or_offset else {
65            return Vec::new();
66        };
67
68        // Check if there's an ORDER BY clause.
69        let has_order_by = children
70            .iter()
71            .any(|s| s.is_type(SyntaxKind::OrderbyClause));
72
73        if has_order_by {
74            return Vec::new();
75        }
76
77        vec![LintResult::new(
78            Some(anchor.clone()),
79            Vec::new(),
80            None,
81            Some("LIMIT/OFFSET without ORDER BY may lead to non-deterministic results.".into()),
82        )]
83    }
84
85    fn crawl_behaviour(&self) -> Crawler {
86        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
87    }
88}