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.
15pub enum GetReply<'a> {
16    /// Inline-encoded value — caller memcpys the bytes into its output Vec
17    /// (small replies; encoding cost is tiny vs the RTT floor).
18    Bytes(Cow<'a, [u8]>),
19    /// Arc-backed bulk. The reactor's reply path pushes
20    /// the Arc into the conn's `output_arcs` so the next `writev` iovec
21    /// list points DIRECTLY at the value bytes — skipping the per-GET
22    /// memcpy that valkey's `tryAvoidBulkStrCopyToReply` likewise avoids.
23    ArcBulk(Arc<Box<[u8]>>),
24}
25
26/// Owned GET result for the FFI zero-copy shared lane
27/// ([`Store::get_shared_owned`]). Bulk values ride out as an Arc clone (no
28/// byte copy); small values as a plain Vec (one alloc — cheaper than a fresh
29/// Arc). The FFI's shared free reconstructs whichever the tag says.
30pub enum GetShared {
31    /// Bulk value — the engine's Arc, cloned. Zero byte copy.
32    Arc(Arc<Box<[u8]>>),
33    /// Small value (Str/Int) — a plain owned Vec, one allocation.
34    Bytes(Vec<u8>),
35}
36
37impl Store {
38    // ---- strings -------------------------------------------------------
39    /// GET variant that exposes the underlying encoding
40    /// so the reactor's reply path can choose zero-copy
41    /// (`Value::ArcBulk` → push the Arc to the conn's `output_arcs` for a
42    /// writev iovec) vs memcpy (`Value::Str` / `Value::Int` → encode bytes
43    /// into the conn's output Vec). ONE keyspace lookup; the variant tag
44    /// chooses the encoding without a second probe.
45    pub fn get_for_reply(&mut self, key: &[u8]) -> Result<Option<GetReply<'_>>, StoreError> {
46        match self.tier_serve(key, crate::value::COLD_TAG_STRING)? {
47            None => Ok(None),
48            Some(e) => match &e.value {
49                Value::Str(v) => Ok(Some(GetReply::Bytes(Cow::Borrowed(v.as_slice())))),
50                Value::ArcBulk(a) => Ok(Some(GetReply::ArcBulk(Arc::clone(a)))),
51                Value::Int(n) => {
52                    let mut tmp = itoa_i64_stack();
53                    let s = format_i64_into(*n, &mut tmp);
54                    Ok(Some(GetReply::Bytes(Cow::Owned(s.to_vec()))))
55                }
56                _ => Err(StoreError::WrongType),
57            },
58        }
59    }
60
61    /// Owned GET for the FFI scalar *shared* lane (`kevy_get_shared`). Bulk
62    /// values (`Value::ArcBulk`) return an `Arc::clone` — **no byte copy**; the
63    /// FFI holds the Arc alive and hands JS a buffer that views it directly
64    /// (mirrors MMKV's zero-copy mmap-page view, the thing that made kevy lose
65    /// large GET). Small values (`Str`/`Int`) allocate a fresh `Arc<Box<[u8]>>`
66    /// — the same one copy the Vec lane already pays — so the caller's free path
67    /// is uniform. Read-only (`&self`, like [`Self::get_shared`]) so the FFI
68    /// can take it under a SHARED shard lock — no LRU stamp, matching the
69    /// `maxmemory == 0` fast path the mobile door runs on. Wrong type errors
70    /// like [`Self::get_for_reply`].
71    pub fn get_shared_owned(&self, key: &[u8]) -> Result<Option<GetShared>, StoreError> {
72        match self.map.get(key) {
73            None => Ok(None),
74            Some(e) if e.is_expired(self.cached_clock, self.cached_ns) => Ok(None),
75            // Bulk (already Arc-backed) → clone the Arc: ZERO byte copy. Small
76            // (`Str`/`Int`) → a plain Vec (one alloc, cheaper than wrapping a
77            // fresh Arc — the FFI's shared free handles either).
78            Some(e) => match &e.value {
79                Value::ArcBulk(a) => Ok(Some(GetShared::Arc(Arc::clone(a)))),
80                Value::Str(v) => Ok(Some(GetShared::Bytes(v.as_slice().to_vec()))),
81                Value::Int(n) => {
82                    let mut tmp = itoa_i64_stack();
83                    let s = format_i64_into(*n, &mut tmp);
84                    Ok(Some(GetShared::Bytes(s.to_vec())))
85                }
86                // Cold, `&self` shared lane: pread a fresh value — never
87                // promotes, never sets the probation mark (it cannot:
88                // no `&mut`). Documented: shared-lane reads pay a pread
89                // until a `&mut`-path access promotes the key.
90                Value::Cold(c) if c.type_tag == crate::value::COLD_TAG_STRING => {
91                    match self.tier_peek_value(&e.value).expect("cold peek") {
92                        Value::ArcBulk(a) => Ok(Some(GetShared::Arc(a))),
93                        v => Ok(Some(GetShared::Bytes(cold_string_bytes(&v)))),
94                    }
95                }
96                _ => Err(StoreError::WrongType),
97            },
98        }
99    }
100
101    /// Fused GET-into-output. Skips the [`GetReply`] enum tag
102    /// round-trip + caller match arm by writing the RESP frame directly into
103    /// `output` (header + bytes + CRLF for Str/Int) or pushing the Arc into
104    /// `output_arcs` at the right offset (ArcBulk zero-copy via writev).
105    /// Returns the same outcomes as [`Self::get_for_reply`]: `Ok(true)` if
106    /// the key was found and emitted, `Ok(false)` if absent (the caller
107    /// emits the `$-1` null bulk — preserves the existing inline-null
108    /// semantics on the reactor side), `Err` for WRONGTYPE.
109    pub fn get_into_output(
110        &mut self,
111        key: &[u8],
112        output: &mut Vec<u8>,
113        output_arcs: &mut Vec<(usize, Arc<Box<[u8]>>)>,
114    ) -> Result<bool, StoreError> {
115        match self.tier_serve(key, crate::value::COLD_TAG_STRING)? {
116            None => Ok(false),
117            Some(e) => match &e.value {
118                Value::Str(v) => {
119                    let bytes = v.as_slice();
120                    crate::util::bulk_header_into(output, bytes.len());
121                    output.extend_from_slice(bytes);
122                    output.extend_from_slice(b"\r\n");
123                    Ok(true)
124                }
125                Value::ArcBulk(a) => {
126                    crate::util::bulk_header_into(output, a.len());
127                    let pos = output.len();
128                    output_arcs.push((pos, Arc::clone(a)));
129                    output.extend_from_slice(b"\r\n");
130                    Ok(true)
131                }
132                Value::Int(n) => {
133                    let mut tmp = itoa_i64_stack();
134                    let s = format_i64_into(*n, &mut tmp);
135                    crate::util::bulk_header_into(output, s.len());
136                    output.extend_from_slice(s);
137                    output.extend_from_slice(b"\r\n");
138                    Ok(true)
139                }
140                _ => Err(StoreError::WrongType),
141            },
142        }
143    }
144
145    /// `GET` — returns a `Cow<[u8]>` so `Value::Int` callers can format the
146    /// integer to ASCII without storing it. `Value::Str`
147    /// returns `Cow::Borrowed` (zero copy); `Value::Int`
148    /// formats to a small owned `Vec<u8>` (up to 20 bytes for `i64::MIN`).
149    pub fn get(&mut self, key: &[u8]) -> Result<Option<Cow<'_, [u8]>>, StoreError> {
150        match self.tier_serve(key, crate::value::COLD_TAG_STRING)? {
151            None => Ok(None),
152            Some(e) => match &e.value {
153                Value::Str(v) => Ok(Some(Cow::Borrowed(v.as_slice()))),
154                // L1: Arc-backed bulk — return borrow into the Arc's
155                // bytes. Caller can either memcpy via Cow::Borrowed
156                // (default `encode_bulk` path) OR look up the
157                // underlying `Value::ArcBulk(arc)` separately for the
158                // writev zero-copy reply path.
159                Value::ArcBulk(a) => Ok(Some(Cow::Borrowed(a.as_ref()))),
160                Value::Int(n) => {
161                    let mut tmp = itoa_i64_stack();
162                    let s = format_i64_into(*n, &mut tmp);
163                    Ok(Some(Cow::Owned(s.to_vec())))
164                }
165                _ => Err(StoreError::WrongType),
166            },
167        }
168    }
169
170    /// Read-only `GET`: `&self`, so concurrent readers can run under a shared
171    /// lock (embedded mode's `RwLock` read path). Expiry is checked against the
172    /// coarse cached clock but an expired key is *not* removed here (no `&mut`)
173    /// — the reaper / next write reclaims it; a reader just sees `None`. LRU is
174    /// not touched, so this path is only used when eviction is off
175    /// (`maxmemory == 0`); with eviction, the caller takes the mutating
176    /// [`Self::get`] under an exclusive lock so access still stamps the LRU.
177    pub fn get_shared(&self, key: &[u8]) -> Result<Option<Cow<'_, [u8]>>, StoreError> {
178        match self.map.get(key) {
179            None => Ok(None),
180            Some(e) if e.is_expired(self.cached_clock, self.cached_ns) => Ok(None),
181            Some(e) => match &e.value {
182                Value::Str(v) => Ok(Some(Cow::Borrowed(v.as_slice()))),
183                Value::ArcBulk(a) => Ok(Some(Cow::Borrowed(a.as_ref()))),
184                Value::Int(n) => {
185                    let mut tmp = itoa_i64_stack();
186                    let s = format_i64_into(*n, &mut tmp);
187                    Ok(Some(Cow::Owned(s.to_vec())))
188                }
189                // Cold, `&self` shared lane — see `get_shared_owned`.
190                Value::Cold(c) if c.type_tag == crate::value::COLD_TAG_STRING => {
191                    let v = self.tier_peek_value(&e.value).expect("cold peek");
192                    Ok(Some(Cow::Owned(cold_string_bytes(&v))))
193                }
194                _ => Err(StoreError::WrongType),
195            },
196        }
197    }
198
199    pub fn strlen(&mut self, key: &[u8]) -> Result<usize, StoreError> {
200        Ok(self.get(key)?.map_or(0, |c| c.len()))
201    }
202
203    /// `INCRBY` family; preserves any TTL.
204    ///
205    /// Following valkey's OBJ_ENCODING_INT approach: the hot path
206    /// matches `Value::Int(n)` and does the increment in place — no parse,
207    /// no format, no allocation. The `Value::Str` arm parses,
208    /// increments, and **promotes** to `Value::Int(next)` so subsequent
209    /// INCRs land on the fast path. Insert-new path also lands as `Int`.
210    pub fn incr_by(&mut self, key: &[u8], delta: i64) -> Result<i64, StoreError> {
211        self.tier_resolve(key, crate::value::COLD_TAG_STRING)?; // cold string pages in
212
213        let outcome = match self.live_entry_mut(key) {
214            Some(e) => match &mut e.value {
215                Value::Int(n) => {
216                    let next = n.checked_add(delta).ok_or(StoreError::Overflow)?;
217                    *n = next;
218                    // In-place i64 mutation — weight unchanged (still 0
219                    // heap bytes for an Int). Skip the reweigh entirely.
220                    return Ok(next);
221                }
222                Value::Str(v) => {
223                    let next = parse_i64(v.as_slice())
224                        .ok_or(StoreError::NotInteger)?
225                        .checked_add(delta)
226                        .ok_or(StoreError::Overflow)?;
227                    // Promote to Int: future INCRs hit the fast path.
228                    e.value = Value::Int(next);
229                    IncrOutcome::Reweigh(next)
230                }
231                Value::ArcBulk(a) => {
232                    // L1: large value claimed to be numeric — parse and
233                    // promote to Int. Subsequent INCRs hit the fast path.
234                    let next = parse_i64(a.as_ref())
235                        .ok_or(StoreError::NotInteger)?
236                        .checked_add(delta)
237                        .ok_or(StoreError::Overflow)?;
238                    e.value = Value::Int(next);
239                    IncrOutcome::Reweigh(next)
240                }
241                _ => return Err(StoreError::WrongType),
242            },
243            // Absent/expired ⇒ start from 0; 0 + delta can't overflow i64.
244            None => IncrOutcome::Insert(delta),
245        };
246        match outcome {
247            IncrOutcome::Reweigh(next) => {
248                self.reweigh_entry(key);
249                Ok(next)
250            }
251            IncrOutcome::Insert(next) => {
252                self.insert_entry(
253                    SmallBytes::from_slice(key),
254                    Entry::new(Value::Int(next), None),
255                );
256                Ok(next)
257            }
258        }
259    }
260
261}
262
263enum IncrOutcome {
264    Reweigh(i64),
265    Insert(i64),
266}
267
268/// The bytes a string-class value materializes to on the cold shared
269/// lane (the `Value::Int` re-pick case included — a canonical-integer
270/// spill decodes back through the SET rules).
271fn cold_string_bytes(v: &Value) -> Vec<u8> {
272    match v {
273        Value::Str(s) => s.as_slice().to_vec(),
274        Value::ArcBulk(a) => a.as_ref().to_vec(),
275        Value::Int(n) => {
276            let mut tmp = itoa_i64_stack();
277            format_i64_into(*n, &mut tmp).to_vec()
278        }
279        _ => unreachable!("string-tagged cold record decodes to a string class"),
280    }
281}