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 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 !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 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 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
486fn 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 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 #[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}