1use crate::linter::config::LintConfig;
7use crate::linter::rule::{LintContext, LintRule};
8use crate::types::{issue_codes, Dialect, Issue, IssueAutofixApplicability, IssuePatchEdit};
9use sqlparser::ast::*;
10use sqlparser::tokenizer::{Token, TokenWithSpan, Tokenizer, Whitespace};
11use std::collections::{HashMap, HashSet};
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14enum AliasCaseCheck {
15 Dialect,
16 CaseInsensitive,
17 QuotedCsNakedUpper,
18 QuotedCsNakedLower,
19 CaseSensitive,
20}
21
22impl AliasCaseCheck {
23 fn from_config(config: &LintConfig) -> Self {
24 match config
25 .rule_option_str(issue_codes::LINT_AL_005, "alias_case_check")
26 .unwrap_or("dialect")
27 .to_ascii_lowercase()
28 .as_str()
29 {
30 "case_insensitive" => Self::CaseInsensitive,
31 "quoted_cs_naked_upper" => Self::QuotedCsNakedUpper,
32 "quoted_cs_naked_lower" => Self::QuotedCsNakedLower,
33 "case_sensitive" => Self::CaseSensitive,
34 _ => Self::Dialect,
35 }
36 }
37}
38
39#[derive(Clone, Debug, Eq, Hash, PartialEq)]
40struct AliasRef {
41 name: String,
42 quoted: bool,
43 relation_key: Option<String>,
44}
45
46#[derive(Clone, Debug, Eq, Hash, PartialEq)]
47struct QualifierRef {
48 name: String,
49 quoted: bool,
50}
51
52pub struct UnusedTableAlias {
53 alias_case_check: AliasCaseCheck,
54}
55
56impl UnusedTableAlias {
57 pub fn from_config(config: &LintConfig) -> Self {
58 Self {
59 alias_case_check: AliasCaseCheck::from_config(config),
60 }
61 }
62}
63
64impl Default for UnusedTableAlias {
65 fn default() -> Self {
66 Self {
67 alias_case_check: AliasCaseCheck::Dialect,
68 }
69 }
70}
71
72impl LintRule for UnusedTableAlias {
73 fn code(&self) -> &'static str {
74 issue_codes::LINT_AL_005
75 }
76
77 fn name(&self) -> &'static str {
78 "Unused table alias"
79 }
80
81 fn description(&self) -> &'static str {
82 "Tables should not be aliased if that alias is not used."
83 }
84
85 fn check(&self, stmt: &Statement, ctx: &LintContext) -> Vec<Issue> {
86 let mut issues = Vec::new();
87 match stmt {
88 Statement::Query(q) => check_query(q, self.alias_case_check, ctx, &mut issues),
89 Statement::Insert(ins) => {
90 if let Some(ref source) = ins.source {
91 check_query(source, self.alias_case_check, ctx, &mut issues);
92 }
93 }
94 Statement::CreateView { query, .. } => {
95 check_query(query, self.alias_case_check, ctx, &mut issues)
96 }
97 Statement::CreateTable(create) => {
98 if let Some(ref q) = create.query {
99 check_query(q, self.alias_case_check, ctx, &mut issues);
100 }
101 }
102 Statement::Delete(delete) => {
103 check_delete(delete, self.alias_case_check, ctx, &mut issues);
104 }
105 _ => {}
106 }
107
108 if let Some(first_issue) = issues.first_mut() {
109 let autofix_edits: Vec<IssuePatchEdit> = al005_legacy_autofix_edits(
110 ctx.statement_sql(),
111 ctx.dialect(),
112 self.alias_case_check,
113 )
114 .into_iter()
115 .map(|(start, end)| IssuePatchEdit::new(ctx.span_from_statement_offset(start, end), ""))
116 .collect();
117 if !autofix_edits.is_empty() {
118 *first_issue = first_issue
119 .clone()
120 .with_autofix_edits(IssueAutofixApplicability::Safe, autofix_edits);
121 }
122 }
123
124 issues
125 }
126}
127
128fn check_query(
129 query: &Query,
130 alias_case_check: AliasCaseCheck,
131 ctx: &LintContext,
132 issues: &mut Vec<Issue>,
133) {
134 if let Some(ref with) = query.with {
135 for cte in &with.cte_tables {
136 check_query(&cte.query, alias_case_check, ctx, issues);
137 }
138 }
139 match query.body.as_ref() {
140 SetExpr::Select(select) => check_select(
141 select,
142 query.order_by.as_ref(),
143 alias_case_check,
144 ctx,
145 issues,
146 ),
147 _ => check_set_expr(&query.body, alias_case_check, ctx, issues),
148 }
149}
150
151fn check_set_expr(
152 body: &SetExpr,
153 alias_case_check: AliasCaseCheck,
154 ctx: &LintContext,
155 issues: &mut Vec<Issue>,
156) {
157 match body {
158 SetExpr::Select(select) => {
159 check_select(select, None, alias_case_check, ctx, issues);
160 }
161 SetExpr::Query(q) => check_query(q, alias_case_check, ctx, issues),
162 SetExpr::SetOperation { left, right, .. } => {
163 check_set_expr(left, alias_case_check, ctx, issues);
164 check_set_expr(right, alias_case_check, ctx, issues);
165 }
166 _ => {}
167 }
168}
169
170fn check_select(
171 select: &Select,
172 order_by: Option<&OrderBy>,
173 alias_case_check: AliasCaseCheck,
174 ctx: &LintContext,
175 issues: &mut Vec<Issue>,
176) {
177 for from_item in &select.from {
178 check_table_factor_subqueries(&from_item.relation, alias_case_check, ctx, issues);
179 for join in &from_item.joins {
180 check_table_factor_subqueries(&join.relation, alias_case_check, ctx, issues);
181 }
182 }
183
184 let mut aliases: HashMap<String, AliasRef> = HashMap::new();
186 for from_item in &select.from {
187 collect_aliases(&from_item.relation, ctx.dialect(), &mut aliases);
188 for join in &from_item.joins {
189 collect_aliases(&join.relation, ctx.dialect(), &mut aliases);
190 }
191 }
192
193 if aliases.is_empty() {
194 return;
195 }
196
197 let mut used_prefixes: HashSet<QualifierRef> = HashSet::new();
198 collect_identifier_prefixes_from_select(select, order_by, ctx.dialect(), &mut used_prefixes);
199
200 if matches!(ctx.dialect(), Dialect::Redshift) {
201 if let Some(qualify) = &select.qualify {
202 if include_qualify_alias_references(ctx.dialect(), select) {
203 for alias in aliases.values() {
204 if redshift_qualify_uses_alias_prefixed_identifier(qualify, &alias.name) {
205 used_prefixes.insert(QualifierRef {
206 name: alias.name.clone(),
207 quoted: alias.quoted,
208 });
209 }
210 }
211 }
212 }
213 }
214
215 emit_unused_alias_issues(
216 &aliases,
217 &used_prefixes,
218 alias_case_check,
219 ctx.dialect(),
220 ctx.statement_index,
221 issues,
222 );
223}
224
225fn check_delete(
226 delete: &Delete,
227 alias_case_check: AliasCaseCheck,
228 ctx: &LintContext,
229 issues: &mut Vec<Issue>,
230) {
231 let mut aliases: HashMap<String, AliasRef> = HashMap::new();
232 let mut used_prefixes: HashSet<QualifierRef> = HashSet::new();
233
234 for table in delete_source_tables(delete) {
235 check_table_factor_subqueries(&table.relation, alias_case_check, ctx, issues);
236 collect_aliases(&table.relation, ctx.dialect(), &mut aliases);
237 collect_identifier_prefixes_from_table_factor(
238 &table.relation,
239 ctx.dialect(),
240 &mut used_prefixes,
241 );
242
243 for join in &table.joins {
244 check_table_factor_subqueries(&join.relation, alias_case_check, ctx, issues);
245 collect_aliases(&join.relation, ctx.dialect(), &mut aliases);
246 collect_identifier_prefixes_from_table_factor(
247 &join.relation,
248 ctx.dialect(),
249 &mut used_prefixes,
250 );
251 if let Some(constraint) = join_constraint(&join.join_operator) {
252 collect_identifier_prefixes(constraint, ctx.dialect(), &mut used_prefixes);
253 }
254 }
255 }
256
257 if let Some(selection) = &delete.selection {
258 collect_identifier_prefixes(selection, ctx.dialect(), &mut used_prefixes);
259 }
260 if let Some(returning) = &delete.returning {
261 for item in returning {
262 collect_identifier_prefixes_from_select_item(item, ctx.dialect(), &mut used_prefixes);
263 }
264 }
265 for order_expr in &delete.order_by {
266 collect_identifier_prefixes(&order_expr.expr, ctx.dialect(), &mut used_prefixes);
267 }
268 if let Some(limit) = &delete.limit {
269 collect_identifier_prefixes(limit, ctx.dialect(), &mut used_prefixes);
270 }
271
272 emit_unused_alias_issues(
273 &aliases,
274 &used_prefixes,
275 alias_case_check,
276 ctx.dialect(),
277 ctx.statement_index,
278 issues,
279 );
280}
281
282fn delete_source_tables(delete: &Delete) -> Vec<&TableWithJoins> {
283 let mut tables = Vec::new();
284
285 match &delete.from {
286 FromTable::WithFromKeyword(from) | FromTable::WithoutKeyword(from) => {
287 tables.extend(from.iter());
288 }
289 }
290
291 if let Some(using_tables) = &delete.using {
292 tables.extend(using_tables.iter());
293 }
294
295 tables
296}
297
298fn emit_unused_alias_issues(
299 aliases: &HashMap<String, AliasRef>,
300 used_prefixes: &HashSet<QualifierRef>,
301 alias_case_check: AliasCaseCheck,
302 dialect: Dialect,
303 statement_index: usize,
304 issues: &mut Vec<Issue>,
305) {
306 if aliases.is_empty() {
307 return;
308 }
309
310 let mut used_alias_names: HashSet<String> = HashSet::new();
311 for alias in aliases.values() {
312 let used = used_prefixes
313 .iter()
314 .any(|prefix| qualifier_matches_alias(prefix, alias, alias_case_check, dialect));
315 if used {
316 used_alias_names.insert(alias.name.clone());
317 }
318 }
319
320 let mut relation_alias_counts: HashMap<String, usize> = HashMap::new();
321 let mut relations_with_used_alias: HashSet<String> = HashSet::new();
322 for alias in aliases.values() {
323 let Some(relation_key) = &alias.relation_key else {
324 continue;
325 };
326 *relation_alias_counts
327 .entry(relation_key.clone())
328 .or_insert(0) += 1;
329 if used_alias_names.contains(&alias.name) {
330 relations_with_used_alias.insert(relation_key.clone());
331 }
332 }
333
334 for alias in aliases.values() {
335 if used_alias_names.contains(&alias.name) {
336 continue;
337 }
338
339 let repeated_self_join_alias_exempt = alias.relation_key.as_ref().is_some_and(|key| {
340 relation_alias_counts.get(key).copied().unwrap_or_default() > 1
341 && relations_with_used_alias.contains(key)
342 });
343
344 if repeated_self_join_alias_exempt {
345 continue;
346 }
347
348 issues.push(
349 Issue::warning(
350 issue_codes::LINT_AL_005,
351 format!(
352 "Table alias '{}' is defined but never referenced.",
353 alias.name
354 ),
355 )
356 .with_statement(statement_index),
357 );
358 }
359}
360
361fn collect_identifier_prefixes_from_order_by(
362 order_by: &OrderBy,
363 dialect: Dialect,
364 prefixes: &mut HashSet<QualifierRef>,
365) {
366 if let OrderByKind::Expressions(order_by_exprs) = &order_by.kind {
367 for order_expr in order_by_exprs {
368 collect_identifier_prefixes(&order_expr.expr, dialect, prefixes);
369 }
370 }
371}
372
373fn collect_identifier_prefixes_from_query(
374 query: &Query,
375 dialect: Dialect,
376 prefixes: &mut HashSet<QualifierRef>,
377) {
378 if let Some(ref with) = query.with {
379 for cte in &with.cte_tables {
380 collect_identifier_prefixes_from_query(&cte.query, dialect, prefixes);
381 }
382 }
383
384 match query.body.as_ref() {
385 SetExpr::Select(select) => {
386 collect_identifier_prefixes_from_select(
387 select,
388 query.order_by.as_ref(),
389 dialect,
390 prefixes,
391 );
392 }
393 SetExpr::Query(q) => collect_identifier_prefixes_from_query(q, dialect, prefixes),
394 SetExpr::SetOperation { left, right, .. } => {
395 collect_identifier_prefixes_from_set_expr(left, dialect, prefixes);
396 collect_identifier_prefixes_from_set_expr(right, dialect, prefixes);
397 }
398 _ => {}
399 }
400}
401
402fn collect_identifier_prefixes_from_set_expr(
403 body: &SetExpr,
404 dialect: Dialect,
405 prefixes: &mut HashSet<QualifierRef>,
406) {
407 match body {
408 SetExpr::Select(select) => {
409 collect_identifier_prefixes_from_select(select, None, dialect, prefixes)
410 }
411 SetExpr::Query(q) => collect_identifier_prefixes_from_query(q, dialect, prefixes),
412 SetExpr::SetOperation { left, right, .. } => {
413 collect_identifier_prefixes_from_set_expr(left, dialect, prefixes);
414 collect_identifier_prefixes_from_set_expr(right, dialect, prefixes);
415 }
416 _ => {}
417 }
418}
419
420fn collect_identifier_prefixes_from_select(
421 select: &Select,
422 order_by: Option<&OrderBy>,
423 dialect: Dialect,
424 prefixes: &mut HashSet<QualifierRef>,
425) {
426 for item in &select.projection {
427 collect_identifier_prefixes_from_select_item(item, dialect, prefixes);
428 }
429 if let Some(ref prewhere) = select.prewhere {
430 collect_identifier_prefixes(prewhere, dialect, prefixes);
431 }
432 if let Some(ref selection) = select.selection {
433 collect_identifier_prefixes(selection, dialect, prefixes);
434 }
435 if let GroupByExpr::Expressions(exprs, _) = &select.group_by {
436 for expr in exprs {
437 collect_identifier_prefixes(expr, dialect, prefixes);
438 }
439 }
440 for expr in &select.cluster_by {
441 collect_identifier_prefixes(expr, dialect, prefixes);
442 }
443 for expr in &select.distribute_by {
444 collect_identifier_prefixes(expr, dialect, prefixes);
445 }
446 for sort_expr in &select.sort_by {
447 collect_identifier_prefixes(&sort_expr.expr, dialect, prefixes);
448 }
449 if let Some(ref having) = select.having {
450 collect_identifier_prefixes(having, dialect, prefixes);
451 }
452 if let Some(ref qualify) = select.qualify {
453 if include_qualify_alias_references(dialect, select) {
454 collect_identifier_prefixes(qualify, dialect, prefixes);
455 }
456 }
457 if let Some(Distinct::On(exprs)) = &select.distinct {
458 for expr in exprs {
459 collect_identifier_prefixes(expr, dialect, prefixes);
460 }
461 }
462 for named_window in &select.named_window {
463 if let NamedWindowExpr::WindowSpec(spec) = &named_window.1 {
464 for expr in &spec.partition_by {
465 collect_identifier_prefixes(expr, dialect, prefixes);
466 }
467 for order_expr in &spec.order_by {
468 collect_identifier_prefixes(&order_expr.expr, dialect, prefixes);
469 }
470 }
471 }
472 for lateral_view in &select.lateral_views {
473 collect_identifier_prefixes(&lateral_view.lateral_view, dialect, prefixes);
474 }
475 if let Some(connect_by) = &select.connect_by {
476 collect_identifier_prefixes(&connect_by.condition, dialect, prefixes);
477 for relationship in &connect_by.relationships {
478 collect_identifier_prefixes(relationship, dialect, prefixes);
479 }
480 }
481 for from_item in &select.from {
482 collect_identifier_prefixes_from_table_factor(&from_item.relation, dialect, prefixes);
483 for join in &from_item.joins {
484 collect_identifier_prefixes_from_table_factor(&join.relation, dialect, prefixes);
485 if let Some(constraint) = join_constraint(&join.join_operator) {
486 collect_identifier_prefixes(constraint, dialect, prefixes);
487 }
488 }
489 }
490 if let Some(order_by) = order_by {
491 collect_identifier_prefixes_from_order_by(order_by, dialect, prefixes);
492 }
493}
494
495fn collect_aliases(
496 relation: &TableFactor,
497 dialect: Dialect,
498 aliases: &mut HashMap<String, AliasRef>,
499) {
500 match relation {
501 TableFactor::Table {
502 name,
503 alias: Some(alias),
504 args,
505 ..
506 } => {
507 if args.is_some() {
508 return;
509 }
510 if is_implicit_array_relation_alias(dialect, name, aliases) {
511 return;
512 }
513 let table_name = name.to_string();
514 let alias_name = alias.name.value.clone();
515 if alias_name.to_uppercase() != table_name.to_uppercase() {
517 aliases.insert(
518 alias_name.clone(),
519 AliasRef {
520 name: alias_name,
521 quoted: alias.name.quote_style.is_some(),
522 relation_key: Some(table_name.to_ascii_uppercase()),
523 },
524 );
525 }
526 }
527 TableFactor::Derived {
528 subquery,
529 alias: Some(alias),
530 ..
531 } => {
532 if derived_values_alias_can_be_unused(dialect, subquery) {
533 aliases.insert(
534 alias.name.value.clone(),
535 AliasRef {
536 name: alias.name.value.clone(),
537 quoted: alias.name.quote_style.is_some(),
538 relation_key: None,
539 },
540 );
541 }
542 }
543 TableFactor::Function {
544 lateral: true,
545 alias: Some(alias),
546 ..
547 } => {
548 aliases.insert(
549 alias.name.value.clone(),
550 AliasRef {
551 name: alias.name.value.clone(),
552 quoted: alias.name.quote_style.is_some(),
553 relation_key: None,
554 },
555 );
556 }
557 TableFactor::NestedJoin {
558 table_with_joins, ..
559 } => {
560 collect_aliases(&table_with_joins.relation, dialect, aliases);
561 for join in &table_with_joins.joins {
562 collect_aliases(&join.relation, dialect, aliases);
563 }
564 }
565 TableFactor::Pivot { table, .. }
566 | TableFactor::Unpivot { table, .. }
567 | TableFactor::MatchRecognize { table, .. } => collect_aliases(table, dialect, aliases),
568 _ => {}
569 }
570}
571
572fn collect_identifier_prefixes_from_select_item(
573 item: &SelectItem,
574 dialect: Dialect,
575 prefixes: &mut HashSet<QualifierRef>,
576) {
577 match item {
578 SelectItem::UnnamedExpr(expr) | SelectItem::ExprWithAlias { expr, .. } => {
579 collect_identifier_prefixes(expr, dialect, prefixes);
580 }
581 SelectItem::QualifiedWildcard(name, _) => {
582 let name_str = name.to_string();
583 if let Some(prefix) = name_str.split('.').next() {
584 prefixes.insert(QualifierRef {
585 name: prefix
586 .trim_matches(|ch| matches!(ch, '"' | '`' | '\'' | '[' | ']'))
587 .to_string(),
588 quoted: prefix.starts_with('"')
589 || prefix.starts_with('`')
590 || prefix.starts_with('['),
591 });
592 }
593 }
594 _ => {}
595 }
596}
597
598fn collect_identifier_prefixes(
599 expr: &Expr,
600 dialect: Dialect,
601 prefixes: &mut HashSet<QualifierRef>,
602) {
603 match expr {
604 Expr::CompoundIdentifier(parts) => {
605 if parts.len() >= 2 {
606 prefixes.insert(QualifierRef {
607 name: parts[0].value.clone(),
608 quoted: parts[0].quote_style.is_some(),
609 });
610 }
611 }
612 Expr::BinaryOp { left, right, .. } => {
613 collect_identifier_prefixes(left, dialect, prefixes);
614 collect_identifier_prefixes(right, dialect, prefixes);
615 }
616 Expr::UnaryOp { expr: inner, .. } => collect_identifier_prefixes(inner, dialect, prefixes),
617 Expr::Nested(inner) => collect_identifier_prefixes(inner, dialect, prefixes),
618 Expr::Function(func) => {
619 let function_name = function_name(func);
620 if let FunctionArguments::List(arg_list) = &func.args {
621 for (index, arg) in arg_list.args.iter().enumerate() {
622 match arg {
623 FunctionArg::Unnamed(FunctionArgExpr::Expr(e))
624 | FunctionArg::Named {
625 arg: FunctionArgExpr::Expr(e),
626 ..
627 } => {
628 collect_identifier_prefixes(e, dialect, prefixes);
629 if function_arg_is_table_alias_reference(
630 dialect,
631 function_name.as_str(),
632 index,
633 ) {
634 if let Expr::Identifier(ident) = e {
635 prefixes.insert(QualifierRef {
636 name: ident.value.clone(),
637 quoted: ident.quote_style.is_some(),
638 });
639 }
640 }
641 }
642 _ => {}
643 }
644 }
645 }
646 if let Some(filter) = &func.filter {
647 collect_identifier_prefixes(filter, dialect, prefixes);
648 }
649 for order_expr in &func.within_group {
650 collect_identifier_prefixes(&order_expr.expr, dialect, prefixes);
651 }
652 if let Some(WindowType::WindowSpec(spec)) = &func.over {
653 for expr in &spec.partition_by {
654 collect_identifier_prefixes(expr, dialect, prefixes);
655 }
656 for order_expr in &spec.order_by {
657 collect_identifier_prefixes(&order_expr.expr, dialect, prefixes);
658 }
659 }
660 }
661 Expr::IsNull(inner)
662 | Expr::IsNotNull(inner)
663 | Expr::Cast { expr: inner, .. }
664 | Expr::JsonAccess { value: inner, .. } => {
665 collect_identifier_prefixes(inner, dialect, prefixes);
666 }
667 Expr::Case {
668 operand,
669 conditions,
670 else_result,
671 ..
672 } => {
673 if let Some(op) = operand {
674 collect_identifier_prefixes(op, dialect, prefixes);
675 }
676 for case_when in conditions {
677 collect_identifier_prefixes(&case_when.condition, dialect, prefixes);
678 collect_identifier_prefixes(&case_when.result, dialect, prefixes);
679 }
680 if let Some(el) = else_result {
681 collect_identifier_prefixes(el, dialect, prefixes);
682 }
683 }
684 Expr::InList { expr, list, .. } => {
685 collect_identifier_prefixes(expr, dialect, prefixes);
686 for item in list {
687 collect_identifier_prefixes(item, dialect, prefixes);
688 }
689 }
690 Expr::InSubquery { expr, subquery, .. } => {
691 collect_identifier_prefixes(expr, dialect, prefixes);
692 collect_identifier_prefixes_from_query(subquery, dialect, prefixes);
693 }
694 Expr::AnyOp { left, right, .. } | Expr::AllOp { left, right, .. } => {
695 collect_identifier_prefixes(left, dialect, prefixes);
696 collect_identifier_prefixes(right, dialect, prefixes);
697 }
698 Expr::Subquery(subquery) | Expr::Exists { subquery, .. } => {
699 collect_identifier_prefixes_from_query(subquery, dialect, prefixes);
700 }
701 Expr::Between {
702 expr, low, high, ..
703 } => {
704 collect_identifier_prefixes(expr, dialect, prefixes);
705 collect_identifier_prefixes(low, dialect, prefixes);
706 collect_identifier_prefixes(high, dialect, prefixes);
707 }
708 _ => {}
709 }
710}
711
712fn collect_identifier_prefixes_from_table_factor(
713 table_factor: &TableFactor,
714 dialect: Dialect,
715 prefixes: &mut HashSet<QualifierRef>,
716) {
717 match table_factor {
718 TableFactor::Table { name, .. } => {
719 if let Some(prefix) = implicit_array_relation_prefix(dialect, name) {
720 prefixes.insert(prefix);
721 }
722 }
723 TableFactor::Derived {
724 lateral: true,
725 subquery,
726 ..
727 } => collect_identifier_prefixes_from_query(subquery, dialect, prefixes),
728 TableFactor::TableFunction { expr, .. } => {
729 collect_identifier_prefixes(expr, dialect, prefixes);
730 }
731 TableFactor::Function { args, .. } => {
732 for arg in args {
733 collect_identifier_prefixes_from_function_arg(arg, dialect, prefixes);
734 }
735 }
736 TableFactor::UNNEST { array_exprs, .. } => {
737 for expr in array_exprs {
738 collect_identifier_prefixes(expr, dialect, prefixes);
739 }
740 }
741 TableFactor::JsonTable { json_expr, .. } | TableFactor::OpenJsonTable { json_expr, .. } => {
742 collect_identifier_prefixes(json_expr, dialect, prefixes);
743 }
744 TableFactor::NestedJoin {
745 table_with_joins, ..
746 } => {
747 collect_identifier_prefixes_from_table_factor(
748 &table_with_joins.relation,
749 dialect,
750 prefixes,
751 );
752 for join in &table_with_joins.joins {
753 collect_identifier_prefixes_from_table_factor(&join.relation, dialect, prefixes);
754 if let Some(constraint) = join_constraint(&join.join_operator) {
755 collect_identifier_prefixes(constraint, dialect, prefixes);
756 }
757 }
758 }
759 TableFactor::Pivot {
760 table,
761 aggregate_functions,
762 value_column,
763 default_on_null,
764 ..
765 } => {
766 collect_identifier_prefixes_from_table_factor(table, dialect, prefixes);
767 for expr_with_alias in aggregate_functions {
768 collect_identifier_prefixes(&expr_with_alias.expr, dialect, prefixes);
769 }
770 for expr in value_column {
771 collect_identifier_prefixes(expr, dialect, prefixes);
772 }
773 if let Some(expr) = default_on_null {
774 collect_identifier_prefixes(expr, dialect, prefixes);
775 }
776 }
777 TableFactor::Unpivot {
778 table,
779 value,
780 columns,
781 ..
782 } => {
783 collect_identifier_prefixes_from_table_factor(table, dialect, prefixes);
784 collect_identifier_prefixes(value, dialect, prefixes);
785 for expr_with_alias in columns {
786 collect_identifier_prefixes(&expr_with_alias.expr, dialect, prefixes);
787 }
788 }
789 TableFactor::MatchRecognize {
790 table,
791 partition_by,
792 order_by,
793 measures,
794 ..
795 } => {
796 collect_identifier_prefixes_from_table_factor(table, dialect, prefixes);
797 for expr in partition_by {
798 collect_identifier_prefixes(expr, dialect, prefixes);
799 }
800 for order in order_by {
801 collect_identifier_prefixes(&order.expr, dialect, prefixes);
802 }
803 for measure in measures {
804 collect_identifier_prefixes(&measure.expr, dialect, prefixes);
805 }
806 }
807 TableFactor::XmlTable { row_expression, .. } => {
808 collect_identifier_prefixes(row_expression, dialect, prefixes);
809 }
810 _ => {}
811 }
812}
813
814fn collect_identifier_prefixes_from_function_arg(
815 arg: &FunctionArg,
816 dialect: Dialect,
817 prefixes: &mut HashSet<QualifierRef>,
818) {
819 match arg {
820 FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
821 | FunctionArg::Named {
822 arg: FunctionArgExpr::Expr(expr),
823 ..
824 } => collect_identifier_prefixes(expr, dialect, prefixes),
825 _ => {}
826 }
827}
828
829fn function_name(function: &Function) -> String {
830 function
831 .name
832 .0
833 .last()
834 .and_then(ObjectNamePart::as_ident)
835 .map(|ident| ident.value.to_ascii_uppercase())
836 .unwrap_or_default()
837}
838
839fn function_arg_is_table_alias_reference(
840 dialect: Dialect,
841 function_name: &str,
842 arg_index: usize,
843) -> bool {
844 matches!(dialect, Dialect::Bigquery) && arg_index == 0 && function_name == "TO_JSON_STRING"
845}
846
847fn include_qualify_alias_references(dialect: Dialect, select: &Select) -> bool {
848 !matches!(dialect, Dialect::Redshift) || select.selection.is_none()
851}
852
853fn derived_values_alias_can_be_unused(dialect: Dialect, subquery: &Query) -> bool {
854 matches!(dialect, Dialect::Databricks) && matches!(subquery.body.as_ref(), SetExpr::Values(_))
858}
859
860fn redshift_qualify_uses_alias_prefixed_identifier(expr: &Expr, alias: &str) -> bool {
861 match expr {
862 Expr::Identifier(identifier) => {
863 let value = identifier.value.as_str();
864 value
865 .strip_prefix(alias)
866 .is_some_and(|suffix| suffix.starts_with('_'))
867 || value
868 .to_ascii_uppercase()
869 .strip_prefix(&alias.to_ascii_uppercase())
870 .is_some_and(|suffix| suffix.starts_with('_'))
871 }
872 Expr::CompoundIdentifier(_) => false,
873 Expr::BinaryOp { left, right, .. }
874 | Expr::AnyOp { left, right, .. }
875 | Expr::AllOp { left, right, .. } => {
876 redshift_qualify_uses_alias_prefixed_identifier(left, alias)
877 || redshift_qualify_uses_alias_prefixed_identifier(right, alias)
878 }
879 Expr::UnaryOp { expr: inner, .. }
880 | Expr::Nested(inner)
881 | Expr::IsNull(inner)
882 | Expr::IsNotNull(inner)
883 | Expr::Cast { expr: inner, .. } => {
884 redshift_qualify_uses_alias_prefixed_identifier(inner, alias)
885 }
886 Expr::InList { expr, list, .. } => {
887 redshift_qualify_uses_alias_prefixed_identifier(expr, alias)
888 || list
889 .iter()
890 .any(|item| redshift_qualify_uses_alias_prefixed_identifier(item, alias))
891 }
892 Expr::Between {
893 expr, low, high, ..
894 } => {
895 redshift_qualify_uses_alias_prefixed_identifier(expr, alias)
896 || redshift_qualify_uses_alias_prefixed_identifier(low, alias)
897 || redshift_qualify_uses_alias_prefixed_identifier(high, alias)
898 }
899 Expr::Case {
900 operand,
901 conditions,
902 else_result,
903 ..
904 } => {
905 operand
906 .as_ref()
907 .is_some_and(|inner| redshift_qualify_uses_alias_prefixed_identifier(inner, alias))
908 || conditions.iter().any(|when| {
909 redshift_qualify_uses_alias_prefixed_identifier(&when.condition, alias)
910 || redshift_qualify_uses_alias_prefixed_identifier(&when.result, alias)
911 })
912 || else_result.as_ref().is_some_and(|inner| {
913 redshift_qualify_uses_alias_prefixed_identifier(inner, alias)
914 })
915 }
916 Expr::Function(function) => {
917 let args_match = if let FunctionArguments::List(arguments) = &function.args {
918 arguments.args.iter().any(|arg| match arg {
919 FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
920 | FunctionArg::Named {
921 arg: FunctionArgExpr::Expr(expr),
922 ..
923 } => redshift_qualify_uses_alias_prefixed_identifier(expr, alias),
924 _ => false,
925 })
926 } else {
927 false
928 };
929 let filter_match = function.filter.as_ref().is_some_and(|filter| {
930 redshift_qualify_uses_alias_prefixed_identifier(filter, alias)
931 });
932 let within_group_match = function.within_group.iter().any(|order_expr| {
933 redshift_qualify_uses_alias_prefixed_identifier(&order_expr.expr, alias)
934 });
935 let over_match = match &function.over {
936 Some(WindowType::WindowSpec(spec)) => {
937 spec.partition_by
938 .iter()
939 .any(|expr| redshift_qualify_uses_alias_prefixed_identifier(expr, alias))
940 || spec.order_by.iter().any(|order_expr| {
941 redshift_qualify_uses_alias_prefixed_identifier(&order_expr.expr, alias)
942 })
943 }
944 _ => false,
945 };
946 args_match || filter_match || within_group_match || over_match
947 }
948 _ => false,
949 }
950}
951
952fn implicit_array_relation_prefix(dialect: Dialect, name: &ObjectName) -> Option<QualifierRef> {
953 if !matches!(dialect, Dialect::Bigquery | Dialect::Redshift) {
954 return None;
955 }
956 if name.0.len() != 2 {
957 return None;
958 }
959 let first = name.0.first()?.as_ident()?;
960 Some(QualifierRef {
961 name: first.value.clone(),
962 quoted: first.quote_style.is_some(),
963 })
964}
965
966fn is_implicit_array_relation_alias(
967 dialect: Dialect,
968 name: &ObjectName,
969 aliases: &HashMap<String, AliasRef>,
970) -> bool {
971 let Some(prefix) = implicit_array_relation_prefix(dialect, name) else {
972 return false;
973 };
974 aliases
975 .values()
976 .any(|alias| alias.name.eq_ignore_ascii_case(&prefix.name))
977}
978
979fn qualifier_matches_alias(
980 qualifier: &QualifierRef,
981 alias: &AliasRef,
982 alias_case_check: AliasCaseCheck,
983 dialect: Dialect,
984) -> bool {
985 match alias_case_check {
986 AliasCaseCheck::CaseInsensitive => qualifier.name.eq_ignore_ascii_case(&alias.name),
987 AliasCaseCheck::CaseSensitive => qualifier.name == alias.name,
988 AliasCaseCheck::Dialect => {
989 normalize_identifier_for_dialect(&qualifier.name, qualifier.quoted, dialect)
990 == normalize_identifier_for_dialect(&alias.name, alias.quoted, dialect)
991 }
992 AliasCaseCheck::QuotedCsNakedUpper => {
993 normalize_case_for_mode(qualifier, alias_case_check)
994 == normalize_case_for_mode_alias(alias, alias_case_check)
995 }
996 AliasCaseCheck::QuotedCsNakedLower => {
997 normalize_case_for_mode(qualifier, alias_case_check)
998 == normalize_case_for_mode_alias(alias, alias_case_check)
999 }
1000 }
1001}
1002
1003fn normalize_identifier_for_dialect(identifier: &str, quoted: bool, dialect: Dialect) -> String {
1004 if quoted && !quoted_identifiers_case_insensitive_for_dialect(dialect) {
1005 identifier.to_string()
1006 } else {
1007 normalize_naked_identifier_for_dialect(identifier, dialect)
1008 }
1009}
1010
1011fn normalize_naked_identifier_for_dialect(identifier: &str, dialect: Dialect) -> String {
1012 if matches!(
1013 dialect,
1014 Dialect::Postgres
1015 | Dialect::Redshift
1016 | Dialect::Mysql
1017 | Dialect::Sqlite
1018 | Dialect::Mssql
1019 | Dialect::Clickhouse
1020 ) {
1021 identifier.to_ascii_lowercase()
1022 } else {
1023 identifier.to_ascii_uppercase()
1024 }
1025}
1026
1027fn quoted_identifiers_case_insensitive_for_dialect(dialect: Dialect) -> bool {
1028 matches!(
1029 dialect,
1030 Dialect::Duckdb | Dialect::Hive | Dialect::Sqlite | Dialect::Databricks
1031 )
1032}
1033
1034fn normalize_case_for_mode(reference: &QualifierRef, mode: AliasCaseCheck) -> String {
1035 match mode {
1036 AliasCaseCheck::QuotedCsNakedUpper => {
1037 if reference.quoted {
1038 reference.name.clone()
1039 } else {
1040 reference.name.to_ascii_uppercase()
1041 }
1042 }
1043 AliasCaseCheck::QuotedCsNakedLower => {
1044 if reference.quoted {
1045 reference.name.clone()
1046 } else {
1047 reference.name.to_ascii_lowercase()
1048 }
1049 }
1050 _ => reference.name.clone(),
1051 }
1052}
1053
1054fn normalize_case_for_mode_alias(alias: &AliasRef, mode: AliasCaseCheck) -> String {
1055 match mode {
1056 AliasCaseCheck::QuotedCsNakedUpper => {
1057 if alias.quoted {
1058 alias.name.clone()
1059 } else {
1060 alias.name.to_ascii_uppercase()
1061 }
1062 }
1063 AliasCaseCheck::QuotedCsNakedLower => {
1064 if alias.quoted {
1065 alias.name.clone()
1066 } else {
1067 alias.name.to_ascii_lowercase()
1068 }
1069 }
1070 _ => alias.name.clone(),
1071 }
1072}
1073
1074fn join_constraint(op: &JoinOperator) -> Option<&Expr> {
1075 let constraint = match op {
1076 JoinOperator::Join(c)
1077 | JoinOperator::Left(c)
1078 | JoinOperator::Inner(c)
1079 | JoinOperator::Right(c)
1080 | JoinOperator::LeftOuter(c)
1081 | JoinOperator::RightOuter(c)
1082 | JoinOperator::FullOuter(c)
1083 | JoinOperator::LeftSemi(c)
1084 | JoinOperator::RightSemi(c)
1085 | JoinOperator::LeftAnti(c)
1086 | JoinOperator::RightAnti(c) => c,
1087 _ => return None,
1088 };
1089 match constraint {
1090 JoinConstraint::On(expr) => Some(expr),
1091 _ => None,
1092 }
1093}
1094
1095fn check_table_factor_subqueries(
1096 relation: &TableFactor,
1097 alias_case_check: AliasCaseCheck,
1098 ctx: &LintContext,
1099 issues: &mut Vec<Issue>,
1100) {
1101 match relation {
1102 TableFactor::Derived { subquery, .. } => {
1103 check_query(subquery, alias_case_check, ctx, issues);
1104 }
1105 TableFactor::NestedJoin {
1106 table_with_joins, ..
1107 } => {
1108 check_table_factor_subqueries(
1109 &table_with_joins.relation,
1110 alias_case_check,
1111 ctx,
1112 issues,
1113 );
1114 for join in &table_with_joins.joins {
1115 check_table_factor_subqueries(&join.relation, alias_case_check, ctx, issues);
1116 }
1117 }
1118 TableFactor::Pivot { table, .. }
1119 | TableFactor::Unpivot { table, .. }
1120 | TableFactor::MatchRecognize { table, .. } => {
1121 check_table_factor_subqueries(table, alias_case_check, ctx, issues);
1122 }
1123 _ => {}
1124 }
1125}
1126
1127#[derive(Debug, Clone)]
1128struct LegacySimpleTableAliasDecl {
1129 table_end: usize,
1130 alias_end: usize,
1131 alias: String,
1132 quoted: bool,
1133}
1134
1135#[derive(Clone)]
1136struct LegacyLocatedToken {
1137 token: Token,
1138 end: usize,
1139}
1140
1141fn al005_legacy_autofix_edits(
1142 sql: &str,
1143 dialect: Dialect,
1144 alias_case_check: AliasCaseCheck,
1145) -> Vec<(usize, usize)> {
1146 let Some(decls) = legacy_collect_simple_table_alias_declarations(sql, dialect) else {
1147 return Vec::new();
1148 };
1149 if decls.is_empty() {
1150 return Vec::new();
1151 }
1152
1153 let mut seen_aliases = HashSet::new();
1154 let mut removals = Vec::new();
1155 for decl in &decls {
1156 let alias_key = decl.alias.to_ascii_lowercase();
1157 if !seen_aliases.insert(alias_key.clone()) {
1158 continue;
1159 }
1160 if legacy_is_sql_keyword(&decl.alias) || legacy_is_generated_alias_identifier(&decl.alias) {
1161 continue;
1162 }
1163 if legacy_contains_alias_qualifier_dialect(
1164 sql,
1165 &decl.alias,
1166 decl.quoted,
1167 dialect,
1168 alias_case_check,
1169 ) {
1170 continue;
1171 }
1172
1173 removals.extend(
1174 decls
1175 .iter()
1176 .filter(|candidate| candidate.alias.eq_ignore_ascii_case(&alias_key))
1177 .map(|candidate| (candidate.table_end, candidate.alias_end)),
1178 );
1179 }
1180
1181 removals.sort_unstable();
1182 removals.dedup();
1183 removals.retain(|(start, end)| start < end);
1184 removals
1185}
1186
1187fn legacy_collect_simple_table_alias_declarations(
1188 sql: &str,
1189 dialect: Dialect,
1190) -> Option<Vec<LegacySimpleTableAliasDecl>> {
1191 let tokens = legacy_tokenize_with_offsets(sql, dialect)?;
1192 let mut out = Vec::new();
1193 let mut index = 0usize;
1194
1195 while index < tokens.len() {
1196 if !legacy_token_matches_keyword(&tokens[index].token, "FROM")
1197 && !legacy_token_matches_keyword(&tokens[index].token, "JOIN")
1198 {
1199 index += 1;
1200 continue;
1201 }
1202
1203 let Some(next) = legacy_next_non_trivia_token(&tokens, index + 1) else {
1205 index += 1;
1206 continue;
1207 };
1208 index = legacy_try_parse_table_item(&tokens, next, dialect, &mut out);
1209
1210 loop {
1212 let Some(comma_index) = legacy_next_non_trivia_token(&tokens, index) else {
1213 break;
1214 };
1215 if !matches!(tokens[comma_index].token, Token::Comma) {
1216 break;
1217 }
1218 let Some(next_item) = legacy_next_non_trivia_token(&tokens, comma_index + 1) else {
1219 index = comma_index + 1;
1220 break;
1221 };
1222 index = legacy_try_parse_table_item(&tokens, next_item, dialect, &mut out);
1223 }
1224 }
1225
1226 Some(out)
1227}
1228
1229fn legacy_try_parse_table_item(
1232 tokens: &[LegacyLocatedToken],
1233 start: usize,
1234 dialect: Dialect,
1235 out: &mut Vec<LegacySimpleTableAliasDecl>,
1236) -> usize {
1237 if start >= tokens.len() {
1238 return start;
1239 }
1240
1241 if legacy_token_matches_keyword(&tokens[start].token, "LATERAL") {
1243 if let Some(func_end) = legacy_skip_lateral_function_call(tokens, start + 1) {
1244 let Some(mut alias_index) = legacy_next_non_trivia_token(tokens, func_end) else {
1245 return func_end;
1246 };
1247 if legacy_token_matches_keyword(&tokens[alias_index].token, "AS") {
1248 let Some(next_index) = legacy_next_non_trivia_token(tokens, alias_index + 1) else {
1249 return alias_index + 1;
1250 };
1251 alias_index = next_index;
1252 }
1253 if let Some((alias_value, alias_quoted)) =
1254 legacy_token_any_identifier(&tokens[alias_index].token)
1255 {
1256 out.push(LegacySimpleTableAliasDecl {
1257 table_end: tokens[func_end - 1].end,
1258 alias_end: tokens[alias_index].end,
1259 alias: alias_value.to_string(),
1260 quoted: alias_quoted,
1261 });
1262 return alias_index + 1;
1263 }
1264 return func_end;
1265 }
1266 return start + 1;
1267 }
1268
1269 if matches!(tokens[start].token, Token::LParen)
1272 && matches!(dialect, Dialect::Databricks)
1273 && legacy_parenthesized_relation_starts_with_values(tokens, start)
1274 {
1275 let Some(paren_end) = legacy_skip_parenthesized(tokens, start) else {
1276 return start + 1;
1277 };
1278 let table_end = tokens[paren_end - 1].end;
1279
1280 let Some(mut alias_index) = legacy_next_non_trivia_token(tokens, paren_end) else {
1281 return paren_end;
1282 };
1283 if legacy_token_matches_keyword(&tokens[alias_index].token, "AS") {
1284 let Some(next_index) = legacy_next_non_trivia_token(tokens, alias_index + 1) else {
1285 return alias_index + 1;
1286 };
1287 alias_index = next_index;
1288 }
1289 let Some((alias_value, alias_quoted)) =
1290 legacy_token_any_identifier(&tokens[alias_index].token)
1291 else {
1292 return paren_end;
1293 };
1294
1295 let mut alias_end = tokens[alias_index].end;
1296 let mut next_cursor = alias_index + 1;
1297 if let Some(cols_start) = legacy_next_non_trivia_token(tokens, alias_index + 1) {
1298 if matches!(tokens[cols_start].token, Token::LParen) {
1299 if let Some(cols_end) = legacy_skip_parenthesized(tokens, cols_start) {
1300 alias_end = tokens[cols_end - 1].end;
1301 next_cursor = cols_end;
1302 }
1303 }
1304 }
1305
1306 out.push(LegacySimpleTableAliasDecl {
1307 table_end,
1308 alias_end,
1309 alias: alias_value.to_string(),
1310 quoted: alias_quoted,
1311 });
1312 return next_cursor;
1313 }
1314
1315 if legacy_token_any_identifier(&tokens[start].token).is_none() {
1317 return start + 1;
1318 }
1319
1320 let mut table_end = tokens[start].end;
1321 let mut cursor = start + 1;
1322
1323 loop {
1324 let Some(dot_index) = legacy_next_non_trivia_token(tokens, cursor) else {
1325 break;
1326 };
1327 if !matches!(tokens[dot_index].token, Token::Period) {
1328 break;
1329 }
1330 let Some(next_index) = legacy_next_non_trivia_token(tokens, dot_index + 1) else {
1331 break;
1332 };
1333 if legacy_token_any_identifier(&tokens[next_index].token).is_none() {
1334 break;
1335 }
1336 table_end = tokens[next_index].end;
1337 cursor = next_index + 1;
1338 }
1339
1340 let Some(mut alias_index) = legacy_next_non_trivia_token(tokens, cursor) else {
1341 return cursor;
1342 };
1343 if legacy_token_matches_keyword(&tokens[alias_index].token, "AS") {
1344 let Some(next_index) = legacy_next_non_trivia_token(tokens, alias_index + 1) else {
1345 return alias_index + 1;
1346 };
1347 alias_index = next_index;
1348 }
1349
1350 let Some((alias_value, alias_quoted)) = legacy_token_any_identifier(&tokens[alias_index].token)
1351 else {
1352 return cursor;
1353 };
1354
1355 out.push(LegacySimpleTableAliasDecl {
1356 table_end,
1357 alias_end: tokens[alias_index].end,
1358 alias: alias_value.to_string(),
1359 quoted: alias_quoted,
1360 });
1361 alias_index + 1
1362}
1363
1364fn legacy_tokenize_with_offsets(sql: &str, dialect: Dialect) -> Option<Vec<LegacyLocatedToken>> {
1365 let dialect = dialect.to_sqlparser_dialect();
1366 let mut tokenizer = Tokenizer::new(dialect.as_ref(), sql);
1367 let tokens = tokenizer.tokenize_with_location().ok()?;
1368
1369 let mut out = Vec::with_capacity(tokens.len());
1370 for token in tokens {
1371 let (_, end) = legacy_token_with_span_offsets(sql, &token)?;
1372 out.push(LegacyLocatedToken {
1373 token: token.token,
1374 end,
1375 });
1376 }
1377 Some(out)
1378}
1379
1380fn legacy_token_with_span_offsets(sql: &str, token: &TokenWithSpan) -> Option<(usize, usize)> {
1381 let start = legacy_line_col_to_offset(
1382 sql,
1383 token.span.start.line as usize,
1384 token.span.start.column as usize,
1385 )?;
1386 let end = legacy_line_col_to_offset(
1387 sql,
1388 token.span.end.line as usize,
1389 token.span.end.column as usize,
1390 )?;
1391 Some((start, end))
1392}
1393
1394fn legacy_line_col_to_offset(sql: &str, line: usize, column: usize) -> Option<usize> {
1395 if line == 0 || column == 0 {
1396 return None;
1397 }
1398
1399 let mut current_line = 1usize;
1400 let mut current_col = 1usize;
1401
1402 for (offset, ch) in sql.char_indices() {
1403 if current_line == line && current_col == column {
1404 return Some(offset);
1405 }
1406
1407 if ch == '\n' {
1408 current_line += 1;
1409 current_col = 1;
1410 } else {
1411 current_col += 1;
1412 }
1413 }
1414
1415 if current_line == line && current_col == column {
1416 return Some(sql.len());
1417 }
1418
1419 None
1420}
1421
1422fn legacy_next_non_trivia_token(tokens: &[LegacyLocatedToken], mut start: usize) -> Option<usize> {
1423 while start < tokens.len() {
1424 if !legacy_is_trivia_token(&tokens[start].token) {
1425 return Some(start);
1426 }
1427 start += 1;
1428 }
1429 None
1430}
1431
1432fn legacy_skip_lateral_function_call(tokens: &[LegacyLocatedToken], start: usize) -> Option<usize> {
1435 let func_index = legacy_next_non_trivia_token(tokens, start)?;
1437 legacy_token_any_identifier(&tokens[func_index].token)?;
1438 let lparen_index = legacy_next_non_trivia_token(tokens, func_index + 1)?;
1439 if !matches!(tokens[lparen_index].token, Token::LParen) {
1440 return None;
1441 }
1442 let mut depth = 1u32;
1444 let mut cursor = lparen_index + 1;
1445 while cursor < tokens.len() && depth > 0 {
1446 match &tokens[cursor].token {
1447 Token::LParen => depth += 1,
1448 Token::RParen => depth -= 1,
1449 _ => {}
1450 }
1451 cursor += 1;
1452 }
1453 if depth == 0 {
1454 Some(cursor)
1455 } else {
1456 None
1457 }
1458}
1459
1460fn legacy_parenthesized_relation_starts_with_values(
1461 tokens: &[LegacyLocatedToken],
1462 lparen_index: usize,
1463) -> bool {
1464 let Some(first_inner) = legacy_next_non_trivia_token(tokens, lparen_index + 1) else {
1465 return false;
1466 };
1467 legacy_token_matches_keyword(&tokens[first_inner].token, "VALUES")
1468}
1469
1470fn legacy_skip_parenthesized(tokens: &[LegacyLocatedToken], lparen_index: usize) -> Option<usize> {
1471 if !matches!(tokens.get(lparen_index)?.token, Token::LParen) {
1472 return None;
1473 }
1474 let mut depth = 1u32;
1475 let mut cursor = lparen_index + 1;
1476 while cursor < tokens.len() && depth > 0 {
1477 match &tokens[cursor].token {
1478 Token::LParen => depth += 1,
1479 Token::RParen => depth -= 1,
1480 _ => {}
1481 }
1482 cursor += 1;
1483 }
1484 if depth == 0 {
1485 Some(cursor)
1486 } else {
1487 None
1488 }
1489}
1490
1491fn legacy_is_trivia_token(token: &Token) -> bool {
1492 matches!(
1493 token,
1494 Token::Whitespace(
1495 Whitespace::Space
1496 | Whitespace::Newline
1497 | Whitespace::Tab
1498 | Whitespace::SingleLineComment { .. }
1499 | Whitespace::MultiLineComment(_)
1500 )
1501 )
1502}
1503
1504fn legacy_token_matches_keyword(token: &Token, keyword: &str) -> bool {
1505 matches!(token, Token::Word(word) if word.value.eq_ignore_ascii_case(keyword))
1506}
1507
1508fn legacy_token_any_identifier(token: &Token) -> Option<(&str, bool)> {
1511 match token {
1512 Token::Word(word) if legacy_is_simple_identifier(&word.value) => {
1513 if word.quote_style.is_some() {
1514 Some((&word.value, true))
1515 } else {
1516 Some((&word.value, false))
1517 }
1518 }
1519 Token::SingleQuotedString(s) => Some((s.as_str(), true)),
1520 _ => None,
1521 }
1522}
1523
1524fn legacy_contains_alias_qualifier_dialect(
1525 sql: &str,
1526 alias: &str,
1527 alias_quoted: bool,
1528 dialect: Dialect,
1529 alias_case_check: AliasCaseCheck,
1530) -> bool {
1531 if matches!(dialect, Dialect::Redshift)
1532 && legacy_redshift_qualify_uses_alias_prefixed_identifier(sql, alias)
1533 {
1534 return true;
1535 }
1536
1537 let alias_bytes = alias.as_bytes();
1538 if alias_bytes.is_empty() {
1539 return false;
1540 }
1541
1542 let normalized_alias = match alias_case_check {
1544 AliasCaseCheck::Dialect => normalize_identifier_for_dialect(alias, alias_quoted, dialect),
1545 AliasCaseCheck::CaseInsensitive => alias.to_ascii_lowercase(),
1546 AliasCaseCheck::CaseSensitive => alias.to_string(),
1547 AliasCaseCheck::QuotedCsNakedUpper => {
1548 if alias_quoted {
1549 alias.to_string()
1550 } else {
1551 alias.to_ascii_uppercase()
1552 }
1553 }
1554 AliasCaseCheck::QuotedCsNakedLower => {
1555 if alias_quoted {
1556 alias.to_string()
1557 } else {
1558 alias.to_ascii_lowercase()
1559 }
1560 }
1561 };
1562
1563 let bytes = sql.as_bytes();
1564 let mut index = 0usize;
1565 while index < bytes.len() {
1566 let (ref_name, ref_quoted, ref_end) = if index < bytes.len()
1568 && (bytes[index] == b'"' || bytes[index] == b'`' || bytes[index] == b'[')
1569 {
1570 let close_char = match bytes[index] {
1571 b'"' => b'"',
1572 b'`' => b'`',
1573 b'[' => b']',
1574 _ => unreachable!(),
1575 };
1576 let start = index + 1;
1577 let mut end = start;
1578 while end < bytes.len() && bytes[end] != close_char {
1579 end += 1;
1580 }
1581 if end >= bytes.len() {
1582 index += 1;
1583 continue;
1584 }
1585 let name = &sql[start..end];
1586 (name.to_string(), true, end + 1)
1588 } else if index < bytes.len() && legacy_is_ascii_ident_start(bytes[index]) {
1589 let start = index;
1590 let mut end = start;
1591 while end < bytes.len() && legacy_is_ascii_ident_continue(bytes[end]) {
1592 end += 1;
1593 }
1594 let name = &sql[start..end];
1595 (name.to_string(), false, end)
1596 } else {
1597 index += 1;
1598 continue;
1599 };
1600
1601 if ref_end < bytes.len() && bytes[ref_end] == b'.' {
1603 let normalized_ref = match alias_case_check {
1604 AliasCaseCheck::Dialect => {
1605 normalize_identifier_for_dialect(&ref_name, ref_quoted, dialect)
1606 }
1607 AliasCaseCheck::CaseInsensitive => ref_name.to_ascii_lowercase(),
1608 AliasCaseCheck::CaseSensitive => ref_name.clone(),
1609 AliasCaseCheck::QuotedCsNakedUpper => {
1610 if ref_quoted {
1611 ref_name.clone()
1612 } else {
1613 ref_name.to_ascii_uppercase()
1614 }
1615 }
1616 AliasCaseCheck::QuotedCsNakedLower => {
1617 if ref_quoted {
1618 ref_name.clone()
1619 } else {
1620 ref_name.to_ascii_lowercase()
1621 }
1622 }
1623 };
1624 if normalized_ref == normalized_alias {
1625 return true;
1626 }
1627 }
1628
1629 index = if ref_end > index { ref_end } else { index + 1 };
1630 }
1631
1632 false
1633}
1634
1635fn legacy_redshift_qualify_uses_alias_prefixed_identifier(sql: &str, alias: &str) -> bool {
1636 let Some(tokens) = legacy_tokenize_with_offsets(sql, Dialect::Redshift) else {
1637 return false;
1638 };
1639 let Some(qualify_index) = tokens
1640 .iter()
1641 .position(|token| legacy_token_matches_keyword(&token.token, "QUALIFY"))
1642 else {
1643 return false;
1644 };
1645
1646 if tokens[..qualify_index]
1649 .iter()
1650 .any(|token| legacy_token_matches_keyword(&token.token, "WHERE"))
1651 {
1652 return false;
1653 }
1654
1655 tokens[qualify_index + 1..]
1656 .iter()
1657 .filter_map(|token| legacy_token_reference_identifier(&token.token))
1658 .any(|identifier| legacy_alias_prefixed_identifier(identifier, alias))
1659}
1660
1661fn legacy_token_reference_identifier(token: &Token) -> Option<&str> {
1662 match token {
1663 Token::Word(word) => Some(word.value.as_str()),
1664 _ => None,
1665 }
1666}
1667
1668fn legacy_alias_prefixed_identifier(identifier: &str, alias: &str) -> bool {
1669 if identifier.is_empty() || alias.is_empty() {
1670 return false;
1671 }
1672 identifier
1673 .to_ascii_uppercase()
1674 .strip_prefix(&alias.to_ascii_uppercase())
1675 .is_some_and(|suffix| suffix.starts_with('_'))
1676}
1677
1678fn legacy_is_generated_alias_identifier(alias: &str) -> bool {
1679 let mut chars = alias.chars();
1680 match chars.next() {
1681 Some('t') => {}
1682 _ => return false,
1683 }
1684 let mut saw_digit = false;
1685 for ch in chars {
1686 if !ch.is_ascii_digit() {
1687 return false;
1688 }
1689 saw_digit = true;
1690 }
1691 saw_digit
1692}
1693
1694fn legacy_is_sql_keyword(token: &str) -> bool {
1695 matches!(
1696 token.to_ascii_uppercase().as_str(),
1697 "ALL"
1698 | "ALTER"
1699 | "AND"
1700 | "ANY"
1701 | "AS"
1702 | "ASC"
1703 | "BEGIN"
1704 | "BETWEEN"
1705 | "BOOLEAN"
1706 | "BY"
1707 | "CASE"
1708 | "CAST"
1709 | "CHECK"
1710 | "COLUMN"
1711 | "CONSTRAINT"
1712 | "CREATE"
1713 | "CROSS"
1714 | "DEFAULT"
1715 | "DELETE"
1716 | "DESC"
1717 | "DISTINCT"
1718 | "DROP"
1719 | "ELSE"
1720 | "END"
1721 | "EXCEPT"
1722 | "EXISTS"
1723 | "FALSE"
1724 | "FETCH"
1725 | "FOR"
1726 | "FOREIGN"
1727 | "FROM"
1728 | "FULL"
1729 | "GROUP"
1730 | "HAVING"
1731 | "IF"
1732 | "IN"
1733 | "INDEX"
1734 | "INNER"
1735 | "INSERT"
1736 | "INT"
1737 | "INTEGER"
1738 | "INTERSECT"
1739 | "INTO"
1740 | "IS"
1741 | "JOIN"
1742 | "KEY"
1743 | "LEFT"
1744 | "LIKE"
1745 | "LIMIT"
1746 | "NOT"
1747 | "NULL"
1748 | "OFFSET"
1749 | "ON"
1750 | "OR"
1751 | "ORDER"
1752 | "OUTER"
1753 | "OVER"
1754 | "PARTITION"
1755 | "PRIMARY"
1756 | "REFERENCES"
1757 | "RIGHT"
1758 | "SELECT"
1759 | "SET"
1760 | "TABLE"
1761 | "TEXT"
1762 | "THEN"
1763 | "TRUE"
1764 | "UNION"
1765 | "UNIQUE"
1766 | "UPDATE"
1767 | "USING"
1768 | "VALUES"
1769 | "VARCHAR"
1770 | "VIEW"
1771 | "WHEN"
1772 | "WHERE"
1773 | "WINDOW"
1774 | "WITH"
1775 )
1776}
1777
1778fn legacy_is_simple_identifier(value: &str) -> bool {
1779 let bytes = value.as_bytes();
1780 if bytes.is_empty() || !legacy_is_ascii_ident_start(bytes[0]) {
1781 return false;
1782 }
1783 bytes[1..]
1784 .iter()
1785 .copied()
1786 .all(legacy_is_ascii_ident_continue)
1787}
1788
1789fn legacy_is_ascii_ident_start(byte: u8) -> bool {
1790 byte.is_ascii_alphabetic() || byte == b'_' || byte == b'#'
1791}
1792
1793fn legacy_is_ascii_ident_continue(byte: u8) -> bool {
1794 byte.is_ascii_alphanumeric() || byte == b'_'
1795}
1796
1797#[cfg(test)]
1798mod tests {
1799 use super::*;
1800 use crate::linter::config::LintConfig;
1801 use crate::linter::rule::with_active_dialect;
1802 use crate::parser::{parse_sql, parse_sql_with_dialect};
1803 use crate::types::{Dialect, IssueAutofixApplicability};
1804
1805 fn check_sql(sql: &str) -> Vec<Issue> {
1806 let stmts = parse_sql(sql).unwrap();
1807 let rule = UnusedTableAlias::default();
1808 let ctx = LintContext {
1809 sql,
1810 statement_range: 0..sql.len(),
1811 statement_index: 0,
1812 };
1813 let mut issues = Vec::new();
1814 for stmt in &stmts {
1815 issues.extend(rule.check(stmt, &ctx));
1816 }
1817 issues
1818 }
1819
1820 fn apply_issue_autofix(sql: &str, issue: &Issue) -> Option<String> {
1821 let autofix = issue.autofix.as_ref()?;
1822 let mut edits = autofix.edits.clone();
1823 edits.sort_by_key(|edit| (edit.span.start, edit.span.end));
1824 let mut rewritten = sql.to_string();
1825 for edit in edits.into_iter().rev() {
1826 rewritten.replace_range(edit.span.start..edit.span.end, &edit.replacement);
1827 }
1828 Some(rewritten)
1829 }
1830
1831 fn check_sql_in_dialect(sql: &str, dialect: Dialect) -> Vec<Issue> {
1832 let stmts = parse_sql_with_dialect(sql, dialect).unwrap();
1833 let rule = UnusedTableAlias::default();
1834 let ctx = LintContext {
1835 sql,
1836 statement_range: 0..sql.len(),
1837 statement_index: 0,
1838 };
1839 let mut issues = Vec::new();
1840 with_active_dialect(dialect, || {
1841 for stmt in &stmts {
1842 issues.extend(rule.check(stmt, &ctx));
1843 }
1844 });
1845 issues
1846 }
1847
1848 #[test]
1849 fn test_unused_alias_detected() {
1850 let issues = check_sql("SELECT * FROM users u JOIN orders o ON users.id = orders.user_id");
1851 assert_eq!(issues.len(), 2);
1853 assert_eq!(issues[0].code, "LINT_AL_005");
1854 }
1855
1856 #[test]
1857 fn test_unused_alias_emits_safe_autofix_patch() {
1858 let sql = "SELECT users.name FROM users AS u JOIN orders AS o ON users.id = orders.user_id";
1859 let issues = check_sql(sql);
1860 assert_eq!(issues.len(), 2);
1861 let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
1862 assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
1863 let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
1864 assert_eq!(
1865 fixed,
1866 "SELECT users.name FROM users JOIN orders ON users.id = orders.user_id"
1867 );
1868 }
1869
1870 #[test]
1871 fn test_generated_alias_does_not_emit_autofix() {
1872 let sql = "SELECT * FROM users AS t1";
1873 let issues = check_sql(sql);
1874 assert_eq!(issues.len(), 1);
1875 assert!(
1876 issues[0].autofix.is_none(),
1877 "legacy AL005 parity skips generated aliases like t1"
1878 );
1879 }
1880
1881 #[test]
1882 fn test_used_alias_ok() {
1883 let issues = check_sql("SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id");
1884 assert!(issues.is_empty());
1885 }
1886
1887 #[test]
1888 fn test_single_table_unused_alias_detected() {
1889 let issues = check_sql("SELECT * FROM users u");
1890 assert_eq!(issues.len(), 1);
1891 assert!(issues[0].message.contains("u"));
1892 }
1893
1894 #[test]
1895 fn test_single_table_alias_used_ok() {
1896 let issues = check_sql("SELECT u.id FROM users u");
1897 assert!(issues.is_empty());
1898 }
1899
1900 #[test]
1903 fn test_alias_used_in_where() {
1904 let issues = check_sql(
1905 "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE u.active = true",
1906 );
1907 assert!(issues.is_empty());
1908 }
1909
1910 #[test]
1911 fn test_alias_used_in_group_by() {
1912 let issues = check_sql(
1913 "SELECT u.name, COUNT(*) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name",
1914 );
1915 assert!(issues.is_empty());
1916 }
1917
1918 #[test]
1919 fn test_alias_used_in_having() {
1920 let issues = check_sql(
1921 "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id \
1922 GROUP BY u.name HAVING COUNT(o.id) > 5",
1923 );
1924 assert!(issues.is_empty());
1925 }
1926
1927 #[test]
1928 fn test_alias_used_in_qualified_wildcard() {
1929 let issues = check_sql("SELECT u.* FROM users u JOIN orders o ON u.id = o.user_id");
1931 assert!(issues.is_empty());
1932 }
1933
1934 #[test]
1935 fn test_alias_unused_despite_qualified_wildcard() {
1936 let issues = check_sql("SELECT u.* FROM users u JOIN orders o ON u.id = orders.user_id");
1938 assert_eq!(issues.len(), 1);
1939 assert!(issues[0].message.contains("o"));
1940 }
1941
1942 #[test]
1943 fn test_partial_alias_usage() {
1944 let issues = check_sql("SELECT u.name FROM users u JOIN orders o ON u.id = orders.user_id");
1946 assert_eq!(issues.len(), 1);
1947 assert!(issues[0].message.contains("o"));
1948 }
1949
1950 #[test]
1951 fn test_three_tables_one_unused() {
1952 let issues = check_sql(
1953 "SELECT a.name, b.total \
1954 FROM users a \
1955 JOIN orders b ON a.id = b.user_id \
1956 JOIN products c ON b.product_id = products.id",
1957 );
1958 assert_eq!(issues.len(), 1);
1959 assert!(issues[0].message.contains("c"));
1960 }
1961
1962 #[test]
1963 fn test_no_aliases_ok() {
1964 let issues =
1965 check_sql("SELECT users.name FROM users JOIN orders ON users.id = orders.user_id");
1966 assert!(issues.is_empty());
1967 }
1968
1969 #[test]
1970 fn test_self_join_with_aliases() {
1971 let issues =
1972 check_sql("SELECT a.name, b.name FROM users a JOIN users b ON a.manager_id = b.id");
1973 assert!(issues.is_empty());
1974 }
1975
1976 #[test]
1977 fn test_alias_in_case_expression() {
1978 let issues = check_sql(
1979 "SELECT CASE WHEN u.active THEN 'yes' ELSE 'no' END \
1980 FROM users u JOIN orders o ON u.id = o.user_id",
1981 );
1982 assert!(issues.is_empty());
1984 }
1985
1986 #[test]
1987 fn test_alias_used_in_order_by() {
1988 let issues = check_sql(
1989 "SELECT u.name \
1990 FROM users u \
1991 JOIN orders o ON users.id = orders.user_id \
1992 ORDER BY o.created_at",
1993 );
1994 assert!(issues.is_empty());
1995 }
1996
1997 #[test]
1998 fn test_left_join_alias_used_in_on_clause() {
1999 let issues = check_sql("SELECT u.name FROM users u LEFT JOIN orders o ON u.id = o.user_id");
2000 assert!(issues.is_empty());
2001 }
2002
2003 #[test]
2004 fn test_alias_used_only_in_correlated_exists_subquery() {
2005 let issues = check_sql(
2006 "SELECT 1 \
2007 FROM users u \
2008 JOIN orders o ON 1 = 1 \
2009 WHERE EXISTS (SELECT 1 WHERE u.id = o.user_id)",
2010 );
2011 assert!(issues.is_empty());
2012 }
2013
2014 #[test]
2015 fn test_alias_used_in_qualify_clause() {
2016 let issues = check_sql(
2017 "SELECT u.id \
2018 FROM users u \
2019 JOIN orders o ON users.id = orders.user_id \
2020 QUALIFY ROW_NUMBER() OVER (PARTITION BY o.user_id ORDER BY o.user_id) = 1",
2021 );
2022 assert!(issues.is_empty());
2023 }
2024
2025 #[test]
2026 fn test_alias_used_in_named_window_clause() {
2027 let issues = check_sql(
2028 "SELECT SUM(u.id) OVER w \
2029 FROM users u \
2030 JOIN orders o ON users.id = orders.user_id \
2031 WINDOW w AS (PARTITION BY o.user_id)",
2032 );
2033 assert!(issues.is_empty());
2034 }
2035
2036 #[test]
2037 fn test_unused_derived_alias_detected() {
2038 let issues = check_sql(
2039 "SELECT u.id \
2040 FROM users u \
2041 JOIN (SELECT id FROM orders) o2 ON u.id = u.id",
2042 );
2043 assert!(issues.is_empty());
2044 }
2045
2046 #[test]
2047 fn test_lateral_alias_is_ignored() {
2048 let issues = check_sql("SELECT u.id FROM users u JOIN LATERAL (SELECT 1) lx ON TRUE");
2049 assert!(issues.is_empty());
2050 }
2051
2052 #[test]
2053 fn test_alias_used_only_in_lateral_subquery_relation() {
2054 let issues = check_sql(
2055 "SELECT 1 \
2056 FROM users u \
2057 JOIN LATERAL (SELECT u.id) lx ON TRUE",
2058 );
2059 assert!(issues.is_empty());
2060 }
2061
2062 #[test]
2063 fn test_alias_used_only_in_unnest_join_relation() {
2064 let issues = check_sql(
2065 "SELECT 1 \
2066 FROM users u \
2067 LEFT JOIN UNNEST(u.tags) tag ON TRUE",
2068 );
2069 assert!(issues.is_empty());
2070 }
2071
2072 #[test]
2073 fn alias_case_check_case_sensitive_flags_case_mismatch() {
2074 let config = LintConfig {
2075 enabled: true,
2076 disabled_rules: vec![],
2077 rule_configs: std::collections::BTreeMap::from([(
2078 "aliasing.unused".to_string(),
2079 serde_json::json!({"alias_case_check": "case_sensitive"}),
2080 )]),
2081 };
2082 let rule = UnusedTableAlias::from_config(&config);
2083 let sql = "SELECT zoo.id, b.id FROM users AS \"Zoo\" JOIN books b ON zoo.id = b.user_id";
2084 let stmts = parse_sql(sql).expect("parse");
2085 let issues = rule.check(
2086 &stmts[0],
2087 &LintContext {
2088 sql,
2089 statement_range: 0..sql.len(),
2090 statement_index: 0,
2091 },
2092 );
2093 assert_eq!(issues.len(), 1);
2094 assert!(issues[0].message.contains("Zoo"));
2095 }
2096
2097 #[test]
2098 fn alias_case_check_case_insensitive_allows_case_mismatch() {
2099 let config = LintConfig {
2100 enabled: true,
2101 disabled_rules: vec![],
2102 rule_configs: std::collections::BTreeMap::from([(
2103 "LINT_AL_005".to_string(),
2104 serde_json::json!({"alias_case_check": "case_insensitive"}),
2105 )]),
2106 };
2107 let rule = UnusedTableAlias::from_config(&config);
2108 let sql = "SELECT zoo.id, b.id FROM users AS \"Zoo\" JOIN books b ON zoo.id = b.user_id";
2109 let stmts = parse_sql(sql).expect("parse");
2110 let issues = rule.check(
2111 &stmts[0],
2112 &LintContext {
2113 sql,
2114 statement_range: 0..sql.len(),
2115 statement_index: 0,
2116 },
2117 );
2118 assert!(issues.is_empty());
2119 }
2120
2121 #[test]
2122 fn alias_case_check_quoted_cs_naked_upper_allows_unquoted_upper_fold_for_quoted_alias() {
2123 let config = LintConfig {
2124 enabled: true,
2125 disabled_rules: vec![],
2126 rule_configs: std::collections::BTreeMap::from([(
2127 "aliasing.unused".to_string(),
2128 serde_json::json!({"alias_case_check": "quoted_cs_naked_upper"}),
2129 )]),
2130 };
2131 let rule = UnusedTableAlias::from_config(&config);
2132 let sql = "SELECT foo.id, b.id FROM users AS \"FOO\" JOIN books b ON foo.id = b.user_id";
2133 let stmts = parse_sql(sql).expect("parse");
2134 let issues = rule.check(
2135 &stmts[0],
2136 &LintContext {
2137 sql,
2138 statement_range: 0..sql.len(),
2139 statement_index: 0,
2140 },
2141 );
2142 assert!(issues.is_empty());
2143 }
2144
2145 #[test]
2146 fn alias_case_check_quoted_cs_naked_lower_allows_unquoted_lower_fold_for_quoted_alias() {
2147 let config = LintConfig {
2148 enabled: true,
2149 disabled_rules: vec![],
2150 rule_configs: std::collections::BTreeMap::from([(
2151 "aliasing.unused".to_string(),
2152 serde_json::json!({"alias_case_check": "quoted_cs_naked_lower"}),
2153 )]),
2154 };
2155 let rule = UnusedTableAlias::from_config(&config);
2156 let sql = "SELECT FOO.id, b.id FROM users AS \"foo\" JOIN books b ON FOO.id = b.user_id";
2157 let stmts = parse_sql(sql).expect("parse");
2158 let issues = rule.check(
2159 &stmts[0],
2160 &LintContext {
2161 sql,
2162 statement_range: 0..sql.len(),
2163 statement_index: 0,
2164 },
2165 );
2166 assert!(issues.is_empty());
2167 }
2168
2169 #[test]
2170 fn dialect_mode_generic_allows_quoted_unquoted_fold_match() {
2171 let issues = check_sql("SELECT a.col1 FROM tab1 AS \"A\"");
2172 assert!(issues.is_empty());
2173 }
2174
2175 #[test]
2176 fn dialect_mode_generic_allows_quoted_prefix_against_unquoted_alias() {
2177 let issues = check_sql("SELECT \"A\".col1 FROM tab1 AS a");
2178 assert!(issues.is_empty());
2179 }
2180
2181 #[test]
2182 fn dialect_mode_generic_flags_single_quoted_alias_case_mismatch() {
2183 let issues = check_sql("SELECT a.col1 FROM tab1 AS 'a'");
2184 assert_eq!(issues.len(), 1);
2185 assert!(issues[0].message.contains("a"));
2186 }
2187
2188 #[test]
2189 fn dialect_mode_postgres_allows_lower_fold_for_quoted_alias() {
2190 let issues =
2191 check_sql_in_dialect("SELECT A.col_1 FROM table_a AS \"a\"", Dialect::Postgres);
2192 assert!(issues.is_empty());
2193 }
2194
2195 #[test]
2196 fn dialect_mode_snowflake_flags_mixed_quoted_case_mismatch() {
2197 let issues =
2198 check_sql_in_dialect("SELECT a.col_1 FROM table_a AS \"a\"", Dialect::Snowflake);
2199 assert_eq!(issues.len(), 1);
2200 assert!(issues[0].message.contains("a"));
2201 }
2202
2203 #[test]
2204 fn dialect_mode_bigquery_allows_backtick_quoted_alias_fold_match() {
2205 let issues = check_sql_in_dialect("SELECT a.col1 FROM tab1 AS `A`", Dialect::Bigquery);
2206 assert!(issues.is_empty());
2207 }
2208
2209 #[test]
2210 fn dialect_mode_redshift_allows_lower_fold_for_quoted_alias() {
2211 let issues =
2212 check_sql_in_dialect("SELECT A.col_1 FROM table_a AS \"a\"", Dialect::Redshift);
2213 assert!(issues.is_empty());
2214 }
2215
2216 #[test]
2217 fn dialect_mode_redshift_flags_mixed_quoted_case_mismatch() {
2218 let issues =
2219 check_sql_in_dialect("SELECT a.col_1 FROM table_a AS \"A\"", Dialect::Redshift);
2220 assert_eq!(issues.len(), 1);
2221 assert!(issues[0].message.contains("A"));
2222 }
2223
2224 #[test]
2225 fn dialect_mode_mysql_allows_backtick_qualified_reference_against_unquoted_alias() {
2226 let issues = check_sql_in_dialect(
2227 "SELECT `nih`.`userID` FROM `flight_notification_item_history` AS nih",
2228 Dialect::Mysql,
2229 );
2230 assert!(issues.is_empty());
2231 }
2232
2233 #[test]
2234 fn dialect_mode_duckdb_allows_case_insensitive_quoted_reference() {
2235 let issues = check_sql_in_dialect("SELECT \"a\".col_1 FROM table_a AS A", Dialect::Duckdb);
2236 assert!(issues.is_empty());
2237 }
2238
2239 #[test]
2240 fn dialect_mode_hive_allows_case_insensitive_quoted_reference() {
2241 let issues = check_sql_in_dialect("SELECT `a`.col1 FROM tab1 AS A", Dialect::Hive);
2242 assert!(issues.is_empty());
2243 }
2244
2245 #[test]
2246 fn flags_inner_subquery_unused_alias() {
2247 let issues = check_sql("SELECT * FROM (SELECT * FROM my_tbl AS foo)");
2248 assert_eq!(issues.len(), 1);
2249 assert!(issues[0].message.contains("foo"));
2250 }
2251
2252 #[test]
2253 fn allows_unreferenced_subquery_alias() {
2254 let issues = check_sql("SELECT * FROM (SELECT 1 AS a) subquery");
2255 assert!(issues.is_empty());
2256 }
2257
2258 #[test]
2259 fn allows_postgres_generate_series_alias() {
2260 let issues = check_sql_in_dialect(
2261 "SELECT date_trunc('day', dd)::timestamp FROM generate_series('2022-02-01'::timestamp, NOW()::timestamp, '1 day'::interval) dd",
2262 Dialect::Postgres,
2263 );
2264 assert!(issues.is_empty());
2265 }
2266
2267 #[test]
2268 fn flags_unused_snowflake_lateral_flatten_alias() {
2269 let issues = check_sql_in_dialect(
2270 "SELECT a.test1, a.test2, b.test3 \
2271 FROM table1 AS a, \
2272 LATERAL flatten(input => some_field) AS b, \
2273 LATERAL flatten(input => b.value) AS c, \
2274 LATERAL flatten(input => c.value) AS d, \
2275 LATERAL flatten(input => d.value) AS e, \
2276 LATERAL flatten(input => e.value) AS f",
2277 Dialect::Snowflake,
2278 );
2279 assert_eq!(issues.len(), 1);
2280 assert!(issues[0].message.contains("f"));
2281 }
2282
2283 #[test]
2284 fn flags_unused_alias_inside_snowflake_delete_using_cte() {
2285 let issues = check_sql_in_dialect(
2286 "DELETE FROM MYTABLE1 \
2287 USING ( \
2288 WITH MYCTE AS (SELECT COLUMN2 FROM MYTABLE3 AS MT3) \
2289 SELECT COLUMN3 FROM MYTABLE3 \
2290 ) X \
2291 WHERE COLUMN1 = X.COLUMN3",
2292 Dialect::Snowflake,
2293 );
2294 assert_eq!(issues.len(), 1);
2295 assert!(issues[0].message.contains("MT3"));
2296 }
2297
2298 #[test]
2299 fn allows_bigquery_to_json_string_table_alias_argument() {
2300 let issues = check_sql_in_dialect(
2301 "SELECT TO_JSON_STRING(t) FROM my_table AS t",
2302 Dialect::Bigquery,
2303 );
2304 assert!(issues.is_empty());
2305 }
2306
2307 #[test]
2308 fn flags_ansi_to_json_string_table_alias_argument() {
2309 let issues =
2310 check_sql_in_dialect("SELECT TO_JSON_STRING(t) FROM my_table AS t", Dialect::Ansi);
2311 assert_eq!(issues.len(), 1);
2312 assert!(issues[0].message.contains("t"));
2313 }
2314
2315 #[test]
2316 fn redshift_qualify_after_from_counts_alias_usage() {
2317 let issues = check_sql_in_dialect(
2318 "SELECT * \
2319 FROM store AS s \
2320 INNER JOIN store_sales AS ss \
2321 QUALIFY ROW_NUMBER() OVER (PARTITION BY ss.sold_date ORDER BY ss.sales_price DESC) <= 2",
2322 Dialect::Redshift,
2323 );
2324 assert_eq!(issues.len(), 1);
2325 assert!(issues[0].message.contains("s"));
2326 }
2327
2328 #[test]
2329 fn redshift_qualify_after_where_does_not_count_alias_usage() {
2330 let issues = check_sql_in_dialect(
2331 "SELECT * \
2332 FROM store AS s \
2333 INNER JOIN store_sales AS ss \
2334 WHERE col = 1 \
2335 QUALIFY ROW_NUMBER() OVER (PARTITION BY ss.sold_date ORDER BY ss.sales_price DESC) <= 2",
2336 Dialect::Redshift,
2337 );
2338 assert_eq!(issues.len(), 2);
2339 assert!(issues.iter().any(|issue| issue.message.contains("s")));
2340 assert!(issues.iter().any(|issue| issue.message.contains("ss")));
2341 }
2342
2343 #[test]
2344 fn redshift_qualify_unqualified_alias_prefixed_identifier_counts_alias_usage() {
2345 let issues = check_sql_in_dialect(
2346 "SELECT * \
2347 FROM #store_sales AS ss \
2348 QUALIFY row_number() OVER (PARTITION BY ss_sold_date ORDER BY ss_sales_price DESC) <= 2",
2349 Dialect::Redshift,
2350 );
2351 assert!(issues.is_empty());
2352 }
2353
2354 #[test]
2355 fn redshift_qualify_after_from_autofix_keeps_used_join_alias() {
2356 let sql = "SELECT *\n\
2357FROM #store as s\n\
2358INNER JOIN #store_sales AS ss\n\
2359QUALIFY row_number() OVER (PARTITION BY ss_sold_date ORDER BY ss_sales_price DESC) <= 2";
2360 let issues = check_sql_in_dialect(sql, Dialect::Redshift);
2361 assert_eq!(issues.len(), 1);
2362 let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
2363 assert_eq!(
2364 fixed,
2365 "SELECT *\n\
2366FROM #store\n\
2367INNER JOIN #store_sales AS ss\n\
2368QUALIFY row_number() OVER (PARTITION BY ss_sold_date ORDER BY ss_sales_price DESC) <= 2"
2369 );
2370 }
2371
2372 #[test]
2373 fn redshift_qualify_after_where_autofix_removes_both_unused_aliases() {
2374 let sql = "SELECT *\n\
2375FROM #store as s\n\
2376INNER JOIN #store_sales AS ss\n\
2377WHERE col = 1\n\
2378QUALIFY row_number() OVER (PARTITION BY ss_sold_date ORDER BY ss_sales_price DESC) <= 2";
2379 let issues = check_sql_in_dialect(sql, Dialect::Redshift);
2380 assert_eq!(issues.len(), 2);
2381 let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
2382 assert_eq!(
2383 fixed,
2384 "SELECT *\n\
2385FROM #store\n\
2386INNER JOIN #store_sales\n\
2387WHERE col = 1\n\
2388QUALIFY row_number() OVER (PARTITION BY ss_sold_date ORDER BY ss_sales_price DESC) <= 2"
2389 );
2390 }
2391
2392 #[test]
2393 fn sparksql_values_derived_alias_is_detected_and_autofixed() {
2394 let sql = "SELECT *\n\
2395FROM (\n\
2396 VALUES (1, 2), (3, 4)\n\
2397) AS t(c1, c2)";
2398 let issues = check_sql_in_dialect(sql, Dialect::Databricks);
2399 assert_eq!(issues.len(), 1);
2400 let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
2401 assert_eq!(
2402 fixed,
2403 "SELECT *\n\
2404FROM (\n\
2405 VALUES (1, 2), (3, 4)\n\
2406)"
2407 );
2408 }
2409
2410 #[test]
2411 fn allows_bigquery_implicit_array_table_reference() {
2412 let issues = check_sql_in_dialect(
2413 "WITH table_arr AS (SELECT [1,2,4,2] AS arr) \
2414 SELECT arr \
2415 FROM table_arr AS t, t.arr",
2416 Dialect::Bigquery,
2417 );
2418 assert!(issues.is_empty());
2419 }
2420
2421 #[test]
2422 fn allows_redshift_super_array_relation_reference() {
2423 let issues = check_sql_in_dialect(
2424 "SELECT my_column, my_array_value \
2425 FROM my_schema.my_table AS t, t.super_array AS my_array_value",
2426 Dialect::Redshift,
2427 );
2428 assert!(issues.is_empty());
2429 }
2430
2431 #[test]
2432 fn allows_repeat_referenced_table_aliases() {
2433 let issues = check_sql(
2434 "SELECT ROW_NUMBER() OVER(PARTITION BY a.object_id ORDER BY a.object_id) \
2435 FROM sys.objects a \
2436 CROSS JOIN sys.objects b \
2437 CROSS JOIN sys.objects c",
2438 );
2439 assert!(issues.is_empty());
2440 }
2441
2442 #[test]
2443 fn dialect_mode_databricks_allows_backtick_case_insensitive_reference() {
2444 let issues =
2445 check_sql_in_dialect("SELECT `a`.col_1 FROM table_a AS A", Dialect::Databricks);
2446 assert!(issues.is_empty());
2447 }
2448
2449 #[test]
2450 fn snowflake_json_access_counts_as_alias_usage() {
2451 let issues = check_sql_in_dialect(
2452 "SELECT r.rec:foo::string FROM foo.bar AS r",
2453 Dialect::Snowflake,
2454 );
2455 assert!(issues.is_empty());
2456 }
2457
2458 #[test]
2459 fn snowflake_lateral_flatten_unused_alias_detected_and_fixable() {
2460 let sql = "SELECT r.rec:foo::string, value:bar::string \
2461 FROM foo.bar AS r, LATERAL FLATTEN(input => rec:result) AS x";
2462 let issues = check_sql_in_dialect(sql, Dialect::Snowflake);
2463 assert_eq!(issues.len(), 1);
2464 assert!(issues[0].message.contains("x"));
2465 let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
2466 assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
2467 let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
2468 assert!(
2469 !fixed.contains("AS x"),
2470 "autofix should remove the unused LATERAL alias"
2471 );
2472 }
2473
2474 #[test]
2475 fn autofix_removes_double_quoted_alias_in_dialect_mode() {
2476 let sql = "SELECT a.col_1\nFROM table_a AS \"A\"\n";
2477 let issues = check_sql_in_dialect(sql, Dialect::Postgres);
2478 assert_eq!(issues.len(), 1);
2479 assert!(issues[0].message.contains("A"));
2480 let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
2481 let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
2482 assert_eq!(fixed, "SELECT a.col_1\nFROM table_a\n");
2483 assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
2484 }
2485
2486 #[test]
2487 fn autofix_removes_single_quoted_alias() {
2488 let sql = "SELECT a.col1\nFROM tab1 as 'a'\n";
2489 let issues = check_sql(sql);
2490 assert_eq!(issues.len(), 1);
2491 let autofix = issues[0].autofix.as_ref().expect("autofix metadata");
2492 let fixed = apply_issue_autofix(sql, &issues[0]).expect("apply autofix");
2493 assert_eq!(fixed, "SELECT a.col1\nFROM tab1\n");
2494 assert_eq!(autofix.applicability, IssueAutofixApplicability::Safe);
2495 }
2496}