Skip to content

Make BoolExpressionMethods not always return a Nullable<bool>#2597

Merged
weiznich merged 12 commits intodiesel-rs:masterfrom
Ten0:fix_2589
Jan 25, 2021
Merged

Make BoolExpressionMethods not always return a Nullable<bool>#2597
weiznich merged 12 commits intodiesel-rs:masterfrom
Ten0:fix_2589

Conversation

@Ten0
Copy link
Member

@Ten0 Ten0 commented Dec 18, 2020

Fixes #2589

With new ideas from #2589 and specifically #2589 (comment), it turns out we can solve the issue encountered when writing 7ff5e63 of "not all methods had an equivalent type in diesel::dsl" in a simpler way, without making "BoolExpressionMethods::and (and or) unconditonally nullable" nor being a breaking change in most places.

As a consequence, this mostly reverts the part of 7ff5e63 that was focusing on this change, while introducing typing suggested in #2589 (comment), reducing a bit the overall amount of code.

 - Fix corresponding usages in diesel
 - Fix test

 Note: tests do not pass atm due to a massive arrival of `Cannot infer
 type` errors.
to allow compiler to always infer ST when using `.and` or `.or`
@Ten0 Ten0 changed the title Make BoolExpressionMethods not always return a nullable bool Make BoolExpressionMethods not always return a Nullable<bool> Dec 18, 2020
@Ten0 Ten0 marked this pull request as ready for review December 18, 2020 21:37
@Ten0 Ten0 requested a review from weiznich December 19, 2020 14:04
Copy link
Member

@pksunkara pksunkara left a comment

Choose a reason for hiding this comment

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

I might be misunderstanding something, but don't we need to revert the following?

Otherwise looks good.

@pksunkara
Copy link
Member

I am still having trouble using this PR.

If there's a column email VARCHAR (it is nullable), why does email.eq(...) produce Nullable<Bool> for boxed expression? Shouldn't .eq always result in Bool?

error[E0271]: type mismatch resolving `<Grouped<diesel::expression::operators::And<Box<dyn BoxableExpression<table, Pg, SqlType = Bool>>, Grouped<diesel::expression::operators::Eq<columns::email, diesel::expression::bound::Bound<diesel::sql_types::Nullable<Text>, Option<&str>>>>>> as diesel::Expression>::SqlType == Bool`
  --> src/main.rs:25:17
   |
25 |     statement = Box::new(statement.and(users::email.eq(Some("John"))));
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `Bool`, found struct `diesel::sql_types::Nullable`
   |
   = note: expected struct `Bool`
              found struct `diesel::sql_types::Nullable<Bool>`
   = note: required for the cast to the object type `dyn BoxableExpression<table, Pg, SqlType = Bool>`

@Ten0
Copy link
Member Author

Ten0 commented Dec 19, 2020

If there's a column email VARCHAR (it is nullable), why does email.eq(...) produce Nullable<Bool> for boxed expression? Shouldn't .eq always result in Bool?

In SQL, if one of the values of an operator such as AND or OR is null, the result may be null, so this is the correct mapping. It was incorrectly expressed in Diesel 1 due to #104 being pretty difficult to really solve, and which was only solved recently (#2182 (comment)). (NULL being deserialized as false ended up causing other issues in the deserialization of Option (#2274/#2182 (comment)), as well as some silent fails when #1459/#2001 happened (#2063), so it appears deserializing as false is not a correct approach).

Consequently, the behavior you're looking for should be achieved by using an SQL operator that has this behavior internally to your database, e.g. IS DISTINCT FROM for PostgreSQL (doc: postgres, diesel).

Note: it looks like as declared in Diesel, this one does not know it can never return null regardless of whether the input values are NULL:

infix_operator!(IsDistinctFrom, " IS DISTINCT FROM ", backend: Pg);

return_ty = (
$crate::sql_types::is_nullable::MaybeNullable<
$crate::sql_types::is_nullable::IsOneNullable<
<T as $crate::expression::Expression>::SqlType,
<U as $crate::expression::Expression>::SqlType
>,
$($return_ty)::*
>
),

That would be a separate issue to solve.

@pksunkara
Copy link
Member

pksunkara commented Dec 19, 2020

In SQL, if one of the values of an operator such as AND or OR is null, the result may be null, so this is the correct mapping.

But we are not doing AND or OR here. I am doing eq which is checking for equality.

