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}