Search code examples
rustrust-diesel

Diesel: BoxableExpressions generic over a table and its joins?


I'm trying to construct some filters at runtime which can be applied either to a table tunnel or to tunnel LEFT OUTER JOIN connection ON (tunnel.id = connection.tunnel_id).

The tables are defined like this:


// Define the tunnel table and struct
table! {
    #[allow(unused_imports)]
    use diesel::sql_types::*;
    tunnel (id) {
        id -> BigInt,
        name -> Text,
    }
}
#[derive(Queryable, Identifiable, Clone, Debug, PartialEq, Eq)]
#[table_name = "tunnel"]
pub struct Tunnel {
    pub id: i64,
    pub name: String,
}

// Define the connection table and struct
table! {
    #[allow(unused_imports)]
    use diesel::sql_types::*;
    connection(id) {
        id -> BigInt,
        tunnel_id -> BigInt,
    }
}

#[derive(Debug, Associations, Identifiable, Queryable)]
#[table_name = "connection"]
#[primary_key(id)]
#[belongs_to(Tunnel)]
pub struct Connection {
    pub id: i64,
    pub tunnel_id: i64,
}

joinable!(connection -> tunnel(tunnel_id));
allow_tables_to_appear_in_same_query!(connection, tunnel);

I can write a function that constructs dynamic for either the single table:

fn filters_t(
    name: &'static str,
) -> Vec<Box<dyn BoxableExpression<tunnel::table, Pg, SqlType = Bool>>> {
    let mut wheres: Vec<Box<dyn BoxableExpression<tunnel::table, Pg, SqlType = Bool>>> = Vec::new();
    wheres.push(Box::new(tunnel::name.eq(name)));
    wheres
}

Or for the join:

pub type TunnelJoinConnection = JoinOn<
    Join<tunnel::table, connection::table, LeftOuter>,
    Eq<Nullable<connection::columns::tunnel_id>, Nullable<tunnel::columns::id>>,
>;

fn filters_j(
    name: &'static str,
) -> Vec<Box<dyn BoxableExpression<TunnelJoinConnection, Pg, SqlType = Bool>>> {
    let mut wheres: Vec<Box<dyn BoxableExpression<TunnelJoinConnection, Pg, SqlType = Bool>>> =
        Vec::new();
    wheres.push(Box::new(tunnel::name.eq(name)));
    wheres
}

Note that the two filter functions have the exact same function body, so I should be able to make a generic function that achieves both of them. But I get an error when I try to make it generic.

fn filters<T>(name: &'static str) -> Vec<Box<dyn BoxableExpression<T, Pg, SqlType = Bool>>>
where
    T: AppearsInFromClause<tunnel::table, Count = Once>,
{
    vec![Box::new(tunnel::name.eq(name))]
}

The error is

   |
85 |     vec![Box::new(tunnel::name.eq(name))]
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `diesel::SelectableExpression<T>` is not implemented for `tunnel::columns::name`
   |
   = note: required because of the requirements on the impl of `diesel::SelectableExpression<T>` for `diesel::expression::operators::Eq<tunnel::columns::name, diesel::expression::bound::Bound<diesel::sql_types::Text, &str>>`
   = note: required because of the requirements on the impl of `diesel::BoxableExpression<T, diesel::pg::Pg>` for `diesel::expression::operators::Eq<tunnel::columns::name, diesel::expression::bound::Bound<diesel::sql_types::Text, &str>>`
   = note: required for the cast to the object type `dyn diesel::BoxableExpression<T, diesel::pg::Pg, SqlType = diesel::sql_types::Bool>`

Minimal example here, you can clone that minimal example and run cargo check yourself to see the error.


Solution

  • This can be be fixed with one small change:

    fn filters<T>(name: &'static str) -> Vec<Box<dyn BoxableExpression<T, Pg, SqlType = Bool>>>
    where
        diesel::dsl::Eq<tunnel::name, &'static str>: BoxableExpression<T, Pg, SqlType = Bool>,
    {
        vec![Box::new(tunnel::name.eq(name))]
    }
    

    Basically you need to assert at compile time that your boxed expression really implements BoxableExpression<T, Pg, SqlType = Bool> for all possible T's. If there is only a specific T that is checked by rustc, for the generic case this needs to be written out explicitly. The diesel::dsl::Eq helper type is a type level constructor for the type returned by tunnel::name.eq(name). This implies you will need a similar clause for each expression you add to the list.

    Another unrelated note:

    pub type TunnelJoinConnection = JoinOn<
        Join<tunnel::table, connection::table, LeftOuter>,
        Eq<Nullable<connection::columns::tunnel_id>, Nullable<tunnel::columns::id>>,
    >;
    

    uses types that are not considered to be part of the public API of diesel. This means such a expression can break with any update. The correct way to write this type using the public API is

    pub type TunnelJoinConnection = diesel::dsl::LeftJoin<tunnel::table, connection::table>;`