Skip to main content

bottle_orm/
pagination.rs

1//! # Pagination Module
2//!
3//! This module provides a standard `Pagination` struct that is compatible with
4//! web frameworks like `axum`, `actix-web`, and `serde`. It allows for easy
5//! extraction of pagination parameters from HTTP requests and application
6//! to `QueryBuilder` instances.
7//!
8//! ## Features
9//!
10//! - **Serde Compatibility**: derives `Serialize` and `Deserialize`
11//! - **Query Integration**: `apply` method to automatically paginate queries
12//! - **Defaults**: sane defaults (page 0, limit 10)
13//!
14//! ## Example with Axum
15//!
16//! ```rust,ignore
17//! use axum::{extract::Query, Json};
18//! use bottle_orm::{Database, pagination::Pagination};
19//!
20//! async fn list_users(
21//!     State(db): State<Database>,
22//!     Query(pagination): Query<Pagination>
23//! ) -> Json<Vec<User>> {
24//!     let users = pagination.apply(db.model::<User>())
25//!         .scan()
26//!         .await
27//!         .unwrap();
28//!
29//!     Json(users)
30//! }
31//! ```
32
33use crate::{AnyImpl, any_struct::FromAnyRow, database::Connection, model::Model, query_builder::QueryBuilder};
34use serde::{Deserialize, Serialize};
35use sqlx::Row;
36
37/// A standard pagination structure.
38///
39/// Can be deserialized from query parameters (e.g., `?page=1&limit=20`).
40#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
41pub struct Pagination {
42    /// The page number (0-indexed). Default: 0.
43    #[serde(default)]
44    pub page: usize,
45
46    /// The number of items per page. Default: 10.
47    #[serde(default = "default_limit")]
48    pub limit: usize,
49
50    /// The maximum allowed limit for pagination to prevent large result sets.
51    /// If the requested `limit` exceeds `max_limit`, it will be capped (default: 100).
52    #[serde(default = "default_max_limit", skip_deserializing)]
53    pub max_limit: usize,
54}
55
56/// A wrapper for paginated results.
57///
58/// Contains the data items and metadata about the pagination state (total, pages, etc.).
59/// This struct is `Serialize`d to JSON for API responses.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct Paginated<T> {
62    /// The list of items for the current page.
63    pub data: Vec<T>,
64    /// The total number of items matching the query.
65    pub total: i64,
66    /// The current page number (0-indexed).
67    pub page: usize,
68    /// The number of items per page.
69    pub limit: usize,
70    /// The total number of pages.
71    pub total_pages: i64,
72}
73
74fn default_limit() -> usize {
75    10
76}
77
78fn default_max_limit() -> usize {
79	100
80}
81
82impl Default for Pagination {
83    fn default() -> Self {
84        Self { page: 0, limit: 10, max_limit: 100 }
85    }
86}
87
88impl Pagination {
89    /// Creates a new Pagination instance with a specified max_limit.
90    ///
91    /// If `limit` is greater than `max_limit`, it defaults to 10.
92    pub fn new(page: usize, mut limit: usize, max_limit: usize) -> Self {
93    	if limit > max_limit {
94     		limit = 10;
95     	}
96        Self { page, limit, max_limit }
97    }
98
99    /// Applies the pagination to a `QueryBuilder`.
100    ///
101    /// This method sets the `limit` and `offset` of the query builder
102    /// based on the pagination parameters. It also enforces the `max_limit`
103    /// check before applying the limit.
104    ///
105    /// # Arguments
106    ///
107    /// * `query` - The `QueryBuilder` to paginate
108    ///
109    /// # Returns
110    ///
111    /// The modified `QueryBuilder`
112    pub fn apply<'a, T, E>(mut self, query: QueryBuilder<'a, T, E>) -> QueryBuilder<'a, T, E>
113    where
114        T: Model + Send + Sync + Unpin,
115        E: Connection + Send,
116    {
117        // Enforce max_limit again during application to ensure safety
118        if self.limit > self.max_limit {
119        	self.limit = 10;
120        }
121        query.limit(self.limit).offset(self.page * self.limit)
122    }
123
124    /// Executes the query and returns a `Paginated<T>` result with metadata.
125    ///
126    /// This method performs two database queries:
127    /// 1. A `COUNT(*)` query to get the total number of records matching the filters.
128    /// 2. The actual `SELECT` query with `LIMIT` and `OFFSET` applied.
129    ///
130    /// # Type Parameters
131    ///
132    /// * `T` - The Model type.
133    /// * `E` - The connection type (Database or Transaction).
134    /// * `R` - The result type (usually same as T, but can be a DTO/Projection).
135    ///
136    /// # Returns
137    ///
138    /// * `Ok(Paginated<R>)` - The paginated results.
139    /// * `Err(sqlx::Error)` - Database error.
140    ///
141    /// # Example
142    ///
143    /// ```rust,ignore
144    /// let pagination = Pagination::new(0, 10);
145    /// let result = pagination.paginate(db.model::<User>()).await?;
146    ///
147    /// println!("Total users: {}", result.total);
148    /// for user in result.data {
149    ///     println!("User: {}", user.username);
150    /// }
151    /// ```
152    pub async fn paginate<'a, T, E, R>(self, mut query: QueryBuilder<'a, T, E>) -> Result<Paginated<R>, sqlx::Error>
153    where
154        T: Model + Send + Sync + Unpin,
155        E: Connection + Send,
156        R: FromAnyRow + AnyImpl + Send + Unpin,
157    {
158        // 1. Prepare COUNT query
159        // We temporarily replace selected columns with COUNT(*) and remove order/limit/offset
160        let original_select = query.select_columns.clone();
161        let original_order = query.order_clauses.clone();
162        let _original_limit = query.limit;
163        let _original_offset = query.offset;
164
165        query.select_columns = vec!["COUNT(*)".to_string()];
166        query.order_clauses.clear();
167        query.limit = None;
168        query.offset = None;
169
170        // 2. Generate and Execute Count SQL
171        // We cannot use query.scalar() easily because it consumes self.
172        // We use query.to_sql() and construct a manual query execution using the builder's state.
173
174        let count_sql = query.to_sql();
175
176        // We need to re-bind arguments. This logic mirrors QueryBuilder::scan
177        let mut args = sqlx::any::AnyArguments::default();
178        let mut arg_counter = 1;
179
180        // Re-bind arguments for count query
181        // Note: We access internal fields of QueryBuilder. This assumes this module is part of the crate.
182        // If WHERE clauses are complex, this manual reconstruction is necessary.
183        let mut dummy_query = String::new(); // Just to satisfy the closure signature
184        for clause in &query.where_clauses {
185            clause(&mut dummy_query, &mut args, &query.driver, &mut arg_counter);
186        }
187        if !query.having_clauses.is_empty() {
188            for clause in &query.having_clauses {
189                clause(&mut dummy_query, &mut args, &query.driver, &mut arg_counter);
190            }
191        }
192
193        // Execute count query
194        let count_row = sqlx::query_with::<_, _>(&count_sql, args).fetch_one(query.tx.executor()).await?;
195
196        let total: i64 = count_row.try_get(0)?;
197
198        // 3. Restore Query State for Data Fetch
199        query.select_columns = original_select;
200        query.order_clauses = original_order;
201        // Apply Pagination
202        query.limit = Some(self.limit);
203        query.offset = Some(self.page * self.limit);
204
205        // 4. Execute Data Query
206        // Now we can consume the builder with scan()
207        let data = query.scan::<R>().await?;
208
209        // 5. Calculate Metadata
210        let total_pages = (total as f64 / self.limit as f64).ceil() as i64;
211
212        Ok(Paginated { data, total, page: self.page, limit: self.limit, total_pages })
213    }
214}