sqruff_lib/rules/references/
rf03.rs

1use 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        // TODO: add hive
359        &[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}