Skip to main content

sqlmodel_query/
join.rs

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