sqruff_lib/rules/aliasing/
al05.rs1use std::cell::RefCell;
2
3use hashbrown::{HashMap, HashSet};
4use smol_str::SmolStr;
5use sqruff_lib_core::dialects::Dialect;
6use sqruff_lib_core::dialects::common::AliasInfo;
7use sqruff_lib_core::dialects::init::DialectKind;
8use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
9use sqruff_lib_core::lint_fix::LintFix;
10use sqruff_lib_core::parser::segments::ErasedSegment;
11use sqruff_lib_core::parser::segments::object_reference::ObjectReferenceLevel;
12use sqruff_lib_core::utils::analysis::query::{Query, QueryInner};
13use sqruff_lib_core::utils::analysis::select::{
14 SelectStatementColumnsAndTables, get_select_statement_info,
15};
16
17use crate::core::config::Value;
18use crate::core::rules::context::RuleContext;
19use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
20use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
21
22#[derive(Default, Clone)]
23struct AL05QueryData {
24 aliases: Vec<AliasInfo>,
25 tbl_refs: Vec<SmolStr>,
26}
27
28type QueryKey<'a> = *const RefCell<QueryInner<'a>>;
29type AL05State<'a> = HashMap<QueryKey<'a>, AL05QueryData>;
30
31#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
32enum AliasCaseCheck {
33 #[default]
34 Dialect,
35 CaseInsensitive,
36 QuotedCaseSensitiveNakedUpper,
37 QuotedCaseSensitiveNakedLower,
38 CaseSensitive,
39}
40
41impl AliasCaseCheck {
42 fn from_config(value: &str) -> Result<Self, String> {
43 match value {
44 "dialect" => Ok(Self::Dialect),
45 "case_insensitive" => Ok(Self::CaseInsensitive),
46 "quoted_cs_naked_upper" => Ok(Self::QuotedCaseSensitiveNakedUpper),
47 "quoted_cs_naked_lower" => Ok(Self::QuotedCaseSensitiveNakedLower),
48 "case_sensitive" => Ok(Self::CaseSensitive),
49 other => Err(format!("Invalid alias_case_check value: {other}")),
50 }
51 }
52}
53
54#[derive(Debug, Default, Clone)]
55pub struct RuleAL05 {
56 alias_case_check: AliasCaseCheck,
57}
58
59impl Rule for RuleAL05 {
60 fn load_from_config(&self, config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
61 Ok(RuleAL05 {
62 alias_case_check: AliasCaseCheck::from_config(
63 config["alias_case_check"].as_string().unwrap(),
64 )?,
65 }
66 .erased())
67 }
68
69 fn name(&self) -> &'static str {
70 "aliasing.unused"
71 }
72
73 fn description(&self) -> &'static str {
74 "Tables should not be aliased if that alias is not used."
75 }
76
77 fn long_description(&self) -> &'static str {
78 r#"
79**Anti-pattern**
80
81In this example, alias `zoo` is not used.
82
83```sql
84SELECT
85 a
86FROM foo AS zoo
87```
88
89**Best practice**
90
91Use the alias or remove it. An unused alias makes code harder to read without changing any functionality.
92
93```sql
94SELECT
95 zoo.a
96FROM foo AS zoo
97
98-- Alternatively...
99
100SELECT
101 a
102FROM foo
103```
104"#
105 }
106
107 fn groups(&self) -> &'static [RuleGroups] {
108 &[RuleGroups::All, RuleGroups::Core, RuleGroups::Aliasing]
109 }
110
111 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
112 let mut violations = Vec::new();
113 let select_info = get_select_statement_info(&context.segment, context.dialect.into(), true);
114
115 let Some(select_info) = select_info else {
116 return Vec::new();
117 };
118
119 if select_info.table_aliases.is_empty() {
120 return Vec::new();
121 }
122
123 let query = Query::from_segment(&context.segment, context.dialect, None);
124 let mut payloads = AL05State::default();
125 self.analyze_table_aliases(query.clone(), context.dialect, &mut payloads);
126
127 let payload = payloads.get(&query.id()).cloned().unwrap_or_default();
128
129 if matches!(
130 context.dialect.name,
131 DialectKind::Redshift | DialectKind::Bigquery
132 ) {
133 let mut references: HashSet<SmolStr> = HashSet::new();
134 let mut aliases: HashSet<SmolStr> = HashSet::new();
135
136 for alias in &payload.aliases {
137 aliases.insert(self.alias_name(alias, context.dialect.name));
138 if let Some(alias_segment) = &alias.segment {
139 aliases.insert(self.normalize_identifier(alias_segment, context.dialect.name));
140 }
141 if let Some(object_reference) = &alias.object_reference {
142 for seg in object_reference.segments() {
143 if const {
144 SyntaxSet::new(&[
145 SyntaxKind::Identifier,
146 SyntaxKind::NakedIdentifier,
147 SyntaxKind::QuotedIdentifier,
148 SyntaxKind::ObjectReference,
149 ])
150 }
151 .contains(seg.get_type())
152 {
153 references.insert(self.normalize_identifier(seg, context.dialect.name));
154 }
155 }
156 }
157 }
158
159 if aliases.intersection(&references).next().is_some() {
160 return Vec::new();
161 }
162 }
163
164 let mut ref_counter: HashMap<SmolStr, usize> = HashMap::new();
165 for alias in &payload.aliases {
166 let Some(object_reference) = &alias.object_reference else {
167 continue;
168 };
169 let Some(last_segment) = object_reference.segments().last() else {
170 continue;
171 };
172
173 *ref_counter
174 .entry(self.normalize_identifier(last_segment, context.dialect.name))
175 .or_default() += 1;
176 }
177
178 for alias in &payload.aliases {
179 if Self::is_alias_required(&alias.from_expression_element, context.dialect.name) {
180 continue;
181 }
182
183 if let Some(object_reference) = &alias.object_reference
184 && let Some(last_segment) = object_reference.segments().last()
185 && ref_counter
186 .get(&self.normalize_identifier(last_segment, context.dialect.name))
187 .copied()
188 .unwrap_or_default()
189 > 1
190 {
191 continue;
192 }
193
194 if context.dialect.name == DialectKind::Redshift
195 && alias.alias_expression.is_some()
196 && self.followed_by_qualify(context, alias)
197 {
198 continue;
199 }
200
201 if self.has_function_alias_reference(alias, &select_info, context.dialect.name) {
202 continue;
203 }
204
205 if alias.aliased
206 && !payload
207 .tbl_refs
208 .contains(&self.alias_name(alias, context.dialect.name))
209 {
210 if Self::has_used_column_aliases(alias, &select_info, context.dialect.name) {
211 continue;
212 }
213 let violation = self.report_unused_alias(alias);
214 violations.push(violation);
215 }
216 }
217
218 violations
219 }
220
221 fn is_fix_compatible(&self) -> bool {
222 true
223 }
224
225 fn crawl_behaviour(&self) -> Crawler {
226 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
227 }
228}
229
230impl RuleAL05 {
231 #[allow(clippy::only_used_in_recursion)]
232 fn analyze_table_aliases<'a>(
233 &self,
234 query: Query<'a>,
235 dialect: &Dialect,
236 payloads: &mut AL05State<'a>,
237 ) {
238 payloads.entry(query.id()).or_default();
239 let selectables = std::mem::take(&mut RefCell::borrow_mut(&query.inner).selectables);
240
241 for selectable in &selectables {
242 if let Some(select_info) = selectable.select_info() {
243 let table_aliases = select_info.table_aliases;
244 let reference_buffer = select_info.reference_buffer;
245 let table_reference_buffer = select_info.table_reference_buffer;
246
247 payloads
248 .entry(query.id())
249 .or_default()
250 .aliases
251 .extend(table_aliases);
252
253 for r in reference_buffer.into_iter().chain(table_reference_buffer) {
254 for tr in
255 r.extract_possible_references(ObjectReferenceLevel::Table, dialect.name)
256 {
257 self.resolve_and_mark_reference(query.clone(), &tr, dialect.name, payloads);
258 }
259 }
260 }
261 }
262
263 RefCell::borrow_mut(&query.inner).selectables = selectables;
264
265 for child in query.children() {
266 self.analyze_table_aliases(child, dialect, payloads);
267 }
268 }
269
270 fn resolve_and_mark_reference<'a>(
271 &self,
272 query: Query<'a>,
273 reference: &sqruff_lib_core::parser::segments::object_reference::ObjectReferencePart,
274 dialect: DialectKind,
275 payloads: &mut AL05State<'a>,
276 ) {
277 let Some(reference_segment) = reference.segments.first() else {
278 return;
279 };
280 let normalized_ref = self.normalize_identifier(reference_segment, dialect);
281
282 if let Some(payload) = payloads.get_mut(&query.id())
283 && payload
284 .aliases
285 .iter()
286 .any(|it| self.alias_name(it, dialect) == normalized_ref)
287 {
288 payload.tbl_refs.push(normalized_ref);
289 return;
290 }
291
292 if let Some(parent) = RefCell::borrow(&query.inner).parent.clone() {
293 self.resolve_and_mark_reference(parent, reference, dialect, payloads);
294 }
295 }
296
297 fn is_alias_required(
298 from_expression_element: &ErasedSegment,
299 dialect_name: DialectKind,
300 ) -> bool {
301 for segment in from_expression_element
302 .iter_segments(const { &SyntaxSet::new(&[SyntaxKind::Bracketed]) }, false)
303 {
304 if segment.is_type(SyntaxKind::TableExpression) {
305 return if segment
306 .child(const { &SyntaxSet::new(&[SyntaxKind::ValuesClause]) })
307 .is_some()
308 {
309 matches!(
310 dialect_name,
311 DialectKind::Athena
312 | DialectKind::Snowflake
313 | DialectKind::Tsql
314 | DialectKind::Postgres
315 )
316 } else {
317 segment
318 .iter_segments(const { &SyntaxSet::new(&[SyntaxKind::Bracketed]) }, false)
319 .iter()
320 .any(|seg| {
321 const {
322 SyntaxSet::new(&[
323 SyntaxKind::SelectStatement,
324 SyntaxKind::SetExpression,
325 SyntaxKind::WithCompoundStatement,
326 ])
327 }
328 .contains(seg.get_type())
329 })
330 };
331 }
332 }
333 false
334 }
335
336 fn has_used_column_aliases(
337 alias: &AliasInfo,
338 select_info: &SelectStatementColumnsAndTables,
339 dialect_name: DialectKind,
340 ) -> bool {
341 let Some(alias_expression) = &alias.alias_expression else {
342 return false;
343 };
344
345 let Some(bracketed) =
348 alias_expression.child(const { &SyntaxSet::single(SyntaxKind::Bracketed) })
349 else {
350 return false;
351 };
352
353 let col_alias_names: Vec<SmolStr> = bracketed
355 .recursive_crawl(
356 const {
357 &SyntaxSet::new(&[
358 SyntaxKind::NakedIdentifier,
359 SyntaxKind::Identifier,
360 SyntaxKind::QuotedIdentifier,
361 ])
362 },
363 true,
364 &SyntaxSet::EMPTY,
365 true,
366 )
367 .into_iter()
368 .map(|seg| seg.raw().to_uppercase().into())
369 .collect();
370
371 if col_alias_names.is_empty() {
372 return false;
373 }
374
375 for reference in &select_info.reference_buffer {
380 let table_refs =
381 reference.extract_possible_references(ObjectReferenceLevel::Table, dialect_name);
382 if let Some(tbl) = table_refs.first() {
383 if tbl.part.to_uppercase() != alias.ref_str.to_uppercase() {
386 continue;
387 }
388 }
389 for obj_ref in
391 reference.extract_possible_references(ObjectReferenceLevel::Object, dialect_name)
392 {
393 if col_alias_names.contains(&SmolStr::from(obj_ref.part.to_uppercase())) {
394 return true;
395 }
396 }
397 }
398
399 false
400 }
401
402 fn has_function_alias_reference(
403 &self,
404 alias: &AliasInfo,
405 select_info: &SelectStatementColumnsAndTables,
406 dialect_name: DialectKind,
407 ) -> bool {
408 let Some(table_expression) = alias
409 .from_expression_element
410 .child(const { &SyntaxSet::single(SyntaxKind::TableExpression) })
411 else {
412 return false;
413 };
414
415 if table_expression
416 .child(const { &SyntaxSet::single(SyntaxKind::Function) })
417 .is_none()
418 {
419 return false;
420 }
421
422 let alias_name = self.alias_name(alias, dialect_name);
423
424 select_info.reference_buffer.iter().any(|reference| {
425 let references = reference.iter_raw_references();
426 if references.len() != 1 {
427 return false;
428 }
429
430 references
431 .first()
432 .and_then(|reference_part| reference_part.segments.first())
433 .is_some_and(|segment| {
434 self.normalize_identifier(segment, dialect_name) == alias_name
435 })
436 })
437 }
438
439 fn report_unused_alias(&self, alias: &AliasInfo) -> LintResult {
440 let mut fixes = vec![LintFix::delete(alias.alias_expression.clone().unwrap())];
441
442 if let Some(alias_idx) = alias
445 .from_expression_element
446 .segments()
447 .iter()
448 .position(|s| s == alias.alias_expression.as_ref().unwrap())
449 {
450 for seg in alias.from_expression_element.segments()[..alias_idx]
451 .iter()
452 .rev()
453 .take_while(|s| s.is_whitespace() || s.is_meta())
454 {
455 fixes.push(LintFix::delete(seg.clone()));
456 }
457 }
458
459 LintResult::new(
460 alias.segment.clone(),
461 fixes,
462 format!(
463 "Alias '{}' is never used in SELECT statement.",
464 self.display_alias_name(alias)
465 )
466 .into(),
467 None,
468 )
469 }
470
471 fn alias_name(&self, alias: &AliasInfo, dialect: DialectKind) -> SmolStr {
472 alias
473 .segment
474 .as_ref()
475 .map(|segment| self.normalize_identifier(segment, dialect))
476 .unwrap_or_else(|| self.normalize_identifier_str(&alias.ref_str, None, dialect))
477 }
478
479 fn display_alias_name(&self, alias: &AliasInfo) -> String {
480 alias
481 .segment
482 .as_ref()
483 .map(|segment| {
484 self.normalize_identifier_str(
485 segment.raw(),
486 Some(segment.get_type()),
487 DialectKind::Ansi,
488 )
489 })
490 .unwrap_or_else(|| {
491 self.normalize_identifier_str(&alias.ref_str, None, DialectKind::Ansi)
492 })
493 .to_string()
494 }
495
496 fn normalize_identifier(&self, identifier: &ErasedSegment, dialect: DialectKind) -> SmolStr {
497 self.normalize_identifier_str(identifier.raw(), Some(identifier.get_type()), dialect)
498 }
499
500 fn normalize_identifier_str(
501 &self,
502 raw: &str,
503 syntax_kind: Option<SyntaxKind>,
504 dialect: DialectKind,
505 ) -> SmolStr {
506 let is_naked = syntax_kind.is_none_or(|kind| {
507 matches!(kind, SyntaxKind::Identifier | SyntaxKind::NakedIdentifier)
508 }) && !matches!(
509 raw.chars().next(),
510 Some('"') | Some('\'') | Some('`') | Some('[')
511 );
512 let mut normalized = if raw.starts_with('[') && raw.ends_with(']') && raw.len() >= 2 {
513 raw[1..raw.len() - 1].to_string()
514 } else if matches!(raw.chars().next(), Some('"') | Some('\'') | Some('`'))
515 && raw.len() >= 2
516 && raw.chars().next() == raw.chars().last()
517 {
518 let quote = raw.chars().next().unwrap();
519 raw[1..raw.len() - 1].replace(&format!("{quote}{quote}"), "e.to_string())
520 } else {
521 raw.to_string()
522 };
523
524 match self.alias_case_check {
525 AliasCaseCheck::Dialect => {
526 if is_naked {
527 normalized = match dialect {
528 DialectKind::Postgres | DialectKind::Redshift => normalized.to_lowercase(),
529 _ => normalized.to_uppercase(),
530 };
531 }
532 }
533 AliasCaseCheck::CaseInsensitive => normalized = normalized.to_uppercase(),
534 AliasCaseCheck::QuotedCaseSensitiveNakedUpper => {
535 if is_naked {
536 normalized = normalized.to_uppercase();
537 }
538 }
539 AliasCaseCheck::QuotedCaseSensitiveNakedLower => {
540 if is_naked {
541 normalized = normalized.to_lowercase();
542 }
543 }
544 AliasCaseCheck::CaseSensitive => {}
545 }
546
547 normalized.into()
548 }
549
550 fn followed_by_qualify(&self, context: &RuleContext, alias: &AliasInfo) -> bool {
551 let Some(alias_expression) = &alias.alias_expression else {
552 return false;
553 };
554 let mut current_from_seen = false;
555
556 for seg in context.segment.segments() {
557 if alias_expression.get_end_loc() == seg.get_end_loc() {
558 current_from_seen = true;
559 } else if current_from_seen && !seg.is_code() {
560 continue;
561 } else if current_from_seen && seg.is_type(SyntaxKind::QualifyClause) {
562 return true;
563 } else if current_from_seen {
564 return false;
565 }
566 }
567
568 false
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use crate::core::config::FluffConfig;
575 use crate::core::linter::core::Linter;
576
577 const POSTGRES_JSON_ALIAS_REPRODUCER: &str = r#"with stanza as (
578 select
579 data -> 'name' as name,
580 data -> 'backup' -> (
581 jsonb_array_length(data -> 'backup') - 1
582 ) as last_backup,
583 data -> 'archive' -> (
584 jsonb_array_length(data -> 'archive') - 1
585 ) as current_archive
586 from jsonb_array_elements(monitor.pgbackrest_info()) as data
587)
588
589select
590 name,
591 to_timestamp(
592 (last_backup -> 'timestamp' ->> 'stop')::numeric
593 ) as last_successful_backup,
594 current_archive ->> 'max' as last_archived_wal
595from stanza;
596"#;
597
598 fn postgres_al05_linter() -> Linter {
599 let config = FluffConfig::from_source(
600 r#"
601[sqruff]
602rules = AL05
603dialect = postgres
604"#,
605 None,
606 );
607
608 Linter::new(config, None, None, true).unwrap()
609 }
610
611 #[test]
612 fn test_al05_postgres_json_operator_alias_is_used() {
613 let mut linter = postgres_al05_linter();
614 let linted = linter
615 .lint_string_wrapped(POSTGRES_JSON_ALIAS_REPRODUCER, false)
616 .unwrap();
617
618 assert_eq!(linted.violations(), &[]);
619 }
620
621 #[test]
622 fn test_al05_postgres_json_operator_fix_preserves_alias() {
623 let mut linter = postgres_al05_linter();
624 let linted = linter
625 .lint_string_wrapped(POSTGRES_JSON_ALIAS_REPRODUCER, true)
626 .unwrap();
627
628 assert_eq!(linted.fix_string(), POSTGRES_JSON_ALIAS_REPRODUCER);
629 }
630}