Skip to main content

flowscope_core/linter/rules/
al_004.rs

1//! LINT_AL_004: Unique table alias.
2//!
3//! Table aliases should be unique within a query scope.
4
5use 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}