1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use ahash::{AHashMap, AHashSet};

use crate::core::config::Value;
use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
use crate::core::rules::context::RuleContext;
use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
use crate::dialects::{SyntaxKind, SyntaxSet};

#[derive(Default, Clone, Debug)]
pub struct RuleCV08;

impl Rule for RuleCV08 {
    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
        Ok(RuleCV08.erased())
    }

    fn name(&self) -> &'static str {
        "convention.left_join"
    }

    fn description(&self) -> &'static str {
        "Use LEFT JOIN instead of RIGHT JOIN."
    }

    fn long_description(&self) -> &'static str {
        r#"
**Anti-pattern**

`RIGHT JOIN` is used.

```sql
SELECT
    foo.col1,
    bar.col2
FROM foo
RIGHT JOIN bar
    ON foo.bar_id = bar.id;
```

**Best practice**

Refactor and use ``LEFT JOIN`` instead.

```sql
SELECT
    foo.col1,
    bar.col2
FROM bar
LEFT JOIN foo
   ON foo.bar_id = bar.id;
```
"#
    }

    fn groups(&self) -> &'static [RuleGroups] {
        &[RuleGroups::All, RuleGroups::Convention]
    }

    fn eval(&self, context: RuleContext) -> Vec<LintResult> {
        assert!(context.segment.is_type(SyntaxKind::JoinClause));

        let segments = context
            .segment
            .segments()
            .iter()
            .map(|segment| segment.get_raw_upper().unwrap())
            .collect::<AHashSet<_>>();

        let mut set = AHashSet::new();
        set.insert("RIGHT".to_string());
        set.insert("JOIN".to_string());

        if set.is_subset(&segments) {
            vec![LintResult::new(
                Some(context.segment.segments()[0].clone()),
                vec![],
                None,
                None,
                None,
            )]
        } else {
            vec![]
        }
    }

    fn crawl_behaviour(&self) -> Crawler {
        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::JoinClause]) }).into()
    }
}

#[cfg(test)]
mod test {
    use pretty_assertions::assert_eq;

    use super::RuleCV08;
    use crate::api::simple::lint;
    use crate::core::rules::base::{Erased, ErasedRule};

    fn rules() -> Vec<ErasedRule> {
        vec![RuleCV08.erased()]
    }

    #[test]
    fn test_pass() {
        let pass_str = r#"
SELECT
    foo.col1,
    bar.col2
FROM foo
LEFT JOIN bar
    ON foo.bar_id = bar.id;
"#;

        let violations = lint(pass_str.to_owned(), "ansi".into(), rules(), None, None).unwrap();

        assert!(violations.is_empty());
    }

    #[test]
    fn test_fail() {
        let sql = r#"
SELECT
    foo.col1,
    bar.col2
FROM foo
RIGHT JOIN bar
    ON foo.bar_id = bar.id;
"#;

        let violations = lint(sql.to_owned(), "ansi".into(), rules(), None, None).unwrap();

        assert_eq!(violations.len(), 1);
    }
}