kaccy_db/
cursor_pagination.rs

1//! Cursor-based pagination for efficient navigation of large datasets.
2//!
3//! This module provides:
4//! - Keyset pagination for large datasets
5//! - Bidirectional cursor support
6//! - Stable ordering guarantees
7
8use base64::{engine::general_purpose, Engine as _};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12/// Cursor for pagination
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct Cursor(String);
15
16impl Cursor {
17    /// Create a new cursor from a string
18    pub fn new(value: String) -> Self {
19        Self(value)
20    }
21
22    /// Encode a cursor value to base64
23    pub fn encode(value: &str) -> Self {
24        let encoded = general_purpose::STANDARD.encode(value.as_bytes());
25        Self(encoded)
26    }
27
28    /// Decode a cursor from base64
29    pub fn decode(&self) -> Result<String, String> {
30        general_purpose::STANDARD
31            .decode(&self.0)
32            .map_err(|e| format!("Failed to decode cursor: {}", e))
33            .and_then(|bytes| {
34                String::from_utf8(bytes).map_err(|e| format!("Invalid UTF-8 in cursor: {}", e))
35            })
36    }
37
38    /// Get the raw cursor value
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42}
43
44impl fmt::Display for Cursor {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}", self.0)
47    }
48}
49
50impl From<String> for Cursor {
51    fn from(s: String) -> Self {
52        Self(s)
53    }
54}
55
56/// Direction for cursor pagination
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58pub enum Direction {
59    /// Forward pagination (next page)
60    Forward,
61    /// Backward pagination (previous page)
62    Backward,
63}
64
65/// Cursor pagination parameters
66#[derive(Debug, Clone)]
67pub struct CursorPagination {
68    /// Cursor to start from
69    pub cursor: Option<Cursor>,
70    /// Number of items to fetch
71    pub limit: u64,
72    /// Direction of pagination
73    pub direction: Direction,
74}
75
76impl Default for CursorPagination {
77    fn default() -> Self {
78        Self {
79            cursor: None,
80            limit: 20,
81            direction: Direction::Forward,
82        }
83    }
84}
85
86impl CursorPagination {
87    /// Create a new cursor pagination
88    pub fn new(cursor: Option<Cursor>, limit: u64, direction: Direction) -> Self {
89        Self {
90            cursor,
91            limit,
92            direction,
93        }
94    }
95
96    /// Create forward pagination
97    pub fn forward(limit: u64) -> Self {
98        Self {
99            cursor: None,
100            limit,
101            direction: Direction::Forward,
102        }
103    }
104
105    /// Create forward pagination with cursor
106    pub fn after(cursor: Cursor, limit: u64) -> Self {
107        Self {
108            cursor: Some(cursor),
109            limit,
110            direction: Direction::Forward,
111        }
112    }
113
114    /// Create backward pagination with cursor
115    pub fn before(cursor: Cursor, limit: u64) -> Self {
116        Self {
117            cursor: Some(cursor),
118            limit,
119            direction: Direction::Backward,
120        }
121    }
122
123    /// Get the SQL limit clause
124    pub fn sql_limit(&self) -> i64 {
125        // Fetch one extra to determine if there are more pages
126        (self.limit + 1) as i64
127    }
128}
129
130/// Edge in a cursor-paginated result
131#[derive(Debug, Clone, Serialize)]
132pub struct Edge<T> {
133    /// The cursor for this edge
134    pub cursor: Cursor,
135    /// The node (actual data)
136    pub node: T,
137}
138
139impl<T> Edge<T> {
140    /// Create a new edge
141    pub fn new(cursor: Cursor, node: T) -> Self {
142        Self { cursor, node }
143    }
144}
145
146/// Page information for cursor pagination
147#[derive(Debug, Clone, Serialize)]
148pub struct PageInfo {
149    /// Whether there is a next page
150    pub has_next_page: bool,
151    /// Whether there is a previous page
152    pub has_previous_page: bool,
153    /// Cursor of the first edge
154    pub start_cursor: Option<Cursor>,
155    /// Cursor of the last edge
156    pub end_cursor: Option<Cursor>,
157}
158
159impl PageInfo {
160    /// Create a new page info
161    pub fn new(
162        has_next_page: bool,
163        has_previous_page: bool,
164        start_cursor: Option<Cursor>,
165        end_cursor: Option<Cursor>,
166    ) -> Self {
167        Self {
168            has_next_page,
169            has_previous_page,
170            start_cursor,
171            end_cursor,
172        }
173    }
174}
175
176/// Cursor-paginated connection
177#[derive(Debug, Clone, Serialize)]
178pub struct Connection<T> {
179    /// Edges in this connection
180    pub edges: Vec<Edge<T>>,
181    /// Page information
182    pub page_info: PageInfo,
183    /// Total count (optional, can be expensive to compute)
184    pub total_count: Option<u64>,
185}
186
187impl<T> Connection<T> {
188    /// Create a new connection from edges
189    pub fn new(
190        mut edges: Vec<Edge<T>>,
191        pagination: &CursorPagination,
192        has_previous: bool,
193        total_count: Option<u64>,
194    ) -> Self {
195        let has_next = edges.len() > pagination.limit as usize;
196
197        // Remove the extra item we fetched
198        if has_next {
199            edges.pop();
200        }
201
202        let start_cursor = edges.first().map(|e| e.cursor.clone());
203        let end_cursor = edges.last().map(|e| e.cursor.clone());
204
205        let page_info = PageInfo::new(has_next, has_previous, start_cursor, end_cursor);
206
207        Self {
208            edges,
209            page_info,
210            total_count,
211        }
212    }
213
214    /// Get the nodes (without cursor information)
215    pub fn nodes(&self) -> Vec<&T> {
216        self.edges.iter().map(|e| &e.node).collect()
217    }
218
219    /// Convert to nodes (consuming the connection)
220    pub fn into_nodes(self) -> Vec<T> {
221        self.edges.into_iter().map(|e| e.node).collect()
222    }
223}
224
225/// Helper to build cursor from multiple fields
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct CursorBuilder {
228    fields: Vec<String>,
229}
230
231impl CursorBuilder {
232    /// Create a new cursor builder
233    pub fn new() -> Self {
234        Self { fields: Vec::new() }
235    }
236
237    /// Add a field to the cursor
238    pub fn add_field<T: ToString>(mut self, value: T) -> Self {
239        self.fields.push(value.to_string());
240        self
241    }
242
243    /// Build the cursor
244    pub fn build(self) -> Cursor {
245        let value = self.fields.join("|");
246        Cursor::encode(&value)
247    }
248
249    /// Parse a cursor into fields
250    pub fn parse(cursor: &Cursor) -> Result<Vec<String>, String> {
251        let decoded = cursor.decode()?;
252        Ok(decoded.split('|').map(|s| s.to_string()).collect())
253    }
254}
255
256impl Default for CursorBuilder {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_cursor_encode_decode() {
268        let cursor = Cursor::encode("test-id-123");
269        let decoded = cursor.decode().unwrap();
270        assert_eq!(decoded, "test-id-123");
271    }
272
273    #[test]
274    fn test_cursor_display() {
275        let cursor = Cursor::new("abc123".to_string());
276        assert_eq!(format!("{}", cursor), "abc123");
277    }
278
279    #[test]
280    fn test_cursor_pagination_default() {
281        let pagination = CursorPagination::default();
282        assert_eq!(pagination.cursor, None);
283        assert_eq!(pagination.limit, 20);
284        assert_eq!(pagination.direction, Direction::Forward);
285    }
286
287    #[test]
288    fn test_cursor_pagination_forward() {
289        let pagination = CursorPagination::forward(10);
290        assert_eq!(pagination.cursor, None);
291        assert_eq!(pagination.limit, 10);
292        assert_eq!(pagination.direction, Direction::Forward);
293    }
294
295    #[test]
296    fn test_cursor_pagination_after() {
297        let cursor = Cursor::new("abc".to_string());
298        let pagination = CursorPagination::after(cursor.clone(), 15);
299        assert_eq!(pagination.cursor, Some(cursor));
300        assert_eq!(pagination.limit, 15);
301        assert_eq!(pagination.direction, Direction::Forward);
302    }
303
304    #[test]
305    fn test_cursor_pagination_before() {
306        let cursor = Cursor::new("xyz".to_string());
307        let pagination = CursorPagination::before(cursor.clone(), 10);
308        assert_eq!(pagination.cursor, Some(cursor));
309        assert_eq!(pagination.limit, 10);
310        assert_eq!(pagination.direction, Direction::Backward);
311    }
312
313    #[test]
314    fn test_cursor_pagination_sql_limit() {
315        let pagination = CursorPagination::forward(20);
316        assert_eq!(pagination.sql_limit(), 21); // Fetches one extra
317    }
318
319    #[test]
320    fn test_edge_creation() {
321        let cursor = Cursor::new("cursor1".to_string());
322        let edge = Edge::new(cursor.clone(), "data");
323        assert_eq!(edge.cursor, cursor);
324        assert_eq!(edge.node, "data");
325    }
326
327    #[test]
328    fn test_page_info_creation() {
329        let cursor1 = Cursor::new("c1".to_string());
330        let cursor2 = Cursor::new("c2".to_string());
331        let page_info = PageInfo::new(true, false, Some(cursor1.clone()), Some(cursor2.clone()));
332
333        assert!(page_info.has_next_page);
334        assert!(!page_info.has_previous_page);
335        assert_eq!(page_info.start_cursor, Some(cursor1));
336        assert_eq!(page_info.end_cursor, Some(cursor2));
337    }
338
339    #[test]
340    fn test_connection_creation() {
341        let edges = vec![
342            Edge::new(Cursor::new("c1".to_string()), 1),
343            Edge::new(Cursor::new("c2".to_string()), 2),
344            Edge::new(Cursor::new("c3".to_string()), 3),
345        ];
346
347        let pagination = CursorPagination::forward(2);
348        let connection = Connection::new(edges, &pagination, false, Some(10));
349
350        assert_eq!(connection.edges.len(), 2); // Extra removed
351        assert!(connection.page_info.has_next_page);
352        assert!(!connection.page_info.has_previous_page);
353        assert_eq!(connection.total_count, Some(10));
354    }
355
356    #[test]
357    fn test_connection_nodes() {
358        let edges = vec![
359            Edge::new(Cursor::new("c1".to_string()), "a"),
360            Edge::new(Cursor::new("c2".to_string()), "b"),
361        ];
362
363        let pagination = CursorPagination::forward(5);
364        let connection = Connection::new(edges, &pagination, false, None);
365
366        let nodes = connection.nodes();
367        assert_eq!(nodes, vec![&"a", &"b"]);
368    }
369
370    #[test]
371    fn test_cursor_builder() {
372        let cursor = CursorBuilder::new()
373            .add_field("field1")
374            .add_field(123)
375            .add_field("field3")
376            .build();
377
378        let fields = CursorBuilder::parse(&cursor).unwrap();
379        assert_eq!(fields, vec!["field1", "123", "field3"]);
380    }
381
382    #[test]
383    fn test_cursor_builder_empty() {
384        let cursor = CursorBuilder::new().build();
385        let fields = CursorBuilder::parse(&cursor).unwrap();
386        assert_eq!(fields, vec![""]);
387    }
388}