Skip to main content

flowscope_core/linter/rules/
al_006.rs

1//! LINT_AL_006: Alias length.
2//!
3//! SQLFluff AL06 parity (current scope): table aliases longer than 30
4//! characters are discouraged.
5
6use crate::linter::config::LintConfig;
7use crate::linter::rule::{LintContext, LintRule};
8use crate::types::{issue_codes, Issue};
9use sqlparser::ast::{Select, Statement, TableFactor, TableWithJoins};
10
11use super::semantic_helpers::{table_factor_alias_name, visit_selects_in_statement};
12
13const DEFAULT_MIN_ALIAS_LENGTH: usize = 0;
14const DEFAULT_MAX_ALIAS_LENGTH: Option<usize> = None;
15
16pub struct AliasingLength {
17    min_alias_length: usize,
18    max_alias_length: Option<usize>,
19}
20
21impl AliasingLength {
22    pub fn from_config(config: &LintConfig) -> Self {
23        Self {
24            min_alias_length: config
25                .rule_option_usize(issue_codes::LINT_AL_006, "min_alias_length")
26                .unwrap_or(DEFAULT_MIN_ALIAS_LENGTH),
27            max_alias_length: config
28                .rule_option_usize(issue_codes::LINT_AL_006, "max_alias_length")
29                .or(DEFAULT_MAX_ALIAS_LENGTH),
30        }
31    }
32}
33
34impl Default for AliasingLength {
35    fn default() -> Self {
36        Self {
37            min_alias_length: DEFAULT_MIN_ALIAS_LENGTH,
38            max_alias_length: DEFAULT_MAX_ALIAS_LENGTH,
39        }
40    }
41}
42
43impl LintRule for AliasingLength {
44    fn code(&self) -> &'static str {
45        issue_codes::LINT_AL_006
46    }
47
48    fn name(&self) -> &'static str {
49        "Alias length"
50    }
51
52    fn description(&self) -> &'static str {
53        "Enforce table alias lengths in from clauses and join conditions."
54    }
55
56    fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
57        let mut violations = 0usize;
58
59        visit_selects_in_statement(statement, &mut |select| {
60            violations += alias_length_violation_count_in_select(
61                select,
62                self.min_alias_length,
63                self.max_alias_length,
64            );
65        });
66
67        (0..violations)
68            .map(|_| {
69                Issue::info(
70                    issue_codes::LINT_AL_006,
71                    "Alias length violates configured bounds.",
72                )
73                .with_statement(ctx.statement_index)
74            })
75            .collect()
76    }
77}
78
79fn alias_length_violation_count_in_select(
80    select: &Select,
81    min_alias_length: usize,
82    max_alias_length: Option<usize>,
83) -> usize {
84    let mut count = 0usize;
85
86    for table in &select.from {
87        count += alias_length_violation_count_in_table_with_joins(
88            table,
89            min_alias_length,
90            max_alias_length,
91        );
92    }
93
94    count
95}
96
97fn alias_length_violation_count_in_table_with_joins(
98    table_with_joins: &TableWithJoins,
99    min_alias_length: usize,
100    max_alias_length: Option<usize>,
101) -> usize {
102    let mut count = alias_length_violation_count_in_table_factor(
103        &table_with_joins.relation,
104        min_alias_length,
105        max_alias_length,
106    );
107    for join in &table_with_joins.joins {
108        count += alias_length_violation_count_in_table_factor(
109            &join.relation,
110            min_alias_length,
111            max_alias_length,
112        );
113    }
114    count
115}
116
117fn alias_length_violation_count_in_table_factor(
118    table_factor: &TableFactor,
119    min_alias_length: usize,
120    max_alias_length: Option<usize>,
121) -> usize {
122    let mut count = 0usize;
123
124    if table_factor_alias_name(table_factor)
125        .is_some_and(|alias| alias_length_violates(alias, min_alias_length, max_alias_length))
126    {
127        count += 1;
128    }
129
130    match table_factor {
131        TableFactor::NestedJoin {
132            table_with_joins, ..
133        } => {
134            count += alias_length_violation_count_in_table_with_joins(
135                table_with_joins,
136                min_alias_length,
137                max_alias_length,
138            )
139        }
140        TableFactor::Pivot { table, .. }
141        | TableFactor::Unpivot { table, .. }
142        | TableFactor::MatchRecognize { table, .. } => {
143            count += alias_length_violation_count_in_table_factor(
144                table,
145                min_alias_length,
146                max_alias_length,
147            )
148        }
149        _ => {}
150    }
151
152    count
153}
154
155fn alias_length_violates(
156    alias: &str,
157    min_alias_length: usize,
158    max_alias_length: Option<usize>,
159) -> bool {
160    let length = alias.len();
161    if length < min_alias_length {
162        return true;
163    }
164
165    if let Some(max_alias_length) = max_alias_length {
166        return length > max_alias_length;
167    }
168
169    false
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::parser::parse_sql;
176
177    fn run(sql: &str) -> Vec<Issue> {
178        let statements = parse_sql(sql).expect("parse");
179        let rule = AliasingLength::default();
180        statements
181            .iter()
182            .enumerate()
183            .flat_map(|(index, statement)| {
184                rule.check(
185                    statement,
186                    &LintContext {
187                        sql,
188                        statement_range: 0..sql.len(),
189                        statement_index: index,
190                    },
191                )
192            })
193            .collect()
194    }
195
196    #[test]
197    fn default_does_not_flag_overlong_table_alias() {
198        let issues = run("SELECT * FROM users this_alias_name_is_longer_than_thirty_chars");
199        assert!(issues.is_empty());
200    }
201
202    #[test]
203    fn does_not_flag_short_alias() {
204        let issues = run("SELECT * FROM users u");
205        assert!(issues.is_empty());
206    }
207
208    #[test]
209    fn does_not_flag_alias_at_length_limit() {
210        let issues = run("SELECT * FROM users aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
211        assert!(issues.is_empty());
212    }
213
214    #[test]
215    fn default_does_not_flag_nested_select_alias() {
216        let issues = run(
217            "SELECT * FROM (SELECT * FROM users this_alias_name_is_longer_than_thirty_chars) sub",
218        );
219        assert!(issues.is_empty());
220    }
221
222    #[test]
223    fn flags_overlong_table_alias_with_max_length_config() {
224        let statements =
225            parse_sql("SELECT * FROM users this_alias_name_is_longer_than_thirty_chars")
226                .expect("parse");
227        let config = LintConfig {
228            enabled: true,
229            disabled_rules: vec![],
230            rule_configs: std::collections::BTreeMap::from([(
231                "aliasing.length".to_string(),
232                serde_json::json!({"max_alias_length": 30}),
233            )]),
234        };
235        let rule = AliasingLength::from_config(&config);
236
237        let issues = statements
238            .iter()
239            .enumerate()
240            .flat_map(|(index, statement)| {
241                rule.check(
242                    statement,
243                    &LintContext {
244                        sql: "SELECT * FROM users this_alias_name_is_longer_than_thirty_chars",
245                        statement_range: 0
246                            .."SELECT * FROM users this_alias_name_is_longer_than_thirty_chars"
247                                .len(),
248                        statement_index: index,
249                    },
250                )
251            })
252            .collect::<Vec<_>>();
253
254        assert_eq!(issues.len(), 1);
255        assert_eq!(issues[0].code, issue_codes::LINT_AL_006);
256    }
257
258    #[test]
259    fn applies_max_alias_length_from_config() {
260        let statements = parse_sql("SELECT * FROM users eleven_chars").expect("parse");
261        let config = LintConfig {
262            enabled: true,
263            disabled_rules: vec![],
264            rule_configs: std::collections::BTreeMap::from([(
265                "LINT_AL_006".to_string(),
266                serde_json::json!({"max_alias_length": 10}),
267            )]),
268        };
269        let rule = AliasingLength::from_config(&config);
270
271        let issues = statements
272            .iter()
273            .enumerate()
274            .flat_map(|(index, statement)| {
275                rule.check(
276                    statement,
277                    &LintContext {
278                        sql: "SELECT * FROM users eleven_chars",
279                        statement_range: 0.."SELECT * FROM users eleven_chars".len(),
280                        statement_index: index,
281                    },
282                )
283            })
284            .collect::<Vec<_>>();
285
286        assert_eq!(issues.len(), 1);
287    }
288
289    #[test]
290    fn applies_min_alias_length_from_config() {
291        let statements = parse_sql("SELECT * FROM users a").expect("parse");
292        let config = LintConfig {
293            enabled: true,
294            disabled_rules: vec![],
295            rule_configs: std::collections::BTreeMap::from([(
296                "aliasing.length".to_string(),
297                serde_json::json!({"min_alias_length": 2}),
298            )]),
299        };
300        let rule = AliasingLength::from_config(&config);
301
302        let issues = statements
303            .iter()
304            .enumerate()
305            .flat_map(|(index, statement)| {
306                rule.check(
307                    statement,
308                    &LintContext {
309                        sql: "SELECT * FROM users a",
310                        statement_range: 0.."SELECT * FROM users a".len(),
311                        statement_index: index,
312                    },
313                )
314            })
315            .collect::<Vec<_>>();
316
317        assert_eq!(issues.len(), 1);
318    }
319}