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::base::{ErasedSegment, SegmentBuilder, Tables};
9
10use crate::core::config::Value;
11use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
12use crate::core::rules::context::RuleContext;
13use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
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 ) {
96 if used_alias_ref.raw() == alias_name {
97 ids_refs.push(used_alias_ref);
98 }
99 }
100 }
101
102 for exp_ref in column_reference_segments.clone() {
104 if let Some(used_alias_ref) = exp_ref.child(
105 const {
106 &SyntaxSet::new(&[
107 SyntaxKind::Identifier,
108 SyntaxKind::NakedIdentifier,
109 ])
110 },
111 ) {
112 if used_alias_ref.raw() == alias_name
113 && exp_ref
114 .child(const { &SyntaxSet::new(&[SyntaxKind::Dot]) })
115 .is_some()
116 {
117 ids_refs.push(used_alias_ref);
118 }
119 }
120 }
121 }
122
123 let mut fixes = Vec::new();
125
126 fixes.push(LintFix::delete(alias_info.alias_exp_ref));
127
128 if let Some(whitespace_ref) = &alias_info.whitespace_ref {
129 fixes.push(LintFix::delete(whitespace_ref.clone()));
130 }
131
132 for alias in ids_refs.iter().chain(once(alias_identifier_ref)) {
133 let tmp = table_ref.raw();
134 let identifier_parts: Vec<_> = tmp.split('.').collect();
135 let mut edits = Vec::new();
136 for (i, part) in identifier_parts.iter().enumerate() {
137 if i > 0 {
138 edits.push(SegmentBuilder::symbol(tables.next_id(), "."));
139 }
140 edits.push(
141 SegmentBuilder::token(tables.next_id(), part, SyntaxKind::Identifier)
142 .finish(),
143 );
144 }
145 fixes.push(LintFix::replace(
146 alias.clone(),
147 edits,
148 Some(vec![table_ref.clone()]),
149 ));
150 }
151
152 violation_buff.push(LintResult::new(
153 alias_info.alias_identifier_ref,
154 fixes,
155 "Avoid aliases in from clauses and join conditions."
156 .to_owned()
157 .into(),
158 None,
159 ));
160 }
161 }
162
163 violation_buff
164 }
165
166 fn filter_table_expressions(
167 &self,
168 base_table: Option<ErasedSegment>,
169 from_expression_elements: Vec<ErasedSegment>,
170 ) -> Vec<TableAliasInfo> {
171 let mut acc = Vec::new();
172
173 for from_expression in from_expression_elements {
174 let table_expression =
175 from_expression.child(const { &SyntaxSet::new(&[SyntaxKind::TableExpression]) });
176 let Some(table_expression) = table_expression else {
177 continue;
178 };
179
180 let table_ref =
181 table_expression.child(const { &SyntaxSet::new(&[SyntaxKind::ObjectReference, SyntaxKind::TableReference]) });
182 let Some(table_ref) = table_ref else {
183 continue;
184 };
185
186 if let Some(ref base_table) = base_table {
187 if base_table.raw() == table_ref.raw() && base_table != &table_ref {
188 continue;
189 }
190 }
191
192 let whitespace_ref =
193 from_expression.child(const { &SyntaxSet::new(&[SyntaxKind::Whitespace]) });
194
195 let alias_exp_ref =
196 from_expression.child(const { &SyntaxSet::new(&[SyntaxKind::AliasExpression]) });
197 let Some(alias_exp_ref) = alias_exp_ref else {
198 continue;
199 };
200
201 let alias_identifier_ref = alias_exp_ref.child(
202 const { &SyntaxSet::new(&[SyntaxKind::Identifier, SyntaxKind::NakedIdentifier]) },
203 );
204
205 acc.push(TableAliasInfo {
206 table_ref,
207 whitespace_ref,
208 alias_exp_ref,
209 alias_identifier_ref,
210 });
211 }
212
213 acc
214 }
215}
216
217impl Rule for RuleAL07 {
218 fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
219 Ok(RuleAL07 {
220 force_enable: _config["force_enable"].as_bool().unwrap(),
221 }
222 .erased())
223 }
224
225 fn name(&self) -> &'static str {
226 "aliasing.forbid"
227 }
228 fn description(&self) -> &'static str {
229 "Avoid table aliases in from clauses and join conditions."
230 }
231
232 fn long_description(&self) -> &'static str {
233 r#"
234**Anti-pattern**
235
236In this example, alias o is used for the orders table, and c is used for customers table.
237
238```sql
239SELECT
240 COUNT(o.customer_id) as order_amount,
241 c.name
242FROM orders as o
243JOIN customers as c on o.id = c.user_id
244```
245
246**Best practice**
247
248Avoid aliases.
249
250```sql
251SELECT
252 COUNT(orders.customer_id) as order_amount,
253 customers.name
254FROM orders
255JOIN customers on orders.id = customers.user_id
256
257-- Self-join will not raise issue
258
259SELECT
260 table1.a,
261 table_alias.b,
262FROM
263 table1
264 LEFT JOIN table1 AS table_alias ON
265 table1.foreign_key = table_alias.foreign_key
266```
267"#
268 }
269
270 fn groups(&self) -> &'static [RuleGroups] {
271 &[RuleGroups::All, RuleGroups::Aliasing]
272 }
273
274 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
275 if !self.force_enable {
276 return Vec::new();
277 }
278
279 let children = FunctionalContext::new(context).segment().children(None);
280 let from_clause_segment = children
281 .select(
282 Some(|it: &ErasedSegment| it.is_type(SyntaxKind::FromClause)),
283 None,
284 None,
285 None,
286 )
287 .find_first::<fn(&_) -> _>(None);
288
289 let base_table = from_clause_segment
290 .children(Some(|it| it.is_type(SyntaxKind::FromExpression)))
291 .find_first::<fn(&_) -> _>(None)
292 .children(Some(|it| it.is_type(SyntaxKind::FromExpressionElement)))
293 .find_first::<fn(&_) -> _>(None)
294 .children(Some(|it| it.is_type(SyntaxKind::TableExpression)))
295 .find_first::<fn(&_) -> _>(None)
296 .children(Some(|it| {
297 it.is_type(SyntaxKind::ObjectReference) || it.is_type(SyntaxKind::TableReference)
298 }));
299
300 if base_table.is_empty() {
301 return Vec::new();
302 }
303
304 let mut from_expression_elements = Vec::new();
305 let mut column_reference_segments = Vec::new();
306
307 let after_from_clause = children.select::<fn(&ErasedSegment) -> bool>(
308 None,
309 None,
310 Some(&from_clause_segment[0]),
311 None,
312 );
313 for clause in chain(from_clause_segment, after_from_clause) {
314 for from_expression_element in clause.recursive_crawl(
315 const { &SyntaxSet::new(&[SyntaxKind::FromExpressionElement]) },
316 true,
317 &SyntaxSet::EMPTY,
318 true,
319 ) {
320 from_expression_elements.push(from_expression_element);
321 }
322
323 for from_expression_element in clause.recursive_crawl(
324 const { &SyntaxSet::new(&[SyntaxKind::ColumnReference]) },
325 true,
326 &SyntaxSet::EMPTY,
327 true,
328 ) {
329 column_reference_segments.push(from_expression_element);
330 }
331 }
332
333 self.lint_aliases_in_join(
334 context.tables,
335 base_table.first().cloned(),
336 from_expression_elements,
337 column_reference_segments,
338 context.segment.clone(),
339 )
340 }
341
342 fn is_fix_compatible(&self) -> bool {
343 true
344 }
345
346 fn crawl_behaviour(&self) -> Crawler {
347 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
348 }
349}