Skip to main content

bottle_orm/
pagination.rs

1//! # Pagination Module
2//!
3//! This module provides pagination functionality for Bottle ORM queries.
4//! It handles the calculation of limits, offsets, and total page counts,
5//! and integrates seamlessly with the `QueryBuilder`.
6
7// ============================================================================
8// External Crate Imports
9// ============================================================================
10
11use serde::{Deserialize, Serialize};
12use sqlx::Row;
13
14// ============================================================================
15// Internal Crate Imports
16// ============================================================================
17
18use crate::{
19    any_struct::FromAnyRow,
20    database::Connection,
21    model::Model,
22    query_builder::QueryBuilder,
23    AnyImpl,
24};
25
26// ============================================================================
27// Pagination Structs
28// ============================================================================
29
30/// Represents a paginated result set from the database.
31///
32/// Contains the requested subset of data along with metadata about the total
33/// number of records and pages available.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Paginated<T> {
36    /// The list of items for the current page
37    pub data: Vec<T>,
38    /// The total number of records matching the query (ignoring pagination)
39    pub total: i64,
40    /// The current page number (zero-based)
41    pub page: usize,
42    /// The number of items per page
43    pub limit: usize,
44    /// The total number of pages available
45    pub total_pages: i64,
46}
47
48/// A builder for pagination settings.
49///
50/// Use this struct to define how results should be paginated before executing
51/// a query via `paginate()`.
52#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
53pub struct Pagination {
54    /// Zero-based page index
55    #[serde(default)]
56    pub page: usize,
57    
58    /// Number of items per page
59    #[serde(default = "default_limit")]
60    pub limit: usize,
61    
62    /// Maximum allowed items per page (safety limit)
63    #[serde(default = "default_max_limit", skip_deserializing)]
64    pub max_limit: usize,
65}
66
67/// Sets defaults values to limit.
68fn default_limit() -> usize {
69    10
70}
71
72
73/// Sets max values to cap queries in database.
74fn default_max_limit() -> usize {
75	100
76}
77
78/// Default for axum headers
79impl Default for Pagination {
80    fn default() -> Self {
81        Self { page: 0, limit: 10, max_limit: 100 }
82    }
83}
84
85impl Pagination {
86    /// Creates a new Pagination instance with a custom safety limit.
87    ///
88    /// # Arguments
89    ///
90    /// * `page` - Zero-based page number
91    /// * `limit` - Items per page
92    /// * `max_limit` - Maximum allowed items per page
93    pub fn new_with_limit(page: usize, limit: usize, max_limit: usize) -> Self {
94        let mut f_limit = limit;
95        if f_limit > max_limit {
96            f_limit = 10;
97        }
98        Self { page, limit: f_limit, max_limit }
99    }
100
101    /// Creates a new Pagination instance with a default safety limit of 100.
102    ///
103    /// # Arguments
104    ///
105    /// * `page` - Zero-based page number
106    /// * `limit` - Items per page
107    pub fn new(page: usize, limit: usize) -> Self {
108        Self::new_with_limit(page, limit, 100)
109    }
110
111    /// Applies pagination settings to a `QueryBuilder`.
112    ///
113    /// This method sets the `limit` and `offset` of the query builder
114    /// based on the pagination parameters. It also enforces the `max_limit`
115    /// check before applying the limit.
116    ///
117    /// # Arguments
118    ///
119    /// * `query` - The `QueryBuilder` to paginate
120    ///
121    /// # Returns
122    ///
123    /// The modified `QueryBuilder`
124    pub fn apply<T, E>(mut self, query: QueryBuilder<T, E>) -> QueryBuilder<T, E>
125    where
126        T: Model + Send + Sync + Unpin + AnyImpl,
127        E: Connection + Send,
128    {
129        // Enforce max_limit again during application to ensure safety
130        if self.limit > self.max_limit {
131        	self.limit = 10;
132        }
133
134        query.limit(self.limit).offset(self.page * self.limit)
135    }
136
137    /// Executes the query and returns a `Paginated<R>` structure.
138    ///
139    /// This method performs two database operations:
140    /// 1. A `COUNT(*)` query to determine total records.
141    /// 2. The actual data query with `LIMIT` and `OFFSET` applied.
142    ///
143    /// # Type Parameters
144    ///
145    /// * `T` - The base Model type for the query.
146    /// * `E` - The connection type.
147    /// * `R` - The target result type (usually the same as T or a DTO).
148    ///
149    /// # Returns
150    ///
151    /// * `Ok(Paginated<R>)` - The data and pagination metadata.
152    /// * `Err(sqlx::Error)` - Database error.
153    ///
154    /// # Example
155    ///
156    /// ```rust,ignore
157    /// let p = Pagination::new(0, 20);
158    /// let res: Paginated<User> = p.paginate(db.model::<User>()).await?;
159    ///
160    /// for user in res.data {
161    ///     println!("User: {}", user.username);
162    /// }
163    /// ```
164    pub async fn paginate<T, E, R>(self, mut query: QueryBuilder<T, E>) -> Result<Paginated<R>, sqlx::Error>
165    where
166        T: Model + Send + Sync + Unpin + AnyImpl,
167        E: Connection + Send,
168        R: FromAnyRow + AnyImpl + Send + Unpin,
169    {
170        // 1. Prepare COUNT query
171        // We temporarily replace selected columns with COUNT(*) and remove order/limit/offset
172        let original_select = query.select_columns.clone();
173        let original_order = query.order_clauses.clone();
174        let _original_limit = query.limit;
175        let _original_offset = query.offset;
176
177        query.select_columns = vec!["COUNT(*)".to_string()];
178        query.order_clauses.clear();
179        query.limit = None;
180        query.offset = None;
181
182        // 2. Generate and Execute Count SQL
183        // We cannot use query.scalar() easily because it consumes self.
184        // We use query.to_sql() and construct a manual query execution using the builder's state.
185
186        let count_sql = query.to_sql();
187
188        // We need to re-bind arguments. This logic mirrors QueryBuilder::scan
189        let mut args = sqlx::any::AnyArguments::default();
190        let mut arg_counter = 1;
191
192        // Re-bind arguments for count query
193        // Note: We access internal fields of QueryBuilder. This assumes this module is part of the crate.
194        // If WHERE clauses are complex, this manual reconstruction is necessary.
195        let mut dummy_query = String::new(); // Just to satisfy the closure signature
196        for clause in &query.where_clauses {
197            clause(&mut dummy_query, &mut args, &query.driver, &mut arg_counter);
198        }
199        if !query.having_clauses.is_empty() {
200            for clause in &query.having_clauses {
201                clause(&mut dummy_query, &mut args, &query.driver, &mut arg_counter);
202            }
203        }
204
205        // Execute count query
206        let count_row = query.tx.fetch_one(&count_sql, args).await?;
207
208        let total: i64 = count_row.try_get(0)?;
209
210        // 3. Restore Query State for Data Fetch
211        query.select_columns = original_select;
212        query.order_clauses = original_order;
213        // Apply Pagination
214        query.limit = Some(self.limit);
215        query.offset = Some(self.page * self.limit);
216
217        // 4. Execute Data Query
218        // Now we can consume the builder with scan()
219        let data = query.scan::<R>().await?;
220
221        // 5. Calculate Metadata
222        let total_pages = (total as f64 / self.limit as f64).ceil() as i64;
223
224        Ok(Paginated { data, total, page: self.page, limit: self.limit, total_pages })
225    }
226    
227    /// Executes the query and returns a `Paginated<R>` mapping to a custom DTO.
228    ///
229    /// This method is similar to `paginate`, but it uses `scan_as` to map the results
230    /// to a type `R` that implements `FromAnyRow` but does not necessarily implement `AnyImpl`.
231    /// This is particularly useful for complex queries involving JOINs where the result
232    /// doesn't map directly to a single `Model`.
233    ///
234    /// # Type Parameters
235    ///
236    /// * `T` - The base Model type for the query.
237    /// * `E` - The connection type.
238    /// * `R` - The target result type (DTO/Projection).
239    ///
240    /// # Returns
241    ///
242    /// * `Ok(Paginated<R>)` - The paginated results mapped to type `R`.
243    /// * `Err(sqlx::Error)` - Database error.
244    pub async fn paginate_as<T, E, R>(self, mut query: QueryBuilder<T, E>) -> Result<Paginated<R>, sqlx::Error>
245    where
246        T: Model + Send + Sync + Unpin + AnyImpl,
247        E: Connection + Send,
248        R: FromAnyRow + AnyImpl + Send + Unpin,
249    {
250        // 1. Prepare COUNT query
251        let original_select = query.select_columns.clone();
252        let original_order = query.order_clauses.clone();
253        let _original_limit = query.limit;
254        let _original_offset = query.offset;
255    
256        query.select_columns = vec!["COUNT(*)".to_string()];
257        query.order_clauses.clear();
258        query.limit = None;
259        query.offset = None;
260    
261        let count_sql = query.to_sql();
262    
263        let mut args = sqlx::any::AnyArguments::default();
264        let mut arg_counter = 1;
265    
266        let mut dummy_query = String::new();
267        for clause in &query.where_clauses {
268            clause(&mut dummy_query, &mut args, &query.driver, &mut arg_counter);
269        }
270        if !query.having_clauses.is_empty() {
271            for clause in &query.having_clauses {
272                clause(&mut dummy_query, &mut args, &query.driver, &mut arg_counter);
273            }
274        }
275    
276        let count_row = query.tx.fetch_one(&count_sql, args).await?;
277        let total: i64 = count_row.try_get(0)?;
278    
279        // 3. Restore Query State
280        query.select_columns = original_select;
281        query.order_clauses = original_order;
282        query.limit = Some(self.limit);
283        query.offset = Some(self.page * self.limit);
284    
285        // 4. Execute Data Query usando o novo SCAN_AS
286        let data = query.scan_as::<R>().await?;
287    
288        // 5. Calculate Metadata
289        let total_pages = (total as f64 / self.limit as f64).ceil() as i64;
290    
291        Ok(Paginated { data, total, page: self.page, limit: self.limit, total_pages })
292    }
293}