-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
When using .optional()
, convert a QueryBuilderError
into None
#4019
When using .optional()
, convert a QueryBuilderError
into None
#4019
Conversation
- Related discussion: diesel-rs#4018
@@ -234,6 +234,28 @@ fn update_with_no_changes() { | |||
.unwrap(); | |||
} | |||
|
|||
#[test] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this test is just copied from the one above, it might make sense to add the new line just above the panic one above. Or just keep it as its own test, up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to have a separate test for this. These tests are quite fast so there is no point to worry about anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The structure of the change looks ok, although I'm quite unhappy with the new matching there as it allows a potentially large number of different error. That's not something I would like to support there.
diesel/src/result.rs
Outdated
@@ -284,6 +292,7 @@ impl<T> OptionalExtension<T> for QueryResult<T> { | |||
match self { | |||
Ok(value) => Ok(Some(value)), | |||
Err(Error::NotFound) => Ok(None), | |||
Err(Error::QueryBuilderError(_)) => Ok(None), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't want to allow such a wide match here. We should only allow that one specific error that happens for empty updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a String, should I match that exact string then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to make that into an error that we may match on, then we probably want to turn it into a some form of typed error: it's less error prone (typos, error message updates...), and does not imply the additional runtime cost that string comparisons would imply.
So if it's currently a String
(or &'static str
rather) that we then turn into a Box<dyn StdError + ...>
, we probably want to create a dedicated type for it (with the same message as before in its Display impl) and match it with .is::<_>()
, as is done with UnexpectedNullError
in the QueryableByName
code for Option
.
Lines 393 to 403 in df89687
/// An unexpected `NULL` was encountered during deserialization | |
#[derive(Debug, Clone, Copy)] | |
pub struct UnexpectedNullError; | |
impl fmt::Display for UnexpectedNullError { | |
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
write!(f, "Unexpected null for non-null column") | |
} | |
} | |
impl StdError for UnexpectedNullError {} |
diesel/diesel/src/type_impls/option.rs
Line 94 in df89687
Err(e) if e.is::<crate::result::UnexpectedNullError>() => Ok(None), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking the same, I'd be glad to do this if its desired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for tackling this topic and opening this PR! 😊
It does indeed look like it would be useful to be able to catch this error in an ergonomic manner. 🙂
However mixing it within the existing .optional()
extension feels odd to me: it feels like it might make it harder to use .optional()
without mixing in potential unrelated/unexpected scenarios into None
.
I find that it is typically better to avoid falling back from larger error categories than may really happen, as this may turn into silent fails, resulting in hard-to-debug issues.
As far as I'm concerned we have many use-cases where we use the update(table.find(id)).filter(make_sure_that_state_is_ok()).optional()
combination, where make_sure_that_state_is_ok()
is for us to get single-query happy path, and then if None
re-query to know whether the ID was wrong or the state was wrong and return the appropriate user error.
However we have no use-case such as the one described by this issue.
This tends to make me believe that it's better to be able to guarantee that .optional
really only catches the scenario I'm personally always looking for where make_sure_that_state_is_ok()
is FALSE
, to avoid potentially enforcing upon users the unexpected silent fail scenario described above, so I'd probably prefer if it stayed dedicated to the current meaning it has. I think a design that wouldn't have this downside of too-large-matching, but would still have the same ergonomics as the solution currently proposed by this PR, would be introducing a separate function similar to .optional()
that catches precisely that empty changeset scenario. (.optional_empty_changeset()
?)
However as weiznich pointed out here, .optional()
is just an extension trait that is reasonably easy to re-create outside of Diesel. (We do have a few similar extension traits ourselves, such as .get_only_result()
which is a get_result
that errors if there are multiple results, which is an extremely common case for us, prevents bugs where one would forget to write the appropriate .find
or .filter
clause (again because hard errors are preferable to silent fails that would cause incorrect behavior)... and have found that having that in our diesel_helpers
crate doesn't generate any friction.)
So overall I have no strong opinion on whether the extension trait itself should even live within Diesel itself - I guess it depends on really how common this use-case is, as this is a compromise between providing the feature and API&Doc size/maintenance cost, and all I can say is I personally haven't encountered this use-case.
However what seems pretty clear that we should do within Diesel is adding a typed error for this and exporting it in a similar way to UnexpectedNullError
, so that users can actually match this error and construct any extension trait related to this that they see fit, in an idiomatic and ~typed manner, (that is, not by matching on a string which the API does not guarantee may not change and is slow).
I'd be fine with this also, although I'd probably prefer
Its at least common in our case, to build form large form objects without knowing beforehand which fields we're receiving are updated, because they come from data sources outside our control. We've added some hacky code to check individually that all fields are not |
Ok so next steps for this PR:
|
Sorry, missclick |
|
||
/// Expected when an update has no changes to save | ||
#[derive(Debug, Clone, Copy)] | ||
pub struct EmptyChangeset; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if set should be capitalized or not.
@@ -228,10 +227,30 @@ fn update_with_no_changes() { | |||
name: None, | |||
hair_color: None, | |||
}; | |||
update(users::table) | |||
let update_result = update(users::table).set(&changes).execute(connection); | |||
assert!(update_result.is_err()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed this from a panic check, to an is_err
check, since this now returns QueryBuilder(EmptyChangeset)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks fine for me beside the comment about the breaking change to the OptionalExtension
trait. I think it's safer to just introduce a new trait for that + reexport that from the prelude.
@@ -234,6 +234,28 @@ fn update_with_no_changes() { | |||
.unwrap(); | |||
} | |||
|
|||
#[test] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to have a separate test for this. These tests are quite fast so there is no point to worry about anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good for me with the small documentation fix applied.
diesel/src/result.rs
Outdated
@@ -45,6 +45,8 @@ pub enum Error { | |||
/// An example of when this error could occur is if you are attempting to | |||
/// construct an update statement with no changes (e.g. all fields on the | |||
/// struct are `None`). | |||
/// | |||
/// When using `optional_empty_changeset`, this error is turned into `None`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor note: That comment is not correct anymore. It might be a got idea to move this comment to the new error type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
K, will do.
@weiznich can we please have a minor release for this? |
@JMLX42 As this is a new feature it needs to wait until the next diesel feature release happens. As for the timeline for such a release: This is a free time project, so that will happen as soon as it is ready. We generally do not give ETA's there. You can contribute or sponsor the development to make such a release happen faster. |
This is my first diesel PR, I apologize in advance for any issues. Let me know of any documentation I missed, tests I should add, or anything I can do to improve it.
I also had to comment a few tests in
bin/test
to get this to pass, which leads me to believe themain
branch has some unaddressed errors which might fail CI before reaching the test I added.Related discussion: #4018