use hashbrown::HashMap;
use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
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, Clone)]
pub struct RuleCV12;
impl Rule for RuleCV12 {
fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
Ok(RuleCV12.erased())
}
fn name(&self) -> &'static str {
"convention.join_condition"
}
fn description(&self) -> &'static str {
"Join conditions should use the JOIN ... ON syntax."
}
fn long_description(&self) -> &'static str {
r#"
**Anti-pattern**
Placing join conditions in the `WHERE` clause instead of using `JOIN ... ON` mixes join logic with filtering logic, making queries harder to read.
```sql
SELECT
foo
FROM bar
JOIN baz
WHERE bar.id = baz.id;
```
**Best practice**
Use `JOIN ... ON` to specify join conditions.
```sql
SELECT
foo
FROM bar
JOIN baz ON bar.id = baz.id;
```
"#
}
fn groups(&self) -> &'static [RuleGroups] {
&[RuleGroups::All, RuleGroups::Convention]
}
fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
assert!(context.segment.is_type(SyntaxKind::SelectStatement));
let children = context.segment.segments();
let from_clause = children.iter().find(|s| s.is_type(SyntaxKind::FromClause));
let Some(from_clause) = from_clause else {
return Vec::new();
};
let has_where = children.iter().any(|s| s.is_type(SyntaxKind::WhereClause));
if !has_where {
return Vec::new();
}
let join_clauses = from_clause.recursive_crawl(
const { &SyntaxSet::single(SyntaxKind::JoinClause) },
true,
&SyntaxSet::EMPTY,
true,
);
let mut results = Vec::new();
for join in join_clauses {
let join_children = join.segments();
let has_condition = join_children.iter().any(|s| {
s.is_type(SyntaxKind::JoinOnCondition)
|| s.is_type(SyntaxKind::UsingClause)
|| s.is_keyword("USING")
});
if has_condition {
continue;
}
let is_cross_or_natural = join_children.iter().any(|s| {
s.is_type(SyntaxKind::Keyword)
&& (s.raw().eq_ignore_ascii_case("CROSS")
|| s.raw().eq_ignore_ascii_case("NATURAL"))
});
if is_cross_or_natural {
continue;
}
results.push(LintResult::new(
Some(join.clone()),
Vec::new(),
None,
Some(
"Join conditions should use the JOIN ... ON syntax rather than the WHERE clause."
.into(),
),
));
}
results
}
fn is_fix_compatible(&self) -> bool {
false
}
fn crawl_behaviour(&self) -> Crawler {
SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
}
}