Skip to content

FULL OUTER JOIN support #4526

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions diesel/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,25 @@ pub trait SqlDialect: self::private::TrustedBackend {
doc = "See [`sql_dialect::alias_syntax`] for provided default implementations"
)]
type AliasSyntax;

/// Configures how this backend supports `FULL JOIN` clauses
///
/// This allows backends to opt in to [`.full_join(rhs)`](crate::prelude::QueryDsl::full_join) support and to
/// provide a custom [`QueryFragment`](crate::query_builder::QueryFragment)
#[cfg_attr(
feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes",
doc = "implementation for [`FullJoinSupport`](crate::query_builder::FullJoinSupport)"
)]
#[cfg_attr(
not(feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes"),
doc = "implementation for `FullJoinSupport`"
)]
///
#[cfg_attr(
feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes",
doc = "See [`sql_dialect::full_join`] for provided default implementations"
)]
type FullJoinSupport;
}

/// This module contains all options provided by diesel to configure the [`SqlDialect`] trait.
Expand Down Expand Up @@ -539,6 +558,22 @@ pub(crate) mod sql_dialect {
#[derive(Debug, Copy, Clone)]
pub struct AsAliasSyntax;
}

/// This module contains all reusable options to configure
/// [`SqlDialect::FullJoinSupport`]
#[diesel_derives::__diesel_public_if(
feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes"
)]
pub mod full_join_support {
/// Indicates that a backend provides support for `FULL JOIN` clauses
/// using the postgresql `FULL (OUTER) JOIN` syntax
#[derive(Debug, Copy, Clone)]
pub struct PostgresLikeFullJoinSupport;

/// Indicates that a backend does not support `FULL JOIN` clauses
#[derive(Debug, Copy, Clone)]
pub struct NoFullJoinSupport;
}
}

// These traits are not part of the public API
Expand Down
6 changes: 4 additions & 2 deletions diesel/src/expression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub(crate) mod subselect;
#[cfg(feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes")]
pub use self::operators::Concat;

//pub use nullable::{IntoNullableExpression, NullableExpressionOf};

// we allow unreachable_pub here
// as rustc otherwise shows false positives
// for every item in this module. We reexport
Expand Down Expand Up @@ -322,8 +324,8 @@ where
/// expressions will implement this if each of their parts implement it.
///
/// Notably, columns will not implement this trait for the right side of a left
/// join. To select a column or expression using a column from the right side of
/// a left join, you must call `.nullable()` on it.
/// join, or either side of a full join. To select a column or expression using
/// a column from the nullable side of such a join, you must call `.nullable()` on it.
#[diagnostic::on_unimplemented(
message = "Cannot select `{Self}` from `{QS}`",
note = "`{Self}` is no valid selection for `{QS}`"
Expand Down
55 changes: 55 additions & 0 deletions diesel/src/expression/nullable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::expression::*;
use crate::query_builder::*;
use crate::query_source::joins::ToInnerJoin;
use crate::result::QueryResult;
use crate::sql_types::is_nullable;
use crate::sql_types::{DieselNumericOps, IntoNullable};

#[doc(hidden)] // This is used by the `table!` macro internally
Expand Down Expand Up @@ -56,3 +57,57 @@ where
}

impl<T> SelectableExpression<NoFromClause> for Nullable<T> where Self: AppearsOnTable<NoFromClause> {}

pub(crate) type NullableExpressionOf<E> =
<<E as Expression>::SqlType as IntoNullableExpression<E>>::NullableExpression;

/// Convert expressions into their nullable form, without double wrapping them in `Nullable<...>`.
pub trait IntoNullableExpression<E> {
/// The nullable expression of this type.
///
/// For all expressions except `Nullable`, this will be `Nullable<Self>`.
type NullableExpression;

/// Convert this expression into its nullable representation.
///
/// For `Nullable<T>`, this remain as `Nullable<T>`, otherwise the expression will be wrapped and be `Nullable<Self>`.
fn into_nullable_expression(e: E) -> Self::NullableExpression;
}

