zino_orm/
join.rs

1use super::{query::QueryExt, Entity, Schema};
2use std::marker::PhantomData;
3use zino_core::model::Query;
4
5/// Variants for `JOIN` types.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub(super) enum JoinType {
8    /// The `INNER JOIN` type.
9    #[default]
10    Inner,
11    /// The `LEFT (OUTER) JOIN` type.
12    Left,
13    /// The `RIGHT (OUTER) JOIN` type.
14    Right,
15    /// The `FULL (OUTER) JOIN` type.
16    Full,
17    /// The `CROSS JOIN` type.
18    Cross,
19}
20
21impl JoinType {
22    /// Returns the join type as str.
23    #[inline]
24    pub(super) fn as_str(&self) -> &'static str {
25        match self {
26            JoinType::Inner => "INNER JOIN",
27            JoinType::Left => "LEFT JOIN",
28            JoinType::Right => "RIGHT JOIN",
29            JoinType::Full => "FULL JOIN",
30            JoinType::Cross => "CROSS JOIN",
31        }
32    }
33}
34
35/// SQL joins on two tables.
36///
37/// # Examples
38/// ```rust,ignore
39/// use crate::model::{Project, ProjectColumn, Task, TaskColumn};
40/// use zino_core::Map;
41/// use zino_orm::{JoinOn, QueryBuilder, Schema};
42///
43/// let query = QueryBuilder::<Task>::new()
44///     .fields([TaskColumn::Id, TaskColumn::Name, TaskColumn::ProjectId])
45///     .and_eq(TaskColumn::Status, "Completed")
46///     .and(
47///         QueryBuilder::<Project>::new()
48///             .alias(ProjectColumn::Name, "project_name")
49///             .fields([ProjectColumn::StartDate, ProjectColumn::EndDate])
50///             .and_overlaps(
51///                 (ProjectColumn::StartDate, ProjectColumn::EndDate),
52///                 ("2023-10-01", "2023-10-07"),
53///             ),
54///     )
55///     .order_desc(TaskColumn::UpdatedAt)
56///     .build();
57/// let join_on = JoinOn::<Task, Project>::new()
58///     .eq(TaskColumn::ProjectId, ProjectColumn::Id);
59/// let entries = Task::lookup::<Project, Map>(&query, &join_on).await?;
60/// ```
61#[derive(Debug, Clone, Default)]
62pub struct JoinOn<L: Schema, R: Schema> {
63    /// The join type.
64    join_type: JoinType,
65    /// The join conditions.
66    conditions: Vec<String>,
67    /// The phantom data.
68    phantom: PhantomData<(L, R)>,
69}
70
71impl<L: Schema, R: Schema> JoinOn<L, R> {
72    /// Creates a new instance.
73    #[inline]
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Constructs an instance for the `INNER JOIN`.
79    #[inline]
80    pub fn inner_join() -> Self {
81        Self {
82            join_type: JoinType::Inner,
83            conditions: Vec::new(),
84            phantom: PhantomData,
85        }
86    }
87
88    /// Constructs an instance for the `LEFT (OUTER) JOIN`.
89    #[inline]
90    pub fn left_join() -> Self {
91        Self {
92            join_type: JoinType::Left,
93            conditions: Vec::new(),
94            phantom: PhantomData,
95        }
96    }
97
98    /// Constructs an instance for the `RIGHT (OUTER) JOIN`.
99    #[inline]
100    pub fn right_join() -> Self {
101        Self {
102            join_type: JoinType::Right,
103            conditions: Vec::new(),
104            phantom: PhantomData,
105        }
106    }
107
108    /// Constructs an instance for the `FULL (OUTER) JOIN`.
109    #[inline]
110    pub fn full_join() -> Self {
111        Self {
112            join_type: JoinType::Full,
113            conditions: Vec::new(),
114            phantom: PhantomData,
115        }
116    }
117
118    /// Constructs an instance for the `CROSS JOIN`.
119    #[inline]
120    pub fn cross_join() -> Self {
121        Self {
122            join_type: JoinType::Cross,
123            conditions: Vec::new(),
124            phantom: PhantomData,
125        }
126    }
127
128    /// Specifies an equality relation for the two columns.
129    pub fn with(mut self, left_col: &str, right_col: &str) -> Self {
130        let left_col = [L::model_name(), ".", left_col].concat();
131        let right_col = [R::model_name(), ".", right_col].concat();
132        let left_col_field = Query::format_field(&left_col);
133        let right_col_field = Query::format_field(&right_col);
134        let condition = format!("{left_col_field} = {right_col_field}");
135        self.conditions.push(condition);
136        self
137    }
138
139    /// Returns the join type.
140    #[inline]
141    pub(super) fn join_type(&self) -> JoinType {
142        self.join_type
143    }
144
145    /// Formats the conditions.
146    #[inline]
147    pub(super) fn format_conditions(&self) -> String {
148        self.conditions.join(" AND ")
149    }
150}
151
152impl<L: Entity + Schema, R: Entity + Schema> JoinOn<L, R> {
153    /// Specifies a relation for which the left column is equal to the right column.
154    #[inline]
155    pub fn eq(self, left_col: L::Column, right_col: R::Column) -> Self {
156        self.push_op(left_col, "=", right_col)
157    }
158
159    /// Specifies a relation for which the left column is not equal to the right column.
160    #[inline]
161    pub fn ne(self, left_col: L::Column, right_col: R::Column) -> Self {
162        self.push_op(left_col, "<>", right_col)
163    }
164
165    /// Specifies a relation for which the left column is less than the right column.
166    #[inline]
167    pub fn lt(self, left_col: L::Column, right_col: R::Column) -> Self {
168        self.push_op(left_col, "<", right_col)
169    }
170
171    /// Specifies a relation for which the left column is not greater than the right column.
172    #[inline]
173    pub fn le(self, left_col: L::Column, right_col: R::Column) -> Self {
174        self.push_op(left_col, "<=", right_col)
175    }
176
177    /// Specifies a relation for which the left column is greater than the right column.
178    #[inline]
179    pub fn gt(self, left_col: L::Column, right_col: R::Column) -> Self {
180        self.push_op(left_col, ">", right_col)
181    }
182
183    /// Specifies a relation for which the left column is not less than the right column.
184    #[inline]
185    pub fn ge(self, left_col: L::Column, right_col: R::Column) -> Self {
186        self.push_op(left_col, ">=", right_col)
187    }
188
189    /// Pushes a condition for the two columns.
190    fn push_op(mut self, left_col: L::Column, operator: &str, right_col: R::Column) -> Self {
191        let left_col = L::format_column(&left_col);
192        let right_col = R::format_column(&right_col);
193        let left_col_field = Query::format_field(&left_col);
194        let right_col_field = Query::format_field(&right_col);
195        let condition = format!("{left_col_field} {operator} {right_col_field}");
196        self.conditions.push(condition);
197        self
198    }
199}