Skip to content

Commit 448df6b

Browse files
committed
This is a tricky one. It seems like the behaviour described in that issue should work out of the box, but it doesn't. I've spend some time to investigate various solutions to make this work, but I came to the conclusion that the current behaviour is the correct one. The underlying issue here is that such an query promotes the inner `Nullable<_>` of the field onto the outer `Queryable` wrapper. Without `Selectable` that would require a select clause like `.select((table::column.assume_not_null(),).nullable())`. This is technically a safe pattern, but requires the usage of the "advanced" `assume_not_null()` method to forcibly convert types to their not null representation. Possible solutions tried to make the enable constructs shown in that issue: * I've tried to make the `impl Selectable for Option<T>` return the following `SelectExpression`: `dsl::Nullable<dsl::AssumeNotNull<T::SelectExpression>>` where `AssumeNotNull` converts each tuple element to the corresponding not nullable expression, while `Nullable` wraps the tuple itself into a nullable type wrapper. * I've tried to apply a similar approach like that one above, but only for derived impls by manipulating the generated code for a optional field with `#[diesel(embed)]` Both solutions require changes to our sql type system, as for example allowing to load a non nullable value into a `Option<T>` to enable their usage in a more general scope as the presented example case. (See the added test cases for this). That by itself would be fine in my opinion, as this is always a safe operation. Unfortunately the `AssumeNotNull` transformation would be applied recursively for all sub-tuples, which in turn would cause trouble with nested joins (again see the examples). We would be able to workaround this issue by allowing the `FromSql<ST, DB> for Option<T>` impl for non-nullable types to catch null values, which in turn really feels like a bad hack. (You would like to get an error there instead, but nested nullable information are gone.) All in all this lead me to the conclusion that the current behaviour is correct. This PR adds a few additional tests (an adjusted version of the test from the bug report + more tests around nested joins) and does move around some code bits that I noticed while working on this. I think the official answer for the bug report would be: Either wrap the inner type also in an `Option` or provide a manual `Selectable` impl that does the "right" thing there.
1 parent 983209a commit 448df6b

11 files changed

Lines changed: 583 additions & 37 deletions

File tree

diesel/src/expression/nullable.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,6 @@ impl<T: QueryId> QueryId for Nullable<T> {
4848
const HAS_STATIC_QUERY_ID: bool = T::HAS_STATIC_QUERY_ID;
4949
}
5050

51-
impl<T, DB> Selectable<DB> for Option<T>
52-
where
53-
DB: Backend,
54-
T: Selectable<DB>,
55-
Nullable<T::SelectExpression>: Expression,
56-
{
57-
type SelectExpression = Nullable<T::SelectExpression>;
58-
59-
fn construct_selection() -> Self::SelectExpression {
60-
Nullable::new(T::construct_selection())
61-
}
62-
}
63-
6451
impl<T, QS> SelectableExpression<QS> for Nullable<T>
6552
where
6653
Self: AppearsOnTable<QS>,

diesel/src/query_dsl/load_dsl.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,8 @@ mod private {
195195
}
196196

197197
#[cfg_attr(
198-
feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes",
199-
cfg(feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes")
198+
doc_cfg,
199+
doc(cfg(feature = "i-implement-a-third-party-backend-and-opt-into-breaking-changes"))
200200
)]
201201
pub trait CompatibleType<U, DB> {
202202
type SqlType;

diesel/src/type_impls/option.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::expression::*;
55
use crate::query_builder::QueryId;
66
use crate::serialize::{self, IsNull, Output, ToSql};
77
use crate::sql_types::{is_nullable, HasSqlType, Nullable, SingleValue, SqlType};
8+
use crate::NullableExpressionMethods;
89

910
impl<T, DB> HasSqlType<Nullable<T>> for DB
1011
where
@@ -109,15 +110,25 @@ where
109110
}
110111
}
111112

112-
#[cfg(all(test, feature = "postgres"))]
113-
use crate::pg::Pg;
114-
#[cfg(all(test, feature = "postgres"))]
115-
use crate::sql_types;
113+
impl<T, DB> Selectable<DB> for Option<T>
114+
where
115+
DB: Backend,
116+
T: Selectable<DB>,
117+
crate::dsl::Nullable<T::SelectExpression>: Expression,
118+
{
119+
type SelectExpression = crate::dsl::Nullable<T::SelectExpression>;
120+
121+
fn construct_selection() -> Self::SelectExpression {
122+
T::construct_selection().nullable()
123+
}
124+
}
116125

