Skip to content

Commit abd9f11

Browse files
committed
WIP FullOuter join impl
1 parent 82df9f0 commit abd9f11

File tree

8 files changed

+196
-1
lines changed

8 files changed

+196
-1
lines changed

diesel/src/internal/table_macro.rs

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ pub use crate::query_source::aliasing::{
1919
AliasAppearsInFromClause, FieldAliasMapperAssociatedTypesDisjointnessTrick,
2020
};
2121
#[doc(hidden)]
22+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
23+
pub use crate::query_source::joins::FullOuter;
24+
#[doc(hidden)]
2225
pub use crate::query_source::joins::{Inner, Join, JoinOn, LeftOuter};
2326
#[doc(hidden)]
2427
pub use crate::query_source::private::Pick;

diesel/src/lib.rs

+16-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@
129129
//! executing a query:
130130
//! This error message indicates that you're trying to select a field from a table
131131
//! that does not appear in your from clause. If your query joins the relevant table via
132-
//! [`left_join`](crate::query_dsl::QueryDsl::left_join) you need to call
132+
//! [`left_join`](crate::query_dsl::QueryDsl::left_join) or [`full_join`](crate::query_dsl::QueryDsl::full_join) you need to call
133133
//! [`.nullable()`](crate::expression_methods::NullableExpressionMethods::nullable)
134134
//! on the relevant column in your select clause.
135135
//!
@@ -467,6 +467,16 @@ pub mod helper_types {
467467
pub type LeftJoinOn<Source, Rhs, On> =
468468
<Source as InternalJoinDsl<Rhs, joins::LeftOuter, On>>::Output;
469469

470+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
471+
/// Represents the return type of [`.full_join(rhs)`](crate::prelude::QueryDsl::full_join)
472+
pub type FullJoin<Source, Rhs> =
473+
<Source as JoinWithImplicitOnClause<Rhs, joins::FullOuter>>::Output;
474+
475+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
476+
/// Represents the return type of [`.full_join(rhs.on(on))`](crate::prelude::QueryDsl::full_join)
477+
pub type FullJoinOn<Source, Rhs, On> =
478+
<Source as InternalJoinDsl<Rhs, joins::FullOuter, On>>::Output;
479+
470480
/// Represents the return type of [`rhs.on(on)`](crate::query_dsl::JoinOnDsl::on)
471481
pub type On<Source, On> = joins::OnClauseWrapper<Source, On>;
472482

@@ -646,6 +656,11 @@ pub mod helper_types {
646656
pub type LeftJoinQuerySource<Left, Right, On = <Left as joins::JoinTo<Right>>::OnClause> =
647657
JoinQuerySource<Left, Right, joins::LeftOuter, On>;
648658

659+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
660+
// TODO: doc comment
661+
pub type FullJoinQuerySource<Left, Right, On = <Left as joins::JoinTo<Right>>::OnClause> =
662+
JoinQuerySource<Left, Right, joins::FullOuter, On>;
663+
649664
/// Maps `F` to `Alias<S>`
650665
///
651666
/// Any column `F` that belongs to `S::Table` will be transformed into

diesel/src/query_dsl/mod.rs

+20
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,26 @@ pub trait QueryDsl: Sized {
618618
self.left_outer_join(rhs)
619619
}
620620

621+
// TODO: doc comment
622+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
623+
fn full_outer_join<Rhs>(self, rhs: Rhs) -> FullJoin<Self, Rhs>
624+
where
625+
Self: JoinWithImplicitOnClause<Rhs, joins::FullOuter>,
626+
{
627+
self.join_with_implicit_on_clause(rhs, joins::FullOuter)
628+
}
629+
630+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
631+
/// Alias for [`full_outer_join`].
632+
///
633+
/// [`full_outer_join`]: QueryDsl::full_outer_join()
634+
fn full_join<Rhs>(self, rhs: Rhs) -> FullJoin<Self, Rhs>
635+
where
636+
Self: JoinWithImplicitOnClause<Rhs, joins::FullOuter>,
637+
{
638+
self.full_outer_join(rhs)
639+
}
640+
621641
/// Adds to the `WHERE` clause of a query.
622642
///
623643
/// If there is already a `WHERE` clause, the result will be `old AND new`.

diesel/src/query_source/aliasing/joins.rs

+13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use super::{Alias, AliasSource, AliasedField};
66
use crate::expression::{AppearsOnTable, SelectableExpression};
77
use crate::query_builder::AsQuery;
88
use crate::query_dsl::InternalJoinDsl;
9+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
10+
use crate::query_source::joins::FullOuter;
911
use crate::query_source::joins::{
1012
AppendSelection, Inner, Join, JoinOn, JoinTo, LeftOuter, OnClauseWrapper, ToInnerJoin,
1113
};
@@ -99,6 +101,17 @@ where
99101
{
100102
}
101103

104+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
105+
impl<Left, Right, S, C> SelectableExpression<Join<Left, Right, FullOuter>> for AliasedField<S, C>
106+
where
107+
Self: AppearsOnTable<Join<Left, Right, FullOuter>>,
108+
Self: SelectableExpression<Left>,
109+
Left: AppearsInFromClause<Alias<S>, Count = Never> + QuerySource,
110+
Right: AppearsInFromClause<Alias<S>, Count = Never> + QuerySource,
111+
// TODO: unsure what specifics are needed here
112+
{
113+
}
114+
102115
impl<Left, Right, S, C> SelectableExpression<Join<Left, Right, Inner>> for AliasedField<S, C>
103116
where
104117
Self: AppearsOnTable<Join<Left, Right, Inner>>,

diesel/src/query_source/joins.rs

+63
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,43 @@ where
160160
}
161161
}
162162

