1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{
3 Expr, Function, FunctionArg, FunctionArgExpr, FunctionArguments, OrderByExpr, Query, Select,
4 SelectItem, SetExpr, Statement, TableFactor, WindowType,
5};
6
7pub struct WindowWithoutOrderBy;
8
9impl Rule for WindowWithoutOrderBy {
10 fn name(&self) -> &'static str {
11 "WindowWithoutOrderBy"
12 }
13
14 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
15 if !ctx.parse_errors.is_empty() {
16 return Vec::new();
17 }
18
19 let mut diags = Vec::new();
20
21 for stmt in &ctx.statements {
22 if let Statement::Query(query) = stmt {
23 check_query(query, ctx, &mut diags);
24 }
25 }
26
27 diags
28 }
29}
30
31fn check_query(query: &Query, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
34 if let Some(with) = &query.with {
36 for cte in &with.cte_tables {
37 check_query(&cte.query, ctx, diags);
38 }
39 }
40
41 check_set_expr(&query.body, ctx, diags);
42
43 if let Some(order_by) = &query.order_by {
45 for ob in &order_by.exprs {
46 check_order_by_expr(ob, ctx, diags);
47 }
48 }
49}
50
51fn check_set_expr(expr: &SetExpr, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
52 match expr {
53 SetExpr::Select(sel) => {
54 check_select(sel, ctx, diags);
55 }
56 SetExpr::Query(inner) => {
57 check_query(inner, ctx, diags);
58 }
59 SetExpr::SetOperation { left, right, .. } => {
60 check_set_expr(left, ctx, diags);
61 check_set_expr(right, ctx, diags);
62 }
63 _ => {}
64 }
65}
66
67fn check_select(sel: &Select, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
68 for item in &sel.projection {
70 if let SelectItem::UnnamedExpr(e) | SelectItem::ExprWithAlias { expr: e, .. } = item {
71 check_expr(e, ctx, diags);
72 }
73 }
74
75 if let Some(selection) = &sel.selection {
77 check_expr(selection, ctx, diags);
78 }
79
80 if let Some(having) = &sel.having {
82 check_expr(having, ctx, diags);
83 }
84
85 for twj in &sel.from {
87 check_table_factor(&twj.relation, ctx, diags);
88 for join in &twj.joins {
89 check_table_factor(&join.relation, ctx, diags);
90 }
91 }
92}
93
94fn check_table_factor(tf: &TableFactor, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
95 if let TableFactor::Derived { subquery, .. } = tf {
96 check_query(subquery, ctx, diags);
97 }
98}
99
100fn check_order_by_expr(ob: &OrderByExpr, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
101 check_expr(&ob.expr, ctx, diags);
102}
103
104fn check_expr(expr: &Expr, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
107 match expr {
108 Expr::Function(func) => {
109 check_function(func, ctx, diags);
110 }
111 Expr::BinaryOp { left, right, .. } => {
112 check_expr(left, ctx, diags);
113 check_expr(right, ctx, diags);
114 }
115 Expr::UnaryOp { expr, .. } => {
116 check_expr(expr, ctx, diags);
117 }
118 Expr::Nested(e) => {
119 check_expr(e, ctx, diags);
120 }
121 Expr::IsNull(e) => {
122 check_expr(e, ctx, diags);
123 }
124 Expr::IsNotNull(e) => {
125 check_expr(e, ctx, diags);
126 }
127 Expr::Case {
128 operand,
129 conditions,
130 results,
131 else_result,
132 } => {
133 if let Some(op) = operand {
134 check_expr(op, ctx, diags);
135 }
136 for c in conditions {
137 check_expr(c, ctx, diags);
138 }
139 for r in results {
140 check_expr(r, ctx, diags);
141 }
142 if let Some(el) = else_result {
143 check_expr(el, ctx, diags);
144 }
145 }
146 Expr::Subquery(q) => {
147 check_query(q, ctx, diags);
148 }
149 Expr::InSubquery { subquery, .. } => {
150 check_query(subquery, ctx, diags);
151 }
152 Expr::Exists { subquery, .. } => {
153 check_query(subquery, ctx, diags);
154 }
155 _ => {}
156 }
157}
158
159fn check_function(func: &Function, ctx: &FileContext, diags: &mut Vec<Diagnostic>) {
162 if let Some(WindowType::WindowSpec(spec)) = &func.over {
163 if spec.window_frame.is_some() && spec.order_by.is_empty() {
164 let (line, col) = find_over_pos(&ctx.source);
165 diags.push(Diagnostic {
166 rule: "WindowWithoutOrderBy",
167 message: "Window function has a frame specification but no ORDER BY; results are non-deterministic".to_string(),
168 line,
169 col,
170 });
171 }
172 }
173
174 if let FunctionArguments::List(list) = &func.args {
176 for arg in &list.args {
177 let fae = match arg {
178 FunctionArg::Named { arg, .. }
179 | FunctionArg::ExprNamed { arg, .. }
180 | FunctionArg::Unnamed(arg) => arg,
181 };
182 if let FunctionArgExpr::Expr(e) = fae {
183 check_expr(e, ctx, diags);
184 }
185 }
186 }
187}
188
189fn find_over_pos(source: &str) -> (usize, usize) {
194 let keyword = "OVER";
195 let upper = source.to_uppercase();
196 let kw_len = keyword.len();
197 let bytes = upper.as_bytes();
198 let len = bytes.len();
199
200 let mut pos = 0;
201 while pos + kw_len <= len {
202 if let Some(rel) = upper[pos..].find(keyword) {
203 let abs = pos + rel;
204
205 let before_ok = abs == 0 || {
206 let b = bytes[abs - 1];
207 !b.is_ascii_alphanumeric() && b != b'_'
208 };
209 let after = abs + kw_len;
210 let after_ok = after >= len || {
211 let b = bytes[after];
212 !b.is_ascii_alphanumeric() && b != b'_'
213 };
214
215 if before_ok && after_ok {
216 return line_col(source, abs);
217 }
218
219 pos = abs + 1;
220 } else {
221 break;
222 }
223 }
224
225 (1, 1)
226}
227
228fn line_col(source: &str, offset: usize) -> (usize, usize) {
230 let before = &source[..offset];
231 let line = before.chars().filter(|&c| c == '\n').count() + 1;
232 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
233 (line, col)
234}