117126
#[test]
118127
#[cfg(feature = "postgres")]
119128
fn option_to_sql() {
129+
use crate::pg::Pg;
120130
use crate::query_builder::bind_collector::ByteWrapper;
131+
use crate::sql_types;
121132

122133
type Type = sql_types::Nullable<sql_types::VarChar>;
123134

diesel_derives/tests/as_changeset.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use diesel::deserialize::FromSqlRow;
2+
use diesel::expression::AsExpression;
13
use diesel::*;
24
use helpers::*;
35
use schema::*;

diesel_derives/tests/selectable.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,31 @@ fn embedded_option() {
8686
let data = my_structs::table.select(A::as_select()).get_result(conn);
8787
assert!(data.is_err());
8888
}
89+
90+
#[test]
91+
fn embedded_option_with_nullable_field() {
92+
table! {
93+
my_structs (foo) {
94+
foo -> Integer,
95+
bar -> Nullable<Integer>,
96+
}
97+
}
98+
99+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Queryable, Selectable)]
100+
#[diesel(table_name = my_structs)]
101+
struct A {
102+
foo: i32,
103+
#[diesel(embed)]
104+
b: Option<B>,
105+
}
106+
107+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Queryable, Selectable)]
108+
#[diesel(table_name = my_structs)]
109+
struct B {
110+
bar: Option<i32>,
111+
}
112+
113+
let conn = &mut connection();
114+
let data = my_structs::table.select(A::as_select()).get_result(conn);
115+
assert!(data.is_err());
116+
}

diesel_derives/tests/tests.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#![deny(warnings)]
2-
31
#[macro_use]
42
extern crate cfg_if;
53
#[macro_use]

diesel_tests/tests/joins.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,7 @@ fn selecting_crazy_nested_joins() {
701701
assert_eq!(Ok(expected), data);
702702
}
703703

704-
fn connection_with_fixture_data_for_multitable_joins() -> (TestConnection, TestData) {
704+
pub(crate) fn connection_with_fixture_data_for_multitable_joins() -> (TestConnection, TestData) {
705705
let mut connection = connection_with_sean_and_tess_in_users_table();
706706

707707
let sean = find_user_by_name("Sean", &mut connection);
@@ -775,11 +775,11 @@ fn connection_with_fixture_data_for_multitable_joins() -> (TestConnection, TestD
775775
(connection, test_data)
776776
}
777777

778-
struct TestData {
779-
sean: User,
780-
tess: User,
781-
posts: Vec<Post>,
782-
comments: Vec<Comment>,
783-
likes: Vec<Like>,
784-
followings: Vec<Following>,
778+
pub struct TestData {
779+
pub sean: User,
780+
pub tess: User,
781+
pub posts: Vec<Post>,
782+
pub comments: Vec<Comment>,
783+
pub likes: Vec<Like>,
784+
pub followings: Vec<Following>,
785785
}

diesel_tests/tests/schema/backend_specifics.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use super::*;
22

3-
#[derive(PartialEq, Eq, Debug, Clone, Queryable, Identifiable, Associations, QueryableByName)]
3+
#[derive(
4+
PartialEq, Eq, Debug, Clone, Queryable, Identifiable, Associations, QueryableByName, Selectable,
5+
)]
46
#[diesel(belongs_to(User))]
57
#[diesel(table_name = posts)]
68
pub struct Post {

diesel_tests/tests/schema/mod.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ impl UserName {
6161
}
6262
}
6363

64-
#[derive(PartialEq, Eq, Debug, Clone, Queryable, Identifiable, Associations)]
65-
#[diesel(belongs_to(Post))]
64+
#[derive(PartialEq, Eq, Debug, Clone, Queryable, Identifiable, Associations, Selectable)]
65+
#[diesel(belongs_to(Post), table_name = comments)]
6666
pub struct Comment {
6767
id: i32,
6868
post_id: i32,
@@ -79,7 +79,9 @@ impl Comment {
7979
}
8080
}
8181

82-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Queryable, Insertable, Associations, Identifiable)]
82+
#[derive(
83+
Debug, Clone, Copy, PartialEq, Eq, Queryable, Insertable, Associations, Identifiable, Selectable,
84+
)]
8385
#[diesel(belongs_to(User))]
8486
#[diesel(belongs_to(Post))]
8587
#[diesel(table_name = followings)]
@@ -177,14 +179,16 @@ pub struct NullableColumn {
177179
value: Option<i32>,
178180
}
179181

180-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Queryable, Insertable, Identifiable, Associations)]
182+
#[derive(
183+
Debug, Clone, Copy, PartialEq, Eq, Queryable, Insertable, Identifiable, Associations, Selectable,
184+
)]
181185
#[diesel(table_name = likes)]
182-
#[diesel(primary_key(user_id, comment_id))]
186+
#[diesel(primary_key(comment_id, user_id))]
183187
#[diesel(belongs_to(User))]
184188
#[diesel(belongs_to(Comment))]
185189
pub struct Like {
186-
pub user_id: i32,
187190
pub comment_id: i32,
191+
pub user_id: i32,
188192
}
189193

190194
#[cfg(feature = "postgres")]

diesel_tests/tests/schema/postgres_specific_schema.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use super::{posts, User};
22

3-
#[derive(PartialEq, Eq, Debug, Clone, Queryable, Identifiable, Associations, QueryableByName)]
3+
#[derive(
4+
PartialEq, Eq, Debug, Clone, Queryable, Identifiable, Associations, QueryableByName, Selectable,
5+
)]
46
#[diesel(table_name = posts)]
57
#[diesel(belongs_to(User))]
68
pub struct Post {

0 commit comments

Comments
 (0)