sqruff_lib/rules/references/
rf03.rs1use std::cell::RefCell;
2
3use ahash::{AHashMap, AHashSet};
4use itertools::Itertools;
5use smol_str::SmolStr;
6use sqruff_lib_core::dialects::common::{AliasInfo, ColumnAliasInfo};
7use sqruff_lib_core::dialects::init::DialectKind;
8use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
9use sqruff_lib_core::helpers::capitalize;
10use sqruff_lib_core::lint_fix::LintFix;
11use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder, Tables};
12use sqruff_lib_core::parser::segments::object_reference::ObjectReferenceSegment;
13use sqruff_lib_core::utils::analysis::query::Query;
14
15use crate::core::config::Value;
16use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
17use crate::core::rules::context::RuleContext;
18use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
19
20#[derive(Debug, Clone, Default)]
21pub struct RuleRF03 {
22 single_table_references: Option<String>,
23 force_enable: bool,
24}
25
26impl RuleRF03 {
27 fn visit_queries(
28 tables: &Tables,
29 single_table_references: &str,
30 is_struct_dialect: bool,
31 query: Query<()>,
32 _visited: &mut AHashSet<ErasedSegment>,
33 ) -> Vec<LintResult> {
34 #[allow(unused_assignments)]
35 let mut select_info = None;
36
37 let mut acc = Vec::new();
38 let selectables = &RefCell::borrow(&query.inner).selectables;
39
40 if !selectables.is_empty() {
41 select_info = selectables[0].select_info();
42
43 if let Some(select_info) = select_info
44 .clone()
45 .filter(|select_info| select_info.table_aliases.len() == 1)
46 {
47 let mut fixable = true;
48 let possible_ref_tables = iter_available_targets(query.clone());
49
50 if let Some(_parent) = &RefCell::borrow(&query.inner).parent {}
51
52 if possible_ref_tables.len() > 1 {
53 fixable = false;
54 }
55
56 let results = check_references(
57 tables,
58 select_info.table_aliases,
59 select_info.standalone_aliases,
60 select_info.reference_buffer,
61 select_info.col_aliases,
62 single_table_references,
63 is_struct_dialect,
64 Some("qualified".into()),
65 fixable,
66 );
67
68 acc.extend(results);
69 }
70 }
71
72 let children = query.children();
73 for child in children {
74 acc.extend(Self::visit_queries(
75 tables,
76 single_table_references,
77 is_struct_dialect,
78 child,
79 _visited,
80 ));
81 }
82
83 acc
84 }
85}
86
87fn iter_available_targets(query: Query<()>) -> Vec<SmolStr> {
88 RefCell::borrow(&query.inner)
89 .selectables
90 .iter()
91 .flat_map(|selectable| {
92 selectable
93 .select_info()
94 .unwrap()
95 .table_aliases
96 .iter()
97 .map(|alias| alias.ref_str.clone())
98 .collect_vec()
99 })
100 .collect_vec()
101}
102
103#[allow(clippy::too_many_arguments)]
104fn check_references(
105 tables: &Tables,
106 table_aliases: Vec<AliasInfo>,
107 standalone_aliases: Vec<SmolStr>,
108 references: Vec<ObjectReferenceSegment>,
109 col_aliases: Vec<ColumnAliasInfo>,
110 single_table_references: &str,
111 is_struct_dialect: bool,
112 fix_inconsistent_to: Option<String>,
113 fixable: bool,
114) -> Vec<LintResult> {
115 let mut acc = Vec::new();
116
117 let col_alias_names = col_aliases
118 .clone()
119 .into_iter()
120 .map(|it| it.alias_identifier_name)
121 .collect_vec();
122
123 let table_ref_str = &table_aliases[0].ref_str;
124 let table_ref_str_source = table_aliases[0].segment.clone();
125 let mut seen_ref_types = AHashSet::new();
126
127 for reference in references.clone() {
128 let mut this_ref_type = reference.qualification();
129 if this_ref_type == "qualified"
130 && is_struct_dialect
131 && &reference
132 .iter_raw_references()
133 .into_iter()
134 .next()
135 .unwrap()
136 .part
137 != table_ref_str
138 {
139 this_ref_type = "unqualified";
140 }
141
142 let lint_res = validate_one_reference(
143 tables,
144 single_table_references,
145 reference,
146 this_ref_type,
147 &standalone_aliases,
148 table_ref_str,
149 table_ref_str_source.clone(),
150 &col_alias_names,
151 &seen_ref_types,
152 fixable,
153 );
154
155 seen_ref_types.insert(this_ref_type);
156 let Some(lint_res) = lint_res else {
157 continue;
158 };
159
160 if let Some(fix_inconsistent_to) = fix_inconsistent_to
161 .as_ref()
162 .filter(|_| single_table_references == "consistent")
163 {
164 let results = check_references(
165 tables,
166 table_aliases.clone(),
167 standalone_aliases.clone(),
168 references.clone(),
169 col_aliases.clone(),
170 fix_inconsistent_to,
171 is_struct_dialect,
172 None,
173 fixable,
174 );
175
176 acc.extend(results);
177 }
178
179 acc.push(lint_res);
180 }
181
182 acc
183}
184
185#[allow(clippy::too_many_arguments)]
186fn validate_one_reference(
187 tables: &Tables,
188 single_table_references: &str,
189 ref_: ObjectReferenceSegment,
190 this_ref_type: &str,
191 standalone_aliases: &[SmolStr],
192 table_ref_str: &str,
193 _table_ref_str_source: Option<ErasedSegment>,
194 col_alias_names: &[SmolStr],
195 seen_ref_types: &AHashSet<&str>,
196 fixable: bool,
197) -> Option<LintResult> {
198 if !ref_.is_qualified() && ref_.0.is_type(SyntaxKind::WildcardIdentifier) {
199 return None;
200 }
201
202 if standalone_aliases.contains(ref_.0.raw()) {
203 return None;
204 }
205
206 if table_ref_str.is_empty() {
207 return None;
208 }
209
210 if col_alias_names.contains(ref_.0.raw()) {
211 return None;
212 }
213
214 if single_table_references == "consistent" {
215 return if !seen_ref_types.is_empty() && !seen_ref_types.contains(this_ref_type) {
216 LintResult::new(
217 ref_.clone().0.into(),
218 Vec::new(),
219 format!(
220 "{} reference '{}' found in single table select which is inconsistent with \
221 previous references.",
222 capitalize(this_ref_type),
223 ref_.0.raw()
224 )
225 .into(),
226 None,
227 )
228 .into()
229 } else {
230 None
231 };
232 }
233
234 if single_table_references == this_ref_type {
235 return None;
236 }
237
238 if single_table_references == "unqualified" {
239 let fixes = if fixable {
240 ref_.0
241 .segments()
242 .iter()
243 .take(2)
244 .cloned()
245 .map(LintFix::delete)
246 .collect::<Vec<_>>()
247 } else {
248 Vec::new()
249 };
250
251 return LintResult::new(
252 ref_.0.clone().into(),
253 fixes,
254 format!(
255 "{} reference '{}' found in single table select.",
256 capitalize(this_ref_type),
257 ref_.0.raw()
258 )
259 .into(),
260 None,
261 )
262 .into();
263 }
264
265 let ref_ = ref_.0.clone();
266 let fixes = if fixable {
267 vec![LintFix::create_before(
268 if !ref_.segments().is_empty() {
269 ref_.segments()[0].clone()
270 } else {
271 ref_.clone()
272 },
273 vec![
274 SegmentBuilder::token(tables.next_id(), table_ref_str, SyntaxKind::NakedIdentifier)
275 .finish(),
276 SegmentBuilder::symbol(tables.next_id(), "."),
277 ],
278 )]
279 } else {
280 Vec::new()
281 };
282
283 LintResult::new(
284 ref_.clone().into(),
285 fixes,
286 format!(
287 "{} reference '{}' found in single table select.",
288 capitalize(this_ref_type),
289 ref_.raw()
290 )
291 .into(),
292 None,
293 )
294 .into()
295}
296
297impl Rule for RuleRF03 {
298 fn load_from_config(&self, config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
299 Ok(RuleRF03 {
300 single_table_references: config
301 .get("single_table_references")
302 .and_then(|it| it.as_string().map(ToString::to_string)),
303 force_enable: config["force_enable"].as_bool().unwrap(),
304 }
305 .erased())
306 }
307
308 fn name(&self) -> &'static str {
309 "references.consistent"
310 }
311
312 fn description(&self) -> &'static str {
313 "References should be consistent in statements with a single table."
314 }
315
316 fn long_description(&self) -> &'static str {
317 r#"
318**Anti-pattern**
319
320In this example, only the field b is referenced.
321
322```sql
323SELECT
324 a,
325 foo.b
326FROM foo
327```
328
329**Best practice**
330
331Add or remove references to all fields.
332
333```sql
334SELECT
335 a,
336 b
337FROM foo
338
339-- Also good
340
341SELECT
342 foo.a,
343 foo.b
344FROM foo
345```
346"#
347 }
348
349 fn groups(&self) -> &'static [RuleGroups] {
350 &[RuleGroups::All, RuleGroups::References]
351 }
352
353 fn force_enable(&self) -> bool {
354 self.force_enable
355 }
356
357 fn dialect_skip(&self) -> &'static [DialectKind] {
358 &[DialectKind::Bigquery, DialectKind::Redshift]
360 }
361
362 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
363 let single_table_references =
364 self.single_table_references.as_deref().unwrap_or_else(|| {
365 context.config.raw["rules"]["single_table_references"]
366 .as_string()
367 .unwrap()
368 });
369
370 let query: Query<()> = Query::from_segment(&context.segment, context.dialect, None);
371 let mut visited: AHashSet<ErasedSegment> = AHashSet::new();
372 let is_struct_dialect = self.dialect_skip().contains(&context.dialect.name);
373
374 Self::visit_queries(
375 context.tables,
376 single_table_references,
377 is_struct_dialect,
378 query,
379 &mut visited,
380 )
381 }
382
383 fn is_fix_compatible(&self) -> bool {
384 true
385 }
386
387 fn crawl_behaviour(&self) -> Crawler {
388 SegmentSeekerCrawler::new(
389 const {
390 SyntaxSet::new(&[
391 SyntaxKind::SelectStatement,
392 SyntaxKind::SetExpression,
393 SyntaxKind::WithCompoundStatement,
394 ])
395 },
396 )
397 .disallow_recurse()
398 .into()
399 }
400}