Skip to content

Commit bc6110d

Browse files
authored
Merge pull request diesel-rs#2234 from diesel-rs/sg-fix-nested-joins
Correctly enforce nullability with nested joins
2 parents 5592a50 + 07989f2 commit bc6110d

File tree

7 files changed

+237
-39
lines changed

7 files changed

+237
-39
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
6161
have been. All types in Diesel are now correctly only considered
6262
non-aggregate if their parts are.
6363

64+
* Nullability requirements are now properly enforced for nested joins.
65+
Previously, only the rules for the outer-most join were considered. For
66+
example, `users.left_join(posts).left_join(comments)` would allow selecting
67+
any columns from `posts`. That will now fail to compile, and any selections
68+
from `posts` will need to be made explicitly nullable.
69+
6470
### Deprecated
6571

6672
* `diesel_(prefix|postfix|infix)_operator!` have been deprecated. These macros

diesel/src/expression/nullable.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::backend::Backend;
22
use crate::expression::*;
33
use crate::query_builder::*;
4-
use crate::query_source::Table;
4+
use crate::query_source::joins::ToInnerJoin;
55
use crate::result::QueryResult;
66
use crate::sql_types::IntoNullable;
77

@@ -50,7 +50,7 @@ impl<T: QueryId> QueryId for Nullable<T> {
5050
impl<T, QS> SelectableExpression<QS> for Nullable<T>
5151
where
5252
Self: AppearsOnTable<QS>,
53-
T: SelectableExpression<QS>,
54-
QS: Table,
53+
QS: ToInnerJoin,
54+
T: SelectableExpression<QS::InnerJoin>,
5555
{
5656
}

diesel/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@
9595
9696
#![cfg_attr(feature = "unstable", feature(specialization))]
9797
// Built-in Lints
98-
#![deny(
99-
warnings,
98+
#![deny(warnings)]
99+
#![warn(
100100
missing_debug_implementations,
101101
missing_copy_implementations,
102102
missing_docs

diesel/src/macros/mod.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ macro_rules! __diesel_column {
4444
Join<Left, Right, LeftOuter>,
4545
> for $column_name where
4646
$column_name: AppearsOnTable<Join<Left, Right, LeftOuter>>,
47-
Left: AppearsInFromClause<$table, Count=Once>,
47+
Self: SelectableExpression<Left>,
48+
// If our table is on the right side of this join, only
49+
// `Nullable<Self>` can be selected
4850
Right: AppearsInFromClause<$table, Count=Never>,
4951
{
5052
}
@@ -53,7 +55,12 @@ macro_rules! __diesel_column {
5355
Join<Left, Right, Inner>,
5456
> for $column_name where
5557
$column_name: AppearsOnTable<Join<Left, Right, Inner>>,
56-
Join<Left, Right, Inner>: AppearsInFromClause<$table, Count=Once>,
58+
Left: AppearsInFromClause<$table>,
59+
Right: AppearsInFromClause<$table>,
60+
(Left::Count, Right::Count): Pick<Left, Right>,
61+
Self: SelectableExpression<
62+
<(Left::Count, Right::Count) as Pick<Left, Right>>::Selection,
63+
>,
5764
{
5865
}
5966

@@ -791,7 +798,7 @@ macro_rules! __diesel_table_impl {
791798
use $crate::backend::Backend;
792799
use $crate::query_builder::{QueryFragment, AstPass, SelectStatement};
793800
use $crate::query_source::joins::{Join, JoinOn, Inner, LeftOuter};
794-
use $crate::query_source::{AppearsInFromClause, Once, Never};
801+
use $crate::query_source::{AppearsInFromClause, Once, Never, Pick};
795802
use $crate::result::QueryResult;
796803
$($imports)*
797804

diesel/src/query_source/joins.rs

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -121,35 +121,6 @@ where
121121
}
122122
}
123123

124-
impl<Left, Right, Kind, T> SelectableExpression<Join<Left, Right, Kind>> for Nullable<T>
125-
where
126-
T: SelectableExpression<Join<Left, Right, Inner>>,
127-
Nullable<T>: AppearsOnTable<Join<Left, Right, Kind>>,
128-
{
129-
}
130-
131-
// FIXME: Remove this when overlapping marker traits are stable
132-
impl<Join, On, T> SelectableExpression<JoinOn<Join, On>> for Nullable<T>
133-
where
134-
Nullable<T>: SelectableExpression<Join>,
135-
Nullable<T>: AppearsOnTable<JoinOn<Join, On>>,
136-
{
137-
}
138-
139-
// FIXME: Remove this when overlapping marker traits are stable
140-
impl<From, T> SelectableExpression<SelectStatement<From>> for Nullable<T>
141-
where
142-
Nullable<T>: SelectableExpression<From>,
143-
Nullable<T>: AppearsOnTable<SelectStatement<From>>,
144-
{
145-
}
146-
147-
// FIXME: We want these blanket impls when overlapping marker traits are stable
148-
// impl<T, Join, On> SelectableExpression<JoinOn<Join, On>> for T where
149-
// T: SelectableExpression<Join> + AppearsOnTable<JoinOn<Join, On>>,
150-
// {
151-
// }
152-
153124
/// Indicates that two tables can be joined without an explicit `ON` clause.
154125
///
155126
/// Implementations of this trait are generated by invoking [`joinable!`].
@@ -300,3 +271,41 @@ where
300271
(rhs.source, rhs.on)
301272
}
302273
}
274+
275+
#[doc(hidden)]
276+
/// Convert any joins in a `FROM` clause into an inner join.
277+
///
278+
/// This trait is used to determine whether
279+
/// `Nullable<T>: SelectableExpression<SomeJoin>`. We consider it to be
280+
/// selectable if `T: SelectableExpression<InnerJoin>`. Since `SomeJoin`
281+
/// may be deeply nested, we need to recursively change any appearances of
282+
/// `LeftOuter` to `Inner` in order to perform this check.
283+
pub trait ToInnerJoin {
284+
type InnerJoin;
285+
}
286+
287+
impl<Left, Right, Kind> ToInnerJoin for Join<Left, Right, Kind>
288+
where
289+
Left: ToInnerJoin,
290+
Right: ToInnerJoin,
291+
{
292+
type InnerJoin = Join<Left::InnerJoin, Right::InnerJoin, Inner>;
293+
}
294+
295+
impl<Join, On> ToInnerJoin for JoinOn<Join, On>
296+
where
297+
Join: ToInnerJoin,
298+
{
299+
type InnerJoin = JoinOn<Join::InnerJoin, On>;
300+
}
301+
302+
impl<From> ToInnerJoin for SelectStatement<From>
303+
where
304+
From: ToInnerJoin,
305+
{
306+
type InnerJoin = SelectStatement<From::InnerJoin>;
307+
}
308+
309+
impl<T: Table> ToInnerJoin for T {
310+
type InnerJoin = T;
311+
}

