1use crate::linter::config::LintConfig;
6use crate::linter::rule::{LintContext, LintRule};
7use crate::types::{issue_codes, Issue};
8use sqlparser::ast::{
9 Expr, FunctionArg, FunctionArgExpr, FunctionArguments, Query, Select, SetExpr, Statement,
10 TableFactor, TableWithJoins, WindowType,
11};
12use std::collections::HashSet;
13
14use super::semantic_helpers::visit_select_expressions;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17enum AliasCaseCheck {
18 Dialect,
19 CaseInsensitive,
20 QuotedCsNakedUpper,
21 QuotedCsNakedLower,
22 CaseSensitive,
23}
24
25impl AliasCaseCheck {
26 fn from_config(config: &LintConfig) -> Self {
27 match config
28 .rule_option_str(issue_codes::LINT_AL_004, "alias_case_check")
29 .unwrap_or("dialect")
30 .to_ascii_lowercase()
31 .as_str()
32 {
33 "case_insensitive" => Self::CaseInsensitive,
34 "quoted_cs_naked_upper" => Self::QuotedCsNakedUpper,
35 "quoted_cs_naked_lower" => Self::QuotedCsNakedLower,
36 "case_sensitive" => Self::CaseSensitive,
37 _ => Self::Dialect,
38 }
39 }
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
43struct AliasRef {
44 name: String,
45 quoted: bool,
46}
47
48pub struct AliasingUniqueTable {
49 alias_case_check: AliasCaseCheck,
50}
51
52impl AliasingUniqueTable {
53 pub fn from_config(config: &LintConfig) -> Self {
54 Self {
55 alias_case_check: AliasCaseCheck::from_config(config),
56 }
57 }
58}
59
60impl Default for AliasingUniqueTable {
61 fn default() -> Self {
62 Self {
63 alias_case_check: AliasCaseCheck::Dialect,
64 }
65 }
66}
67
68impl LintRule for AliasingUniqueTable {
69 fn code(&self) -> &'static str {
70 issue_codes::LINT_AL_004
71 }
72
73 fn name(&self) -> &'static str {
74 "Unique table alias"
75 }
76
77 fn description(&self) -> &'static str {
78 "Table aliases should be unique within each clause."
79 }
80
81 fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
82 if first_duplicate_table_alias_in_statement(statement, self.alias_case_check).is_none() {
83 return Vec::new();
84 }
85
86 vec![Issue::warning(
87 issue_codes::LINT_AL_004,
88 "Table aliases should be unique within a statement.",
89 )
90 .with_statement(ctx.statement_index)]
91 }
92}
93
94fn first_duplicate_table_alias_in_statement(
95 statement: &Statement,
96 alias_case_check: AliasCaseCheck,
97) -> Option<String> {
98 match statement {
99 Statement::Query(query) => {
100 first_duplicate_table_alias_in_query_with_parent(query, &[], alias_case_check)
101 }
102 Statement::Insert(insert) => insert.source.as_deref().and_then(|query| {
103 first_duplicate_table_alias_in_query_with_parent(query, &[], alias_case_check)
104 }),
105 Statement::CreateView { query, .. } => {
106 first_duplicate_table_alias_in_query_with_parent(query, &[], alias_case_check)
107 }
108 Statement::CreateTable(create) => create.query.as_deref().and_then(|query| {
109 first_duplicate_table_alias_in_query_with_parent(query, &[], alias_case_check)
110 }),
111 _ => None,
112 }
113}
114
115fn first_duplicate_table_alias_in_query_with_parent(
116 query: &Query,
117 parent_aliases: &[AliasRef],
118 alias_case_check: AliasCaseCheck,
119) -> Option<String> {
120 if let Some(with) = &query.with {
121 for cte in &with.cte_tables {
122 if let Some(duplicate) =
123 first_duplicate_table_alias_in_query_with_parent(&cte.query, &[], alias_case_check)
124 {
125 return Some(duplicate);
126 }
127 }
128 }
129
130 first_duplicate_table_alias_in_set_expr_with_parent(
131 &query.body,
132 parent_aliases,
133 alias_case_check,
134 )
135}
136
137fn first_duplicate_table_alias_in_set_expr_with_parent(
138 set_expr: &SetExpr,
139 parent_aliases: &[AliasRef],
140 alias_case_check: AliasCaseCheck,
141) -> Option<String> {
142 match set_expr {
143 SetExpr::Select(select) => first_duplicate_table_alias_in_select_with_parent(
144 select,
145 parent_aliases,
146 alias_case_check,
147 ),
148 SetExpr::Query(query) => first_duplicate_table_alias_in_query_with_parent(
149 query,
150 parent_aliases,
151 alias_case_check,
152 ),
153 SetExpr::SetOperation { left, right, .. } => {
154 first_duplicate_table_alias_in_set_expr_with_parent(
155 left,
156 parent_aliases,
157 alias_case_check,
158 )
159 .or_else(|| {
160 first_duplicate_table_alias_in_set_expr_with_parent(
161 right,
162 parent_aliases,
163 alias_case_check,
164 )
165 })
166 }
167 SetExpr::Insert(statement)
168 | SetExpr::Update(statement)
169 | SetExpr::Delete(statement)
170 | SetExpr::Merge(statement) => {
171 first_duplicate_table_alias_in_statement(statement, alias_case_check)
172 }
173 _ => None,
174 }
175}
176
177fn first_duplicate_table_alias_in_select_with_parent(
178 select: &Select,
179 parent_aliases: &[AliasRef],
180 alias_case_check: AliasCaseCheck,
181) -> Option<String> {
182 let mut aliases = Vec::new();
183 for table_with_joins in &select.from {
184 collect_scope_table_aliases(table_with_joins, &mut aliases);
185 }
186
187 let mut aliases_with_parent = parent_aliases.to_vec();
188 aliases_with_parent.extend(aliases);
189
190 if let Some(duplicate) = first_duplicate_alias(&aliases_with_parent, alias_case_check) {
191 return Some(duplicate);
192 }
193
194 if let Some(duplicate) = first_duplicate_table_alias_in_select_expression_subqueries(
195 select,
196 &aliases_with_parent,
197 alias_case_check,
198 ) {
199 return Some(duplicate);
200 }
201
202 for table_with_joins in &select.from {
203 if let Some(duplicate) = first_duplicate_table_alias_in_table_with_joins_children(
204 table_with_joins,
205 &aliases_with_parent,
206 alias_case_check,
207 ) {
208 return Some(duplicate);
209 }
210 }
211
212 None
213}
214
215fn first_duplicate_table_alias_in_select_expression_subqueries(
216 select: &Select,
217 parent_aliases: &[AliasRef],
218 alias_case_check: AliasCaseCheck,
219) -> Option<String> {
220 let mut duplicate = None;
221 visit_select_expressions(select, &mut |expr| {
222 if duplicate.is_none() {
223 duplicate = first_duplicate_table_alias_in_expr_with_parent(
224 expr,
225 parent_aliases,
226 alias_case_check,
227 );
228 }
229 });
230 duplicate
231}
232
233fn first_duplicate_table_alias_in_expr_with_parent(
234 expr: &Expr,
235 parent_aliases: &[AliasRef],
236 alias_case_check: AliasCaseCheck,
237) -> Option<String> {
238 match expr {
239 Expr::Subquery(query)
240 | Expr::Exists {
241 subquery: query, ..
242 } => first_duplicate_table_alias_in_query_with_parent(
243 query,
244 parent_aliases,
245 alias_case_check,
246 ),
247 Expr::InSubquery {
248 expr: inner,
249 subquery,
250 ..
251 } => {
252 first_duplicate_table_alias_in_expr_with_parent(inner, parent_aliases, alias_case_check)
253 .or_else(|| {
254 first_duplicate_table_alias_in_query_with_parent(
255 subquery,
256 parent_aliases,
257 alias_case_check,
258 )
259 })
260 }
261 Expr::BinaryOp { left, right, .. }
262 | Expr::AnyOp { left, right, .. }
263 | Expr::AllOp { left, right, .. } => {
264 first_duplicate_table_alias_in_expr_with_parent(left, parent_aliases, alias_case_check)
265 .or_else(|| {
266 first_duplicate_table_alias_in_expr_with_parent(
267 right,
268 parent_aliases,
269 alias_case_check,
270 )
271 })
272 }
273 Expr::UnaryOp { expr: inner, .. }
274 | Expr::Nested(inner)
275 | Expr::IsNull(inner)
276 | Expr::IsNotNull(inner)
277 | Expr::Cast { expr: inner, .. } => {
278 first_duplicate_table_alias_in_expr_with_parent(inner, parent_aliases, alias_case_check)
279 }
280 Expr::InList { expr, list, .. } => {
281 first_duplicate_table_alias_in_expr_with_parent(expr, parent_aliases, alias_case_check)
282 .or_else(|| {
283 for item in list {
284 if let Some(duplicate) = first_duplicate_table_alias_in_expr_with_parent(
285 item,
286 parent_aliases,
287 alias_case_check,
288 ) {
289 return Some(duplicate);
290 }
291 }
292 None
293 })
294 }
295 Expr::Between {
296 expr, low, high, ..
297 } => {
298 first_duplicate_table_alias_in_expr_with_parent(expr, parent_aliases, alias_case_check)
299 .or_else(|| {
300 first_duplicate_table_alias_in_expr_with_parent(
301 low,
302 parent_aliases,
303 alias_case_check,
304 )
305 })
306 .or_else(|| {
307 first_duplicate_table_alias_in_expr_with_parent(
308 high,
309 parent_aliases,
310 alias_case_check,
311 )
312 })
313 }
314 Expr::Case {
315 operand,
316 conditions,
317 else_result,
318 ..
319 } => {
320 if let Some(operand) = operand {
321 if let Some(duplicate) = first_duplicate_table_alias_in_expr_with_parent(
322 operand,
323 parent_aliases,
324 alias_case_check,
325 ) {
326 return Some(duplicate);
327 }
328 }
329 for when in conditions {
330 if let Some(duplicate) = first_duplicate_table_alias_in_expr_with_parent(
331 &when.condition,
332 parent_aliases,
333 alias_case_check,
334 ) {
335 return Some(duplicate);
336 }
337 if let Some(duplicate) = first_duplicate_table_alias_in_expr_with_parent(
338 &when.result,
339 parent_aliases,
340 alias_case_check,
341 ) {
342 return Some(duplicate);
343 }
344 }
345 if let Some(otherwise) = else_result {
346 return first_duplicate_table_alias_in_expr_with_parent(
347 otherwise,
348 parent_aliases,
349 alias_case_check,
350 );
351 }
352 None
353 }
354 Expr::Function(function) => {
355 if let FunctionArguments::List(arguments) = &function.args {
356 for arg in &arguments.args {
357 match arg {
358 FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))
359 | FunctionArg::Named {
360 arg: FunctionArgExpr::Expr(expr),
361 ..
362 } => {
363 if let Some(duplicate) = first_duplicate_table_alias_in_expr_with_parent(
364 expr,
365 parent_aliases,
366 alias_case_check,
367 ) {
368 return Some(duplicate);
369 }
370 }
371 _ => {}
372 }
373 }
374 }
375
376 if let Some(filter) = &function.filter {
377 if let Some(duplicate) = first_duplicate_table_alias_in_expr_with_parent(
378 filter,
379 parent_aliases,
380 alias_case_check,
381 ) {
382 return Some(duplicate);
383 }
384 }
385
386 for order_expr in &function.within_group {
387 if let Some(duplicate) = first_duplicate_table_alias_in_expr_with_parent(
388 &order_expr.expr,
389 parent_aliases,
390 alias_case_check,
391 ) {
392 return Some(duplicate);
393 }
394 }
395
396 if let Some(WindowType::WindowSpec(spec)) = &function.over {
397 for expr in &spec.partition_by {
398 if let Some(duplicate) = first_duplicate_table_alias_in_expr_with_parent(
399 expr,
400 parent_aliases,
401 alias_case_check,
402 ) {
403 return Some(duplicate);
404 }
405 }
406 for order_expr in &spec.order_by {
407 if let Some(duplicate) = first_duplicate_table_alias_in_expr_with_parent(
408 &order_expr.expr,
409 parent_aliases,
410 alias_case_check,
411 ) {
412 return Some(duplicate);
413 }
414 }
415 }
416
417 None
418 }
419 _ => None,
420 }
421}
422
423fn collect_scope_table_aliases(table_with_joins: &TableWithJoins, aliases: &mut Vec<AliasRef>) {
424 collect_scope_table_aliases_from_factor(&table_with_joins.relation, aliases);
425 for join in &table_with_joins.joins {
426 collect_scope_table_aliases_from_factor(&join.relation, aliases);
427 }
428}
429
430fn collect_scope_table_aliases_from_factor(
431 table_factor: &TableFactor,
432 aliases: &mut Vec<AliasRef>,
433) {
434 if let Some(alias) = inferred_alias_name(table_factor) {
435 aliases.push(alias);
436 }
437
438 match table_factor {
439 TableFactor::NestedJoin {
440 table_with_joins, ..
441 } => collect_scope_table_aliases(table_with_joins, aliases),
442 TableFactor::Pivot { table, .. }
443 | TableFactor::Unpivot { table, .. }
444 | TableFactor::MatchRecognize { table, .. } => {
445 collect_scope_table_aliases_from_factor(table, aliases)
446 }
447 _ => {}
448 }
449}
450
451fn inferred_alias_name(table_factor: &TableFactor) -> Option<AliasRef> {
452 if let Some(alias) = explicit_alias_name(table_factor) {
453 return Some(alias);
454 }
455
456 match table_factor {
457 TableFactor::Table { name, .. } => name.0.last().map(|part| {
458 if let Some(ident) = part.as_ident() {
459 AliasRef {
460 name: ident.value.clone(),
461 quoted: ident.quote_style.is_some(),
462 }
463 } else {
464 AliasRef {
465 name: part.to_string(),
466 quoted: false,
467 }
468 }
469 }),
470 _ => None,
471 }
472}
473
474fn explicit_alias_name(table_factor: &TableFactor) -> Option<AliasRef> {
475 let alias = match table_factor {
476 TableFactor::Table { alias, .. }
477 | TableFactor::Derived { alias, .. }
478 | TableFactor::TableFunction { alias, .. }
479 | TableFactor::Function { alias, .. }
480 | TableFactor::UNNEST { alias, .. }
481 | TableFactor::JsonTable { alias, .. }
482 | TableFactor::OpenJsonTable { alias, .. }
483 | TableFactor::NestedJoin { alias, .. }
484 | TableFactor::Pivot { alias, .. }
485 | TableFactor::Unpivot { alias, .. }
486 | TableFactor::MatchRecognize { alias, .. }
487 | TableFactor::XmlTable { alias, .. }
488 | TableFactor::SemanticView { alias, .. } => alias.as_ref(),
489 }?;
490
491 Some(AliasRef {
492 name: alias.name.value.clone(),
493 quoted: alias.name.quote_style.is_some(),
494 })
495}
496
497fn first_duplicate_table_alias_in_table_with_joins_children(
498 table_with_joins: &TableWithJoins,
499 parent_aliases: &[AliasRef],
500 alias_case_check: AliasCaseCheck,
501) -> Option<String> {
502 first_duplicate_table_alias_in_table_factor_children(
503 &table_with_joins.relation,
504 parent_aliases,
505 alias_case_check,
506 )
507 .or_else(|| {
508 for join in &table_with_joins.joins {
509 if let Some(duplicate) = first_duplicate_table_alias_in_table_factor_children(
510 &join.relation,
511 parent_aliases,
512 alias_case_check,
513 ) {
514 return Some(duplicate);
515 }
516 }
517 None
518 })
519}
520
521fn child_parent_aliases(
522 parent_aliases: &[AliasRef],
523 table_factor: &TableFactor,
524 alias_case_check: AliasCaseCheck,
525) -> Vec<AliasRef> {
526 let mut next = parent_aliases.to_vec();
527 if let Some(alias) = inferred_alias_name(table_factor) {
528 if let Some(index) = next
529 .iter()
530 .position(|existing| aliases_match(existing, &alias, alias_case_check))
531 {
532 next.remove(index);
533 }
534 }
535 next
536}
537
538fn first_duplicate_table_alias_in_table_factor_children(
539 table_factor: &TableFactor,
540 parent_aliases: &[AliasRef],
541 alias_case_check: AliasCaseCheck,
542) -> Option<String> {
543 let child_parent_aliases = child_parent_aliases(parent_aliases, table_factor, alias_case_check);
544
545 match table_factor {
546 TableFactor::Derived { subquery, .. } => first_duplicate_table_alias_in_query_with_parent(
547 subquery,
548 &child_parent_aliases,
549 alias_case_check,
550 ),
551 TableFactor::NestedJoin {
552 table_with_joins, ..
553 } => first_duplicate_table_alias_in_nested_scope(
554 table_with_joins,
555 &child_parent_aliases,
556 alias_case_check,
557 ),
558 TableFactor::Pivot { table, .. }
559 | TableFactor::Unpivot { table, .. }
560 | TableFactor::MatchRecognize { table, .. } => {
561 first_duplicate_table_alias_in_table_factor_children(
562 table,
563 &child_parent_aliases,
564 alias_case_check,
565 )
566 }
567 _ => None,
568 }
569}
570
571fn first_duplicate_table_alias_in_nested_scope(
572 table_with_joins: &TableWithJoins,
573 parent_aliases: &[AliasRef],
574 alias_case_check: AliasCaseCheck,
575) -> Option<String> {
576 let mut aliases = Vec::new();
577 collect_scope_table_aliases(table_with_joins, &mut aliases);
578 let mut aliases_with_parent = parent_aliases.to_vec();
579 aliases_with_parent.extend(aliases);
580
581 if let Some(duplicate) = first_duplicate_alias(&aliases_with_parent, alias_case_check) {
582 return Some(duplicate);
583 }
584
585 first_duplicate_table_alias_in_table_with_joins_children(
586 table_with_joins,
587 &aliases_with_parent,
588 alias_case_check,
589 )
590}
591
592fn first_duplicate_alias(values: &[AliasRef], alias_case_check: AliasCaseCheck) -> Option<String> {
593 let mut seen_case_insensitive = HashSet::new();
594 let mut seen: Vec<&AliasRef> = Vec::new();
595
596 for value in values {
597 if matches!(alias_case_check, AliasCaseCheck::CaseInsensitive) {
598 let key = value.name.to_ascii_uppercase();
599 if !seen_case_insensitive.insert(key) {
600 return Some(value.name.clone());
601 }
602 continue;
603 }
604
605 let is_duplicate = seen
606 .iter()
607 .any(|existing| aliases_match(existing, value, alias_case_check));
608 if is_duplicate {
609 return Some(value.name.clone());
610 }
611 seen.push(value);
612 }
613
614 None
615}
616
617fn aliases_match(left: &AliasRef, right: &AliasRef, alias_case_check: AliasCaseCheck) -> bool {
618 match alias_case_check {
619 AliasCaseCheck::CaseInsensitive => left.name.eq_ignore_ascii_case(&right.name),
620 AliasCaseCheck::CaseSensitive => left.name == right.name,
621 AliasCaseCheck::Dialect => {
622 if left.quoted || right.quoted {
623 left.name == right.name
624 } else {
625 left.name.eq_ignore_ascii_case(&right.name)
626 }
627 }
628 AliasCaseCheck::QuotedCsNakedUpper | AliasCaseCheck::QuotedCsNakedLower => {
629 normalize_alias_for_mode(left, alias_case_check)
630 == normalize_alias_for_mode(right, alias_case_check)
631 }
632 }
633}
634
635fn normalize_alias_for_mode(alias: &AliasRef, mode: AliasCaseCheck) -> String {
636 match mode {
637 AliasCaseCheck::QuotedCsNakedUpper => {
638 if alias.quoted {
639 alias.name.clone()
640 } else {
641 alias.name.to_ascii_uppercase()
642 }
643 }
644 AliasCaseCheck::QuotedCsNakedLower => {
645 if alias.quoted {
646 alias.name.clone()
647 } else {
648 alias.name.to_ascii_lowercase()
649 }
650 }
651 _ => alias.name.clone(),
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use crate::parser::parse_sql;
659
660 fn run(sql: &str) -> Vec<Issue> {
661 let statements = parse_sql(sql).expect("parse");
662 let rule = AliasingUniqueTable::default();
663 statements
664 .iter()
665 .enumerate()
666 .flat_map(|(index, statement)| {
667 rule.check(
668 statement,
669 &LintContext {
670 sql,
671 statement_range: 0..sql.len(),
672 statement_index: index,
673 },
674 )
675 })
676 .collect()
677 }
678
679 #[test]
680 fn flags_duplicate_alias_in_same_scope() {
681 let issues = run("select * from users u join orders u on u.id = u.user_id");
682 assert_eq!(issues.len(), 1);
683 assert_eq!(issues[0].code, issue_codes::LINT_AL_004);
684 }
685
686 #[test]
687 fn allows_unique_aliases() {
688 let issues = run("select * from users u join orders o on u.id = o.user_id");
689 assert!(issues.is_empty());
690 }
691
692 #[test]
693 fn allows_same_alias_in_separate_cte_scopes() {
694 let sql = "with a as (select * from users u), b as (select * from orders u) select * from a join b on a.id = b.id";
695 let issues = run(sql);
696 assert!(issues.is_empty());
697 }
698
699 #[test]
700 fn flags_duplicate_alias_in_nested_subquery() {
701 let sql = "select * from (select * from users u join orders u on u.id = u.user_id) t";
702 let issues = run(sql);
703 assert_eq!(issues.len(), 1);
704 }
705
706 #[test]
707 fn flags_duplicate_implicit_table_name_aliases() {
708 let sql =
709 "select * from analytics.foo join reporting.foo on analytics.foo.id = reporting.foo.id";
710 let issues = run(sql);
711 assert_eq!(issues.len(), 1);
712 assert_eq!(issues[0].code, issue_codes::LINT_AL_004);
713 }
714
715 #[test]
716 fn flags_duplicate_alias_between_parent_and_subquery_scope() {
717 let sql = "select * from (select * from users a) s join orders a on s.id = a.user_id";
718 let issues = run(sql);
719 assert_eq!(issues.len(), 1);
720 assert_eq!(issues[0].code, issue_codes::LINT_AL_004);
721 }
722
723 #[test]
724 fn flags_duplicate_alias_between_parent_and_where_subquery_scope() {
725 let sql = "select * from tbl as t where t.val in (select t.val from tbl2 as t)";
726 let issues = run(sql);
727 assert_eq!(issues.len(), 1);
728 assert_eq!(issues[0].code, issue_codes::LINT_AL_004);
729 }
730
731 #[test]
732 fn flags_implicit_table_name_alias_collision_in_where_subquery() {
733 let sql = "select * from tbl where val in (select tbl.val from tbl2 as tbl)";
734 let issues = run(sql);
735 assert_eq!(issues.len(), 1);
736 assert_eq!(issues[0].code, issue_codes::LINT_AL_004);
737 }
738
739 #[test]
740 fn flags_implicit_table_name_collision_in_where_subquery() {
741 let sql = "select * from tbl where val in (select tbl.val from tbl)";
742 let issues = run(sql);
743 assert_eq!(issues.len(), 1);
744 assert_eq!(issues[0].code, issue_codes::LINT_AL_004);
745 }
746
747 #[test]
748 fn does_not_treat_subquery_alias_as_parent_alias_conflict() {
749 let sql = "select * from (select * from users s) s";
750 let issues = run(sql);
751 assert!(issues.is_empty());
752 }
753
754 #[test]
755 fn default_dialect_mode_does_not_flag_quoted_case_mismatch() {
756 let sql = "select * from users \"A\" join orders a on \"A\".id = a.user_id";
757 let issues = run(sql);
758 assert!(issues.is_empty());
759 }
760
761 #[test]
762 fn alias_case_check_case_sensitive_allows_case_mismatch() {
763 let sql = "select * from users a join orders A on a.id = A.user_id";
764 let statements = parse_sql(sql).expect("parse");
765 let rule = AliasingUniqueTable::from_config(&LintConfig {
766 enabled: true,
767 disabled_rules: vec![],
768 rule_configs: std::collections::BTreeMap::from([(
769 "aliasing.unique.table".to_string(),
770 serde_json::json!({"alias_case_check": "case_sensitive"}),
771 )]),
772 });
773 let issues = rule.check(
774 &statements[0],
775 &LintContext {
776 sql,
777 statement_range: 0..sql.len(),
778 statement_index: 0,
779 },
780 );
781 assert!(issues.is_empty());
782 }
783
784 #[test]
785 fn alias_case_check_case_sensitive_flags_exact_duplicates() {
786 let sql = "select * from users a join orders a on a.id = a.user_id";
787 let statements = parse_sql(sql).expect("parse");
788 let rule = AliasingUniqueTable::from_config(&LintConfig {
789 enabled: true,
790 disabled_rules: vec![],
791 rule_configs: std::collections::BTreeMap::from([(
792 "LINT_AL_004".to_string(),
793 serde_json::json!({"alias_case_check": "case_sensitive"}),
794 )]),
795 });
796 let issues = rule.check(
797 &statements[0],
798 &LintContext {
799 sql,
800 statement_range: 0..sql.len(),
801 statement_index: 0,
802 },
803 );
804 assert_eq!(issues.len(), 1);
805 assert_eq!(issues[0].code, issue_codes::LINT_AL_004);
806 }
807
808 #[test]
809 fn alias_case_check_quoted_cs_naked_upper_flags_upper_fold_match() {
810 let sql = "select * from users \"FOO\" join orders foo on \"FOO\".id = foo.user_id";
811 let statements = parse_sql(sql).expect("parse");
812 let rule = AliasingUniqueTable::from_config(&LintConfig {
813 enabled: true,
814 disabled_rules: vec![],
815 rule_configs: std::collections::BTreeMap::from([(
816 "aliasing.unique.table".to_string(),
817 serde_json::json!({"alias_case_check": "quoted_cs_naked_upper"}),
818 )]),
819 });
820 let issues = rule.check(
821 &statements[0],
822 &LintContext {
823 sql,
824 statement_range: 0..sql.len(),
825 statement_index: 0,
826 },
827 );
828 assert_eq!(issues.len(), 1);
829 assert_eq!(issues[0].code, issue_codes::LINT_AL_004);
830 }
831
832 #[test]
833 fn alias_case_check_quoted_cs_naked_upper_allows_nonmatching_quoted_case() {
834 let sql = "select * from users \"foo\" join orders foo on \"foo\".id = foo.user_id";
835 let statements = parse_sql(sql).expect("parse");
836 let rule = AliasingUniqueTable::from_config(&LintConfig {
837 enabled: true,
838 disabled_rules: vec![],
839 rule_configs: std::collections::BTreeMap::from([(
840 "aliasing.unique.table".to_string(),
841 serde_json::json!({"alias_case_check": "quoted_cs_naked_upper"}),
842 )]),
843 });
844 let issues = rule.check(
845 &statements[0],
846 &LintContext {
847 sql,
848 statement_range: 0..sql.len(),
849 statement_index: 0,
850 },
851 );
852 assert!(issues.is_empty());
853 }
854
855 #[test]
856 fn alias_case_check_quoted_cs_naked_lower_flags_lower_fold_match() {
857 let sql = "select * from users \"foo\" join orders FOO on \"foo\".id = FOO.user_id";
858 let statements = parse_sql(sql).expect("parse");
859 let rule = AliasingUniqueTable::from_config(&LintConfig {
860 enabled: true,
861 disabled_rules: vec![],
862 rule_configs: std::collections::BTreeMap::from([(
863 "aliasing.unique.table".to_string(),
864 serde_json::json!({"alias_case_check": "quoted_cs_naked_lower"}),
865 )]),
866 });
867 let issues = rule.check(
868 &statements[0],
869 &LintContext {
870 sql,
871 statement_range: 0..sql.len(),
872 statement_index: 0,
873 },
874 );
875 assert_eq!(issues.len(), 1);
876 assert_eq!(issues[0].code, issue_codes::LINT_AL_004);
877 }
878
879 #[test]
880 fn alias_case_check_quoted_cs_naked_lower_allows_nonmatching_quoted_case() {
881 let sql = "select * from users \"FOO\" join orders FOO on \"FOO\".id = FOO.user_id";
882 let statements = parse_sql(sql).expect("parse");
883 let rule = AliasingUniqueTable::from_config(&LintConfig {
884 enabled: true,
885 disabled_rules: vec![],
886 rule_configs: std::collections::BTreeMap::from([(
887 "aliasing.unique.table".to_string(),
888 serde_json::json!({"alias_case_check": "quoted_cs_naked_lower"}),
889 )]),
890 });
891 let issues = rule.check(
892 &statements[0],
893 &LintContext {
894 sql,
895 statement_range: 0..sql.len(),
896 statement_index: 0,
897 },
898 );
899 assert!(issues.is_empty());
900 }
901}