1use 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 !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 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 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
485fn 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 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 #[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}