Skip to main content

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}