Skip to main content

drizzle_core/
join.rs

1//! JOIN keywords and the macros dialect crates use to build join helpers.
2//!
3//! Users join tables with the builder methods (`.join(...)`,
4//! `.left_join(...)`, ...). This module holds the shared pieces those
5//! methods render with.
6
7use crate::{SQL, ToSQL, traits::SQLParam};
8
9// =============================================================================
10// Join Type Enum
11// =============================================================================
12
13/// The kind of a JOIN.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
15pub enum JoinType {
16    /// Plain `JOIN` (an inner join).
17    #[default]
18    Join,
19    /// `INNER JOIN`.
20    Inner,
21    /// `LEFT JOIN`.
22    Left,
23    /// `RIGHT JOIN`.
24    Right,
25    /// `FULL JOIN`.
26    Full,
27    /// `CROSS JOIN`.
28    Cross,
29}
30
31// =============================================================================
32// Join Builder Struct
33// =============================================================================
34
35/// The JOIN keyword of a join clause, such as `NATURAL LEFT OUTER JOIN`.
36///
37/// Built with `const` methods, so it can be a constant. Renders through
38/// [`ToSQL`].
39///
40/// # Examples
41///
42/// ```
43/// use drizzle_core::{Join, SQL, ToSQL};
44/// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
45/// # use std::borrow::Cow;
46/// # #[derive(Debug, Clone, PartialEq)]
47/// # struct Value(i64);
48/// # impl SQLParam for Value {
49/// #     const DIALECT: Dialect = Dialect::SQLite;
50/// #     type DialectMarker = SQLiteDialect;
51/// # }
52/// # impl From<Value> for Cow<'_, Value> {
53/// #     fn from(value: Value) -> Self { Cow::Owned(value) }
54/// # }
55///
56/// let sql: SQL<'_, Value> = Join::new().left().outer().to_sql();
57/// assert_eq!(sql.sql(), "LEFT OUTER JOIN");
58/// ```
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
60pub struct Join {
61    /// Adds `NATURAL`.
62    pub natural: bool,
63    /// The kind of join.
64    pub join_type: JoinType,
65    /// Adds `OUTER`. Only used for `LEFT`, `RIGHT` and `FULL`.
66    pub outer: bool,
67}
68
69impl Join {
70    /// Creates a plain `JOIN`.
71    #[must_use]
72    pub const fn new() -> Self {
73        Self {
74            natural: false,
75            join_type: JoinType::Join,
76            outer: false,
77        }
78    }
79
80    /// Makes this a `NATURAL` join.
81    #[must_use]
82    pub const fn natural(mut self) -> Self {
83        self.natural = true;
84        self
85    }
86
87    /// Makes this an `INNER` join.
88    #[must_use]
89    pub const fn inner(mut self) -> Self {
90        self.join_type = JoinType::Inner;
91        self
92    }
93
94    /// Makes this a `LEFT` join.
95    #[must_use]
96    pub const fn left(mut self) -> Self {
97        self.join_type = JoinType::Left;
98        self
99    }
100
101    /// Makes this a `RIGHT` join.
102    #[must_use]
103    pub const fn right(mut self) -> Self {
104        self.join_type = JoinType::Right;
105        self
106    }
107
108    /// Makes this a `FULL` join.
109    #[must_use]
110    pub const fn full(mut self) -> Self {
111        self.join_type = JoinType::Full;
112        self
113    }
114
115    /// Makes this a `CROSS` join.
116    #[must_use]
117    pub const fn cross(mut self) -> Self {
118        self.join_type = JoinType::Cross;
119        self
120    }
121
122    /// Adds `OUTER` (`LEFT OUTER`, `RIGHT OUTER`, `FULL OUTER`). Ignored for
123    /// other kinds.
124    #[must_use]
125    pub const fn outer(mut self) -> Self {
126        self.outer = true;
127        self
128    }
129}
130
131impl<'a, V: SQLParam + 'a> ToSQL<'a, V> for Join {
132    fn to_sql(&self) -> SQL<'a, V> {
133        // Use pre-computed static strings to avoid Vec allocation
134        let join_str = match (self.natural, self.join_type, self.outer) {
135            // NATURAL variants
136            (true, JoinType::Join, _) => "NATURAL JOIN",
137            (true, JoinType::Inner, _) => "NATURAL INNER JOIN",
138            (true, JoinType::Left, false) => "NATURAL LEFT JOIN",
139            (true, JoinType::Left, true) => "NATURAL LEFT OUTER JOIN",
140            (true, JoinType::Right, false) => "NATURAL RIGHT JOIN",
141            (true, JoinType::Right, true) => "NATURAL RIGHT OUTER JOIN",
142            (true, JoinType::Full, false) => "NATURAL FULL JOIN",
143            (true, JoinType::Full, true) => "NATURAL FULL OUTER JOIN",
144            (true, JoinType::Cross, _) => "NATURAL CROSS JOIN",
145            // Non-NATURAL variants
146            (false, JoinType::Join, _) => "JOIN",
147            (false, JoinType::Inner, _) => "INNER JOIN",
148            (false, JoinType::Left, false) => "LEFT JOIN",
149            (false, JoinType::Left, true) => "LEFT OUTER JOIN",
150            (false, JoinType::Right, false) => "RIGHT JOIN",
151            (false, JoinType::Right, true) => "RIGHT OUTER JOIN",
152            (false, JoinType::Full, false) => "FULL JOIN",
153            (false, JoinType::Full, true) => "FULL OUTER JOIN",
154            (false, JoinType::Cross, _) => "CROSS JOIN",
155        };
156        SQL::raw(join_str)
157    }
158}
159
160/// A `(derived table, condition)` pair accepted by `JOIN LATERAL`.
161#[doc(hidden)]
162pub trait LateralArg<'a, V: SQLParam>: lateral_private::Arg {
163    /// The source added to the query scope.
164    type JoinedTable;
165    /// Sources read by the `ON` condition (see [`crate::scope`]).
166    type OnSources;
167
168    /// Renders `<join> LATERAL <source> ON <condition>`.
169    fn into_lateral_sql(self, join: Join) -> SQL<'a, V>;
170}
171
172impl<'a, V, Name, Projection, Query, Condition> LateralArg<'a, V>
173    for (crate::Derived<'a, V, Name, Projection, Query>, Condition)
174where
175    V: SQLParam + 'a,
176    Name: crate::Tag,
177    Projection: crate::DerivedProjection<Name>,
178    Query: ToSQL<'a, V>,
179    Condition: crate::expr::Expr<'a, V>,
180    Condition::SQLType: crate::types::BooleanLike,
181{
182    type JoinedTable = crate::Derived<'a, V, Name, Projection, Query>;
183    type OnSources = Condition::Sources;
184
185    fn into_lateral_sql(self, join: Join) -> SQL<'a, V> {
186        let (source, condition) = self;
187        join.to_sql()
188            .append(SQL::raw(" LATERAL "))
189            .append(source.into_sql())
190            .push(crate::Token::ON)
191            .append(condition.into_sql())
192    }
193}
194
195/// A derived table accepted by `CROSS JOIN LATERAL`.
196#[doc(hidden)]
197pub trait LateralSource<'a, V: SQLParam>: lateral_private::Source {
198    /// The source added to the query scope.
199    type JoinedTable;
200
201    /// Renders `CROSS JOIN LATERAL <source>`.
202    fn into_cross_lateral_sql(self) -> SQL<'a, V>;
203}
204
205impl<'a, V, Name, Projection, Query> LateralSource<'a, V>
206    for crate::Derived<'a, V, Name, Projection, Query>
207where
208    V: SQLParam + 'a,
209    Name: crate::Tag,
210    Projection: crate::DerivedProjection<Name>,
211    Query: ToSQL<'a, V>,
212{
213    type JoinedTable = Self;
214
215    fn into_cross_lateral_sql(self) -> SQL<'a, V> {
216        Join::new()
217            .cross()
218            .to_sql()
219            .append(SQL::raw(" LATERAL "))
220            .append(self.into_sql())
221    }
222}
223
224mod lateral_private {
225    pub trait Arg {}
226    pub trait Source {}
227
228    impl<V, Name, Projection, Query, Condition> Arg
229        for (crate::Derived<'_, V, Name, Projection, Query>, Condition)
230    where
231        V: crate::SQLParam,
232    {
233    }
234
235    impl<V, Name, Projection, Query> Source for crate::Derived<'_, V, Name, Projection, Query> where
236        V: crate::SQLParam
237    {
238    }
239}
240
241// =============================================================================
242// Join Helper Macro
243// =============================================================================
244
245/// Generates free functions that render join clauses (`join`, `left_join`,
246/// `natural_full_outer_join`, ...) for one dialect.
247///
248/// Each function takes a table and, except for the `natural_*` ones, an ON
249/// condition, and returns the rendered clause. Dialect crates invoke this
250/// once with their table trait, condition trait and SQL type.
251///
252/// # Examples
253///
254/// ```
255/// use drizzle_core::SQL;
256/// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
257/// # use std::borrow::Cow;
258/// # #[derive(Debug, Clone, PartialEq)]
259/// # struct Value(i64);
260/// # impl SQLParam for Value {
261/// #     const DIALECT: Dialect = Dialect::SQLite;
262/// #     type DialectMarker = SQLiteDialect;
263/// # }
264/// # impl From<Value> for Cow<'_, Value> {
265/// #     fn from(value: Value) -> Self { Cow::Owned(value) }
266/// # }
267///
268/// mod joins {
269///     use super::Value;
270///     use drizzle_core::{SQL, ToSQL};
271///
272///     drizzle_core::impl_join_helpers!(
273///         table_trait: ToSQL<'a, Value>,
274///         condition_trait: ToSQL<'a, Value>,
275///         sql_type: SQL<'a, Value>,
276///     );
277/// }
278///
279/// fn main() {
280///     let clause = joins::left_join(SQL::<Value>::ident("posts"), SQL::<Value>::raw("TRUE"));
281///     assert_eq!(clause.sql(), r#"LEFT JOIN "posts" ON TRUE"#);
282/// }
283/// ```
284#[macro_export]
285macro_rules! impl_join_helpers {
286    (
287        table_trait: $TableTrait:path,
288        condition_trait: $ConditionTrait:path,
289        sql_type: $SQLType:ty $(,)?
290    ) => {
291        fn join_internal<'a, Table>(
292            table: Table,
293            join: $crate::Join,
294            condition: impl $ConditionTrait,
295        ) -> $SQLType
296        where
297            Table: $TableTrait,
298        {
299            use $crate::ToSQL;
300            join.to_sql()
301                .append(&table)
302                .push($crate::Token::ON)
303                .append(&condition)
304        }
305
306        /// Renders `NATURAL JOIN table`.
307        ///
308        /// A natural join matches the columns both sides share by name, so it
309        /// takes no ON condition.
310        pub fn natural_join<'a, Table>(table: Table) -> $SQLType
311        where
312            Table: $TableTrait,
313        {
314            use $crate::ToSQL;
315            $crate::Join::new().natural().to_sql().append(&table)
316        }
317
318        /// Renders `JOIN table ON condition`.
319        pub fn join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
320        where
321            Table: $TableTrait,
322        {
323            join_internal(table, $crate::Join::new(), condition)
324        }
325
326        /// Renders `NATURAL LEFT JOIN table`.
327        ///
328        /// A natural join matches the columns both sides share by name, so it
329        /// takes no ON condition.
330        pub fn natural_left_join<'a, Table>(table: Table) -> $SQLType
331        where
332            Table: $TableTrait,
333        {
334            use $crate::ToSQL;
335            $crate::Join::new().natural().left().to_sql().append(&table)
336        }
337
338        /// Renders `LEFT JOIN table ON condition`.
339        pub fn left_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
340        where
341            Table: $TableTrait,
342        {
343            join_internal(table, $crate::Join::new().left(), condition)
344        }
345
346        /// Renders `LEFT OUTER JOIN table ON condition`.
347        pub fn left_outer_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
348        where
349            Table: $TableTrait,
350        {
351            join_internal(table, $crate::Join::new().left().outer(), condition)
352        }
353
354        /// Renders `NATURAL LEFT OUTER JOIN table`.
355        ///
356        /// A natural join matches the columns both sides share by name, so it
357        /// takes no ON condition.
358        pub fn natural_left_outer_join<'a, Table>(table: Table) -> $SQLType
359        where
360            Table: $TableTrait,
361        {
362            use $crate::ToSQL;
363            $crate::Join::new()
364                .natural()
365                .left()
366                .outer()
367                .to_sql()
368                .append(&table)
369        }
370
371        /// Renders `NATURAL RIGHT JOIN table`.
372        ///
373        /// A natural join matches the columns both sides share by name, so it
374        /// takes no ON condition.
375        pub fn natural_right_join<'a, Table>(table: Table) -> $SQLType
376        where
377            Table: $TableTrait,
378        {
379            use $crate::ToSQL;
380            $crate::Join::new()
381                .natural()
382                .right()
383                .to_sql()
384                .append(&table)
385        }
386
387        /// Renders `RIGHT JOIN table ON condition`.
388        pub fn right_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
389        where
390            Table: $TableTrait,
391        {
392            join_internal(table, $crate::Join::new().right(), condition)
393        }
394
395        /// Renders `RIGHT OUTER JOIN table ON condition`.
396        pub fn right_outer_join<'a, Table>(
397            table: Table,
398            condition: impl $ConditionTrait,
399        ) -> $SQLType
400        where
401            Table: $TableTrait,
402        {
403            join_internal(table, $crate::Join::new().right().outer(), condition)
404        }
405
406        /// Renders `NATURAL RIGHT OUTER JOIN table`.
407        ///
408        /// A natural join matches the columns both sides share by name, so it
409        /// takes no ON condition.
410        pub fn natural_right_outer_join<'a, Table>(table: Table) -> $SQLType
411        where
412            Table: $TableTrait,
413        {
414            use $crate::ToSQL;
415            $crate::Join::new()
416                .natural()
417                .right()
418                .outer()
419                .to_sql()
420                .append(&table)
421        }
422
423        /// Renders `NATURAL FULL JOIN table`.
424        ///
425        /// A natural join matches the columns both sides share by name, so it
426        /// takes no ON condition.
427        pub fn natural_full_join<'a, Table>(table: Table) -> $SQLType
428        where
429            Table: $TableTrait,
430        {
431            use $crate::ToSQL;
432            $crate::Join::new().natural().full().to_sql().append(&table)
433        }
434
435        /// Renders `FULL JOIN table ON condition`.
436        pub fn full_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
437        where
438            Table: $TableTrait,
439        {
440            join_internal(table, $crate::Join::new().full(), condition)
441        }
442
443        /// Renders `FULL OUTER JOIN table ON condition`.
444        pub fn full_outer_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
445        where
446            Table: $TableTrait,
447        {
448            join_internal(table, $crate::Join::new().full().outer(), condition)
449        }
450
451        /// Renders `NATURAL FULL OUTER JOIN table`.
452        ///
453        /// A natural join matches the columns both sides share by name, so it
454        /// takes no ON condition.
455        pub fn natural_full_outer_join<'a, Table>(table: Table) -> $SQLType
456        where
457            Table: $TableTrait,
458        {
459            use $crate::ToSQL;
460            $crate::Join::new()
461                .natural()
462                .full()
463                .outer()
464                .to_sql()
465                .append(&table)
466        }
467
468        /// Renders `NATURAL INNER JOIN table`.
469        ///
470        /// A natural join matches the columns both sides share by name, so it
471        /// takes no ON condition.
472        pub fn natural_inner_join<'a, Table>(table: Table) -> $SQLType
473        where
474            Table: $TableTrait,
475        {
476            use $crate::ToSQL;
477            $crate::Join::new()
478                .natural()
479                .inner()
480                .to_sql()
481                .append(&table)
482        }
483
484        /// Renders `INNER JOIN table ON condition`.
485        pub fn inner_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
486        where
487            Table: $TableTrait,
488        {
489            join_internal(table, $crate::Join::new().inner(), condition)
490        }
491
492        /// Compatibility helper for a conditional cross join.
493        ///
494        /// This renders the portable equivalent `INNER JOIN ... ON ...`.
495        /// Use the dialect builder's bare `.cross_join(source)` for an
496        /// unconditional `CROSS JOIN`.
497        pub fn cross_join<'a, Table>(table: Table, condition: impl $ConditionTrait) -> $SQLType
498        where
499            Table: $TableTrait,
500        {
501            join_internal(table, $crate::Join::new().inner(), condition)
502        }
503    };
504}
505
506/// Generates a dialect's `JoinArg` trait: what `.join(...)` and its variants
507/// accept.
508///
509/// Two forms are accepted:
510/// - `(source, condition)`: an explicit ON condition;
511/// - a bare table: the ON condition comes from the foreign key between the
512///   two tables ([`Joinable`](crate::Joinable)).
513#[macro_export]
514macro_rules! impl_join_arg_trait {
515    (
516        table_trait: $TableTrait:path,
517        table_info_trait: $TableInfoTrait:path,
518        condition_trait: $ConditionTrait:path,
519        join_source_trait: $JoinSourceTrait:path,
520        value_type: $ValueType:ty $(,)?
521    ) => {
522        /// Trait for arguments accepted by `.join()` and related join methods.
523        ///
524        /// `Via` is inferred: which table's foreign key a bare table joins on
525        /// ([`JoinKey`]($crate::JoinKey)), or `JoinExplicit` for a condition.
526        pub trait JoinArg<'a, FromTable, Via> {
527            /// Table added to the query scope by this join.
528            type JoinedTable;
529
530            /// Sources read by the `ON` condition (see `drizzle_core::scope`).
531            type OnSources;
532
533            /// Renders the join source and its `ON` condition.
534            fn into_join_sql(self, join: $crate::Join) -> $crate::SQL<'a, $ValueType>;
535        }
536
537        /// Bare table: the ON condition matches the columns of the one
538        /// foreign key between the two tables, whichever declares it.
539        impl<'a, U, T, Via> JoinArg<'a, T, Via> for U
540        where
541            U: $TableTrait + $crate::JoinKey<T, Via>,
542            T: $TableInfoTrait + ::core::default::Default,
543        {
544            type JoinedTable = U;
545            // The derived condition reads only the joined and current tables.
546            type OnSources = ();
547
548            fn into_join_sql(self, join: $crate::Join) -> $crate::SQL<'a, $ValueType> {
549                use $crate::ToSQL;
550
551                let from = T::default();
552                let cols = <U as $crate::JoinKey<T, Via>>::pairs();
553                let join_name = self.name();
554                let from_name = from.name();
555
556                let mut condition = $crate::SQL::with_capacity_chunks(cols.len() * 7);
557                for (idx, pair) in cols.iter().enumerate() {
558                    let (self_col, target_col) = <U as $crate::JoinKey<T, Via>>::pair(pair);
559                    if idx > 0 {
560                        condition.push_mut($crate::Token::AND);
561                    }
562                    condition.append_mut(
563                        $crate::SQL::ident(join_name)
564                            .push($crate::Token::DOT)
565                            .append($crate::SQL::ident(self_col)),
566                    );
567                    condition.push_mut($crate::Token::EQ);
568                    condition.append_mut(
569                        $crate::SQL::ident(from_name)
570                            .push($crate::Token::DOT)
571                            .append($crate::SQL::ident(target_col)),
572                    );
573                }
574
575                join.to_sql()
576                    .append(&self)
577                    .push($crate::Token::ON)
578                    .append(&condition)
579            }
580        }
581
582        /// Tuple `(table, condition)`: explicit ON condition.
583        impl<'a, U, C, T> JoinArg<'a, T, $crate::JoinExplicit> for (U, C)
584        where
585            U: $JoinSourceTrait,
586            C: $ConditionTrait + $crate::expr::ExprSources,
587        {
588            type JoinedTable = U::JoinedTable;
589            type OnSources = C::Sources;
590
591            fn into_join_sql(self, join: $crate::Join) -> $crate::SQL<'a, $ValueType> {
592                let (source, condition) = self;
593                join.to_sql()
594                    .append($crate::SQL::raw(" "))
595                    .append(source.into_join_source_sql())
596                    .push($crate::Token::ON)
597                    .append(condition.into_sql())
598            }
599        }
600    };
601}