sqruff_lib/rules/references/
rf02.rs1use 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 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 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 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}