diesel/src/query_source/mod.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,83 @@ pub trait AppearsInFromClause<QS> {
7676
/// How many times does `Self` appear in `QS`?
7777
type Count;
7878
}
79+
80+
#[doc(hidden)]
81+
/// Used to determine which of two from clauses contains a given table.
82+
///
83+
/// This trait can be used to emulate "or" conditions in where clauses when
84+
/// we want a trait to be implemented with one of two type parameters.
85+
///
86+
/// For example, if we wanted to write:
87+
///
88+
/// ```rust,ignore
89+
/// where
90+
/// T: SelectableExpression<Left> | SelectableExpression<Right>,
91+
/// ```
92+
///
93+
/// we can emulate this by writing:
94+
///
95+
/// ```rust,ignore
96+
/// where
97+
/// Left: AppearsInFromClause<T::Table>,
98+
/// Right: AppearsInFromClause<T::Table>,
99+
/// (Left::Count, Right::Count): Pick<Left, Right>,
100+
/// T: SelectableExpression<
101+
/// <(Left::Count, Right::Count) as Pick<Left, Right>>::Selection,
102+
/// >,
103+
/// ```
104+
///
105+
/// In order to aquire the counts in the first place, we must already know
106+
/// the table we're searching for.
107+
pub trait Pick<Left, Right> {
108+
/// The selected type.
109+
///
110+
/// For `(Once, Never)` this type will be `Left`. For `(Never, Once)`, this type will be
111+
/// `Right`
112+
type Selection;
113+
}
114+
115+
impl<Left, Right> Pick<Left, Right> for (Once, Never) {
116+
type Selection = Left;
117+
}
118+
119+
impl<Left, Right> Pick<Left, Right> for (Never, Once) {
120+
type Selection = Right;
121+
}
122+
123+
#[doc(hidden)]
124+
#[allow(
125+
non_camel_case_types,
126+
missing_debug_implementations,
127+
missing_copy_implementations
128+
)]
129+
/// Everything in this module is here to give something more helpful than:
130+
///
131+
/// > (Never, Never): Pick<table1, table2> is not satisifed
132+
///
133+
/// Any of these impls can be deleted if they are getting in the way of
134+
/// other functionality. Any code which is using these impls is already
135+
/// failing to compile.
136+
mod impls_which_are_only_here_to_improve_error_messages {
137+
use super::*;
138+
139+
pub struct this_table_doesnt_appear_in_the_from_clause_of_your_query;
140+
141+
impl<Left, Right> Pick<Left, Right> for (Never, Never) {
142+
type Selection = this_table_doesnt_appear_in_the_from_clause_of_your_query;
143+
}
144+
145+
pub struct this_table_appears_in_your_query_more_than_once_and_must_be_aliased;
146+
147+
impl<Left, Right, OtherCount> Pick<Left, Right> for (MoreThanOnce, OtherCount) {
148+
type Selection = this_table_appears_in_your_query_more_than_once_and_must_be_aliased;
149+
}
150+
151+
impl<Left, Right> Pick<Left, Right> for (Never, MoreThanOnce) {
152+
type Selection = this_table_appears_in_your_query_more_than_once_and_must_be_aliased;
153+
}
154+
155+
impl<Left, Right> Pick<Left, Right> for (Once, MoreThanOnce) {
156+
type Selection = this_table_appears_in_your_query_more_than_once_and_must_be_aliased;
157+
}
158+
}

