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(
160        column: impl Into<String>,
161        value: CursorValue,
162        direction: CursorDirection,
163    ) -> Self {
164        Self {
165            column: column.into(),
166            value,
167            direction,
168        }
169    }
170
171    /// Create a cursor for fetching records after this value.
172    pub fn after(column: impl Into<String>, value: impl Into<CursorValue>) -> Self {
173        Self::new(column, value.into(), CursorDirection::After)
174    }
175
176    /// Create a cursor for fetching records before this value.
177    pub fn before(column: impl Into<String>, value: impl Into<CursorValue>) -> Self {
178        Self::new(column, value.into(), CursorDirection::Before)
179    }
180
181    /// Generate the WHERE clause for cursor-based pagination.
182    ///
183    /// Optimized to write directly to a pre-sized buffer.
184    pub fn to_sql_condition(&self) -> String {
185        // Estimate: column + " " + op + " $cursor" = column.len() + 10
186        let mut sql = String::with_capacity(self.column.len() + 12);
187        sql.push_str(&self.column);
188        sql.push(' ');
189        sql.push_str(match self.direction {
190            CursorDirection::After => "> $cursor",
191            CursorDirection::Before => "< $cursor",
192        });
193        sql
194    }
195
196    /// Write the cursor condition directly to a buffer (zero allocation).
197    #[inline]
198    pub fn write_sql_condition(&self, buffer: &mut String) {
199        buffer.push_str(&self.column);
200        buffer.push(' ');
201        buffer.push_str(match self.direction {
202            CursorDirection::After => "> $cursor",
203            CursorDirection::Before => "< $cursor",
204        });
205    }
206
207    /// Get the operator for this cursor direction.
208    #[inline]
209    pub const fn operator(&self) -> &'static str {
210        match self.direction {
211            CursorDirection::After => ">",
212            CursorDirection::Before => "<",
213        }
214    }
215}
216
217/// Cursor value type.
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub enum CursorValue {
220    /// Integer cursor (e.g., auto-increment ID).
221    Int(i64),
222    /// String cursor (e.g., UUID).
223    String(String),
224}
225
226impl From<i32> for CursorValue {
227    fn from(v: i32) -> Self {
228        Self::Int(v as i64)
229    }
230}
231
232impl From<i64> for CursorValue {
233    fn from(v: i64) -> Self {
234        Self::Int(v)
235    }
236}
237
238impl From<String> for CursorValue {
239    fn from(v: String) -> Self {
240        Self::String(v)
241    }
242}
243
244impl From<&str> for CursorValue {
245    fn from(v: &str) -> Self {
246        Self::String(v.to_string())
247    }
248}
249
250/// Direction for cursor-based pagination.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
252pub enum CursorDirection {
253    /// Fetch records after the cursor.
254    After,
255    /// Fetch records before the cursor.
256    Before,
257}
258
259/// Result of a paginated query with metadata.
260#[derive(Debug, Clone)]
261pub struct PaginatedResult<T> {
262    /// The query results.
263    pub data: Vec<T>,
264    /// Whether there are more records after these.
265    pub has_next: bool,
266    /// Whether there are more records before these.
267    pub has_previous: bool,
268    /// The cursor for the next page (last item's cursor).
269    pub next_cursor: Option<CursorValue>,
270    /// The cursor for the previous page (first item's cursor).
271    pub previous_cursor: Option<CursorValue>,
272    /// Total count (if requested).
273    pub total_count: Option<u64>,
274}
275
276impl<T> PaginatedResult<T> {
277    /// Create a new paginated result.
278    pub fn new(data: Vec<T>) -> Self {
279        Self {
280            data,
281            has_next: false,
282            has_previous: false,
283            next_cursor: None,
284            previous_cursor: None,
285            total_count: None,
286        }
287    }
288
289    /// Set pagination metadata.
290    pub fn with_pagination(
291        mut self,
292        has_next: bool,
293        has_previous: bool,
294    ) -> Self {
295        self.has_next = has_next;
296        self.has_previous = has_previous;
297        self
298    }
299
300    /// Set total count.
301    pub fn with_total(mut self, total: u64) -> Self {
302        self.total_count = Some(total);
303        self
304    }
305
306    /// Set cursors.
307    pub fn with_cursors(
308        mut self,
309        next: Option<CursorValue>,
310        previous: Option<CursorValue>,
311    ) -> Self {
312        self.next_cursor = next;
313        self.previous_cursor = previous;
314        self
315    }
316
317    /// Get the number of records in this result.
318    pub fn len(&self) -> usize {
319        self.data.len()
320    }
321
322    /// Check if the result is empty.
323    pub fn is_empty(&self) -> bool {
324        self.data.is_empty()
325    }
326}
327
328impl<T> IntoIterator for PaginatedResult<T> {
329    type Item = T;
330    type IntoIter = std::vec::IntoIter<T>;
331
332    fn into_iter(self) -> Self::IntoIter {
333        self.data.into_iter()
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_pagination_skip_take() {
343        let pagination = Pagination::new().skip(10).take(20);
344        assert_eq!(pagination.to_sql(), "LIMIT 20 OFFSET 10");
345    }
346
347    #[test]
348    fn test_pagination_page() {
349        let pagination = Pagination::page(3, 10);
350        assert_eq!(pagination.skip, Some(20));
351        assert_eq!(pagination.take, Some(10));
352    }
353
354    #[test]
355    fn test_cursor_after() {
356        let cursor = Cursor::after("id", 100i64);
357        assert_eq!(cursor.to_sql_condition(), "id > $cursor");
358    }
359
360    #[test]
361    fn test_cursor_before() {
362        let cursor = Cursor::before("id", 100i64);
363        assert_eq!(cursor.to_sql_condition(), "id < $cursor");
364    }
365
366    #[test]
367    fn test_paginated_result() {
368        let result = PaginatedResult::new(vec![1, 2, 3])
369            .with_pagination(true, false)
370            .with_total(100);
371
372        assert_eq!(result.len(), 3);
373        assert!(result.has_next);
374        assert!(!result.has_previous);
375        assert_eq!(result.total_count, Some(100));
376    }
377}
378