sqrust_rules/lint/
subquery_without_alias.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{Query, SetExpr, Statement, TableFactor};
3
4pub struct SubqueryWithoutAlias;
5
6impl Rule for SubqueryWithoutAlias {
7 fn name(&self) -> &'static str {
8 "Lint/SubqueryWithoutAlias"
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, &mut diags);
20 }
21 }
22 diags
23 }
24}
25
26fn check_query(query: &Query, source: &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, diags);
31 }
32 }
33 check_set_expr(&query.body, source, diags);
34}
35
36fn check_set_expr(expr: &SetExpr, source: &str, diags: &mut Vec<Diagnostic>) {
37 match expr {
38 SetExpr::Select(sel) => {
39 for table in &sel.from {
40 check_table_factor(&table.relation, source, diags);
41 for join in &table.joins {
42 check_table_factor(&join.relation, source, diags);
43 }
44 }
45 }
46 SetExpr::SetOperation { left, right, .. } => {
47 check_set_expr(left, source, diags);
48 check_set_expr(right, source, diags);
49 }
50 SetExpr::Query(inner) => {
51 check_query(inner, source, diags);
52 }
53 _ => {}
54 }
55}
56
57fn check_table_factor(tf: &TableFactor, source: &str, diags: &mut Vec<Diagnostic>) {
58 if let TableFactor::Derived {
59 subquery, alias, ..
60 } = tf
61 {
62 if alias.is_none() {
63 let (line, col) = find_subquery_position(source, subquery);
65 diags.push(Diagnostic {
66 rule: "Lint/SubqueryWithoutAlias",
67 message: "Derived table (subquery in FROM) has no alias; add an alias for portability".to_string(),
68 line,
69 col,
70 });
71 }
72 check_query(subquery, source, diags);
74 }
75}
76
77fn find_subquery_position(source: &str, _subquery: &Query) -> (usize, usize) {
80 let source_upper = source.to_uppercase();
82 let needle = "(SELECT";
83
84 if let Some(pos) = source_upper.find(needle) {
86 return offset_to_line_col(source, pos);
87 }
88
89 if let Some(pos) = source.find('(') {
91 return offset_to_line_col(source, pos);
92 }
93
94 (1, 1)
95}
96
97fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
99 let before = &source[..offset];
100 let line = before.chars().filter(|&c| c == '\n').count() + 1;
101 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
102 (line, col)
103}