Skip to main content

clicktype_query/
join.rs

1//! Type-safe JOIN operations
2
3use std::marker::PhantomData;
4use clicktype_core::traits::{ClickTable, TypedColumn};
5
6/// Join type marker
7pub trait JoinType {
8    fn keyword() -> &'static str;
9}
10
11/// INNER JOIN
12pub struct Inner;
13impl JoinType for Inner {
14    fn keyword() -> &'static str {
15        "INNER JOIN"
16    }
17}
18
19/// LEFT JOIN
20pub struct Left;
21impl JoinType for Left {
22    fn keyword() -> &'static str {
23        "LEFT JOIN"
24    }
25}
26
27/// RIGHT JOIN
28pub struct Right;
29impl JoinType for Right {
30    fn keyword() -> &'static str {
31        "RIGHT JOIN"
32    }
33}
34
35/// FULL OUTER JOIN
36pub struct FullOuter;
37impl JoinType for FullOuter {
38    fn keyword() -> &'static str {
39        "FULL OUTER JOIN"
40    }
41}
42
43/// CROSS JOIN
44pub struct Cross;
45impl JoinType for Cross {
46    fn keyword() -> &'static str {
47        "CROSS JOIN"
48    }
49}
50
51/// Join specification
52pub struct JoinSpec<T1: ClickTable, T2: ClickTable, J: JoinType> {
53    pub(crate) _table1: PhantomData<T1>,
54    pub(crate) _table2: PhantomData<T2>,
55    pub(crate) _join_type: PhantomData<J>,
56    pub(crate) table2_name: &'static str,
57    pub(crate) on_condition: Option<String>,
58}
59
60impl<T1: ClickTable, T2: ClickTable, J: JoinType> JoinSpec<T1, T2, J> {
61    pub fn new() -> Self {
62        Self {
63            _table1: PhantomData,
64            _table2: PhantomData,
65            _join_type: PhantomData,
66            table2_name: T2::table_name(),
67            on_condition: None,
68        }
69    }
70
71    pub fn on<C1, C2>(mut self, _col1: C1, _col2: C2) -> Self
72    where
73        C1: TypedColumn<Table = T1>,
74        C2: TypedColumn<Table = T2, Type = C1::Type>,
75    {
76        self.on_condition = Some(format!(
77            "{}.{} = {}.{}",
78            T1::table_name(),
79            C1::name(),
80            T2::table_name(),
81            C2::name()
82        ));
83        self
84    }
85
86    /// Generate the SQL JOIN clause
87    pub fn to_sql(&self) -> String {
88        let mut sql = format!("{} {}", J::keyword(), self.table2_name);
89        if let Some(ref condition) = self.on_condition {
90            sql.push_str(" ON ");
91            sql.push_str(condition);
92        }
93        sql
94    }
95}
96
97/// Helper functions for creating joins
98pub fn inner_join<T1: ClickTable, T2: ClickTable>() -> JoinSpec<T1, T2, Inner> {
99    JoinSpec::new()
100}
101
102pub fn left_join<T1: ClickTable, T2: ClickTable>() -> JoinSpec<T1, T2, Left> {
103    JoinSpec::new()
104}
105
106pub fn right_join<T1: ClickTable, T2: ClickTable>() -> JoinSpec<T1, T2, Right> {
107    JoinSpec::new()
108}
109
110pub fn full_outer_join<T1: ClickTable, T2: ClickTable>() -> JoinSpec<T1, T2, FullOuter> {
111    JoinSpec::new()
112}
113
114pub fn cross_join<T1: ClickTable, T2: ClickTable>() -> JoinSpec<T1, T2, Cross> {
115    JoinSpec::new()
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_join_types() {
124        assert_eq!(Inner::keyword(), "INNER JOIN");
125        assert_eq!(Left::keyword(), "LEFT JOIN");
126        assert_eq!(Right::keyword(), "RIGHT JOIN");
127        assert_eq!(FullOuter::keyword(), "FULL OUTER JOIN");
128        assert_eq!(Cross::keyword(), "CROSS JOIN");
129    }
130}