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(&mut self, src: &[u8], dst: &[u8]) -> Result<Option<Vec<u8>>, StoreError> {
24        // WRONGTYPE pre-check on dst: if dst exists but isn't a list,
25        // we must reject BEFORE consuming the src element (Redis: the
26        // pop is reverted on WRONGTYPE at the destination).
27        match self.live_entry(dst) {
28            None => {}
29            Some(e) => match &e.value {
30                Value::List(_) | Value::SegList(_) | Value::SmallListInline(_) => {}
31                _ => return Err(StoreError::WrongType),
32            },
33        }
34        let mut popped = self.rpop(src, 1)?;
35        let Some(v) = popped.pop() else {
36            return Ok(None);
37        };
38        // Push to the head of dst. `lpush` returns the new
39        // length; we want the popped value back to the caller.
40        self.lpush(dst, &[v.as_slice()])?;
41        Ok(Some(v))
42    }
43
44    /// `LMOVE source destination LEFT|RIGHT LEFT|RIGHT` — generalised
45    /// `RPOPLPUSH`. `from_left=true` pops from the head, otherwise the
46    /// tail; `to_left=true` pushes to the head, otherwise the tail.
47    pub fn lmove(
48        &mut self,
49        src: &[u8],
50        dst: &[u8],
51        from_left: bool,
52        to_left: bool,
53    ) -> Result<Option<Vec<u8>>, StoreError> {
54        match self.live_entry(dst) {
55            None => {}
56            Some(e) => match &e.value {
57                Value::List(_) | Value::SegList(_) | Value::SmallListInline(_) => {}
58                _ => return Err(StoreError::WrongType),
59            },
60        }
61        let mut popped = if from_left { self.lpop(src, 1)? } else { self.rpop(src, 1)? };
62        let Some(v) = popped.pop() else {
63            return Ok(None);
64        };
65        if to_left {
66            self.lpush(dst, &[v.as_slice()])?;
67        } else {
68            self.rpush(dst, &[v.as_slice()])?;
69        }
70        Ok(Some(v))
71    }
72
73    /// `LPOS key element [RANK n] [COUNT n] [MAXLEN n]` — find the
74    /// zero-based position(s) of `element` in the list.
75    ///
76    /// * `rank > 0` — scan head→tail, skipping the first `rank-1`
77    ///   matches. `rank == 1` (default) returns the first match.
78    /// * `rank < 0` — scan tail→head, returning matches as
79    ///   absolute (head-relative) indices.
80    /// * `count` — `None` returns the first match as a 1-element vec
81    ///   (caller emits an integer / nil); `Some(0)` returns all
82    ///   matches; `Some(n)` caps to `n`.
83    /// * `maxlen` — `0` means unlimited; otherwise stop after
84    ///   scanning that many elements (in the chosen direction).
85    ///
86    /// Returns the matched indices in scan order. An empty result with
87    /// `count == None` is the caller's signal to emit RESP nil.
88    pub fn lpos(
89        &mut self,
90        key: &[u8],
91        element: &[u8],
92        rank: i64,
93        count: Option<i64>,
94        maxlen: usize,
95    ) -> Result<Vec<i64>, StoreError> {
96        if rank == 0 {
97            return Err(StoreError::OutOfRange);
98        }
99        if let Some(c) = count
100            && c < 0
101        {
102            return Err(StoreError::OutOfRange);
103        }
104        let entries: Vec<Vec<u8>> = match self.live_entry(key) {
105            None => return Ok(Vec::new()),
106            Some(e) => match &e.value {
107                Value::List(l) => l.iter().cloned().collect(),
108                Value::SegList(l) => l.iter().cloned().collect(),
109                Value::SmallListInline(l) => l.iter().map(<[u8]>::to_vec).collect(),
110                _ => return Err(StoreError::WrongType),
111            },
112        };
113        let n = entries.len();
114        if n == 0 {
115            return Ok(Vec::new());
116        }
117        let skip = (rank.unsigned_abs() as usize).saturating_sub(1);
118        let cap = match count {
119            None => 1,
120            Some(0) => usize::MAX,
121            Some(c) => c as usize,
122        };
123        let want_reverse = rank < 0;
124        let scan_limit = if maxlen == 0 { n } else { maxlen.min(n) };
125        Ok(lpos_scan(&entries, element, skip, cap, want_reverse, scan_limit))
126    }
127}
128
129/// The scan loop of [`Store::lpos`]: walk `entries` (reversed for a
130/// negative rank), skip the first `skip` matches, collect up to `cap`
131/// matched indices, stop after `scan_limit` scanned elements.
132fn lpos_scan(
133    entries: &[Vec<u8>],
134    element: &[u8],
135    skip: usize,
136    cap: usize,
137    want_reverse: bool,
138    scan_limit: usize,
139) -> Vec<i64> {
140    let mut out = Vec::new();
141    let mut skipped = 0usize;
142    let iter: Box<dyn Iterator<Item = (usize, &Vec<u8>)>> = if want_reverse {
143        Box::new(entries.iter().enumerate().rev())
144    } else {
145        Box::new(entries.iter().enumerate())
146    };
147    for (idx, v) in iter.take(scan_limit) {
148        if v.as_slice() == element {
149            if skipped < skip {
150                skipped += 1;
151                continue;
152            }
153            out.push(idx as i64);
154            if out.len() >= cap {
155                break;
156            }
157        }
158    }
159    out
160}