Skip to main content

flowscope_core/linter/rules/
am_009.rs

1//! LINT_AM_009: LIMIT/OFFSET without ORDER BY.
2//!
3//! SQLFluff AM09 parity: use of LIMIT/OFFSET without ORDER BY may lead to
4//! non-deterministic results.
5
6use crate::linter::rule::{LintContext, LintRule};
7use crate::types::{issue_codes, Issue};
8use sqlparser::ast::{
9    Expr, FunctionArg, FunctionArgExpr, FunctionArguments, LimitClause, OrderByKind, Query, Select,
10    SetExpr, Statement, TableFactor, WindowType,
11};
12
13use super::semantic_helpers::join_on_expr;
14
15pub struct LimitOffsetWithoutOrderBy;
16
17impl LintRule for LimitOffsetWithoutOrderBy {
18    fn code(&self) -> &'static str {
19        issue_codes::LINT_AM_009
20    }
21
22    fn name(&self) -> &'static str {
23        "LIMIT/OFFSET without ORDER BY"
24    }
25
26    fn description(&self) -> &'static str {
27        "Use of LIMIT and OFFSET without ORDER BY may lead to non-deterministic results."
28    }
29
30    fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
31        let mut violation_count = 0usize;
32        check_statement(statement, &mut violation_count);
33
34        (0..violation_count)
35            .map(|_| {
36                Issue::warning(
37                    issue_codes::LINT_AM_009,
38                    "LIMIT/OFFSET used without ORDER BY may lead to non-deterministic results.",
39                )
40                .with_statement(ctx.statement_index)
41            })
42            .collect()
43    }
44}
45
46fn check_statement(statement: &Statement, violations: &mut usize) {
47    match statement {
48        Statement::Query(query) => check_query(query, violations),
49        Statement::Insert(insert) => {
50            if let Some(source) = &insert.source {
51                check_query(source, violations);
52            }
53        }
54        Statement::CreateView { query, .. } => check_query(query, violations),
55        Statement::CreateTable(create) => {
56            if let Some(query) = &create.query {
57                check_query(query, violations);
58            }
59        }
60        _ => {}
61    }
62}
63
64fn check_query(query: &Query, violations: &mut usize) {
65    if let Some(with) = &query.with {
66        for cte in &with.cte_tables {
67            check_query(&cte.query, violations);
68        }
69    }
70
71    check_set_expr(&query.body, violations);
72
73    if query_has_limit_or_offset(query) && !query_has_order_by(query) {
74        *violations += 1;
75    }
76}
77
78fn check_set_expr(set_expr: &SetExpr, violations: &mut usize) {
79    match set_expr {
80        SetExpr::Select(select) => check_select(select, violations),
81        SetExpr::Query(query) => check_query(query, violations),
82        SetExpr::SetOperation { left, right, .. } => {
83            check_set_expr(left, violations);
84            check_set_expr(right, violations);
85        }
86        SetExpr::Insert(statement)
87        | SetExpr::Update(statement)
88        | SetExpr::Delete(statement)
89        | SetExpr::Merge(statement) => check_statement(statement, violations),
90        _ => {}
91    }
92}
93
94fn check_select(select: &Select, violations: &mut usize) {
95    for table in &select.from {
96        check_table_factor(&table.relation, violations);
97        for join in &table.joins {
98            check_table_factor(&join.relation, violations);
99            if let Some(on_expr) = join_on_expr(&join.join_operator) {
100                check_expr_for_subqueries(on_expr, violations);
101            }
102        }
103    }
104
105    for item in &select.projection {
106        if let sqlparser::ast::SelectItem::UnnamedExpr(expr)
107        | sqlparser::ast::SelectItem::ExprWithAlias { expr, .. } = item
108        {
109            check_expr_for_subqueries(expr, violations);
110        }
111    }
112
113    if let Some(prewhere) = &select.prewhere {
114        check_expr_for_subqueries(prewhere, violations);
115    }
116
117    if let Some(selection) = &select.selection {
118        check_expr_for_subqueries(selection, violations);
119    }
120
121    if let sqlparser::ast::GroupByExpr::Expressions(exprs, _) = &select.group_by {
122        for expr in exprs {
123            check_expr_for_subqueries(expr, violations);
124        }
125    }
126
127    if let Some(having) = &select.having {
128        check_expr_for_subqueries(having, violations);
129    }
130
131    if let Some(qualify) = &select.qualify {
132        check_expr_for_subqueries(qualify, violations);
133    }
134
135    for order_expr in &select.sort_by {
136        check_expr_for_subqueries(&order_expr.expr, violations);
137    }
138}
139
140fn check_table_factor(table_factor: &TableFactor, violations: &mut usize) {
141    match table_factor {
142        TableFactor::Derived { subquery, .. } => check_query(subquery, violations),
143        TableFactor::NestedJoin {
144            table_with_joins, ..
145        } => {
146            check_table_factor(&table_with_joins.relation, violations);
147            for join in &table_with_joins.joins {
148                check_table_factor(&join.relation, violations);
149                if let Some(on_expr) = join_on_expr(&join.join_operator) {
150                    check_expr_for_subqueries(on_expr, violations);
151                }
152            }
153        }
154        TableFactor::Pivot { table, .. }
155        | TableFactor::Unpivot { table, .. }
156        | TableFactor::MatchRecognize { table, .. } => check_table_factor(table, violations),
157        _ => {}
158    }
159}
160
161fn check_expr_for_subqueries(expr: &Expr, violations: &mut usize) {
162    match expr {
163        Expr::Subquery(query)
164        | Expr::Exists {
165            subquery: query, ..
166        } => check_query(query, violations),
167        Expr::InSubquery {
168            expr: inner,
169            subquery,
170            ..
171        } => {
172            check_expr_for_subqueries(inner, violations);
173            check_query(subquery, violations);
174        }
175        Expr::BinaryOp { left, right, .. }
176        | Expr::AnyOp { left, right, .. }
177        | Expr::AllOp { left, right, .. } => {
178            check_expr_for_subqueries(left, violations);
179            check_expr_for_subqueries(right, violations);
180        }
181        Expr::UnaryOp { expr: inner, .. }
182        | Expr::Nested(inner)
183        | Expr::IsNull(inner)
184        | Expr::IsNotNull(inner)
185        | Expr::Cast { expr: inner, .. } => check_expr_for_subqueries(inner, violations),
186        Expr::InList { expr, list, .. } => {
187            check_expr_for_subqueries(expr, violations);
188            for item in list {
189                check_expr_for_subqueries(item, violations);
190            }
191        }
192        Expr::Between {
193            expr, low, high, ..
194        } => {
195            check_expr_for_subqueries(expr, violations);
196            check_expr_for_subqueries(low, violations);
197            check_expr_for_subqueries(high, violations);
198        }
199        Expr::Case {
200            operand,
201            conditions,
202            else_result,
203            ..
204        } => {
205            if let Some(operand) = operand {
206                check_expr_for_subqueries(operand, violations);
207            }
208            for when in conditions {
209                check_expr_for_subqueries(&when.condition, violations);
210                check_expr_for_subqueries(&when.result, violations);
211            }
212            if let Some(otherwise) = else_result {
213                check_expr_for_subqueries(otherwise, violations);
214            }
215        }
216        Expr::Function(function) => {
217            if let FunctionArguments::List(arguments) = &function.args {
218                for arg in &arguments.args {
219                    match arg {
220                        FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
221                        | FunctionArg::Named {
222                            arg: FunctionArgExpr::Expr(expr),
223                            ..
224                        } => check_expr_for_subqueries(expr, violations),
225                        _ => {}
226                    }
227                }
228            }
229
230            if let Some(filter) = &function.filter {
231                check_expr_for_subqueries(filter, violations);
232            }
233
234            for order_expr in &function.within_group {
235                check_expr_for_subqueries(&order_expr.expr, violations);
236            }
237
238            if let Some(WindowType::WindowSpec(spec)) = &function.over {
239                for expr in &spec.partition_by {
240                    check_expr_for_subqueries(expr, violations);
241                }
242                for order_expr in &spec.order_by {
243                    check_expr_for_subqueries(&order_expr.expr, violations);
244                }
245            }
246        }
247        _ => {}
248    }
249}
250
251fn query_has_order_by(query: &Query) -> bool {
252    let Some(order_by) = &query.order_by else {
253        return false;
254    };
255
256    match &order_by.kind {
257        OrderByKind::Expressions(order_exprs) => !order_exprs.is_empty(),
258        OrderByKind::All(_) => true,
259    }
260}
261
262fn query_has_limit_or_offset(query: &Query) -> bool {
263    match &query.limit_clause {
264        Some(LimitClause::LimitOffset { limit, offset, .. }) => limit.is_some() || offset.is_some(),
265        Some(LimitClause::OffsetCommaLimit { .. }) => true,
266        None => false,
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use crate::parser::parse_sql;
274
275    fn run(sql: &str) -> Vec<Issue> {
276        let statements = parse_sql(sql).expect("parse");
277        let rule = LimitOffsetWithoutOrderBy;
278        statements
279            .iter()
280            .enumerate()
281            .flat_map(|(index, statement)| {
282                rule.check(
283                    statement,
284                    &LintContext {
285                        sql,
286                        statement_range: 0..sql.len(),
287                        statement_index: index,
288                    },
289                )
290            })
291            .collect()
292    }
293
294    // --- Edge cases adopted from sqlfluff AM09 ---
295
296    #[test]
297    fn fails_limit_without_order_by() {
298        let issues = run("SELECT * FROM foo LIMIT 10");
299        assert_eq!(issues.len(), 1);
300        assert_eq!(issues[0].code, issue_codes::LINT_AM_009);
301    }
302
303    #[test]
304    fn fails_limit_and_offset_without_order_by() {
305        let issues = run("SELECT * FROM foo LIMIT 10 OFFSET 5");
306        assert_eq!(issues.len(), 1);
307    }
308
309    #[test]
310    fn passes_limit_with_order_by() {
311        let issues = run("SELECT * FROM foo ORDER BY id LIMIT 10");
312        assert!(issues.is_empty());
313    }
314
315    #[test]
316    fn passes_limit_and_offset_with_order_by() {
317        let issues = run("SELECT * FROM foo ORDER BY id LIMIT 10 OFFSET 5");
318        assert!(issues.is_empty());
319    }
320
321    #[test]
322    fn passes_without_limit_or_offset() {
323        let issues = run("SELECT * FROM foo");
324        assert!(issues.is_empty());
325    }
326
327    #[test]
328    fn fails_limit_in_subquery_without_order_by() {
329        let issues = run("SELECT * FROM (SELECT * FROM foo LIMIT 10) subquery");
330        assert_eq!(issues.len(), 1);
331    }
332
333    #[test]
334    fn passes_limit_in_subquery_with_order_by() {
335        let issues = run("SELECT * FROM (SELECT * FROM foo ORDER BY id LIMIT 10) subquery");
336        assert!(issues.is_empty());
337    }
338
339    #[test]
340    fn fails_limit_in_cte_without_order_by() {
341        let issues = run("WITH cte AS (SELECT * FROM foo LIMIT 10) SELECT * FROM cte");
342        assert_eq!(issues.len(), 1);
343    }
344
345    #[test]
346    fn passes_fetch_without_order_by() {
347        let issues = run("SELECT * FROM foo FETCH FIRST 10 ROWS ONLY");
348        assert!(issues.is_empty());
349    }
350
351    #[test]
352    fn passes_top_without_order_by() {
353        let issues = run("SELECT TOP 10 * FROM foo");
354        assert!(issues.is_empty());
355    }
356}