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