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 serde::{Deserialize, Serialize};
34use crate::{database::Connection, model::Model, query_builder::QueryBuilder, AnyImpl};
35use sqlx::{any::AnyRow, FromRow, 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
51/// A wrapper for paginated results.
52///
53/// Contains the data items and metadata about the pagination state (total, pages, etc.).
54/// This struct is `Serialize`d to JSON for API responses.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Paginated<T> {
57 /// The list of items for the current page.
58 pub data: Vec<T>,
59 /// The total number of items matching the query.
60 pub total: i64,
61 /// The current page number (0-indexed).
62 pub page: usize,
63 /// The number of items per page.
64 pub limit: usize,
65 /// The total number of pages.
66 pub total_pages: i64,
67}
68
69fn default_limit() -> usize {
70 10
71}
72
73impl Default for Pagination {
74 fn default() -> Self {
75 Self {
76 page: 0,
77 limit: 10,
78 }
79 }
80}
81
82impl Pagination {
83 /// Creates a new Pagination instance.
84 pub fn new(page: usize, limit: usize) -> Self {
85 Self { page, limit }
86 }
87
88 /// Applies the pagination to a `QueryBuilder`.
89 ///
90 /// This method sets the `limit` and `offset` of the query builder
91 /// based on the pagination parameters.
92 ///
93 /// # Arguments
94 ///
95 /// * `query` - The `QueryBuilder` to paginate
96 ///
97 /// # Returns
98 ///
99 /// The modified `QueryBuilder`
100 pub fn apply<'a, T, E>(self, query: QueryBuilder<'a, T, E>) -> QueryBuilder<'a, T, E>
101 where
102 T: Model + Send + Sync + Unpin,
103 E: Connection + Send,
104 {
105 query.limit(self.limit).offset(self.page * self.limit)
106 }
107
108 /// Executes the query and returns a `Paginated<T>` result with metadata.
109 ///
110 /// This method performs two database queries:
111 /// 1. A `COUNT(*)` query to get the total number of records matching the filters.
112 /// 2. The actual `SELECT` query with `LIMIT` and `OFFSET` applied.
113 ///
114 /// # Type Parameters
115 ///
116 /// * `T` - The Model type.
117 /// * `E` - The connection type (Database or Transaction).
118 /// * `R` - The result type (usually same as T, but can be a DTO/Projection).
119 ///
120 /// # Returns
121 ///
122 /// * `Ok(Paginated<R>)` - The paginated results.
123 /// * `Err(sqlx::Error)` - Database error.
124 ///
125 /// # Example
126 ///
127 /// ```rust,ignore
128 /// let pagination = Pagination::new(0, 10);
129 /// let result = pagination.paginate(db.model::<User>()).await?;
130 ///
131 /// println!("Total users: {}", result.total);
132 /// for user in result.data {
133 /// println!("User: {}", user.username);
134 /// }
135 /// ```
136 pub async fn paginate<'a, T, E, R>(self, mut query: QueryBuilder<'a, T, E>) -> Result<Paginated<R>, sqlx::Error>
137 where
138 T: Model + Send + Sync + Unpin,
139 E: Connection + Send,
140 R: for<'r> FromRow<'r, AnyRow> + AnyImpl + Send + Unpin,
141 {
142 // 1. Prepare COUNT query
143 // We temporarily replace selected columns with COUNT(*) and remove order/limit/offset
144 let original_select = query.select_columns.clone();
145 let original_order = query.order_clauses.clone();
146 let original_limit = query.limit;
147 let original_offset = query.offset;
148
149 query.select_columns = vec!["COUNT(*)".to_string()];
150 query.order_clauses.clear();
151 query.limit = None;
152 query.offset = None;
153
154 // 2. Generate and Execute Count SQL
155 // We cannot use query.scalar() easily because it consumes self.
156 // We use query.to_sql() and construct a manual query execution using the builder's state.
157
158 let count_sql = query.to_sql();
159
160 // We need to re-bind arguments. This logic mirrors QueryBuilder::scan
161 let mut args = sqlx::any::AnyArguments::default();
162 let mut arg_counter = 1;
163
164 // Re-bind arguments for count query
165 // Note: We access internal fields of QueryBuilder. This assumes this module is part of the crate.
166 // If WHERE clauses are complex, this manual reconstruction is necessary.
167 let mut dummy_query = String::new(); // Just to satisfy the closure signature
168 for clause in &query.where_clauses {
169 clause(&mut dummy_query, &mut args, &query.driver, &mut arg_counter);
170 }
171 if !query.having_clauses.is_empty() {
172 for clause in &query.having_clauses {
173 clause(&mut dummy_query, &mut args, &query.driver, &mut arg_counter);
174 }
175 }
176
177 // Execute count query
178 let count_row = sqlx::query_with::<_, _>(&count_sql, args)
179 .fetch_one(query.tx.executor())
180 .await?;
181
182 let total: i64 = count_row.try_get(0)?;
183
184 // 3. Restore Query State for Data Fetch
185 query.select_columns = original_select;
186 query.order_clauses = original_order;
187 // Apply Pagination
188 query.limit = Some(self.limit);
189 query.offset = Some(self.page * self.limit);
190
191 // 4. Execute Data Query
192 // Now we can consume the builder with scan()
193 let data = query.scan::<R>().await?;
194
195 // 5. Calculate Metadata
196 let total_pages = (total as f64 / self.limit as f64).ceil() as i64;
197
198 Ok(Paginated {
199 data,
200 total,
201 page: self.page,
202 limit: self.limit,
203 total_pages,
204 })
205 }
206}