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}