Skip to content
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

Add some additional checks to #[derive(Selectable)] to ensure field #3359

Merged
merged 5 commits into from
Mar 1, 2023

Conversation

weiznich
Copy link
Member

@weiznich weiznich commented Oct 2, 2022

compatibility

We generate an additional function with trait bounds for each fields to check whether a certain rust field type is compatible with the underlying database type. This greatly improves error messages for potential type mismatches, as the user now gets an error message that points to the field causing the problem + which has a pretty clear meaning (stating that FromSql<…> is not implemented).

Unfortunaly that's not a perfect solution as we need to check against concrete backend implementation. This PR solves this by hard-coding all three out of the box supported backends (behind the corresponding feature flags). That works for cases where users want to have compatibility with all enabled backends, but there are certainly use-cases where this is not the case. We might need some opt-in/opt-out system here to prevent this from being a breaking change or making it impossible to support valid use-cases. On the one hand we might want to have this as default as the improved error messages is something which is really helpful for new users, on the other hand I cannot see how that would be possible without breaking changes.

Another down-side of this approach is that it generates basically duplicated error messages when more than one backend is enabled.

This is currently marked as Draft as I see that more as request for comments due to the various problems outlined above. I really would like to see something like that in stable diesel as this would improve the usability quite significantly in my opinion. (We would just need to tell everyone to use Selectable and after that at least many cryptic errors will go away)

@weiznich weiznich requested a review from a team October 2, 2022 17:02
Copy link
Member

@Ten0 Ten0 left a comment

Choose a reason for hiding this comment

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

What makes it work with generics? (#2962)


if cfg!(feature = "postgres") {
out.push(quote::quote_spanned! {span=>
#field_ty: diesel::prelude::Queryable<diesel::dsl::SqlTypeOf<#ty>, diesel::pg::Pg>
Copy link
Member

@Ten0 Ten0 Oct 7, 2022

Choose a reason for hiding this comment

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

Isn't the Queryable requirement a bit harsh? (since it's essentially a helper to implement FromSqlRow shouldn't we require that instead?)

@weiznich
Copy link
Member Author

What makes it work with generics? (#2962)

Currently it just skips embedded fields. So it handles generics, but not handling them at all…

Isn't the Queryable requirement a bit harsh? (since it's essentially a helper to implement FromSqlRow shouldn't we require that instead?)

Yes that's a good idea.

compatibility

We generate an additional function with trait bounds for each fields to
check whether a certain rust field type is compatible with the
underlying database type. This greatly improves error messages for
potential type mismatches, as the user now gets an error message that
points to the field causing the problem + which has a pretty clear
meaning (stating that `FromSql<…>` is not implemented).

Unfortunaly that's not a perfect solution as we need to check
against **concrete** backend implementation. This PR solves this by
hard-coding all three out of the box supported backends (behind the
corresponding feature flags). That works for cases where users want to
have compatibility with all enabled backends, but there are certainly
use-cases where this is not the case. We might need some opt-in/opt-out
system here to prevent this from being a breaking change or making it
impossible to support valid use-cases. On the one hand we might want to
have this as default as the improved error messages is something which
is really helpful for new users, on the other hand I cannot see how that
would be possible without breaking changes.

Another down-side of this approach is that it generates basically
duplicated error messages when more than one backend is enabled.
* generate the corresponding lifetimes for `Cow<'…>` using higher ranked
lifetimes
* Filter out any embedded fields as they are generic over the
database (they are checked on their own anyway so that should be fine)
Comment on lines 33 to 34
/// time. This drastically improves the quality of the generated error messages by pointing
/// to concrete mismatches at field level.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think, you also should describe downsides of this approach, otherwise it is unclear, why you need to manually enable great compiler messages. Also, it is unclear why the user should specify backend, because the struct is the same for all backends and isn't depend on them.

It would also be useful, if you include the example of the generated code that implements the check, maybe under the implementation note heading.

Copy link
Member Author

Choose a reason for hiding this comment

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

For the downsides: There are no other downsides that this is backend specific, which should be obvious from the way you enable this.

For why not enabling that by default:

  • It would be a breaking change
  • Even if it wouldn't be a breaking change, there is no way to make this work without knowing the concrete backend (which sometimes might come from a third party crate).

For the generated code: That's really uninteresting as it's just another (well placed) trait bound.

I will update the documentation to clarify why the concrete backend is required. That's because deserialization is generic over backends and rustc fails to see through this for the generic case.

Copy link
Contributor

Choose a reason for hiding this comment

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

At least example of generated code here in comment could be used if someone would take its time to think about possible solutions of mentioned problems -- just to understand how exactly the solution worked right now.

Copy link
Member Author

Choose a reason for hiding this comment

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

The following usage

#[derive(Selectable, Queryable)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct User {
    id: String,
    name: i32,
}

generates the following rust code

impl<__DB: diesel::backend::Backend> Selectable<__DB> for User {
    type SelectExpression = (users::id, users::name);
    fn construct_selection() -> Self::SelectExpression {
        (users::id, users::name)
    }
}
fn _check_field_compatibility()
where
    String: diesel::deserialize::FromSqlRow<diesel::dsl::SqlTypeOf<users::id>, diesel::pg::Pg>,
    i32: diesel::deserialize::FromSqlRow<diesel::dsl::SqlTypeOf<users::name>, diesel::pg::Pg>,
{
}

Without the usage of #[diesel(check_for_backend()] the function is not generated at all.

Copy link
Contributor

@Mingun Mingun Feb 24, 2023

Choose a reason for hiding this comment

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

Ok, it seems that the problem with the generic check is also lied in the way, that types in schema module is backend dependent, right? I tried to illustrate the diesel approach of mapping SQL->Rust types here and it seems so.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, that's not the case. This playground is closer to what diesel does. Notability the sql types can be shared between different backends (although there are also backend dependent types there). This makes the schema.rs module backend independent, as long as you only use types that are supported by all backends.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, you show the same thing. I'd just try to show the generic case where backend type markers are different. Of course, in reality many of them are shared.

Copy link
Member Author

Choose a reason for hiding this comment

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

While that's technically true, it's an important concept that they are mostly shared in practice.

* Explicitly accept for which backend the relevant code is generated,
which makes this an opt-in feature and allows the use with third party
backends as WELL
* Add documentation
@weiznich weiznich marked this pull request as ready for review February 24, 2023 13:11
@weiznich weiznich requested a review from a team February 24, 2023 13:11
@Mingun
Copy link
Contributor

Mingun commented Feb 24, 2023

Another down-side of this approach is that it generates basically duplicated error messages when more than one backend is enabled.

Probably this could be solved if generate something like that:

// The implementation of `Check` conceptually is this,
// but supports tuples in DBs parameter
type Check<Column, DBs> = FromSqlRow<SqlTypeOf<Column>::SqlType, DBs>;

// Check for `check_for_backend(Pg, Mysql)`
fn check()
where
    String: Check<table::str_column, (Pg, Mysql)>,
    i32: Check<table::int_column, (Pg, Mysql)>,
{}

@weiznich
Copy link
Member Author

Probably this could be solved if generate something like that:

I would guess that this results in the same problem as the current approach without check_for_backend(…): Rustc is not able to decide which of the impls is meant for the tuple + generic impl case, so it will not suggest a concrete impl, resulting it in suggesting an impl far of the actual error. In that case the error message would likely only mention that i32: Check<table::str_column, (Pg, Mysql)> is not satisfied, but not that that's because of i32 is missing the relevant FromSql impls.

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.

3 participants