Skip to main content

ember_core/keyspace/
hash.rs

1use compact_str::CompactString;
2
3use super::*;
4
5impl Keyspace {
6    /// Sets one or more field-value pairs in a hash.
7    ///
8    /// Creates the hash if the key doesn't exist. Returns the number of
9    /// new fields added (fields that were updated don't count).
10    pub fn hset(&mut self, key: &str, fields: &[(String, Bytes)]) -> Result<usize, WriteError> {
11        if fields.is_empty() {
12            return Ok(0);
13        }
14
15        self.remove_if_expired(key);
16
17        let is_new = self.ensure_collection_type(key, |v| matches!(v, Value::Hash(_)))?;
18
19        let field_increase: usize = fields
20            .iter()
21            .map(|(f, v)| f.len() + v.len() + memory::PACKED_HASH_ENTRY_OVERHEAD)
22            .sum();
23        self.reserve_memory(
24            is_new,
25            key,
26            memory::PACKED_HASH_BASE_OVERHEAD,
27            field_increase,
28        )?;
29
30        if is_new {
31            self.insert_empty(key, Value::Hash(Box::default()));
32        }
33
34        let track_access = self.track_access;
35        let added = self
36            .track_size(key, |entry| {
37                let Value::Hash(ref mut hash) = entry.value else {
38                    unreachable!("type verified by ensure_collection_type");
39                };
40                let mut added = 0;
41                for (field, value) in fields {
42                    if hash
43                        .insert(CompactString::from(field.as_str()), value.clone())
44                        .is_none()
45                    {
46                        added += 1;
47                    }
48                }
49                entry.touch(track_access);
50                added
51            })
52            .unwrap_or(0);
53
54        Ok(added)
55    }
56
57    /// Gets the value of a field in a hash.
58    ///
59    /// Returns `None` if the key or field doesn't exist.
60    pub fn hget(&mut self, key: &str, field: &str) -> Result<Option<Bytes>, WrongType> {
61        let Some(entry) = self.get_live_entry(key) else {
62            return Ok(None);
63        };
64        match &entry.value {
65            Value::Hash(hash) => Ok(hash.get(field).map(Bytes::copy_from_slice)),
66            _ => Err(WrongType),
67        }
68    }
69
70    /// Gets all field-value pairs from a hash.
71    ///
72    /// Returns an empty vec if the key doesn't exist.
73    pub fn hgetall(&mut self, key: &str) -> Result<Vec<(String, Bytes)>, WrongType> {
74        let Some(entry) = self.get_live_entry(key) else {
75            return Ok(vec![]);
76        };
77        match &entry.value {
78            Value::Hash(hash) => Ok(hash
79                .iter()
80                .map(|(k, v)| (k.to_string(), Bytes::copy_from_slice(v)))
81                .collect()),
82            _ => Err(WrongType),
83        }
84    }
85
86    /// Deletes one or more fields from a hash.
87    ///
88    /// Returns the fields that were actually removed.
89    pub fn hdel(&mut self, key: &str, fields: &[String]) -> Result<Vec<String>, WrongType> {
90        if self.remove_if_expired(key) {
91            return Ok(vec![]);
92        }
93
94        let Some(entry) = self.entries.get_mut(key) else {
95            return Ok(vec![]);
96        };
97        if !matches!(entry.value, Value::Hash(_)) {
98            return Err(WrongType);
99        }
100
101        let old_entry_size = entry.entry_size(key);
102        let mut removed = Vec::new();
103        let mut removed_bytes: usize = 0;
104        let is_empty = if let Value::Hash(ref mut hash) = entry.value {
105            for field in fields {
106                if let Some(val) = hash.remove(field) {
107                    removed_bytes += field.len() + val.len() + memory::PACKED_HASH_ENTRY_OVERHEAD;
108                    removed.push(field.clone());
109                }
110            }
111            hash.is_empty()
112        } else {
113            false
114        };
115        if !removed.is_empty() {
116            self.bump_version(key);
117        }
118
119        self.cleanup_after_remove(key, old_entry_size, is_empty, removed_bytes);
120
121        Ok(removed)
122    }
123
124    /// Checks if a field exists in a hash.
125    pub fn hexists(&mut self, key: &str, field: &str) -> Result<bool, WrongType> {
126        let Some(entry) = self.get_live_entry(key) else {
127            return Ok(false);
128        };
129        match &entry.value {
130            Value::Hash(hash) => Ok(hash.contains_key(field)),
131            _ => Err(WrongType),
132        }
133    }
134
135    /// Returns the number of fields in a hash.
136    pub fn hlen(&mut self, key: &str) -> Result<usize, WrongType> {
137        if self.remove_if_expired(key) {
138            return Ok(0);
139        }
140        match self.entries.get(key) {
141            None => Ok(0),
142            Some(entry) => match &entry.value {
143                Value::Hash(hash) => Ok(hash.len()),
144                _ => Err(WrongType),
145            },
146        }
147    }
148
149    /// Increments a field's integer value by the given amount.
150    ///
151    /// Creates the hash and field if they don't exist, starting from 0.
152    pub fn hincrby(&mut self, key: &str, field: &str, delta: i64) -> Result<i64, IncrError> {
153        self.remove_if_expired(key);
154
155        let is_new = match self.entries.get(key) {
156            None => true,
157            Some(e) if matches!(e.value, Value::Hash(_)) => false,
158            Some(_) => return Err(IncrError::WrongType),
159        };
160
161        // estimate memory for new field (worst case: new hash + new field)
162        let val_str_len = 20; // max i64 string length
163        let estimated_increase = if is_new {
164            memory::ENTRY_OVERHEAD
165                + key.len()
166                + memory::PACKED_HASH_BASE_OVERHEAD
167                + field.len()
168                + val_str_len
169                + memory::PACKED_HASH_ENTRY_OVERHEAD
170        } else {
171            field.len() + val_str_len + memory::PACKED_HASH_ENTRY_OVERHEAD
172        };
173
174        if !self.enforce_memory_limit(estimated_increase) {
175            return Err(IncrError::OutOfMemory);
176        }
177
178        if is_new {
179            let value = Value::Hash(Box::default());
180            self.memory.add(key, &value);
181            let entry = Entry::new(value, None);
182            self.entries.insert(CompactString::from(key), entry);
183            self.bump_version(key);
184        }
185
186        // safe: key was either just inserted above or verified to exist
187        let Some(entry) = self.entries.get_mut(key) else {
188            return Err(IncrError::WrongType);
189        };
190        let old_entry_size = entry.entry_size(key);
191
192        let Value::Hash(ref mut hash) = entry.value else {
193            return Err(IncrError::WrongType);
194        };
195        let current_val = match hash.get(field) {
196            Some(data) => {
197                let s = std::str::from_utf8(data).map_err(|_| IncrError::NotAnInteger)?;
198                s.parse::<i64>().map_err(|_| IncrError::NotAnInteger)?
199            }
200            None => 0,
201        };
202        let new_val = current_val.checked_add(delta).ok_or(IncrError::Overflow)?;
203        hash.insert(field.into(), Bytes::from(new_val.to_string()));
204        entry.touch(self.track_access);
205
206        let new_value_size = memory::value_size(&entry.value);
207        entry.cached_value_size = new_value_size as u32;
208        let new_entry_size = key.len() + new_value_size + memory::ENTRY_OVERHEAD;
209        self.memory.adjust(old_entry_size, new_entry_size);
210        self.bump_version(key);
211
212        Ok(new_val)
213    }
214
215    /// Increments a field's float value by the given amount.
216    ///
217    /// If the field doesn't exist, it is created with the increment as its value.
218    /// The stored value must be a valid float or the call returns an error.
219    /// Returns the new value as a formatted string.
220    pub fn hincrbyfloat(
221        &mut self,
222        key: &str,
223        field: &str,
224        delta: f64,
225    ) -> Result<String, IncrFloatError> {
226        self.remove_if_expired(key);
227
228        let is_new = match self.entries.get(key) {
229            None => true,
230            Some(e) if matches!(e.value, Value::Hash(_)) => false,
231            Some(_) => return Err(IncrFloatError::WrongType),
232        };
233
234        // generous float string length estimate
235        let val_str_len = 32usize;
236        let estimated_increase = if is_new {
237            memory::ENTRY_OVERHEAD
238                + key.len()
239                + memory::PACKED_HASH_BASE_OVERHEAD
240                + field.len()
241                + val_str_len
242                + memory::PACKED_HASH_ENTRY_OVERHEAD
243        } else {
244            field.len() + val_str_len + memory::PACKED_HASH_ENTRY_OVERHEAD
245        };
246
247        if !self.enforce_memory_limit(estimated_increase) {
248            return Err(IncrFloatError::OutOfMemory);
249        }
250
251        if is_new {
252            let value = Value::Hash(Box::default());
253            self.memory.add(key, &value);
254            let entry = Entry::new(value, None);
255            self.entries.insert(CompactString::from(key), entry);
256            self.bump_version(key);
257        }
258
259        let Some(entry) = self.entries.get_mut(key) else {
260            return Err(IncrFloatError::WrongType);
261        };
262        let old_entry_size = entry.entry_size(key);
263
264        let Value::Hash(ref mut hash) = entry.value else {
265            return Err(IncrFloatError::WrongType);
266        };
267        let current = match hash.get(field) {
268            Some(data) => {
269                let s = std::str::from_utf8(data).map_err(|_| IncrFloatError::NotAFloat)?;
270                s.parse::<f64>().map_err(|_| IncrFloatError::NotAFloat)?
271            }
272            None => 0.0,
273        };
274        let new_val = current + delta;
275        if new_val.is_nan() || new_val.is_infinite() {
276            return Err(IncrFloatError::NanOrInfinity);
277        }
278
279        let formatted = format_float(new_val);
280        hash.insert(field.into(), Bytes::from(formatted.clone()));
281        entry.touch(self.track_access);
282
283        let new_value_size = memory::value_size(&entry.value);
284        entry.cached_value_size = new_value_size as u32;
285        let new_entry_size = key.len() + new_value_size + memory::ENTRY_OVERHEAD;
286        self.memory.adjust(old_entry_size, new_entry_size);
287        self.bump_version(key);
288
289        Ok(formatted)
290    }
291
292    /// Returns all field names in a hash.
293    pub fn hkeys(&mut self, key: &str) -> Result<Vec<String>, WrongType> {
294        let Some(entry) = self.get_live_entry(key) else {
295            return Ok(vec![]);
296        };
297        match &entry.value {
298            Value::Hash(hash) => Ok(hash.iter().map(|(k, _)| k.to_string()).collect()),
299            _ => Err(WrongType),
300        }
301    }
302
303    /// Returns all values in a hash.
304    pub fn hvals(&mut self, key: &str) -> Result<Vec<Bytes>, WrongType> {
305        let Some(entry) = self.get_live_entry(key) else {
306            return Ok(vec![]);
307        };
308        match &entry.value {
309            Value::Hash(hash) => Ok(hash
310                .iter()
311                .map(|(_, v)| Bytes::copy_from_slice(v))
312                .collect()),
313            _ => Err(WrongType),
314        }
315    }
316
317    /// Incrementally iterates fields of a hash.
318    ///
319    /// Returns the next cursor and a batch of field-value pairs. A returned
320    /// cursor of `0` means the iteration is complete. Pattern matching
321    /// (MATCH) filters on field names.
322    pub fn scan_hash(
323        &mut self,
324        key: &str,
325        cursor: u64,
326        count: usize,
327        pattern: Option<&str>,
328    ) -> Result<(u64, Vec<(String, Bytes)>), WrongType> {
329        let Some(entry) = self.get_live_entry(key) else {
330            return Ok((0, vec![]));
331        };
332        let Value::Hash(ref hash) = entry.value else {
333            return Err(WrongType);
334        };
335
336        let target = if count == 0 { 10 } else { count };
337        let compiled = pattern.map(GlobPattern::new);
338        let mut result = Vec::with_capacity(target);
339        let mut pos = 0u64;
340        let mut done = true;
341
342        for (field, value) in hash.iter() {
343            if pos < cursor {
344                pos += 1;
345                continue;
346            }
347            if let Some(ref pat) = compiled {
348                if !pat.matches(field) {
349                    pos += 1;
350                    continue;
351                }
352            }
353            result.push((field.to_string(), Bytes::copy_from_slice(value)));
354            pos += 1;
355            if result.len() >= target {
356                done = false;
357                break;
358            }
359        }
360
361        Ok(if done { (0, result) } else { (pos, result) })
362    }
363
364    /// Gets multiple field values from a hash.
365    ///
366    /// Returns `None` for fields that don't exist.
367    pub fn hmget(&mut self, key: &str, fields: &[String]) -> Result<Vec<Option<Bytes>>, WrongType> {
368        let Some(entry) = self.get_live_entry(key) else {
369            return Ok(fields.iter().map(|_| None).collect());
370        };
371        match &entry.value {
372            Value::Hash(hash) => Ok(fields
373                .iter()
374                .map(|f| hash.get(f.as_str()).map(Bytes::copy_from_slice))
375                .collect()),
376            _ => Err(WrongType),
377        }
378    }
379
380    /// Returns random field(s) from a hash.
381    ///
382    /// - `count = None`: return one random field (no value, even if `with_values` is set)
383    /// - `count > 0`: return up to count distinct fields
384    /// - `count < 0`: return |count| fields, allowing duplicates
385    ///
386    /// If `with_values` is true and count is `Some`, returns interleaved `(field, Some(value))`
387    /// pairs. When `count` is `None`, only the field name is returned as `(field, None)`.
388    pub fn hrandfield(
389        &mut self,
390        key: &str,
391        count: Option<i64>,
392        with_values: bool,
393    ) -> Result<Vec<(String, Option<Bytes>)>, WrongType> {
394        let Some(entry) = self.get_live_entry(key) else {
395            return Ok(vec![]);
396        };
397        let Value::Hash(ref hash) = entry.value else {
398            return Err(WrongType);
399        };
400        if hash.is_empty() {
401            return Ok(vec![]);
402        }
403
404        // collect into a vec for indexed random access
405        let fields: Vec<(&str, &[u8])> = hash.iter().collect();
406        let mut rng = rand::rng();
407
408        let result = match count {
409            None => {
410                // single random field — no value even with with_values
411                use rand::seq::IteratorRandom;
412                fields
413                    .iter()
414                    .choose(&mut rng)
415                    .map(|(f, _)| ((*f).to_owned(), None))
416                    .into_iter()
417                    .collect()
418            }
419            Some(n) if n > 0 => {
420                use rand::seq::IteratorRandom;
421                let n = (n as usize).min(fields.len());
422                fields
423                    .iter()
424                    .choose_multiple(&mut rng, n)
425                    .into_iter()
426                    .map(|(f, v)| {
427                        let val = if with_values {
428                            Some(Bytes::copy_from_slice(v))
429                        } else {
430                            None
431                        };
432                        ((*f).to_owned(), val)
433                    })
434                    .collect()
435            }
436            Some(n) => {
437                // negative count: allow duplicates, return |n| entries
438                use rand::Rng;
439                let n = n.unsigned_abs() as usize;
440                (0..n)
441                    .map(|_| {
442                        let idx = rng.random_range(0..fields.len());
443                        let (f, v) = fields[idx];
444                        let val = if with_values {
445                            Some(Bytes::copy_from_slice(v))
446                        } else {
447                            None
448                        };
449                        (f.to_owned(), val)
450                    })
451                    .collect()
452            }
453        };
454
455        Ok(result)
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[test]
464    fn hset_creates_hash() {
465        let mut ks = Keyspace::new();
466        let count = ks
467            .hset("h", &[("field1".into(), Bytes::from("value1"))])
468            .unwrap();
469        assert_eq!(count, 1);
470        assert_eq!(ks.value_type("h"), "hash");
471    }
472
473    #[test]
474    fn hset_returns_new_field_count() {
475        let mut ks = Keyspace::new();
476        // add two new fields
477        let count = ks
478            .hset(
479                "h",
480                &[
481                    ("f1".into(), Bytes::from("v1")),
482                    ("f2".into(), Bytes::from("v2")),
483                ],
484            )
485            .unwrap();
486        assert_eq!(count, 2);
487
488        // update one, add one new
489        let count = ks
490            .hset(
491                "h",
492                &[
493                    ("f1".into(), Bytes::from("updated")),
494                    ("f3".into(), Bytes::from("v3")),
495                ],
496            )
497            .unwrap();
498        assert_eq!(count, 1); // only f3 is new
499    }
500
501    #[test]
502    fn hget_returns_value() {
503        let mut ks = Keyspace::new();
504        ks.hset("h", &[("name".into(), Bytes::from("alice"))])
505            .unwrap();
506        let val = ks.hget("h", "name").unwrap();
507        assert_eq!(val, Some(Bytes::from("alice")));
508    }
509
510    #[test]
511    fn hget_missing_field_returns_none() {
512        let mut ks = Keyspace::new();
513        ks.hset("h", &[("a".into(), Bytes::from("1"))]).unwrap();
514        assert_eq!(ks.hget("h", "b").unwrap(), None);
515    }
516
517    #[test]
518    fn hget_missing_key_returns_none() {
519        let mut ks = Keyspace::new();
520        assert_eq!(ks.hget("missing", "field").unwrap(), None);
521    }
522
523    #[test]
524    fn hgetall_returns_all_fields() {
525        let mut ks = Keyspace::new();
526        ks.hset(
527            "h",
528            &[
529                ("a".into(), Bytes::from("1")),
530                ("b".into(), Bytes::from("2")),
531            ],
532        )
533        .unwrap();
534        let mut fields = ks.hgetall("h").unwrap();
535        fields.sort_by(|a, b| a.0.cmp(&b.0));
536        assert_eq!(fields.len(), 2);
537        assert_eq!(fields[0], ("a".into(), Bytes::from("1")));
538        assert_eq!(fields[1], ("b".into(), Bytes::from("2")));
539    }
540
541    #[test]
542    fn hdel_removes_fields() {
543        let mut ks = Keyspace::new();
544        ks.hset(
545            "h",
546            &[
547                ("a".into(), Bytes::from("1")),
548                ("b".into(), Bytes::from("2")),
549                ("c".into(), Bytes::from("3")),
550            ],
551        )
552        .unwrap();
553        let removed = ks.hdel("h", &["a".into(), "c".into()]).unwrap();
554        assert_eq!(removed.len(), 2);
555        assert!(removed.contains(&"a".into()));
556        assert!(removed.contains(&"c".into()));
557        assert_eq!(ks.hlen("h").unwrap(), 1);
558    }
559
560    #[test]
561    fn hdel_auto_deletes_empty_hash() {
562        let mut ks = Keyspace::new();
563        ks.hset("h", &[("only".into(), Bytes::from("field"))])
564            .unwrap();
565        ks.hdel("h", &["only".into()]).unwrap();
566        assert_eq!(ks.value_type("h"), "none");
567    }
568
569    #[test]
570    fn hexists_returns_true_for_existing_field() {
571        let mut ks = Keyspace::new();
572        ks.hset("h", &[("field".into(), Bytes::from("val"))])
573            .unwrap();
574        assert!(ks.hexists("h", "field").unwrap());
575    }
576
577    #[test]
578    fn hexists_returns_false_for_missing_field() {
579        let mut ks = Keyspace::new();
580        ks.hset("h", &[("a".into(), Bytes::from("1"))]).unwrap();
581        assert!(!ks.hexists("h", "missing").unwrap());
582    }
583
584    #[test]
585    fn hlen_returns_field_count() {
586        let mut ks = Keyspace::new();
587        ks.hset(
588            "h",
589            &[
590                ("a".into(), Bytes::from("1")),
591                ("b".into(), Bytes::from("2")),
592            ],
593        )
594        .unwrap();
595        assert_eq!(ks.hlen("h").unwrap(), 2);
596    }
597
598    #[test]
599    fn hlen_missing_key_returns_zero() {
600        let mut ks = Keyspace::new();
601        assert_eq!(ks.hlen("missing").unwrap(), 0);
602    }
603
604    #[test]
605    fn hincrby_new_field() {
606        let mut ks = Keyspace::new();
607        ks.hset("h", &[("x".into(), Bytes::from("ignored"))])
608            .unwrap();
609        let val = ks.hincrby("h", "counter", 5).unwrap();
610        assert_eq!(val, 5);
611    }
612
613    #[test]
614    fn hincrby_existing_field() {
615        let mut ks = Keyspace::new();
616        ks.hset("h", &[("n".into(), Bytes::from("10"))]).unwrap();
617        let val = ks.hincrby("h", "n", 3).unwrap();
618        assert_eq!(val, 13);
619    }
620
621    #[test]
622    fn hincrby_negative_delta() {
623        let mut ks = Keyspace::new();
624        ks.hset("h", &[("n".into(), Bytes::from("10"))]).unwrap();
625        let val = ks.hincrby("h", "n", -7).unwrap();
626        assert_eq!(val, 3);
627    }
628
629    #[test]
630    fn hincrby_non_integer_returns_error() {
631        let mut ks = Keyspace::new();
632        ks.hset("h", &[("s".into(), Bytes::from("notanumber"))])
633            .unwrap();
634        assert_eq!(
635            ks.hincrby("h", "s", 1).unwrap_err(),
636            IncrError::NotAnInteger
637        );
638    }
639
640    #[test]
641    fn hkeys_returns_field_names() {
642        let mut ks = Keyspace::new();
643        ks.hset(
644            "h",
645            &[
646                ("alpha".into(), Bytes::from("1")),
647                ("beta".into(), Bytes::from("2")),
648            ],
649        )
650        .unwrap();
651        let mut keys = ks.hkeys("h").unwrap();
652        keys.sort();
653        assert_eq!(keys, vec!["alpha", "beta"]);
654    }
655
656    #[test]
657    fn hvals_returns_values() {
658        let mut ks = Keyspace::new();
659        ks.hset(
660            "h",
661            &[
662                ("a".into(), Bytes::from("x")),
663                ("b".into(), Bytes::from("y")),
664            ],
665        )
666        .unwrap();
667        let mut vals = ks.hvals("h").unwrap();
668        vals.sort();
669        assert_eq!(vals, vec![Bytes::from("x"), Bytes::from("y")]);
670    }
671
672    #[test]
673    fn hmget_returns_values_for_existing_fields() {
674        let mut ks = Keyspace::new();
675        ks.hset(
676            "h",
677            &[
678                ("a".into(), Bytes::from("1")),
679                ("b".into(), Bytes::from("2")),
680            ],
681        )
682        .unwrap();
683        let vals = ks
684            .hmget("h", &["a".into(), "missing".into(), "b".into()])
685            .unwrap();
686        assert_eq!(vals.len(), 3);
687        assert_eq!(vals[0], Some(Bytes::from("1")));
688        assert_eq!(vals[1], None);
689        assert_eq!(vals[2], Some(Bytes::from("2")));
690    }
691
692    #[test]
693    fn hash_on_string_key_returns_wrongtype() {
694        let mut ks = Keyspace::new();
695        ks.set("s".into(), Bytes::from("string"), None, false, false);
696        assert!(ks.hset("s", &[("f".into(), Bytes::from("v"))]).is_err());
697        assert!(ks.hget("s", "f").is_err());
698        assert!(ks.hgetall("s").is_err());
699        assert!(ks.hdel("s", &["f".into()]).is_err());
700        assert!(ks.hexists("s", "f").is_err());
701        assert!(ks.hlen("s").is_err());
702        assert!(ks.hincrby("s", "f", 1).is_err());
703        assert!(ks.hkeys("s").is_err());
704        assert!(ks.hvals("s").is_err());
705        assert!(ks.hmget("s", &["f".into()]).is_err());
706    }
707
708    #[test]
709    fn hincrby_overflow_returns_error() {
710        let mut ks = Keyspace::new();
711        // set field to near max
712        ks.hset("h", &[("count".into(), Bytes::from(i64::MAX.to_string()))])
713            .unwrap();
714
715        // try to increment by 1 - should overflow
716        let result = ks.hincrby("h", "count", 1);
717        assert!(result.is_err());
718    }
719
720    #[test]
721    fn hincrby_on_non_integer_returns_error() {
722        let mut ks = Keyspace::new();
723        ks.hset("h", &[("field".into(), Bytes::from("not_a_number"))])
724            .unwrap();
725
726        let result = ks.hincrby("h", "field", 1);
727        assert!(result.is_err());
728    }
729
730    // --- scan_hash ---
731
732    #[test]
733    fn scan_hash_returns_all() {
734        let mut ks = Keyspace::new();
735        ks.hset(
736            "h",
737            &[
738                ("a".into(), Bytes::from("1")),
739                ("b".into(), Bytes::from("2")),
740                ("c".into(), Bytes::from("3")),
741            ],
742        )
743        .unwrap();
744        let (cursor, fields) = ks.scan_hash("h", 0, 100, None).unwrap();
745        assert_eq!(cursor, 0);
746        assert_eq!(fields.len(), 3);
747    }
748
749    #[test]
750    fn scan_hash_missing_key() {
751        let mut ks = Keyspace::new();
752        let (cursor, fields) = ks.scan_hash("missing", 0, 10, None).unwrap();
753        assert_eq!(cursor, 0);
754        assert!(fields.is_empty());
755    }
756
757    #[test]
758    fn scan_hash_wrong_type() {
759        let mut ks = Keyspace::new();
760        ks.set("h".into(), Bytes::from("string"), None, false, false);
761        assert!(ks.scan_hash("h", 0, 10, None).is_err());
762    }
763
764    #[test]
765    fn scan_hash_with_pattern() {
766        let mut ks = Keyspace::new();
767        ks.hset(
768            "h",
769            &[
770                ("name".into(), Bytes::from("alice")),
771                ("age".into(), Bytes::from("30")),
772                ("nickname".into(), Bytes::from("ali")),
773            ],
774        )
775        .unwrap();
776        let (_, fields) = ks.scan_hash("h", 0, 100, Some("n*")).unwrap();
777        assert_eq!(fields.len(), 2);
778        assert!(fields.iter().all(|(f, _)| f.starts_with('n')));
779    }
780
781    #[test]
782    fn scan_hash_pagination() {
783        let mut ks = Keyspace::new();
784        let fields: Vec<(String, Bytes)> = (0..20)
785            .map(|i| (format!("f{i}"), Bytes::from(format!("v{i}"))))
786            .collect();
787        ks.hset("h", &fields).unwrap();
788
789        let mut collected = Vec::new();
790        let mut cursor = 0u64;
791        loop {
792            let (next, batch) = ks.scan_hash("h", cursor, 5, None).unwrap();
793            collected.extend(batch);
794            if next == 0 {
795                break;
796            }
797            cursor = next;
798        }
799        assert_eq!(collected.len(), 20);
800    }
801
802    #[test]
803    fn hash_auto_deleted_when_empty() {
804        let mut ks = Keyspace::new();
805        ks.hset(
806            "h",
807            &[
808                ("f1".into(), Bytes::from("v1")),
809                ("f2".into(), Bytes::from("v2")),
810            ],
811        )
812        .unwrap();
813        assert_eq!(ks.len(), 1);
814
815        // delete all fields
816        ks.hdel("h", &["f1".into(), "f2".into()]).unwrap();
817
818        // hash should be auto-deleted
819        assert_eq!(ks.len(), 0);
820        assert!(!ks.exists("h"));
821    }
822
823    // --- hrandfield ---
824
825    #[test]
826    fn hrandfield_no_count_returns_single_field() {
827        let mut ks = Keyspace::new();
828        ks.hset(
829            "h",
830            &[
831                ("a".into(), Bytes::from("1")),
832                ("b".into(), Bytes::from("2")),
833                ("c".into(), Bytes::from("3")),
834            ],
835        )
836        .unwrap();
837        let result = ks.hrandfield("h", None, false).unwrap();
838        assert_eq!(result.len(), 1);
839        assert!(["a", "b", "c"].contains(&result[0].0.as_str()));
840        // no count means no value, even with_values would be ignored
841        assert!(result[0].1.is_none());
842    }
843
844    #[test]
845    fn hrandfield_positive_count_distinct() {
846        let mut ks = Keyspace::new();
847        ks.hset(
848            "h",
849            &[
850                ("a".into(), Bytes::from("1")),
851                ("b".into(), Bytes::from("2")),
852                ("c".into(), Bytes::from("3")),
853            ],
854        )
855        .unwrap();
856        let result = ks.hrandfield("h", Some(2), false).unwrap();
857        assert_eq!(result.len(), 2);
858        // all returned should be valid fields
859        for (f, v) in &result {
860            assert!(["a", "b", "c"].contains(&f.as_str()));
861            assert!(v.is_none());
862        }
863        // distinct
864        let unique: std::collections::HashSet<_> = result.iter().map(|(f, _)| f).collect();
865        assert_eq!(unique.len(), 2);
866    }
867
868    #[test]
869    fn hrandfield_positive_count_capped_at_hash_size() {
870        let mut ks = Keyspace::new();
871        ks.hset(
872            "h",
873            &[
874                ("a".into(), Bytes::from("1")),
875                ("b".into(), Bytes::from("2")),
876            ],
877        )
878        .unwrap();
879        let result = ks.hrandfield("h", Some(10), false).unwrap();
880        assert_eq!(result.len(), 2);
881    }
882
883    #[test]
884    fn hrandfield_negative_count_allows_duplicates() {
885        let mut ks = Keyspace::new();
886        ks.hset("h", &[("only".into(), Bytes::from("v"))]).unwrap();
887        let result = ks.hrandfield("h", Some(-5), false).unwrap();
888        assert_eq!(result.len(), 5);
889        assert!(result.iter().all(|(f, _)| f == "only"));
890    }
891
892    #[test]
893    fn hrandfield_with_values() {
894        let mut ks = Keyspace::new();
895        ks.hset(
896            "h",
897            &[
898                ("field".into(), Bytes::from("value")),
899                ("other".into(), Bytes::from("data")),
900            ],
901        )
902        .unwrap();
903        let result = ks.hrandfield("h", Some(2), true).unwrap();
904        assert_eq!(result.len(), 2);
905        for (f, v) in &result {
906            assert!(["field", "other"].contains(&f.as_str()));
907            assert!(v.is_some());
908        }
909    }
910
911    #[test]
912    fn hrandfield_missing_key_returns_empty() {
913        let mut ks = Keyspace::new();
914        assert!(ks.hrandfield("missing", None, false).unwrap().is_empty());
915        assert!(ks.hrandfield("missing", Some(5), true).unwrap().is_empty());
916    }
917
918    #[test]
919    fn hrandfield_wrong_type_returns_error() {
920        let mut ks = Keyspace::new();
921        ks.set("s".into(), Bytes::from("val"), None, false, false);
922        assert!(ks.hrandfield("s", None, false).is_err());
923    }
924}