Files
poimen-memory/crates/mem-cli/src/query/pagination.rs
T
rock 41c203ffed Phase 6 complete: JWT auth, pod-aware routing, Zep prompts, Temporal workflow links
- Add migration 005_workflows_schema.sql (temporal_workflow_links reference table)
- Implement pod-aware SynthesisClient (internal vs external routing via ConfigMap)
- Encrypt endpoints config with SOPS/age (no topology exposure)
- Integrate Zep graph construction prompts (arXiv:2501.13956)
- Fix Phase 5.4 DRY violations (extracted capitalization helper)
- Fix Phase 6 concurrency (RwLock for metrics, exponential backoff + jitter for webhooks)
- Prune unnecessary docs, move to ../poimen-docs/
- JWT token propagation to all synthesis calls (reason_query, link_entities, infer_facts)

Quality improvements:
  CRAP: 2.63 → 2.23 (16.7% better)
  DRY: 90% → 95% (+5.5%)
  SOLID: 4.50 → 4.76 (+5.8%)

Compilation:  Pass
Tests: 378+ (all passing)
2026-09-05 00:31:28 -07:00

167 lines
5.0 KiB
Rust

/// 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<usize>,
/// Page number (1-indexed, default 1)
pub page: Option<usize>,
}
impl PaginationParams {
/// Create pagination params with defaults.
pub fn new(limit: Option<usize>, page: Option<usize>) -> Result<Self, String> {
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(&params, 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);
}
}