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        self.unpack_row(key);
51        if self.live_entry_mut(key).is_none() {
52            if !create {
53                return Ok(None);
54            }
55            self.insert_entry(
56                SmallBytes::from_slice(key),
57                Entry::new(Value::Hash(Arc::default()), None),
58            );
59        }
60        // A.8: detect encoding first (independent borrow), upgrade
61        // out-of-scope of the &mut, then re-borrow — the borrow checker
62        // rejects the in-place match.
63        let needs = match self.map.get(key).map(|e| &e.value) {
64            Some(Value::SmallHashInline(_)) => true,
65            Some(Value::Hash(h)) => h.len() >= HS_PROMOTE,
66            _ => false,
67        };
68        if needs {
69            self.promote_hash_encoding(key);
70        }
71        match &mut self.map.get_mut(key).expect("present").value {
72            Value::Hash(h) => Ok(Some(HashRefMut::Flat(Arc::make_mut(h)))),
73            Value::SegHash(h) => Ok(Some(HashRefMut::Seg(Arc::make_mut(h)))),
74            _ => Err(StoreError::WrongType),
75        }
76    }
77
78    /// One promotion step: inline → flat, or flat-at-threshold →
79    /// sharded. Reweighs the entry (the encoding switch changes the
80    /// overhead model).
81    fn promote_hash_encoding(&mut self, key: &[u8]) {
82        let Some(e) = self.map.get_mut(key) else { return };
83        match &mut e.value {
84            Value::SmallHashInline(s) => {
85                e.value = Value::Hash(Arc::new(small_hash::promote(s)));
86            }
87            Value::Hash(h) => {
88                let flat = Arc::try_unwrap(core::mem::take(h)).unwrap_or_else(|a| (*a).clone());
89                e.value = Value::SegHash(Arc::new(SegMap::from_flat(flat)));
90            }
91            _ => return,
92        }
93        self.reweigh_entry(key);
94    }
95
96    /// A.8: read the key's hash slot for HSET. `WrongType` if the entry
97    /// is not a hash. Returns `None` when the key is absent.
98    fn hash_value_for_set(&mut self, key: &[u8]) -> Result<Option<&mut Value>, StoreError> {
99        self.tier_resolve(key, crate::value::COLD_TAG_HASH)?;
100        match self.live_entry_mut(key) {
101            None => Ok(None),
102            Some(e) => match &e.value {
103                Value::Hash(_)
104                | Value::SegHash(_)
105                | Value::SmallHashInline(_)
106                | Value::PackedRow(_) => Ok(Some(&mut e.value)),
107                _ => Err(StoreError::WrongType),
108            },
109        }
110    }
111
112    /// `HSET` into a declared row.
113    ///
114    /// Three ways out, and every one keeps the data: same width overwrites in
115    /// place, a different width rebuilds the buffer, and anything the packed
116    /// form cannot hold — a field the table never declared, or a payload past
117    /// what u16 offsets address — leaves the packed form for the general one
118    /// with every value intact. None of them is an error: a packed row is a
119    /// size class and a declaration, not a type.
120    fn hset_packed(
121        &mut self,
122        key: &[u8],
123        field: &[u8],
124        value: &[u8],
125    ) -> Result<HsetOutcome, StoreError> {
126        let v = self.hash_value_for_set(key)?.expect("present and packed");
127        let Value::PackedRow(r) = v else { return Err(StoreError::WrongType) };
128        let slot = r.names().iter().position(|c| c == field);
129        let existed = slot.is_some_and(|i| r.has(i));
130        let rebuilt = match slot {
131            Some(i) if r.set_same_width(i, value) => Some(()),
132            Some(i) => r.with_column(i, Some(value)).map(|next| {
133                *r = next;
134            }),
135            None => None,
136        };
137        if rebuilt.is_none() {
138            return self.unpack_then_set(key, field, value);
139        }
140        self.reweigh_entry(key);
141        Ok(if existed { HsetOutcome::UpdatedInline } else { HsetOutcome::AddedInline })
142    }
143
144    /// Leave the packed form for the general one, then apply the write.
145    fn unpack_then_set(
146        &mut self,
147        key: &[u8],
148        field: &[u8],
149        value: &[u8],
150    ) -> Result<HsetOutcome, StoreError> {
151        self.unpack_row(key);
152        let v = self.hash_value_for_set(key)?.expect("present");
153        let Value::Hash(h) = v else { return Err(StoreError::WrongType) };
154        let outcome = heap_hash_set(HashRefMut::Flat(Arc::make_mut(h)), field, value);
155        self.reweigh_entry(key);
156        Ok(outcome)
157    }
158
159    /// Turn a packed row back into the general hash, in place. A no-op on
160    /// every other value, including a hash that is already general.
161    ///
162    /// Every mutation that is not the packed form's own fast path goes
163    /// through here first. The alternative — teaching each mutating verb to
164    /// edit the packed buffer — is how the form came to answer WRONGTYPE
165    /// from `HDEL` and `HINCRBYFLOAT`: those verbs never named the packed
166    /// form, so a catch-all answered for them. One conversion in front of
167    /// the mutation makes the general arms right for all of them, and the
168    /// table's write hook packs the row again afterwards.
169    pub(crate) fn unpack_row(&mut self, key: &[u8]) {
170        let Some(e) = self.map.get_mut(key) else { return };
171        let Value::PackedRow(r) = &e.value else { return };
172        let mut flat = HashData::with_capacity(r.len().max(1));
173        for (f, val) in r.fields() {
174            flat.insert(SmallBytes::from_slice(f), SmallBytes::from_slice(val));
175        }
176        e.value = Value::Hash(Arc::new(flat));
177        self.reweigh_entry(key);
178    }
179
180    /// `HSET` — returns the count of newly-added fields.
181    pub fn hset(&mut self, key: &[u8], pairs: &[(&[u8], &[u8])]) -> Result<usize, StoreError> {
182        self.purge_hash_ttl(key);
183        // Overwriting a field discards its TTL (Redis 7.4).
184        if !self.hfttl.is_empty() {
185            let fs: Vec<&[u8]> = pairs.iter().map(|(f, _)| *f).collect();
186            self.clear_hash_field_ttls(key, &fs);
187        }
188        if pairs.is_empty() {
189            return Ok(0);
190        }
191        let mut added = 0usize;
192        let mut delta: i64 = 0;
193        for (f, v) in pairs {
194            match self.hset_one(key, f, v)? {
195                HsetOutcome::AddedInline => added += 1,
196                HsetOutcome::UpdatedInline => {}
197                HsetOutcome::AddedHeap(w) => {
198                    added += 1;
199                    delta += w;
200                }
201                HsetOutcome::UpdatedHeap(d) => delta += d,
202            }
203        }
204        self.account_delta(key, delta);
205        Ok(added)
206    }
207
208    /// `HSETNX` — set only if the field is absent; returns whether set.
209    pub fn hsetnx(&mut self, key: &[u8], field: &[u8], val: &[u8]) -> Result<bool, StoreError> {
210        self.purge_hash_ttl(key);
211        self.tier_resolve(key, crate::value::COLD_TAG_HASH)?;
212        let exists = match self.live_entry(key) {
213            None => false,
214            Some(e) => match &e.value {
215                Value::Hash(h) => h.contains_key(field),
216                Value::SegHash(h) => h.contains_key(field),
217                Value::SmallHashInline(h) => h.contains_key(field),
218                Value::PackedRow(r) => r.has_named(field),
219                _ => return Err(StoreError::WrongType),
220            },
221        };
222        if exists {
223            return Ok(false);
224        }
225        match self.hset_one(key, field, val)? {
226            HsetOutcome::AddedInline | HsetOutcome::UpdatedInline => Ok(true),
227            HsetOutcome::AddedHeap(w) => {
228                self.account_delta(key, w);
229                Ok(true)
230            }
231            HsetOutcome::UpdatedHeap(_) => Ok(true),
232        }
233    }
234
235    /// `HDEL` — returns count removed; deletes the key if emptied.
236    pub fn hdel(&mut self, key: &[u8], fields: &[&[u8]]) -> Result<usize, StoreError> {
237        self.purge_hash_ttl(key);
238        self.tier_resolve(key, crate::value::COLD_TAG_HASH)?;
239        self.unpack_row(key);
240        let now = now_ns();
241        if !self.reap(key, now) {
242            return Ok(0);
243        }
244        let (removed, delta, drop_key) = {
245            let h_entry = self.map.get_mut(key).expect("live");
246            match &mut h_entry.value {
247                // G-A3: hoist Arc::make_mut OUT of the loop — done once
248                // per command instead of per-field.
249                Value::Hash(h) => heap_hash_del(HashRefMut::Flat(Arc::make_mut(h)), fields),
250                Value::SegHash(h) => heap_hash_del(HashRefMut::Seg(Arc::make_mut(h)), fields),
251                Value::SmallHashInline(h) => {
252                    let mut r = 0usize;
253                    for f in fields {
254                        if h.try_remove(f) {
255                            r += 1;
256                        }
257                    }
258                    (r, 0i64, h.is_empty())
259                }
260                _ => return Err(StoreError::WrongType),
261            }
262        };
263        if drop_key {
264            self.remove_entry(key);
265        } else {
266            self.account_delta(key, delta);
267        }
268        Ok(removed)
269    }
270
271    /// `HINCRBYFLOAT` — atomic float increment of a hash field.
272    pub fn hincrbyfloat(
273        &mut self,
274        key: &[u8],
275        field: &[u8],
276        delta: f64,
277    ) -> Result<f64, StoreError> {
278        self.purge_hash_ttl(key);
279        self.clear_hash_field_ttls(key, &[field]);
280        let (next, weight_delta) = {
281            let mut h = self.hash_mut(key, true)?.expect("created");
282            let cur = match h.get(field) {
283                Some(v) => parse_f64(v.as_slice()).ok_or(StoreError::NotFloat)?,
284                None => 0.0,
285            };
286            let next = cur + delta;
287            if !next.is_finite() {
288                return Err(StoreError::NotFloat);
289            }
290            let vb = SmallBytes::from_vec(format!("{next}").into_bytes());
291            let smb = SmallBytes::from_slice(field);
292            let new_field_w = hash_field_weight(&smb, vb.heap_bytes()) as i64;
293            let new_value_heap = vb.heap_bytes() as i64;
294            let wd = match h.insert(smb, vb) {
295                None => new_field_w,
296                Some(old) => new_value_heap - old.heap_bytes() as i64,
297            };
298            (next, wd)
299        };
300        self.account_delta(key, weight_delta);
301        Ok(next)
302    }
303
304    /// `HINCRBY` — preserves TTL; errors if the field isn't an integer.
305    pub fn hincrby(&mut self, key: &[u8], field: &[u8], delta: i64) -> Result<i64, StoreError> {
306        self.purge_hash_ttl(key);
307        self.clear_hash_field_ttls(key, &[field]);
308        let (next, weight_delta) = {
309            let mut h = self.hash_mut(key, true)?.expect("created");
310            let cur = match h.get(field) {
311                Some(v) => parse_i64(v.as_slice()).ok_or(StoreError::NotInteger)?,
312                None => 0,
313            };
314            let next = cur.checked_add(delta).ok_or(StoreError::Overflow)?;
315            let vb = SmallBytes::from_vec(next.to_string().into_bytes());
316            let smb = SmallBytes::from_slice(field);
317            let new_field_w = hash_field_weight(&smb, vb.heap_bytes()) as i64;
318            let new_value_heap = vb.heap_bytes() as i64;
319            let wd = match h.insert(smb, vb) {
320                None => new_field_w,
321                Some(old) => new_value_heap - old.heap_bytes() as i64,
322            };
323            (next, wd)
324        };
325        self.account_delta(key, weight_delta);
326        Ok(next)
327    }
328
329    /// A.8 core: set one `(field, value)` pair, applying the
330    /// encoding-switch.
331    fn hset_one(
332        &mut self,
333        key: &[u8],
334        field: &[u8],
335        value: &[u8],
336    ) -> Result<HsetOutcome, StoreError> {
337        if self.hash_value_for_set(key)?.is_none() {
338            return Ok(self.hset_create(key, field, value));
339        }
340        let v = self.hash_value_for_set(key)?.expect("present and a hash");
341        match v {
342            Value::SmallHashInline(h) => match h.try_set(field, value) {
343                HAddResult::Added => Ok(HsetOutcome::AddedInline),
344                HAddResult::Updated => Ok(HsetOutcome::UpdatedInline),
345                HAddResult::NoRoom => {
346                    let mut promoted = small_hash::promote(h);
347                    let outcome = heap_hash_set(HashRefMut::Flat(&mut promoted), field, value);
348                    *v = Value::Hash(Arc::new(promoted));
349                    self.reweigh_entry(key);
350                    Ok(outcome)
351                }
352            },
353            Value::PackedRow(_) => self.hset_packed(key, field, value),
354            // Flat hash at the threshold: shard, then set. One-time
355            // O(HS_PROMOTE) re-bucket (or clone, if a view pins it now).
356            Value::Hash(h) if h.len() >= HS_PROMOTE => {
357                let flat = Arc::try_unwrap(core::mem::take(h)).unwrap_or_else(|a| (*a).clone());
358                let mut seg = SegMap::from_flat(flat);
359                let outcome = heap_hash_set(HashRefMut::Seg(&mut seg), field, value);
360                *v = Value::SegHash(Arc::new(seg));
361                self.reweigh_entry(key);
362                // Reweighed from scratch — swallow the per-pair delta.
363                Ok(match outcome {
364                    HsetOutcome::AddedHeap(_) => HsetOutcome::AddedHeap(0),
365                    other => other,
366                })
367            }
368            Value::Hash(h) => Ok(heap_hash_set(HashRefMut::Flat(Arc::make_mut(h)), field, value)),
369            Value::SegHash(h) => Ok(heap_hash_set(HashRefMut::Seg(Arc::make_mut(h)), field, value)),
370            _ => Err(StoreError::WrongType),
371        }
372    }
373
374    /// Create a fresh entry for `key` holding one pair.
375    fn hset_create(&mut self, key: &[u8], field: &[u8], value: &[u8]) -> HsetOutcome {
376        if let Some(inline) = SmallHashData::with_one(field, value) {
377            self.insert_entry(
378                SmallBytes::from_slice(key),
379                Entry::new(Value::SmallHashInline(inline), None),
380            );
381            HsetOutcome::AddedInline
382        } else {
383            let smb_f = SmallBytes::from_slice(field);
384            let mut h = HashData::with_capacity(1);
385            h.insert(smb_f, SmallBytes::from_slice(value));
386            self.insert_entry(
387                SmallBytes::from_slice(key),
388                Entry::new(Value::Hash(Arc::new(h)), None),
389            );
390            HsetOutcome::AddedInline
391        }
392    }
393}
394
395/// Set one `(field, value)` pair into a heap-backed hash (either
396/// encoding), charging heap bytes only.
397fn heap_hash_set(mut h: HashRefMut<'_>, field: &[u8], value: &[u8]) -> HsetOutcome {
398    let smb = SmallBytes::from_slice(field);
399    let vb = SmallBytes::from_slice(value);
400    let new_value_heap = vb.heap_bytes() as i64;
401    let new_w = hash_field_weight(&smb, vb.heap_bytes()) as i64;
402    match h.insert(smb, vb) {
403        None => HsetOutcome::AddedHeap(new_w),
404        Some(old) => HsetOutcome::UpdatedHeap(new_value_heap - old.heap_bytes() as i64),
405    }
406}
407
408/// The HDEL field loop over a heap-backed hash (either encoding).
409/// Returns `(removed, weight_delta, now_empty)`.
410fn heap_hash_del(mut h: HashRefMut<'_>, fields: &[&[u8]]) -> (usize, i64, bool) {
411    let mut r = 0usize;
412    let mut d: i64 = 0;
413    for f in fields {
414        let old = match &mut h {
415            HashRefMut::Flat(m) => m.remove(*f),
416            HashRefMut::Seg(m) => m.remove(f),
417        };
418        if let Some(old_v) = old {
419            r += 1;
420            let smb = SmallBytes::from_slice(f);
421            d -= hash_field_weight(&smb, old_v.heap_bytes()) as i64;
422        }
423    }
424    let empty = match &h {
425        HashRefMut::Flat(m) => m.is_empty(),
426        HashRefMut::Seg(m) => m.is_empty(),
427    };
428    (r, d, empty)
429}
430
431enum HsetOutcome {
432    /// Field was new and lives in the inline variant (zero heap delta).
433    AddedInline,
434    /// Field existed in the inline variant (no count bump, no delta).
435    UpdatedInline,
436    /// Field was new in the heap variant; carries the new field's weight.
437    AddedHeap(i64),
438    /// Field existed in the heap variant; carries the value-length delta.
439    UpdatedHeap(i64),
440}