163+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
164+
impl<Left, Right> QuerySource for Join<Left, Right, FullOuter>
165+
where
166+
Left: QuerySource,
167+
Right: QuerySource,
168+
Nullable<Left::DefaultSelection>: AppendSelection<Nullable<Right::DefaultSelection>>,
169+
<Nullable<Left::DefaultSelection> as AppendSelection<Nullable<Right::DefaultSelection>>>::Output:
170+
AppearsOnTable<Self>,
171+
Self: Clone,
172+
{
173+
type FromClause = Self;
174+
// combining two valid selectable expressions for both tables will always yield a
175+
// valid selectable expressions for the whole join, so no need to check that here
176+
// again. These checked turned out to be quite expensive in terms of compile time
177+
// so we use a wrapper type to just skip the check and forward other more relevant
178+
// trait implementations to the inner type
179+
//
180+
// See https://github.com/diesel-rs/diesel/issues/3223 for details
181+
type DefaultSelection = self::private::SkipSelectableExpressionBoundCheckWrapper<
182+
<Nullable<Left::DefaultSelection> as AppendSelection<Nullable<Right::DefaultSelection>>>::Output,
183+
>;
184+
185+
fn from_clause(&self) -> Self::FromClause {
186+
self.clone()
187+
}
188+
189+
fn default_selection(&self) -> Self::DefaultSelection {
190+
self::private::SkipSelectableExpressionBoundCheckWrapper(
191+
self.left
192+
.source
193+
.default_selection()
194+
.nullable()
195+
.append_selection(self.right.source.default_selection().nullable()),
196+
)
197+
}
198+
}
199+
163200
#[derive(Debug, Clone, Copy)]
164201
pub struct OnKeyword;
165202

@@ -251,6 +288,15 @@ impl<T: Table, Selection> AppendSelection<Selection> for T {
251288
}
252289
}
253290

291+
// TODO: not sure if this is the best way, but the existing impls aren't sufficient
292+
impl<Left: Expression + Clone, Selection> AppendSelection<Selection> for Nullable<Left> {
293+
type Output = (Nullable<Left>, Selection);
294+
295+
fn append_selection(&self, selection: Selection) -> Self::Output {
296+
(self.clone(), selection)
297+
}
298+
}
299+
254300
impl<Left, Mid, Selection, Kind> AppendSelection<Selection> for Join<Left, Mid, Kind>
255301
where
256302
Left: QuerySource,
@@ -304,6 +350,22 @@ where
304350
}
305351
}
306352

353+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
354+
#[doc(hidden)]
355+
#[derive(Debug, Clone, Copy, Default, QueryId)]
356+
pub struct FullOuter;
357+
358+
#[cfg(any(feature = "postgres_backend", feature = "sqlite"))]
359+
impl<DB> QueryFragment<DB> for FullOuter
360+
where
361+
DB: Backend + DieselReserveSpecialization,
362+
{
363+
fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, DB>) -> QueryResult<()> {
364+
out.push_sql(" FULL OUTER");
365+
Ok(())
366+
}
367+
}
368+
307369
impl<Left, Mid, Right, Kind> JoinTo<Right> for Join<Left, Mid, Kind>
308370
where
309371
Left: JoinTo<Right> + QuerySource,
@@ -407,6 +469,7 @@ impl<Qs, On> QueryDsl for OnClauseWrapper<Qs, On> {}
407469
/// may be deeply nested, we need to recursively change any appearances of
408470
/// `LeftOuter` to `Inner` in order to perform this check.
409471
pub trait ToInnerJoin {
472+
// TODO: does anything need to be done for full join here?
410473
type InnerJoin;
411474
}
412475

diesel_compile_tests/tests/fail/invalid_joins.rs

