Fri 10 May 2024

Pagination in Async Diesel

In the Rust ecosystem, there are quite a few ways to approach interacting with Postgres. One of the older and more established libraries is diesel, which effectively gives you a Rust-wrapped path to constructing and executing queries. If it helps, you can think of it as a "light" ORM.

One of the bigger questions over the years has been whether diesel not being "async by default" is an issue. You could always wrap your queries in tasks and just await those, and that's probably fine - in fact, if I understand correctly, this is what crates.io does. However, diesel-async also exists for those who want to have a "pure" approach to the async interaction pattern. It's a smaller crate that effectively just requires importing a few extra traits to "override" the core diesel traits, and changing your connection type to be an asynchronous one. Overall, I've found it works pretty well.

One thing I did run into recently was needing some pagination for an admin interface I was throwing together. diesel actually has an example for this that works well if you're using synchronous calls or wrapping in tasks, but if you're using diesel-async, you'll need to tweak some things to make it work. I figured I'd share my changes here in case anyone needs them; yes, these could be on GitHub, but I'm in a weird phase of re-evaluating my usage of the platform, so I'm dumping it here.

You're smart, you can figure it out. The only real changes needed were importing some diesel-async items and tweaking the bounds on load_and_count_pages to accomodate the async differences. With this, you should be able to just call .paginate(page) on your queries and have mostly automatic paging. Enjoy.

use diesel::pg::Pg;
use diesel::prelude::*;
use diesel::query_builder::*;
use diesel::sql_types::BigInt;

// Import these, since we'll need them.
use diesel_async::AsyncPgConnection;
use diesel_async::methods::LoadQuery;

pub trait Paginate: Sized {
    fn paginate(self, page: i64) -> Paginated<Self>;
}

impl<T> Paginate for T {
    fn paginate(self, page: i64) -> Paginated<Self> {
        Paginated {
            query: self,
            per_page: DEFAULT_PER_PAGE,
            page,
            offset: (page - 1) * DEFAULT_PER_PAGE,
        }
    }
}

const DEFAULT_PER_PAGE: i64 = 10;

#[derive(Debug, Clone, Copy, QueryId)]
pub struct Paginated<T> {
    query: T,
    page: i64,
    per_page: i64,
    offset: i64,
}

impl<T> Paginated<T> {
    pub fn per_page(self, per_page: i64) -> Self {
        Paginated {
            per_page,
            offset: (self.page - 1) * per_page,
            ..self
        }
    }

    // Mark the bounds on `T` and `U` to ensure the async calls compile correctly,
    // and have `results` go through the async `RunQueryDsl`.
    pub async fn load_and_count_pages<'a, U>(
        self,
        conn: &mut AsyncPgConnection
    ) -> QueryResult<(Vec<U>, i64)>
    where
        T: 'a,
        U: Send + 'a,
        Self: LoadQuery<'a, AsyncPgConnection, (U, i64)>,
    {
        let per_page = self.per_page;
        let results = diesel_async::RunQueryDsl::load::<(U, i64)>(self, conn).await?;
        let total = results.first().map(|x| x.1).unwrap_or(0);
        let records = results.into_iter().map(|x| x.0).collect();
        let total_pages = (total as f64 / per_page as f64).ceil() as i64;
        Ok((records, total_pages))
    }
}

impl<T: Query> Query for Paginated<T> {
    type SqlType = (T::SqlType, BigInt);
}

impl<T> RunQueryDsl<PgConnection> for Paginated<T> {}

impl<T> QueryFragment<Pg> for Paginated<T>
where
    T: QueryFragment<Pg>,
{
    fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> QueryResult<()> {
        out.push_sql("SELECT *, COUNT(*) OVER () FROM (");
        self.query.walk_ast(out.reborrow())?;
        out.push_sql(") t LIMIT ");
        out.push_bind_param::<BigInt, _>(&self.per_page)?;
        out.push_sql(" OFFSET ");
        out.push_bind_param::<BigInt, _>(&self.offset)?;
        Ok(())
    }
}

Ryan around the Web