mik_sql/
pagination.rs

1//! Pagination utilities for cursor and keyset pagination.
2//!
3//! # Pagination Strategies
4//!
5//! | Strategy   | Jump to Page | Performance | Stability | Use Case               |
6//! |------------|--------------|-------------|-----------|------------------------|
7//! | **Offset** | Yes          | O(n) skip   | Unstable* | Admin panels, reports  |
8//! | **Cursor** | No           | O(1)        | Stable    | Feeds, infinite scroll |
9//! | **Keyset** | No           | O(1)        | Stable    | Large datasets, APIs   |
10//!
11//! *Unstable = results shift if data changes between requests
12//!
13//! # Cursor Pagination Example
14//!
15//! ```ignore
16//! use mik_sql::{postgres, Cursor, PageInfo, SortDir};
17//!
18//! // Build query with cursor pagination from request query params
19//! // after_cursor accepts: &Cursor, &str, String, Option<&str>, etc.
20//! let result = postgres("users")
21//!     .fields(&["id", "name", "created_at"])
22//!     .sort("created_at", SortDir::Desc)
23//!     .sort("id", SortDir::Asc)
24//!     .after_cursor(req.query("after"))  // Silently ignored if None or invalid
25//!     .limit(20)
26//!     .build();
27//!
28//! // Execute query and create response with page info
29//! let items = db.query(&result.sql, &result.params);
30//! let page_info = PageInfo::new(items.len(), 20)
31//!     .with_next_cursor(PageInfo::cursor_from(items.last(), |u| {
32//!         Cursor::new()
33//!             .string("created_at", &u.created_at)
34//!             .int("id", u.id)
35//!     }));
36//! ```
37//!
38//! # DX Features
39//!
40//! The `after_cursor` and `before_cursor` methods accept any type implementing `IntoCursor`:
41//! - `&Cursor` - Already parsed cursor
42//! - `&str` / `String` - Automatically decoded from base64
43//! - `Option<&str>` - Perfect for `req.query("after")`
44//!
45//! Invalid or missing cursors are silently ignored, making the API resilient.
46
47use crate::builder::{
48    CompoundFilter, Filter, FilterExpr, LogicalOp, Operator, SortDir, SortField, Value,
49};
50
51/// Maximum allowed cursor size in bytes (4KB).
52/// This prevents DoS attacks via oversized cursor payloads.
53const MAX_CURSOR_SIZE: usize = 4 * 1024;
54
55/// Maximum number of fields allowed in a cursor.
56/// This prevents DoS attacks via cursors with many tiny fields
57/// (e.g., `{"a":1,"b":2,...}` with hundreds of fields).
58const MAX_CURSOR_FIELDS: usize = 16;
59
60/// A cursor for cursor-based pagination.
61///
62/// Cursors encode the position in a result set as a base64 JSON object.
63/// The cursor contains the values of the sort fields for the last item.
64///
65/// # Security Note
66///
67/// Cursors use simple base64 encoding, **not encryption**. The cursor content
68/// is easily decoded by clients. This is intentional - cursors are opaque
69/// pagination tokens, not security mechanisms.
70///
71/// **Do not include sensitive data in cursor fields.** Only include the
72/// values needed for pagination (e.g., `id`, `created_at`).
73///
74/// If you need to prevent cursor tampering, validate cursor values against
75/// expected ranges or sign cursors server-side.
76#[derive(Debug, Clone, PartialEq)]
77pub struct Cursor {
78    /// Field values that define the cursor position.
79    pub fields: Vec<(String, Value)>,
80}
81
82impl Cursor {
83    /// Create a new empty cursor.
84    #[must_use]
85    pub fn new() -> Self {
86        Self { fields: Vec::new() }
87    }
88
89    /// Add a field value to the cursor.
90    pub fn field(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
91        self.fields.push((name.into(), value.into()));
92        self
93    }
94
95    /// Add an integer field.
96    pub fn int(self, name: impl Into<String>, value: i64) -> Self {
97        self.field(name, Value::Int(value))
98    }
99
100    /// Add a string field.
101    pub fn string(self, name: impl Into<String>, value: impl Into<String>) -> Self {
102        self.field(name, Value::String(value.into()))
103    }
104
105    /// Encode the cursor to a base64 string.
106    ///
107    /// Note: This uses simple base64, not encryption. See [`Cursor`] security note.
108    #[must_use]
109    pub fn encode(&self) -> String {
110        let json = self.to_json();
111        base64_encode(&json)
112    }
113
114    /// Decode a cursor from a base64 string.
115    ///
116    /// Returns an error if the cursor exceeds `MAX_CURSOR_SIZE` (4KB).
117    pub fn decode(encoded: &str) -> Result<Self, CursorError> {
118        // Check size before decoding to prevent DoS attacks
119        if encoded.len() > MAX_CURSOR_SIZE {
120            return Err(CursorError::TooLarge);
121        }
122        let json = base64_decode(encoded).map_err(|()| CursorError::InvalidBase64)?;
123        Self::from_json(&json)
124    }
125
126    /// Convert cursor to JSON string.
127    fn to_json(&self) -> String {
128        let mut parts = Vec::new();
129        for (name, value) in &self.fields {
130            let val_str = match value {
131                Value::Null => "null".to_string(),
132                Value::Bool(b) => b.to_string(),
133                Value::Int(i) => i.to_string(),
134                Value::Float(f) => f.to_string(),
135                Value::String(s) => format!("\"{}\"", escape_json(s)),
136                Value::Array(_) => continue, // Skip arrays in cursors
137            };
138            parts.push(format!("\"{name}\":{val_str}"));
139        }
140        format!("{{{}}}", parts.join(","))
141    }
142
143    /// Parse cursor from JSON string.
144    fn from_json(json: &str) -> Result<Self, CursorError> {
145        let mut cursor = Cursor::new();
146        let json = json.trim();
147
148        if !json.starts_with('{') || !json.ends_with('}') {
149            return Err(CursorError::InvalidFormat);
150        }
151
152        let inner = &json[1..json.len() - 1];
153        if inner.is_empty() {
154            return Ok(cursor);
155        }
156
157        // Simple JSON parser for cursor format
158        for pair in split_json_pairs(inner) {
159            let pair = pair.trim();
160            if pair.is_empty() {
161                continue;
162            }
163
164            let colon_idx = pair.find(':').ok_or(CursorError::InvalidFormat)?;
165            let key = pair[..colon_idx].trim();
166            let value = pair[colon_idx + 1..].trim();
167
168            // Parse key (remove quotes)
169            if !key.starts_with('"') || !key.ends_with('"') {
170                return Err(CursorError::InvalidFormat);
171            }
172            let key = &key[1..key.len() - 1];
173
174            // Parse value
175            let parsed_value = if value == "null" {
176                Value::Null
177            } else if value == "true" {
178                Value::Bool(true)
179            } else if value == "false" {
180                Value::Bool(false)
181            } else if value.starts_with('"') && value.ends_with('"') {
182                Value::String(unescape_json(&value[1..value.len() - 1]))
183            } else if value.contains('.') {
184                value
185                    .parse::<f64>()
186                    .map(Value::Float)
187                    .map_err(|_| CursorError::InvalidFormat)?
188            } else {
189                value
190                    .parse::<i64>()
191                    .map(Value::Int)
192                    .map_err(|_| CursorError::InvalidFormat)?
193            };
194
195            cursor.fields.push((key.to_string(), parsed_value));
196
197            // Limit field count to prevent DoS via many tiny fields
198            if cursor.fields.len() > MAX_CURSOR_FIELDS {
199                return Err(CursorError::TooManyFields);
200            }
201        }
202
203        Ok(cursor)
204    }
205}
206
207impl Default for Cursor {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213/// Errors that can occur when parsing a cursor.
214#[derive(Debug, Clone, PartialEq)]
215pub enum CursorError {
216    /// The base64 encoding is invalid.
217    InvalidBase64,
218    /// The cursor format is invalid.
219    InvalidFormat,
220    /// The cursor exceeds the maximum allowed size.
221    TooLarge,
222    /// The cursor has too many fields.
223    TooManyFields,
224}
225
226impl std::fmt::Display for CursorError {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        match self {
229            Self::InvalidBase64 => write!(f, "Invalid base64 encoding"),
230            Self::InvalidFormat => write!(f, "Invalid cursor format"),
231            Self::TooLarge => write!(f, "Cursor exceeds maximum size"),
232            Self::TooManyFields => write!(f, "Cursor has too many fields"),
233        }
234    }
235}
236
237impl std::error::Error for CursorError {}
238
239/// Trait for types that can be converted into a cursor.
240///
241/// This provides flexible DX for cursor pagination methods:
242/// - `Cursor` - zero-cost move when you own the cursor
243/// - `&str` - automatically decodes, returns None if invalid
244/// - `Option<&str>` - perfect for `req.query("after")` results
245///
246/// # Example
247///
248/// ```ignore
249/// // All of these work:
250/// .after_cursor(cursor)            // Cursor (zero-cost move)
251/// .after_cursor(cursor.clone())    // explicit clone if you need to keep it
252/// .after_cursor("eyJpZCI6MTAwfQ") // &str (base64 encoded)
253/// .after_cursor(req.query("after")) // Option<&str>
254/// ```
255pub trait IntoCursor {
256    /// Convert into an optional cursor.
257    /// Returns None if the input is invalid or missing.
258    fn into_cursor(self) -> Option<Cursor>;
259}
260
261impl IntoCursor for Cursor {
262    fn into_cursor(self) -> Option<Cursor> {
263        // Empty cursor should not add any conditions
264        if self.fields.is_empty() {
265            None
266        } else {
267            Some(self)
268        }
269    }
270}
271
272impl IntoCursor for &str {
273    fn into_cursor(self) -> Option<Cursor> {
274        if self.is_empty() || self.len() > MAX_CURSOR_SIZE {
275            return None;
276        }
277        Cursor::decode(self).ok()
278    }
279}
280
281impl IntoCursor for String {
282    fn into_cursor(self) -> Option<Cursor> {
283        self.as_str().into_cursor()
284    }
285}
286
287impl IntoCursor for &String {
288    fn into_cursor(self) -> Option<Cursor> {
289        self.as_str().into_cursor()
290    }
291}
292
293impl<T: IntoCursor> IntoCursor for Option<T> {
294    fn into_cursor(self) -> Option<Cursor> {
295        self.and_then(IntoCursor::into_cursor)
296    }
297}
298
299/// Page information for paginated responses.
300///
301/// # Example
302///
303/// ```ignore
304/// let page_info = PageInfo::new(items.len(), limit)
305///     .with_next_cursor(next_cursor)
306///     .with_prev_cursor(prev_cursor);
307///
308/// ok!({
309///     "data": items,
310///     "page_info": {
311///         "has_next": page_info.has_next,
312///         "has_prev": page_info.has_prev,
313///         "next_cursor": page_info.next_cursor,
314///         "prev_cursor": page_info.prev_cursor
315///     }
316/// })
317/// ```
318#[derive(Debug, Clone, Default)]
319pub struct PageInfo {
320    /// Whether there are more items after this page.
321    pub has_next: bool,
322    /// Whether there are items before this page.
323    pub has_prev: bool,
324    /// Cursor to fetch the next page.
325    pub next_cursor: Option<String>,
326    /// Cursor to fetch the previous page.
327    pub prev_cursor: Option<String>,
328    /// Total count (if available).
329    pub total: Option<u64>,
330}
331
332impl PageInfo {
333    /// Create page info based on returned count vs requested limit.
334    ///
335    /// If `count >= limit`, assumes there are more items.
336    #[must_use]
337    pub fn new(count: usize, limit: usize) -> Self {
338        Self {
339            has_next: count >= limit,
340            has_prev: false,
341            next_cursor: None,
342            prev_cursor: None,
343            total: None,
344        }
345    }
346
347    /// Set whether there are previous items.
348    #[must_use]
349    pub fn with_has_prev(mut self, has_prev: bool) -> Self {
350        self.has_prev = has_prev;
351        self
352    }
353
354    /// Set the next cursor.
355    #[must_use]
356    pub fn with_next_cursor(mut self, cursor: Option<String>) -> Self {
357        self.next_cursor = cursor;
358        if self.next_cursor.is_some() {
359            self.has_next = true;
360        }
361        self
362    }
363
364    /// Set the previous cursor.
365    #[must_use]
366    pub fn with_prev_cursor(mut self, cursor: Option<String>) -> Self {
367        self.prev_cursor = cursor;
368        if self.prev_cursor.is_some() {
369            self.has_prev = true;
370        }
371        self
372    }
373
374    /// Set the total count.
375    #[must_use]
376    pub fn with_total(mut self, total: u64) -> Self {
377        self.total = Some(total);
378        self
379    }
380
381    /// Create cursor from the last item using a builder function.
382    pub fn cursor_from<T, F>(item: Option<&T>, builder: F) -> Option<String>
383    where
384        F: FnOnce(&T) -> Cursor,
385    {
386        item.map(|item| builder(item).encode())
387    }
388}
389
390/// Keyset pagination condition.
391///
392/// Generates efficient `(col1, col2) > ($1, $2)` style WHERE clauses
393/// for keyset/seek pagination.
394#[derive(Debug, Clone)]
395pub struct KeysetCondition {
396    /// The sort fields and their directions.
397    pub sort_fields: Vec<SortField>,
398    /// The cursor values for each field.
399    pub cursor_values: Vec<Value>,
400    /// Direction: true for "after", false for "before".
401    pub forward: bool,
402}
403
404impl KeysetCondition {
405    /// Create a new keyset condition for paginating after a cursor.
406    #[must_use]
407    pub fn after(sorts: &[SortField], cursor: &Cursor) -> Option<Self> {
408        Self::new(sorts, cursor, true)
409    }
410
411    /// Create a new keyset condition for paginating before a cursor.
412    #[must_use]
413    pub fn before(sorts: &[SortField], cursor: &Cursor) -> Option<Self> {
414        Self::new(sorts, cursor, false)
415    }
416
417    fn new(sorts: &[SortField], cursor: &Cursor, forward: bool) -> Option<Self> {
418        if sorts.is_empty() {
419            return None;
420        }
421
422        // Match cursor fields to sort fields
423        let mut cursor_values = Vec::new();
424        for sort in sorts {
425            let value = cursor
426                .fields
427                .iter()
428                .find(|(name, _)| name == &sort.field)
429                .map(|(_, v)| v.clone())?;
430            cursor_values.push(value);
431        }
432
433        Some(Self {
434            sort_fields: sorts.to_vec(),
435            cursor_values,
436            forward,
437        })
438    }
439
440    /// Convert to a filter expression for the query builder.
441    ///
442    /// For a single field, generates: `field > $1` (or `<` for DESC)
443    ///
444    /// For multiple fields, generates proper compound OR conditions:
445    /// `(a, b) > (1, 2)` becomes: `(a > 1) OR (a = 1 AND b > 2)`
446    ///
447    /// For 3+ fields: `(a > 1) OR (a = 1 AND b > 2) OR (a = 1 AND b = 2 AND c > 3)`
448    ///
449    /// This follows the keyset pagination standard used by PostgreSQL, GraphQL Relay,
450    /// and major ORMs. See: <https://use-the-index-luke.com/no-offset>
451    #[must_use]
452    pub fn to_filter_expr(&self) -> FilterExpr {
453        if self.sort_fields.is_empty() || self.cursor_values.is_empty() {
454            // Return a tautology (always true) - will be optimized away
455            return FilterExpr::Simple(Filter {
456                field: "1".to_string(),
457                op: Operator::Eq,
458                value: Value::Int(1),
459            });
460        }
461
462        if self.sort_fields.len() == 1 {
463            // Simple case: single field comparison
464            let sort = &self.sort_fields[0];
465            let value = &self.cursor_values[0];
466            let op = self.get_operator(sort.dir);
467
468            return FilterExpr::Simple(Filter {
469                field: sort.field.clone(),
470                op,
471                value: value.clone(),
472            });
473        }
474
475        // Multi-field keyset: generate OR conditions
476        // (a, b, c) > (1, 2, 3) expands to:
477        //   (a > 1)
478        //   OR (a = 1 AND b > 2)
479        //   OR (a = 1 AND b = 2 AND c > 3)
480        let mut or_conditions: Vec<FilterExpr> = Vec::new();
481
482        for i in 0..self.sort_fields.len() {
483            // Build: equality on fields 0..i, then comparison on field i
484            let mut and_conditions: Vec<FilterExpr> = Vec::new();
485
486            // Add equality conditions for all preceding fields
487            for j in 0..i {
488                and_conditions.push(FilterExpr::Simple(Filter {
489                    field: self.sort_fields[j].field.clone(),
490                    op: Operator::Eq,
491                    value: self.cursor_values[j].clone(),
492                }));
493            }
494
495            // Add comparison condition for current field
496            let sort = &self.sort_fields[i];
497            let value = &self.cursor_values[i];
498            let op = self.get_operator(sort.dir);
499            and_conditions.push(FilterExpr::Simple(Filter {
500                field: sort.field.clone(),
501                op,
502                value: value.clone(),
503            }));
504
505            // Combine with AND
506            let condition = if and_conditions.len() == 1 {
507                and_conditions.into_iter().next().unwrap()
508            } else {
509                FilterExpr::Compound(CompoundFilter {
510                    op: LogicalOp::And,
511                    filters: and_conditions,
512                })
513            };
514
515            or_conditions.push(condition);
516        }
517
518        // Combine all with OR
519        if or_conditions.len() == 1 {
520            or_conditions.into_iter().next().unwrap()
521        } else {
522            FilterExpr::Compound(CompoundFilter {
523                op: LogicalOp::Or,
524                filters: or_conditions,
525            })
526        }
527    }
528
529    fn get_operator(&self, dir: SortDir) -> Operator {
530        match (self.forward, dir) {
531            (true, SortDir::Asc) => Operator::Gt,
532            (true, SortDir::Desc) => Operator::Lt,
533            (false, SortDir::Asc) => Operator::Lt,
534            (false, SortDir::Desc) => Operator::Gt,
535        }
536    }
537}
538
539// ============================================================================
540// Helper functions
541// ============================================================================
542
543/// Simple base64 encoding (URL-safe, no padding).
544///
545/// # Why Custom Implementation?
546///
547/// This crate avoids external dependencies for base64 encoding/decoding to:
548/// 1. Minimize binary size in WASM targets
549/// 2. Avoid dependency version conflicts
550/// 3. Keep the implementation simple and auditable
551///
552/// The implementation uses URL-safe alphabet (`-_` instead of `+/`) and omits
553/// padding, making cursors safe for use in query strings without additional encoding.
554fn base64_encode(input: &str) -> String {
555    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
556
557    let bytes = input.as_bytes();
558    let mut result = String::new();
559
560    for chunk in bytes.chunks(3) {
561        let b0 = u32::from(chunk[0]);
562        let b1 = u32::from(chunk.get(1).copied().unwrap_or(0));
563        let b2 = u32::from(chunk.get(2).copied().unwrap_or(0));
564
565        let n = (b0 << 16) | (b1 << 8) | b2;
566
567        result.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char);
568        result.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char);
569
570        if chunk.len() > 1 {
571            result.push(ALPHABET[((n >> 6) & 0x3F) as usize] as char);
572        }
573        if chunk.len() > 2 {
574            result.push(ALPHABET[(n & 0x3F) as usize] as char);
575        }
576    }
577
578    result
579}
580
581/// Simple base64 decoding (URL-safe, no padding).
582///
583/// Accepts both URL-safe (`-_`) and standard (`+/`) alphabet for compatibility.
584/// See [`base64_encode`] for rationale on custom implementation.
585fn base64_decode(input: &str) -> Result<String, ()> {
586    const DECODE: [i8; 128] = {
587        let mut table = [-1i8; 128];
588        let mut i = 0u8;
589        while i < 26 {
590            table[(b'A' + i) as usize] = i as i8;
591            table[(b'a' + i) as usize] = (i + 26) as i8;
592            i += 1;
593        }
594        let mut i = 0u8;
595        while i < 10 {
596            table[(b'0' + i) as usize] = (i + 52) as i8;
597            i += 1;
598        }
599        table[b'-' as usize] = 62;
600        table[b'_' as usize] = 63;
601        // Also support standard base64
602        table[b'+' as usize] = 62;
603        table[b'/' as usize] = 63;
604        table
605    };
606
607    let bytes: Vec<u8> = input.bytes().collect();
608    let mut result = Vec::new();
609
610    for chunk in bytes.chunks(4) {
611        let mut n = 0u32;
612        let mut valid_chars = 0;
613
614        for (i, &b) in chunk.iter().enumerate() {
615            if b as usize >= 128 {
616                return Err(());
617            }
618            let val = DECODE[b as usize];
619            if val < 0 {
620                return Err(());
621            }
622            n |= (val as u32) << (18 - i * 6);
623            valid_chars += 1;
624        }
625
626        result.push((n >> 16) as u8);
627        if valid_chars > 2 {
628            result.push((n >> 8) as u8);
629        }
630        if valid_chars > 3 {
631            result.push(n as u8);
632        }
633    }
634
635    String::from_utf8(result).map_err(|_| ())
636}
637
638/// Escape a string for JSON per RFC 8259.
639///
640/// Escapes:
641/// - `"` → `\"`
642/// - `\` → `\\`
643/// - Control characters (U+0000 to U+001F) → `\uXXXX` or named escapes
644fn escape_json(s: &str) -> String {
645    let mut result = String::with_capacity(s.len());
646    for c in s.chars() {
647        match c {
648            '"' => result.push_str("\\\""),
649            '\\' => result.push_str("\\\\"),
650            '\n' => result.push_str("\\n"),
651            '\r' => result.push_str("\\r"),
652            '\t' => result.push_str("\\t"),
653            '\x08' => result.push_str("\\b"), // backspace
654            '\x0C' => result.push_str("\\f"), // form feed
655            // Other control characters (U+0000 to U+001F)
656            c if c.is_control() && (c as u32) < 0x20 => {
657                result.push_str(&format!("\\u{:04x}", c as u32));
658            },
659            c => result.push(c),
660        }
661    }
662    result
663}
664
665/// Unescape a JSON string per RFC 8259.
666fn unescape_json(s: &str) -> String {
667    let mut result = String::with_capacity(s.len());
668    let mut chars = s.chars().peekable();
669    while let Some(c) = chars.next() {
670        if c == '\\' {
671            match chars.next() {
672                Some('"') => result.push('"'),
673                Some('\\') => result.push('\\'),
674                Some('/') => result.push('/'),
675                Some('n') => result.push('\n'),
676                Some('r') => result.push('\r'),
677                Some('t') => result.push('\t'),
678                Some('b') => result.push('\x08'),
679                Some('f') => result.push('\x0C'),
680                Some('u') => {
681                    // Parse \uXXXX escape
682                    let mut hex = String::with_capacity(4);
683                    for _ in 0..4 {
684                        if let Some(h) = chars.next() {
685                            hex.push(h);
686                        }
687                    }
688                    if let Ok(code) = u32::from_str_radix(&hex, 16)
689                        && let Some(ch) = char::from_u32(code)
690                    {
691                        result.push(ch);
692                    }
693                },
694                Some(c) => {
695                    result.push('\\');
696                    result.push(c);
697                },
698                None => result.push('\\'),
699            }
700        } else {
701            result.push(c);
702        }
703    }
704    result
705}
706
707/// Split JSON object into key:value pairs, respecting nesting.
708fn split_json_pairs(s: &str) -> Vec<&str> {
709    let mut pairs = Vec::new();
710    let mut start = 0;
711    let mut depth = 0;
712    let mut in_string = false;
713    let mut escape = false;
714
715    for (i, c) in s.char_indices() {
716        if escape {
717            escape = false;
718            continue;
719        }
720
721        match c {
722            '\\' if in_string => escape = true,
723            '"' => in_string = !in_string,
724            '{' | '[' if !in_string => depth += 1,
725            '}' | ']' if !in_string => depth -= 1,
726            ',' if !in_string && depth == 0 => {
727                pairs.push(&s[start..i]);
728                start = i + 1;
729            },
730            _ => {},
731        }
732    }
733
734    if start < s.len() {
735        pairs.push(&s[start..]);
736    }
737
738    pairs
739}
740
741// ============================================================================
742// Value conversion helpers
743// ============================================================================
744
745impl From<i64> for Value {
746    fn from(v: i64) -> Self {
747        Value::Int(v)
748    }
749}
750
751impl From<i32> for Value {
752    fn from(v: i32) -> Self {
753        Value::Int(i64::from(v))
754    }
755}
756
757impl From<f64> for Value {
758    fn from(v: f64) -> Self {
759        Value::Float(v)
760    }
761}
762
763impl From<String> for Value {
764    fn from(v: String) -> Self {
765        Value::String(v)
766    }
767}
768
769impl From<&str> for Value {
770    fn from(v: &str) -> Self {
771        Value::String(v.to_string())
772    }
773}
774
775impl From<bool> for Value {
776    fn from(v: bool) -> Self {
777        Value::Bool(v)
778    }
779}
780
781#[cfg(test)]
782mod tests {
783    use super::*;
784
785    #[test]
786    fn test_cursor_encode_decode() {
787        let cursor = Cursor::new().int("id", 100).string("name", "Alice");
788
789        let encoded = cursor.encode();
790        let decoded = Cursor::decode(&encoded).unwrap();
791
792        assert_eq!(cursor.fields, decoded.fields);
793    }
794
795    #[test]
796    fn test_cursor_empty() {
797        let cursor = Cursor::new();
798        let encoded = cursor.encode();
799        let decoded = Cursor::decode(&encoded).unwrap();
800        assert!(decoded.fields.is_empty());
801    }
802
803    #[test]
804    fn test_cursor_with_special_chars() {
805        let cursor = Cursor::new().string("name", "Hello \"World\"");
806
807        let encoded = cursor.encode();
808        let decoded = Cursor::decode(&encoded).unwrap();
809
810        assert_eq!(cursor.fields, decoded.fields);
811    }
812
813    #[test]
814    fn test_cursor_with_float() {
815        let cursor = Cursor::new().field("score", 1.234f64);
816
817        let encoded = cursor.encode();
818        let decoded = Cursor::decode(&encoded).unwrap();
819
820        assert_eq!(decoded.fields.len(), 1);
821        match &decoded.fields[0].1 {
822            Value::Float(f) => assert!((f - 1.234).abs() < 0.001),
823            _ => panic!("Expected float"),
824        }
825    }
826
827    #[test]
828    fn test_cursor_invalid_base64() {
829        let result = Cursor::decode("not valid base64!!!");
830        assert!(matches!(result, Err(CursorError::InvalidBase64)));
831    }
832
833    #[test]
834    fn test_cursor_too_large() {
835        // Create a cursor string larger than MAX_CURSOR_SIZE (4KB)
836        let oversized = "a".repeat(5 * 1024);
837        let result = Cursor::decode(&oversized);
838        assert!(matches!(result, Err(CursorError::TooLarge)));
839
840        // IntoCursor should return None for oversized cursors
841        let cursor: Option<Cursor> = oversized.as_str().into_cursor();
842        assert!(cursor.is_none());
843    }
844
845    #[test]
846    fn test_cursor_too_many_fields() {
847        // Create JSON with more than MAX_CURSOR_FIELDS (16) fields
848        let mut fields = Vec::new();
849        for i in 0..20 {
850            fields.push(format!("\"f{}\":1", i));
851        }
852        let json = format!("{{{}}}", fields.join(","));
853        let encoded = base64_encode(&json);
854
855        let result = Cursor::decode(&encoded);
856        assert!(matches!(result, Err(CursorError::TooManyFields)));
857
858        // IntoCursor should return None for cursors with too many fields
859        let cursor: Option<Cursor> = encoded.as_str().into_cursor();
860        assert!(cursor.is_none());
861    }
862
863    #[test]
864    fn test_page_info_basic() {
865        let info = PageInfo::new(20, 20);
866        assert!(info.has_next);
867        assert!(!info.has_prev);
868
869        let info = PageInfo::new(15, 20);
870        assert!(!info.has_next);
871    }
872
873    #[test]
874    fn test_page_info_with_cursors() {
875        let info = PageInfo::new(20, 20)
876            .with_next_cursor(Some("abc".to_string()))
877            .with_prev_cursor(Some("xyz".to_string()))
878            .with_total(100);
879
880        assert!(info.has_next);
881        assert!(info.has_prev);
882        assert_eq!(info.next_cursor, Some("abc".to_string()));
883        assert_eq!(info.prev_cursor, Some("xyz".to_string()));
884        assert_eq!(info.total, Some(100));
885    }
886
887    #[test]
888    fn test_keyset_condition_asc() {
889        let sorts = vec![SortField::new("id", SortDir::Asc)];
890        let cursor = Cursor::new().int("id", 100);
891
892        let condition = KeysetCondition::after(&sorts, &cursor).unwrap();
893        let expr = condition.to_filter_expr();
894
895        match expr {
896            FilterExpr::Simple(f) => {
897                assert_eq!(f.field, "id");
898                assert_eq!(f.op, Operator::Gt);
899            },
900            _ => panic!("Expected simple filter"),
901        }
902    }
903
904    #[test]
905    fn test_keyset_condition_desc() {
906        let sorts = vec![SortField::new("created_at", SortDir::Desc)];
907        let cursor = Cursor::new().string("created_at", "2024-01-01");
908
909        let condition = KeysetCondition::after(&sorts, &cursor).unwrap();
910        let expr = condition.to_filter_expr();
911
912        match expr {
913            FilterExpr::Simple(f) => {
914                assert_eq!(f.op, Operator::Lt);
915            },
916            _ => panic!("Expected simple filter"),
917        }
918    }
919
920    #[test]
921    fn test_keyset_condition_before() {
922        let sorts = vec![SortField::new("id", SortDir::Asc)];
923        let cursor = Cursor::new().int("id", 100);
924
925        let condition = KeysetCondition::before(&sorts, &cursor).unwrap();
926        let expr = condition.to_filter_expr();
927
928        match expr {
929            FilterExpr::Simple(f) => {
930                assert_eq!(f.op, Operator::Lt);
931            },
932            _ => panic!("Expected simple filter"),
933        }
934    }
935
936    #[test]
937    fn test_keyset_condition_multi_field_asc_asc() {
938        // Test: (created_at, id) > ('2024-01-01', 100)
939        // Should generate: (created_at > '2024-01-01') OR (created_at = '2024-01-01' AND id > 100)
940        let sorts = vec![
941            SortField::new("created_at", SortDir::Asc),
942            SortField::new("id", SortDir::Asc),
943        ];
944        let cursor = Cursor::new()
945            .string("created_at", "2024-01-01")
946            .int("id", 100);
947
948        let condition = KeysetCondition::after(&sorts, &cursor).unwrap();
949        let expr = condition.to_filter_expr();
950
951        // Should be OR compound
952        match expr {
953            FilterExpr::Compound(compound) => {
954                assert_eq!(compound.op, LogicalOp::Or);
955                assert_eq!(compound.filters.len(), 2);
956
957                // First: created_at > '2024-01-01'
958                match &compound.filters[0] {
959                    FilterExpr::Simple(f) => {
960                        assert_eq!(f.field, "created_at");
961                        assert_eq!(f.op, Operator::Gt);
962                    },
963                    _ => panic!("Expected simple filter for first condition"),
964                }
965
966                // Second: (created_at = '2024-01-01' AND id > 100)
967                match &compound.filters[1] {
968                    FilterExpr::Compound(and_compound) => {
969                        assert_eq!(and_compound.op, LogicalOp::And);
970                        assert_eq!(and_compound.filters.len(), 2);
971                    },
972                    _ => panic!("Expected compound AND filter for second condition"),
973                }
974            },
975            _ => panic!("Expected compound OR filter for multi-field keyset"),
976        }
977    }
978
979    #[test]
980    fn test_keyset_condition_multi_field_desc_asc() {
981        // Test: ORDER BY created_at DESC, id ASC with cursor after
982        let sorts = vec![
983            SortField::new("created_at", SortDir::Desc),
984            SortField::new("id", SortDir::Asc),
985        ];
986        let cursor = Cursor::new()
987            .string("created_at", "2024-01-01")
988            .int("id", 100);
989
990        let condition = KeysetCondition::after(&sorts, &cursor).unwrap();
991        let expr = condition.to_filter_expr();
992
993        match expr {
994            FilterExpr::Compound(compound) => {
995                assert_eq!(compound.op, LogicalOp::Or);
996
997                // First condition: created_at < '2024-01-01' (DESC means <)
998                match &compound.filters[0] {
999                    FilterExpr::Simple(f) => {
1000                        assert_eq!(f.field, "created_at");
1001                        assert_eq!(f.op, Operator::Lt); // DESC + After = Lt
1002                    },
1003                    _ => panic!("Expected simple filter"),
1004                }
1005            },
1006            _ => panic!("Expected compound filter"),
1007        }
1008    }
1009
1010    #[test]
1011    fn test_keyset_condition_three_fields() {
1012        // Test: (a, b, c) > (1, 2, 3) expands to:
1013        //   (a > 1)
1014        //   OR (a = 1 AND b > 2)
1015        //   OR (a = 1 AND b = 2 AND c > 3)
1016        let sorts = vec![
1017            SortField::new("a", SortDir::Asc),
1018            SortField::new("b", SortDir::Asc),
1019            SortField::new("c", SortDir::Asc),
1020        ];
1021        let cursor = Cursor::new().int("a", 1).int("b", 2).int("c", 3);
1022
1023        let condition = KeysetCondition::after(&sorts, &cursor).unwrap();
1024        let expr = condition.to_filter_expr();
1025
1026        match expr {
1027            FilterExpr::Compound(compound) => {
1028                assert_eq!(compound.op, LogicalOp::Or);
1029                assert_eq!(compound.filters.len(), 3);
1030
1031                // First: a > 1 (simple)
1032                match &compound.filters[0] {
1033                    FilterExpr::Simple(f) => {
1034                        assert_eq!(f.field, "a");
1035                        assert_eq!(f.op, Operator::Gt);
1036                    },
1037                    _ => panic!("Expected simple filter"),
1038                }
1039
1040                // Second: a = 1 AND b > 2
1041                match &compound.filters[1] {
1042                    FilterExpr::Compound(and_compound) => {
1043                        assert_eq!(and_compound.filters.len(), 2);
1044                    },
1045                    _ => panic!("Expected compound filter"),
1046                }
1047
1048                // Third: a = 1 AND b = 2 AND c > 3
1049                match &compound.filters[2] {
1050                    FilterExpr::Compound(and_compound) => {
1051                        assert_eq!(and_compound.filters.len(), 3);
1052                    },
1053                    _ => panic!("Expected compound filter"),
1054                }
1055            },
1056            _ => panic!("Expected compound filter"),
1057        }
1058    }
1059
1060    #[test]
1061    fn test_base64_roundtrip() {
1062        let original = "{\"id\":100,\"name\":\"test\"}";
1063        let encoded = base64_encode(original);
1064        let decoded = base64_decode(&encoded).unwrap();
1065        assert_eq!(original, decoded);
1066    }
1067
1068    #[test]
1069    fn test_cursor_from_helper() {
1070        #[derive(Debug)]
1071        struct User {
1072            id: i64,
1073        }
1074
1075        let user = User { id: 42 };
1076        let cursor = PageInfo::cursor_from(Some(&user), |u| Cursor::new().int("id", u.id));
1077
1078        assert!(cursor.is_some());
1079        let decoded = Cursor::decode(&cursor.unwrap()).unwrap();
1080        assert_eq!(decoded.fields[0], ("id".to_string(), Value::Int(42)));
1081    }
1082
1083    #[test]
1084    fn test_value_from_conversions() {
1085        let _: Value = 42i64.into();
1086        let _: Value = 42i32.into();
1087        let _: Value = 1.234f64.into();
1088        let _: Value = "hello".into();
1089        let _: Value = String::from("world").into();
1090        let _: Value = true.into();
1091    }
1092
1093    // =========================================================================
1094    // CURSOR NEAR-LIMIT SCENARIO TESTS
1095    // =========================================================================
1096
1097    #[test]
1098    fn test_cursor_exactly_at_max_fields() {
1099        // Create JSON with exactly MAX_CURSOR_FIELDS (16) fields - should succeed
1100        let mut fields = Vec::new();
1101        for i in 0..16 {
1102            fields.push(format!("\"f{}\":1", i));
1103        }
1104        let json = format!("{{{}}}", fields.join(","));
1105        let encoded = base64_encode(&json);
1106
1107        let result = Cursor::decode(&encoded);
1108        assert!(
1109            result.is_ok(),
1110            "Cursor with exactly 16 fields should succeed"
1111        );
1112        assert_eq!(result.unwrap().fields.len(), 16);
1113    }
1114
1115    #[test]
1116    fn test_cursor_one_under_max_fields() {
1117        // Create JSON with MAX_CURSOR_FIELDS - 1 (15) fields - should succeed
1118        let mut fields = Vec::new();
1119        for i in 0..15 {
1120            fields.push(format!("\"f{}\":1", i));
1121        }
1122        let json = format!("{{{}}}", fields.join(","));
1123        let encoded = base64_encode(&json);
1124
1125        let result = Cursor::decode(&encoded);
1126        assert!(result.is_ok(), "Cursor with 15 fields should succeed");
1127        assert_eq!(result.unwrap().fields.len(), 15);
1128    }
1129
1130    #[test]
1131    fn test_cursor_one_over_max_fields() {
1132        // Create JSON with MAX_CURSOR_FIELDS + 1 (17) fields - should fail
1133        let mut fields = Vec::new();
1134        for i in 0..17 {
1135            fields.push(format!("\"f{}\":1", i));
1136        }
1137        let json = format!("{{{}}}", fields.join(","));
1138        let encoded = base64_encode(&json);
1139
1140        let result = Cursor::decode(&encoded);
1141        assert!(matches!(result, Err(CursorError::TooManyFields)));
1142    }
1143
1144    #[test]
1145    fn test_cursor_near_max_size() {
1146        // Create a cursor near MAX_CURSOR_SIZE (4KB) but under
1147        // Each field "fXXX":1 is about 9 chars, we need ~450 fields for 4KB
1148        // But we're limited to 16 fields, so use long string values instead
1149        let long_value = "x".repeat(200);
1150        let cursor = Cursor::new()
1151            .string("f1", &long_value)
1152            .string("f2", &long_value)
1153            .string("f3", &long_value)
1154            .string("f4", &long_value);
1155
1156        let encoded = cursor.encode();
1157        assert!(encoded.len() < 4096, "Cursor should be under 4KB limit");
1158
1159        // Should decode successfully
1160        let decoded = Cursor::decode(&encoded);
1161        assert!(decoded.is_ok());
1162    }
1163
1164    #[test]
1165    fn test_cursor_exactly_at_max_size_boundary() {
1166        // The check is `> MAX_CURSOR_SIZE`, so exactly 4096 passes
1167        // Test cursor at exactly 4097 bytes (should fail)
1168        let oversized = "a".repeat(4097);
1169        let result = Cursor::decode(&oversized);
1170        assert!(matches!(result, Err(CursorError::TooLarge)));
1171
1172        // Test at exactly 4096 bytes (should attempt decode, not TooLarge)
1173        let at_limit = "a".repeat(4096);
1174        let result = Cursor::decode(&at_limit);
1175        // May be InvalidBase64 or InvalidFormat, but not TooLarge
1176        assert!(!matches!(result, Err(CursorError::TooLarge)));
1177    }
1178
1179    #[test]
1180    fn test_into_cursor_boundary_behavior() {
1181        // Empty string
1182        let cursor: Option<Cursor> = "".into_cursor();
1183        assert!(cursor.is_none(), "Empty string should return None");
1184
1185        // At size limit
1186        let oversized = "a".repeat(4097);
1187        let cursor: Option<Cursor> = oversized.as_str().into_cursor();
1188        assert!(cursor.is_none(), "Oversized cursor should return None");
1189    }
1190
1191    #[test]
1192    fn test_cursor_with_various_value_types() {
1193        // Test cursor with all supported value types
1194        let cursor = Cursor::new()
1195            .int("int_field", 42)
1196            .string("str_field", "hello")
1197            .field("float_field", 1.234f64)
1198            .field("bool_field", true);
1199
1200        let encoded = cursor.encode();
1201        let decoded = Cursor::decode(&encoded).unwrap();
1202
1203        assert_eq!(decoded.fields.len(), 4);
1204
1205        // Verify each field type
1206        assert!(matches!(
1207            decoded.fields.iter().find(|(k, _)| k == "int_field"),
1208            Some((_, Value::Int(42)))
1209        ));
1210        assert!(matches!(
1211            decoded.fields.iter().find(|(k, _)| k == "str_field"),
1212            Some((_, Value::String(s))) if s == "hello"
1213        ));
1214    }
1215
1216    #[test]
1217    fn test_cursor_with_special_json_characters() {
1218        // Test cursor with values that need JSON escaping
1219        let cursor = Cursor::new()
1220            .string("quotes", "say \"hello\"")
1221            .string("backslash", "path\\to\\file")
1222            .string("newline", "line1\nline2");
1223
1224        let encoded = cursor.encode();
1225        let decoded = Cursor::decode(&encoded).unwrap();
1226
1227        assert_eq!(decoded.fields.len(), 3);
1228    }
1229
1230    #[test]
1231    fn test_keyset_with_missing_cursor_field() {
1232        // Sort by field not in cursor should return None
1233        let sorts = vec![SortField::new("missing_field", SortDir::Asc)];
1234        let cursor = Cursor::new().int("id", 100);
1235
1236        let condition = KeysetCondition::after(&sorts, &cursor);
1237        assert!(
1238            condition.is_none(),
1239            "Should return None when cursor missing required field"
1240        );
1241    }
1242
1243    #[test]
1244    fn test_keyset_with_empty_sorts() {
1245        let cursor = Cursor::new().int("id", 100);
1246        let condition = KeysetCondition::after(&[], &cursor);
1247        assert!(
1248            condition.is_none(),
1249            "Should return None for empty sort list"
1250        );
1251    }
1252}