paginate_core/sort/mod.rs
1//! Stable, multi-key in-memory sort with null placement.
2//!
3//! Faithful port of pypaginate's `sorting/`:
4//!
5//! * Each spec contributes a `(null_flag, value)` key; `reverse` (for DESC) is
6//! applied to the whole key so null placement stays independent of direction.
7//! * Specs are applied in **reverse priority order** with a **stable** sort, so
8//! the first spec wins ties — identical to Python's repeated `list.sort`.
9//! * A missing field or an explicit null is treated as null (the Python engine
10//! catches the accessor error); incomparable values raise [`CoreError::Sort`].
11//!
12//! [`sort_indices`] returns a permutation so the binding can reorder the
13//! original host objects without cloning them through the core.
14
15use std::cmp::Ordering;
16
17use crate::accessor::{compile_path, resolve_opt};
18use crate::coerce;
19use crate::error::{CoreError, Result};
20use crate::value::Value;
21
22/// Sort direction.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum SortDirection {
26 /// Ascending order.
27 Asc,
28 /// Descending order.
29 Desc,
30}
31
32impl SortDirection {
33 /// Parse the wire token (`"asc"` / `"desc"`) shared by every binding.
34 ///
35 /// # Errors
36 /// [`CoreError::Sort`] for any other token.
37 pub fn from_token(token: &str) -> Result<Self> {
38 match token {
39 "asc" => Ok(Self::Asc),
40 "desc" => Ok(Self::Desc),
41 other => Err(CoreError::Sort {
42 message: format!("unknown sort direction: {other}"),
43 }),
44 }
45 }
46}
47
48/// Where nulls are placed relative to non-null values.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum NullsPosition {
52 /// Nulls sort before non-null values.
53 First,
54 /// Nulls sort after non-null values.
55 Last,
56}
57
58impl NullsPosition {
59 /// Parse the wire token (`"first"` / `"last"`) shared by every binding.
60 ///
61 /// # Errors
62 /// [`CoreError::Sort`] for any other token.
63 pub fn from_token(token: &str) -> Result<Self> {
64 match token {
65 "first" => Ok(Self::First),
66 "last" => Ok(Self::Last),
67 other => Err(CoreError::Sort {
68 message: format!("unknown nulls position: {other}"),
69 }),
70 }
71 }
72}
73
74/// A single sort key.
75#[derive(Debug, Clone)]
76pub struct SortSpec {
77 /// Dotted field path to sort by.
78 pub field: String,
79 /// Ascending or descending.
80 pub direction: SortDirection,
81 /// Null placement.
82 pub nulls: NullsPosition,
83}
84
85/// Return a permutation of `items` indices sorted by `specs` (first = highest
86/// priority). With no specs the original order is returned.
87///
88/// # Errors
89/// [`CoreError::Filter`] for a path segment starting with `_`, or
90/// [`CoreError::Sort`] when two field values are not order-comparable.
91pub fn sort_indices(items: &[Value], specs: &[SortSpec]) -> Result<Vec<usize>> {
92 sort_indices_of(items, (0..items.len()).collect(), specs)
93}
94
95/// Sort an existing index list (a subset/permutation of `items`) by `specs`,
96/// preserving its relative order for equal keys (stable). The pipeline uses this
97/// to sort a *filtered* subset without disturbing the rest.
98///
99/// # Errors
100/// See [`sort_indices`].
101pub fn sort_indices_of(
102 items: &[Value],
103 mut order: Vec<usize>,
104 specs: &[SortSpec],
105) -> Result<Vec<usize>> {
106 if specs.is_empty() {
107 return Ok(order);
108 }
109 // Apply specs in reverse so the first spec has highest priority under a
110 // stable sort — exactly how the Python engine layers `list.sort` calls.
111 for spec in specs.iter().rev() {
112 let path = compile_path(&spec.field)?;
113 let null_first = null_sorts_first(spec.direction, spec.nulls);
114 let reverse = spec.direction == SortDirection::Desc;
115 // Decorate: resolve each item's sort key ONCE, not on every comparison
116 // (a comparison sort does O(n log n) compares — re-resolving the field
117 // each time dominated the cost). `keys[i]` borrows item `i`'s value.
118 let keys: Vec<Option<&Value>> = items.iter().map(|it| resolve_opt(it, &path)).collect();
119 let mut failure: Option<CoreError> = None;
120 order.sort_by(|&a, &b| {
121 if failure.is_some() {
122 return Ordering::Equal;
123 }
124 match compare_keys(keys[a], keys[b], null_first) {
125 Ok(ordering) if reverse => ordering.reverse(),
126 Ok(ordering) => ordering,
127 Err(err) => {
128 failure = Some(err);
129 Ordering::Equal
130 }
131 }
132 });
133 if let Some(err) = failure {
134 return Err(err);
135 }
136 }
137 Ok(order)
138}
139
140/// Convenience wrapper around [`sort_indices`] that clones items into order.
141///
142/// # Errors
143/// See [`sort_indices`].
144pub fn apply(items: &[Value], specs: &[SortSpec]) -> Result<Vec<Value>> {
145 let order = sort_indices(items, specs)?;
146 Ok(order.into_iter().map(|i| items[i].clone()).collect())
147}
148
149fn null_sorts_first(direction: SortDirection, nulls: NullsPosition) -> bool {
150 (nulls == NullsPosition::First) != (direction == SortDirection::Desc)
151}
152
153/// Compare two precomputed keys as the tuple `(null_flag, value)`.
154fn compare_keys(av: Option<&Value>, bv: Option<&Value>, null_first: bool) -> Result<Ordering> {
155 let a_flag = null_first ^ av.is_none();
156 let b_flag = null_first ^ bv.is_none();
157 if a_flag != b_flag {
158 return Ok(a_flag.cmp(&b_flag));
159 }
160 match (av, bv) {
161 (Some(x), Some(y)) => coerce::compare(x, y).ok_or_else(|| CoreError::Sort {
162 message: "field values are not order-comparable".to_owned(),
163 }),
164 _ => Ok(Ordering::Equal),
165 }
166}
167
168#[cfg(test)]
169mod tests;