I understand eq doesn't give me true when comparing two NULLs, but it still gives me a boolean and not null.

@weiznich
Copy link
Member

I understand eq doesn't give me true when comparing two NULLs, but it still gives me a boolean and not null.

Thats not correct. 1 = NULL resolves to NULL.

@Ten0
Copy link
Member Author

Ten0 commented Dec 19, 2020

In SQL, if one of the values of an operator such as AND or OR is null, the result may be null, so this is the correct mapping.

But we are not doing AND or OR here. I am doing eq which is checking for equality.

I understand eq doesn't give me true when comparing two NULLs, but it still gives me a boolean and not null.

I meant = as well yes, which is why I pointed to IS DISTINCT FROM.

Copy link
Member

@weiznich weiznich left a comment

Choose a reason for hiding this comment

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

Looks good but we should talk about that minor nit regarding to the not function.

pub fn not<T>(expr: T) -> not<T>
where
T: AsExpression<Nullable<Bool>>,
T: AsExpression<Bool>,
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if that should only accept non nullable values. I think accepting both Bool and Nullable<Bool> here is more meaningful. On the other hand that could potentially lead to situations where rustc cannot infer the correct type for ST from AsExpression here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm that looks like kind of the same problem as and/or no? Where if we enforce that it takes an AsExpression<Nullable<Bool>> we'll never be able to make it return a non-nullable bool?

Additionally, not all expressions of SqlType Bool implement AsExpression<Nullable<Bool>> anymore, because otherwise this creates type inference issues with and/or: #2597 (comment)
Hence the following does not compile anymore if we do this: not(name.eq("Sean") (from tests)

Finally, I believe the behavior of not on Nullable<Bool> is a bit of a trap in that NULL values result in NULL, so they do not change "truthiness" with regards to e.g. a filter, which may lead to nasty bugs in case this kind of confusion is made and the compiler does not prevent it.

So overall I believe we should keep this behavior where we only accept non-nullable values here.

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 accepting both Bool and Nullable

I had encountered a situation where I want to store the same. Is there a way we can specify that?

Copy link
Member

@weiznich weiznich Jan 7, 2021

Choose a reason for hiding this comment

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

We must accept both variants (Bool and Nullable<Bool>) here otherwise it's not possible to write queries like not(foo.and(bar)) or even not(foo.eq(bar)) anymore in cases one of the arguments is Nullable<Bool>

Edit: @pksunkara Seems like you've added your comment while I typed mine. Yes we need this method to automatically accept both variants, otherwise we would restrict quite a lot of possible queries using not.

Copy link
Member Author

Choose a reason for hiding this comment

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

I had encountered a situation where I want to store the same

Not sure what you mean by "the same".

We must accept both variants (Bool and Nullable<Bool>) here otherwise it's not possible to write queries like not(foo.and(bar)) or even not(foo.eq(bar)) anymore in cases one of the arguments is Nullable<Bool>

So I thought it maybe wasn't a good idea to have not support this implicitly, as this may easily lead to nasty bugs:

I believe the behavior of not on Nullable<Bool> is a bit of a trap in that NULL values result in NULL, so they do not change "truthiness" with regards to e.g. a filter, which may lead to nasty bugs in case this kind of confusion is made and the compiler does not prevent it.

Example:

table.filter(not(foo.eq(bar)))

Where foo = true and bar = NULL, the lines won't show, because not won't change the "truthiness" of the expression, which is a bit counter-intuitive.

Also, our codebase is rather large and we have zero such patterns.

Given that, do you still believe it's important for not to support this? (and we couldn't be happy with a separate nullable_not?)

Copy link
Member

Choose a reason for hiding this comment

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

I still believe that not should support that as this is just the behavior of the underlying SQL operator. I feel that we had this discussion already about a few operators (for example eq in another context). Every time the final answer was: We follow the semantic of the underlying SQL operator as we do not try to hide complexity at this level of abstraction. Hiding this complexity can be done in a crate build on top of diesel, providing it owns set of operators that is not based on SQL in my opinion.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree. Done in df8b5e5.

@Ten0 Ten0 force-pushed the fix_2589 branch 2 times, most recently from 9319d74 to 4847044 Compare January 22, 2021 15:25
@weiznich weiznich merged commit d2af226 into diesel-rs:master Jan 25, 2021
@Ten0 Ten0 deleted the fix_2589 branch January 25, 2021 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

master: a.and(b) is nullable even though a and b are non nullable

4 participants