1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{Expr, Query, Select, SelectItem, SetExpr, Statement, TableFactor, Value};
3
4use crate::capitalisation::{is_word_char, SkipMap};
5
6pub struct ZeroLimitClause;
7
8impl Rule for ZeroLimitClause {
9 fn name(&self) -> &'static str {
10 "Structure/ZeroLimitClause"
11 }
12
13 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
14 if !ctx.parse_errors.is_empty() {
15 return Vec::new();
16 }
17
18 let mut diags = Vec::new();
19 let mut limit_counter: usize = 0;
23
24 for stmt in &ctx.statements {
25 if let Statement::Query(query) = stmt {
26 check_query(query, ctx, &mut limit_counter, &mut diags);
27 }
28 }
29
30 diags
31 }
32}
33
34fn check_query(
37 query: &Query,
38 ctx: &FileContext,
39 limit_counter: &mut usize,
40 diags: &mut Vec<Diagnostic>,
41) {
42 if let Some(with) = &query.with {
44 for cte in &with.cte_tables {
45 check_query(&cte.query, ctx, limit_counter, diags);
46 }
47 }
48
49 if let Some(limit_expr) = &query.limit {
51 if is_zero_literal(limit_expr) {
52 let occurrence = *limit_counter;
53 *limit_counter += 1;
54 let (line, col) = find_nth_keyword_pos(&ctx.source, "LIMIT", occurrence);
55 diags.push(Diagnostic {
56 rule: "Structure/ZeroLimitClause",
57 message: "LIMIT 0 always returns an empty result set".to_string(),
58 line,
59 col,
60 });
61 }
62 }
63
64 check_set_expr(&query.body, ctx, limit_counter, diags);
67}
68
69fn check_set_expr(
70 expr: &SetExpr,
71 ctx: &FileContext,
72 limit_counter: &mut usize,
73 diags: &mut Vec<Diagnostic>,
74) {
75 match expr {
76 SetExpr::Select(sel) => {
77 check_select(sel, ctx, limit_counter, diags);
78 }
79 SetExpr::Query(inner) => {
80 check_query(inner, ctx, limit_counter, diags);
81 }
82 SetExpr::SetOperation { left, right, .. } => {
83 check_set_expr(left, ctx, limit_counter, diags);
84 check_set_expr(right, ctx, limit_counter, diags);
85 }
86 _ => {}
87 }
88}
89
90fn check_select(
91 select: &Select,
92 ctx: &FileContext,
93 limit_counter: &mut usize,
94 diags: &mut Vec<Diagnostic>,
95) {
96 for table_with_joins in &select.from {
98 check_table_factor(&table_with_joins.relation, ctx, limit_counter, diags);
99 for join in &table_with_joins.joins {
100 check_table_factor(&join.relation, ctx, limit_counter, diags);
101 }
102 }
103
104 if let Some(selection) = &select.selection {
106 check_expr_for_subqueries(selection, ctx, limit_counter, diags);
107 }
108
109 for item in &select.projection {
111 if let SelectItem::UnnamedExpr(e) | SelectItem::ExprWithAlias { expr: e, .. } = item {
112 check_expr_for_subqueries(e, ctx, limit_counter, diags);
113 }
114 }
115}
116
117fn check_table_factor(
118 factor: &TableFactor,
119 ctx: &FileContext,
120 limit_counter: &mut usize,
121 diags: &mut Vec<Diagnostic>,
122) {
123 if let TableFactor::Derived { subquery, .. } = factor {
124 check_query(subquery, ctx, limit_counter, diags);
125 }
126}
127
128fn check_expr_for_subqueries(
129 expr: &Expr,
130 ctx: &FileContext,
131 limit_counter: &mut usize,
132 diags: &mut Vec<Diagnostic>,
133) {
134 match expr {
135 Expr::Subquery(q) => check_query(q, ctx, limit_counter, diags),
136 Expr::InSubquery { subquery, .. } => check_query(subquery, ctx, limit_counter, diags),
137 Expr::Exists { subquery, .. } => check_query(subquery, ctx, limit_counter, diags),
138 Expr::BinaryOp { left, right, .. } => {
139 check_expr_for_subqueries(left, ctx, limit_counter, diags);
140 check_expr_for_subqueries(right, ctx, limit_counter, diags);
141 }
142 _ => {}
143 }
144}
145
146fn is_zero_literal(expr: &Expr) -> bool {
150 if let Expr::Value(Value::Number(s, _)) = expr {
151 s == "0"
152 } else {
153 false
154 }
155}
156
157fn find_nth_keyword_pos(source: &str, keyword: &str, nth: usize) -> (usize, usize) {
161 let bytes = source.as_bytes();
162 let len = bytes.len();
163 let skip_map = SkipMap::build(source);
164 let kw_upper: Vec<u8> = keyword.bytes().map(|b| b.to_ascii_uppercase()).collect();
165 let kw_len = kw_upper.len();
166
167 let mut count = 0usize;
168 let mut i = 0usize;
169
170 while i + kw_len <= len {
171 if !skip_map.is_code(i) {
172 i += 1;
173 continue;
174 }
175
176 let before_ok = i == 0 || !is_word_char(bytes[i - 1]);
177 if !before_ok {
178 i += 1;
179 continue;
180 }
181
182 let matches = bytes[i..i + kw_len]
183 .iter()
184 .zip(kw_upper.iter())
185 .all(|(a, b)| a.eq_ignore_ascii_case(b));
186
187 if matches {
188 let after = i + kw_len;
189 let after_ok = after >= len || !is_word_char(bytes[after]);
190 let all_code = (i..i + kw_len).all(|k| skip_map.is_code(k));
191
192 if after_ok && all_code {
193 if count == nth {
194 return offset_to_line_col(source, i);
195 }
196 count += 1;
197 }
198 }
199
200 i += 1;
201 }
202
203 (1, 1)
204}
205
206fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
208 let before = &source[..offset];
209 let line = before.chars().filter(|&c| c == '\n').count() + 1;
210 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
211 (line, col)
212}