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