Skip to main content

kevy_store/
list.rs

1//! `Store` list write commands. Reads live in `list_read.rs`.
2//!
3//! Three encodings, promoted in order of size: `SmallListInline`
4//! (≤8 tiny elements, in the Value body) → `List(Arc<VecDeque>)` (flat
5//! heap) → `SegList` (per-segment COW past
6//! [`crate::list_seg::SEG_PROMOTE`] elements — a write under a live
7//! snapshot view clones one segment, not the whole value).
8
9use crate::list_seg::{SEG_PROMOTE, SegListData};
10#[cfg(not(feature = "std"))]
11use crate::nostd_prelude::*;
12use crate::small_list::{self, PushResult, SmallListData};
13use crate::util::{norm_index, range_bounds};
14use crate::value::{ListData, SmallBytes, Value, list_item_weight};
15use crate::{Entry, Store, StoreError};
16use alloc::sync::Arc;
17
18/// Push into an inline list, promoting it to the heap encoding if the value
19/// does not fit. Returns whether the promotion happened, which is the only
20/// thing the caller does differently.
21///
22/// Split from `list_push_one` for the 50-line rule. `slot` must be a
23/// `Value::SmallListInline`; the caller checks, and the `else` here pushes
24/// nothing rather than asserting, because a wrong caller should not be a
25/// panic in the write path.
26fn push_inline(slot: &mut Value, v: &[u8], front: bool) -> bool {
27    let Value::SmallListInline(s) = slot else { return false };
28    let push = if front { s.try_push_front(v) } else { s.try_push_back(v) };
29    match push {
30        PushResult::Pushed => false,
31        PushResult::NoRoom => {
32            let mut promoted = small_list::promote(s);
33            if front {
34                promoted.push_front(v.to_vec());
35            } else {
36                promoted.push_back(v.to_vec());
37            }
38            *slot = Value::List(Arc::new(promoted));
39            true
40        }
41    }
42}
43
44impl Store {
45    // ---- lists ---------------------------------------------------------
46
47    /// Borrow the key's flat list mutably; promote inline → heap if
48    /// needed. `create == true` materialises a fresh empty heap list
49    /// when the key is missing (the `lset/lpop/rpop/lrem/ltrim` legacy
50    /// paths). Callers dispatch the `SegList` encoding BEFORE calling
51    /// this — a seg-encoded key never reaches the `make_mut` here.
52    fn list_mut(&mut self, key: &[u8], create: bool) -> Result<Option<&mut ListData>, StoreError> {
53        if self.live_entry_mut(key).is_none() {
54            if !create {
55                return Ok(None);
56            }
57            self.insert_entry(
58                SmallBytes::from_slice(key),
59                Entry::new(Value::List(Arc::default()), None),
60            );
61        }
62        // A.8: see hash.rs::hash_mut — promote out-of-scope, then
63        // re-borrow as the heap variant.
64        let is_inline =
65            matches!(self.map.get(key).map(|e| &e.value), Some(Value::SmallListInline(_)));
66        if is_inline {
67            let promoted = {
68                let e = self.map.get(key).expect("present");
69                if let Value::SmallListInline(s) = &e.value {
70                    small_list::promote(s)
71                } else {
72                    unreachable!()
73                }
74            };
75            self.map.get_mut(key).expect("present").value = Value::List(Arc::new(promoted));
76            self.reweigh_entry(key);
77        }
78        match &mut self.map.get_mut(key).expect("present").value {
79            Value::List(l) => Ok(Some(Arc::make_mut(l))),
80            _ => Err(StoreError::WrongType),
81        }
82    }
83
84    /// Whether `key` currently holds the `SegList` encoding (after lazy
85    /// expiry). The write ops branch on this before the flat path.
86    fn is_seglist(&mut self, key: &[u8]) -> bool {
87        matches!(self.live_entry_mut(key).map(|e| &e.value), Some(Value::SegList(_)))
88    }
89
90    /// Borrow the seg-encoded list mutably. Caller has checked
91    /// [`Self::is_seglist`]; the outer `make_mut` here is the cheap
92    /// pointer-array clone (the per-segment clones happen inside
93    /// `SegListData`'s ops, only on touched segments).
94    fn seglist_mut(&mut self, key: &[u8]) -> &mut SegListData {
95        match &mut self.map.get_mut(key).expect("is_seglist checked").value {
96            Value::SegList(l) => Arc::make_mut(l),
97            _ => unreachable!("is_seglist checked"),
98        }
99    }
100
101    /// A.8: read the key's list slot for LPUSH/RPUSH. `WrongType` on
102    /// non-list. Returns `None` when key is absent — caller creates.
103    fn list_value_for_push(&mut self, key: &[u8]) -> Result<Option<&mut Value>, StoreError> {
104        match self.live_entry_mut(key) {
105            None => Ok(None),
106            Some(e) => match &e.value {
107                Value::List(_) | Value::SegList(_) | Value::SmallListInline(_) => {
108                    Ok(Some(&mut e.value))
109                }
110                _ => Err(StoreError::WrongType),
111            },
112        }
113    }
114
115    /// Remove `key` if it now holds an empty list (any encoding).
116    fn drop_if_empty_list(&mut self, key: &[u8]) {
117        let empty = match self.map.get(key).map(|e| &e.value) {
118            Some(Value::List(l)) => l.is_empty(),
119            Some(Value::SegList(l)) => l.is_empty(),
120            Some(Value::SmallListInline(l)) => l.is_empty(),
121            _ => false,
122        };
123        if empty {
124            self.remove_entry(key);
125        }
126    }
127
128    /// Return the list's length (any encoding). Used by the public
129    /// push functions to compute "new length" after spending entries.
130    fn list_len(&self, key: &[u8]) -> usize {
131        match self.map.get(key).map(|e| &e.value) {
132            Some(Value::List(l)) => l.len(),
133            Some(Value::SegList(l)) => l.len(),
134            Some(Value::SmallListInline(l)) => l.len(),
135            _ => 0,
136        }
137    }
138
139    /// `LPUSH` — prepend each value in turn; returns the new length.
140    pub fn lpush(&mut self, key: &[u8], values: &[&[u8]]) -> Result<usize, StoreError> {
141        if values.is_empty() {
142            return Ok(self.list_len(key));
143        }
144        let mut delta: i64 = 0;
145        for v in values {
146            delta += self.list_push_one(key, v, /* front= */ true)?;
147        }
148        self.account_delta(key, delta);
149        Ok(self.list_len(key))
150    }
151
152    /// `RPUSH` — append each value; returns the new length.
153    pub fn rpush(&mut self, key: &[u8], values: &[&[u8]]) -> Result<usize, StoreError> {
154        if values.is_empty() {
155            return Ok(self.list_len(key));
156        }
157        let mut delta: i64 = 0;
158        for v in values {
159            delta += self.list_push_one(key, v, /* front= */ false)?;
160        }
161        self.account_delta(key, delta);
162        Ok(self.list_len(key))
163    }
164
165    /// Push one element, applying the encoding-switch. Returns the
166    /// per-element weight delta (zero for inline / reweighed cases,
167    /// list_item_weight for heap). `front=true` for LPUSH.
168    fn list_push_one(&mut self, key: &[u8], v: &[u8], front: bool) -> Result<i64, StoreError> {
169        if self.list_value_for_push(key)?.is_none() {
170            return Ok(self.list_push_create(key, v));
171        }
172        let slot = self.list_value_for_push(key)?.expect("present and a list");
173        if matches!(slot, Value::SmallListInline(_)) {
174            // Promotion reweighs from scratch, so the caller's delta for
175            // THIS pair is 0 either way — the new weight already has it.
176            if push_inline(slot, v, front) {
177                self.reweigh_entry(key);
178            }
179            return Ok(0);
180        }
181        let reweigh = match slot {
182            Value::List(l) if l.len() >= SEG_PROMOTE => {
183                promote_flat_to_seg(slot, v, front);
184                true
185            }
186            Value::List(l) => {
187                let l = Arc::make_mut(l);
188                if front {
189                    l.push_front(v.to_vec())
190                } else {
191                    l.push_back(v.to_vec())
192                }
193                return Ok(list_item_weight(v.len()) as i64);
194            }
195            Value::SegList(l) => {
196                let l = Arc::make_mut(l);
197                if front {
198                    l.push_front(v.to_vec())
199                } else {
200                    l.push_back(v.to_vec())
201                }
202                return Ok(list_item_weight(v.len()) as i64);
203            }
204            _ => return Err(StoreError::WrongType),
205        };
206        if reweigh {
207            self.reweigh_entry(key);
208        }
209        Ok(0)
210    }
211
212    /// Create a fresh entry holding one element. Inline if it fits,
213    /// else heap.
214    fn list_push_create(&mut self, key: &[u8], v: &[u8]) -> i64 {
215        if let Some(inline) = SmallListData::with_one(v) {
216            self.insert_entry(
217                SmallBytes::from_slice(key),
218                Entry::new(Value::SmallListInline(inline), None),
219            );
220            0
221        } else {
222            let mut d = alloc::collections::VecDeque::with_capacity(1);
223            d.push_back(v.to_vec());
224            self.insert_entry(
225                SmallBytes::from_slice(key),
226                Entry::new(Value::List(Arc::new(d)), None),
227            );
228            0
229        }
230    }
231
232    /// `LPOP`/`RPOP` shared body — pop up to `count` from one end.
233    fn list_pop(
234        &mut self,
235        key: &[u8],
236        count: usize,
237        front: bool,
238    ) -> Result<Vec<Vec<u8>>, StoreError> {
239        // Inline → promote first if there is anything to pop; simpler
240        // than maintaining a second pop path on the packed buffer.
241        if matches!(self.map.get(key).map(|e| &e.value), Some(Value::SmallListInline(_))) {
242            self.promote_list_inline_to_heap(key);
243        }
244        let (out, delta) = {
245            let mut o = Vec::new();
246            let mut d: i64 = 0;
247            if self.is_seglist(key) {
248                let l = self.seglist_mut(key);
249                for _ in 0..count {
250                    let popped = if front { l.pop_front() } else { l.pop_back() };
251                    match popped {
252                        Some(v) => {
253                            d -= list_item_weight(v.len()) as i64;
254                            o.push(v);
255                        }
256                        None => break,
257                    }
258                }
259            } else if let Some(l) = self.list_mut(key, false)? {
260                for _ in 0..count {
261                    let popped = if front { l.pop_front() } else { l.pop_back() };
262                    match popped {
263                        Some(v) => {
264                            d -= list_item_weight(v.len()) as i64;
265                            o.push(v);
266                        }
267                        None => break,
268                    }
269                }
270            }
271            (o, d)
272        };
273        self.account_delta(key, delta);
274        self.drop_if_empty_list(key);
275        Ok(out)
276    }
277
278    /// `LPOP` — pop up to `count` from the head (deleting emptied key).
279    pub fn lpop(&mut self, key: &[u8], count: usize) -> Result<Vec<Vec<u8>>, StoreError> {
280        self.list_pop(key, count, true)
281    }
282
283    /// `RPOP` — pop up to `count` from the tail.
284    pub fn rpop(&mut self, key: &[u8], count: usize) -> Result<Vec<Vec<u8>>, StoreError> {
285        self.list_pop(key, count, false)
286    }
287
288    /// Force-promote an inline list at `key` to its heap variant
289    /// (no-op if already heap or absent). Used by mutating paths that
290    /// only support the heap representations (pop/lrem/lset/ltrim).
291    fn promote_list_inline_to_heap(&mut self, key: &[u8]) {
292        let needs = matches!(self.map.get(key).map(|e| &e.value), Some(Value::SmallListInline(_)));
293        if !needs {
294            return;
295        }
296        let Some(e) = self.map.get_mut(key) else { return };
297        if let Value::SmallListInline(s) = &e.value {
298            let promoted = small_list::promote(s);
299            e.value = Value::List(Arc::new(promoted));
300        }
301        self.reweigh_entry(key);
302    }
303
304    /// `LSET` — errors with `NoSuchKey` / `OutOfRange` like Redis.
305    pub fn lset(&mut self, key: &[u8], idx: i64, val: &[u8]) -> Result<(), StoreError> {
306        self.promote_list_inline_to_heap(key);
307        let delta = if self.is_seglist(key) {
308            let l = self.seglist_mut(key);
309            let i = norm_index(idx, l.len()).ok_or(StoreError::OutOfRange)?;
310            let old = l.set(i, val.to_vec());
311            val.len() as i64 - old.len() as i64
312        } else {
313            let l = self.list_mut(key, false)?.ok_or(StoreError::NoSuchKey)?;
314            let i = norm_index(idx, l.len()).ok_or(StoreError::OutOfRange)?;
315            let old_len = l[i].len() as i64;
316            l[i] = val.to_vec();
317            val.len() as i64 - old_len
318        };
319        self.account_delta(key, delta);
320        Ok(())
321    }
322
323    /// `LINSERT key BEFORE|AFTER pivot value` — insert `value`
324    /// before/after the first occurrence of `pivot` in the list at
325    /// `key`. Returns:
326    /// - new list length on success (`>= 1`);
327    /// - `0` when `key` does not exist;
328    /// - `-1` when `pivot` was not found in the list.
329    ///
330    /// Matches Redis semantics.
331    pub fn linsert(
332        &mut self,
333        key: &[u8],
334        before: bool,
335        pivot: &[u8],
336        val: &[u8],
337    ) -> Result<i64, StoreError> {
338        self.promote_list_inline_to_heap(key);
339        let (result, delta) = if self.is_seglist(key) {
340            let l = self.seglist_mut(key);
341            let Some(idx) = l.position(pivot) else {
342                return Ok(-1);
343            };
344            let insert_at = if before { idx } else { idx + 1 };
345            l.insert(insert_at, val.to_vec());
346            (l.len() as i64, list_item_weight(val.len()) as i64)
347        } else {
348            match self.list_mut(key, false)? {
349                None => return Ok(0),
350                Some(l) => {
351                    let Some(idx) = l.iter().position(|v| v.as_slice() == pivot) else {
352                        return Ok(-1);
353                    };
354                    let insert_at = if before { idx } else { idx + 1 };
355                    l.insert(insert_at, val.to_vec());
356                    (l.len() as i64, list_item_weight(val.len()) as i64)
357                }
358            }
359        };
360        self.account_delta(key, delta);
361        Ok(result)
362    }
363
364    /// `LREM` — remove `count` occurrences (>0 head, <0 tail, 0 all).
365    pub fn lrem(&mut self, key: &[u8], count: i64, val: &[u8]) -> Result<usize, StoreError> {
366        self.promote_list_inline_to_heap(key);
367        let (removed, delta) = if self.is_seglist(key) {
368            self.seglist_mut(key).remove_occurrences(val, count)
369        } else {
370            match self.list_mut(key, false)? {
371                None => (0, 0),
372                Some(l) => flat_lrem(l, count, val),
373            }
374        };
375        self.account_delta(key, delta);
376        self.drop_if_empty_list(key);
377        Ok(removed)
378    }
379
380    /// `LTRIM` — keep only `[start, stop]` (deleting emptied key).
381    pub fn ltrim(&mut self, key: &[u8], start: i64, stop: i64) -> Result<(), StoreError> {
382        self.promote_list_inline_to_heap(key);
383        let delta = if self.is_seglist(key) {
384            let l = self.seglist_mut(key);
385            match range_bounds(start, stop, l.len()) {
386                None => l.clear(),
387                Some((s, e)) => l.trim_to(s, e),
388            }
389        } else if let Some(l) = self.list_mut(key, false)? {
390            match range_bounds(start, stop, l.len()) {
391                None => {
392                    let d = -(l.iter().map(|v| list_item_weight(v.len()) as i64).sum::<i64>());
393                    l.clear();
394                    d
395                }
396                Some((s, e)) => {
397                    let mut d: i64 = 0;
398                    for v in l.iter().skip(e + 1) {
399                        d -= list_item_weight(v.len()) as i64;
400                    }
401                    l.drain(e + 1..);
402                    for v in l.iter().take(s) {
403                        d -= list_item_weight(v.len()) as i64;
404                    }
405                    l.drain(..s);
406                    d
407                }
408            }
409        } else {
410            0
411        };
412        self.account_delta(key, delta);
413        self.drop_if_empty_list(key);
414        Ok(())
415    }
416}
417
418/// Flat list at the promotion threshold: re-encode as segments, then
419/// push. One-time O(SEG_PROMOTE) move (or clone, if a snapshot view
420/// pins the flat Arc right now). Caller reweighs the entry.
421fn promote_flat_to_seg(slot: &mut Value, v: &[u8], front: bool) {
422    let flat = match core::mem::replace(slot, Value::Int(0)) {
423        Value::List(a) => Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone()),
424        _ => unreachable!("matched List"),
425    };
426    let mut seg = SegListData::from_flat(flat);
427    if front {
428        seg.push_front(v.to_vec())
429    } else {
430        seg.push_back(v.to_vec())
431    }
432    *slot = Value::SegList(Arc::new(seg));
433}
434
435/// The flat-list LREM walk (unchanged semantics; hoisted out of the
436/// method so the seg/flat dispatch stays within the fn-LOC cap).
437fn flat_lrem(l: &mut ListData, count: i64, val: &[u8]) -> (usize, i64) {
438    let mut r = 0usize;
439    let mut d: i64 = 0;
440    if count >= 0 {
441        let limit = if count == 0 { usize::MAX } else { count as usize };
442        let mut i = 0;
443        while i < l.len() {
444            if r < limit && l[i] == val {
445                d -= list_item_weight(l[i].len()) as i64;
446                l.remove(i);
447                r += 1;
448            } else {
449                i += 1;
450            }
451        }
452    } else {
453        let limit = (-count) as usize;
454        let mut i = l.len();
455        while i > 0 {
456            i -= 1;
457            if r < limit && l[i] == val {
458                d -= list_item_weight(l[i].len()) as i64;
459                l.remove(i);
460                r += 1;
461            }
462        }
463    }
464    (r, d)
465}