sqruff_lib/rules/ambiguous/
am03.rs

1use ahash::{AHashMap, AHashSet};
2use smol_str::{SmolStr, StrExt};
3use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
4use sqruff_lib_core::lint_fix::LintFix;
5use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder};
6
7use crate::core::config::Value;
8use crate::core::rules::base::{CloneRule, ErasedRule, LintResult, Rule, RuleGroups};
9use crate::core::rules::context::RuleContext;
10use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
11
12#[derive(Clone, Debug, Default)]
13pub struct RuleAM03;
14
15impl Rule for RuleAM03 {
16    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
17        Ok(RuleAM03.erased())
18    }
19
20    fn name(&self) -> &'static str {
21        "ambiguous.order_by"
22    }
23
24    fn description(&self) -> &'static str {
25        "Ambiguous ordering directions for columns in order by clause."
26    }
27
28    fn long_description(&self) -> &'static str {
29        r#"
30**Anti-pattern**
31
32In this example, the `ORDER BY` clause is ambiguous because some columns are explicitly ordered, while others are not.
33
34```sql
35SELECT
36    a, b
37FROM foo
38ORDER BY a, b DESC
39```
40
41**Best practice**
42
43If any columns in the `ORDER BY` clause specify `ASC` or `DESC`, they should all do so.
44
45```sql
46SELECT
47    a, b
48FROM foo
49ORDER BY a ASC, b DESC
50```
51"#
52    }
53
54    fn groups(&self) -> &'static [RuleGroups] {
55        &[RuleGroups::All, RuleGroups::Ambiguous]
56    }
57
58    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
59        // Only trigger on orderby_clause
60        let order_by_spec = Self::get_order_by_info(context.segment.clone());
61        let order_types = order_by_spec
62            .iter()
63            .map(|spec| spec.order.clone())
64            .collect::<AHashSet<Option<_>>>();
65
66        // If all or no columns are explicitly ordered, then it's not ambiguous
67        if !order_types.contains(&None) || (order_types.len() == 1 && order_types.contains(&None)) {
68            return vec![];
69        }
70
71        // If there is a mix of explicit and implicit ordering, then it's ambiguous
72        let fixes = order_by_spec
73            .into_iter()
74            .filter(|spec| spec.order.is_none())
75            .map(|spec| {
76                LintFix::create_after(
77                    spec.column_reference,
78                    vec![
79                        SegmentBuilder::whitespace(context.tables.next_id(), " "),
80                        SegmentBuilder::keyword(context.tables.next_id(), "ASC"),
81                    ],
82                    None,
83                )
84            })
85            .collect();
86
87        vec![LintResult::new(
88            Some(context.segment.clone()),
89            fixes,
90            None,
91            None,
92        )]
93    }
94
95    fn is_fix_compatible(&self) -> bool {
96        true
97    }
98
99    fn crawl_behaviour(&self) -> Crawler {
100        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::OrderbyClause]) }).into()
101    }
102}
103
104/// For AM03, segment that ends an ORDER BY column and any order provided.
105struct OrderByColumnInfo {
106    column_reference: ErasedSegment,
107    order: Option<SmolStr>,
108}
109
110impl RuleAM03 {
111    fn get_order_by_info(segment: ErasedSegment) -> Vec<OrderByColumnInfo> {
112        assert!(segment.is_type(SyntaxKind::OrderbyClause));
113
114        let mut result = vec![];
115        let mut column_reference = None;
116        let mut ordering_reference = None;
117
118        for child_segment in segment.segments() {
119            if child_segment.is_type(SyntaxKind::ColumnReference) {
120                column_reference = Some(child_segment.clone());
121            } else if child_segment.is_type(SyntaxKind::Keyword)
122                && (child_segment.raw().eq_ignore_ascii_case("ASC")
123                    || child_segment.raw().eq_ignore_ascii_case("DESC"))
124            {
125                ordering_reference = Some(child_segment.raw().to_uppercase_smolstr());
126            };
127
128            if column_reference.is_some() && child_segment.raw() == "," {
129                result.push(OrderByColumnInfo {
130                    column_reference: column_reference.clone().unwrap(),
131                    order: ordering_reference.clone(),
132                });
133
134                column_reference = None;
135                ordering_reference = None;
136            }
137        }
138        // Special handling for last column
139        if column_reference.is_some() {
140            result.push(OrderByColumnInfo {
141                column_reference: column_reference.clone().unwrap(),
142                order: ordering_reference.clone(),
143            });
144        }
145
146        result
147    }
148}