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