1use std::collections::HashSet;
6
7use crate::linter::rule::{LintContext, LintRule};
8use crate::types::{issue_codes, Issue};
9use sqlparser::ast::{
10 ConnectByKind, CreateView, Expr, FunctionArg, FunctionArgExpr, JoinOperator, NamedWindowExpr,
11 OrderByKind, Query, Select, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, Statement,
12 TableFactor,
13};
14
15use super::semantic_helpers::{
16 collect_qualifier_prefixes_in_expr, count_reference_qualification_in_expr_excluding_aliases,
17 join_on_expr, select_projection_alias_set, table_factor_reference_name,
18};
19
20pub struct StructureUnusedJoin;
21
22impl LintRule for StructureUnusedJoin {
23 fn code(&self) -> &'static str {
24 issue_codes::LINT_ST_011
25 }
26
27 fn name(&self) -> &'static str {
28 "Structure unused join"
29 }
30
31 fn description(&self) -> &'static str {
32 "Joined table not referenced in query."
33 }
34
35 fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
36 let violations = unused_join_count_for_statement(statement);
37
38 (0..violations)
39 .map(|_| {
40 Issue::warning(issue_codes::LINT_ST_011, "Joined source appears unused.")
41 .with_statement(ctx.statement_index)
42 })
43 .collect()
44 }
45}
46
47fn unused_join_count_for_statement(statement: &Statement) -> usize {
48 match statement {
49 Statement::Query(query) => unused_join_count_for_query(query),
50 Statement::Insert(insert) => insert
51 .source
52 .as_ref()
53 .map_or(0, |query| unused_join_count_for_query(query)),
54 Statement::CreateView(CreateView { query, .. }) => unused_join_count_for_query(query),
55 Statement::CreateTable(create) => create
56 .query
57 .as_ref()
58 .map_or(0, |query| unused_join_count_for_query(query)),
59 _ => 0,
60 }
61}
62
63fn unused_join_count_for_query(query: &Query) -> usize {
64 let mut total = 0usize;
65
66 if let Some(with) = &query.with {
67 for cte in &with.cte_tables {
68 total += unused_join_count_for_query(&cte.query);
69 }
70 }
71
72 let query_order_by_exprs = query_order_by_exprs(query);
73 total + unused_join_count_for_set_expr(&query.body, &query_order_by_exprs)
74}
75
76fn unused_join_count_for_set_expr(set_expr: &SetExpr, query_order_by_exprs: &[&Expr]) -> usize {
77 match set_expr {
78 SetExpr::Select(select) => {
79 let mut total = unused_join_count_for_select(select, query_order_by_exprs);
80
81 for table in &select.from {
82 total += unused_join_count_for_table_factor(&table.relation);
83 for join in &table.joins {
84 total += unused_join_count_for_table_factor(&join.relation);
85 }
86 }
87
88 visit_non_join_select_expressions(select, &mut |expr| {
89 total += unused_join_count_for_expr_subqueries(expr);
90 });
91 visit_distinct_on_expressions(select, &mut |expr| {
92 total += unused_join_count_for_expr_subqueries(expr);
93 });
94 visit_named_window_expressions(select, &mut |expr| {
95 total += unused_join_count_for_expr_subqueries(expr);
96 });
97
98 total
99 }
100 SetExpr::Query(query) => unused_join_count_for_query(query),
101 SetExpr::SetOperation { left, right, .. } => {
102 unused_join_count_for_set_expr(left, &[]) + unused_join_count_for_set_expr(right, &[])
103 }
104 SetExpr::Insert(statement)
105 | SetExpr::Update(statement)
106 | SetExpr::Delete(statement)
107 | SetExpr::Merge(statement) => unused_join_count_for_statement(statement),
108 _ => 0,
109 }
110}
111
112fn query_order_by_exprs(query: &Query) -> Vec<&Expr> {
113 let Some(order_by) = &query.order_by else {
114 return Vec::new();
115 };
116
117 match &order_by.kind {
118 OrderByKind::Expressions(order_exprs) => {
119 order_exprs.iter().map(|item| &item.expr).collect()
120 }
121 _ => Vec::new(),
122 }
123}
124
125fn unused_join_count_for_select(select: &Select, query_order_by_exprs: &[&Expr]) -> usize {
126 if select.from.is_empty() {
127 return 0;
128 }
129
130 let mut joined_sources = joined_sources(select);
131 if joined_sources.is_empty() {
132 return 0;
133 }
134
135 let referenced_in_join_clauses = referenced_tables_in_join_clauses(select);
136 joined_sources.retain(|source| !referenced_in_join_clauses.contains(source));
137 if joined_sources.is_empty() {
138 return 0;
139 }
140
141 if select_has_unqualified_wildcard(select) {
144 return 0;
145 }
146
147 let aliases = select_projection_alias_set(select);
148 let mut used_prefixes = HashSet::new();
149 let mut unqualified_references = 0usize;
150
151 visit_non_join_select_expressions(select, &mut |expr| {
152 collect_qualifier_prefixes_in_expr(expr, &mut used_prefixes);
153 let (_, unqualified) =
154 count_reference_qualification_in_expr_excluding_aliases(expr, &aliases);
155 unqualified_references += unqualified;
156 });
157 visit_distinct_on_expressions(select, &mut |expr| {
158 collect_qualifier_prefixes_in_expr(expr, &mut used_prefixes);
159 let (_, unqualified) =
160 count_reference_qualification_in_expr_excluding_aliases(expr, &aliases);
161 unqualified_references += unqualified;
162 });
163 visit_named_window_expressions(select, &mut |expr| {
164 collect_qualifier_prefixes_in_expr(expr, &mut used_prefixes);
165 let (_, unqualified) =
166 count_reference_qualification_in_expr_excluding_aliases(expr, &aliases);
167 unqualified_references += unqualified;
168 });
169
170 for expr in query_order_by_exprs {
171 collect_qualifier_prefixes_in_expr(expr, &mut used_prefixes);
172 let (_, unqualified) =
173 count_reference_qualification_in_expr_excluding_aliases(expr, &aliases);
174 unqualified_references += unqualified;
175 }
176
177 if unqualified_references > 0 {
180 return 0;
181 }
182
183 collect_projection_wildcard_prefixes(select, &mut used_prefixes);
184 collect_join_relation_reference_prefixes(select, &mut used_prefixes);
185
186 joined_sources
187 .iter()
188 .filter(|source| !used_prefixes.contains(*source))
189 .count()
190}
191
192fn unused_join_count_for_table_factor(table_factor: &TableFactor) -> usize {
193 match table_factor {
194 TableFactor::Derived { subquery, .. } => unused_join_count_for_query(subquery),
195 TableFactor::NestedJoin {
196 table_with_joins, ..
197 } => {
198 let mut total = unused_join_count_for_table_factor(&table_with_joins.relation);
199 for join in &table_with_joins.joins {
200 total += unused_join_count_for_table_factor(&join.relation);
201 if let Some(on_expr) = join_on_expr(&join.join_operator) {
202 total += unused_join_count_for_expr_subqueries(on_expr);
203 }
204 }
205 total
206 }
207 TableFactor::Pivot {
208 table,
209 aggregate_functions,
210 value_column,
211 default_on_null,
212 ..
213 } => {
214 let mut total = unused_join_count_for_table_factor(table);
215 for expr_with_alias in aggregate_functions {
216 total += unused_join_count_for_expr_subqueries(&expr_with_alias.expr);
217 }
218 for expr in value_column {
219 total += unused_join_count_for_expr_subqueries(expr);
220 }
221 if let Some(expr) = default_on_null {
222 total += unused_join_count_for_expr_subqueries(expr);
223 }
224 total
225 }
226 TableFactor::Unpivot {
227 table,
228 value,
229 columns,
230 ..
231 } => {
232 let mut total = unused_join_count_for_table_factor(table);
233 total += unused_join_count_for_expr_subqueries(value);
234 for expr_with_alias in columns {
235 total += unused_join_count_for_expr_subqueries(&expr_with_alias.expr);
236 }
237 total
238 }
239 TableFactor::MatchRecognize {
240 table,
241 partition_by,
242 order_by,
243 measures,
244 ..
245 } => {
246 let mut total = unused_join_count_for_table_factor(table);
247 for expr in partition_by {
248 total += unused_join_count_for_expr_subqueries(expr);
249 }
250 for order in order_by {
251 total += unused_join_count_for_expr_subqueries(&order.expr);
252 }
253 for measure in measures {
254 total += unused_join_count_for_expr_subqueries(&measure.expr);
255 }
256 total
257 }
258 TableFactor::TableFunction { expr, .. } => unused_join_count_for_expr_subqueries(expr),
259 TableFactor::Function { args, .. } => args
260 .iter()
261 .map(unused_join_count_for_function_arg)
262 .sum::<usize>(),
263 TableFactor::UNNEST { array_exprs, .. } => array_exprs
264 .iter()
265 .map(unused_join_count_for_expr_subqueries)
266 .sum::<usize>(),
267 TableFactor::JsonTable { json_expr, .. } | TableFactor::OpenJsonTable { json_expr, .. } => {
268 unused_join_count_for_expr_subqueries(json_expr)
269 }
270 TableFactor::XmlTable { row_expression, .. } => {
271 unused_join_count_for_expr_subqueries(row_expression)
272 }
273 TableFactor::Table { .. } | TableFactor::SemanticView { .. } => 0,
274 }
275}
276
277fn unused_join_count_for_function_arg(arg: &FunctionArg) -> usize {
278 match arg {
279 FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
280 | FunctionArg::Named {
281 arg: FunctionArgExpr::Expr(expr),
282 ..
283 } => unused_join_count_for_expr_subqueries(expr),
284 _ => 0,
285 }
286}
287
288fn unused_join_count_for_expr_subqueries(expr: &Expr) -> usize {
289 match expr {
290 Expr::Subquery(query)
291 | Expr::Exists {
292 subquery: query, ..
293 } => unused_join_count_for_query(query),
294 Expr::InSubquery {
295 expr: inner,
296 subquery,
297 ..
298 } => unused_join_count_for_expr_subqueries(inner) + unused_join_count_for_query(subquery),
299 Expr::BinaryOp { left, right, .. }
300 | Expr::AnyOp { left, right, .. }
301 | Expr::AllOp { left, right, .. } => {
302 unused_join_count_for_expr_subqueries(left)
303 + unused_join_count_for_expr_subqueries(right)
304 }
305 Expr::UnaryOp { expr: inner, .. }
306 | Expr::Nested(inner)
307 | Expr::IsNull(inner)
308 | Expr::IsNotNull(inner)
309 | Expr::Cast { expr: inner, .. } => unused_join_count_for_expr_subqueries(inner),
310 Expr::InList { expr, list, .. } => {
311 unused_join_count_for_expr_subqueries(expr)
312 + list
313 .iter()
314 .map(unused_join_count_for_expr_subqueries)
315 .sum::<usize>()
316 }
317 Expr::Between {
318 expr, low, high, ..
319 } => {
320 unused_join_count_for_expr_subqueries(expr)
321 + unused_join_count_for_expr_subqueries(low)
322 + unused_join_count_for_expr_subqueries(high)
323 }
324 Expr::Case {
325 operand,
326 conditions,
327 else_result,
328 ..
329 } => {
330 let operand_count = operand
331 .as_ref()
332 .map_or(0, |expr| unused_join_count_for_expr_subqueries(expr));
333 let condition_count = conditions
334 .iter()
335 .map(|when| {
336 unused_join_count_for_expr_subqueries(&when.condition)
337 + unused_join_count_for_expr_subqueries(&when.result)
338 })
339 .sum::<usize>();
340 let else_count = else_result
341 .as_ref()
342 .map_or(0, |expr| unused_join_count_for_expr_subqueries(expr));
343 operand_count + condition_count + else_count
344 }
345 Expr::Function(function) => {
346 let args_count =
347 if let sqlparser::ast::FunctionArguments::List(arguments) = &function.args {
348 arguments
349 .args
350 .iter()
351 .map(unused_join_count_for_function_arg)
352 .sum::<usize>()
353 } else {
354 0
355 };
356 let filter_count = function
357 .filter
358 .as_ref()
359 .map_or(0, |expr| unused_join_count_for_expr_subqueries(expr));
360 let within_group_count = function
361 .within_group
362 .iter()
363 .map(|order| unused_join_count_for_expr_subqueries(&order.expr))
364 .sum::<usize>();
365 let window_count = match &function.over {
366 Some(sqlparser::ast::WindowType::WindowSpec(spec)) => {
367 spec.partition_by
368 .iter()
369 .map(unused_join_count_for_expr_subqueries)
370 .sum::<usize>()
371 + spec
372 .order_by
373 .iter()
374 .map(|order| unused_join_count_for_expr_subqueries(&order.expr))
375 .sum::<usize>()
376 }
377 _ => 0,
378 };
379 args_count + filter_count + within_group_count + window_count
380 }
381 _ => 0,
382 }
383}
384
385fn select_has_unqualified_wildcard(select: &Select) -> bool {
386 select
387 .projection
388 .iter()
389 .any(|item| matches!(item, SelectItem::Wildcard(_)))
390}
391
392fn joined_sources(select: &Select) -> HashSet<String> {
393 let mut joined_sources = HashSet::new();
394
395 for table in &select.from {
396 for join in &table.joins {
397 if !is_tracked_join(&join.join_operator) {
398 continue;
399 }
400
401 if let Some(name) = table_factor_reference_name(&join.relation) {
402 joined_sources.insert(name);
403 }
404 }
405 }
406
407 joined_sources
408}
409
410fn referenced_tables_in_join_clauses(select: &Select) -> HashSet<String> {
411 let mut referenced = HashSet::new();
412
413 for table in &select.from {
414 for join in &table.joins {
415 let self_ref = table_factor_reference_name(&join.relation);
416
417 if let Some(on_expr) = join_on_expr(&join.join_operator) {
418 let mut refs = HashSet::new();
419 collect_qualifier_prefixes_in_expr(on_expr, &mut refs);
420 for table_ref in refs {
421 if self_ref.as_deref() != Some(table_ref.as_str()) {
422 referenced.insert(table_ref);
423 }
424 }
425 }
426 }
427 }
428
429 referenced
430}
431
432fn is_tracked_join(operator: &JoinOperator) -> bool {
433 matches!(
434 operator,
435 JoinOperator::Left(_)
436 | JoinOperator::LeftOuter(_)
437 | JoinOperator::Right(_)
438 | JoinOperator::RightOuter(_)
439 | JoinOperator::FullOuter(_)
440 )
441}
442
443fn visit_non_join_select_expressions<F: FnMut(&sqlparser::ast::Expr)>(
444 select: &Select,
445 visitor: &mut F,
446) {
447 for item in &select.projection {
448 if let sqlparser::ast::SelectItem::UnnamedExpr(expr)
449 | sqlparser::ast::SelectItem::ExprWithAlias { expr, .. } = item
450 {
451 visitor(expr);
452 }
453 }
454
455 if let Some(prewhere) = &select.prewhere {
456 visitor(prewhere);
457 }
458
459 if let Some(selection) = &select.selection {
460 visitor(selection);
461 }
462
463 if let sqlparser::ast::GroupByExpr::Expressions(exprs, _) = &select.group_by {
464 for expr in exprs {
465 visitor(expr);
466 }
467 }
468
469 for expr in &select.cluster_by {
470 visitor(expr);
471 }
472
473 for expr in &select.distribute_by {
474 visitor(expr);
475 }
476
477 for lateral_view in &select.lateral_views {
478 visitor(&lateral_view.lateral_view);
479 }
480
481 if let Some(having) = &select.having {
482 visitor(having);
483 }
484
485 if let Some(qualify) = &select.qualify {
486 visitor(qualify);
487 }
488
489 for connect_by_kind in &select.connect_by {
490 match connect_by_kind {
491 ConnectByKind::ConnectBy { relationships, .. } => {
492 for relationship in relationships {
493 visitor(relationship);
494 }
495 }
496 ConnectByKind::StartWith { condition, .. } => {
497 visitor(condition);
498 }
499 }
500 }
501
502 for sort in &select.sort_by {
503 visitor(&sort.expr);
504 }
505}
506
507fn visit_distinct_on_expressions<F: FnMut(&sqlparser::ast::Expr)>(
508 select: &Select,
509 visitor: &mut F,
510) {
511 if let Some(sqlparser::ast::Distinct::On(expressions)) = &select.distinct {
512 for expr in expressions {
513 visitor(expr);
514 }
515 }
516}
517
518fn visit_named_window_expressions<F: FnMut(&sqlparser::ast::Expr)>(
519 select: &Select,
520 visitor: &mut F,
521) {
522 for named_window in &select.named_window {
523 if let NamedWindowExpr::WindowSpec(spec) = &named_window.1 {
524 for expr in &spec.partition_by {
525 visitor(expr);
526 }
527 for order in &spec.order_by {
528 visitor(&order.expr);
529 }
530 }
531 }
532}
533
534fn collect_projection_wildcard_prefixes(select: &Select, prefixes: &mut HashSet<String>) {
535 for item in &select.projection {
536 if let SelectItem::QualifiedWildcard(kind, _) = item {
537 match kind {
538 SelectItemQualifiedWildcardKind::ObjectName(name) => {
539 if let Some(last) = name.0.last().and_then(|part| part.as_ident()) {
540 prefixes.insert(last.value.to_ascii_uppercase());
541 }
542 }
543 SelectItemQualifiedWildcardKind::Expr(expr) => match expr {
544 Expr::Identifier(identifier) => {
545 prefixes.insert(identifier.value.to_ascii_uppercase());
546 }
547 Expr::CompoundIdentifier(parts) if parts.len() > 1 => {
548 if let Some(first) = parts.first() {
549 prefixes.insert(first.value.to_ascii_uppercase());
550 }
551 }
552 _ => collect_qualifier_prefixes_in_expr(expr, prefixes),
553 },
554 }
555 }
556 }
557}
558
559fn collect_join_relation_reference_prefixes(select: &Select, prefixes: &mut HashSet<String>) {
560 for table in &select.from {
561 for join in &table.joins {
562 collect_table_factor_reference_prefixes(&join.relation, prefixes);
563 }
564 }
565}
566
567fn collect_table_factor_reference_prefixes(
568 table_factor: &TableFactor,
569 prefixes: &mut HashSet<String>,
570) {
571 match table_factor {
572 TableFactor::Table { .. } => {}
573 TableFactor::Derived { .. } => {}
574 TableFactor::TableFunction { expr, .. } => {
575 collect_qualifier_prefixes_in_expr(expr, prefixes);
576 }
577 TableFactor::Function { args, .. } => {
578 for arg in args {
579 collect_function_arg_prefixes(arg, prefixes);
580 }
581 }
582 TableFactor::UNNEST { array_exprs, .. } => {
583 for expr in array_exprs {
584 collect_qualifier_prefixes_in_expr(expr, prefixes);
585 }
586 }
587 TableFactor::JsonTable { json_expr, .. } | TableFactor::OpenJsonTable { json_expr, .. } => {
588 collect_qualifier_prefixes_in_expr(json_expr, prefixes);
589 }
590 TableFactor::NestedJoin {
591 table_with_joins, ..
592 } => {
593 collect_table_factor_reference_prefixes(&table_with_joins.relation, prefixes);
594 for join in &table_with_joins.joins {
595 collect_table_factor_reference_prefixes(&join.relation, prefixes);
596 }
597 }
598 TableFactor::Pivot {
599 table,
600 aggregate_functions,
601 value_column,
602 default_on_null,
603 ..
604 } => {
605 collect_table_factor_reference_prefixes(table, prefixes);
606 for expr_with_alias in aggregate_functions {
607 collect_qualifier_prefixes_in_expr(&expr_with_alias.expr, prefixes);
608 }
609 for expr in value_column {
610 collect_qualifier_prefixes_in_expr(expr, prefixes);
611 }
612 if let Some(expr) = default_on_null {
613 collect_qualifier_prefixes_in_expr(expr, prefixes);
614 }
615 }
616 TableFactor::Unpivot {
617 table,
618 value,
619 columns,
620 ..
621 } => {
622 collect_table_factor_reference_prefixes(table, prefixes);
623 collect_qualifier_prefixes_in_expr(value, prefixes);
624 for expr_with_alias in columns {
625 collect_qualifier_prefixes_in_expr(&expr_with_alias.expr, prefixes);
626 }
627 }
628 TableFactor::MatchRecognize {
629 table,
630 partition_by,
631 order_by,
632 measures,
633 ..
634 } => {
635 collect_table_factor_reference_prefixes(table, prefixes);
636 for expr in partition_by {
637 collect_qualifier_prefixes_in_expr(expr, prefixes);
638 }
639 for order in order_by {
640 collect_qualifier_prefixes_in_expr(&order.expr, prefixes);
641 }
642 for measure in measures {
643 collect_qualifier_prefixes_in_expr(&measure.expr, prefixes);
644 }
645 }
646 TableFactor::XmlTable { row_expression, .. } => {
647 collect_qualifier_prefixes_in_expr(row_expression, prefixes);
648 }
649 TableFactor::SemanticView { .. } => {}
650 }
651}
652
653fn collect_function_arg_prefixes(arg: &FunctionArg, prefixes: &mut HashSet<String>) {
654 match arg {
655 FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
656 | FunctionArg::Named {
657 arg: FunctionArgExpr::Expr(expr),
658 ..
659 } => collect_qualifier_prefixes_in_expr(expr, prefixes),
660 _ => {}
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use crate::{parser::parse_sql_with_dialect, types::Dialect};
668
669 fn run(sql: &str) -> Vec<Issue> {
670 run_in_dialect(sql, Dialect::Generic)
671 }
672
673 fn run_in_dialect(sql: &str, dialect: Dialect) -> Vec<Issue> {
674 let statements = parse_sql_with_dialect(sql, dialect).expect("parse");
675 let rule = StructureUnusedJoin;
676 statements
677 .iter()
678 .enumerate()
679 .flat_map(|(index, statement)| {
680 rule.check(
681 statement,
682 &LintContext {
683 sql,
684 statement_range: 0..sql.len(),
685 statement_index: index,
686 },
687 )
688 })
689 .collect()
690 }
691
692 #[test]
695 fn flags_unused_outer_joined_source() {
696 let issues = run("select 1 from b left join c on b.x = c.x");
697 assert_eq!(issues.len(), 1);
698 assert_eq!(issues[0].code, issue_codes::LINT_ST_011);
699 }
700
701 #[test]
702 fn allows_single_table_statement() {
703 let issues = run("select 1 from foo");
704 assert!(issues.is_empty());
705 }
706
707 #[test]
708 fn allows_single_values_table_statement() {
709 let issues = run("select 1 from (values (1, 'one'), (2, 'two'))");
710 assert!(issues.is_empty());
711 }
712
713 #[test]
714 fn allows_inner_join_when_joined_source_unreferenced() {
715 let issues = run("select a.* from a inner join b using(x)");
716 assert!(issues.is_empty());
717 }
718
719 #[test]
720 fn allows_implicit_inner_join_when_joined_source_unreferenced() {
721 let issues = run("select a.* from a join b using(x)");
722 assert!(issues.is_empty());
723 }
724
725 #[test]
726 fn flags_unreferenced_right_joined_source() {
727 let issues = run("select a.* from a right join d using(x)");
728 assert_eq!(issues.len(), 1);
729 assert_eq!(issues[0].code, issue_codes::LINT_ST_011);
730 }
731
732 #[test]
733 fn defers_when_unqualified_references_exist() {
734 let issues = run("select a from b left join c using(d)");
735 assert!(issues.is_empty());
736 }
737
738 #[test]
739 fn allows_outer_join_when_joined_source_is_referenced() {
740 let issues = run("select widget.id, inventor.id from widget left join inventor on widget.inventor_id = inventor.id");
741 assert!(issues.is_empty());
742 }
743
744 #[test]
745 fn allows_outer_join_when_joined_source_only_referenced_in_query_order_by() {
746 let issues = run("select a.id from a left join b on a.id = b.id order by b.id");
747 assert!(issues.is_empty());
748 }
749
750 #[test]
751 fn does_not_flag_base_from_source_as_unused_with_using_join() {
752 let issues = run("select c.id from b left join c using(id)");
753 assert!(issues.is_empty());
754 }
755
756 #[test]
757 fn allows_unqualified_wildcard_projection() {
758 let issues = run("select * from a left join b on a.id = b.id");
759 assert!(issues.is_empty());
760 }
761
762 #[test]
763 fn detects_unused_join_in_subquery_scope() {
764 let issues = run(
765 "SELECT a.col1 FROM a LEFT JOIN b ON a.id = b.a_id WHERE a.some_column IN (SELECT c.some_column FROM c WHERE c.other_column = a.col)",
766 );
767 assert_eq!(issues.len(), 1);
768 }
769
770 #[test]
771 fn allows_join_reference_inside_subquery() {
772 let issues = run(
773 "SELECT a.col1 FROM a LEFT JOIN b ON a.id = b.a_id WHERE a.some_column IN (SELECT c.some_column FROM c WHERE c.other_column = b.col)",
774 );
775 assert!(issues.is_empty());
776 }
777
778 #[test]
779 fn flags_unused_outer_join_inside_derived_subquery() {
780 let issues =
781 run("SELECT * FROM (SELECT t1.col1 FROM db1.t1 LEFT JOIN t2 ON t1.id = t2.id)");
782 assert_eq!(issues.len(), 1);
783 assert_eq!(issues[0].code, issue_codes::LINT_ST_011);
784 }
785
786 #[test]
787 fn allows_outer_join_table_referenced_by_another_join_condition() {
788 let issues =
789 run("SELECT a.id FROM a LEFT JOIN b ON a.id = b.a_id LEFT JOIN c ON b.c_id = c.id");
790 assert_eq!(issues.len(), 1);
791 assert_eq!(issues[0].code, issue_codes::LINT_ST_011);
792 }
793
794 #[test]
795 fn flags_unused_outer_join_in_multi_root_from_clause() {
796 let issues = run("SELECT a.id FROM a, b LEFT JOIN c ON b.id = c.id");
797 assert_eq!(issues.len(), 1);
798 assert_eq!(issues[0].code, issue_codes::LINT_ST_011);
799 }
800
801 #[test]
802 fn allows_used_outer_join_in_multi_root_from_clause() {
803 let issues = run("SELECT c.id FROM a, b LEFT JOIN c ON b.id = c.id");
804 assert!(issues.is_empty());
805 }
806
807 #[test]
808 fn allows_outer_join_source_referenced_by_later_unnest_join_relation() {
809 let issues = run(
810 "SELECT ft.id, n.generic_field FROM fact_table AS ft LEFT JOIN UNNEST(ft.generic_array) AS g LEFT JOIN UNNEST(g.nested_array) AS n",
811 );
812 assert!(issues.is_empty());
813 }
814
815 #[test]
816 fn allows_outer_join_source_referenced_in_named_window_clause() {
817 let issues = run(
818 "SELECT sum(a.value) OVER w FROM a LEFT JOIN b ON a.id = b.id WINDOW w AS (PARTITION BY b.group_key)",
819 );
820 assert!(issues.is_empty());
821 }
822
823 #[test]
824 fn defers_when_named_window_clause_has_unqualified_reference() {
825 let issues = run(
826 "SELECT sum(a.value) OVER w FROM a LEFT JOIN b ON a.id = b.id WINDOW w AS (PARTITION BY group_key)",
827 );
828 assert!(issues.is_empty());
829 }
830
831 #[test]
832 fn allows_outer_join_source_referenced_in_distinct_on_clause() {
833 let issues = run("SELECT DISTINCT ON (b.id) a.id FROM a LEFT JOIN b ON a.id = b.id");
834 assert!(issues.is_empty());
835 }
836
837 #[test]
838 fn defers_when_distinct_on_clause_has_unqualified_reference() {
839 let issues = run("SELECT DISTINCT ON (id) a.id FROM a LEFT JOIN b ON a.id = b.id");
840 assert!(issues.is_empty());
841 }
842
843 #[test]
844 fn allows_outer_join_source_referenced_in_cluster_by_clause() {
845 let issues = run("SELECT a.id FROM a LEFT JOIN b ON a.id = b.id CLUSTER BY b.id");
846 assert!(issues.is_empty());
847 }
848
849 #[test]
850 fn allows_outer_join_source_referenced_in_distribute_by_clause() {
851 let issues = run("SELECT a.id FROM a LEFT JOIN b ON a.id = b.id DISTRIBUTE BY b.id");
852 assert!(issues.is_empty());
853 }
854
855 #[test]
856 fn allows_outer_join_source_referenced_with_mysql_backtick_quoted_table_name() {
857 let issues = run_in_dialect(
858 "SELECT `test`.one, `test-2`.two FROM `test` LEFT JOIN `test-2` ON `test`.id = `test-2`.id",
859 Dialect::Mysql,
860 );
861 assert!(issues.is_empty());
862 }
863
864 #[test]
865 fn allows_outer_join_source_referenced_with_mssql_bracket_quoted_table_name() {
866 let issues = run_in_dialect(
867 "SELECT [test].one, [test-2].two FROM [test] LEFT JOIN [test-2] ON [test].id = [test-2].id",
868 Dialect::Mssql,
869 );
870 assert!(issues.is_empty());
871 }
872
873 #[test]
874 fn allows_snowflake_qualified_wildcard_exclude_for_joined_source() {
875 let issues = run_in_dialect(
876 "select \
877 simulation_source_data_reference.*, \
878 sourcings.* exclude sourcing_job_id \
879 from simulation_source_data_reference \
880 left join sourcings \
881 on simulation_source_data_reference.sourcing_job_id = sourcings.sourcing_job_id",
882 Dialect::Snowflake,
883 );
884 assert!(issues.is_empty());
885 }
886
887 #[test]
888 fn allows_outer_join_source_referenced_in_hive_lateral_view_clause() {
889 let issues = run_in_dialect(
890 "SELECT a.id FROM a LEFT JOIN b ON a.id = b.id LATERAL VIEW explode(b.items) lv AS item",
891 Dialect::Hive,
892 );
893 assert!(issues.is_empty());
894 }
895
896 #[test]
897 fn defers_when_hive_lateral_view_clause_has_unqualified_reference() {
898 let issues = run_in_dialect(
899 "SELECT a.id FROM a LEFT JOIN b ON a.id = b.id LATERAL VIEW explode(items) lv AS item",
900 Dialect::Hive,
901 );
902 assert!(issues.is_empty());
903 }
904
905 #[test]
906 fn allows_outer_join_source_referenced_in_connect_by_clause() {
907 let issues = run_in_dialect(
908 "SELECT a.id FROM a LEFT JOIN b ON a.id = b.id START WITH b.id IS NOT NULL CONNECT BY PRIOR a.id = b.id",
909 Dialect::Snowflake,
910 );
911 assert!(issues.is_empty());
912 }
913
914 #[test]
915 fn defers_when_connect_by_clause_has_unqualified_reference() {
916 let issues = run_in_dialect(
917 "SELECT a.id FROM a LEFT JOIN b ON a.id = b.id START WITH id IS NOT NULL CONNECT BY PRIOR a.id = b.id",
918 Dialect::Snowflake,
919 );
920 assert!(issues.is_empty());
921 }
922}