Skip to content

Commit 452ebd0

Browse files
authored
Merge pull request diesel-rs#1026 from diesel-rs/sg-join-on
Allow the `ON` clause of a join to be manually specified
2 parents ab5fcdb + 5cd4c0c commit 452ebd0

6 files changed

Lines changed: 146 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
1010

1111
* Added support for the PG `IS DISTINCT FROM` operator
1212

13+
* The `ON` clause of a join can now be manually specified. See [the
14+
docs][join-on-dsl-1.0.0] for details.
15+
16+
[join-on-dsl-1.0.0]: http://docs.diesel.rs/diesel/prelude/trait.JoinOnDsl.html#method.on
17+
1318
### Changed
1419

1520
* Diesel will now automatically invoke `numeric_expr!` for your columns in the

diesel/src/query_dsl/join_dsl.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use query_builder::AsQuery;
2-
use query_source::{joins, Table, JoinTo};
2+
use query_source::{Table, JoinTo, QuerySource};
3+
use query_source::joins::{self, OnClauseWrapper};
34

45
#[doc(hidden)]
56
/// `JoinDsl` support trait to emulate associated type constructors
@@ -106,3 +107,52 @@ pub trait JoinDsl: Sized {
106107

107108
impl<T: AsQuery> JoinDsl for T {
108109
}
110+
111+
pub trait JoinOnDsl: Sized {
112+
/// Specify the `ON` clause for a join statement. This will override
113+
/// any implicit `ON` clause that would come from `#[belongs_to]`
114+
///
115+
/// # Example
116+
///
117+
/// ```rust
118+
/// # #[macro_use] extern crate diesel;
119+
/// # include!("src/doctest_setup.rs");
120+
/// #
121+
/// # table! {
122+
/// # users {
123+
/// # id -> Integer,
124+
/// # name -> VarChar,
125+
/// # }
126+
/// # }
127+
/// #
128+
/// # table! {
129+
/// # posts {
130+
/// # id -> Integer,
131+
/// # user_id -> Integer,
132+
/// # title -> Text,
133+
/// # }
134+
/// # }
135+
/// #
136+
/// # enable_multi_table_joins!(users, posts);
137+
/// #
138+
/// # fn main() {
139+
/// # let connection = establish_connection();
140+
/// let data = users::table
141+
/// .left_join(posts::table.on(
142+
/// users::id.eq(posts::user_id).and(
143+
/// posts::title.eq("My first post"))
144+
/// ))
145+
/// .select((users::name, posts::title.nullable()))
146+
/// .load(&connection);
147+
/// let expected = vec![
148+
/// ("Sean".to_string(), Some("My first post".to_string())),
149+
/// ("Tess".to_string(), None),
150+
/// ];
151+
/// assert_eq!(Ok(expected), data);
152+
/// # }
153+
fn on<On>(self, on: On) -> OnClauseWrapper<Self, On> {
154+
OnClauseWrapper::new(self, on)
155+
}
156+
}
157+
158+
impl<T: QuerySource> JoinOnDsl for T {}

diesel/src/query_dsl/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub use self::distinct_dsl::DistinctDsl;
2424
pub use self::filter_dsl::{FilterDsl, FindDsl};
2525
#[doc(hidden)]
2626
pub use self::group_by_dsl::GroupByDsl;
27-
pub use self::join_dsl::{InternalJoinDsl, JoinDsl};
27+
pub use self::join_dsl::{InternalJoinDsl, JoinDsl, JoinOnDsl};
2828
pub use self::limit_dsl::LimitDsl;
2929
pub use self::load_dsl::{LoadDsl, ExecuteDsl, FirstDsl, LoadQuery};
3030
pub use self::offset_dsl::OffsetDsl;

diesel/src/query_source/joins.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use prelude::*;
55
use query_builder::*;
66
use result::QueryResult;
77
use super::QuerySource;
8+
use types::Bool;
89
use util::TupleAppend;
910

1011
#[derive(Debug, Clone, Copy)]
@@ -47,7 +48,7 @@ impl_query_id!(Join<Left, Right, Kind>);
4748
impl_query_id!(JoinOn<Join, On>);
4849

4950
impl<Left, Right> QuerySource for Join<Left, Right, Inner> where
50-
Left: QuerySource + JoinTo<Right> + AppendSelection<Right::DefaultSelection>,
51+
Left: QuerySource + AppendSelection<Right::DefaultSelection>,
5152
Right: QuerySource,
5253
Left::Output: SelectableExpression<Self>,
5354
Self: Clone,
@@ -65,7 +66,7 @@ impl<Left, Right> QuerySource for Join<Left, Right, Inner> where
6566
}
6667

