sqruff_lib/rules/ambiguous/
am03.rs1use 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 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 !order_types.contains(&None) || (order_types.len() == 1 && order_types.contains(&None)) {
68 return vec![];
69 }
70
71 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
104struct 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 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}