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/// An explicit derived source and boolean condition for `JOIN LATERAL`.
130#[doc(hidden)]
131pub trait LateralArg<'a, V: SQLParam>: lateral_private::Arg {
132    type JoinedTable;
133
134    fn into_lateral_sql(self, join: Join) -> SQL<'a, V>;
135}
136
137impl<'a, V, Name, Projection, Query, Condition> LateralArg<'a, V>
138    for (crate::Derived<'a, V, Name, Projection, Query>, Condition)
139where
140    V: SQLParam + 'a,
141    Name: crate::Tag,
142    Projection: crate::DerivedProjection<Name>,
143    Query: ToSQL<'a, V>,
144    Condition: crate::expr::Expr<'a, V>,
145    Condition::SQLType: crate::types::BooleanLike,
146{
147    type JoinedTable = crate::Derived<'a, V, Name, Projection, Query>;
148
149    fn into_lateral_sql(self, join: Join) -> SQL<'a, V> {
150        let (source, condition) = self;
151        join.to_sql()
152            .append(SQL::raw(" LATERAL "))
153            .append(source.into_sql())
154            .push(crate::Token::ON)
155            .append(condition.into_sql())
156    }
157}
158
159/// A derived source accepted by `CROSS JOIN LATERAL`.
160#[doc(hidden)]
161pub trait LateralSource<'a, V: SQLParam>: lateral_private::Source {
162    type JoinedTable;
163
164    fn into_cross_lateral_sql(self) -> SQL<'a, V>;
165}
166
167impl<'a, V, Name, Projection, Query> LateralSource<'a, V>
168    for crate::Derived<'a, V, Name, Projection, Query>
169where
170    V: SQLParam + 'a,
171    Name: crate::Tag,
172    Projection: crate::DerivedProjection<Name>,
173    Query: ToSQL<'a, V>,
174{
175    type JoinedTable = Self;
176
177    fn into_cross_lateral_sql(self) -> SQL<'a, V> {
178        Join::new()
179            .cross()
180            .to_sql()
181            .append(SQL::raw(" LATERAL "))
182            .append(self.into_sql())
183    }
184}
185
186mod lateral_private {
187    pub trait Arg {}
188    pub trait Source {}
189
190    impl<V, Name, Projection, Query, Condition> Arg
191        for (crate::Derived<'_, V, Name, Projection, Query>, Condition)
192    where
193        V: crate::SQLParam,
194    {
195    }
196
197    impl<V, Name, Projection, Query> Source for crate::Derived<'_, V, Name, Projection, Query> where
198        V: crate::SQLParam
199    {
200    }
201}
202
203// =============================================================================
204// Join Helper Macro
205// =============================================================================
206
207/// Macro to generate join helper functions for a specific dialect.
208///
209/// This macro generates all the standard join helper functions (`natural_join`,
210/// `left_join`, etc.) that create SQL JOIN clauses. Each dialect invokes this
211/// macro with their specific table trait and SQL type.
212///
213/// # Usage
214/// ```rust
215/// # let _ = r####"
216/// impl_join_helpers!(
217///     /// Trait bound for table types
218///     table_trait: SQLiteTable<'a>,
219///     /// Trait bound for condition types
220///     condition_trait: ToSQL<'a, SQLiteValue<'a>>,
221///     /// Return type for SQL
222///     sql_type: SQL<'a, SQLiteValue<'a>>,
223/// );
224/// # "####;
225/// ```
226#[macro_export]
227macro_rules! impl_join_helpers {
228    (
229        table_trait: $TableTrait:path,
230        condition_trait: $ConditionTrait:path,
231        sql_type: $SQLType:ty $(,)?
232    ) => {
233        fn join_internal<'a, Table>(
234            table: Table,
235            join: $crate::Join,
236            condition: impl $ConditionTrait,
237        ) -> $SQLType
238        where
239            Table: $TableTrait,
240        {
241            use $crate::ToSQL;
242            join.to_sql()
243                .append(&table)
244                .push($crate::Token::ON)
245                .append(&condition)
246        }
247
248        /// Helper function to create a NATURAL JOIN clause.
249        ///
250        /// A natural join matches the columns both sides share by name,
251        /// so it takes no ON condition.
252        pub fn natural_join<'a, Table>(table: Table) -> $SQLType
253        where
254            Table: $TableTrait,
255        {
256            use $crate::ToSQL;
257            $crate::Join::new().natural().to_sql().append(&table)
258        }
259
260        /// Helper function to create a JOIN clause
261        pub fn join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
262        where
263            Table: $TableTrait,
264        {
265            join_internal(table, $crate::Join::new(), condition)
266        }
267
268        /// Helper function to create a NATURAL LEFT JOIN clause.
269        ///
270        /// A natural join matches the columns both sides share by name,
271        /// so it takes no ON condition.
272        pub fn natural_left_join<'a, Table>(table: Table) -> $SQLType
273        where
274            Table: $TableTrait,
275        {
276            use $crate::ToSQL;
277            $crate::Join::new().natural().left().to_sql().append(&table)
278        }
279
280        /// Helper function to create a LEFT JOIN clause
281        pub fn left_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
282        where
283            Table: $TableTrait,
284        {
285            join_internal(table, $crate::Join::new().left(), condition)
286        }
287
288        /// Helper function to create a LEFT OUTER JOIN clause
289        pub fn left_outer_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
290        where
291            Table: $TableTrait,
292        {
293            join_internal(table, $crate::Join::new().left().outer(), condition)
294        }
295
296        /// Helper function to create a NATURAL LEFT OUTER JOIN clause.
297        ///
298        /// A natural join matches the columns both sides share by name,
299        /// so it takes no ON condition.
300        pub fn natural_left_outer_join<'a, Table>(table: Table) -> $SQLType
301        where
302            Table: $TableTrait,
303        {
304            use $crate::ToSQL;
305            $crate::Join::new()
306                .natural()
307                .left()
308                .outer()
309                .to_sql()
310                .append(&table)
311        }
312
313        /// Helper function to create a NATURAL RIGHT JOIN clause.
314        ///
315        /// A natural join matches the columns both sides share by name,
316        /// so it takes no ON condition.
317        pub fn natural_right_join<'a, Table>(table: Table) -> $SQLType
318        where
319            Table: $TableTrait,
320        {
321            use $crate::ToSQL;
322            $crate::Join::new()
323                .natural()
324                .right()
325                .to_sql()
326                .append(&table)
327        }
328
329        /// Helper function to create a RIGHT JOIN clause
330        pub fn right_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
331        where
332            Table: $TableTrait,
333        {
334            join_internal(table, $crate::Join::new().right(), condition)
335        }
336
337        /// Helper function to create a RIGHT OUTER JOIN clause
338        pub fn right_outer_join<'a, Table>(
339            table: Table,
340            condition: impl $ConditionTrait,
341        ) -> $SQLType
342        where
343            Table: $TableTrait,
344        {
345            join_internal(table, $crate::Join::new().right().outer(), condition)
346        }
347
348        /// Helper function to create a NATURAL RIGHT OUTER JOIN clause.
349        ///
350        /// A natural join matches the columns both sides share by name,
351        /// so it takes no ON condition.
352        pub fn natural_right_outer_join<'a, Table>(table: Table) -> $SQLType
353        where
354            Table: $TableTrait,
355        {
356            use $crate::ToSQL;
357            $crate::Join::new()
358                .natural()
359                .right()
360                .outer()
361                .to_sql()
362                .append(&table)
363        }
364
365        /// Helper function to create a NATURAL FULL JOIN clause.
366        ///
367        /// A natural join matches the columns both sides share by name,
368        /// so it takes no ON condition.
369        pub fn natural_full_join<'a, Table>(table: Table) -> $SQLType
370        where
371            Table: $TableTrait,
372        {
373            use $crate::ToSQL;
374            $crate::Join::new().natural().full().to_sql().append(&table)
375        }
376
377        /// Helper function to create a FULL JOIN clause
378        pub fn full_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
379        where
380            Table: $TableTrait,
381        {
382            join_internal(table, $crate::Join::new().full(), condition)
383        }
384
385        /// Helper function to create a FULL OUTER JOIN clause
386        pub fn full_outer_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
387        where
388            Table: $TableTrait,
389        {
390            join_internal(table, $crate::Join::new().full().outer(), condition)
391        }
392
393        /// Helper function to create a NATURAL FULL OUTER JOIN clause.
394        ///
395        /// A natural join matches the columns both sides share by name,
396        /// so it takes no ON condition.
397        pub fn natural_full_outer_join<'a, Table>(table: Table) -> $SQLType
398        where
399            Table: $TableTrait,
400        {
401            use $crate::ToSQL;
402            $crate::Join::new()
403                .natural()
404                .full()
405                .outer()
406                .to_sql()
407                .append(&table)
408        }
409
410        /// Helper function to create a NATURAL INNER JOIN clause.
411        ///
412        /// A natural join matches the columns both sides share by name,
413        /// so it takes no ON condition.
414        pub fn natural_inner_join<'a, Table>(table: Table) -> $SQLType
415        where
416            Table: $TableTrait,
417        {
418            use $crate::ToSQL;
419            $crate::Join::new()
420                .natural()
421                .inner()
422                .to_sql()
423                .append(&table)
424        }
425
426        /// Helper function to create an INNER JOIN clause
427        pub fn inner_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
428        where
429            Table: $TableTrait,
430        {
431            join_internal(table, $crate::Join::new().inner(), condition)
432        }
433
434        /// Compatibility helper for a conditional cross join.
435        ///
436        /// This renders the portable equivalent `INNER JOIN ... ON ...`.
437        /// Use the dialect builder's bare `.cross_join(source)` for an
438        /// unconditional `CROSS JOIN`.
439        pub fn cross_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
440        where
441            Table: $TableTrait,
442        {
443            join_internal(table, $crate::Join::new().inner(), condition)
444        }
445    };
446}
447
448/// Macro to generate dialect-specific `JoinArg` trait and impls.
449///
450/// This consolidates the shared logic for:
451/// - explicit join tuples: `(table, condition)`
452/// - auto-FK joins for bare tables
453#[macro_export]
454macro_rules! impl_join_arg_trait {
455    (
456        table_trait: $TableTrait:path,
457        table_info_trait: $TableInfoTrait:path,
458        condition_trait: $ConditionTrait:path,
459        join_source_trait: $JoinSourceTrait:path,
460        value_type: $ValueType:ty $(,)?
461    ) => {
462        /// Trait for arguments accepted by `.join()` and related join methods.
463        pub trait JoinArg<'a, FromTable> {
464            /// Table added to the query scope by this join.
465            type JoinedTable;
466
467            /// Renders the join source and its `ON` condition.
468            fn into_join_sql(self, join: $crate::Join) -> $crate::SQL<'a, $ValueType>;
469        }
470
471        /// Bare table: derives the ON condition from `Joinable::fk_columns()`.
472        impl<'a, U, T> JoinArg<'a, T> for U
473        where
474            U: $TableTrait + $crate::Joinable<T>,
475            T: $TableInfoTrait + ::core::default::Default,
476        {
477            type JoinedTable = U;
478
479            fn into_join_sql(self, join: $crate::Join) -> $crate::SQL<'a, $ValueType> {
480                use $crate::ToSQL;
481
482                let from = T::default();
483                let cols = <U as $crate::Joinable<T>>::fk_columns();
484                let join_name = self.name();
485                let from_name = from.name();
486
487                let mut condition = $crate::SQL::with_capacity_chunks(cols.len() * 7);
488                for (idx, (self_col, target_col)) in cols.iter().enumerate() {
489                    if idx > 0 {
490                        condition.push_mut($crate::Token::AND);
491                    }
492                    condition.append_mut(
493                        $crate::SQL::ident(join_name)
494                            .push($crate::Token::DOT)
495                            .append($crate::SQL::ident(*self_col)),
496                    );
497                    condition.push_mut($crate::Token::EQ);
498                    condition.append_mut(
499                        $crate::SQL::ident(from_name)
500                            .push($crate::Token::DOT)
501                            .append($crate::SQL::ident(*target_col)),
502                    );
503                }
504
505                join.to_sql()
506                    .append(&self)
507                    .push($crate::Token::ON)
508                    .append(&condition)
509            }
510        }
511
512        /// Tuple `(table, condition)`: explicit ON condition.
513        impl<'a, U, C, T> JoinArg<'a, T> for (U, C)
514        where
515            U: $JoinSourceTrait,
516            C: $ConditionTrait,
517        {
518            type JoinedTable = U::JoinedTable;
519
520            fn into_join_sql(self, join: $crate::Join) -> $crate::SQL<'a, $ValueType> {
521                let (source, condition) = self;
522                join.to_sql()
523                    .append($crate::SQL::raw(" "))
524                    .append(source.into_join_source_sql())
525                    .push($crate::Token::ON)
526                    .append(condition.into_sql())
527            }
528        }
529    };
530}