Skip to main content

kevy_store/
string.rs

1//! `Store` string read commands (GET family) + INCRBY. The SET-family
2//! write path lives in `string_set.rs` (500-LOC house cap).
3
4#[cfg(not(feature = "std"))]
5use crate::nostd_prelude::*;
6use crate::util::{format_i64_into, itoa_i64_stack, parse_i64};
7use crate::value::{SmallBytes, Value};
8use crate::{Entry, Store, StoreError};
9use alloc::borrow::Cow;
10use alloc::sync::Arc;
11
12/// L1 return shape for [`Store::get_for_reply`] — lets the reactor's reply
13/// path choose between memcpy (`Bytes`) and writev zero-copy (`ArcBulk`)
14/// off one keyspace lookup.
15///
16/// ```
17/// use kevy_store::{GetReply, SetCondition, Store};
18/// let mut s = Store::new();
19/// s.set(b"small", b"hi".to_vec(), None, SetCondition::Always);
20/// s.set(b"big", vec![b'x'; 1024], None, SetCondition::Always);
21/// assert!(matches!(s.get_for_reply(b"small")?, Some(GetReply::Bytes(_))));
22/// assert!(matches!(s.get_for_reply(b"big")?, Some(GetReply::ArcBulk(_))));
23/// assert!(s.get_for_reply(b"missing")?.is_none());
24/// # Ok::<(), kevy_store::StoreError>(())
25/// ```
26#[derive(Debug)]
27pub enum GetReply<'a> {
28    /// Inline-encoded value — caller memcpys the bytes into its output Vec
29    /// (small replies; encoding cost is tiny vs the RTT floor).
30    ///
31    /// ```
32    /// use kevy_store::{GetReply, SetCondition, Store};
33    /// let mut s = Store::new();
34    /// s.set(b"n", b"42".to_vec(), None, SetCondition::Always);
35    /// let Some(GetReply::Bytes(b)) = s.get_for_reply(b"n")? else { panic!() };
36    /// assert_eq!(&*b, b"42");
37    /// # Ok::<(), kevy_store::StoreError>(())
38    /// ```
39    Bytes(Cow<'a, [u8]>),
40    /// Arc-backed bulk. The reactor's reply path pushes
41    /// the Arc into the conn's `output_arcs` so the next `writev` iovec
42    /// list points DIRECTLY at the value bytes — skipping the per-GET
43    /// memcpy that valkey's `tryAvoidBulkStrCopyToReply` likewise avoids.
44    ///
45    /// ```
46    /// use kevy_store::{GetReply, SetCondition, Store};
47    /// let mut s = Store::new();
48    /// s.set(b"big", vec![b'x'; 1024], None, SetCondition::Always);
49    /// let Some(GetReply::ArcBulk(a)) = s.get_for_reply(b"big")? else { panic!() };
50    /// // the reply holds the stored bytes themselves, kept alive by the Arc
51    /// assert_eq!(a.len(), 1024);
52    /// # Ok::<(), kevy_store::StoreError>(())
53    /// ```
54    ArcBulk(Arc<Box<[u8]>>),
55}
56
57/// Owned GET result for the FFI zero-copy shared lane
58/// ([`Store::get_shared_owned`]). Bulk values ride out as an Arc clone (no
59/// byte copy); small values as a plain Vec (one alloc — cheaper than a fresh
60/// Arc). The FFI's shared free reconstructs whichever the tag says.
61///
62/// ```
63/// use kevy_store::{GetShared, SetCondition, Store};
64/// let mut s = Store::new();
65/// s.set(b"k", b"v".to_vec(), None, SetCondition::Always);
66/// assert!(matches!(s.get_shared_owned(b"k")?, Some(GetShared::Bytes(_))));
67/// assert!(s.get_shared_owned(b"missing")?.is_none());
68/// # Ok::<(), kevy_store::StoreError>(())
69/// ```
70#[derive(Debug)]
71pub enum GetShared {
72    /// Bulk value — the engine's Arc, cloned. Zero byte copy.
73    ///
74    /// ```
75    /// use kevy_store::{GetShared, SetCondition, Store};
76    /// let mut s = Store::new();
77    /// s.set(b"big", vec![b'x'; 1024], None, SetCondition::Always);
78    /// let Some(GetShared::Arc(a)) = s.get_shared_owned(b"big")? else { panic!() };
79    /// assert_eq!(a.len(), 1024);
80    /// # Ok::<(), kevy_store::StoreError>(())
81    /// ```
82    Arc(Arc<Box<[u8]>>),
83    /// Small value (Str/Int) — a plain owned Vec, one allocation.
84    ///
85    /// ```
86    /// use kevy_store::{GetShared, SetCondition, Store};
87    /// let mut s = Store::new();
88    /// s.set(b"n", b"7".to_vec(), None, SetCondition::Always);
89    /// let Some(GetShared::Bytes(b)) = s.get_shared_owned(b"n")? else { panic!() };
90    /// assert_eq!(b, b"7");
91    /// # Ok::<(), kevy_store::StoreError>(())
92    /// ```
93    Bytes(Vec<u8>),
94}
95
96impl Store {
97    // ---- strings -------------------------------------------------------
98    /// GET variant that exposes the underlying encoding
99    /// so the reactor's reply path can choose zero-copy
100    /// (`Value::ArcBulk` → push the Arc to the conn's `output_arcs` for a
101    /// writev iovec) vs memcpy (`Value::Str` / `Value::Int` → encode bytes
102    /// into the conn's output Vec). ONE keyspace lookup; the variant tag
103    /// chooses the encoding without a second probe.
104    pub fn get_for_reply(&mut self, key: &[u8]) -> Result<Option<GetReply<'_>>, StoreError> {
105        match self.tier_serve(key, crate::value::COLD_TAG_STRING)? {
106            None => Ok(None),
107            Some(e) => match &e.value {
108                Value::Str(v) => Ok(Some(GetReply::Bytes(Cow::Borrowed(v.as_slice())))),
109                Value::ArcBulk(a) => Ok(Some(GetReply::ArcBulk(Arc::clone(a)))),
110                Value::Int(n) => {
111                    let mut tmp = itoa_i64_stack();
112                    let s = format_i64_into(*n, &mut tmp);
113                    Ok(Some(GetReply::Bytes(Cow::Owned(s.to_vec()))))
114                }
115                _ => Err(StoreError::WrongType),
116            },
117        }
118    }
119
120    /// Owned GET for the FFI scalar *shared* lane (`kevy_get_shared`). Bulk
121    /// values (`Value::ArcBulk`) return an `Arc::clone` — **no byte copy**; the
122    /// FFI holds the Arc alive and hands JS a buffer that views it directly
123    /// (mirrors MMKV's zero-copy mmap-page view, the thing that made kevy lose
124    /// large GET). Small values (`Str`/`Int`) allocate a fresh `Arc<Box<[u8]>>`
125    /// — the same one copy the Vec lane already pays — so the caller's free path
126    /// is uniform. Read-only (`&self`, like [`Self::get_shared`]) so the FFI
127    /// can take it under a SHARED shard lock — no LRU stamp, matching the
128    /// `maxmemory == 0` fast path the mobile door runs on. Wrong type errors
129    /// like [`Self::get_for_reply`].
130    pub fn get_shared_owned(&self, key: &[u8]) -> Result<Option<GetShared>, StoreError> {
131        match self.map.get(key) {
132            None => Ok(None),
133            Some(e) if e.is_expired(self.cached_clock, self.cached_ns) => Ok(None),
134            // Bulk (already Arc-backed) → clone the Arc: ZERO byte copy. Small
135            // (`Str`/`Int`) → a plain Vec (one alloc, cheaper than wrapping a
136            // fresh Arc — the FFI's shared free handles either).
137            Some(e) => match &e.value {
138                Value::ArcBulk(a) => Ok(Some(GetShared::Arc(Arc::clone(a)))),
139                Value::Str(v) => Ok(Some(GetShared::Bytes(v.as_slice().to_vec()))),
140                Value::Int(n) => {
141                    let mut tmp = itoa_i64_stack();
142                    let s = format_i64_into(*n, &mut tmp);
143                    Ok(Some(GetShared::Bytes(s.to_vec())))
144                }
145                // Cold, `&self` shared lane: pread a fresh value — never
146                // promotes, never sets the probation mark (it cannot:
147                // no `&mut`). Documented: shared-lane reads pay a pread
148                // until a `&mut`-path access promotes the key.
149                Value::Cold(c) if c.type_tag == crate::value::COLD_TAG_STRING => {
150                    match self.tier_peek_value(key, &e.value).expect("cold peek") {
151                        Value::ArcBulk(a) => Ok(Some(GetShared::Arc(a))),
152                        v => Ok(Some(GetShared::Bytes(cold_string_bytes(&v)))),
153                    }
154                }
155                _ => Err(StoreError::WrongType),
156            },
157        }
158    }
159
160    /// [`Store::get_shared_owned`] that lends the value to `f` instead of
161    /// handing out an owner: the bytes as the store holds them for a hot
162    /// value (an integer formatted on the stack), a read copy for a cold
163    /// one. For a caller that copies the bytes somewhere of its own anyway —
164    /// a binding building its reply — this saves the allocation a small
165    /// value's owner would cost. Non-mutating, like the owner lane.
166    ///
167    /// ```
168    /// let mut s = kevy_store::Store::new();
169    /// s.set(b"k", b"v".to_vec(), None, kevy_store::SetCondition::Always);
170    /// assert_eq!(s.get_shared_with(b"k", |v| v.map(<[u8]>::len)), Ok(Some(1)));
171    /// assert_eq!(s.get_shared_with(b"none", |v| v.is_none()), Ok(true));
172    /// ```
173    pub fn get_shared_with<R>(
174        &self,
175        key: &[u8],
176        f: impl FnOnce(Option<&[u8]>) -> R,
177    ) -> Result<R, StoreError> {
178        let Some(e) =
179            self.map.get(key).filter(|e| !e.is_expired(self.cached_clock, self.cached_ns))
180        else {
181            return Ok(f(None));
182        };
183        match &e.value {
184            Value::ArcBulk(a) => Ok(f(Some(a))),
185            Value::Str(v) => Ok(f(Some(v.as_slice()))),
186            Value::Int(n) => {
187                let mut tmp = itoa_i64_stack();
188                Ok(f(Some(format_i64_into(*n, &mut tmp))))
189            }
190            Value::Cold(c) if c.type_tag == crate::value::COLD_TAG_STRING => {
191                match self.tier_peek_value(key, &e.value).expect("cold peek") {
192                    Value::ArcBulk(a) => Ok(f(Some(&a))),
193                    v => Ok(f(Some(&cold_string_bytes(&v)))),
194                }
195            }
196            _ => Err(StoreError::WrongType),
197        }
198    }
199
200    /// Fused GET-into-output. Skips the [`GetReply`] enum tag
201    /// round-trip + caller match arm by writing the RESP frame directly into
202    /// `output` (header + bytes + CRLF for Str/Int) or pushing the Arc into
203    /// `output_arcs` at the right offset (ArcBulk zero-copy via writev).
204    /// Returns the same outcomes as [`Self::get_for_reply`]: `Ok(true)` if
205    /// the key was found and emitted, `Ok(false)` if absent (the caller
206    /// emits the `$-1` null bulk — preserves the existing inline-null
207    /// semantics on the reactor side), `Err` for WRONGTYPE.
208    pub fn get_into_output(
209        &mut self,
210        key: &[u8],
211        output: &mut Vec<u8>,
212        output_arcs: &mut Vec<(usize, Arc<Box<[u8]>>)>,
213    ) -> Result<bool, StoreError> {
214        match self.tier_serve(key, crate::value::COLD_TAG_STRING)? {
215            None => Ok(false),
216            Some(e) => match &e.value {
217                Value::Str(v) => {
218                    let bytes = v.as_slice();
219                    crate::util::bulk_header_into(output, bytes.len());
220                    output.extend_from_slice(bytes);
221                    output.extend_from_slice(b"\r\n");
222                    Ok(true)
223                }
224                Value::ArcBulk(a) => {
225                    crate::util::bulk_header_into(output, a.len());
226                    let pos = output.len();
227                    output_arcs.push((pos, Arc::clone(a)));
228                    output.extend_from_slice(b"\r\n");
229                    Ok(true)
230                }
231                Value::Int(n) => {
232                    let mut tmp = itoa_i64_stack();
233                    let s = format_i64_into(*n, &mut tmp);
234                    crate::util::bulk_header_into(output, s.len());
235                    output.extend_from_slice(s);
236                    output.extend_from_slice(b"\r\n");
237                    Ok(true)
238                }
239                _ => Err(StoreError::WrongType),
240            },
241        }
242    }
243
244    /// `GET` — returns a `Cow<[u8]>` so `Value::Int` callers can format the
245    /// integer to ASCII without storing it. `Value::Str`
246    /// returns `Cow::Borrowed` (zero copy); `Value::Int`
247    /// formats to a small owned `Vec<u8>` (up to 20 bytes for `i64::MIN`).
248    pub fn get(&mut self, key: &[u8]) -> Result<Option<Cow<'_, [u8]>>, StoreError> {
249        match self.tier_serve(key, crate::value::COLD_TAG_STRING)? {
250            None => Ok(None),
251            Some(e) => match &e.value {
252                Value::Str(v) => Ok(Some(Cow::Borrowed(v.as_slice()))),
253                // L1: Arc-backed bulk — return borrow into the Arc's
254                // bytes. Caller can either memcpy via Cow::Borrowed
255                // (default `encode_bulk` path) OR look up the
256                // underlying `Value::ArcBulk(arc)` separately for the
257                // writev zero-copy reply path.
258                Value::ArcBulk(a) => Ok(Some(Cow::Borrowed(a.as_ref()))),
259                Value::Int(n) => {
260                    let mut tmp = itoa_i64_stack();
261                    let s = format_i64_into(*n, &mut tmp);
262                    Ok(Some(Cow::Owned(s.to_vec())))
263                }
264                _ => Err(StoreError::WrongType),
265            },
266        }
267    }
268
269    /// Read-only `GET`: `&self`, so concurrent readers can run under a shared
270    /// lock (embedded mode's `RwLock` read path). Expiry is checked against the
271    /// coarse cached clock but an expired key is *not* removed here (no `&mut`)
272    /// — the reaper / next write reclaims it; a reader just sees `None`. LRU is
273    /// not touched, so this path is only used when eviction is off
274    /// (`maxmemory == 0`); with eviction, the caller takes the mutating
275    /// [`Self::get`] under an exclusive lock so access still stamps the LRU.
276    pub fn get_shared(&self, key: &[u8]) -> Result<Option<Cow<'_, [u8]>>, StoreError> {
277        match self.map.get(key) {
278            None => Ok(None),
279            Some(e) if e.is_expired(self.cached_clock, self.cached_ns) => Ok(None),
280            Some(e) => match &e.value {
281                Value::Str(v) => Ok(Some(Cow::Borrowed(v.as_slice()))),
282                Value::ArcBulk(a) => Ok(Some(Cow::Borrowed(a.as_ref()))),
283                Value::Int(n) => {
284                    let mut tmp = itoa_i64_stack();
285                    let s = format_i64_into(*n, &mut tmp);
286                    Ok(Some(Cow::Owned(s.to_vec())))
287                }
288                // Cold, `&self` shared lane — see `get_shared_owned`.
289                Value::Cold(c) if c.type_tag == crate::value::COLD_TAG_STRING => {
290                    let v = self.tier_peek_value(key, &e.value).expect("cold peek");
291                    Ok(Some(Cow::Owned(cold_string_bytes(&v))))
292                }
293                _ => Err(StoreError::WrongType),
294            },
295        }
296    }
297
298    /// Byte length of a string value. A missing key is 0, matching
299    /// STRLEN; an integer-encoded value reports the length it would
300    /// format to, not 8.
301    pub fn strlen(&mut self, key: &[u8]) -> Result<usize, StoreError> {
302        Ok(self.get(key)?.map_or(0, |c| c.len()))
303    }
304
305    /// `INCRBY` family; preserves any TTL.
306    ///
307    /// Following valkey's OBJ_ENCODING_INT approach: the hot path
308    /// matches `Value::Int(n)` and does the increment in place — no parse,
309    /// no format, no allocation. The `Value::Str` arm parses,
310    /// increments, and **promotes** to `Value::Int(next)` so subsequent
311    /// INCRs land on the fast path. Insert-new path also lands as `Int`.
312    pub fn incr_by(&mut self, key: &[u8], delta: i64) -> Result<i64, StoreError> {
313        self.tier_resolve(key, crate::value::COLD_TAG_STRING)?; // cold string pages in
314
315        let outcome = match self.live_entry_mut(key) {
316            Some(e) => match &mut e.value {
317                Value::Int(n) => {
318                    let next = n.checked_add(delta).ok_or(StoreError::Overflow)?;
319                    *n = next;
320                    // In-place i64 mutation — weight unchanged (still 0
321                    // heap bytes for an Int). Skip the reweigh entirely.
322                    return Ok(next);
323                }
324                Value::Str(v) => {
325                    let next = parse_i64(v.as_slice())
326                        .ok_or(StoreError::NotInteger)?
327                        .checked_add(delta)
328                        .ok_or(StoreError::Overflow)?;
329                    // Promote to Int: future INCRs hit the fast path.
330                    e.value = Value::Int(next);
331                    IncrOutcome::Reweigh(next)
332                }
333                Value::ArcBulk(a) => {
334                    // L1: large value claimed to be numeric — parse and
335                    // promote to Int. Subsequent INCRs hit the fast path.
336                    let next = parse_i64(a.as_ref())
337                        .ok_or(StoreError::NotInteger)?
338                        .checked_add(delta)
339                        .ok_or(StoreError::Overflow)?;
340                    e.value = Value::Int(next);
341                    IncrOutcome::Reweigh(next)
342                }
343                _ => return Err(StoreError::WrongType),
344            },
345            // Absent/expired ⇒ start from 0; 0 + delta can't overflow i64.
346            None => IncrOutcome::Insert(delta),
347        };
348        match outcome {
349            IncrOutcome::Reweigh(next) => {
350                self.reweigh_entry(key);
351                Ok(next)
352            }
353            IncrOutcome::Insert(next) => {
354                self.insert_entry(SmallBytes::from_slice(key), Entry::new(Value::Int(next), None));
355                Ok(next)
356            }
357        }
358    }
359}
360
361enum IncrOutcome {
362    Reweigh(i64),
363    Insert(i64),
364}
365
366/// The bytes a string-class value materializes to on the cold shared
367/// lane (the `Value::Int` re-pick case included — a canonical-integer
368/// spill decodes back through the SET rules).
369fn cold_string_bytes(v: &Value) -> Vec<u8> {
370    match v {
371        Value::Str(s) => s.as_slice().to_vec(),
372        Value::ArcBulk(a) => a.as_ref().to_vec(),
373        Value::Int(n) => {
374            let mut tmp = itoa_i64_stack();
375            format_i64_into(*n, &mut tmp).to_vec()
376        }
377        _ => unreachable!("string-tagged cold record decodes to a string class"),
378    }
379}
380
381#[cfg(test)]
382mod lend_tests {
383    use super::*;
384
385    fn owned(s: &Store, k: &[u8]) -> Result<Option<Vec<u8>>, StoreError> {
386        s.get_shared_owned(k).map(|g| {
387            g.map(|g| match g {
388                GetShared::Arc(a) => a.to_vec(),
389                GetShared::Bytes(b) => b,
390            })
391        })
392    }
393
394    #[test]
395    fn the_lent_bytes_are_the_owned_ones_for_every_string_class() {
396        let mut s = Store::new();
397        s.set(b"small", b"v".to_vec(), None, crate::SetCondition::Always);
398        s.set(b"bulk", vec![7u8; 4096], None, crate::SetCondition::Always);
399        s.set(b"int", b"-12345".to_vec(), None, crate::SetCondition::Always);
400        s.rpush(b"list", &[b"x"]).unwrap();
401        for k in [&b"small"[..], b"bulk", b"int", b"missing", b"list"] {
402            let lent = s.get_shared_with(k, |v| v.map(<[u8]>::to_vec));
403            assert_eq!(lent, owned(&s, k), "{}", String::from_utf8_lossy(k));
404        }
405    }
406}