impl<ST, E> IntoNullableExpression<E> for ST
where
ST: SingleValue,
E: Expression<SqlType = ST>,
ST: SqlType,
(ST::IsNull, ST): IntoNullableExpression<E>,
{
type NullableExpression = <(ST::IsNull, ST) as IntoNullableExpression<E>>::NullableExpression;

fn into_nullable_expression(e: E) -> Self::NullableExpression {
<(ST::IsNull, ST) as IntoNullableExpression<E>>::into_nullable_expression(e)
}
}

impl<ST, E> IntoNullableExpression<E> for (is_nullable::NotNull, ST)
where
E: Expression<SqlType = ST>,
ST: SqlType<IsNull = is_nullable::NotNull>,
{
type NullableExpression = Nullable<E>;

fn into_nullable_expression(e: E) -> Self::NullableExpression {
Nullable(e)
}
}

impl<ST, E> IntoNullableExpression<E> for (is_nullable::IsNullable, ST)
where
E: Expression<SqlType = ST>,
ST: SqlType<IsNull = is_nullable::IsNullable>,
{
type NullableExpression = E;

fn into_nullable_expression(e: E) -> Self::NullableExpression {
e
}
}
2 changes: 1 addition & 1 deletion diesel/src/internal/table_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub use crate::query_source::aliasing::{
AliasAppearsInFromClause, FieldAliasMapperAssociatedTypesDisjointnessTrick,
};
#[doc(hidden)]
pub use crate::query_source::joins::{Inner, Join, JoinOn, LeftOuter};
pub use crate::query_source::joins::{FullOuter, Inner, Join, JoinOn, LeftOuter};
#[doc(hidden)]
pub use crate::query_source::private::Pick;

