use hashbrown::HashMap;
use smol_str::StrExt;
use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
use sqruff_lib_core::parser::segments::ErasedSegment;
use sqruff_lib_core::parser::segments::from::FromExpressionElementSegment;
use sqruff_lib_core::parser::segments::object_reference::{
ObjectReferenceKind, ObjectReferenceSegment,
};
use sqruff_lib_core::utils::analysis::query::Query;
use crate::core::config::Value;
use crate::core::rules::context::RuleContext;
use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
#[derive(Debug, Default, Clone)]
pub struct RuleST11;
impl Rule for RuleST11 {
fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
Ok(RuleST11.erased())
}
fn name(&self) -> &'static str {
"structure.unused_join"
}
fn description(&self) -> &'static str {
"Joined table not referenced in query."
}
fn long_description(&self) -> &'static str {
r#"
**Anti-pattern**
In this example, the table ``bar`` is included in the ``JOIN`` clause
but no columns from it are referenced elsewhere in the query.
```sql
SELECT
foo.a,
foo.b
FROM foo
LEFT JOIN bar ON foo.a = bar.a
```
**Best practice**
Remove the join, or use the table.
```sql
SELECT
foo.a,
foo.b,
bar.c
FROM foo
LEFT JOIN bar ON foo.a = bar.a
```
"#
}
fn groups(&self) -> &'static [RuleGroups] {
&[RuleGroups::All, RuleGroups::Structure]
}
fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
let segment = &context.segment;
let joined_tables = match self.extract_references_from_select(segment) {
Some(tables) => tables,
None => return Vec::new(),
};
if joined_tables.is_empty() {
return Vec::new();
}
let mut table_references = hashbrown::HashSet::new();
let reference_clause_types: &[SyntaxKind] = &[
SyntaxKind::SelectClause,
SyntaxKind::WhereClause,
SyntaxKind::GroupbyClause,
SyntaxKind::OrderbyClause,
SyntaxKind::HavingClause,
SyntaxKind::QualifyClause,
];
for clause_type in reference_clause_types {
let clause = segment.child(&SyntaxSet::new(&[*clause_type]));
if let Some(clause) = clause {
for col_ref in clause.recursive_crawl(
const { &SyntaxSet::new(&[SyntaxKind::ColumnReference]) },
true,
&SyntaxSet::EMPTY,
true,
) {
let obj_ref = ObjectReferenceSegment(col_ref, ObjectReferenceKind::Object);
let parts = obj_ref.iter_raw_references();
if parts.len() < 2 {
return Vec::new();
}
let table_part = &parts[parts.len() - 2].part;
table_references.insert(
table_part
.to_uppercase()
.trim_matches(|c| {
c == '"' || c == '\'' || c == '`' || c == '[' || c == ']'
})
.to_string(),
);
}
}
}
let query = Query::from_segment(segment, context.dialect, None);
let inner = query.inner.borrow();
for selectable in &inner.selectables {
for wcinfo in selectable.wildcard_info() {
for table in &wcinfo.tables {
table_references.insert(
table
.to_uppercase()
.trim_matches(|c: char| {
c == '"' || c == '\'' || c == '`' || c == '[' || c == ']'
})
.to_string(),
);
}
}
}
let mut results = Vec::new();
for (tbl_ref, segment) in &joined_tables {
if !table_references.contains(tbl_ref.as_str()) {
results.push(LintResult::new(
Some(segment.clone()),
Vec::new(),
Some(format!(
"Joined table '{}' not referenced elsewhere in query.",
segment.raw()
)),
None,
));
}
}
results
}
fn crawl_behaviour(&self) -> Crawler {
SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
}
}
impl RuleST11 {
fn extract_reference_from_expression(&self, segment: &ErasedSegment) -> String {
let alias = FromExpressionElementSegment(segment.clone()).eventual_alias();
let ref_str = alias.ref_str.as_str();
if ref_str.is_empty() {
return String::new();
}
ref_str
.to_uppercase()
.trim_matches(|c| c == '"' || c == '\'' || c == '`' || c == '[' || c == ']')
.to_string()
}
fn extract_referenced_tables(
&self,
segment: &ErasedSegment,
allow_unqualified: bool,
) -> Option<Vec<String>> {
let mut tables = Vec::new();
for col_ref in segment.recursive_crawl(
const { &SyntaxSet::new(&[SyntaxKind::ColumnReference]) },
true,
&SyntaxSet::EMPTY,
true,
) {
let obj_ref = ObjectReferenceSegment(col_ref, ObjectReferenceKind::Object);
let parts = obj_ref.iter_raw_references();
if parts.len() < 2 {
if allow_unqualified {
continue;
} else {
return None;
}
}
let table_part = &parts[parts.len() - 2].part;
tables.push(
table_part
.to_uppercase()
.trim_matches(|c| c == '"' || c == '\'' || c == '`' || c == '[' || c == ']')
.to_string(),
);
}
Some(tables)
}
fn extract_references_from_select(
&self,
segment: &ErasedSegment,
) -> Option<Vec<(String, ErasedSegment)>> {
let from_clause = segment.child(const { &SyntaxSet::new(&[SyntaxKind::FromClause]) })?;
let mut joined_tables: Vec<(String, ErasedSegment)> = Vec::new();
let mut referenced_tables: Vec<String> = Vec::new();
let mut total_table_count = 0;
for from_expression in
from_clause.children(const { &SyntaxSet::new(&[SyntaxKind::FromExpression]) })
{
let from_elements: Vec<_> = from_expression
.children(const { &SyntaxSet::new(&[SyntaxKind::FromExpressionElement]) })
.cloned()
.collect();
if from_elements.len() > 1 {
total_table_count += from_elements.len();
} else {
for elem in &from_elements {
let ref_str = self.extract_reference_from_expression(elem);
if !ref_str.is_empty() {
total_table_count += 1;
}
}
}
for join_clause in
from_expression.children(const { &SyntaxSet::new(&[SyntaxKind::JoinClause]) })
{
let join_keywords: hashbrown::HashSet<String> = join_clause
.children(const { &SyntaxSet::new(&[SyntaxKind::Keyword]) })
.map(|kw| kw.raw().to_uppercase_smolstr().to_string())
.collect();
let is_outer = join_keywords.contains("LEFT")
|| join_keywords.contains("RIGHT")
|| join_keywords.contains("FULL");
let mut this_clause_refs = Vec::new();
for from_elem in join_clause
.children(const { &SyntaxSet::new(&[SyntaxKind::FromExpressionElement]) })
{
let ref_str = self.extract_reference_from_expression(from_elem);
total_table_count += 1;
if !ref_str.is_empty() && is_outer {
joined_tables.push((ref_str.clone(), from_elem.clone()));
this_clause_refs.push(ref_str);
}
if let Some(refs) = self.extract_referenced_tables(from_elem, true) {
for tbl_ref in refs {
if !this_clause_refs.contains(&tbl_ref) {
referenced_tables.push(tbl_ref);
}
}
}
}
for join_on_condition in
join_clause.children(const { &SyntaxSet::new(&[SyntaxKind::JoinOnCondition]) })
{
if let Some(refs) = self.extract_referenced_tables(join_on_condition, true) {
for tbl_ref in refs {
if !this_clause_refs.contains(&tbl_ref) {
referenced_tables.push(tbl_ref);
}
}
}
}
}
}
if total_table_count <= 1 {
return None;
}
let result: Vec<_> = joined_tables
.into_iter()
.filter(|(ref_str, _)| !referenced_tables.contains(ref_str))
.collect();
Some(result)
}
}