Skip to main content

sqruff_lib/rules/aliasing/
al04.rs

1use std::fmt::Debug;
2
3use hashbrown::{HashMap, HashSet};
4use smol_str::SmolStr;
5use sqruff_lib_core::dialects::common::{AliasInfo, ColumnAliasInfo};
6use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
7use sqruff_lib_core::helpers::IndexSet;
8use sqruff_lib_core::parser::segments::ErasedSegment;
9use sqruff_lib_core::parser::segments::object_reference::ObjectReferenceSegment;
10use sqruff_lib_core::utils::analysis::select::get_select_statement_info;
11
12use crate::core::config::Value;
13use crate::core::rules::context::RuleContext;
14use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
15use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
16
17type Handle<T> = fn(
18    Vec<AliasInfo>,
19    Vec<SmolStr>,
20    Vec<ObjectReferenceSegment>,
21    Vec<ColumnAliasInfo>,
22    Vec<SmolStr>,
23    Option<ErasedSegment>,
24    &RuleContext,
25    &T,
26) -> Vec<LintResult>;
27
28#[derive(Debug, Clone)]
29pub struct RuleAL04<T = ()> {
30    pub(crate) lint_references_and_aliases: Handle<T>,
31    pub(crate) context: T,
32}
33
34impl Default for RuleAL04 {
35    fn default() -> Self {
36        RuleAL04 {
37            lint_references_and_aliases: Self::lint_references_and_aliases,
38            context: (),
39        }
40    }
41}
42
43impl<T: Clone + Debug + Send + Sync + 'static> Rule for RuleAL04<T> {
44    fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
45        Ok(RuleAL04::default().erased())
46    }
47
48    fn name(&self) -> &'static str {
49        "aliasing.unique.table"
50    }
51
52    fn description(&self) -> &'static str {
53        "Table aliases should be unique within each clause."
54    }
55
56    fn long_description(&self) -> &'static str {
57        r#"
58**Anti-pattern**
59
60In this example, the alias t is reused for two different tables:
61
62```sql
63SELECT
64    t.a,
65    t.b
66FROM foo AS t, bar AS t
67
68-- This can also happen when using schemas where the
69-- implicit alias is the table name:
70
71SELECT
72    a,
73    b
74FROM
75    2020.foo,
76    2021.foo
77```
78
79**Best practice**
80
81Make all tables have a unique alias.
82
83```sql
84SELECT
85    f.a,
86    b.b
87FROM foo AS f, bar AS b
88
89-- Also use explicit aliases when referencing two tables
90-- with the same name from two different schemas.
91
92SELECT
93    f1.a,
94    f2.b
95FROM
96    2020.foo AS f1,
97    2021.foo AS f2
98```
99"#
100    }
101
102    fn groups(&self) -> &'static [RuleGroups] {
103        &[RuleGroups::All, RuleGroups::Core, RuleGroups::Aliasing]
104    }
105
106    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
107        let Some(select_info) =
108            get_select_statement_info(&context.segment, context.dialect.into(), true)
109        else {
110            return Vec::new();
111        };
112
113        let parent_select = context
114            .parent_stack
115            .iter()
116            .rev()
117            .find(|seg| seg.is_type(SyntaxKind::SelectStatement))
118            .cloned();
119
120        (self.lint_references_and_aliases)(
121            select_info.table_aliases,
122            select_info.standalone_aliases,
123            select_info.reference_buffer,
124            select_info.col_aliases,
125            select_info.using_cols,
126            parent_select,
127            context,
128            &self.context,
129        )
130    }
131
132    fn crawl_behaviour(&self) -> Crawler {
133        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
134    }
135}
136
137impl RuleAL04 {
138    #[allow(clippy::too_many_arguments)]
139    pub fn lint_references_and_aliases(
140        table_aliases: Vec<AliasInfo>,
141        _: Vec<SmolStr>,
142        _: Vec<ObjectReferenceSegment>,
143        _: Vec<ColumnAliasInfo>,
144        _: Vec<SmolStr>,
145        _: Option<ErasedSegment>,
146        _: &RuleContext,
147        _: &(),
148    ) -> Vec<LintResult> {
149        let mut duplicates = IndexSet::default();
150        let mut seen: HashSet<_> = HashSet::new();
151
152        for alias in table_aliases.iter() {
153            if !seen.insert(&alias.ref_str) && !alias.ref_str.is_empty() {
154                duplicates.insert(alias);
155            }
156        }
157
158        duplicates
159            .into_iter()
160            .map(|alias| {
161                LintResult::new(
162                    alias.segment.clone(),
163                    Vec::new(),
164                    format!(
165                        "Duplicate table alias '{}'. Table aliases should be unique.",
166                        alias.ref_str
167                    )
168                    .into(),
169                    None,
170                )
171            })
172            .collect()
173    }
174}