Skip to main content

sqruff_lib/rules/references/
rf02.rs

1use hashbrown::HashMap;
2use itertools::Itertools;
3use regex::Regex;
4use smol_str::SmolStr;
5use sqruff_lib_core::dialects::common::{AliasInfo, ColumnAliasInfo};
6use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
7use sqruff_lib_core::parser::segments::ErasedSegment;
8use sqruff_lib_core::parser::segments::object_reference::ObjectReferenceSegment;
9use sqruff_lib_core::utils::analysis::select::get_select_statement_info;
10
11use crate::core::config::Value;
12use crate::core::rules::context::RuleContext;
13use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
14use crate::core::rules::{Erased as _, ErasedRule, LintResult, Rule, RuleGroups};
15use crate::rules::aliasing::al04::RuleAL04;
16
17#[derive(Clone, Debug, Default)]
18pub struct RuleRF02Config {
19    ignore_words: Vec<String>,
20    ignore_words_regex: Vec<Regex>,
21    subqueries_ignore_external_references: bool,
22}
23
24#[derive(Clone, Debug)]
25pub struct RuleRF02 {
26    base: RuleAL04<RuleRF02Config>,
27}
28
29impl Default for RuleRF02 {
30    fn default() -> Self {
31        Self {
32            base: RuleAL04 {
33                lint_references_and_aliases: Self::lint_references_and_aliases,
34                context: RuleRF02Config::default(),
35            },
36        }
37    }
38}
39
40impl Rule for RuleRF02 {
41    fn load_from_config(&self, config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
42        let ignore_words = config["ignore_words"]
43            .map(|it| {
44                it.as_array()
45                    .unwrap()
46                    .iter()
47                    .map(|it| it.as_string().unwrap().to_lowercase())
48                    .collect()
49            })
50            .unwrap_or_default();
51
52        let ignore_words_regex = config["ignore_words_regex"]
53            .map(|it| {
54                it.as_array()
55                    .unwrap()
56                    .iter()
57                    .map(|it| Regex::new(it.as_string().unwrap()).unwrap())
58                    .collect()
59            })
60            .unwrap_or_default();
61
62        let subqueries_ignore_external_references = config["subqueries_ignore_external_references"]
63            .as_bool()
64            .unwrap_or(false);
65
66        Ok(Self {
67            base: RuleAL04 {
68                lint_references_and_aliases: Self::lint_references_and_aliases,
69                context: RuleRF02Config {
70                    ignore_words,
71                    ignore_words_regex,
72                    subqueries_ignore_external_references,
73                },
74            },
75        }
76        .erased())
77    }
78
79    fn name(&self) -> &'static str {
80        "references.qualification"
81    }
82
83    fn description(&self) -> &'static str {
84        "References should be qualified if select has more than one referenced table/view."
85    }
86
87    fn long_description(&self) -> &'static str {
88        r"
89**Anti-pattern**
90
91In this example, the reference `vee` has not been declared, and the variables `a` and `b` are potentially ambiguous.
92
93```sql
94SELECT a, b
95FROM foo
96LEFT JOIN vee ON vee.a = foo.a
97```
98
99**Best practice**
100
101Add the references.
102
103```sql
104SELECT foo.a, vee.b
105FROM foo
106LEFT JOIN vee ON vee.a = foo.a
107```
108"
109    }
110
111    fn groups(&self) -> &'static [RuleGroups] {
112        &[RuleGroups::All, RuleGroups::References]
113    }
114
115    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
116        self.base.eval(context)
117    }
118
119    fn crawl_behaviour(&self) -> Crawler {
120        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
121    }
122}
123
124impl RuleRF02 {
125    /// Determine if a subquery is part of the `from` clause.
126    ///
127    /// Any subqueries in the `from_clause` should be ignored, unless they are a
128    /// nested correlated query (i.e. inside a `where_clause`).
129    fn is_root_from_clause(rule_context: &RuleContext) -> bool {
130        for x in rule_context.parent_stack.iter().rev() {
131            if x.is_type(SyntaxKind::FromClause) {
132                return true;
133            } else if x.is_type(SyntaxKind::WhereClause) {
134                return false;
135            }
136        }
137        false
138    }
139
140    #[allow(clippy::too_many_arguments)]
141    fn lint_references_and_aliases(
142        mut table_aliases: Vec<AliasInfo>,
143        standalone_aliases: Vec<SmolStr>,
144        references: Vec<ObjectReferenceSegment>,
145        col_aliases: Vec<ColumnAliasInfo>,
146        using_cols: Vec<SmolStr>,
147        parent_select: Option<ErasedSegment>,
148        rule_context: &RuleContext,
149        context: &RuleRF02Config,
150    ) -> Vec<LintResult> {
151        let parent_select_info = parent_select.and_then(|parent| {
152            get_select_statement_info(&parent, rule_context.dialect.into(), true)
153        });
154        if let Some(parent_select_info) = parent_select_info {
155            // If we are looking at a subquery, include any table references
156            // from the parent (outer) select.
157            for table_alias in parent_select_info.table_aliases {
158                let is_from = Self::is_root_from_clause(rule_context);
159                if !table_alias
160                    .from_expression_element
161                    .path_to(&rule_context.segment)
162                    .is_empty()
163                    || is_from
164                    || context.subqueries_ignore_external_references
165                {
166                    // Skip the subquery alias itself, or if the subquery is
167                    // inside a `from`/`join` clause that isn't a nested
168                    // `where` clause.
169                    continue;
170                }
171                table_aliases.push(table_alias);
172            }
173        }
174
175        if table_aliases.len() <= 1 {
176            return Vec::new();
177        }
178
179        let mut violation_buff = Vec::new();
180        for r in references {
181            if context.ignore_words.contains(&r.0.raw().to_lowercase()) {
182                continue;
183            }
184
185            if context
186                .ignore_words_regex
187                .iter()
188                .any(|regex| regex.is_match(r.0.raw().as_ref()))
189            {
190                continue;
191            }
192
193            let this_ref_type = r.qualification();
194            let col_alias_names = col_aliases
195                .iter()
196                .filter_map(|c| {
197                    if !c.column_reference_segments.contains(&r.0) {
198                        Some(c.alias_identifier_name.as_str())
199                    } else {
200                        None
201                    }
202                })
203                .collect_vec();
204
205            if this_ref_type == "unqualified"
206                && !col_alias_names.contains(&r.0.raw().as_ref())
207                && !using_cols.contains(r.0.raw())
208                && !standalone_aliases.contains(r.0.raw())
209            {
210                violation_buff.push(LintResult::new(
211                    r.0.clone().into(),
212                    Vec::new(),
213                    format!(
214                        "Unqualified reference {} found in select with more than one referenced \
215                         table/view.",
216                        r.0.raw()
217                    )
218                    .into(),
219                    None,
220                ));
221            }
222        }
223
224        violation_buff
225    }
226}