Skip to main content

flowscope_core/linter/rules/
rf_001.rs

1//! LINT_RF_001: References from.
2//!
3//! Qualified column prefixes should resolve to known FROM/JOIN sources.
4
5use std::cell::Cell;
6use std::collections::HashSet;
7
8use crate::linter::config::LintConfig;
9use crate::linter::rule::{LintContext, LintRule};
10use crate::types::{issue_codes, Issue};
11use sqlparser::ast::{
12    AlterPolicy, AlterPolicyOperation, Assignment, AssignmentTarget, ConditionalStatements,
13    CreatePolicy, CreateView, Expr, FromTable, FunctionArg, FunctionArgExpr, FunctionArguments,
14    Ident, Merge, MergeAction, MergeInsertKind, MergeUpdateExpr, ObjectName, OrderByKind, Query,
15    Select, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, Statement, TableFactor,
16    TableWithJoins, Update, UpdateTableFromKind,
17};
18
19use super::semantic_helpers::{join_on_expr, table_factor_alias_name, visit_select_expressions};
20
21pub struct ReferencesFrom {
22    force_enable: bool,
23    force_enable_configured: bool,
24}
25
26impl ReferencesFrom {
27    pub fn from_config(config: &LintConfig) -> Self {
28        let force_enable = config.rule_option_bool(issue_codes::LINT_RF_001, "force_enable");
29        Self {
30            force_enable: force_enable.unwrap_or(true),
31            force_enable_configured: force_enable.is_some(),
32        }
33    }
34}
35
36impl Default for ReferencesFrom {
37    fn default() -> Self {
38        Self {
39            force_enable: true,
40            force_enable_configured: false,
41        }
42    }
43}
44
45thread_local! {
46    static RF01_FORCE_ENABLE_EXPLICIT: Cell<bool> = const { Cell::new(false) };
47}
48
49fn with_rf01_force_enable_explicit<T>(explicit: bool, f: impl FnOnce() -> T) -> T {
50    RF01_FORCE_ENABLE_EXPLICIT.with(|active| {
51        struct Reset<'a> {
52            cell: &'a Cell<bool>,
53            previous: bool,
54        }
55
56        impl Drop for Reset<'_> {
57            fn drop(&mut self) {
58                self.cell.set(self.previous);
59            }
60        }
61
62        let reset = Reset {
63            cell: active,
64            previous: active.replace(explicit),
65        };
66        let result = f();
67        drop(reset);
68        result
69    })
70}
71
72impl LintRule for ReferencesFrom {
73    fn code(&self) -> &'static str {
74        issue_codes::LINT_RF_001
75    }
76
77    fn name(&self) -> &'static str {
78        "References from"
79    }
80
81    fn description(&self) -> &'static str {
82        "References cannot reference objects not present in 'FROM' clause."
83    }
84
85    fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
86        let effective_force_enable = if self.force_enable_configured {
87            self.force_enable
88        } else {
89            // SQLFluff keeps RF01 disabled by default for SparkSQL because
90            // dotted struct-field access is ambiguous with table qualification.
91            !matches!(ctx.dialect(), crate::types::Dialect::Databricks)
92        };
93
94        if !effective_force_enable {
95            return Vec::new();
96        }
97
98        let unresolved_count =
99            with_rf01_force_enable_explicit(self.force_enable_configured, || {
100                unresolved_references_in_statement(
101                    statement,
102                    &SourceRegistry::default(),
103                    ctx.dialect(),
104                    false,
105                )
106            });
107
108        (0..unresolved_count)
109            .map(|_| {
110                Issue::warning(
111                    issue_codes::LINT_RF_001,
112                    "Reference prefix appears unresolved from FROM/JOIN sources.",
113                )
114                .with_statement(ctx.statement_index)
115            })
116            .collect()
117    }
118}
119
120#[derive(Clone, Default)]
121struct SourceRegistry {
122    exact: HashSet<String>,
123    unqualified: HashSet<String>,
124}
125
126impl SourceRegistry {
127    fn register_alias(&mut self, alias: &str) {
128        let clean = clean_identifier_component(alias);
129        if clean.is_empty() {
130            return;
131        }
132        self.exact.insert(clean.clone());
133        self.unqualified.insert(clean);
134    }
135
136    fn register_object_name(&mut self, name: &ObjectName) {
137        let parts = object_name_parts(name);
138        if parts.is_empty() {
139            return;
140        }
141
142        let full = parts.join(".");
143        self.exact.insert(full);
144
145        if let Some(last) = parts.last() {
146            // Qualified table references are commonly referred to by their trailing
147            // relation name (e.g. schema.table -> table).
148            self.exact.insert(last.clone());
149        }
150
151        if parts.len() == 1 {
152            self.unqualified.insert(parts[0].clone());
153        }
154    }
155
156    fn register_pseudo_sources(&mut self, in_trigger: bool) {
157        for pseudo in ["EXCLUDED", "INSERTED", "DELETED"] {
158            self.register_alias(pseudo);
159        }
160        if in_trigger {
161            self.register_alias("NEW");
162            self.register_alias("OLD");
163        }
164    }
165
166    fn matches_qualifier(&self, qualifier_parts: &[String]) -> bool {
167        if qualifier_parts.is_empty() {
168            return true;
169        }
170
171        let full = qualifier_parts.join(".");
172        if self.exact.contains(&full) {
173            return true;
174        }
175
176        if qualifier_parts.len() > 1 {
177            if let Some(last) = qualifier_parts.last() {
178                return self.unqualified.contains(last);
179            }
180        }
181
182        false
183    }
184
185    fn is_empty(&self) -> bool {
186        self.exact.is_empty()
187    }
188}
189
190fn unresolved_references_in_statement(
191    statement: &Statement,
192    inherited_sources: &SourceRegistry,
193    dialect: crate::types::Dialect,
194    in_trigger: bool,
195) -> usize {
196    match statement {
197        Statement::Query(query) => {
198            unresolved_references_in_query(query, inherited_sources, dialect, in_trigger)
199        }
200        Statement::Insert(insert) => insert.source.as_ref().map_or(0, |query| {
201            unresolved_references_in_query(query, inherited_sources, dialect, in_trigger)
202        }),
203        Statement::CreateView(CreateView { query, .. }) => {
204            unresolved_references_in_query(query, inherited_sources, dialect, in_trigger)
205        }
206        Statement::CreateTable(create) => create.query.as_ref().map_or(0, |query| {
207            unresolved_references_in_query(query, inherited_sources, dialect, in_trigger)
208        }),
209        Statement::Update(Update {
210            table,
211            assignments,
212            from,
213            selection,
214            returning,
215            ..
216        }) => {
217            let mut scope_sources = inherited_sources.clone();
218            register_table_with_joins_sources(table, &mut scope_sources);
219            if let Some(from_tables) = from {
220                let tables = match from_tables {
221                    UpdateTableFromKind::BeforeSet(tables)
222                    | UpdateTableFromKind::AfterSet(tables) => tables,
223                };
224                for table in tables {
225                    register_table_with_joins_sources(table, &mut scope_sources);
226                }
227            }
228            register_assignment_target_sources(assignments, &mut scope_sources);
229            scope_sources.register_pseudo_sources(in_trigger);
230
231            let mut count = 0usize;
232            for assignment in assignments {
233                count += unresolved_references_in_expr(
234                    &assignment.value,
235                    &scope_sources,
236                    dialect,
237                    in_trigger,
238                );
239            }
240            if let Some(selection) = selection {
241                count +=
242                    unresolved_references_in_expr(selection, &scope_sources, dialect, in_trigger);
243            }
244            if let Some(returning) = returning {
245                for item in returning {
246                    count += unresolved_references_in_select_item(
247                        item,
248                        &scope_sources,
249                        dialect,
250                        in_trigger,
251                    );
252                }
253            }
254            count
255        }
256        Statement::Delete(delete) => {
257            let mut scope_sources = inherited_sources.clone();
258            let delete_from = match &delete.from {
259                FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => tables,
260            };
261            for table in delete_from {
262                register_table_with_joins_sources(table, &mut scope_sources);
263            }
264            if let Some(using) = &delete.using {
265                for table in using {
266                    register_table_with_joins_sources(table, &mut scope_sources);
267                }
268            }
269            scope_sources.register_pseudo_sources(in_trigger);
270
271            let mut count = 0usize;
272            if let Some(selection) = &delete.selection {
273                count +=
274                    unresolved_references_in_expr(selection, &scope_sources, dialect, in_trigger);
275            }
276            if let Some(returning) = &delete.returning {
277                for item in returning {
278                    count += unresolved_references_in_select_item(
279                        item,
280                        &scope_sources,
281                        dialect,
282                        in_trigger,
283                    );
284                }
285            }
286            for order_by in &delete.order_by {
287                count += unresolved_references_in_expr(
288                    &order_by.expr,
289                    &scope_sources,
290                    dialect,
291                    in_trigger,
292                );
293            }
294            if let Some(limit) = &delete.limit {
295                count += unresolved_references_in_expr(limit, &scope_sources, dialect, in_trigger);
296            }
297            count
298        }
299        Statement::Merge(Merge {
300            table,
301            source,
302            on,
303            clauses,
304            ..
305        }) => {
306            let mut scope_sources = inherited_sources.clone();
307            register_table_factor_sources(table, &mut scope_sources);
308            register_table_factor_sources(source, &mut scope_sources);
309            scope_sources.register_pseudo_sources(in_trigger);
310
311            let mut count = unresolved_references_in_expr(on, &scope_sources, dialect, in_trigger);
312            count +=
313                unresolved_references_in_table_factor(table, &scope_sources, dialect, in_trigger);
314            count +=
315                unresolved_references_in_table_factor(source, &scope_sources, dialect, in_trigger);
316
317            for clause in clauses {
318                if let Some(predicate) = &clause.predicate {
319                    count += unresolved_references_in_expr(
320                        predicate,
321                        &scope_sources,
322                        dialect,
323                        in_trigger,
324                    );
325                }
326                match &clause.action {
327                    MergeAction::Update(MergeUpdateExpr { assignments, .. }) => {
328                        for assignment in assignments {
329                            count += unresolved_references_in_expr(
330                                &assignment.value,
331                                &scope_sources,
332                                dialect,
333                                in_trigger,
334                            );
335                        }
336                    }
337                    MergeAction::Insert(insert) => {
338                        if let MergeInsertKind::Values(values) = &insert.kind {
339                            for row in &values.rows {
340                                for expr in row {
341                                    count += unresolved_references_in_expr(
342                                        expr,
343                                        &scope_sources,
344                                        dialect,
345                                        in_trigger,
346                                    );
347                                }
348                            }
349                        }
350                    }
351                    MergeAction::Delete { .. } => {}
352                }
353            }
354
355            count
356        }
357        Statement::CreatePolicy(CreatePolicy {
358            table_name,
359            using,
360            with_check,
361            ..
362        }) => {
363            let mut scope_sources = inherited_sources.clone();
364            scope_sources.register_object_name(table_name);
365            scope_sources.register_pseudo_sources(in_trigger);
366
367            let mut count = 0usize;
368            if let Some(using) = using {
369                count += unresolved_references_in_expr(using, &scope_sources, dialect, in_trigger);
370            }
371            if let Some(with_check) = with_check {
372                count +=
373                    unresolved_references_in_expr(with_check, &scope_sources, dialect, in_trigger);
374            }
375            count
376        }
377        Statement::AlterPolicy(AlterPolicy {
378            table_name,
379            operation,
380            ..
381        }) => {
382            let mut scope_sources = inherited_sources.clone();
383            scope_sources.register_object_name(table_name);
384            scope_sources.register_pseudo_sources(in_trigger);
385
386            match operation {
387                AlterPolicyOperation::Apply {
388                    using, with_check, ..
389                } => {
390                    let mut count = 0usize;
391                    if let Some(using) = using {
392                        count += unresolved_references_in_expr(
393                            using,
394                            &scope_sources,
395                            dialect,
396                            in_trigger,
397                        );
398                    }
399                    if let Some(with_check) = with_check {
400                        count += unresolved_references_in_expr(
401                            with_check,
402                            &scope_sources,
403                            dialect,
404                            in_trigger,
405                        );
406                    }
407                    count
408                }
409                AlterPolicyOperation::Rename { .. } => 0,
410            }
411        }
412        Statement::CreateTrigger(trigger) => {
413            let mut scope_sources = inherited_sources.clone();
414            scope_sources.register_object_name(&trigger.table_name);
415            scope_sources.register_pseudo_sources(true);
416
417            let mut count = 0usize;
418            if let Some(condition) = &trigger.condition {
419                count += unresolved_references_in_expr(condition, &scope_sources, dialect, true);
420            }
421            if let Some(statements) = &trigger.statements {
422                count += unresolved_references_in_conditional_statements(
423                    statements,
424                    &scope_sources,
425                    dialect,
426                    true,
427                );
428            }
429            count
430        }
431        _ => 0,
432    }
433}
434
435fn unresolved_references_in_conditional_statements(
436    statements: &ConditionalStatements,
437    inherited_sources: &SourceRegistry,
438    dialect: crate::types::Dialect,
439    in_trigger: bool,
440) -> usize {
441    statements
442        .statements()
443        .iter()
444        .map(|statement| {
445            unresolved_references_in_statement(statement, inherited_sources, dialect, in_trigger)
446        })
447        .sum()
448}
449
450fn unresolved_references_in_query(
451    query: &Query,
452    inherited_sources: &SourceRegistry,
453    dialect: crate::types::Dialect,
454    in_trigger: bool,
455) -> usize {
456    let mut count = 0usize;
457
458    if let Some(with) = &query.with {
459        for cte in &with.cte_tables {
460            count +=
461                unresolved_references_in_query(&cte.query, inherited_sources, dialect, in_trigger);
462        }
463    }
464
465    count += unresolved_references_in_set_expr(&query.body, inherited_sources, dialect, in_trigger);
466
467    // ORDER BY lives at the Query level but references columns from the
468    // body SELECT's FROM/JOIN scope. Build that scope for validation.
469    if let Some(order_by) = &query.order_by {
470        if let OrderByKind::Expressions(order_exprs) = &order_by.kind {
471            let order_scope = order_by_scope_from_body(&query.body, inherited_sources);
472            for order_expr in order_exprs {
473                count += unresolved_references_in_expr(
474                    &order_expr.expr,
475                    &order_scope,
476                    dialect,
477                    in_trigger,
478                );
479            }
480        }
481    }
482
483    count
484}
485
486/// Build a source registry for ORDER BY by extracting FROM/JOIN sources
487/// from the body SELECT (or both sides of a set operation).
488fn order_by_scope_from_body(body: &SetExpr, inherited: &SourceRegistry) -> SourceRegistry {
489    let mut scope = inherited.clone();
490    match body {
491        SetExpr::Select(select) => {
492            for from_item in &select.from {
493                register_table_with_joins_sources(from_item, &mut scope);
494            }
495        }
496        SetExpr::Query(query) => {
497            return order_by_scope_from_body(&query.body, inherited);
498        }
499        SetExpr::SetOperation { left, .. } => {
500            // For UNION/INTERSECT/EXCEPT, ORDER BY references come from the left branch.
501            return order_by_scope_from_body(left, inherited);
502        }
503        _ => {}
504    }
505    scope
506}
507
508fn unresolved_references_in_set_expr(
509    set_expr: &SetExpr,
510    inherited_sources: &SourceRegistry,
511    dialect: crate::types::Dialect,
512    in_trigger: bool,
513) -> usize {
514    match set_expr {
515        SetExpr::Select(select) => {
516            unresolved_references_in_select(select, inherited_sources, dialect, in_trigger)
517        }
518        SetExpr::Query(query) => {
519            unresolved_references_in_query(query, inherited_sources, dialect, in_trigger)
520        }
521        SetExpr::SetOperation { left, right, .. } => {
522            unresolved_references_in_set_expr(left, inherited_sources, dialect, in_trigger)
523                + unresolved_references_in_set_expr(right, inherited_sources, dialect, in_trigger)
524        }
525        SetExpr::Insert(statement)
526        | SetExpr::Update(statement)
527        | SetExpr::Delete(statement)
528        | SetExpr::Merge(statement) => {
529            unresolved_references_in_statement(statement, inherited_sources, dialect, in_trigger)
530        }
531        _ => 0,
532    }
533}
534
535fn unresolved_references_in_select(
536    select: &Select,
537    inherited_sources: &SourceRegistry,
538    dialect: crate::types::Dialect,
539    in_trigger: bool,
540) -> usize {
541    let mut scope_sources = inherited_sources.clone();
542    for from_item in &select.from {
543        register_table_with_joins_sources(from_item, &mut scope_sources);
544    }
545    scope_sources.register_pseudo_sources(in_trigger);
546
547    let mut count = 0usize;
548
549    if scope_sources.is_empty() {
550        return 0;
551    }
552
553    for item in &select.projection {
554        if let SelectItem::QualifiedWildcard(kind, _) = item {
555            count += match kind {
556                SelectItemQualifiedWildcardKind::ObjectName(name) => {
557                    unresolved_references_in_qualifier_parts(
558                        &object_name_parts(name),
559                        &scope_sources,
560                        dialect,
561                    )
562                }
563                SelectItemQualifiedWildcardKind::Expr(expr) => {
564                    unresolved_references_in_expr(expr, &scope_sources, dialect, in_trigger)
565                }
566            };
567        }
568    }
569
570    visit_select_expressions(select, &mut |expr| {
571        count += unresolved_references_in_expr(expr, &scope_sources, dialect, in_trigger);
572    });
573
574    for from_item in &select.from {
575        count += unresolved_references_in_table_with_joins(
576            from_item,
577            &scope_sources,
578            dialect,
579            in_trigger,
580        );
581    }
582
583    count
584}
585
586fn unresolved_references_in_select_item(
587    item: &SelectItem,
588    scope_sources: &SourceRegistry,
589    dialect: crate::types::Dialect,
590    in_trigger: bool,
591) -> usize {
592    match item {
593        SelectItem::UnnamedExpr(expr) | SelectItem::ExprWithAlias { expr, .. } => {
594            unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger)
595        }
596        SelectItem::QualifiedWildcard(kind, _) => match kind {
597            SelectItemQualifiedWildcardKind::ObjectName(name) => {
598                unresolved_references_in_qualifier_parts(
599                    &object_name_parts(name),
600                    scope_sources,
601                    dialect,
602                )
603            }
604            SelectItemQualifiedWildcardKind::Expr(expr) => {
605                unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger)
606            }
607        },
608        _ => 0,
609    }
610}
611
612fn unresolved_references_in_table_with_joins(
613    table_with_joins: &TableWithJoins,
614    scope_sources: &SourceRegistry,
615    dialect: crate::types::Dialect,
616    in_trigger: bool,
617) -> usize {
618    let mut count = unresolved_references_in_table_factor(
619        &table_with_joins.relation,
620        scope_sources,
621        dialect,
622        in_trigger,
623    );
624
625    for join in &table_with_joins.joins {
626        count += unresolved_references_in_table_factor(
627            &join.relation,
628            scope_sources,
629            dialect,
630            in_trigger,
631        );
632        if let Some(on_expr) = join_on_expr(&join.join_operator) {
633            count += unresolved_references_in_expr(on_expr, scope_sources, dialect, in_trigger);
634        }
635    }
636
637    count
638}
639
640fn unresolved_references_in_table_factor(
641    table_factor: &TableFactor,
642    scope_sources: &SourceRegistry,
643    dialect: crate::types::Dialect,
644    in_trigger: bool,
645) -> usize {
646    match table_factor {
647        TableFactor::Derived {
648            lateral, subquery, ..
649        } => {
650            if *lateral {
651                unresolved_references_in_query(subquery, scope_sources, dialect, in_trigger)
652            } else {
653                unresolved_references_in_query(
654                    subquery,
655                    &SourceRegistry::default(),
656                    dialect,
657                    in_trigger,
658                )
659            }
660        }
661        TableFactor::TableFunction { expr, .. } => {
662            unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger)
663        }
664        TableFactor::Function { args, .. } => args
665            .iter()
666            .map(|arg| {
667                unresolved_references_in_function_arg(arg, scope_sources, dialect, in_trigger)
668            })
669            .sum(),
670        TableFactor::UNNEST { array_exprs, .. } => array_exprs
671            .iter()
672            .map(|expr| unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger))
673            .sum(),
674        TableFactor::JsonTable { json_expr, .. } | TableFactor::OpenJsonTable { json_expr, .. } => {
675            unresolved_references_in_expr(json_expr, scope_sources, dialect, in_trigger)
676        }
677        TableFactor::NestedJoin {
678            table_with_joins, ..
679        } => unresolved_references_in_table_with_joins(
680            table_with_joins,
681            scope_sources,
682            dialect,
683            in_trigger,
684        ),
685        TableFactor::Pivot {
686            table,
687            aggregate_functions,
688            value_column,
689            default_on_null,
690            ..
691        } => {
692            let mut count =
693                unresolved_references_in_table_factor(table, scope_sources, dialect, in_trigger);
694            for expr_with_alias in aggregate_functions {
695                count += unresolved_references_in_expr(
696                    &expr_with_alias.expr,
697                    scope_sources,
698                    dialect,
699                    in_trigger,
700                );
701            }
702            for expr in value_column {
703                count += unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger);
704            }
705            if let Some(expr) = default_on_null {
706                count += unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger);
707            }
708            count
709        }
710        TableFactor::Unpivot {
711            table,
712            value,
713            columns,
714            ..
715        } => {
716            let mut count =
717                unresolved_references_in_table_factor(table, scope_sources, dialect, in_trigger);
718            count += unresolved_references_in_expr(value, scope_sources, dialect, in_trigger);
719            for expr_with_alias in columns {
720                count += unresolved_references_in_expr(
721                    &expr_with_alias.expr,
722                    scope_sources,
723                    dialect,
724                    in_trigger,
725                );
726            }
727            count
728        }
729        TableFactor::MatchRecognize {
730            table,
731            partition_by,
732            order_by,
733            measures,
734            symbols,
735            ..
736        } => {
737            let mut count =
738                unresolved_references_in_table_factor(table, scope_sources, dialect, in_trigger);
739            for expr in partition_by {
740                count += unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger);
741            }
742            for order in order_by {
743                count +=
744                    unresolved_references_in_expr(&order.expr, scope_sources, dialect, in_trigger);
745            }
746            for measure in measures {
747                count += unresolved_references_in_expr(
748                    &measure.expr,
749                    scope_sources,
750                    dialect,
751                    in_trigger,
752                );
753            }
754            for symbol in symbols {
755                count += unresolved_references_in_expr(
756                    &symbol.definition,
757                    scope_sources,
758                    dialect,
759                    in_trigger,
760                );
761            }
762            count
763        }
764        TableFactor::XmlTable { row_expression, .. } => {
765            unresolved_references_in_expr(row_expression, scope_sources, dialect, in_trigger)
766        }
767        _ => 0,
768    }
769}
770
771fn unresolved_references_in_function_arg(
772    arg: &FunctionArg,
773    scope_sources: &SourceRegistry,
774    dialect: crate::types::Dialect,
775    in_trigger: bool,
776) -> usize {
777    match arg {
778        FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
779        | FunctionArg::Named {
780            arg: FunctionArgExpr::Expr(expr),
781            ..
782        }
783        | FunctionArg::ExprNamed {
784            arg: FunctionArgExpr::Expr(expr),
785            ..
786        } => unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger),
787        FunctionArg::Unnamed(FunctionArgExpr::QualifiedWildcard(name))
788        | FunctionArg::Named {
789            arg: FunctionArgExpr::QualifiedWildcard(name),
790            ..
791        }
792        | FunctionArg::ExprNamed {
793            arg: FunctionArgExpr::QualifiedWildcard(name),
794            ..
795        } => unresolved_references_in_qualifier_parts(
796            &object_name_parts(name),
797            scope_sources,
798            dialect,
799        ),
800        _ => 0,
801    }
802}
803
804fn unresolved_references_in_expr(
805    expr: &Expr,
806    scope_sources: &SourceRegistry,
807    dialect: crate::types::Dialect,
808    in_trigger: bool,
809) -> usize {
810    match expr {
811        Expr::CompoundIdentifier(parts) if parts.len() > 1 => {
812            unresolved_references_in_qualifier_parts(
813                &qualifier_parts_from_compound_identifier(parts),
814                scope_sources,
815                dialect,
816            )
817        }
818        Expr::BinaryOp { left, right, .. }
819        | Expr::AnyOp { left, right, .. }
820        | Expr::AllOp { left, right, .. } => {
821            unresolved_references_in_expr(left, scope_sources, dialect, in_trigger)
822                + unresolved_references_in_expr(right, scope_sources, dialect, in_trigger)
823        }
824        Expr::UnaryOp { expr: inner, .. }
825        | Expr::Nested(inner)
826        | Expr::IsNull(inner)
827        | Expr::IsNotNull(inner)
828        | Expr::Cast { expr: inner, .. } => {
829            unresolved_references_in_expr(inner, scope_sources, dialect, in_trigger)
830        }
831        Expr::InList { expr, list, .. } => {
832            unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger)
833                + list
834                    .iter()
835                    .map(|item| {
836                        unresolved_references_in_expr(item, scope_sources, dialect, in_trigger)
837                    })
838                    .sum::<usize>()
839        }
840        Expr::Between {
841            expr, low, high, ..
842        } => {
843            unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger)
844                + unresolved_references_in_expr(low, scope_sources, dialect, in_trigger)
845                + unresolved_references_in_expr(high, scope_sources, dialect, in_trigger)
846        }
847        Expr::Case {
848            operand,
849            conditions,
850            else_result,
851            ..
852        } => {
853            let mut count = 0usize;
854            if let Some(operand) = operand {
855                count += unresolved_references_in_expr(operand, scope_sources, dialect, in_trigger);
856            }
857            for when in conditions {
858                count += unresolved_references_in_expr(
859                    &when.condition,
860                    scope_sources,
861                    dialect,
862                    in_trigger,
863                );
864                count +=
865                    unresolved_references_in_expr(&when.result, scope_sources, dialect, in_trigger);
866            }
867            if let Some(otherwise) = else_result {
868                count +=
869                    unresolved_references_in_expr(otherwise, scope_sources, dialect, in_trigger);
870            }
871            count
872        }
873        Expr::Function(function) => {
874            let mut count = 0usize;
875            if let FunctionArguments::List(arguments) = &function.args {
876                for arg in &arguments.args {
877                    count += unresolved_references_in_function_arg(
878                        arg,
879                        scope_sources,
880                        dialect,
881                        in_trigger,
882                    );
883                }
884            }
885            if let Some(filter) = &function.filter {
886                count += unresolved_references_in_expr(filter, scope_sources, dialect, in_trigger);
887            }
888            for order_expr in &function.within_group {
889                count += unresolved_references_in_expr(
890                    &order_expr.expr,
891                    scope_sources,
892                    dialect,
893                    in_trigger,
894                );
895            }
896            if let Some(sqlparser::ast::WindowType::WindowSpec(spec)) = &function.over {
897                for expr in &spec.partition_by {
898                    count +=
899                        unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger);
900                }
901                for order_expr in &spec.order_by {
902                    count += unresolved_references_in_expr(
903                        &order_expr.expr,
904                        scope_sources,
905                        dialect,
906                        in_trigger,
907                    );
908                }
909            }
910            count
911        }
912        Expr::InSubquery { expr, subquery, .. } => {
913            unresolved_references_in_expr(expr, scope_sources, dialect, in_trigger)
914                + unresolved_references_in_query(subquery, scope_sources, dialect, in_trigger)
915        }
916        Expr::Subquery(subquery) | Expr::Exists { subquery, .. } => {
917            unresolved_references_in_query(subquery, scope_sources, dialect, in_trigger)
918        }
919        _ => 0,
920    }
921}
922
923fn unresolved_references_in_qualifier_parts(
924    qualifier_parts: &[String],
925    scope_sources: &SourceRegistry,
926    dialect: crate::types::Dialect,
927) -> usize {
928    if qualifier_parts.is_empty() {
929        return 0;
930    }
931
932    if should_resolve_nested_field_reference_from_known_prefix(
933        dialect,
934        qualifier_parts,
935        scope_sources,
936    ) {
937        return 0;
938    }
939
940    if should_defer_struct_field_reference(dialect, qualifier_parts, scope_sources) {
941        return 0;
942    }
943
944    usize::from(!scope_sources.matches_qualifier(qualifier_parts))
945}
946
947fn should_resolve_nested_field_reference_from_known_prefix(
948    dialect: crate::types::Dialect,
949    qualifier_parts: &[String],
950    scope_sources: &SourceRegistry,
951) -> bool {
952    if !matches!(
953        dialect,
954        crate::types::Dialect::Bigquery
955            | crate::types::Dialect::Duckdb
956            | crate::types::Dialect::Hive
957            | crate::types::Dialect::Redshift
958    ) {
959        return false;
960    }
961
962    if qualifier_parts.len() < 2 {
963        return false;
964    }
965
966    scope_sources.matches_qualifier(&[qualifier_parts[0].clone()])
967}
968
969fn should_defer_struct_field_reference(
970    dialect: crate::types::Dialect,
971    qualifier_parts: &[String],
972    scope_sources: &SourceRegistry,
973) -> bool {
974    if RF01_FORCE_ENABLE_EXPLICIT.with(Cell::get) {
975        return false;
976    }
977
978    if !matches!(
979        dialect,
980        crate::types::Dialect::Bigquery
981            | crate::types::Dialect::Hive
982            | crate::types::Dialect::Redshift
983    ) {
984        return false;
985    }
986
987    qualifier_parts.len() == 1
988        && !scope_sources.is_empty()
989        && !scope_sources.matches_qualifier(qualifier_parts)
990}
991
992fn register_table_with_joins_sources(
993    table_with_joins: &TableWithJoins,
994    scope_sources: &mut SourceRegistry,
995) {
996    register_table_factor_sources(&table_with_joins.relation, scope_sources);
997    for join in &table_with_joins.joins {
998        register_table_factor_sources(&join.relation, scope_sources);
999    }
1000}
1001
1002fn register_assignment_target_sources(
1003    assignments: &[Assignment],
1004    scope_sources: &mut SourceRegistry,
1005) {
1006    for assignment in assignments {
1007        match &assignment.target {
1008            AssignmentTarget::ColumnName(name) => {
1009                register_assignment_target_name_prefixes(name, scope_sources);
1010            }
1011            AssignmentTarget::Tuple(columns) => {
1012                for name in columns {
1013                    register_assignment_target_name_prefixes(name, scope_sources);
1014                }
1015            }
1016        }
1017    }
1018}
1019
1020fn register_assignment_target_name_prefixes(name: &ObjectName, scope_sources: &mut SourceRegistry) {
1021    let parts = object_name_parts(name);
1022    if parts.len() < 2 {
1023        return;
1024    }
1025
1026    if let Some(first) = parts.first() {
1027        scope_sources.register_alias(first);
1028    }
1029
1030    let full_prefix = parts[..parts.len() - 1].join(".");
1031    if !full_prefix.is_empty() {
1032        scope_sources.exact.insert(full_prefix);
1033    }
1034}
1035
1036fn register_table_factor_sources(table_factor: &TableFactor, scope_sources: &mut SourceRegistry) {
1037    if let Some(alias) = table_factor_alias_name(table_factor) {
1038        scope_sources.register_alias(alias);
1039    }
1040
1041    match table_factor {
1042        TableFactor::Table { name, .. } => scope_sources.register_object_name(name),
1043        TableFactor::NestedJoin {
1044            table_with_joins, ..
1045        } => register_table_with_joins_sources(table_with_joins, scope_sources),
1046        TableFactor::Pivot { table, .. }
1047        | TableFactor::Unpivot { table, .. }
1048        | TableFactor::MatchRecognize { table, .. } => {
1049            register_table_factor_sources(table, scope_sources)
1050        }
1051        _ => {}
1052    }
1053}
1054
1055fn object_name_parts(name: &ObjectName) -> Vec<String> {
1056    let mut parts = Vec::new();
1057    for part in &name.0 {
1058        if let Some(ident) = part.as_ident() {
1059            append_identifier_segments(&ident.value, &mut parts);
1060        } else {
1061            append_identifier_segments(&part.to_string(), &mut parts);
1062        }
1063    }
1064    parts
1065}
1066
1067fn qualifier_parts_from_compound_identifier(parts: &[Ident]) -> Vec<String> {
1068    let mut qualifier_parts = Vec::new();
1069    for part in parts.iter().take(parts.len().saturating_sub(1)) {
1070        append_identifier_segments(&part.value, &mut qualifier_parts);
1071    }
1072    qualifier_parts
1073}
1074
1075fn append_identifier_segments(raw: &str, out: &mut Vec<String>) {
1076    for segment in raw.split('.') {
1077        let clean = clean_identifier_component(segment);
1078        if !clean.is_empty() {
1079            out.push(clean);
1080        }
1081    }
1082}
1083
1084fn clean_identifier_component(raw: &str) -> String {
1085    raw.trim()
1086        .trim_matches(|ch| matches!(ch, '"' | '`' | '\'' | '[' | ']'))
1087        .to_ascii_uppercase()
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092    use super::*;
1093    use crate::linter::config::LintConfig;
1094    use crate::linter::rule::with_active_dialect;
1095    use crate::parser::parse_sql;
1096    use crate::parser::parse_sql_with_dialect;
1097    use crate::types::Dialect;
1098
1099    fn run(sql: &str) -> Vec<Issue> {
1100        let statements = parse_sql(sql).expect("parse");
1101        let rule = ReferencesFrom::default();
1102        statements
1103            .iter()
1104            .enumerate()
1105            .flat_map(|(index, statement)| {
1106                rule.check(
1107                    statement,
1108                    &LintContext {
1109                        sql,
1110                        statement_range: 0..sql.len(),
1111                        statement_index: index,
1112                    },
1113                )
1114            })
1115            .collect()
1116    }
1117
1118    fn run_in_dialect(sql: &str, dialect: Dialect) -> Vec<Issue> {
1119        let statements = parse_sql_with_dialect(sql, dialect).expect("parse");
1120        let rule = ReferencesFrom::default();
1121        let mut issues = Vec::new();
1122        with_active_dialect(dialect, || {
1123            for (index, statement) in statements.iter().enumerate() {
1124                issues.extend(rule.check(
1125                    statement,
1126                    &LintContext {
1127                        sql,
1128                        statement_range: 0..sql.len(),
1129                        statement_index: index,
1130                    },
1131                ));
1132            }
1133        });
1134        issues
1135    }
1136
1137    // --- Edge cases adopted from sqlfluff RF01 ---
1138
1139    #[test]
1140    fn flags_unknown_qualifier() {
1141        let issues = run("SELECT * FROM my_tbl WHERE foo.bar > 0");
1142        assert_eq!(issues.len(), 1);
1143        assert_eq!(issues[0].code, issue_codes::LINT_RF_001);
1144    }
1145
1146    #[test]
1147    fn allows_known_table_qualifier() {
1148        let issues = run("SELECT users.id FROM users");
1149        assert!(issues.is_empty());
1150    }
1151
1152    #[test]
1153    fn allows_nested_subquery_references_that_resolve_locally() {
1154        let issues = run("SELECT * FROM db.sc.tbl2 WHERE a NOT IN (SELECT a FROM db.sc.tbl1)");
1155        assert!(issues.is_empty());
1156    }
1157
1158    #[test]
1159    fn allows_correlated_subquery_reference_to_outer_source() {
1160        let issues =
1161            run("SELECT * FROM tbl2 WHERE a NOT IN (SELECT a FROM tbl1 WHERE tbl2.a = tbl1.a)");
1162        assert!(issues.is_empty());
1163    }
1164
1165    #[test]
1166    fn flags_unresolved_two_part_reference() {
1167        let issues = run("select * from schema1.agent1 where schema2.agent1.agent_code <> 'abc'");
1168        assert_eq!(issues.len(), 1);
1169    }
1170
1171    #[test]
1172    fn allows_simple_delete_statement() {
1173        let issues = run("delete from table1 where 1 = 1");
1174        assert!(issues.is_empty());
1175    }
1176
1177    #[test]
1178    fn allows_three_part_reference_when_source_is_unqualified() {
1179        let issues = run("SELECT * FROM agent1 WHERE public.agent1.agent_code <> 'abc'");
1180        assert!(issues.is_empty());
1181    }
1182
1183    #[test]
1184    fn flags_unresolved_reference_in_update_statement() {
1185        let issues = run("UPDATE my_table SET amount = 1 WHERE my_tableeee.id = my_table.id");
1186        assert_eq!(issues.len(), 1);
1187    }
1188
1189    #[test]
1190    fn flags_old_new_outside_sqlite_trigger_context() {
1191        let issues = run_in_dialect("SELECT old.xyz, new.abc FROM foo", Dialect::Sqlite);
1192        assert_eq!(issues.len(), 2);
1193    }
1194
1195    #[test]
1196    fn allows_bigquery_quoted_qualified_table_reference() {
1197        let issues = run_in_dialect("SELECT bar.user_id FROM `foo.far.bar`", Dialect::Bigquery);
1198        assert!(issues.is_empty());
1199    }
1200
1201    #[test]
1202    fn allows_struct_field_style_reference_in_bigquery() {
1203        let issues = run_in_dialect("SELECT col1.field FROM foo", Dialect::Bigquery);
1204        assert!(issues.is_empty());
1205    }
1206
1207    #[test]
1208    fn flags_unresolved_reference_in_postgres_create_policy() {
1209        let issues = run_in_dialect(
1210            "CREATE POLICY p ON my_table USING (my_tableeee.id = my_table.id)",
1211            Dialect::Postgres,
1212        );
1213        assert_eq!(issues.len(), 1);
1214    }
1215
1216    #[test]
1217    fn sparksql_default_mode_skips_explode_nested_field_check() {
1218        let issues = run_in_dialect(
1219            "SELECT tbl.a AS a_new, EXPLODE(tbl.b.c) AS a_b_new FROM test AS tbl",
1220            Dialect::Databricks,
1221        );
1222        assert!(issues.is_empty());
1223    }
1224
1225    #[test]
1226    fn sparksql_force_enable_flags_explode_nested_field_reference() {
1227        let config = LintConfig {
1228            enabled: true,
1229            disabled_rules: vec![],
1230            rule_configs: std::collections::BTreeMap::from([(
1231                "references.from".to_string(),
1232                serde_json::json!({"force_enable": true}),
1233            )]),
1234        };
1235        let rule = ReferencesFrom::from_config(&config);
1236        let sql = "SELECT tbl.a AS a_new, EXPLODE(tbl.b.c) AS a_b_new FROM test AS tbl";
1237        let statements = parse_sql_with_dialect(sql, Dialect::Databricks).expect("parse");
1238        let mut issues = Vec::new();
1239        with_active_dialect(Dialect::Databricks, || {
1240            for (index, statement) in statements.iter().enumerate() {
1241                issues.extend(rule.check(
1242                    statement,
1243                    &LintContext {
1244                        sql,
1245                        statement_range: 0..sql.len(),
1246                        statement_index: index,
1247                    },
1248                ));
1249            }
1250        });
1251        assert_eq!(issues.len(), 1);
1252    }
1253
1254    #[test]
1255    fn allows_qualified_order_by_from_select_scope() {
1256        let issues = run("SELECT t.a FROM my_table AS t ORDER BY t.a DESC");
1257        assert!(issues.is_empty());
1258    }
1259
1260    #[test]
1261    fn allows_qualified_order_by_in_cte_query() {
1262        let issues = run("\
1263WITH cte AS (SELECT t.a FROM my_table AS t)
1264SELECT cte.a FROM cte ORDER BY cte.a");
1265        assert!(issues.is_empty());
1266    }
1267
1268    #[test]
1269    fn force_enable_false_disables_rule() {
1270        let config = LintConfig {
1271            enabled: true,
1272            disabled_rules: vec![],
1273            rule_configs: std::collections::BTreeMap::from([(
1274                "references.from".to_string(),
1275                serde_json::json!({"force_enable": false}),
1276            )]),
1277        };
1278        let rule = ReferencesFrom::from_config(&config);
1279        let sql = "SELECT * FROM my_tbl WHERE foo.bar > 0";
1280        let statements = parse_sql(sql).expect("parse");
1281        let issues = rule.check(
1282            &statements[0],
1283            &LintContext {
1284                sql,
1285                statement_range: 0..sql.len(),
1286                statement_index: 0,
1287            },
1288        );
1289        assert!(issues.is_empty());
1290    }
1291}