kevy_store/list_ops.rs
1//! `Store` list ops needed for BullMQ end-to-end:
2//! `RPOPLPUSH`, `LMOVE`, `LPOS`. Kept in a sibling module to keep
3//! `list.rs` under the 500-LOC house rule.
4//!
5//! All three are local-shard-only — the cross-shard
6//! Take→Put orchestrator (mirroring `RENAME`'s `exec_rename`) is a
7//! later runtime concern; the dispatch layer routes by source key and
8//! these helpers operate on whatever the local `Store` holds for `dst`.
9
10#[cfg(not(feature = "std"))]
11use crate::nostd_prelude::*;
12use crate::value::Value;
13use crate::{Store, StoreError};
14
15impl Store {
16 /// `RPOPLPUSH source destination` — atomically pop one element from
17 /// the tail of `src` and push it onto the head of `dst`. Returns the
18 /// moved element, or `None` if `src` was empty / absent.
19 ///
20 /// When `src == dst` Redis defines the result as a rotation
21 /// (tail → head of the same list), which falls out of this code
22 /// naturally because the pop sees the pre-rotation tail.
23 pub fn rpoplpush(
24 &mut self,
25 src: &[u8],
26 dst: &[u8],
27 ) -> Result<Option<Vec<u8>>, StoreError> {
28 // WRONGTYPE pre-check on dst: if dst exists but isn't a list,
29 // we must reject BEFORE consuming the src element (Redis: the
30 // pop is reverted on WRONGTYPE at the destination).
31 match self.live_entry(dst) {
32 None => {}
33 Some(e) => match &e.value {
34 Value::List(_) | Value::SegList(_) | Value::SmallListInline(_) => {}
35 _ => return Err(StoreError::WrongType),
36 },
37 }
38 let mut popped = self.rpop(src, 1)?;
39 let Some(v) = popped.pop() else {
40 return Ok(None);
41 };
42 // Push to the head of dst. `lpush` returns the new
43 // length; we want the popped value back to the caller.
44 self.lpush(dst, &[v.as_slice()])?;
45 Ok(Some(v))
46 }
47
48 /// `LMOVE source destination LEFT|RIGHT LEFT|RIGHT` — generalised
49 /// `RPOPLPUSH`. `from_left=true` pops from the head, otherwise the
50 /// tail; `to_left=true` pushes to the head, otherwise the tail.
51 pub fn lmove(
52 &mut self,
53 src: &[u8],
54 dst: &[u8],
55 from_left: bool,
56 to_left: bool,
57 ) -> Result<Option<Vec<u8>>, StoreError> {
58 match self.live_entry(dst) {
59 None => {}
60 Some(e) => match &e.value {
61 Value::List(_) | Value::SegList(_) | Value::SmallListInline(_) => {}
62 _ => return Err(StoreError::WrongType),
63 },
64 }
65 let mut popped = if from_left {
66 self.lpop(src, 1)?
67 } else {
68 self.rpop(src, 1)?
69 };
70 let Some(v) = popped.pop() else {
71 return Ok(None);
72 };
73 if to_left {
74 self.lpush(dst, &[v.as_slice()])?;
75 } else {
76 self.rpush(dst, &[v.as_slice()])?;
77 }
78 Ok(Some(v))
79 }
80
81 /// `LPOS key element [RANK n] [COUNT n] [MAXLEN n]` — find the
82 /// zero-based position(s) of `element` in the list.
83 ///
84 /// * `rank > 0` — scan head→tail, skipping the first `rank-1`
85 /// matches. `rank == 1` (default) returns the first match.
86 /// * `rank < 0` — scan tail→head, returning matches as
87 /// absolute (head-relative) indices.
88 /// * `count` — `None` returns the first match as a 1-element vec
89 /// (caller emits an integer / nil); `Some(0)` returns all
90 /// matches; `Some(n)` caps to `n`.
91 /// * `maxlen` — `0` means unlimited; otherwise stop after
92 /// scanning that many elements (in the chosen direction).
93 ///
94 /// Returns the matched indices in scan order. An empty result with
95 /// `count == None` is the caller's signal to emit RESP nil.
96 pub fn lpos(
97 &mut self,
98 key: &[u8],
99 element: &[u8],
100 rank: i64,
101 count: Option<i64>,
102 maxlen: usize,
103 ) -> Result<Vec<i64>, StoreError> {
104 if rank == 0 {
105 return Err(StoreError::OutOfRange);
106 }
107 if let Some(c) = count
108 && c < 0 {
109 return Err(StoreError::OutOfRange);
110 }
111 let entries: Vec<Vec<u8>> = match self.live_entry(key) {
112 None => return Ok(Vec::new()),
113 Some(e) => match &e.value {
114 Value::List(l) => l.iter().cloned().collect(),
115 Value::SegList(l) => l.iter().cloned().collect(),
116 Value::SmallListInline(l) => l.iter().map(<[u8]>::to_vec).collect(),
117 _ => return Err(StoreError::WrongType),
118 },
119 };
120 let n = entries.len();
121 if n == 0 {
122 return Ok(Vec::new());
123 }
124 let skip = (rank.unsigned_abs() as usize).saturating_sub(1);
125 let cap = match count {
126 None => 1,
127 Some(0) => usize::MAX,
128 Some(c) => c as usize,
129 };
130 let want_reverse = rank < 0;
131 let scan_limit = if maxlen == 0 { n } else { maxlen.min(n) };
132 Ok(lpos_scan(&entries, element, skip, cap, want_reverse, scan_limit))
133 }
134}
135
136/// The scan loop of [`Store::lpos`]: walk `entries` (reversed for a
137/// negative rank), skip the first `skip` matches, collect up to `cap`
138/// matched indices, stop after `scan_limit` scanned elements.
139fn lpos_scan(
140 entries: &[Vec<u8>],
141 element: &[u8],
142 skip: usize,
143 cap: usize,
144 want_reverse: bool,
145 scan_limit: usize,
146) -> Vec<i64> {
147 let mut out = Vec::new();
148 let mut skipped = 0usize;
149 let iter: Box<dyn Iterator<Item = (usize, &Vec<u8>)>> = if want_reverse {
150 Box::new(entries.iter().enumerate().rev())
151 } else {
152 Box::new(entries.iter().enumerate())
153 };
154 for (idx, v) in iter.take(scan_limit) {
155 if v.as_slice() == element {
156 if skipped < skip {
157 skipped += 1;
158 continue;
159 }
160 out.push(idx as i64);
161 if out.len() >= cap {
162 break;
163 }
164 }
165 }
166 out
167}