1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{Expr, Query, Select, SelectItem, SetExpr, Statement, TableFactor, Value};
3
4pub struct RedundantBetween;
5
6impl Rule for RedundantBetween {
7 fn name(&self) -> &'static str {
8 "Ambiguous/RedundantBetween"
9 }
10
11 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12 if !ctx.parse_errors.is_empty() {
13 return Vec::new();
14 }
15
16 let mut diags = Vec::new();
17 for stmt in &ctx.statements {
18 collect_from_statement(stmt, ctx, &mut diags);
19 }
20 diags
21 }
22}
23
24fn collect_from_statement(stmt: &Statement, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
25 if let Statement::Query(query) = stmt {
26 collect_from_query(query, ctx, diags);
27 }
28}
29
30fn collect_from_query(query: &Query, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
31 if let Some(with) = &query.with {
32 for cte in &with.cte_tables {
33 collect_from_query(&cte.query, ctx, diags);
34 }
35 }
36 collect_from_set_expr(&query.body, ctx, diags);
37}
38
39fn collect_from_set_expr(expr: &SetExpr, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
40 match expr {
41 SetExpr::Select(select) => {
42 collect_from_select(select, ctx, diags);
43 }
44 SetExpr::Query(inner) => {
45 collect_from_query(inner, ctx, diags);
46 }
47 SetExpr::SetOperation { left, right, .. } => {
48 collect_from_set_expr(left, ctx, diags);
49 collect_from_set_expr(right, ctx, diags);
50 }
51 _ => {}
52 }
53}
54
55fn collect_from_select(select: &Select, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
56 for item in &select.projection {
58 if let SelectItem::UnnamedExpr(e) | SelectItem::ExprWithAlias { expr: e, .. } = item {
59 check_expr(e, ctx, diags);
60 }
61 }
62
63 for twj in &select.from {
65 collect_from_table_factor(&twj.relation, ctx, diags);
66 for join in &twj.joins {
67 collect_from_table_factor(&join.relation, ctx, diags);
68 }
69 }
70
71 if let Some(selection) = &select.selection {
73 check_expr(selection, ctx, diags);
74 }
75
76 if let Some(having) = &select.having {
78 check_expr(having, ctx, diags);
79 }
80}
81
82fn collect_from_table_factor(
83 factor: &TableFactor,
84 ctx: &FileContext,
85 diags: &mut Vec<Diagnostic>,
86) {
87 if let TableFactor::Derived { subquery, .. } = factor {
88 collect_from_query(subquery, ctx, diags);
89 }
90}
91
92fn is_safe_bound(expr: &Expr) -> bool {
95 matches!(
96 expr,
97 Expr::Value(Value::Number(_, _))
98 | Expr::Value(Value::SingleQuotedString(_))
99 | Expr::Value(Value::Boolean(_))
100 | Expr::Identifier(_)
101 )
102}
103
104fn check_expr(expr: &Expr, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
105 match expr {
106 Expr::Between {
107 expr: inner,
108 negated,
109 low,
110 high,
111 } => {
112 check_expr(inner, ctx, diags);
114 check_expr(low, ctx, diags);
115 check_expr(high, ctx, diags);
116
117 if is_safe_bound(low) && is_safe_bound(high) && low == high {
120 let keyword = if *negated { "NOT BETWEEN" } else { "BETWEEN" };
121 let (line, col) = find_keyword_position(&ctx.source, keyword);
122 let message = if *negated {
123 "NOT BETWEEN with identical bounds; use != instead".to_string()
124 } else {
125 "BETWEEN with identical bounds; use = instead".to_string()
126 };
127 diags.push(Diagnostic {
128 rule: "Ambiguous/RedundantBetween",
129 message,
130 line,
131 col,
132 });
133 }
134 }
135
136 Expr::BinaryOp { left, right, .. } => {
138 check_expr(left, ctx, diags);
139 check_expr(right, ctx, diags);
140 }
141 Expr::UnaryOp { expr: inner, .. } => {
142 check_expr(inner, ctx, diags);
143 }
144 Expr::Nested(inner) => {
145 check_expr(inner, ctx, diags);
146 }
147 Expr::Case {
148 operand,
149 conditions,
150 results,
151 else_result,
152 } => {
153 if let Some(op) = operand {
154 check_expr(op, ctx, diags);
155 }
156 for cond in conditions {
157 check_expr(cond, ctx, diags);
158 }
159 for result in results {
160 check_expr(result, ctx, diags);
161 }
162 if let Some(else_e) = else_result {
163 check_expr(else_e, ctx, diags);
164 }
165 }
166 Expr::InList { expr: inner, list, .. } => {
167 check_expr(inner, ctx, diags);
168 for e in list {
169 check_expr(e, ctx, diags);
170 }
171 }
172 Expr::IsNull(inner) | Expr::IsNotNull(inner) => {
173 check_expr(inner, ctx, diags);
174 }
175 Expr::Subquery(q) | Expr::InSubquery { subquery: q, .. } | Expr::Exists { subquery: q, .. } => {
176 collect_from_query(q, ctx, diags);
177 }
178 _ => {}
179 }
180}
181
182fn find_keyword_position(source: &str, keyword: &str) -> (usize, usize) {
185 let upper = source.to_uppercase();
186 let kw_upper = keyword.to_uppercase();
187 let kw_len = kw_upper.len();
188 let bytes = upper.as_bytes();
189 let len = bytes.len();
190
191 let mut pos = 0;
192 while pos + kw_len <= len {
193 if let Some(rel) = upper[pos..].find(kw_upper.as_str()) {
194 let abs = pos + rel;
195
196 let before_ok = abs == 0 || {
197 let b = bytes[abs - 1];
198 !b.is_ascii_alphanumeric() && b != b'_'
199 };
200 let after = abs + kw_len;
201 let after_ok = after >= len || {
202 let b = bytes[after];
203 !b.is_ascii_alphanumeric() && b != b'_'
204 };
205
206 if before_ok && after_ok {
207 return line_col(source, abs);
208 }
209
210 pos = abs + 1;
211 } else {
212 break;
213 }
214 }
215
216 (1, 1)
217}
218
219fn line_col(source: &str, offset: usize) -> (usize, usize) {
221 let before = &source[..offset];
222 let line = before.chars().filter(|&c| c == '\n').count() + 1;
223 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
224 (line, col)
225}