3

I'm trying to specify different columns for order_by depending on an external parameter.

This works, but is ugly:

#[macro_use]
extern crate diesel;

use crate::diesel::prelude::*;
use diesel::pg::PgConnection;

mod schema {
    table! {
        items (id) {
            id -> Int4,
            name -> Text,
        }
    }
}

#[derive(Queryable, Debug)]
pub struct Item {
    pub id: i32,
    pub name: String,
}

fn load_items(conn: PgConnection, sort_prop: String, sort_dir: String) -> Vec<Item> {
    use schema::items::dsl::*;

    let mut query = items.into_boxed();

    // ugly: duplicating condition by sort_dir and query.order_by() calls
    query = match sort_prop.as_str() {
        "name" => {
            if sort_dir == "asc" {
                query.order_by(name.asc())
            } else {
                query.order_by(name.desc())
            }
        }
        _ => {
            if sort_dir == "asc" {
                query.order_by(id.asc())
            } else {
                query.order_by(id.desc())
            }
        }
    };

    query.load::<Item>(&conn).expect("Failed to load items")
}

fn main() {}

My Cargo.toml has this:

[dependencies]
diesel = { version = "1.4.3", features = ["postgres"] }

I want to condition only by column, and not entire query, something like:

use schema::items::dsl::*;

let mut column = match sort_prop.as_str() {
    "name" => name,
    _ => id // error: match arms have incompatible types
}

column = if sort_dir == "asc" {
    column.asc()
} else {
    column.desc()
}

let results = items
    .order_by(column)
    .load::<Item>(connection)
    .expect("Failed to load items");

Is this possible? Is there any other way to refactor this?

I've read Querying a Diesel table with dynamic parameters, but it's basically about conditioning by the entire query, which is what I'm trying to avoid.

I've also read Creating Diesel.rs queries with a dynamic number of .and()'s, which is about conditioning by filter. This might be close to what I need with order_by, but it's hard for me to apply the BoxableExpression weirdness to my case because there's a lack of good examples for my exact case in the docs and a lack of RLS support for showing any schema::items::dsl::* types in my IDE, so I could scramble through myself.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
artin
  • 1,764
  • 1
  • 17
  • 23

3 Answers3

2

Given the following schema:

table! {
    flights (id) {
        id -> Int4,
        name -> Text,
        country -> Text,
        launch_date -> Timestamptz,
    }
}

And the following struct containing the sorting options:

pub struct SearchFlight {
    pub sort: Option<String>,
    pub sort_dir: Option<String>
}

It is possible to implement the following:

let mut query = flights.into_boxed();
match search.sort.as_ref().map(String::as_str)  {
    Some("name") => sort_by_column(query, flights_schema::name, search.sort_dir),
    Some("country") => sort_by_column(query, flights_schema::country, search.sort_dir),
    Some("launch_date") => sort_by_column(query, flights_schema::launch_date, search.sort_dir),
    _ => query
}
query.load_page::<Flight>(con)

where sort_by_column if the following function

fn sort_by_column<U: 'static>(mut query: BoxedQuery<'static, Pg>,
                     column: U,
                     sort_dir: Option<String>) -> BoxedQuery<'static, Pg>
    where U: ExpressionMethods + QueryFragment<Pg> + AppearsOnTable<flights_schema::table>{
    match sort_dir.as_ref().map(String::as_str) {
        Some("asc") => query.order_by(column.asc()),
        Some("desc") => query.order_by(column.desc()),
        _ => query
    }
}
0

I had the same problem. Wanted this to work for any table/query:

#[macro_use]
extern crate diesel;

use diesel::table;

table! {
    posts (id) {
        id -> Nullable<Integer>,
    }
}

#[derive(Clone, Copy)]
pub enum QueryOrdering {
    Ascending,
    Descending,
}

/// bring ExtendedQueryDsl from below into scope when building queries
fn example(order: QueryOrdering)
{
    use diesel::mysql::Mysql;

    posts::table.order_dyn::<Mysql, _>(order, posts::id).limit(5);

    posts::table
        .into_boxed::<Mysql>()
        .order_dyn2::<Mysql, _>(order, posts::id)
        .limit(5);
    // This will not work since table.into_boed().into_boxed() is not implemented in diesel even
    // though it could be a simple a no-op
    /*
    posts::table
        .into_boxed()
        .order_dyn::<Mysql,_>(order, posts::id)
        .limit(5);
    */
    order
        .order_query::<_, _, Mysql>(posts::table, posts::id)
        .limit(5);
}

Here is my solution (works for both diesel 1 & 2)

use diesel::backend::Backend;
use diesel::dsl::{Asc, Desc};
use diesel::helper_types::IntoBoxed;
use diesel::query_dsl::methods::{BoxedDsl, OrderDsl};
use diesel::{ExpressionMethods, QueryDsl};

#[derive(Clone, Copy)]
pub enum QueryOrdering {
    Ascending,
    Descending,
}

