sqruff_lib/rules/aliasing/
al07.rs1use std::iter::once;
2
3use ahash::{AHashMap, AHashSet};
4use itertools::chain;
5use smol_str::ToSmolStr;
6use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
7use sqruff_lib_core::lint_fix::LintFix;
8use sqruff_lib_core::parser::segments::{ErasedSegment, SegmentBuilder, Tables};
9
10use crate::core::config::Value;
11use crate::core::rules::context::RuleContext;
12use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
13use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
14use crate::utils::functional::context::FunctionalContext;
15
16#[derive(Debug)]
17struct TableAliasInfo {
18 table_ref: ErasedSegment,
19 whitespace_ref: Option<ErasedSegment>,
20 alias_exp_ref: ErasedSegment,
21 alias_identifier_ref: Option<ErasedSegment>,
22}
23
24#[derive(Debug, Clone, Default)]
25pub struct RuleAL07 {
26 force_enable: bool,
27}
28
29impl RuleAL07 {
30 fn lint_aliases_in_join(
31 &self,
32 tables: &Tables,
33 base_table: Option<ErasedSegment>,
34 from_expression_elements: Vec<ErasedSegment>,
35 column_reference_segments: Vec<ErasedSegment>,
36 segment: ErasedSegment,
37 ) -> Vec<LintResult> {
38 let mut violation_buff = Vec::new();
39 let to_check = self.filter_table_expressions(base_table, from_expression_elements);
40
41 let mut table_counts = AHashMap::new();
42 for ai in &to_check {
43 *table_counts
44 .entry(ai.table_ref.raw().to_smolstr())
45 .or_insert(0) += 1;
46 }
47
48 let mut table_aliases: AHashMap<_, AHashSet<_>> = AHashMap::new();
49 for ai in &to_check {
50 if let (table_ref, Some(alias_identifier_ref)) =
51 (&ai.table_ref, &ai.alias_identifier_ref)
52 {
53 table_aliases
54 .entry(table_ref.raw().to_smolstr())
55 .or_default()
56 .insert(alias_identifier_ref.raw().to_smolstr());
57 }
58 }
59
60 for alias_info in to_check {
61 if let (table_ref, Some(alias_identifier_ref)) =
62 (&alias_info.table_ref, &alias_info.alias_identifier_ref)
63 {
64 let raw_table = table_ref.raw().to_smolstr();
66 if table_counts.get(&raw_table).unwrap_or(&0) > &1
67 && table_aliases
68 .get(&raw_table)
69 .is_some_and(|aliases| aliases.len() > 1)
70 {
71 continue;
72 }
73
74 let select_clause = segment
75 .child(const { &SyntaxSet::new(&[SyntaxKind::SelectClause]) })
76 .unwrap();
77 let mut ids_refs = Vec::new();
78
79 let alias_name = alias_identifier_ref.raw();
80 if !alias_name.is_empty() {
81 for alias_with_column in select_clause.recursive_crawl(
83 const { &SyntaxSet::new(&[SyntaxKind::ObjectReference]) },
84 true,
85 &SyntaxSet::EMPTY,
86 true,
87 ) {
88 if let Some(used_alias_ref) = alias_with_column.child(
89 const {
90 &SyntaxSet::new(&[
91 SyntaxKind::Identifier,
92 SyntaxKind::NakedIdentifier,
93 ])
94 },
95 ) && used_alias_ref.raw() == alias_name
96 {
97 ids_refs.push(used_alias_ref);
98 }
99 }
100
101 for exp_ref in column_reference_segments.clone() {
103 if let Some(used_alias_ref) = exp_ref.child(
104 const {
105 &SyntaxSet::new(&[
106 SyntaxKind::Identifier,
107 SyntaxKind::NakedIdentifier,
108 ])
109 },
110 ) && used_alias_ref.raw() == alias_name
111 && exp_ref
112 .child(const { &SyntaxSet::new(&[SyntaxKind::Dot]) })
113 .is_some()
114 {
115 ids_refs.push(used_alias_ref);
116 }
117 }
118 }
119
120 let mut fixes = Vec::new();
122
123 fixes.push(LintFix::delete(alias_info.alias_exp_ref));
124
125 if let Some(whitespace_ref) = &alias_info.whitespace_ref {
126 fixes.push(LintFix::delete(whitespace_ref.clone()));
127 }
128
129 for alias in ids_refs.iter().chain(once(alias_identifier_ref)) {
130 let tmp = table_ref.raw();
131 let identifier_parts: Vec<_> = tmp.split('.').collect();
132 let mut edits = Vec::new();
133 for (i, part) in identifier_parts.iter().enumerate() {
134 if i > 0 {
135 edits.push(SegmentBuilder::symbol(tables.next_id(), "."));
136 }
137 edits.push(
138 SegmentBuilder::token(tables.next_id(), part, SyntaxKind::Identifier)
139 .finish(),
140 );
141 }
142 fixes.push(LintFix::replace(
143 alias.clone(),
144 edits,
145 Some(vec![table_ref.clone()]),
146 ));
147 }
148
149 violation_buff.push(LintResult::new(
150 alias_info.alias_identifier_ref,
151 fixes,
152 "Avoid aliases in from clauses and join conditions."
153 .to_owned()
154 .into(),
155 None,
156 ));
157 }
158 }
159
160 violation_buff
161 }
162
163 fn filter_table_expressions(
164 &self,
165 base_table: Option<ErasedSegment>,
166 from_expression_elements: Vec<ErasedSegment>,
167 ) -> Vec<TableAliasInfo> {
168 let mut acc = Vec::new();
169
170 for from_expression in from_expression_elements {
171 let table_expression =
172 from_expression.child(const { &SyntaxSet::new(&[SyntaxKind::TableExpression]) });
173 let Some(table_expression) = table_expression else {
174 continue;
175 };
176
177 let table_ref =
178 table_expression.child(const { &SyntaxSet::new(&[SyntaxKind::ObjectReference, SyntaxKind::TableReference]) });
179 let Some(table_ref) = table_ref else {
180 continue;
181 };
182
183 if let Some(ref base_table) = base_table
184 && base_table.raw() == table_ref.raw()
185 && base_table != &table_ref
186 {
187 continue;
188 }
189
190 let whitespace_ref =
191 from_expression.child(const { &SyntaxSet::new(&[SyntaxKind::Whitespace]) });
192
193 let alias_exp_ref =
194 from_expression.child(const { &SyntaxSet::new(&[SyntaxKind::AliasExpression]) });
195 let Some(alias_exp_ref) = alias_exp_ref else {
196 continue;
197 };
198
199 let alias_identifier_ref = alias_exp_ref.child(
200 const { &SyntaxSet::new(&[SyntaxKind::Identifier, SyntaxKind::NakedIdentifier]) },
201 );
202
203 acc.push(TableAliasInfo {
204 table_ref,
205 whitespace_ref,
206 alias_exp_ref,
207 alias_identifier_ref,
208 });
209 }
210
211 acc
212 }
213}
214
215impl Rule for RuleAL07 {
216 fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
217 Ok(RuleAL07 {
218 force_enable: _config["force_enable"].as_bool().unwrap(),
219 }
220 .erased())
221 }
222
223 fn name(&self) -> &'static str {
224 "aliasing.forbid"
225 }
226 fn description(&self) -> &'static str {
227 "Avoid table aliases in from clauses and join conditions."
228 }
229
230 fn long_description(&self) -> &'static str {
231 r#"
232**Anti-pattern**
233
234In this example, alias o is used for the orders table, and c is used for customers table.
235
236```sql
237SELECT
238 COUNT(o.customer_id) as order_amount,
239 c.name
240FROM orders as o
241JOIN customers as c on o.id = c.user_id
242```
243
244**Best practice**
245
246Avoid aliases.
247
248```sql
249SELECT
250 COUNT(orders.customer_id) as order_amount,
251 customers.name
252FROM orders
253JOIN customers on orders.id = customers.user_id
254
255-- Self-join will not raise issue
256
257SELECT
258 table1.a,
259 table_alias.b,
260FROM
261 table1
262 LEFT JOIN table1 AS table_alias ON
263 table1.foreign_key = table_alias.foreign_key
264```
265"#
266 }
267
268 fn groups(&self) -> &'static [RuleGroups] {
269 &[RuleGroups::All, RuleGroups::Aliasing]
270 }
271
272 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
273 if !self.force_enable {
274 return Vec::new();
275 }
276
277 let children = FunctionalContext::new(context).segment().children_all();
278 let from_clause_segment = children
279 .filter(|it: &ErasedSegment| it.is_type(SyntaxKind::FromClause))
280 .head();
281
282 let base_table = from_clause_segment
283 .children_where(|it| it.is_type(SyntaxKind::FromExpression))
284 .head()
285 .children_where(|it| it.is_type(SyntaxKind::FromExpressionElement))
286 .head()
287 .children_where(|it| it.is_type(SyntaxKind::TableExpression))
288 .head()
289 .children_where(|it| {
290 it.is_type(SyntaxKind::ObjectReference) || it.is_type(SyntaxKind::TableReference)
291 });
292
293 if base_table.is_empty() {
294 return Vec::new();
295 }
296
297 let mut from_expression_elements = Vec::new();
298 let mut column_reference_segments = Vec::new();
299
300 let after_from_clause = children.after(&from_clause_segment[0]);
301 for clause in chain(from_clause_segment, after_from_clause) {
302 for from_expression_element in clause.recursive_crawl(
303 const { &SyntaxSet::new(&[SyntaxKind::FromExpressionElement]) },
304 true,
305 &SyntaxSet::EMPTY,
306 true,
307 ) {
308 from_expression_elements.push(from_expression_element);
309 }
310
311 for from_expression_element in clause.recursive_crawl(
312 const { &SyntaxSet::new(&[SyntaxKind::ColumnReference]) },
313 true,
314 &SyntaxSet::EMPTY,
315 true,
316 ) {
317 column_reference_segments.push(from_expression_element);
318 }
319 }
320
321 self.lint_aliases_in_join(
322 context.tables,
323 base_table.first().cloned(),
324 from_expression_elements,
325 column_reference_segments,
326 context.segment.clone(),
327 )
328 }
329
330 fn is_fix_compatible(&self) -> bool {
331 true
332 }
333
334 fn crawl_behaviour(&self) -> Crawler {
335 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
336 }
337}