Skip to main content

drizzle_core/
join.rs

1//! Join types and helper macros for SQL JOIN operations
2//!
3//! This module provides shared JOIN functionality that can be used by
4//! dialect-specific implementations (`SQLite`, `PostgreSQL`, etc.)
5
6use crate::{SQL, ToSQL, traits::SQLParam};
7
8// =============================================================================
9// Join Type Enum
10// =============================================================================
11
12/// The type of JOIN operation
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
14pub enum JoinType {
15    #[default]
16    Join,
17    Inner,
18    Left,
19    Right,
20    Full,
21    Cross,
22}
23
24// =============================================================================
25// Join Builder Struct
26// =============================================================================
27
28/// Builder for constructing JOIN clauses
29///
30/// This struct uses a builder pattern with const fn methods to allow
31/// compile-time construction of JOIN specifications.
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
33pub struct Join {
34    pub natural: bool,
35    pub join_type: JoinType,
36    pub outer: bool, // only meaningful for LEFT/RIGHT/FULL
37}
38
39impl Join {
40    /// Creates a new Join with default settings (basic JOIN)
41    #[must_use]
42    pub const fn new() -> Self {
43        Self {
44            natural: false,
45            join_type: JoinType::Join,
46            outer: false,
47        }
48    }
49
50    /// Makes this a NATURAL join
51    #[must_use]
52    pub const fn natural(mut self) -> Self {
53        self.natural = true;
54        self
55    }
56
57    /// Makes this an INNER join
58    #[must_use]
59    pub const fn inner(mut self) -> Self {
60        self.join_type = JoinType::Inner;
61        self
62    }
63
64    /// Makes this a LEFT join
65    #[must_use]
66    pub const fn left(mut self) -> Self {
67        self.join_type = JoinType::Left;
68        self
69    }
70
71    /// Makes this a RIGHT join
72    #[must_use]
73    pub const fn right(mut self) -> Self {
74        self.join_type = JoinType::Right;
75        self
76    }
77
78    /// Makes this a FULL join
79    #[must_use]
80    pub const fn full(mut self) -> Self {
81        self.join_type = JoinType::Full;
82        self
83    }
84
85    /// Makes this a CROSS join
86    #[must_use]
87    pub const fn cross(mut self) -> Self {
88        self.join_type = JoinType::Cross;
89        self
90    }
91
92    /// Makes this an OUTER join (LEFT OUTER, RIGHT OUTER, FULL OUTER)
93    #[must_use]
94    pub const fn outer(mut self) -> Self {
95        self.outer = true;
96        self
97    }
98}
99
100impl<'a, V: SQLParam + 'a> ToSQL<'a, V> for Join {
101    fn to_sql(&self) -> SQL<'a, V> {
102        // Use pre-computed static strings to avoid Vec allocation
103        let join_str = match (self.natural, self.join_type, self.outer) {
104            // NATURAL variants
105            (true, JoinType::Join, _) => "NATURAL JOIN",
106            (true, JoinType::Inner, _) => "NATURAL INNER JOIN",
107            (true, JoinType::Left, false) => "NATURAL LEFT JOIN",
108            (true, JoinType::Left, true) => "NATURAL LEFT OUTER JOIN",
109            (true, JoinType::Right, false) => "NATURAL RIGHT JOIN",
110            (true, JoinType::Right, true) => "NATURAL RIGHT OUTER JOIN",
111            (true, JoinType::Full, false) => "NATURAL FULL JOIN",
112            (true, JoinType::Full, true) => "NATURAL FULL OUTER JOIN",
113            (true, JoinType::Cross, _) => "NATURAL CROSS JOIN",
114            // Non-NATURAL variants
115            (false, JoinType::Join, _) => "JOIN",
116            (false, JoinType::Inner, _) => "INNER JOIN",
117            (false, JoinType::Left, false) => "LEFT JOIN",
118            (false, JoinType::Left, true) => "LEFT OUTER JOIN",
119            (false, JoinType::Right, false) => "RIGHT JOIN",
120            (false, JoinType::Right, true) => "RIGHT OUTER JOIN",
121            (false, JoinType::Full, false) => "FULL JOIN",
122            (false, JoinType::Full, true) => "FULL OUTER JOIN",
123            (false, JoinType::Cross, _) => "CROSS JOIN",
124        };
125        SQL::raw(join_str)
126    }
127}
128
129// =============================================================================
130// Join Helper Macro
131// =============================================================================
132
133/// Macro to generate join helper functions for a specific dialect.
134///
135/// This macro generates all the standard join helper functions (`natural_join`,
136/// `left_join`, etc.) that create SQL JOIN clauses. Each dialect invokes this
137/// macro with their specific table trait and SQL type.
138///
139/// # Usage
140/// ```rust
141/// # let _ = r####"
142/// impl_join_helpers!(
143///     /// Trait bound for table types
144///     table_trait: SQLiteTable<'a>,
145///     /// Trait bound for condition types
146///     condition_trait: ToSQL<'a, SQLiteValue<'a>>,
147///     /// Return type for SQL
148///     sql_type: SQL<'a, SQLiteValue<'a>>,
149/// );
150/// # "####;
151/// ```
152#[macro_export]
153macro_rules! impl_join_helpers {
154    (
155        table_trait: $TableTrait:path,
156        condition_trait: $ConditionTrait:path,
157        sql_type: $SQLType:ty $(,)?
158    ) => {
159        fn join_internal<'a, Table>(
160            table: Table,
161            join: $crate::Join,
162            condition: impl $ConditionTrait,
163        ) -> $SQLType
164        where
165            Table: $TableTrait,
166        {
167            use $crate::ToSQL;
168            join.to_sql()
169                .append(&table)
170                .push($crate::Token::ON)
171                .append(&condition)
172        }
173
174        /// Helper function to create a NATURAL JOIN clause
175        pub fn natural_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
176        where
177            Table: $TableTrait,
178        {
179            join_internal(table, $crate::Join::new().natural(), condition)
180        }
181
182        /// Helper function to create a JOIN clause
183        pub fn join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
184        where
185            Table: $TableTrait,
186        {
187            join_internal(table, $crate::Join::new(), condition)
188        }
189
190        /// Helper function to create a NATURAL LEFT JOIN clause
191        pub fn natural_left_join<'a, Table>(
192            table: Table,
193            condition: impl $ConditionTrait,
194        ) -> $SQLType
195        where
196            Table: $TableTrait,
197        {
198            join_internal(table, $crate::Join::new().natural().left(), condition)
199        }
200
201        /// Helper function to create a LEFT JOIN clause
202        pub fn left_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
203        where
204            Table: $TableTrait,
205        {
206            join_internal(table, $crate::Join::new().left(), condition)
207        }
208
209        /// Helper function to create a LEFT OUTER JOIN clause
210        pub fn left_outer_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
211        where
212            Table: $TableTrait,
213        {
214            join_internal(table, $crate::Join::new().left().outer(), condition)
215        }
216
217        /// Helper function to create a NATURAL LEFT OUTER JOIN clause
218        pub fn natural_left_outer_join<'a, Table>(
219            table: Table,
220            condition: impl $ConditionTrait,
221        ) -> $SQLType
222        where
223            Table: $TableTrait,
224        {
225            join_internal(
226                table,
227                $crate::Join::new().natural().left().outer(),
228                condition,
229            )
230        }
231
232        /// Helper function to create a NATURAL RIGHT JOIN clause
233        pub fn natural_right_join<'a, Table>(
234            table: Table,
235            condition: impl $ConditionTrait,
236        ) -> $SQLType
237        where
238            Table: $TableTrait,
239        {
240            join_internal(table, $crate::Join::new().natural().right(), condition)
241        }
242
243        /// Helper function to create a RIGHT JOIN clause
244        pub fn right_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
245        where
246            Table: $TableTrait,
247        {
248            join_internal(table, $crate::Join::new().right(), condition)
249        }
250
251        /// Helper function to create a RIGHT OUTER JOIN clause
252        pub fn right_outer_join<'a, Table>(
253            table: Table,
254            condition: impl $ConditionTrait,
255        ) -> $SQLType
256        where
257            Table: $TableTrait,
258        {
259            join_internal(table, $crate::Join::new().right().outer(), condition)
260        }
261
262        /// Helper function to create a NATURAL RIGHT OUTER JOIN clause
263        pub fn natural_right_outer_join<'a, Table>(
264            table: Table,
265            condition: impl $ConditionTrait,
266        ) -> $SQLType
267        where
268            Table: $TableTrait,
269        {
270            join_internal(
271                table,
272                $crate::Join::new().natural().right().outer(),
273                condition,
274            )
275        }
276
277        /// Helper function to create a NATURAL FULL JOIN clause
278        pub fn natural_full_join<'a, Table>(
279            table: Table,
280            condition: impl $ConditionTrait,
281        ) -> $SQLType
282        where
283            Table: $TableTrait,
284        {
285            join_internal(table, $crate::Join::new().natural().full(), condition)
286        }
287
288        /// Helper function to create a FULL JOIN clause
289        pub fn full_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
290        where
291            Table: $TableTrait,
292        {
293            join_internal(table, $crate::Join::new().full(), condition)
294        }
295
296        /// Helper function to create a FULL OUTER JOIN clause
297        pub fn full_outer_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
298        where
299            Table: $TableTrait,
300        {
301            join_internal(table, $crate::Join::new().full().outer(), condition)
302        }
303
304        /// Helper function to create a NATURAL FULL OUTER JOIN clause
305        pub fn natural_full_outer_join<'a, Table>(
306            table: Table,
307            condition: impl $ConditionTrait,
308        ) -> $SQLType
309        where
310            Table: $TableTrait,
311        {
312            join_internal(
313                table,
314                $crate::Join::new().natural().full().outer(),
315                condition,
316            )
317        }
318
319        /// Helper function to create a NATURAL INNER JOIN clause
320        pub fn natural_inner_join<'a, Table>(
321            table: Table,
322            condition: impl $ConditionTrait,
323        ) -> $SQLType
324        where
325            Table: $TableTrait,
326        {
327            join_internal(table, $crate::Join::new().natural().inner(), condition)
328        }
329
330        /// Helper function to create an INNER JOIN clause
331        pub fn inner_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
332        where
333            Table: $TableTrait,
334        {
335            join_internal(table, $crate::Join::new().inner(), condition)
336        }
337
338        /// Helper function to create a CROSS JOIN clause
339        pub fn cross_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
340        where
341            Table: $TableTrait,
342        {
343            join_internal(table, $crate::Join::new().cross(), condition)
344        }
345    };
346}
347
348/// Macro to generate dialect-specific `JoinArg` trait and impls.
349///
350/// This consolidates the shared logic for:
351/// - explicit join tuples: `(table, condition)`
352/// - auto-FK joins for bare tables
353#[macro_export]
354macro_rules! impl_join_arg_trait {
355    (
356        table_trait: $TableTrait:path,
357        table_info_trait: $TableInfoTrait:path,
358        condition_trait: $ConditionTrait:path,
359        value_type: $ValueType:ty $(,)?
360    ) => {
361        /// Trait for arguments accepted by `.join()` and related join methods.
362        pub trait JoinArg<'a, FromTable> {
363            type JoinedTable;
364            fn into_join_sql(self, join: $crate::Join) -> $crate::SQL<'a, $ValueType>;
365        }
366
367        /// Bare table: derives the ON condition from `Joinable::fk_columns()`.
368        impl<'a, U, T> JoinArg<'a, T> for U
369        where
370            U: $TableTrait + $crate::Joinable<T>,
371            T: $TableInfoTrait + ::core::default::Default,
372        {
373            type JoinedTable = U;
374
375            fn into_join_sql(self, join: $crate::Join) -> $crate::SQL<'a, $ValueType> {
376                use $crate::ToSQL;
377
378                let from = T::default();
379                let cols = <U as $crate::Joinable<T>>::fk_columns();
380                let join_name = self.name();
381                let from_name = from.name();
382
383                let mut condition = $crate::SQL::with_capacity_chunks(cols.len() * 7);
384                for (idx, (self_col, target_col)) in cols.iter().enumerate() {
385                    if idx > 0 {
386                        condition.push_mut($crate::Token::AND);
387                    }
388                    condition.append_mut(
389                        $crate::SQL::ident(join_name)
390                            .push($crate::Token::DOT)
391                            .append($crate::SQL::ident(*self_col)),
392                    );
393                    condition.push_mut($crate::Token::EQ);
394                    condition.append_mut(
395                        $crate::SQL::ident(from_name)
396                            .push($crate::Token::DOT)
397                            .append($crate::SQL::ident(*target_col)),
398                    );
399                }
400
401                join.to_sql()
402                    .append(&self)
403                    .push($crate::Token::ON)
404                    .append(&condition)
405            }
406        }
407
408        /// Tuple `(table, condition)`: explicit ON condition.
409        impl<'a, U, C, T> JoinArg<'a, T> for (U, C)
410        where
411            U: $TableTrait,
412            C: $ConditionTrait,
413        {
414            type JoinedTable = U;
415
416            fn into_join_sql(self, join: $crate::Join) -> $crate::SQL<'a, $ValueType> {
417                let (table, condition) = self;
418                join_internal(table, join, condition)
419            }
420        }
421    };
422}