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 5 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
4 changes: 2 additions & 2 deletions diesel/src/expression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,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
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
62 changes: 62 additions & 0 deletions diesel/src/query_source/joins.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use super::{AppearsInFromClause, Plus};
use crate::backend::sql_dialect;
use crate::backend::Backend;
use crate::backend::DieselReserveSpecialization;
use crate::backend::SqlDialect;
use crate::expression::grouped::Grouped;
use crate::expression::nullable::Nullable;
use crate::prelude::*;
Expand Down Expand Up @@ -160,6 +162,42 @@ where
}
}

impl<Left, Right> QuerySource for Join<Left, Right, FullOuter>
where
Left: QuerySource,
Right: QuerySource,
Nullable<Left::DefaultSelection>: AppendSelection<Nullable<Right::DefaultSelection>>,
<Nullable<Left::DefaultSelection> as AppendSelection<Nullable<Right::DefaultSelection>>>::Output:
AppearsOnTable<Self>,
Self: Clone,
{
type FromClause = Self;
// combining two valid selectable expressions for both tables will always yield a
// valid selectable expressions for the whole join, so no need to check that here
// again. These checked turned out to be quite expensive in terms of compile time
// so we use a wrapper type to just skip the check and forward other more relevant
// trait implementations to the inner type
//
// See https://github.com/diesel-rs/diesel/issues/3223 for details
type DefaultSelection = self::private::SkipSelectableExpressionBoundCheckWrapper<
<Nullable<Left::DefaultSelection> as AppendSelection<Nullable<Right::DefaultSelection>>>::Output,
>;

fn from_clause(&self) -> Self::FromClause {
self.clone()
}

fn default_selection(&self) -> Self::DefaultSelection {
self::private::SkipSelectableExpressionBoundCheckWrapper(
self.left
.source
.default_selection()
.nullable()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually flawed, and I can't seem to find a good solution. Because .nullable() always wraps the selection in Nullable, even if already Nullable, when using multiple full_join() calls you end up with (Nullable<(Nullable<A>, Nullable<B>)>, Nullable<C>), rather than (Nullable<A>, Nullable<B>, Nullable<C>).

One solution could be to make a IntoNullable trait on expressions (akin to the sql_type version), which would essentially return Self if already Nullable<...>. However, I've been getting conflicting impl errors between the T and Nullable<T> blocks and can't seem to find a way to get this to work. Any ideas or other approaches that could be taken? Seemingly the fact it's checking IsNull with the indirection from Expression::SqlType is preventing Rust from realising they're disjoint as is done in the sql_type version. (still fails if adding + SingleValue)

/// Convert values into their nullable form, without double wrapping them in `Nullable<...>`.
pub trait IntoNullable<ST> {
    /// The nullable value of this type.
    ///
    /// For all values except `Nullable`, this will be `Nullable<Self>`.
    type Nullable;

    /// Convert this value into its nullable representation.
    ///
    /// For `Nullable<T>`, this remain as `Nullable<T>`, otherwise the value will be wrapped and be `Nullable<Self>`.
    fn into_nullable(self) -> Self::Nullable;
}

impl<T> IntoNullable for T
where
    T: Expression,
    T::SqlType: SqlType<IsNull = sql_types::is_nullable::NotNull>,
{
    type Nullable = Nullable<T>;

    fn into_nullable(self) -> Self::Nullable {
        Nullable(self)
    }
}

impl<T> IntoNullable for Nullable<T>
where
    Nullable<T>: Expression,
    <Nullable<T> as Expression>::SqlType: SqlType, // also fails with SqlType<IsNull = sql_types::is_nullable::IsNullable>
{
    type Nullable = Self;

    fn into_nullable(self) -> Self::Nullable {
        self
    }
}

Then replace Nullable<Left::DefaultSelection> with <Left::DefaultSelection as IntoNullable>::Nullable.

Alternatively, we could add a AppendSelection impl
for Nullable<(Nullable<Left>, Nullable<Mid>)> which unpacks it, but you have the same type issues.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what could work is using the associated type specialization trick used in other places.

That means we would need to restructure that trait like follows:

// that shouldn't be part of the public exposed API, i.e. it shouldn't be nameable by 
// users outside of diesel
// Also: Prefer going with adding the `Expression` prefix everywhere 
// as it will otherwise be confused with the sql type trait
// (That already happens from time to time for the types, no 
// need to introduce another case here.)
pub trait IntoNullableExpression<E> {
    type NullableExpression;
    fn into_nullable_expression(e: E) -> Self:::NullableExpression;
}

impl<ST, E> IntoNullableExpression<E> for ST 
where 
    E: Expression<SqlType = ST>,
    ST: SqlType,
    // forward it to the other two impls
    (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)
    }
}

// these impls cannot overlap with each other as the Self type is clearly
// different. They also shouldn't overlap with the generic one
// above as `SqlType` is only implemented for tuples if all elements
// implement `SqlType` on their own, which is not the case for `is_nullable::NotNull` 
// and `is_nullable::IsNullable`
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(self)
    }
}

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
    }
}

