Search code examples
rustrust-diesel

Cannot implement trait with generic param in rust diesel


I defined a trait which has name exists_by_id_and_password. I don't want to implement it with concrete database backend, so some generic parameters were added to DB struct. but compiler reported an error:

type mismatch resolving `<C as Connection>::Backend == Pg`
expected type parameter `B`
           found struct `Pg`
required because of the requirements on the impl of `LoadQuery<C, bool>` for `diesel::query_builder::SelectStatement<(), query_builder::select_clause::SelectClause<Exists<diesel::query_builder::SelectStatement<table, query_builder::select_clause::DefaultSelectClause, query_builder::distinct_clause::NoDistinctClause, query_builder::where_clause::WhereClause<And<diesel::expression::operators::Eq<columns::username, diesel::expression::bound::Bound<diesel::sql_types::Text, std::string::String>>, diesel::expression::operators::Eq<columns::password, diesel::expression::bound::Bound<diesel::sql_types::Text, std::string::String>>>>>>>>`

It is weird that the error mentioned Pg, I think it is caused by the schema which is generated by diesel, because there isn't any code related to postgres in my code, except cargo.toml, I import diesel with features=["postgres"].

below is my code:

use diesel::select;
    
use crate::authenticator::AuthDB;
use crate::diesel::dsl::exists;
use crate::diesel::prelude::*;
use crate::schema::users;
    
pub struct DB<C, B>
where
    C: diesel::Connection<Backend = B>,
    B: diesel::backend::Backend,
{
    conn: C,
}
    
impl<C, B> AuthDB for DB<C, B>
where
    C: diesel::Connection<Backend = B>,
    B: diesel::backend::Backend + diesel::sql_types::HasSqlType<diesel::sql_types::Bool>,
{
    fn exists_by_id_and_password(
        &self,
        id: String,
        password: String,
    ) -> Result<bool, crate::error::Error> {
        Ok(select(exists(
            users::dsl::users.filter(
                users::dsl::username
                    .eq(id)
                    .and(users::dsl::password.eq(password)),
            ),
        ))
        .get_result::<bool>(&self.conn)?)
    }
}

I want to know, it is possible to implement the trait without concrete backend type


Solution

  • Rustc does already emit as part of your error message what's the underlying problem:

    required because of the requirements on the impl of `LoadQuery<C, bool>` for `diesel::query_builder::SelectStatement<(), query_builder::select_clause::SelectClause<Exists<diesel::query_builder::SelectStatement<table, query_builder::select_clause::DefaultSelectClause, query_builder::distinct_clause::NoDistinctClause, query_builder::where_clause::WhereClause<And<diesel::expression::operators::Eq<columns::username, diesel::expression::bound::Bound<diesel::sql_types::Text, std::string::String>>, diesel::expression::operators::Eq<columns::password, diesel::expression::bound::Bound<diesel::sql_types::Text, std::string::String>>>>>>>>`
    

    This can be read as LoadQuery<C, bool> needs to be implemented for your query (that long type represents your query at compile time).

    Now rustc and especially the error reporting part is really clever. It is aware of possible existing impl and tries to be helpful by suggesting a "simple" solution here. That's why it is suggesting that Backend should be Pg because for that the only impl of that trait is found. It's the only existing impl because you only enabled one backend (the postgres backend), I expect that the error message changes if you change add more backends in your Cargo.toml via diesels feature flags.

    So how to write that trait bound in such a way that it supports all backends instead. The compiler already hints a direction with that required because … line. You need to state that LoadQuery is implemented for your query. Now unfortunately you cannot just copy the corresponding type from the error message, as this is not a type available in the public API. So this seems like a dead end. The documentation lists two potential impls of the LoadQuery trait. We are interested in the second one, as our type is not SqlQuery. Now looking over the required bounds we can observe the following things:

    • Conn: Connection => already fulfilled,
    • Conn::Backend: HasSqlType<T::SqlType> => already fulfilled,
    • T: AsQuery + RunQueryDsl<Conn> => This requires that we could name T, which is our query type. That's not possible, so let's ignore that for now.
    • T::Query: QueryFragment<Conn::Backend> + QueryId => Same as the last bound, requires to name T.
    • U: Queryable<T::SqlType, Conn::Backend> => That one is interesting.

    So let's have a detailed look at the `U: Queryable<T::SqlType, Conn::Backend> bound:

    • U is the type returned by your query. So bool in this case.
    • T::SqlType is the sql type returned by the query, so a type from diesel::sql_types. exists returns a boolean, so diesel::sql_type::Bool
    • Conn::Backend is your generic B type.

    That means we need the verify that bool: Queryable<diesels::sql_type::Bool, B>. There is an existing impl in diesel which requires that bool: FromSql<diesel::sql_types::Bool, B>. Again there are existing impls for that as well, but note that those are only implemented for a specific backend. So what happens here is that rustc sees that impl and concludes that this is the only available impl. This then means that B must be Pg otherwise this function wouldn't be valid. Now there is another way to convince rustc that this trait impl is there, and that is by adding the it as trait bound. This gives you the following working impl:

    impl<C, B> AuthDB for DB<C, B>
    where
        C: diesel::Connection<Backend = B>,
        B: diesel::backend::Backend + diesel::sql_types::HasSqlType<diesel::sql_types::Bool>,
        bool: diesel::deserialize::FromSql<diesel::sql_types::Bool, B>
    {
        fn exists_by_id_and_password(
            &self,
            id: String,
            password: String,
        ) -> QueryResult<bool> {
            Ok(diesel::select(diesel::dsl::exists(
                users::dsl::users.filter(
                    users::dsl::username
                        .eq(id)
                        .and(users::dsl::password.eq(password)),
                ),
            ))
            .get_result::<bool>(&self.conn)?)
        }
    }
    
    

    Side note:

    If I enable an additional backend I get the following error message

    error[E0277]: the trait bound `bool: FromSql<Bool, B>` is not satisfied
      --> src/main.rs:39:10
       |
    39 |         .get_result::<bool>(&self.conn)?)
       |          ^^^^^^^^^^ the trait `FromSql<Bool, B>` is not implemented for `bool`
       |
       = note: required because of the requirements on the impl of `Queryable<Bool, B>` for `bool`
       = note: required because of the requirements on the impl of `LoadQuery<C, bool>` for `diesel::query_builder::SelectStatement<(), query_builder::select_clause::SelectClause<Exists<diesel::query_builder::SelectStatement<table, query_builder::select_clause::DefaultSelectClause, query_builder::distinct_clause::NoDistinctClause, query_builder::where_clause::WhereClause<And<diesel::expression::operators::Eq<columns::username, diesel::expression::bound::Bound<diesel::sql_types::Text, std::string::String>>, diesel::expression::operators::Eq<columns::password, diesel::expression::bound::Bound<diesel::sql_types::Text, std::string::String>>>>>>>>`
    help: consider extending the `where` bound, but there might be an alternative better way to express this requirement
       |
    25 |     B: diesel::backend::Backend + diesel::sql_types::HasSqlType<diesel::sql_types::Bool>, bool: FromSql<Bool, B>
       |                                                                                         ~~~~~~~~~~~~~~~~~~~~~~~~
    
    

    This exactly suggests the trait bound described above.


    Two other semi-releated side notes:

    • There is no reason to explicitly have the backend as second parameter to your DB struct. You can simply access it at any point via C::Backend.
    • Obviously you should not store a password as plaintext in your database