Expand Down
57 changes: 56 additions & 1 deletion diesel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
//! executing a query:
//! This error message indicates that you're trying to select a field from a table
//! that does not appear in your from clause. If your query joins the relevant table via
//! [`left_join`](crate::query_dsl::QueryDsl::left_join) you need to call
//! [`left_join`](crate::query_dsl::QueryDsl::left_join) or [`full_join`](crate::query_dsl::QueryDsl::full_join) you need to call
//! [`.nullable()`](crate::expression_methods::NullableExpressionMethods::nullable)
//! on the relevant column in your select clause.
//!
Expand Down Expand Up @@ -467,6 +467,14 @@ pub mod helper_types {
pub type LeftJoinOn<Source, Rhs, On> =
<Source as InternalJoinDsl<Rhs, joins::LeftOuter, On>>::Output;

/// Represents the return type of [`.full_join(rhs)`](crate::prelude::QueryDsl::full_join)
pub type FullJoin<Source, Rhs> =
<Source as JoinWithImplicitOnClause<Rhs, joins::FullOuter>>::Output;

/// Represents the return type of [`.full_join(rhs.on(on))`](crate::prelude::QueryDsl::full_join)
pub type FullJoinOn<Source, Rhs, On> =
<Source as InternalJoinDsl<Rhs, joins::FullOuter, On>>::Output;

/// Represents the return type of [`rhs.on(on)`](crate::query_dsl::JoinOnDsl::on)
pub type On<Source, On> = joins::OnClauseWrapper<Source, On>;

Expand Down Expand Up @@ -646,6 +654,53 @@ pub mod helper_types {
pub type LeftJoinQuerySource<Left, Right, On = <Left as joins::JoinTo<Right>>::OnClause> =
JoinQuerySource<Left, Right, joins::LeftOuter, On>;

/// A query source representing the full outer join between two tables.
///
/// The third generic type (`On`) controls how the tables are
/// joined.
///
/// By default, the implicit join established by [`joinable!`][]
/// will be used, allowing you to omit the exact join
/// condition. For example, for the full join between three
/// tables that implement [`JoinTo`][], you only need to specify
/// the tables: `FullJoinQuerySource<FullJoinQuerySource<table1,
/// table2>, table3>`.
///
/// [`JoinTo`]: crate::query_source::JoinTo
///
/// If you use an explicit `ON` clause, you will need to specify
/// the `On` generic type.
///
/// ```rust
/// # include!("doctest_setup.rs");
/// use diesel::{dsl, helper_types::LeftJoinQuerySource};
/// # use diesel::{backend::Backend, serialize::ToSql, sql_types};
/// use schema::*;
///
/// # fn main() -> QueryResult<()> {
/// # let conn = &mut establish_connection();
/// #
/// // If you have an explicit join like this...
/// let join_constraint = comments::columns::post_id.eq(posts::columns::id);
/// # let query =
/// posts::table.full_join(comments::table.on(join_constraint));
/// #
/// # // Dummy usage just to ensure the example compiles.
/// # let filter = posts::columns::id.eq(1);
/// # let filter: &FilterExpression<_> = &filter;
/// # query.filter(filter).select(posts::columns::id.nullable()).get_result::<Option<i32>>(conn)?;
/// #
/// # Ok(())
/// # }
///
/// // ... you can use `FullJoinQuerySource` like this.
/// type JoinConstraint = dsl::Eq<comments::columns::post_id, posts::columns::id>;
/// type MyFullJoinQuerySource = FullJoinQuerySource<posts::table, comments::table, JoinConstraint>;
/// # type FilterExpression<DB> = dyn BoxableExpression<MyFullJoinQuerySource, DB, SqlType = sql_types::Bool>;
/// ```
pub type FullJoinQuerySource<Left, Right, On = <Left as joins::JoinTo<Right>>::OnClause> =
JoinQuerySource<Left, Right, joins::FullOuter, On>;

/// Maps `F` to `Alias<S>`
///
/// Any column `F` that belongs to `S::Table` will be transformed into
Expand Down
2 changes: 2 additions & 0 deletions diesel/src/mysql/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ impl SqlDialect for Mysql {

type ConcatClause = MysqlConcatClause;
type AliasSyntax = sql_dialect::alias_syntax::AsAliasSyntax;

type FullJoinSupport = sql_dialect::full_join_support::NoFullJoinSupport;
}

impl DieselReserveSpecialization for Mysql {}
Expand Down
2 changes: 2 additions & 0 deletions diesel/src/pg/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ impl SqlDialect for Pg {
type ExistsSyntax = sql_dialect::exists_syntax::AnsiSqlExistsSyntax;
type ArrayComparison = PgStyleArrayComparison;
type AliasSyntax = sql_dialect::alias_syntax::AsAliasSyntax;

type FullJoinSupport = sql_dialect::full_join_support::PostgresLikeFullJoinSupport;
}

impl DieselReserveSpecialization for Pg {}
Expand Down
94 changes: 94 additions & 0 deletions diesel/src/query_dsl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,100 @@ pub trait QueryDsl: Sized {
self.left_outer_join(rhs)
}

/// Join two tables using a SQL `FULL OUTER JOIN`.
///
/// This is only supported on PostgreSQL and SQLite.
///
/// Behaves similarly to [`inner_join`] and [`left_join`], but will produce a full join
/// instead. See [`inner_join`] for usage examples.
///
/// [`inner_join`]: QueryDsl::inner_join()
/// [`left_join`]: QueryDsl::left_join()
///
/// Columns in both the left and right hand tables will become `Nullable` which means
/// you must call `nullable()` on the corresponding fields in the select
/// clause:
///
/// ### Selecting after a full join
///
/// ```rust
/// # include!("../doctest_setup.rs");
/// # use schema::{users, posts};
/// #
/// # #[derive(Queryable, PartialEq, Eq, Debug)]
/// # struct User {
/// # id: i32,
/// # name: String,
/// # }
/// #
/// # impl User {
/// # fn new(id: i32, name: &str) -> Self {
/// # User {
/// # id,
/// # name: name.into(),
/// # }
/// # }
/// # }
/// #
/// # #[derive(Queryable, PartialEq, Eq, Debug)]
/// # struct Post {
/// # id: i32,
/// # user_id: i32,
/// # title: String,
/// # }
/// #
/// # impl Post {
/// # fn new(id: i32, user_id: i32, title: &str) -> Self {
/// # Post {
/// # id,
/// # user_id,
/// # title: title.into(),
/// # }
/// # }
/// # }
/// #
/// # fn main() {
/// # run_test().unwrap();
/// # }
/// #
/// # fn run_test() -> QueryResult<()> {
/// # let connection = &mut establish_connection();
/// # diesel::sql_query("DELETE FROM posts").execute(connection)?;
/// # diesel::insert_into(posts::table)
/// # .values((posts::user_id.eq(1), posts::title.eq("Sean's Post")))
/// # .execute(connection)?;
/// # let post_id = posts::table.select(posts::id)
/// # .first::<i32>(connection)?;
/// let join = users::table.full_join(posts::table);
///
/// // As full joins may have NULL values on both sides, `.nullable` is required on all columnes.
/// let names_and_titles = join.select((users::name.nullable(), posts::title.nullable()))
/// .load::<(Option<String>, Option<String>)>(connection)?;
/// let expected_data = vec![
/// (Some(String::from("Sean")), Some(String::from("Sean's Post"))),
/// (Some(String::from("Tess")), None),
/// ];
/// assert_eq!(expected_data, names_and_titles);
/// # Ok(())
/// # }
/// ```
fn full_outer_join<Rhs>(self, rhs: Rhs) -> FullJoin<Self, Rhs>
where
Self: JoinWithImplicitOnClause<Rhs, joins::FullOuter>,
{
self.join_with_implicit_on_clause(rhs, joins::FullOuter)
}

/// Alias for [`full_outer_join`].
///
/// [`full_outer_join`]: QueryDsl::full_outer_join()
fn full_join<Rhs>(self, rhs: Rhs) -> FullJoin<Self, Rhs>
where
Self: JoinWithImplicitOnClause<Rhs, joins::FullOuter>,
{
self.full_outer_join(rhs)
}

/// Adds to the `WHERE` clause of a query.
///
/// If there is already a `WHERE` clause, the result will be `old AND new`.
Expand Down
12 changes: 11 additions & 1 deletion diesel/src/query_source/aliasing/joins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use crate::expression::{AppearsOnTable, SelectableExpression};
use crate::query_builder::AsQuery;
use crate::query_dsl::InternalJoinDsl;
use crate::query_source::joins::{
AppendSelection, Inner, Join, JoinOn, JoinTo, LeftOuter, OnClauseWrapper, ToInnerJoin,
AppendSelection, FullOuter, Inner, Join, JoinOn, JoinTo, LeftOuter, OnClauseWrapper,
ToInnerJoin,
};
use crate::query_source::{
AppearsInFromClause, FromClause, Never, Pick, QuerySource, SelectStatement, Table,
Expand Down Expand Up @@ -99,6 +100,15 @@ where
{
}

impl<Left, Right, S, C> SelectableExpression<Join<Left, Right, FullOuter>> for AliasedField<S, C>
where
Self: AppearsOnTable<Join<Left, Right, FullOuter>>,
Self: SelectableExpression<Left>,
Left: AppearsInFromClause<Alias<S>, Count = Never> + QuerySource,
Right: AppearsInFromClause<Alias<S>, Count = Never> + QuerySource,
{
}

impl<Left, Right, S, C> SelectableExpression<Join<Left, Right, Inner>> for AliasedField<S, C>
where
Self: AppearsOnTable<Join<Left, Right, Inner>>,
Expand Down
Loading
Loading