in that way you have to two impl that are clearly not overlapping as they are for different types and a third impl on top of it that forwards to the other impls. You then need to call it slightly different as <E::SqlType as IntoNullableExpression<E>>::into_nullable_expression(e) from the calling side, but given that this is only one location + that's not public API I would consider that acceptable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh clever! Will try update to use that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs in to an issue unfortunately, as tuples seem to be IsNullable if any of the values are IsNullable. e.g. (Integer, Text, Nullable<Text>)::IsNull == IsNullable. Thus, it keeps (Integer, Text, Nullable<Text>) as (Integer, Text, Nullable<Text>), rather than turning it in to Nullable<(Integer, Text, Nullable<Text>)>.

Tuples are using OneIsNullable, should they perhaps instead be using AllAreNullable? Or if that's correct, is there another way to do this check?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that's correct for tuples. Giving it another thought I noticed that this all is not required. (Nullable<(Nullable<A>, Nullable<B>)>, Nullable<C>) is as far as I remember the expected result here. The sql type code handles unwrapping that to something more meaningful if I remember that correctly. In fact that seems to be also the thing we do for LEFT JOINS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure I follow sorry, I was under the impression that AppendSelection should instead give (Nullable<A>, Nullable<B>, Nullable<C>)? In its doc comment it says:

Used to ensure the sql type of left.join(mid).join(right) is (Left, Mid, Right) and not ((Left, Mid), Right).

The following compiles correctly using left joins:

    let source: Vec<(User, Option<Post>, Option<Comment>, Option<Like>)> = users::table
        .left_join(posts::table.on(posts::user_id.eq(users::id)))
        .left_join(comments::table.on(posts::id.eq(comments::post_id)))
        .left_join(likes::table.on(comments::id.eq(likes::comment_id)))
        .load(connection)
        .unwrap();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That means I need to have another look at this to give better suggestions. I'm not sure if I find the time this or next week. If that doesn't happen until then please ping me again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed c718389 which at least fixes compiling all the tests. I'm still not 100% sure if this impl is correct. It might require an additional step that wraps every element in the tuple returned from the Join query source (left hand side) in a Nullable wrapper if it's not already nullable. I think this would apply if you have first a inner join and then a full join. If that is still problematic it should be possible to implement such a tuple mapping by having a helper trait for all supported tuple sizes that just applies the IntoNullableExpression trait to all tuple elements.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for that, I added a test that did confirm that inner join then full join is problematic, so I've added IntoNullableExpression for tuples.

It is now essentially working, however, I've needed to add IntoNullableExpression for SkipSelectableExpressionBoundCheckWrapper as impl<ST, E> IntoNullableExpression<E> for ST needs to be where ST: SingleValue to avoid conflicting with the tuple impls and SkipSelectableExpressionBoundCheckWrapper no longer has an impl. That's conflicting with impl<ST, E> IntoNullableExpression<E> for ST though. Looking forward to negative trait bounds stabilising...

I don't have more time right now to try and fix, but will come back in a few more days when I get the time. If you have thoughts on it let me know though.

.append_selection(self.right.source.default_selection().nullable()),
)
}
}

#[derive(Debug, Clone, Copy)]
pub struct OnKeyword;

Expand Down Expand Up @@ -251,6 +289,14 @@ impl<T: Table, Selection> AppendSelection<Selection> for T {
}
}

impl<Left: Expression + Clone, Selection> AppendSelection<Selection> for Nullable<Left> {
type Output = (Nullable<Left>, Selection);

fn append_selection(&self, selection: Selection) -> Self::Output {
(self.clone(), selection)
}
}

impl<Left, Mid, Selection, Kind> AppendSelection<Selection> for Join<Left, Mid, Kind>
where
Left: QuerySource,
Expand Down Expand Up @@ -304,6 +350,22 @@ where
}
}

#[doc(hidden)]
#[derive(Debug, Clone, Copy, Default, QueryId)]
pub struct FullOuter;

impl<DB> QueryFragment<DB> for FullOuter
where
DB: Backend
+ SqlDialect<FullJoinSupport = sql_dialect::full_join_support::PostgresLikeFullJoinSupport>
+ DieselReserveSpecialization,
{
fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, DB>) -> QueryResult<()> {
out.push_sql(" FULL OUTER");
Ok(())
}
}

impl<Left, Mid, Right, Kind> JoinTo<Right> for Join<Left, Mid, Kind>
where
Left: JoinTo<Right> + QuerySource,
Expand Down
2 changes: 2 additions & 0 deletions diesel/src/sqlite/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ impl SqlDialect for Sqlite {
type ExistsSyntax = sql_dialect::exists_syntax::AnsiSqlExistsSyntax;
type ArrayComparison = sql_dialect::array_comparison::AnsiSqlArrayComparison;
type AliasSyntax = sql_dialect::alias_syntax::AsAliasSyntax;

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

impl DieselReserveSpecialization for Sqlite {}
Expand Down
Loading
Loading