/// Pagination utilities for query results and graph traversal. /// /// Enables efficient paginated retrieval of large result sets without /// loading everything into memory. /// /// # Example /// ``` /// let params = PaginationParams { limit: 50, page: 1 }; /// let (offset, limit) = params.calculate_offset_limit(); /// // SELECT ... OFFSET 0 LIMIT 50 /// ``` use serde::{Deserialize, Serialize}; const DEFAULT_LIMIT: usize = 50; const MAX_LIMIT: usize = 100; const MIN_LIMIT: usize = 1; /// Pagination parameters extracted from request. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct PaginationParams { /// Results per page (1-100, default 50) pub limit: Option, /// Page number (1-indexed, default 1) pub page: Option, } impl PaginationParams { /// Create pagination params with defaults. pub fn new(limit: Option, page: Option) -> Result { let limit = limit.unwrap_or(DEFAULT_LIMIT); let page = page.unwrap_or(1); // Validate if limit < MIN_LIMIT { return Err(format!("limit must be >= {}", MIN_LIMIT)); } if limit > MAX_LIMIT { return Err(format!("limit must be <= {}", MAX_LIMIT)); } if page < 1 { return Err("page must be >= 1".to_string()); } Ok(Self { limit: Some(limit), page: Some(page), }) } /// Calculate SQL OFFSET and LIMIT for database query. pub fn calculate_offset_limit(&self) -> (usize, usize) { let limit = self.limit.unwrap_or(DEFAULT_LIMIT); let page = self.page.unwrap_or(1); let offset = (page - 1) * limit; (offset, limit) } /// Calculate total pages given result count. pub fn calculate_total_pages(&self, total_results: usize) -> usize { let limit = self.limit.unwrap_or(DEFAULT_LIMIT); (total_results + limit - 1) / limit } /// Check if there's a next page. pub fn has_next(&self, total_results: usize) -> bool { let page = self.page.unwrap_or(1); let total_pages = self.calculate_total_pages(total_results); page < total_pages } /// Check if there's a previous page. pub fn has_prev(&self) -> bool { let page = self.page.unwrap_or(1); page > 1 } } /// Pagination metadata in response. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PaginationMeta { pub page: usize, pub limit: usize, pub total_results: usize, pub total_pages: usize, pub has_next: bool, pub has_prev: bool, } impl PaginationMeta { /// Create pagination metadata from params and total count. pub fn new(params: &PaginationParams, total_results: usize) -> Self { let page = params.page.unwrap_or(1); let limit = params.limit.unwrap_or(DEFAULT_LIMIT); let total_pages = params.calculate_total_pages(total_results); let has_next = params.has_next(total_results); let has_prev = params.has_prev(); Self { page, limit, total_results, total_pages, has_next, has_prev, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_pagination_defaults() { let params = PaginationParams::new(None, None).unwrap(); let (offset, limit) = params.calculate_offset_limit(); assert_eq!(offset, 0); assert_eq!(limit, 50); } #[test] fn test_pagination_page_2() { let params = PaginationParams::new(Some(50), Some(2)).unwrap(); let (offset, limit) = params.calculate_offset_limit(); assert_eq!(offset, 50); assert_eq!(limit, 50); } #[test] fn test_pagination_total_pages() { let params = PaginationParams::new(Some(50), Some(1)).unwrap(); assert_eq!(params.calculate_total_pages(127), 3); assert_eq!(params.calculate_total_pages(100), 2); } #[test] fn test_pagination_has_next() { let params = PaginationParams::new(Some(50), Some(1)).unwrap(); assert!(params.has_next(127)); let params = PaginationParams::new(Some(50), Some(3)).unwrap(); assert!(!params.has_next(127)); } #[test] fn test_pagination_validation() { assert!(PaginationParams::new(Some(150), Some(1)).is_err()); // > MAX_LIMIT assert!(PaginationParams::new(Some(0), Some(1)).is_err()); // < MIN_LIMIT assert!(PaginationParams::new(Some(50), Some(0)).is_err()); // page < 1 } #[test] fn test_pagination_meta() { let params = PaginationParams::new(Some(50), Some(1)).unwrap(); let meta = PaginationMeta::new(¶ms, 127); assert_eq!(meta.page, 1); assert_eq!(meta.limit, 50); assert_eq!(meta.total_results, 127); assert_eq!(meta.total_pages, 3); assert!(meta.has_next); assert!(!meta.has_prev); } }