prax_query/
pagination.rs

1//! Pagination types for query results.
2//!
3//! This module provides types for implementing both offset-based and cursor-based pagination.
4//!
5//! # Offset-Based Pagination (Skip/Take)
6//!
7//! Simple pagination using skip and take:
8//!
9//! ```rust
10//! use prax_query::Pagination;
11//!
12//! // Skip 10, take 20
13//! let pagination = Pagination::new()
14//!     .skip(10)
15//!     .take(20);
16//!
17//! assert_eq!(pagination.skip, Some(10));
18//! assert_eq!(pagination.take, Some(20));
19//! assert_eq!(pagination.to_sql(), "LIMIT 20 OFFSET 10");
20//!
21//! // First N records
22//! let first_10 = Pagination::first(10);
23//! assert_eq!(first_10.to_sql(), "LIMIT 10");
24//!
25//! // Page-based pagination (1-indexed)
26//! let page_3 = Pagination::page(3, 25);  // Page 3 with 25 items per page
27//! assert_eq!(page_3.skip, Some(50));   // Skip first 2 pages (50 items)
28//! assert_eq!(page_3.take, Some(25));
29//! ```
30//!
31//! # Checking Pagination State
32//!
33//! ```rust
34//! use prax_query::Pagination;
35//!
36//! let empty = Pagination::new();
37//! assert!(empty.is_empty());
38//!
39//! let with_limit = Pagination::new().take(10);
40//! assert!(!with_limit.is_empty());
41//! ```
42
43use serde::{Deserialize, Serialize};
44use std::fmt::Write;
45
46/// Pagination configuration for queries.
47#[derive(Debug, Clone, Default, PartialEq, Eq)]
48pub struct Pagination {
49    /// Number of records to skip.
50    pub skip: Option<u64>,
51    /// Maximum number of records to take.
52    pub take: Option<u64>,
53    /// Cursor for cursor-based pagination.
54    pub cursor: Option<Cursor>,
55}
56
57impl Pagination {
58    /// Create a new pagination with no limits.
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Set the number of records to skip.
64    pub fn skip(mut self, skip: u64) -> Self {
65        self.skip = Some(skip);
66        self
67    }
68
69    /// Set the maximum number of records to take.
70    pub fn take(mut self, take: u64) -> Self {
71        self.take = Some(take);
72        self
73    }
74
75    /// Set cursor for cursor-based pagination.
76    pub fn cursor(mut self, cursor: Cursor) -> Self {
77        self.cursor = Some(cursor);
78        self
79    }
80
81    /// Check if pagination is specified.
82    pub fn is_empty(&self) -> bool {
83        self.skip.is_none() && self.take.is_none() && self.cursor.is_none()
84    }
85
86    /// Generate SQL LIMIT/OFFSET clause.
87    ///
88    /// Optimized to avoid intermediate allocations by writing directly to a buffer.
89    pub fn to_sql(&self) -> String {
90        // Estimate capacity: "LIMIT " (6) + number (up to 20) + " OFFSET " (8) + number (up to 20)
91        let mut sql = String::with_capacity(54);
92
93        if let Some(take) = self.take {
94            let _ = write!(sql, "LIMIT {}", take);
95        }
96
97        if let Some(skip) = self.skip {
98            if !sql.is_empty() {
99                sql.push(' ');
100            }
101            let _ = write!(sql, "OFFSET {}", skip);
102        }
103
104        sql
105    }
106
107    /// Write SQL LIMIT/OFFSET clause directly to a buffer (zero allocation).
108    ///
109    /// # Examples
110    ///
111    /// ```rust
112    /// use prax_query::Pagination;
113    ///
114    /// let pagination = Pagination::new().skip(10).take(20);
115    /// let mut buffer = String::with_capacity(64);
116    /// buffer.push_str("SELECT * FROM users ");
117    /// pagination.write_sql(&mut buffer);
118    /// assert!(buffer.ends_with("LIMIT 20 OFFSET 10"));
119    /// ```
120    #[inline]
121    pub fn write_sql(&self, buffer: &mut String) {
122        if let Some(take) = self.take {
123            let _ = write!(buffer, "LIMIT {}", take);
124        }
125
126        if let Some(skip) = self.skip {
127            if self.take.is_some() {
128                buffer.push(' ');
129            }
130            let _ = write!(buffer, "OFFSET {}", skip);
131        }
132    }
133
134    /// Get pagination for the first N records.
135    pub fn first(n: u64) -> Self {
136        Self::new().take(n)
137    }
138
139    /// Get pagination for a page (1-indexed).
140    pub fn page(page: u64, page_size: u64) -> Self {
141        let skip = (page.saturating_sub(1)) * page_size;
142        Self::new().skip(skip).take(page_size)
143    }
144}
145
146/// Cursor for cursor-based pagination.
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct Cursor {
149    /// The column to use for cursor.
150    pub column: String,
151    /// The cursor value.
152    pub value: CursorValue,
153    /// Direction of pagination.
154    pub direction: CursorDirection,
155}
156
157impl Cursor {
158    /// Create a new cursor.
159    pub fn new(column: impl Into<String>, value: CursorValue, direction: CursorDirection) -> Self {
160        Self {
161            column: column.into(),
162            value,
163            direction,
164        }
165    }
166
167    /// Create a cursor for fetching records after this value.
168    pub fn after(column: impl Into<String>, value: impl Into<CursorValue>) -> Self {
169        Self::new(column, value.into(), CursorDirection::After)
170    }
171
172    /// Create a cursor for fetching records before this value.
173    pub fn before(column: impl Into<String>, value: impl Into<CursorValue>) -> Self {
174        Self::new(column, value.into(), CursorDirection::Before)
175    }
176
177    /// Generate the WHERE clause for cursor-based pagination.
178    ///
179    /// Optimized to write directly to a pre-sized buffer.
180    pub fn to_sql_condition(&self) -> String {
181        // Estimate: column + " " + op + " $cursor" = column.len() + 10
182        let mut sql = String::with_capacity(self.column.len() + 12);
183        sql.push_str(&self.column);
184        sql.push(' ');
185        sql.push_str(match self.direction {
186            CursorDirection::After => "> $cursor",
187            CursorDirection::Before => "< $cursor",
188        });
189        sql
190    }
191
192    /// Write the cursor condition directly to a buffer (zero allocation).
193    #[inline]
194    pub fn write_sql_condition(&self, buffer: &mut String) {
195        buffer.push_str(&self.column);
196        buffer.push(' ');
197        buffer.push_str(match self.direction {
198            CursorDirection::After => "> $cursor",
199            CursorDirection::Before => "< $cursor",
200        });
201    }
202
203    /// Get the operator for this cursor direction.
204    #[inline]
205    pub const fn operator(&self) -> &'static str {
206        match self.direction {
207            CursorDirection::After => ">",
208            CursorDirection::Before => "<",
209        }
210    }
211}
212
213/// Cursor value type.
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215pub enum CursorValue {
216    /// Integer cursor (e.g., auto-increment ID).
217    Int(i64),
218    /// String cursor (e.g., UUID).
219    String(String),
220}
221
222impl From<i32> for CursorValue {
223    fn from(v: i32) -> Self {
224        Self::Int(v as i64)
225    }
226}
227
228impl From<i64> for CursorValue {
229    fn from(v: i64) -> Self {
230        Self::Int(v)
231    }
232}
233
234impl From<String> for CursorValue {
235    fn from(v: String) -> Self {
236        Self::String(v)
237    }
238}
239
240impl From<&str> for CursorValue {
241    fn from(v: &str) -> Self {
242        Self::String(v.to_string())
243    }
244}
245
246/// Direction for cursor-based pagination.
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
248pub enum CursorDirection {
249    /// Fetch records after the cursor.
250    After,
251    /// Fetch records before the cursor.
252    Before,
253}
254
255/// Result of a paginated query with metadata.
256#[derive(Debug, Clone)]
257pub struct PaginatedResult<T> {
258    /// The query results.
259    pub data: Vec<T>,
260    /// Whether there are more records after these.
261    pub has_next: bool,
262    /// Whether there are more records before these.
263    pub has_previous: bool,
264    /// The cursor for the next page (last item's cursor).
265    pub next_cursor: Option<CursorValue>,
266    /// The cursor for the previous page (first item's cursor).
267    pub previous_cursor: Option<CursorValue>,
268    /// Total count (if requested).
269    pub total_count: Option<u64>,
270}
271
272impl<T> PaginatedResult<T> {
273    /// Create a new paginated result.
274    pub fn new(data: Vec<T>) -> Self {
275        Self {
276            data,
277            has_next: false,
278            has_previous: false,
279            next_cursor: None,
280            previous_cursor: None,
281            total_count: None,
282        }
283    }
284
285    /// Set pagination metadata.
286    pub fn with_pagination(mut self, has_next: bool, has_previous: bool) -> Self {
287        self.has_next = has_next;
288        self.has_previous = has_previous;
289        self
290    }
291
292    /// Set total count.
293    pub fn with_total(mut self, total: u64) -> Self {
294        self.total_count = Some(total);
295        self
296    }
297
298    /// Set cursors.
299    pub fn with_cursors(
300        mut self,
301        next: Option<CursorValue>,
302        previous: Option<CursorValue>,
303    ) -> Self {
304        self.next_cursor = next;
305        self.previous_cursor = previous;
306        self
307    }
308
309    /// Get the number of records in this result.
310    pub fn len(&self) -> usize {
311        self.data.len()
312    }
313
314    /// Check if the result is empty.
315    pub fn is_empty(&self) -> bool {
316        self.data.is_empty()
317    }
318}
319
320impl<T> IntoIterator for PaginatedResult<T> {
321    type Item = T;
322    type IntoIter = std::vec::IntoIter<T>;
323
324    fn into_iter(self) -> Self::IntoIter {
325        self.data.into_iter()
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_pagination_skip_take() {
335        let pagination = Pagination::new().skip(10).take(20);
336        assert_eq!(pagination.to_sql(), "LIMIT 20 OFFSET 10");
337    }
338
339    #[test]
340    fn test_pagination_page() {
341        let pagination = Pagination::page(3, 10);
342        assert_eq!(pagination.skip, Some(20));
343        assert_eq!(pagination.take, Some(10));
344    }
345
346    #[test]
347    fn test_cursor_after() {
348        let cursor = Cursor::after("id", 100i64);
349        assert_eq!(cursor.to_sql_condition(), "id > $cursor");
350    }
351
352    #[test]
353    fn test_cursor_before() {
354        let cursor = Cursor::before("id", 100i64);
355        assert_eq!(cursor.to_sql_condition(), "id < $cursor");
356    }
357
358    #[test]
359    fn test_paginated_result() {
360        let result = PaginatedResult::new(vec![1, 2, 3])
361            .with_pagination(true, false)
362            .with_total(100);
363
364        assert_eq!(result.len(), 3);
365        assert!(result.has_next);
366        assert!(!result.has_previous);
367        assert_eq!(result.total_count, Some(100));
368    }
369}