sqruff_lib/rules/aliasing/
al07.rs

1use 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                // Skip processing if table appears more than once with different aliases
65                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                    // Find all references to alias in select clause
82                    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                    // Find all references to alias in column references
102                    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                // Prepare fixes for deleting and editing references to aliased tables
121                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}