Skip to main content

kevy_store/
hash.rs

1//! `Store` hash write commands. Reads live in `hash_read.rs`.
2//!
3//! Three encodings, promoted in order of size: `SmallHashInline`
4//! (couple of tiny pairs, in the Value body) → `Hash(Arc<KevyMap>)`
5//! (flat heap) → `SegHash` (bucket-sharded COW past
6//! [`crate::seg_map::HS_PROMOTE`] fields — a write under a live
7//! snapshot view clones one bucket, not the whole value).
8
9#[cfg(not(feature = "std"))]
10use crate::nostd_prelude::*;
11use crate::seg_map::{HS_PROMOTE, SegMap};
12use crate::small_hash::{self, AddResult as HAddResult, SmallHashData};
13use crate::util::{parse_f64, parse_i64};
14use crate::value::{HashData, SmallBytes, Value, hash_field_weight};
15use crate::{Entry, Store, StoreError, now_ns};
16use alloc::sync::Arc;
17
18/// A mutable borrow of either heap hash encoding — lets the
19/// read-modify-write entry points (`hincrby` / `hincrbyfloat`) stay
20/// encoding-blind: both arms expose the same `get`/`insert` shape.
21pub(crate) enum HashRefMut<'a> {
22    Flat(&'a mut HashData),
23    Seg(&'a mut SegMap<SmallBytes>),
24}
25
26impl HashRefMut<'_> {
27    fn get(&self, field: &[u8]) -> Option<&SmallBytes> {
28        match self {
29            Self::Flat(h) => h.get(field),
30            Self::Seg(h) => h.get(field),
31        }
32    }
33    fn insert(&mut self, field: SmallBytes, value: SmallBytes) -> Option<SmallBytes> {
34        match self {
35            Self::Flat(h) => h.insert(field, value),
36            Self::Seg(h) => h.insert(field, value),
37        }
38    }
39}
40
41impl Store {
42    // ---- hashes --------------------------------------------------------
43
44    /// Borrow the key's hash mutably, optionally creating it. `Ok(None)`
45    /// means the key is absent and `create` was false. Promotes inline →
46    /// flat, and flat → sharded at the threshold, so HINCRBY-only
47    /// workloads cross the segmentation boundary too.
48    fn hash_mut(&mut self, key: &[u8], create: bool) -> Result<Option<HashRefMut<'_>>, StoreError> {
49        self.tier_resolve(key, crate::value::COLD_TAG_HASH)?;
50        if self.live_entry_mut(key).is_none() {
51            if !create {
52                return Ok(None);
53            }
54            self.insert_entry(
55                SmallBytes::from_slice(key),
56                Entry::new(Value::Hash(Arc::default()), None),
57            );
58        }
59        // A.8: detect encoding first (independent borrow), upgrade
60        // out-of-scope of the &mut, then re-borrow — the borrow checker
61        // rejects the in-place match.
62        let needs = match self.map.get(key).map(|e| &e.value) {
63            Some(Value::SmallHashInline(_)) => true,
64            Some(Value::Hash(h)) => h.len() >= HS_PROMOTE,
65            _ => false,
66        };
67        if needs {
68            self.promote_hash_encoding(key);
69        }
70        match &mut self.map.get_mut(key).expect("present").value {
71            Value::Hash(h) => Ok(Some(HashRefMut::Flat(Arc::make_mut(h)))),
72            Value::SegHash(h) => Ok(Some(HashRefMut::Seg(Arc::make_mut(h)))),
73            _ => Err(StoreError::WrongType),
74        }
75    }
76
77    /// One promotion step: inline → flat, or flat-at-threshold →
78    /// sharded. Reweighs the entry (the encoding switch changes the
79    /// overhead model).
80    fn promote_hash_encoding(&mut self, key: &[u8]) {
81        let Some(e) = self.map.get_mut(key) else { return };
82        match &mut e.value {
83            Value::SmallHashInline(s) => {
84                e.value = Value::Hash(Arc::new(small_hash::promote(s)));
85            }
86            Value::Hash(h) => {
87                let flat = Arc::try_unwrap(core::mem::take(h)).unwrap_or_else(|a| (*a).clone());
88                e.value = Value::SegHash(Arc::new(SegMap::from_flat(flat)));
89            }
90            _ => return,
91        }
92        self.reweigh_entry(key);
93    }
94
95    /// A.8: read the key's hash slot for HSET. `WrongType` if the entry
96    /// is not a hash. Returns `None` when the key is absent.
97    fn hash_value_for_set(&mut self, key: &[u8]) -> Result<Option<&mut Value>, StoreError> {
98        self.tier_resolve(key, crate::value::COLD_TAG_HASH)?;
99        match self.live_entry_mut(key) {
100            None => Ok(None),
101            Some(e) => match &e.value {
102                Value::Hash(_) | Value::SegHash(_) | Value::SmallHashInline(_) => {
103                    Ok(Some(&mut e.value))
104                }
105                _ => Err(StoreError::WrongType),
106            },
107        }
108    }
109
110    /// `HSET` — returns the count of newly-added fields.
111    pub fn hset(
112        &mut self,
113        key: &[u8],
114        pairs: &[(&[u8], &[u8])],
115    ) -> Result<usize, StoreError> {
116        self.purge_hash_ttl(key);
117        // Overwriting a field discards its TTL (Redis 7.4).
118        if !self.hfttl.is_empty() {
119            let fs: Vec<&[u8]> = pairs.iter().map(|(f, _)| *f).collect();
120            self.clear_hash_field_ttls(key, &fs);
121        }
122        if pairs.is_empty() {
123            return Ok(0);
124        }
125        let mut added = 0usize;
126        let mut delta: i64 = 0;
127        for (f, v) in pairs {
128            match self.hset_one(key, f, v)? {
129                HsetOutcome::AddedInline => added += 1,
130                HsetOutcome::UpdatedInline => {}
131                HsetOutcome::AddedHeap(w) => {
132                    added += 1;
133                    delta += w;
134                }
135                HsetOutcome::UpdatedHeap(d) => delta += d,
136            }
137        }
138        self.account_delta(key, delta);
139        Ok(added)
140    }
141
142    /// `HSETNX` — set only if the field is absent; returns whether set.
143    pub fn hsetnx(&mut self, key: &[u8], field: &[u8], val: &[u8]) -> Result<bool, StoreError> {
144        self.purge_hash_ttl(key);
145        self.tier_resolve(key, crate::value::COLD_TAG_HASH)?;
146        let exists = match self.live_entry(key) {
147            None => false,
148            Some(e) => match &e.value {
149                Value::Hash(h) => h.contains_key(field),
150                Value::SegHash(h) => h.contains_key(field),
151                Value::SmallHashInline(h) => h.contains_key(field),
152                _ => return Err(StoreError::WrongType),
153            },
154        };
155        if exists {
156            return Ok(false);
157        }
158        match self.hset_one(key, field, val)? {
159            HsetOutcome::AddedInline | HsetOutcome::UpdatedInline => Ok(true),
160            HsetOutcome::AddedHeap(w) => {
161                self.account_delta(key, w);
162                Ok(true)
163            }
164            HsetOutcome::UpdatedHeap(_) => Ok(true),
165        }
166    }
167
168    /// `HDEL` — returns count removed; deletes the key if emptied.
169    pub fn hdel(
170        &mut self,
171        key: &[u8],
172        fields: &[&[u8]],
173    ) -> Result<usize, StoreError> {
174        self.purge_hash_ttl(key);
175        self.tier_resolve(key, crate::value::COLD_TAG_HASH)?;
176        let now = now_ns();
177        if !self.reap(key, now) {
178            return Ok(0);
179        }
180        let (removed, delta, drop_key) = {
181            let h_entry = self.map.get_mut(key).expect("live");
182            match &mut h_entry.value {
183                // G-A3: hoist Arc::make_mut OUT of the loop — done once
184                // per command instead of per-field.
185                Value::Hash(h) => heap_hash_del(HashRefMut::Flat(Arc::make_mut(h)), fields),
186                Value::SegHash(h) => heap_hash_del(HashRefMut::Seg(Arc::make_mut(h)), fields),
187                Value::SmallHashInline(h) => {
188                    let mut r = 0usize;
189                    for f in fields {
190                        if h.try_remove(f) {
191                            r += 1;
192                        }
193                    }
194                    (r, 0i64, h.is_empty())
195                }
196                _ => return Err(StoreError::WrongType),
197            }
198        };
199        if drop_key {
200            self.remove_entry(key);
201        } else {
202            self.account_delta(key, delta);
203        }
204        Ok(removed)
205    }
206
207    /// `HINCRBYFLOAT` — atomic float increment of a hash field.
208    pub fn hincrbyfloat(
209        &mut self,
210        key: &[u8],
211        field: &[u8],
212        delta: f64,
213    ) -> Result<f64, StoreError> {
214        self.purge_hash_ttl(key);
215        self.clear_hash_field_ttls(key, &[field]);
216        let (next, weight_delta) = {
217            let mut h = self.hash_mut(key, true)?.expect("created");
218            let cur = match h.get(field) {
219                Some(v) => parse_f64(v.as_slice()).ok_or(StoreError::NotFloat)?,
220                None => 0.0,
221            };
222            let next = cur + delta;
223            if !next.is_finite() {
224                return Err(StoreError::NotFloat);
225            }
226            let vb = SmallBytes::from_vec(format!("{next}").into_bytes());
227            let smb = SmallBytes::from_slice(field);
228            let new_field_w = hash_field_weight(&smb, vb.heap_bytes()) as i64;
229            let new_value_heap = vb.heap_bytes() as i64;
230            let wd = match h.insert(smb, vb) {
231                None => new_field_w,
232                Some(old) => new_value_heap - old.heap_bytes() as i64,
233            };
234            (next, wd)
235        };
236        self.account_delta(key, weight_delta);
237        Ok(next)
238    }
239
240    /// `HINCRBY` — preserves TTL; errors if the field isn't an integer.
241    pub fn hincrby(&mut self, key: &[u8], field: &[u8], delta: i64) -> Result<i64, StoreError> {
242        self.purge_hash_ttl(key);
243        self.clear_hash_field_ttls(key, &[field]);
244        let (next, weight_delta) = {
245            let mut h = self.hash_mut(key, true)?.expect("created");
246            let cur = match h.get(field) {
247                Some(v) => parse_i64(v.as_slice()).ok_or(StoreError::NotInteger)?,
248                None => 0,
249            };
250            let next = cur.checked_add(delta).ok_or(StoreError::Overflow)?;
251            let vb = SmallBytes::from_vec(next.to_string().into_bytes());
252            let smb = SmallBytes::from_slice(field);
253            let new_field_w = hash_field_weight(&smb, vb.heap_bytes()) as i64;
254            let new_value_heap = vb.heap_bytes() as i64;
255            let wd = match h.insert(smb, vb) {
256                None => new_field_w,
257                Some(old) => new_value_heap - old.heap_bytes() as i64,
258            };
259            (next, wd)
260        };
261        self.account_delta(key, weight_delta);
262        Ok(next)
263    }
264
265    /// A.8 core: set one `(field, value)` pair, applying the
266    /// encoding-switch.
267    fn hset_one(
268        &mut self,
269        key: &[u8],
270        field: &[u8],
271        value: &[u8],
272    ) -> Result<HsetOutcome, StoreError> {
273        if self.hash_value_for_set(key)?.is_none() {
274            return Ok(self.hset_create(key, field, value));
275        }
276        let v = self.hash_value_for_set(key)?.expect("present and a hash");
277        match v {
278            Value::SmallHashInline(h) => match h.try_set(field, value) {
279                HAddResult::Added => Ok(HsetOutcome::AddedInline),
280                HAddResult::Updated => Ok(HsetOutcome::UpdatedInline),
281                HAddResult::NoRoom => {
282                    let mut promoted = small_hash::promote(h);
283                    let outcome = heap_hash_set(HashRefMut::Flat(&mut promoted), field, value);
284                    *v = Value::Hash(Arc::new(promoted));
285                    self.reweigh_entry(key);
286                    Ok(outcome)
287                }
288            },
289            // Flat hash at the threshold: shard, then set. One-time
290            // O(HS_PROMOTE) re-bucket (or clone, if a view pins it now).
291            Value::Hash(h) if h.len() >= HS_PROMOTE => {
292                let flat = Arc::try_unwrap(core::mem::take(h)).unwrap_or_else(|a| (*a).clone());
293                let mut seg = SegMap::from_flat(flat);
294                let outcome = heap_hash_set(HashRefMut::Seg(&mut seg), field, value);
295                *v = Value::SegHash(Arc::new(seg));
296                self.reweigh_entry(key);
297                // Reweighed from scratch — swallow the per-pair delta.
298                Ok(match outcome {
299                    HsetOutcome::AddedHeap(_) => HsetOutcome::AddedHeap(0),
300                    other => other,
301                })
302            }
303            Value::Hash(h) => Ok(heap_hash_set(HashRefMut::Flat(Arc::make_mut(h)), field, value)),
304            Value::SegHash(h) => {
305                Ok(heap_hash_set(HashRefMut::Seg(Arc::make_mut(h)), field, value))
306            }
307            _ => Err(StoreError::WrongType),
308        }
309    }
310
311    /// Create a fresh entry for `key` holding one pair.
312    fn hset_create(&mut self, key: &[u8], field: &[u8], value: &[u8]) -> HsetOutcome {
313        if let Some(inline) = SmallHashData::with_one(field, value) {
314            self.insert_entry(
315                SmallBytes::from_slice(key),
316                Entry::new(Value::SmallHashInline(inline), None),
317            );
318            HsetOutcome::AddedInline
319        } else {
320            let smb_f = SmallBytes::from_slice(field);
321            let mut h = HashData::with_capacity(1);
322            h.insert(smb_f, SmallBytes::from_slice(value));
323            self.insert_entry(
324                SmallBytes::from_slice(key),
325                Entry::new(Value::Hash(Arc::new(h)), None),
326            );
327            HsetOutcome::AddedInline
328        }
329    }
330}
331
332/// Set one `(field, value)` pair into a heap-backed hash (either
333/// encoding), charging heap bytes only.
334fn heap_hash_set(mut h: HashRefMut<'_>, field: &[u8], value: &[u8]) -> HsetOutcome {
335    let smb = SmallBytes::from_slice(field);
336    let vb = SmallBytes::from_slice(value);
337    let new_value_heap = vb.heap_bytes() as i64;
338    let new_w = hash_field_weight(&smb, vb.heap_bytes()) as i64;
339    match h.insert(smb, vb) {
340        None => HsetOutcome::AddedHeap(new_w),
341        Some(old) => HsetOutcome::UpdatedHeap(new_value_heap - old.heap_bytes() as i64),
342    }
343}
344
345/// The HDEL field loop over a heap-backed hash (either encoding).
346/// Returns `(removed, weight_delta, now_empty)`.
347fn heap_hash_del(mut h: HashRefMut<'_>, fields: &[&[u8]]) -> (usize, i64, bool) {
348    let mut r = 0usize;
349    let mut d: i64 = 0;
350    for f in fields {
351        let old = match &mut h {
352            HashRefMut::Flat(m) => m.remove(*f),
353            HashRefMut::Seg(m) => m.remove(f),
354        };
355        if let Some(old_v) = old {
356            r += 1;
357            let smb = SmallBytes::from_slice(f);
358            d -= hash_field_weight(&smb, old_v.heap_bytes()) as i64;
359        }
360    }
361    let empty = match &h {
362        HashRefMut::Flat(m) => m.is_empty(),
363        HashRefMut::Seg(m) => m.is_empty(),
364    };
365    (r, d, empty)
366}
367
368enum HsetOutcome {
369    /// Field was new and lives in the inline variant (zero heap delta).
370    AddedInline,
371    /// Field existed in the inline variant (no count bump, no delta).
372    UpdatedInline,
373    /// Field was new in the heap variant; carries the new field's weight.
374    AddedHeap(i64),
375    /// Field existed in the heap variant; carries the value-length delta.
376    UpdatedHeap(i64),
377}