diesel: No way to express inconsistency when deserializing for `Queryable` data since #2182
Setup
Versions
- Rust: Latest stable (1.46.0)
- Diesel: Latest master (d7ce43d2130bca434cd1acc2943a0c944d1deba4)
- Database: PostgreSQL
- Operating System: Linux
Problem Description
Since #2480 was fixed, I’ve just been giving another try at updating our codebase to the latest master (#2182).
It turns out I still couldn’t because I can’t define custom FromSqlRow impls, due to conflicting impls with downstream crates may implement trait, where Rust claims that Diesel could go ahead and implement Queryable or QueryableByName themselves on my type, which would create conflicts because of the following impls:
https://github.com/diesel-rs/diesel/blob/d7ce43d2130bca434cd1acc2943a0c944d1deba4/diesel/src/deserialize.rs#L359-L367
https://github.com/diesel-rs/diesel/blob/d7ce43d2130bca434cd1acc2943a0c944d1deba4/diesel/src/deserialize.rs#L388-L399
Btw, same error when trying to implement StaticallySizedRow because of
https://github.com/diesel-rs/diesel/blob/d7ce43d2130bca434cd1acc2943a0c944d1deba4/diesel/src/deserialize.rs#L435-L442
What are you trying to accomplish?
I want to customize a FromSqlRowImpl impl because I want to be allowed to fail on deserialization when seeing a data inconsistency, as well as easily reorganize my data in different sub-structures at the same time, while performing validation (which Queryable does not allow me to do).
What is the expected output?
compiles
What is the actual output?
error[E0119]: conflicting implementations of trait `diesel::deserialize::FromSqlRow<diesel::sql_types::Untyped, _>` for type `MyType`:
--> lib.rs:30:1
|
30 | / impl<ST, DB> diesel::deserialize::FromSqlRow<ST, DB> for MyType
31 | | where
32 | | DB: diesel::backend::Backend,
33 | | (Option<i32>, (Option<i32>, Option<i32>), Option<f64>): diesel::deserialize::FromSqlRow<ST, DB>,
... |
54 | | }
55 | | }
| |_^
|
= note: conflicting implementation in crate `diesel`:
- impl<DB, T> diesel::deserialize::FromSqlRow<diesel::sql_types::Untyped, DB> for T
where DB: diesel::backend::Backend, T: diesel::QueryableByName<DB>;
= note: downstream crates may implement trait `diesel::QueryableByName<_>` for type `MyType`
error[E0119]: conflicting implementations of trait `diesel::deserialize::StaticallySizedRow<_, _>` for type `MyType`:
--> lib.rs:57:1
|
57 | / impl<ST, DB> diesel::deserialize::StaticallySizedRow<ST, DB> for MyType
58 | | where
59 | | DB: diesel::backend::Backend,
60 | | (Option<i32>, (Option<i32>, Option<i32>), Option<f64>): diesel::deserialize::StaticallySizedRow<ST, DB>,
61 | | {
62 | | const FIELD_COUNT: usize = <(Option<i32>, (Option<i32>, Option<i32>), Option<f64>) as diesel::deserialize::StaticallySizedRow<ST, DB>>::...
63 | | }
| |_^
|
= note: conflicting implementation in crate `diesel`:
- impl<T, ST, DB> diesel::deserialize::StaticallySizedRow<ST, DB> for T
where ST: diesel::sql_types::SqlType, ST: diesel::type_impls::tuples::TupleSize, T: diesel::Queryable<ST, DB>, DB: diesel::backend::Backend;
= note: downstream crates may implement trait `diesel::Queryable<_, _>` for type `MyType`
Steps to reproduce
e.g. (for the above error)
impl<ST, DB> diesel::deserialize::FromSqlRow<ST, DB> for MyType
where
DB: diesel::backend::Backend,
(Option<i32>, (Option<i32>, Option<i32>), Option<f64>): diesel::deserialize::FromSqlRow<ST, DB>,
{
fn build_from_row<'a>(row: &impl diesel::row::Row<'a, DB>) -> diesel::deserialize::Result<Self> {
let values = <(Option<i32>, (Option<i32>, Option<i32>), Option<f64>) as diesel::deserialize::FromSqlRow<
ST,
DB,
>>::build_from_row(row)?;
Ok(Self {
delayed_by_days: values.0.map(|delay| delay.try_into()).transpose()?,
delayed_by_months: match values.1 {
(Some(nb_months), day_of_month) => Some(MonthDelay {
nb_months: nb_months.try_into()?,
post_rounding_delay: day_of_month.map(<_>::try_into).transpose()?,
}),
(None, None) => None,
(None, Some(_)) => {
return Err("Failed to deserialize MonthDelay, nb_months null and day_of_month not null".into())
}
},
some_other_field: values.2.filter(|&float| float != 0.),
})
}
}
impl<ST, DB> diesel::deserialize::StaticallySizedRow<ST, DB> for MyType
where
DB: diesel::backend::Backend,
(Option<i32>, (Option<i32>, Option<i32>), Option<f64>): diesel::deserialize::StaticallySizedRow<ST, DB>,
{
const FIELD_COUNT: usize = <(Option<i32>, (Option<i32>, Option<i32>), Option<f64>) as diesel::deserialize::StaticallySizedRow<ST, DB>>::FIELD_COUNT;
}
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 18 (18 by maintainers)
Commits related to this issue
- Make Queryable faillible One approach to fixing #2523 — committed to Ten0/diesel by Ten0 4 years ago
So it seems we could have a workaround, making this indeed not completely “necessary”.
Also I understand it may sometimes be better to put aside 1% of the cases when the large majority of the time this should never possibly return an error, in order to keep the concepts simpler and save the hassle of error handling to callers of this function.
However,
Queryable::buildimpls, where error handling would be reduced to adding anOkand a?FromSqlRow::build_from_rowimplQueryableinstead of working around it would allow for other great natural extensions, such as:#[diesel(deserialize_as = "SomeType")]could be naturally extended toTryInto, allowing e.g. to write the following:#[diesel(deserialize_as = "SomeType")]could be naturally extended to structures, allowing e.g. to solve my previous problem in the following very diesel-internal-abstract and clean way:Queryableallows to abstract and make easy and clean. (When I create a custom FromSqlRow impl, I’m actually always rewriting theimpl FromSqlRow for T where T: Queryable, just stripping theOk).#[diesel(deserialize_as = "SomeType")]improvementsSo while it may indeed not be absolutely “necessary”, I still feel like this use-case is legitimate, and that allowing
Queryable::buildto return a failure would still be the cleanest and correct approach to solving it.Given all this, do you think:
Thanks for your quick answer and for expressing your thoughts in such great detail! 🙂
From the doc, I read “The purpose of this trait is to convert from a tuple of Rust values that have been deserialized into your struct.”, but I’m not sure where there is that conceptual constraint of if being “simple”, or “one-to-one” with the fields. Besides, another part of the code seems to say it’s “typically a tuple of all of your struct’s fields”, but could also be any other Rust type that can be deserialized from this SQL type. So the way I understand it,
Queryablecould be the concept of “defining deserialization through converting from an already-deserializable Rust type to another”.That being said, even if it is a conceptual change, I think changing a concept can not break concept boundaries if both these properties are satisfied:
Error + Send + Sync, so it can be boxed into the error type used there: https://github.com/diesel-rs/diesel/blob/d7ce43d2130bca434cd1acc2943a0c944d1deba4/diesel/src/deserialize.rs#L12Because the standard library implements
TryFrom<U> for T where U: Into<T>, and becausestd::convert::InfaillibleisError + Send + Syncthey would naturally not break and keep the same behaviour.(I actually had these two answers in mind when suggesting this could be done, but I used the idea that the message was heavy enough already as an excuse for myself to not detail them. 😁)
Not sure I understand: do you consider
to not be boilerplate, even though it has to be redundant with
?
Otherwise I don’t see what you mean, as clearly these lines disappearing is by definition less boilerplate and shorter, and the conversion just using
TryIntodefinitely feels easier to grasp (especially for a new user) than all these diesel bounds and traits/types:I’m aware of this, what I was trying to express is that the fact that I’m always rewriting most of what
Queryabledoes when writing customFromSqlRowimpls highlights that this need actually fits very well with theQueryableconcept, should we extend it a little.Oh I might have mis-counted when writing my previous message. I had found (within diesel):
and the code of the macro itself. Where else ?
Or do you mean
Queryableis manually implemented in several places in external crates ? In which case I’m surprised because that is not at all something we’ve been using precisely because it wouldn’t be allowed to fail to build these structures, so it doesn’t extend our capabilities compared to derivingQueryableand selecting the appropriate fields in the right place.I don’t see how the change I suggest changes anything to how
FromSqlRowis declared or used, or to its concept. If you meant “code usingQueryable” instead, then yes it would require some change, however it would be “as small as possible” as per the question just before, and may be compensated by the fact we provide a clean solution for what used to be the customFromSqlRowimpls. (At least for us, that would have been worth).I’m not saying I have a concrete use-case where genericity is actually required, I’m saying when I read code that mentions “I only work with this specific backend and these specific SQL types”, I tend to wonder why it wouldn’t work otherwise, question on which I could spend a rather unreasonable amount of time if the answer is “because under the strict condition of non genericity, Rust will not trigger conflicting implementation errors of the kind
downstream crates may implement trait”, which means we would have to have comments describing this issue instead of having a code that’s self-explanatory. Not to mention the fact we’d have to manually match the Rust types with their SQL counterparts everytime we’d work on such an impl.Which is what we’re doing right now, that’s great, thanks a lot 😊
Same applies in the opposite direction. (😜 Joke, I see how this is completely linked to your previous sentence and I completely agree we should always be careful. It’s true though.)
Both the technical and conceptual difference would be that
FromSqlRowis responsible for the whole process of recomposing the structure from the diesel/databaseRows, whileQueryableis the part of thatFromSqlRowprocess responsible for the Rust-to-Rust reorganization/validation part of it, independently of how the SQL types are naturally and fail-safely (provided valid schema) converted to Rust types.Implementing
Queryableinstead ofFromSqlRowwould allow (and already allows) to easily safely abstract out of the SQL-to-Rust conversions, provided a valid schema.I think both would make even more sense if they allowed for easy extra validation.
Aaaand that’s it! 😅 I was already writing answers to your previous message for a while when your last one came in, and it’s already been an hour since then. 😄 What do you think ?