Skip to main content

kevy_embedded/
ops_more.rs

1//! Set membership and pop, sorted-set ranking and pop, list `lset` /
2//! `ltrim`, and key rename: `sismember`, `spop`, `srandmember`,
3//! `zrank`, `zcount`, `zpopmin`, `zremrangebyrank`,
4//! `zremrangebyscore`, `zrev_range_by_score`, `lset`, `ltrim`,
5//! `rename`, `renamenx`.
6//!
7//! Every method wraps a corresponding `kevy_store::Store` method.
8
9use std::io;
10
11use kevy_store::ScoreBound;
12
13#[cfg(not(target_arch = "wasm32"))]
14use crate::replica_glue::ensure_writable;
15use crate::store::{Store, commit_write, store_err};
16
17#[cfg(target_arch = "wasm32")]
18fn ensure_writable(_s: &Store) -> io::Result<()> { Ok(()) }
19
20impl Store {
21    // ---- set extras --------------------------------------------------
22
23    /// `SISMEMBER key member` — `true` when `member` is in the set.
24    pub fn sismember(&self, key: &[u8], member: &[u8]) -> io::Result<bool> {
25        self.wshard(key).store.sismember(key, member).map_err(store_err)
26    }
27
28    /// `SPOP key count` — atomically remove + return up to `count`
29    /// random members.
30    ///
31    /// AOF form: logged as `SREM key <popped…>` (the members actually
32    /// removed), not `SPOP key count` — a random pick replayed against
33    /// a store whose internal layout differs (replica applying frames
34    /// onto snapshot-loaded state) would remove *different* members.
35    /// Redis propagates SPOP the same way.
36    pub fn spop(&self, key: &[u8], count: usize) -> io::Result<Vec<Vec<u8>>> {
37        ensure_writable(self)?;
38        let mut g = self.wshard(key);
39        let popped = g.store.spop(key, count).map_err(store_err)?;
40        if !popped.is_empty() {
41            let mut argv: Vec<&[u8]> = Vec::with_capacity(2 + popped.len());
42            argv.push(b"SREM");
43            argv.push(key);
44            argv.extend(popped.iter().map(Vec::as_slice));
45            commit_write(&mut g, &argv)?;
46        }
47        Ok(popped)
48    }
49
50    /// `SRANDMEMBER key count` — return up to `count` random members
51    /// without removing them.
52    pub fn srandmember(&self, key: &[u8], count: usize) -> io::Result<Vec<Vec<u8>>> {
53        self.wshard(key).store.srandmember(key, count).map_err(store_err)
54    }
55
56    // ---- sorted set extras ------------------------------------------
57
58    /// `ZRANK key member` — rank (0-based, ascending) of `member`;
59    /// `None` if not present.
60    pub fn zrank(&self, key: &[u8], member: &[u8]) -> io::Result<Option<usize>> {
61        self.wshard(key).store.zrank(key, member).map_err(store_err)
62    }
63
64    /// `ZCOUNT key min max` — count members whose score falls in
65    /// `[min, max]` (inclusive). Pass `±INFINITY` for open bounds.
66    pub fn zcount(&self, key: &[u8], min: f64, max: f64) -> io::Result<usize> {
67        self.wshard(key)
68            .store
69            .zcount(
70                key,
71                ScoreBound { value: min, exclusive: false },
72                ScoreBound { value: max, exclusive: false },
73            )
74            .map_err(store_err)
75    }
76
77    /// `ZPOPMIN key count` — atomically remove + return up to `count`
78    /// members with the lowest scores. Pairs are `(member, score)`.
79    pub fn zpopmin(&self, key: &[u8], count: usize) -> io::Result<Vec<(Vec<u8>, f64)>> {
80        ensure_writable(self)?;
81        let mut g = self.wshard(key);
82        let popped = g.store.zpopmin(key, count).map_err(store_err)?;
83        if !popped.is_empty() {
84            let count_str = format!("{count}");
85            commit_write(&mut g, &[b"ZPOPMIN", key, count_str.as_bytes()])?;
86        }
87        Ok(popped)
88    }
89
90    /// `ZREMRANGEBYRANK key start stop` — remove members in
91    /// `[start, stop]` rank range (inclusive, Redis-style negative
92    /// indexing). Returns count removed.
93    pub fn zremrangebyrank(
94        &self,
95        key: &[u8],
96        start: i64,
97        stop: i64,
98    ) -> io::Result<usize> {
99        ensure_writable(self)?;
100        let mut g = self.wshard(key);
101        let removed = g.store.zrem_range_by_rank(key, start, stop).map_err(store_err)?;
102        if removed > 0 {
103            let s = format!("{start}");
104            let e = format!("{stop}");
105            commit_write(&mut g, &[b"ZREMRANGEBYRANK", key, s.as_bytes(), e.as_bytes()])?;
106        }
107        Ok(removed)
108    }
109
110    /// `ZREMRANGEBYSCORE key min max` — remove members with scores
111    /// in `[min, max]` (inclusive). Returns count removed.
112    pub fn zremrangebyscore(
113        &self,
114        key: &[u8],
115        min: f64,
116        max: f64,
117    ) -> io::Result<usize> {
118        ensure_writable(self)?;
119        let mut g = self.wshard(key);
120        let removed = g
121            .store
122            .zrem_range_by_score(
123                key,
124                ScoreBound { value: min, exclusive: false },
125                ScoreBound { value: max, exclusive: false },
126            )
127            .map_err(store_err)?;
128        if removed > 0 {
129            let s = format!("{min}");
130            let e = format!("{max}");
131            commit_write(&mut g, &[b"ZREMRANGEBYSCORE", key, s.as_bytes(), e.as_bytes()])?;
132        }
133        Ok(removed)
134    }
135
136    /// `ZREVRANGEBYSCORE key max min` — members with scores in
137    /// `[min, max]` in DESCENDING score order. Inclusive bounds.
138    pub fn zrev_range_by_score(
139        &self,
140        key: &[u8],
141        max: f64,
142        min: f64,
143    ) -> io::Result<Vec<(Vec<u8>, f64)>> {
144        self.wshard(key)
145            .store
146            .zrev_range_by_score(
147                key,
148                ScoreBound { value: min, exclusive: false },
149                ScoreBound { value: max, exclusive: false },
150            )
151            .map_err(store_err)
152    }
153
154    // ---- list extras -------------------------------------------------
155
156    /// `LSET key idx value` — set the element at `idx` (negative
157    /// indexes count from tail). Errors `NoSuchKey` / `OutOfRange`
158    /// matching Redis.
159    pub fn lset(&self, key: &[u8], idx: i64, value: &[u8]) -> io::Result<()> {
160        ensure_writable(self)?;
161        let mut g = self.wshard(key);
162        g.store.lset(key, idx, value).map_err(store_err)?;
163        let i = format!("{idx}");
164        commit_write(&mut g, &[b"LSET", key, i.as_bytes(), value])?;
165        Ok(())
166    }
167
168    /// `LTRIM key start stop` — trim list to `[start, stop]`
169    /// inclusive (Redis-style negative indexing).
170    pub fn ltrim(&self, key: &[u8], start: i64, stop: i64) -> io::Result<()> {
171        ensure_writable(self)?;
172        let mut g = self.wshard(key);
173        g.store.ltrim(key, start, stop).map_err(store_err)?;
174        let s = format!("{start}");
175        let e = format!("{stop}");
176        commit_write(&mut g, &[b"LTRIM", key, s.as_bytes(), e.as_bytes()])?;
177        Ok(())
178    }
179
180    // ---- keyspace extras --------------------------------------------
181
182    /// `RENAME src dst` — atomic rename. Returns `true` when the
183    /// rename happened. Errors when `src` doesn't exist (Redis would
184    /// reply `-ERR no such key`, here `Err(NoSuchKey)`).
185    pub fn rename(&self, src: &[u8], dst: &[u8]) -> io::Result<bool> {
186        ensure_writable(self)?;
187        // Cross-shard rename is non-trivial; the single-shard
188        // embedded default lands src+dst on the same lock.
189        let mut g = self.wshard(src);
190        let outcome = g.store.rename(src, dst, false);
191        match outcome {
192            kevy_store::RenameOutcome::Renamed => {
193                commit_write(&mut g, &[b"RENAME", src, dst])?;
194                Ok(true)
195            }
196            kevy_store::RenameOutcome::NoSuchSrc => {
197                Err(io::Error::other("no such key"))
198            }
199            kevy_store::RenameOutcome::DstExists => Ok(false),
200        }
201    }
202
203    /// `RENAMENX src dst` — rename only when `dst` doesn't exist.
204    /// Returns `true` when the rename happened.
205    pub fn renamenx(&self, src: &[u8], dst: &[u8]) -> io::Result<bool> {
206        ensure_writable(self)?;
207        let mut g = self.wshard(src);
208        let outcome = g.store.rename(src, dst, true);
209        match outcome {
210            kevy_store::RenameOutcome::Renamed => {
211                commit_write(&mut g, &[b"RENAMENX", src, dst])?;
212                Ok(true)
213            }
214            kevy_store::RenameOutcome::DstExists => Ok(false),
215            kevy_store::RenameOutcome::NoSuchSrc => {
216                Err(io::Error::other("no such key"))
217            }
218        }
219    }
220}