1use crate::builder::{Direction, QueryBuilder};
8use crate::{Database, Row, Value, rows_to_json};
9use rustlavel_core::{Json, Result};
10
11#[derive(Debug)]
13pub struct Page {
14 pub rows: Vec<Row>,
15 pub total: i64,
16 pub per_page: i64,
17 pub current_page: i64,
18}
19
20impl Page {
21 pub fn last_page(&self) -> i64 {
22 if self.per_page <= 0 {
23 return 1;
24 }
25 ((self.total as f64) / (self.per_page as f64)).ceil().max(1.0) as i64
27 }
28
29 pub fn has_more(&self) -> bool {
30 self.current_page < self.last_page()
31 }
32
33 pub fn is_empty(&self) -> bool {
34 self.rows.is_empty()
35 }
36
37 pub fn from(&self) -> Option<i64> {
39 (!self.is_empty()).then(|| (self.current_page - 1) * self.per_page + 1)
40 }
41
42 pub fn to(&self) -> Option<i64> {
43 self.from().map(|from| from + self.rows.len() as i64 - 1)
44 }
45
46 pub fn links(&self, window: i64) -> Vec<Option<i64>> {
51 let last = self.last_page();
52 let mut out = Vec::new();
53 let mut previous: Option<i64> = None;
54
55 for page in 1..=last {
56 let near_current = (page - self.current_page).abs() <= window;
57 if page == 1 || page == last || near_current {
58 if previous.is_some_and(|p| page - p > 1) {
59 out.push(None);
60 }
61 out.push(Some(page));
62 previous = Some(page);
63 }
64 }
65 out
66 }
67
68 pub fn to_json(&self) -> Json {
71 Json::object([
72 ("data", rows_to_json(&self.rows)),
73 ("total", Json::from(self.total)),
74 ("per_page", Json::from(self.per_page)),
75 ("current_page", Json::from(self.current_page)),
76 ("last_page", Json::from(self.last_page())),
77 ("from", self.from().map_or(Json::Null, Json::from)),
78 ("to", self.to().map_or(Json::Null, Json::from)),
79 ])
80 }
81}
82
83#[derive(Debug)]
85pub struct CursorPage {
86 pub rows: Vec<Row>,
87 pub next_cursor: Option<String>,
89 pub per_page: i64,
90}
91
92impl CursorPage {
93 pub fn to_json(&self) -> Json {
94 Json::object([
95 ("data", rows_to_json(&self.rows)),
96 ("per_page", Json::from(self.per_page)),
97 ("next_cursor", self.next_cursor.clone().map_or(Json::Null, Json::from)),
98 ])
99 }
100}
101
102impl QueryBuilder {
103 pub async fn paginate(&self, db: &Database, page: i64, per_page: i64) -> Result<Page> {
108 let per_page = per_page.clamp(1, 1000);
109 let page = page.max(1);
110
111 let total = self.count(db).await?;
112 let rows = self.clone().page(page, per_page).get(db).await?;
113
114 Ok(Page { rows, total, per_page, current_page: page })
115 }
116
117 pub async fn cursor_paginate(
123 &self,
124 db: &Database,
125 column: &str,
126 after: Option<&str>,
127 per_page: i64,
128 ) -> Result<CursorPage> {
129 let per_page = per_page.clamp(1, 1000);
130
131 let mut query = self.clone().order_by(column, Direction::Asc).limit(per_page + 1);
132 if let Some(cursor) = after {
133 query = query.filter_op(column, ">", decode_cursor(cursor));
135 }
136
137 let mut rows = query.get(db).await?;
138
139 let has_more = rows.len() as i64 > per_page;
141 if has_more {
142 rows.truncate(per_page as usize);
143 }
144
145 let next_cursor = has_more
146 .then(|| rows.last().and_then(|row| row.value(column).ok().map(Value::to_display)))
147 .flatten();
148
149 Ok(CursorPage { rows, next_cursor, per_page })
150 }
151}
152
153fn decode_cursor(cursor: &str) -> Value {
158 match cursor.parse::<i64>() {
159 Ok(number) => Value::Int(number),
160 Err(_) => Value::Text(cursor.to_string()),
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use std::sync::Arc;
168
169 fn page(total: i64, per_page: i64, current: i64, rows: usize) -> Page {
170 let columns = Arc::new(vec!["id".to_string()]);
171 Page {
172 rows: (0..rows).map(|i| Row::new(Arc::clone(&columns), vec![Value::Int(i as i64)])).collect(),
173 total,
174 per_page,
175 current_page: current,
176 }
177 }
178
179 #[test]
180 fn computes_the_page_count() {
181 assert_eq!(page(100, 10, 1, 10).last_page(), 10);
182 assert_eq!(page(101, 10, 1, 10).last_page(), 11);
183 assert_eq!(page(0, 10, 1, 0).last_page(), 1);
184 }
185
186 #[test]
187 fn reports_the_range_on_the_page() {
188 let second = page(100, 10, 2, 10);
189
190 assert_eq!(second.from(), Some(11));
191 assert_eq!(second.to(), Some(20));
192 assert!(second.has_more());
193
194 let empty = page(0, 10, 1, 0);
195 assert_eq!(empty.from(), None);
196 assert!(!empty.has_more());
197 }
198
199 #[test]
200 fn link_windows_stay_a_fixed_width() {
201 let middle = page(1000, 10, 50, 10);
202 let links = middle.links(2);
203
204 assert_eq!(links.first(), Some(&Some(1)));
205 assert_eq!(links.last(), Some(&Some(100)));
206 assert!(links.contains(&None), "distant pages should be elided");
207 assert!(links.contains(&Some(50)));
208 assert!(links.contains(&Some(48)));
209 assert!(!links.contains(&Some(47)));
210 }
211
212 #[test]
213 fn a_short_run_of_pages_has_no_gaps() {
214 let links = page(30, 10, 2, 10).links(2);
215 assert_eq!(links, vec![Some(1), Some(2), Some(3)]);
216 }
217
218 #[test]
219 fn serializes_the_laravel_shape() {
220 let json = page(25, 10, 3, 5).to_json();
221
222 assert_eq!(json.get("total").unwrap().as_i64(), Some(25));
223 assert_eq!(json.get("last_page").unwrap().as_i64(), Some(3));
224 assert_eq!(json.get("from").unwrap().as_i64(), Some(21));
225 assert_eq!(json.get("to").unwrap().as_i64(), Some(25));
226 assert_eq!(json.get("data").unwrap().as_array().map(<[Json]>::len), Some(5));
227 }
228
229 #[test]
230 fn cursors_keep_their_type() {
231 assert_eq!(decode_cursor("42"), Value::Int(42));
232 assert_eq!(
233 decode_cursor("018f2c1a-0000-7000-8000-000000000000"),
234 Value::Text("018f2c1a-0000-7000-8000-000000000000".into())
235 );
236 }
237
238 #[test]
239 fn cursor_pagination_asks_for_one_extra_row() {
240 let (sql, _) = QueryBuilder::new("posts")
242 .clone()
243 .order_by("id", Direction::Asc)
244 .limit(11)
245 .to_sql(&crate::dialect::Postgres)
246 .unwrap();
247
248 assert!(sql.ends_with("order by \"id\" asc limit 11"));
249 }
250}