Skip to main content

arcature_data/query/
paginate.rs

1//! Pagination: a typed [`Page<T>`] that runs the count + page query in one
2//! call, replacing the application-code pagination the dogfood previously did
3//! by hand (loading all rows and slicing a `Vec`).
4//!
5//! `Query::paginate(per_page).page(n).fetch()` returns a [`Page`] carrying
6//! the rows, the total item count, and the total page count — computed by the
7//! database, not the application. Pages are **1-based** (page 0 is a typed
8//! error, never a silent empty result).
9
10use arcature_db::Db;
11use arcature_db::sea_orm;
12use arcature_db::sea_orm::PaginatorTrait;
13
14use crate::error::{DataError, PaginationError};
15
16/// A page of results from a paginated query.
17///
18/// Returned by [`Paginated::fetch`]. `page` and `per_page` echo the request;
19/// `total` is the total matching row count (from `COUNT(*)`); `num_pages` is the
20/// derived page count (`total.div_ceil(per_page)`). The database computes all
21/// three — the application never slices a full `Vec`.
22#[derive(Debug, Clone)]
23pub struct Page<T> {
24    /// The rows on this page.
25    pub rows: Vec<T>,
26    /// The 1-based page number that was requested.
27    pub page: u64,
28    /// The per-page size that was requested.
29    pub per_page: u64,
30    /// The total number of matching rows across all pages.
31    pub total: u64,
32    /// The total number of pages (`total.div_ceil(per_page)`; 0 when `total` is 0).
33    pub num_pages: u64,
34}
35
36impl<T> Page<T> {
37    /// `true` when this page holds no rows (the last page of an empty result,
38    /// or a page number past the end).
39    #[must_use]
40    pub fn is_empty(&self) -> bool {
41        self.rows.is_empty()
42    }
43}
44
45/// A paginated query awaiting a page number and a fetch.
46///
47/// Construct via [`crate::Query::paginate`]. Chain `.page(n)` (1-based) then
48/// `.fetch()`, or call `.fetch()` directly for page 1.
49pub struct Paginated<'db, E>
50where
51    E: sea_orm::EntityTrait,
52{
53    db: &'db Db,
54    select: sea_orm::Select<E>,
55    per_page: u64,
56    page: u64,
57}
58
59impl<'db, E> Paginated<'db, E>
60where
61    E: sea_orm::EntityTrait,
62{
63    pub(crate) fn new(db: &'db Db, select: sea_orm::Select<E>, per_page: u64) -> Self {
64        Self {
65            db,
66            select,
67            per_page,
68            page: 1,
69        }
70    }
71
72    /// Select a 1-based page number. Page 0 is rejected at [`Paginated::fetch`]
73    /// time with a typed [`PaginationError::PageMustBePositive`].
74    #[must_use]
75    pub fn page(mut self, page: u64) -> Self {
76        self.page = page;
77        self
78    }
79
80    /// Run the count + page query and return a typed [`Page`].
81    ///
82    /// # Errors
83    ///
84    /// Returns [`DataError::Pagination`] if `page` is 0 or `per_page` is 0,
85    /// and [`DataError::Database`] if the underlying SeaORM queries fail.
86    pub async fn fetch(self) -> Result<Page<E::Model>, DataError>
87    where
88        E::Model: sea_orm::FromQueryResult + Send + Sync,
89    {
90        if self.per_page == 0 {
91            return Err(PaginationError::PerPageMustBePositive.into());
92        }
93        if self.page == 0 {
94            return Err(PaginationError::PageMustBePositive.into());
95        }
96
97        let paginator = self.select.paginate(self.db.orm(), self.per_page);
98        let total = paginator.num_items().await.map_err(DataError::from)?;
99        let num_pages = total.div_ceil(self.per_page);
100        let rows = paginator
101            .fetch_page(self.page - 1)
102            .await
103            .map_err(DataError::from)?;
104        Ok(Page {
105            rows,
106            page: self.page,
107            per_page: self.per_page,
108            total,
109            num_pages,
110        })
111    }
112}