Skip to main content

sqlmodel_query/
join.rs

1//! JOIN clause types.
2
3use crate::expr::Expr;
4use sqlmodel_core::Value;
5
6/// A JOIN clause.
7#[derive(Debug, Clone)]
8pub struct Join {
9    /// Type of join
10    pub join_type: JoinType,
11    /// Table to join (table name or subquery SQL)
12    pub table: String,
13    /// Optional table alias
14    pub alias: Option<String>,
15    /// ON condition
16    pub on: Expr,
17    /// Whether this is a LATERAL join (subquery can reference outer query columns).
18    ///
19    /// Supported by PostgreSQL and MySQL 8.0+. Not supported by SQLite.
20    pub lateral: bool,
21    /// Whether the table field contains a subquery (wrapped in parentheses).
22    pub is_subquery: bool,
23    /// Parameters from a subquery table expression.
24    pub subquery_params: Vec<Value>,
25}
26
27/// Types of SQL joins.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum JoinType {
30    Inner,
31    Left,
32    Right,
33    Full,
34    Cross,
35}
36
37impl JoinType {
38    /// Get the SQL keyword for this join type.
39    pub const fn as_str(&self) -> &'static str {
40        match self {
41            JoinType::Inner => "INNER JOIN",
42            JoinType::Left => "LEFT JOIN",
43            JoinType::Right => "RIGHT JOIN",
44            JoinType::Full => "FULL JOIN",
45            JoinType::Cross => "CROSS JOIN",
46        }
47    }
48}
49
50impl Join {
51    /// Create an INNER JOIN.
52    pub fn inner(table: impl Into<String>, on: Expr) -> Self {
53        Self {
54            join_type: JoinType::Inner,
55            table: table.into(),
56            alias: None,
57            on,
58            lateral: false,
59            is_subquery: false,
60            subquery_params: Vec::new(),
61        }
62    }
63
64    /// Create a LEFT JOIN.
65    pub fn left(table: impl Into<String>, on: Expr) -> Self {
66        Self {
67            join_type: JoinType::Left,
68            table: table.into(),
69            alias: None,
70            on,
71            lateral: false,
72            is_subquery: false,
73            subquery_params: Vec::new(),
74        }
75    }
76
77    /// Create a RIGHT JOIN.
78    pub fn right(table: impl Into<String>, on: Expr) -> Self {
79        Self {
80            join_type: JoinType::Right,
81            table: table.into(),
82            alias: None,
83            on,
84            lateral: false,
85            is_subquery: false,
86            subquery_params: Vec::new(),
87        }
88    }
89
90    /// Create a FULL OUTER JOIN.
91    pub fn full(table: impl Into<String>, on: Expr) -> Self {
92        Self {
93            join_type: JoinType::Full,
94            table: table.into(),
95            alias: None,
96            on,
97            lateral: false,
98            is_subquery: false,
99            subquery_params: Vec::new(),
100        }
101    }
102
103    /// Create a CROSS JOIN (no ON condition needed, but we require one for uniformity).
104    pub fn cross(table: impl Into<String>) -> Self {
105        Self {
106            join_type: JoinType::Cross,
107            table: table.into(),
108            alias: None,
109            on: Expr::raw("TRUE"), // Dummy condition for cross join
110            lateral: false,
111            is_subquery: false,
112            subquery_params: Vec::new(),
113        }
114    }
115
116    /// Create a LATERAL JOIN with a subquery.
117    ///
118    /// A LATERAL subquery can reference columns from preceding FROM items.
119    /// Supported by PostgreSQL (9.3+) and MySQL (8.0.14+). Not supported by SQLite.
120    ///
121    /// # Arguments
122    ///
123    /// * `join_type` - The join type (typically `JoinType::Inner` or `JoinType::Left`)
124    /// * `subquery_sql` - The subquery SQL (without parentheses)
125    /// * `alias` - Required alias for the lateral subquery
126    /// * `on` - ON condition (use `Expr::raw("TRUE")` for implicit join)
127    /// * `params` - Parameters for the subquery
128    pub fn lateral(
129        join_type: JoinType,
130        subquery_sql: impl Into<String>,
131        alias: impl Into<String>,
132        on: Expr,
133        params: Vec<Value>,
134    ) -> Self {
135        Self {
136            join_type,
137            table: subquery_sql.into(),
138            alias: Some(alias.into()),
139            on,
140            lateral: true,
141            is_subquery: true,
142            subquery_params: params,
143        }
144    }
145
146    /// Create a LEFT JOIN LATERAL (most common form).
147    ///
148    /// Shorthand for `Join::lateral(JoinType::Left, ...)`.
149    pub fn left_lateral(
150        subquery_sql: impl Into<String>,
151        alias: impl Into<String>,
152        on: Expr,
153        params: Vec<Value>,
154    ) -> Self {
155        Self::lateral(JoinType::Left, subquery_sql, alias, on, params)
156    }
157
158    /// Create an INNER JOIN LATERAL.
159    pub fn inner_lateral(
160        subquery_sql: impl Into<String>,
161        alias: impl Into<String>,
162        on: Expr,
163        params: Vec<Value>,
164    ) -> Self {
165        Self::lateral(JoinType::Inner, subquery_sql, alias, on, params)
166    }
167
168    /// Create a CROSS JOIN LATERAL (no ON condition).
169    pub fn cross_lateral(
170        subquery_sql: impl Into<String>,
171        alias: impl Into<String>,
172        params: Vec<Value>,
173    ) -> Self {
174        Self {
175            join_type: JoinType::Cross,
176            table: subquery_sql.into(),
177            alias: Some(alias.into()),
178            on: Expr::raw("TRUE"),
179            lateral: true,
180            is_subquery: true,
181            subquery_params: params,
182        }
183    }
184
185    /// Set an alias for the joined table.
186    pub fn alias(mut self, alias: impl Into<String>) -> Self {
187        self.alias = Some(alias.into());
188        self
189    }
190
191    /// Mark this join as LATERAL.
192    pub fn set_lateral(mut self) -> Self {
193        self.lateral = true;
194        self
195    }
196
197    /// Generate SQL for this JOIN clause and collect parameters.
198    ///
199    /// Returns a tuple of (sql, params) since the ON condition may contain
200    /// literal values that need to be bound as parameters.
201    pub fn to_sql(&self) -> (String, Vec<Value>) {
202        let mut params = Vec::new();
203        let sql = self.build_sql(&mut params, 0);
204        (sql, params)
205    }
206
207    /// Generate SQL and collect parameters.
208    pub fn build(&self, params: &mut Vec<Value>, offset: usize) -> String {
209        self.build_sql(params, offset)
210    }
211
212    fn build_sql(&self, params: &mut Vec<Value>, offset: usize) -> String {
213        let lateral_keyword = if self.lateral { " LATERAL" } else { "" };
214
215        let table_ref = if self.is_subquery {
216            format!("({})", self.table)
217        } else {
218            self.table.clone()
219        };
220
221        let mut sql = format!(
222            " {}{}{}",
223            self.join_type.as_str(),
224            lateral_keyword,
225            if table_ref.is_empty() {
226                String::new()
227            } else {
228                format!(" {}", table_ref)
229            }
230        );
231
232        // Add subquery params before ON condition params
233        params.extend(self.subquery_params.clone());
234
235        if let Some(alias) = &self.alias {
236            sql.push_str(" AS ");
237            sql.push_str(alias);
238        }
239
240        if self.join_type != JoinType::Cross {
241            let on_sql = self.on.build(params, offset);
242            sql.push_str(" ON ");
243            sql.push_str(&on_sql);
244        }
245
246        sql
247    }
248}