diesel_compile_tests/tests/compile-fail/right_side_of_left_join_requires_nullable.rs

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,122 @@ table! {
1818
}
1919
}
2020

21+
table! {
22+
pets {
23+
id -> Integer,
24+
user_id -> Integer,
25+
name -> Text,
26+
}
27+
}
28+
2129
joinable!(posts -> users (user_id));
22-
allow_tables_to_appear_in_same_query!(posts, users);
30+
joinable!(pets -> users (user_id));
31+
allow_tables_to_appear_in_same_query!(posts, users, pets);
2332
sql_function!(fn lower(x: Text) -> Text);
2433

2534
fn main() {
26-
let conn = PgConnection::establish("some url").unwrap();
35+
}
36+
37+
fn direct_joins() {
2738
let join = users::table.left_outer_join(posts::table);
2839

2940
// Invalid, only Nullable<title> is selectable
3041
let _ = join.select(posts::title);
3142
//~^ ERROR E0271
43+
//~| ERROR E0277
44+
// Valid
45+
let _ = join.select(posts::title.nullable());
46+
// Valid -- NULL to a function will return null
47+
let _ = join.select(lower(posts::title).nullable());
48+
// Invalid, only Nullable<title> is selectable
49+
let _ = join.select(lower(posts::title));
50+
//~^ ERROR E0271
51+
//~| ERROR E0277
52+
// Invalid, Nullable<title> is selectable, but lower expects not-null
53+
let _ = join.select(lower(posts::title.nullable()));
54+
//~^ ERROR E0271
3255
//~| ERROR E0271
56+
}
57+
58+
fn nested_outer_joins_left_associative() {
59+
60+
let join = users::table.left_outer_join(posts::table).left_outer_join(pets::table);
61+
62+
// Invalid, only Nullable<title> is selectable
63+
let _ = join.select(posts::title);
64+
//~^ ERROR E0271
65+
//~| ERROR E0277
3366
// Valid
3467
let _ = join.select(posts::title.nullable());
3568
// Valid -- NULL to a function will return null
3669
let _ = join.select(lower(posts::title).nullable());
3770
// Invalid, only Nullable<title> is selectable
3871
let _ = join.select(lower(posts::title));
3972
//~^ ERROR E0271
73+
//~| ERROR E0277
74+
// Invalid, Nullable<title> is selectable, but lower expects not-null
75+
let _ = join.select(lower(posts::title.nullable()));
76+
//~^ ERROR E0271
4077
//~| ERROR E0271
78+
}
79+
80+
fn nested_mixed_joins_left_associative() {
81+
let join = users::table.left_outer_join(posts::table).inner_join(pets::table);
82+
83+
// Invalid, only Nullable<title> is selectable
84+
let _ = join.select(posts::title);
85+
//~^ ERROR E0271
86+
//~| ERROR E0277
87+
// Valid
88+
let _ = join.select(posts::title.nullable());
89+
// Valid -- NULL to a function will return null
90+
let _ = join.select(lower(posts::title).nullable());
91+
// Invalid, only Nullable<title> is selectable
92+
let _ = join.select(lower(posts::title));
93+
//~^ ERROR E0271
94+
//~| ERROR E0277
95+
// Invalid, Nullable<title> is selectable, but lower expects not-null
96+
let _ = join.select(lower(posts::title.nullable()));
97+
//~^ ERROR E0271
98+
//~| ERROR E0271
99+
}
100+
101+
fn nested_outer_joins_right_associative() {
102+
let join = pets::table.left_outer_join(users::table.left_outer_join(posts::table));
103+
104+
// Invalid, only Nullable<title> is selectable
105+
let _ = join.select(posts::title);
106+
//~^ ERROR E0271
107+
//~| ERROR E0277
108+
// Valid
109+
let _ = join.select(posts::title.nullable());
110+
// Valid -- NULL to a function will return null
111+
let _ = join.select(lower(posts::title).nullable());
112+
// Invalid, only Nullable<title> is selectable
113+
let _ = join.select(lower(posts::title));
114+
//~^ ERROR E0271
115+
//~| ERROR E0277
116+
// Invalid, Nullable<title> is selectable, but lower expects not-null
117+
let _ = join.select(lower(posts::title.nullable()));
118+
//~^ ERROR E0271
119+
//~| ERROR E0271
120+
}
121+
122+
fn nested_mixed_joins_right_associative() {
123+
let join = pets::table.inner_join(users::table.left_outer_join(posts::table));
124+
125+
// Invalid, only Nullable<title> is selectable
126+
let _ = join.select(posts::title);
127+
//~^ ERROR E0271
128+
//~| ERROR E0277
129+
// Valid
130+
let _ = join.select(posts::title.nullable());
131+
// Valid -- NULL to a function will return null
132+
let _ = join.select(lower(posts::title).nullable());
133+
// Invalid, only Nullable<title> is selectable
134+
let _ = join.select(lower(posts::title));
135+
//~^ ERROR E0271
136+
//~| ERROR E0277
41137
// Invalid, Nullable<title> is selectable, but lower expects not-null
42138
let _ = join.select(lower(posts::title.nullable()));
43139
//~^ ERROR E0271

0 commit comments

Comments
 (0)