sqrust_rules/structure/
select_only_literals.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{Expr, Query, SelectItem, SetExpr, Statement};
3
4use crate::capitalisation::{is_word_char, SkipMap};
5
6pub struct SelectOnlyLiterals;
7
8impl Default for SelectOnlyLiterals {
9 fn default() -> Self {
10 SelectOnlyLiterals
11 }
12}
13
14impl Rule for SelectOnlyLiterals {
15 fn name(&self) -> &'static str {
16 "Structure/SelectOnlyLiterals"
17 }
18
19 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
20 if !ctx.parse_errors.is_empty() {
21 return Vec::new();
22 }
23
24 let mut diags = Vec::new();
25 let mut select_occurrence: usize = 0;
27
28 for stmt in &ctx.statements {
29 if let Statement::Query(query) = stmt {
30 check_query(query, ctx, &mut select_occurrence, &mut diags);
31 }
32 }
33
34 diags
35 }
36}
37
38fn check_query(
41 query: &Query,
42 ctx: &FileContext,
43 select_occurrence: &mut usize,
44 diags: &mut Vec<Diagnostic>,
45) {
46 if let Some(with) = &query.with {
48 for cte in &with.cte_tables {
49 check_query(&cte.query, ctx, select_occurrence, diags);
50 }
51 }
52
53 check_set_expr(&query.body, ctx, select_occurrence, diags);
54}
55
56fn check_set_expr(
57 expr: &SetExpr,
58 ctx: &FileContext,
59 select_occurrence: &mut usize,
60 diags: &mut Vec<Diagnostic>,
61) {
62 match expr {
63 SetExpr::Select(sel) => {
64 if sel.from.is_empty() && !sel.projection.is_empty() {
66 let all_literals = sel.projection.iter().all(|item| match item {
67 SelectItem::UnnamedExpr(e) | SelectItem::ExprWithAlias { expr: e, .. } => {
68 is_literal(e)
69 }
70 _ => false,
71 });
72
73 if all_literals {
74 let (line, col) =
75 find_keyword_pos(&ctx.source, "SELECT", *select_occurrence);
76 diags.push(Diagnostic {
77 rule: "Structure/SelectOnlyLiterals",
78 message:
79 "SELECT of only literal values with no FROM clause is likely a test/debug query"
80 .to_string(),
81 line,
82 col,
83 });
84 }
85 }
86
87 *select_occurrence += 1;
88 }
89 SetExpr::Query(inner) => {
90 check_query(inner, ctx, select_occurrence, diags);
91 }
92 SetExpr::SetOperation { left, right, .. } => {
93 check_set_expr(left, ctx, select_occurrence, diags);
94 check_set_expr(right, ctx, select_occurrence, diags);
95 }
96 _ => {}
97 }
98}
99
100fn is_literal(expr: &Expr) -> bool {
106 matches!(expr, Expr::Value(_))
107}
108
109fn find_keyword_pos(source: &str, keyword: &str, nth: usize) -> (usize, usize) {
115 let bytes = source.as_bytes();
116 let len = bytes.len();
117 let skip_map = SkipMap::build(source);
118 let kw_upper: Vec<u8> = keyword.bytes().map(|b| b.to_ascii_uppercase()).collect();
119 let kw_len = kw_upper.len();
120
121 let mut count = 0usize;
122 let mut i = 0;
123 while i + kw_len <= len {
124 if !skip_map.is_code(i) {
125 i += 1;
126 continue;
127 }
128
129 let before_ok = i == 0 || !is_word_char(bytes[i - 1]);
131 if !before_ok {
132 i += 1;
133 continue;
134 }
135
136 let matches = bytes[i..i + kw_len]
138 .iter()
139 .zip(kw_upper.iter())
140 .all(|(a, b)| a.eq_ignore_ascii_case(b));
141
142 if matches {
143 let after = i + kw_len;
145 let after_ok = after >= len || !is_word_char(bytes[after]);
146 let all_code = (i..i + kw_len).all(|k| skip_map.is_code(k));
147
148 if after_ok && all_code {
149 if count == nth {
150 return line_col(source, i);
151 }
152 count += 1;
153 }
154 }
155
156 i += 1;
157 }
158
159 (1, 1)
160}
161
162fn line_col(source: &str, offset: usize) -> (usize, usize) {
164 let before = &source[..offset];
165 let line = before.chars().filter(|&c| c == '\n').count() + 1;
166 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
167 (line, col)
168}