flowscope_core/linter/rules/
am_009.rs1use 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 #[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}