sqrust_rules/ambiguous/
having_without_group_by.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{GroupByExpr, Query, SetExpr, Statement, TableFactor};
3
4pub struct HavingWithoutGroupBy;
5
6impl Rule for HavingWithoutGroupBy {
7 fn name(&self) -> &'static str {
8 "Ambiguous/HavingWithoutGroupBy"
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 if let Statement::Query(query) = stmt {
19 check_query(query, &ctx.source, self.name(), &mut diags);
20 }
21 }
22 diags
23 }
24}
25
26fn check_query(query: &Query, source: &str, rule: &'static str, diags: &mut Vec<Diagnostic>) {
27 if let Some(with) = &query.with {
29 for cte in &with.cte_tables {
30 check_query(&cte.query, source, rule, diags);
31 }
32 }
33 check_set_expr(&query.body, source, rule, diags);
34}
35
36fn check_set_expr(expr: &SetExpr, source: &str, rule: &'static str, diags: &mut Vec<Diagnostic>) {
37 match expr {
38 SetExpr::Select(sel) => {
39 let has_having = sel.having.is_some();
40
41 let has_group_by = match &sel.group_by {
44 GroupByExpr::All(_) => true,
45 GroupByExpr::Expressions(exprs, _) => !exprs.is_empty(),
46 };
47
48 if has_having && !has_group_by {
49 let (line, col) = find_keyword_position(source, "HAVING");
51 diags.push(Diagnostic {
52 rule,
53 message: "HAVING without GROUP BY; did you mean WHERE?".to_string(),
54 line,
55 col,
56 });
57 }
58
59 for table in &sel.from {
61 recurse_table_factor(&table.relation, source, rule, diags);
62 for join in &table.joins {
63 recurse_table_factor(&join.relation, source, rule, diags);
64 }
65 }
66 }
67 SetExpr::SetOperation { left, right, .. } => {
68 check_set_expr(left, source, rule, diags);
69 check_set_expr(right, source, rule, diags);
70 }
71 SetExpr::Query(inner) => {
72 check_query(inner, source, rule, diags);
73 }
74 _ => {}
75 }
76}
77
78fn recurse_table_factor(
79 tf: &TableFactor,
80 source: &str,
81 rule: &'static str,
82 diags: &mut Vec<Diagnostic>,
83) {
84 if let TableFactor::Derived { subquery, .. } = tf {
85 check_query(subquery, source, rule, diags);
86 }
87}
88
89fn find_keyword_position(source: &str, keyword: &str) -> (usize, usize) {
92 let upper = source.to_uppercase();
93 let kw_upper = keyword.to_uppercase();
94 let bytes = upper.as_bytes();
95 let kw_bytes = kw_upper.as_bytes();
96 let kw_len = kw_bytes.len();
97
98 let mut i = 0;
99 while i + kw_len <= bytes.len() {
100 if bytes[i..i + kw_len] == *kw_bytes {
101 let before_ok = i == 0
102 || (!bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_');
103 let after = i + kw_len;
104 let after_ok = after >= bytes.len()
105 || (!bytes[after].is_ascii_alphanumeric() && bytes[after] != b'_');
106 if before_ok && after_ok {
107 return offset_to_line_col(source, i);
108 }
109 }
110 i += 1;
111 }
112 (1, 1)
113}
114
115fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
117 let mut line = 1usize;
118 let mut col = 1usize;
119 for (i, ch) in source.char_indices() {
120 if i == offset {
121 break;
122 }
123 if ch == '\n' {
124 line += 1;
125 col = 1;
126 } else {
127 col += 1;
128 }
129 }
130 (line, col)
131}