kevy_store/value.rs
1//! Value types — one backing structure per Redis type.
2//!
3//! ```
4//! use kevy_store::{SetCondition, Store, Value};
5//! let mut s = Store::new();
6//! s.set(b"n", b"42".to_vec(), None, SetCondition::Always);
7//! s.sadd(b"tags", &[b"a".as_slice()])?;
8//! let mut types = Vec::new();
9//! s.snapshot_each(|_, v: &Value, _| types.push(v.type_name()));
10//! types.sort();
11//! assert_eq!(types, ["set", "string"]);
12//! # Ok::<(), kevy_store::StoreError>(())
13//! ```
14
15#[cfg(not(feature = "std"))]
16use crate::nostd_prelude::*;
17use alloc::collections::VecDeque;
18use core::cmp::Ordering;
19pub use kevy_bytes::SmallBytes;
20use kevy_map::{KevyMap, KevySet};
21use kevy_ranktree::RankTree;
22
23/// Backing structure for a Hash value — [`KevyMap`] keyed by [`SmallBytes`]
24/// (22 B inline / heap-else). Field names ≤22B (the vast majority — `name`,
25/// `email`, etc.) live entirely inside the bucket, saving the 24 B Vec
26/// metadata + heap allocation per field on a 22-byte budget.
27///
28/// ```
29/// use kevy_store::{HashData, SmallBytes};
30/// let mut h = HashData::new();
31/// h.insert(SmallBytes::from_slice(b"name"), SmallBytes::from_slice(b"ada"));
32/// assert_eq!(h.get(b"name".as_slice()).map(|v| v.as_ref()), Some(&b"ada"[..]));
33/// ```
34pub type HashData = KevyMap<SmallBytes, SmallBytes>;
35/// Backing structure for a List value (a ring-buffer deque — O(1) both ends).
36///
37/// ```
38/// use kevy_store::ListData;
39/// let mut l = ListData::new();
40/// l.push_back(b"b".to_vec());
41/// l.push_front(b"a".to_vec());
42/// assert_eq!(l, [b"a".to_vec(), b"b".to_vec()]);
43/// ```
44pub type ListData = VecDeque<Vec<u8>>;
45/// Backing structure for a Set value — [`KevySet`] of [`SmallBytes`].
46///
47/// ```
48/// use kevy_store::{SetData, SmallBytes};
49/// let mut s = SetData::new();
50/// assert!(s.insert(SmallBytes::from_slice(b"m")));
51/// assert!(!s.insert(SmallBytes::from_slice(b"m")), "members are unique");
52/// assert!(s.contains(b"m".as_slice()));
53/// ```
54pub type SetData = KevySet<SmallBytes>;
55
56/// A total-ordered f64 score (Redis scores are never NaN). `total_cmp` gives a
57/// total order so scores can key an ordered container.
58///
59/// `PartialEq` is written rather than derived, and it must stay that way.
60/// Derived, it is `f64`'s `==`, which disagrees with the `total_cmp` below
61/// on `-0.0`: `==` calls it equal to `0.0`, `total_cmp` orders it before.
62/// Rust requires of an `Ord` key that `a == b` exactly when `a.cmp(b)` is
63/// `Equal`, and a `Score` that breaks that is a key whose container and
64/// whose callers disagree about which entries are the same one.
65///
66/// It was latent while nothing compared two `Score`s — `ZSetData::insert`
67/// reached the tree only through `Ord`, and its unconditional
68/// remove-then-insert never had to ask. `ZADD z -0 m` is accepted and
69/// `ZADD z2 -0 a; ZADD z2 0 b` really does order `a` first, so the first
70/// caller to write `old == score` would have skipped a live update and left
71/// `by_member` and `by_score` holding different scores for one member.
72/// NaN cannot arrive: the parser refuses it.
73///
74/// ```
75/// use kevy_store::Score;
76/// assert!(Score::new(-0.0) < Score::new(0.0));
77/// assert_ne!(Score::new(-0.0), Score::new(0.0));
78/// assert_eq!(Score::new(1.5).value(), 1.5);
79/// ```
80#[derive(Debug, Clone, Copy)]
81pub struct Score(pub(crate) f64);
82
83impl Score {
84 /// Wrap a score.
85 pub const fn new(score: f64) -> Self {
86 Self(score)
87 }
88
89 /// The score as a float.
90 pub const fn value(self) -> f64 {
91 self.0
92 }
93}
94impl PartialEq for Score {
95 fn eq(&self, other: &Self) -> bool {
96 self.0.total_cmp(&other.0) == Ordering::Equal
97 }
98}
99impl Eq for Score {}
100impl Ord for Score {
101 fn cmp(&self, other: &Self) -> Ordering {
102 self.0.total_cmp(&other.0)
103 }
104}
105impl PartialOrd for Score {
106 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
107 Some(self.cmp(other))
108 }
109}
110
111/// A score-range endpoint for `ZRANGEBYSCORE`/`ZCOUNT` (inclusive or exclusive).
112/// Use `value = ±INFINITY` for `-inf`/`+inf`.
113///
114/// ```
115/// use kevy_store::ScoreBound;
116/// let lo = ScoreBound::exclusive(1.0);
117/// assert!(lo.exclusive && lo.value == 1.0);
118/// assert!(!ScoreBound::inclusive(f64::INFINITY).exclusive);
119/// ```
120#[derive(Debug, Clone, Copy, PartialEq)]
121#[non_exhaustive]
122pub struct ScoreBound {
123 /// The score itself. `f64::INFINITY` / `NEG_INFINITY` carry `+inf` and
124 /// `-inf`, which is why this is not an `Option`.
125 ///
126 /// ```
127 /// use kevy_store::ScoreBound;
128 /// assert_eq!(ScoreBound::inclusive(2.5).value, 2.5);
129 /// assert_eq!(ScoreBound::inclusive(f64::NEG_INFINITY).value, f64::NEG_INFINITY, "-inf");
130 /// ```
131 pub value: f64,
132 /// `true` for Redis's `(` prefix — the endpoint is excluded from the
133 /// range. An exclusive infinity is accepted and means the same as an
134 /// inclusive one, since nothing equals infinity.
135 ///
136 /// ```
137 /// use kevy_store::{ScoreBound, Store};
138 /// let mut s = Store::new();
139 /// s.zadd(b"z", &[(1.0, b"a".as_slice()), (2.0, b"b")])?;
140 /// let (lo, hi) = (ScoreBound::inclusive(1.0), ScoreBound::inclusive(2.0));
141 /// assert_eq!(s.zcount(b"z", lo, hi)?, 2);
142 /// assert_eq!(s.zcount(b"z", ScoreBound::exclusive(1.0), hi)?, 1, "(1 drops a");
143 /// # Ok::<(), kevy_store::StoreError>(())
144 /// ```
145 pub exclusive: bool,
146}
147impl ScoreBound {
148 /// An endpoint the range includes (`ZRANGEBYSCORE`'s bare form).
149 pub const fn inclusive(value: f64) -> Self {
150 Self { value, exclusive: false }
151 }
152
153 /// An endpoint the range excludes (Redis's `(` prefix).
154 pub const fn exclusive(value: f64) -> Self {
155 Self { value, exclusive: true }
156 }
157
158 /// Does `s` satisfy this as a *minimum* bound?
159 pub(crate) fn ge_ok(&self, s: f64) -> bool {
160 if self.exclusive { s > self.value } else { s >= self.value }
161 }
162 /// Does `s` satisfy this as a *maximum* bound?
163 pub(crate) fn le_ok(&self, s: f64) -> bool {
164 if self.exclusive { s < self.value } else { s <= self.value }
165 }
166}
167
168/// Sorted set: a member→score map plus an order-statistic B-tree keyed by
169/// `(score, member)` ([`kevy_ranktree::RankTree`] — every node carries its
170/// subtree count), so rank queries (`ZRANK`, `ZRANGE` by rank, `ZCOUNT`,
171/// score-bound seeks) are O(log N) descents instead of linear walks.
172///
173/// ```
174/// use kevy_store::{Store, Value};
175/// let mut s = Store::new();
176/// let members: Vec<(f64, &[u8])> =
177/// [(3.0, &b"c"[..]), (1.0, b"a"), (2.0, b"b"), (4.0, b"d"), (5.0, b"e")].to_vec();
178/// s.zadd(b"z", &members)?;
179/// let mut order = Vec::new();
180/// s.snapshot_each(|_, v, _| {
181/// if let Value::ZSet(z) = v {
182/// order = z.ordered().map(|(m, _)| m.to_vec()).collect();
183/// }
184/// });
185/// assert_eq!(order, [b"a", b"b", b"c", b"d", b"e"]);
186/// # Ok::<(), kevy_store::StoreError>(())
187/// ```
188#[derive(Debug, Default, Clone)]
189pub struct ZSetData {
190 pub(crate) by_member: KevyMap<SmallBytes, f64>,
191 /// The `(score, member)` order-statistic index. Member is a
192 /// [`SmallBytes`] (≤22 B inline in the node's key slot), ordered by
193 /// byte-lexicographic `Ord` — the same order the old `Vec<u8>` gave.
194 pub(crate) by_score: RankTree<(Score, SmallBytes)>,
195}
196
197impl ZSetData {
198 pub(crate) fn insert(&mut self, member: &[u8], score: f64) -> bool {
199 let is_new = match self.by_member.insert(SmallBytes::from_slice(member), score) {
200 Some(old) => {
201 // An unchanged score has nothing to reorder. Without this,
202 // the remove below takes (old, member) out of the index and
203 // the insert at the bottom puts (score, member) back — the
204 // same key, so the tree ends exactly where it began, having
205 // paid a descent, a removal, an insertion and two extra
206 // SmallBytes for it. Measured at 30.6% of the operation
207 // on an 8,000-member set — see the ZADD decomposition
208 // under bench/, which prices it against ZSCORE.
209 //
210 // Through `Score`, never `f64`. The two disagree on -0.0,
211 // the tree keys on `Score`, and `==` here would skip a real
212 // reordering and leave this map and that tree holding
213 // different scores for one member.
214 if Score(old) == Score(score) {
215 return false;
216 }
217 self.by_score.remove(&(Score(old), SmallBytes::from_slice(member)));
218 false
219 }
220 None => true,
221 };
222 self.by_score.insert((Score(score), SmallBytes::from_slice(member)));
223 is_new
224 }
225 pub(crate) fn remove(&mut self, member: &[u8]) -> bool {
226 match self.by_member.remove(member) {
227 Some(old) => {
228 self.by_score.remove(&(Score(old), SmallBytes::from_slice(member)));
229 true
230 }
231 None => false,
232 }
233 }
234 pub(crate) fn len(&self) -> usize {
235 self.by_member.len()
236 }
237 /// `(member, score)` pairs in ascending `(score, member)` order.
238 pub fn ordered(&self) -> impl Iterator<Item = (&[u8], f64)> {
239 self.by_score.iter().map(|(s, m)| (m.as_slice(), s.0))
240 }
241 /// Like [`Self::ordered`] but starting at ascending `rank` — one
242 /// O(log N) seek, no skip-walk.
243 pub(crate) fn ordered_from(&self, rank: usize) -> impl Iterator<Item = (&[u8], f64)> {
244 self.by_score.iter_from(rank).map(|(s, m)| (m.as_slice(), s.0))
245 }
246 /// The ascending rank of `member` (whose score is `score`). O(log N).
247 pub(crate) fn rank_of(&self, member: &[u8], score: f64) -> Option<usize> {
248 self.by_score.rank_of(&(Score(score), SmallBytes::from_slice(member)))
249 }
250 /// First rank whose score satisfies `min` as a lower bound. O(log N).
251 pub(crate) fn score_start_rank(&self, min: &ScoreBound) -> usize {
252 self.by_score.partition_point(|(s, _)| !min.ge_ok(s.0))
253 }
254 /// First rank whose score fails `max` as an upper bound (i.e. one past
255 /// the last in-range rank). O(log N).
256 pub(crate) fn score_end_rank(&self, max: &ScoreBound) -> usize {
257 self.by_score.partition_point(|(s, _)| max.le_ok(s.0))
258 }
259}
260
261pub use crate::value_cold::{COLD_TAG_HASH, COLD_TAG_STRING, ColdRef};
262
263#[cfg(feature = "std")]
264pub use crate::value_enum::BioDropSender;
265pub use crate::value_enum::{BULK_THRESHOLD, HEAP_HEAVY_BYTES, Value};
266
267/// Per-bucket footprint for a sorted set's member map (open-addressing Swiss
268/// table). Approximation, not exact: includes metadata byte per slot plus the
269/// boxed `K`/`V` cell, padded for 7/8 load factor. Hashes are charged their
270/// table's real size instead (`hash_weight`).
271pub(crate) const HASH_SLOT_BYTES: u64 = 32;
272pub(crate) const SET_SLOT_BYTES: u64 = 24;
273/// `VecDeque` ring-buffer slot per stored `Vec<u8>` header (24 B Vec metadata).
274pub(crate) const LIST_SLOT_BYTES: u64 = 24;
275/// `BTreeSet`/`BTreeMap` per-entry overhead (node pointers + B-tree node
276/// padding) — the stream index's accounting constant.
277pub(crate) const BTREE_SLOT_BYTES: u64 = 40;
278/// `kevy_ranktree::RankTree` per-key overhead. Measured from the structure:
279/// the `(Score, SmallBytes)` key slot is 32 B; nodes hold ≤15 keys in a Vec
280/// whose buffer rounds to 16 slots at ~2/3 fill (≈10-11 live keys), so the
281/// key arrays amortise to ≈48 B per key; the per-node fixed cost (56 B
282/// header + Box allocation, ~1 node per 10 keys) and the internal nodes'
283/// child-pointer arrays add ≈8 B more. 64 errs slightly high (allocator
284/// size-class rounding), keeping `used_memory` a conservative upper bound —
285/// same policy as [`ENTRY_OVERHEAD`].
286pub(crate) const RANKTREE_SLOT_BYTES: u64 = 64;
287/// A keyspace slot's amortised cost: the inline 24-byte `SmallBytes` key
288/// cell, the `Entry` and a control byte, over the table's load. The tiered
289/// store charges each cold stub this in `stub_bytes`, the estimate of what
290/// its cold keys cost in memory.
291///
292/// `used_memory` does not use it: the keyspace table is charged there at
293/// its real size, whatever its load.
294///
295/// ```
296/// use kevy_store::{ENTRY_OVERHEAD, SetCondition, Store};
297/// let mut s = Store::new();
298/// s.set(b"k", b"v".to_vec(), None, SetCondition::Always);
299/// // one key in a sixteen-slot table: the table costs far more than a slot
300/// assert!(s.used_memory() > ENTRY_OVERHEAD);
301/// ```
302pub const ENTRY_OVERHEAD: u64 = 96;
303
304#[inline]
305pub(crate) fn collection_overhead(capacity: usize, per_slot: u64) -> u64 {
306 (capacity as u64).saturating_mul(per_slot)
307}
308
309/// What a new hash field adds to its hash's weight besides the table: the
310/// allocator's footprint of the field name's heap and of the value's heap
311/// (`value_heap` bytes), each 0 when it is short enough to sit inline in
312/// the slot. The slot itself is part of the table, which is charged as the
313/// table grows rather than per field.
314///
315/// ```
316/// use kevy_store::{SmallBytes, hash_field_weight};
317/// let short = SmallBytes::from_slice(b"name");
318/// let long = SmallBytes::from_slice(&[b'f'; 64]);
319/// // an inline field name and value add nothing
320/// assert_eq!(hash_field_weight(&short, 0), 0);
321/// // a spilled one adds what the allocator holds for it: 64 bytes asked, 80 held
322/// assert_eq!(hash_field_weight(&long, 0), 80);
323/// assert_eq!(hash_field_weight(&short, 900), 912);
324/// ```
325#[inline]
326pub fn hash_field_weight(field: &SmallBytes, value_heap: usize) -> u64 {
327 (kevy_map::malloc_footprint(field.heap_bytes()) + kevy_map::malloc_footprint(value_heap)) as u64
328}
329
330/// Per-member delta a new set member charges. Mirrors [`hash_field_weight`]
331/// for the set variant (no separate value, single bucket slot).
332///
333/// ```
334/// use kevy_store::{SmallBytes, set_member_weight};
335/// let inline = SmallBytes::from_slice(b"m");
336/// let spilled = SmallBytes::from_slice(&[b'm'; 64]);
337/// assert_eq!(
338/// set_member_weight(&spilled) - set_member_weight(&inline),
339/// spilled.heap_bytes() as u64
340/// );
341/// ```
342#[inline]
343pub fn set_member_weight(member: &SmallBytes) -> u64 {
344 member.heap_bytes() as u64 + SET_SLOT_BYTES
345}
346
347/// Per-item delta a new list element charges (Vec header slot + heap cap).
348///
349/// ```
350/// use kevy_store::list_item_weight;
351/// // the slot cost is fixed; the element's capacity is added on top
352/// assert_eq!(list_item_weight(100) - list_item_weight(0), 100);
353/// ```
354#[inline]
355pub fn list_item_weight(value_cap: usize) -> u64 {
356 LIST_SLOT_BYTES + value_cap as u64
357}
358
359/// Per-member delta a new zset member charges: hash slot for `by_member` +
360/// rank-tree slot for `by_score` + the member's heap bytes — twice, because
361/// a heap-spilling member (>22 B) is stored in both structures (inline
362/// members cost 0 here, matching [`Value::weight`]'s ZSet arm).
363///
364/// ```
365/// use kevy_store::{SmallBytes, zset_member_weight};
366/// let inline = SmallBytes::from_slice(b"m");
367/// let spilled = SmallBytes::from_slice(&[b'm'; 64]);
368/// // a spilled member is stored twice: in the map and in the rank tree
369/// assert_eq!(
370/// zset_member_weight(&spilled) - zset_member_weight(&inline),
371/// 2 * spilled.heap_bytes() as u64
372/// );
373/// ```
374#[inline]
375pub fn zset_member_weight(member: &SmallBytes) -> u64 {
376 2 * member.heap_bytes() as u64 + HASH_SLOT_BYTES + RANKTREE_SLOT_BYTES
377}