6768
impl<Left, Right> QuerySource for Join<Left, Right, LeftOuter> where
68-
Left: QuerySource + JoinTo<Right> + AppendSelection<Nullable<Right::DefaultSelection>>,
69+
Left: QuerySource + AppendSelection<Nullable<Right::DefaultSelection>>,
6970
Right: QuerySource,
7071
Left::Output: SelectableExpression<Self>,
7172
Self: Clone,
@@ -84,7 +85,7 @@ impl<Left, Right> QuerySource for Join<Left, Right, LeftOuter> where
8485

8586
impl<Join, On> QuerySource for JoinOn<Join, On> where
8687
Join: QuerySource,
87-
On: AppearsOnTable<Join::FromClause> + Clone,
88+
On: AppearsOnTable<Join::FromClause, SqlType=Bool> + Clone,
8889
Join::DefaultSelection: SelectableExpression<Self>,
8990
{
9091
type FromClause = Grouped<nodes::InfixNode<'static, Join::FromClause, On>>;
@@ -294,3 +295,27 @@ impl<T, U> Plus<T> for Succ<U> where
294295
impl<T> Plus<T> for Never {
295296
type Output = T;
296297
}
298+
299+
#[doc(hidden)]
300+
#[derive(Debug, Clone, Copy)]
301+
pub struct OnClauseWrapper<Source, On> {
302+
source: Source,
303+
on: On,
304+
}
305+
306+
impl<Source, On> OnClauseWrapper<Source, On> {
307+
pub fn new(source: Source, on: On) -> Self {
308+
OnClauseWrapper { source, on }
309+
}
310+
}
311+
312+
impl<Lhs, Rhs, On> JoinTo<OnClauseWrapper<Rhs, On>> for Lhs where
313+
Lhs: Table,
314+
{
315+
type FromClause = Rhs;
316+
type OnClause = On;
317+
318+
fn join_target(rhs: OnClauseWrapper<Rhs, On>) -> (Self::FromClause, Self::OnClause) {
319+
(rhs.source, rhs.on)
320+
}
321+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#[macro_use] extern crate diesel;
2+
3+
use diesel::prelude::*;
4+
5+
table! {
6+
users {
7+
id -> Integer,
8+
}
9+
}
10+
11+
table! {
12+
posts {
13+
id -> Integer,
14+
}
15+
}
16+
17+
table! {
18+
comments {
19+
id -> Integer,
20+
}
21+
}
22+
23+
enable_multi_table_joins!(users, posts);
24+
enable_multi_table_joins!(users, comments);
25+
enable_multi_table_joins!(posts, comments);
26+
27+
fn main() {
28+
// Sanity check, make sure valid joins compile
29+
let _ = users::table.inner_join(posts::table.on(users::id.eq(posts::id)));
30+
// Invalid, references column that isn't being queried
31+
let _ = users::table.inner_join(posts::table.on(users::id.eq(comments::id)));
32+
//~^ ERROR E0271
33+
// Invalid, type is not boolean
34+
let _ = users::table.inner_join(posts::table.on(users::id));
35+
//~^ ERROR E0271
36+
}

diesel_tests/tests/joins.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,31 @@ fn selecting_complex_expression_from_both_sides_of_outer_join() {
298298
assert_eq!(Ok(expected_data), titles);
299299
}
300300

301+
#[test]
302+
fn join_with_explicit_on_clause() {
303+
let connection = connection_with_sean_and_tess_in_users_table();
304+
let new_posts = vec![
305+
NewPost::new(1, "Post One", None),
306+
NewPost::new(1, "Post Two", None),
307+
];
308+
insert(&new_posts).into(posts::table).execute(&connection).unwrap();
309+
310+
let sean = find_user_by_name("Sean", &connection);
311+
let tess = find_user_by_name("Tess", &connection);
312+
let post_one = posts::table.filter(posts::title.eq("Post One"))
313+
.first::<Post>(&connection)
314+
.unwrap();
315+
let expected_data = vec![
316+
(sean, post_one.clone()),
317+
(tess, post_one),
318+
];
319+
320+
let data = users::table.inner_join(posts::table.on(posts::title.eq("Post One")))
321+
.load(&connection);
322+
323+
assert_eq!(Ok(expected_data), data);
324+
}
325+
301326
#[test]
302327
fn selecting_parent_child_grandchild() {
303328
let (connection, test_data) = connection_with_fixture_data_for_multitable_joins();

0 commit comments

Comments
 (0)