impl QueryOrdering {
    fn order_query<'a, Expr, Q, DB>(&self, query: Q, expr: Expr) -> IntoBoxed<'a, Q, DB>
    where
        Expr: ExpressionMethods,
        Q: QueryDsl + BoxedDsl<'a, DB>,
        DB: Backend,
        IntoBoxed<'a, Q, DB>: OrderDsl<Asc<Expr>, Output = IntoBoxed<'a, Q, DB>>
            + OrderDsl<Desc<Expr>, Output = IntoBoxed<'a, Q, DB>>,
    {
        match self {
            Self::Ascending => query.into_boxed().order(expr.asc()),
            Self::Descending => query.into_boxed().order(expr.desc()),
        }
    }
    fn order_query2<'a, Expr, Q, DB>(
        &self,
        query: Q,
        expr: Expr,
    ) -> <Q as OrderDsl<Asc<Expr>>>::Output
    where
        Expr: ExpressionMethods,
        DB: Backend,
        Q: OrderDsl<Asc<Expr>>
            + OrderDsl<Desc<Expr>, Output = <Q as OrderDsl<Asc<Expr>>>::Output>,
    {
        match self {
            Self::Ascending => query.order(expr.asc()),
            Self::Descending => query.order(expr.desc()),
        }
    }
}

pub trait DynOrderDsl2<'a, DB, Expr>
where
    Expr: ExpressionMethods,
    DB: Backend,
    Self: QueryDsl
        + OrderDsl<Asc<Expr>>
        + OrderDsl<Desc<Expr>, Output = <Self as OrderDsl<Asc<Expr>>>::Output>,
{
    fn dyn_order2(
        self,
        order: QueryOrdering,
        expr: Expr,
    ) -> <Self as OrderDsl<Asc<Expr>>>::Output;
}

impl<'a, Expr, DB, Q> DynOrderDsl2<'a, DB, Expr> for Q
where
    Expr: ExpressionMethods,
    DB: Backend,
    Q: QueryDsl
        + OrderDsl<Asc<Expr>>
        + OrderDsl<Desc<Expr>, Output = <Q as OrderDsl<Asc<Expr>>>::Output>,
{
    fn dyn_order2(
        self,
        order: QueryOrdering,
        expr: Expr,
    ) -> <Q as OrderDsl<Asc<Expr>>>::Output {
        order.order_query2::<_, _, DB>(self, expr)
    }
}

pub trait DynOrderDsl<'a, DB, Expr>
where
    Expr: ExpressionMethods,
    Self: QueryDsl + BoxedDsl<'a, DB>,
    DB: Backend,
    IntoBoxed<'a, Self, DB>: OrderDsl<Asc<Expr>, Output = IntoBoxed<'a, Self, DB>>
        + OrderDsl<Desc<Expr>, Output = IntoBoxed<'a, Self, DB>>,
{
    fn dyn_order(self, order: QueryOrdering, expr: Expr) -> IntoBoxed<'a, Self, DB>;
}

impl<'a, Expr, DB, Q> DynOrderDsl<'a, DB, Expr> for Q
where
    Expr: ExpressionMethods,
    Q: QueryDsl + BoxedDsl<'a, DB>,
    DB: Backend,
    IntoBoxed<'a, Q, DB>: OrderDsl<Asc<Expr>, Output = IntoBoxed<'a, Q, DB>>
        + OrderDsl<Desc<Expr>, Output = IntoBoxed<'a, Q, DB>>,
{
    fn dyn_order(self, order: QueryOrdering, expr: Expr) -> IntoBoxed<'a, Q, DB> {
        order.order_query(self, expr)
    }
}

pub trait ExtendedQueryDsl: Sized {
    fn order_dyn<'a, DB, Expr>(self, order: QueryOrdering, expr: Expr) -> IntoBoxed<'a, Self, DB>
    where
        Expr: ExpressionMethods,
        Self: QueryDsl + BoxedDsl<'a, DB>,
        DB: Backend,
        IntoBoxed<'a, Self, DB>: OrderDsl<Asc<Expr>, Output = IntoBoxed<'a, Self, DB>>
            + OrderDsl<Desc<Expr>, Output = IntoBoxed<'a, Self, DB>>,
    {
        DynOrderDsl::<DB, Expr>::dyn_order(self, order, expr)
    }
    fn order_dyn2<DB, Expr>(
        self,
        order: QueryOrdering,
        expr: Expr,
    ) -> <Self as OrderDsl<Asc<Expr>>>::Output
    where
        Expr: ExpressionMethods,
        DB: Backend,
        Self: QueryDsl
            + OrderDsl<Asc<Expr>>
            + OrderDsl<Desc<Expr>, Output = <Self as OrderDsl<Asc<Expr>>>::Output>,
    {
        DynOrderDsl2::<DB, Expr>::dyn_order2(self, order, expr)
    }
}

impl<Q> ExtendedQueryDsl for Q {}
susitsm
  • 465
  • 2
  • 6
0

You're able to pass in a column to a function and use as the order_by parameter from 2.0.3 / 2.1.0 onwards.

https://github.com/diesel-rs/diesel/discussions/2999#discussioncomment-5381480

frankster
  • 1,529
  • 2
  • 16
  • 20