+17
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,20 @@ fn invalid_left_joins() {
6666
// it also fails if we join to another subjoin
6767
let _ = users::table.left_join(comments::table).left_join(posts::table.left_join(comments::table));
6868
}
69+
70+
fn invalid_full_joins() {
71+
// This is a valid join
72+
let _ = users::table.full_join(posts::table);
73+
74+
// This fails, because we join the same table more than once
75+
let _ = users::table.full_join(posts::table.full_join(users::table));
76+
77+
// It also fails if we use an explicit on clause
78+
let _ = users::table.full_join(posts::table.full_join(users::table.on(posts::user_id.eq(users::id))));
79+
80+
// Also if we put the on clause on the first join
81+
let _ = users::table.full_join(posts::table.on(users::id.eq(posts::user_id)).full_join(users::table));
82+
83+
// it also fails if we join to another subjoin
84+
let _ = users::table.full_join(comments::table).full_join(posts::table.full_join(comments::table));
85+
}

diesel_compile_tests/tests/fail/selectable.rs

+46
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ struct UserWithOptionalPost {
3838
post: Option<Post>,
3939
}
4040

41+
#[derive(Selectable, Queryable)]
42+
#[diesel(table_name = users)]
43+
struct OptionalUserWithEmbeddedPost {
44+
id: Option<i32>,
45+
name: Option<String>,
46+
#[diesel(embed)]
47+
post: Post,
48+
}
49+
50+
#[derive(Selectable, Queryable)]
51+
#[diesel(table_name = users)]
52+
struct OptionalUserWithOptionalPost {
53+
id: Option<i32>,
54+
name: Option<String>,
55+
#[diesel(embed)]
56+
post: Option<Post>,
57+
}
58+
4159
#[derive(Selectable, Queryable)]
4260
#[diesel(table_name = posts)]
4361
struct Post {
@@ -125,6 +143,13 @@ fn main() {
125143
.load(&mut conn)
126144
.unwrap();
127145

146+
// full joins require both optional
147+
let _ = users::table
148+
.full_join(posts::table)
149+
.select(OptionalUserWithOptionalPost::as_select())
150+
.load(&mut conn)
151+
.unwrap();
152+
128153
// allow manual impls with complex expressions
129154
// (and group by)
130155
let _ = users::table
@@ -163,6 +188,27 @@ fn main() {
163188
.load(&mut conn)
164189
.unwrap();
165190

191+
// full joins force nullable on both
192+
let _ = users::table
193+
.left_join(posts::table)
194+
.select(UserWithEmbeddedPost::as_select())
195+
.load(&mut conn)
196+
.unwrap();
197+
198+
// full joins force nullable on left
199+
let _ = users::table
200+
.left_join(posts::table)
201+
.select(OptionalUserWithEmbeddedPost::as_select())
202+
.load(&mut conn)
203+
.unwrap();
204+
205+
// full joins force nullable on right
206+
let _ = users::table
207+
.left_join(posts::table)
208+
.select(UserWithOptionalPost::as_select())
209+
.load(&mut conn)
210+
.unwrap();
211+
166212
// group by clauses are considered
167213
let _ = users::table
168214
.inner_join(posts::table)

diesel_derives/src/table.rs

+18
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,23 @@ fn expand_column_def(column_def: &ColumnDef) -> TokenStream {
690690
let sql_name = &column_def.sql_name;
691691
let sql_type = &column_def.tpe;
692692

693+
let full_join_impl = if cfg!(any(feature = "postgres", feature = "sqlite")) {
694+
Some(quote::quote! {
695+
impl<Left, Right> diesel::SelectableExpression<
696+
diesel::internal::table_macro::Join<Left, Right, diesel::internal::table_macro::FullOuter>,
697+
> for #column_name where
698+
#column_name: diesel::AppearsOnTable<diesel::internal::table_macro::Join<Left, Right, diesel::internal::table_macro::FullOuter>>,
699+
Self: diesel::SelectableExpression<Left>,
700+
// `Nullable<Self>` can be selected for both left and right
701+
Left: diesel::query_source::AppearsInFromClause<super::table, Count=diesel::query_source::Never> + diesel::query_source::QuerySource,
702+
Right: diesel::query_source::AppearsInFromClause<super::table, Count=diesel::query_source::Never> + diesel::query_source::QuerySource,
703+
{
704+
}
705+
})
706+
} else {
707+
None
708+
};
709+
693710
let backend_specific_column_impl = if cfg!(feature = "postgres") {
694711
Some(quote::quote! {
695712
impl diesel::query_source::AppearsInFromClause<diesel::query_builder::Only<super::table>>
@@ -851,6 +868,7 @@ fn expand_column_def(column_def: &ColumnDef) -> TokenStream {
851868
#max_length
852869

853870
#ops_impls
871+
#full_join_impl
854872
#backend_specific_column_impl
855873
}
856874
}

0 commit comments

Comments
 (0)