What is this reoccurring generic trait?

I have seen in the Diesel library the following trait definition:

/// The `execute` method
///
/// [`RunQueryDsl`]: crate::RunQueryDsl
pub trait ExecuteDsl<Conn: Connection<Backend = DB>, DB: Backend = <Conn as Connection>::Backend>:
    Sized
{
    /// Execute this command
    fn execute(query: Self, conn: &mut Conn) -> QueryResult<usize>;
}

I was really wondering how this <conn: Connection<Backend = DB>, DB: Backend = <Conn as Connection>::Backend> is to understand especially because its self referencing, and how does it work?

What is this expression <Conn as Connection>::XXX, is it a type cast and why doesnt need it generic declarations on it too Connection<Backend=...> too?

The syntax <A as B>::C means:

  • Find the impl block that implements the trait B for the type A.
  • Find the type C = ...; declaration inside that impl block.
  • Replace <A as B>::C with the right-hand-side of type C = ...;
11 Likes

It's supposed to be read like this:

The ExecuteDsl trait receives two generic type parameters: Conn and DB. Conn must implement the Connection trait (I won't go on to describe the Connection trait, but you can follow along the documentation here), which itself is generic and receives a Backend generic type parameter, which defaults to DB; DB must implement the Backend trait, and defaults to the associated Backend type from Conn casted as an implementor of Connection.

I don't know the specifics, but the syntax Trait<GenericType> is used in a sort of type constructor, while the <Conn as Connection>::XXX is referring to the "instance".

2 Likes

Ah nice. I was somehow guessing the part <Conn as Conneciton> refers to the implementation part.

My follow up question is, why does it need the DB: ... part and not only:

ExecuteDsl<Conn: Connection>: Sized
?

Probably because of this blanket implementation, which makes use of the DB generic type parameter for the QueryFragment trait:

impl<Conn, DB, T> ExecuteDsl<Conn, DB> for T
where
    Conn: Connection<Backend = DB>,
    DB: Backend,
    T: QueryFragment<DB> + QueryId,
{
    fn execute(query: T, conn: &mut Conn) -> Result<usize, Error> {
        conn.execute_returning_count(&query)
    }
}
1 Like

It doesn't actually look like the DB parameter is necessary. For example, if it wasn't there, that blanket impl could still be written:

impl<Conn, T> ExecuteDsl<Conn> for T
where
    Conn: Connection,
    <Conn as Connection>::Backend: Backend,
    T: QueryFragment<<Conn as Connection>::Backend> + QueryId,
{
    fn execute(query: T, conn: &mut Conn) -> Result<usize, Error> {
        conn.execute_returning_count(&query)
    }
}
2 Likes

That's a good point. On the other hand, I think it's less legible and at first glance might not be obvious that <Conn as Connection>::Backend is referring to the database (In a sort of an attempt to try to justify the current API design).

I believe you can still write this:

impl<Conn, DB, T> ExecuteDsl<Conn> for T
where
    Conn: Connection<Backend = DB>,
    DB: Backend,
    T: QueryFragment<DB> + QueryId,
{
    fn execute(query: T, conn: &mut Conn) -> Result<usize, Error> {
        conn.execute_returning_count(&query)
    }
}
1 Like

Then I have run out of possible explanations for the current API design :laughing:

That trait design is necessary for the other non-blanket implementations of this trait.

The blanket implementation covers cases where you can execute a specific database query for a specific database backend (and there for all possible connection implementations there). It does not cover cases where something cannot be expressed as simple SQL query for that backend. An example here is that T represents some sort of batch insert statement, which is not supported by some database systems. By having Conn and DB as generic parameters diesel can now say:

  • Use this "workaround" for all connections of a specific backend by just "rewriting" the query (by keeping Conn generic, but restricting DB to a specific backend)
  • Or use that other specific workaround that only works with a specific connection implementation (by using a specific connection type instead).
2 Likes