Skip to main content

floz_orm/
paginate.rs

1//! Pagination — `Paginated<T>` result type and `PaginateQuery<T>` builder.
2//!
3//! Uses the two-query strategy: COUNT for total, then SELECT with LIMIT/OFFSET.
4
5use std::marker::PhantomData;
6
7use crate::db::FlozRowBound;
8
9use crate::db::Executor;
10use crate::error::FlozError;
11use crate::expr::{Expr, OrderExpr};
12use crate::query::quote_table;
13use crate::value::Value;
14
15/// The result of a paginated query.
16#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
17pub struct Paginated<T> {
18    /// The items on this page.
19    pub items: Vec<T>,
20    /// Total number of matching rows across all pages.
21    pub total: i64,
22    /// Current page number (1-indexed).
23    pub page: i64,
24    /// Items per page.
25    pub per_page: i64,
26    /// Total number of pages.
27    pub total_pages: i64,
28    /// Whether there is a next page.
29    pub has_next: bool,
30    /// Whether there is a previous page.
31    pub has_prev: bool,
32}
33
34impl<T> Paginated<T> {
35    /// Construct a `Paginated` result, computing derived fields.
36    pub fn new(items: Vec<T>, total: i64, page: i64, per_page: i64) -> Self {
37        let total_pages = if per_page > 0 && total > 0 {
38            (total + per_page - 1) / per_page
39        } else {
40            0
41        };
42        Self {
43            items,
44            total,
45            page,
46            per_page,
47            total_pages,
48            has_next: page < total_pages,
49            has_prev: page > 1,
50        }
51    }
52}
53
54/// Builder for paginated queries.
55///
56/// ```ignore
57/// let page = User::paginate()
58///     .page(3)
59///     .per_page(20)
60///     .where_(UserTable::active.eq(true))
61///     .order_by(UserTable::created_at.desc())
62///     .execute(&db)
63///     .await?;
64/// ```
65pub struct PaginateQuery<T> {
66    table: String,
67    filter: Option<Expr>,
68    order: Vec<OrderExpr>,
69    page: i64,
70    per_page: i64,
71    _phantom: PhantomData<T>,
72}
73
74impl<T> PaginateQuery<T> {
75    /// Create a new paginate query for the given table.
76    pub fn new(table: &str) -> Self {
77        Self {
78            table: table.to_string(),
79            filter: None,
80            order: Vec::new(),
81            page: 1,
82            per_page: 25,
83            _phantom: PhantomData,
84        }
85    }
86
87    /// Set the page number (1-indexed). Values < 1 are clamped to 1.
88    pub fn page(mut self, page: i64) -> Self {
89        self.page = if page < 1 { 1 } else { page };
90        self
91    }
92
93    /// Set items per page. Values < 1 are clamped to 1.
94    pub fn per_page(mut self, per_page: i64) -> Self {
95        self.per_page = if per_page < 1 { 1 } else { per_page };
96        self
97    }
98
99    /// Add a WHERE filter.
100    pub fn where_(mut self, expr: Expr) -> Self {
101        self.filter = Some(expr);
102        self
103    }
104
105    /// Add an ORDER BY clause.
106    pub fn order_by(mut self, order: OrderExpr) -> Self {
107        self.order.push(order);
108        self
109    }
110
111    /// Execute the paginated query: runs a COUNT then a SELECT.
112    pub async fn execute(self, db: &impl Executor) -> Result<Paginated<T>, FlozError>
113    where
114        T: Send + Unpin + FlozRowBound,
115    {
116        let quoted = quote_table(&self.table);
117        let offset = (self.page - 1) * self.per_page;
118
119        // ── Query 1: COUNT ──
120        let mut count_sql = format!("SELECT COUNT(*) as count FROM {}", quoted);
121        let mut count_params: Vec<Value> = Vec::new();
122        let mut count_idx = 0usize;
123        if let Some(ref filter) = self.filter {
124            count_sql.push_str(" WHERE ");
125            filter.to_sql(&mut count_sql, &mut count_params, &mut count_idx);
126        }
127
128        let row: CountRow = db.fetch_one(&count_sql, count_params).await?;
129        let total = row.count;
130
131        // ── Query 2: SELECT with LIMIT/OFFSET ──
132        let mut data_sql = format!("SELECT * FROM {}", quoted);
133        let mut data_params: Vec<Value> = Vec::new();
134        let mut data_idx = 0usize;
135        if let Some(ref filter) = self.filter {
136            data_sql.push_str(" WHERE ");
137            filter.to_sql(&mut data_sql, &mut data_params, &mut data_idx);
138        }
139
140        // ORDER BY
141        if !self.order.is_empty() {
142            data_sql.push_str(" ORDER BY ");
143            for (i, ord) in self.order.iter().enumerate() {
144                if i > 0 {
145                    data_sql.push_str(", ");
146                }
147                ord.to_sql(&mut data_sql);
148            }
149        }
150
151        // LIMIT + OFFSET
152        data_idx += 1;
153        data_sql.push_str(&format!(" LIMIT ${}", data_idx));
154        data_params.push(Value::BigInt(self.per_page));
155        data_idx += 1;
156        data_sql.push_str(&format!(" OFFSET ${}", data_idx));
157        data_params.push(Value::BigInt(offset));
158
159        let items: Vec<T> = db.fetch_all(&data_sql, data_params).await?;
160
161        Ok(Paginated::new(items, total, self.page, self.per_page))
162    }
163}
164
165/// Internal struct for COUNT(*) result.
166#[derive(Debug, sqlx::FromRow)]
167struct CountRow {
168    count: i64,
169}
170
171// ═══════════════════════════════════════════════════════════════
172// Unit Tests
173// ═══════════════════════════════════════════════════════════════
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn paginated_basic() {
181        let p: Paginated<()> = Paginated::new(vec![(); 10], 50, 1, 10);
182        assert_eq!(p.total, 50);
183        assert_eq!(p.page, 1);
184        assert_eq!(p.per_page, 10);
185        assert_eq!(p.total_pages, 5);
186        assert!(p.has_next);
187        assert!(!p.has_prev);
188    }
189
190    #[test]
191    fn paginated_last_page() {
192        let p: Paginated<()> = Paginated::new(vec![(); 5], 25, 3, 10);
193        assert_eq!(p.total_pages, 3);
194        assert!(!p.has_next);
195        assert!(p.has_prev);
196    }
197
198    #[test]
199    fn paginated_middle_page() {
200        let p: Paginated<()> = Paginated::new(vec![(); 10], 30, 2, 10);
201        assert_eq!(p.total_pages, 3);
202        assert!(p.has_next);
203        assert!(p.has_prev);
204    }
205
206    #[test]
207    fn paginated_empty() {
208        let p: Paginated<()> = Paginated::new(vec![], 0, 1, 10);
209        assert_eq!(p.total_pages, 0);
210        assert!(!p.has_next);
211        assert!(!p.has_prev);
212    }
213
214    #[test]
215    fn paginated_single_page() {
216        let p: Paginated<()> = Paginated::new(vec![(); 3], 3, 1, 10);
217        assert_eq!(p.total_pages, 1);
218        assert!(!p.has_next);
219        assert!(!p.has_prev);
220    }
221
222    #[test]
223    fn paginated_remainder_page() {
224        let p: Paginated<()> = Paginated::new(vec![(); 1], 21, 3, 10);
225        assert_eq!(p.total_pages, 3);
226        assert!(!p.has_next);
227        assert!(p.has_prev);
228    }
229
230    #[test]
231    fn paginate_query_clamps_page() {
232        let q = PaginateQuery::<()>::new("test").page(0);
233        assert_eq!(q.page, 1);
234    }
235
236    #[test]
237    fn paginate_query_clamps_per_page() {
238        let q = PaginateQuery::<()>::new("test").per_page(-5);
239        assert_eq!(q.per_page, 1);
240    }
241
242    #[test]
243    fn paginate_query_defaults() {
244        let q = PaginateQuery::<()>::new("users");
245        assert_eq!(q.page, 1);
246        assert_eq!(q.per_page, 25);
247        assert!(q.filter.is_none());
248        assert!(q.order.is_empty());
249    }
250}