Skip to main content

kevy_store/
string_rmw.rs

1//! Read-modify-write string ops split out of `string.rs` (500-LOC
2//! rule): `APPEND` / `GETSET` / `GETDEL` / `INCRBYFLOAT`. All four are
3//! encoding-aware across `Value::Str` / `Value::Int` / `Value::ArcBulk`
4//! (fixed 2026-07-03 — they carried pre-L2 `Str`-only arms and replied
5//! WRONGTYPE where Redis succeeds; guard: `tests_string_encoding.rs`).
6
7use crate::string::pick_value_for_set_owned;
8use crate::util::{fmt_num, format_i64_into, itoa_i64_stack, parse_f64};
9use crate::value::{SmallBytes, Value};
10use crate::{Entry, Store, StoreError};
11
12impl Store {
13    pub fn append(&mut self, key: &[u8], data: &[u8]) -> Result<usize, StoreError> {
14        let outcome = match self.live_entry_mut(key) {
15            Some(e) => match &mut e.value {
16                Value::Str(v) => {
17                    // SmallBytes is immutable; pop out, grow via Vec, re-wrap.
18                    let mut owned = std::mem::take(v).into_vec();
19                    owned.extend_from_slice(data);
20                    let new_len = owned.len();
21                    *v = SmallBytes::from_vec(owned);
22                    AppendOutcome::Reweigh(new_len)
23                }
24                // L2: Int-encoded value — materialise the digits, append,
25                // then re-pick the encoding ("1" + "2" = canonical "12"
26                // goes straight back to Int so follow-up INCR stays fast).
27                Value::Int(n) => {
28                    let mut buf = itoa_i64_stack();
29                    let mut owned = format_i64_into(*n, &mut buf).to_vec();
30                    owned.extend_from_slice(data);
31                    let new_len = owned.len();
32                    e.value = pick_value_for_set_owned(owned);
33                    AppendOutcome::Reweigh(new_len)
34                }
35                // L1: APPEND on Arc-backed bulk → materialise to a fresh
36                // Vec (no other reader has refs to the old Arc post-replace),
37                // append, then pick the new encoding via SET routing rules.
38                Value::ArcBulk(a) => {
39                    let mut owned: Vec<u8> = a.as_ref().to_vec();
40                    owned.extend_from_slice(data);
41                    let new_len = owned.len();
42                    e.value = pick_value_for_set_owned(owned);
43                    AppendOutcome::Reweigh(new_len)
44                }
45                _ => return Err(StoreError::WrongType),
46            },
47            None => AppendOutcome::Insert,
48        };
49        match outcome {
50            AppendOutcome::Reweigh(new_len) => {
51                self.reweigh_entry(key);
52                Ok(new_len)
53            }
54            AppendOutcome::Insert => {
55                self.insert_entry(
56                    SmallBytes::from_slice(key),
57                    Entry::new(Value::Str(SmallBytes::from_slice(data)), None),
58                );
59                Ok(data.len())
60            }
61        }
62    }
63    /// `GETSET` — set to `val`, return the previous string (WRONGTYPE if the old
64    /// value isn't a string). Clears any TTL, like SET.
65    pub fn getset(&mut self, key: &[u8], val: Vec<u8>) -> Result<Option<Vec<u8>>, StoreError> {
66        let old = match self.live_entry(key) {
67            Some(e) => match &e.value {
68                Value::Str(v) => Some(v.to_vec()),
69                Value::Int(n) => {
70                    let mut buf = itoa_i64_stack();
71                    Some(format_i64_into(*n, &mut buf).to_vec())
72                }
73                Value::ArcBulk(a) => Some(a.as_ref().to_vec()),
74                _ => return Err(StoreError::WrongType),
75            },
76            None => None,
77        };
78        // Route through the SET encoding rules so a canonical-int new
79        // value takes the Int fast path, same as a plain SET would.
80        self.insert_entry(
81            SmallBytes::from_slice(key),
82            Entry::new(pick_value_for_set_owned(val), None),
83        );
84        Ok(old)
85    }
86
87    /// `GETDEL` — get then delete (WRONGTYPE if non-string).
88    pub fn getdel(&mut self, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError> {
89        match self.live_entry(key) {
90            None => return Ok(None),
91            Some(e) => match &e.value {
92                Value::Str(_) | Value::Int(_) | Value::ArcBulk(_) => {}
93                _ => return Err(StoreError::WrongType),
94            },
95        }
96        match self.remove_entry(key) {
97            Some(Entry {
98                value: Value::Str(v),
99                ..
100            }) => Ok(Some(v.into_vec())),
101            Some(Entry {
102                value: Value::Int(n),
103                ..
104            }) => {
105                let mut buf = itoa_i64_stack();
106                Ok(Some(format_i64_into(n, &mut buf).to_vec()))
107            }
108            Some(Entry {
109                value: Value::ArcBulk(a),
110                ..
111            }) => Ok(Some(a.as_ref().to_vec())),
112            _ => Ok(None),
113        }
114    }
115
116    /// `INCRBYFLOAT` — returns the new value formatted as Redis would. Preserves TTL.
117    pub fn incr_by_float(&mut self, key: &[u8], delta: f64) -> Result<Vec<u8>, StoreError> {
118        let outcome = if let Some(e) = self.live_entry_mut(key) { match &mut e.value {
119            Value::Str(v) => {
120                let next = parse_f64(v.as_slice()).ok_or(StoreError::NotFloat)? + delta;
121                if !next.is_finite() {
122                    return Err(StoreError::NotFloat);
123                }
124                let bytes = fmt_num(next);
125                *v = SmallBytes::from_slice(&bytes);
126                FloatOutcome::Reweigh(bytes)
127            }
128            Value::Int(n) => {
129                let next = *n as f64 + delta;
130                if !next.is_finite() {
131                    return Err(StoreError::NotFloat);
132                }
133                let bytes = fmt_num(next);
134                e.value = Value::Str(SmallBytes::from_slice(&bytes));
135                FloatOutcome::Reweigh(bytes)
136            }
137            Value::ArcBulk(a) => {
138                let next = parse_f64(a.as_ref()).ok_or(StoreError::NotFloat)? + delta;
139                if !next.is_finite() {
140                    return Err(StoreError::NotFloat);
141                }
142                let bytes = fmt_num(next);
143                e.value = Value::Str(SmallBytes::from_slice(&bytes));
144                FloatOutcome::Reweigh(bytes)
145            }
146            _ => return Err(StoreError::WrongType),
147        } } else {
148            // Absent/expired ⇒ start from 0.0.
149            if !delta.is_finite() {
150                return Err(StoreError::NotFloat);
151            }
152            FloatOutcome::Insert(fmt_num(delta))
153        };
154        match outcome {
155            FloatOutcome::Reweigh(bytes) => {
156                self.reweigh_entry(key);
157                Ok(bytes)
158            }
159            FloatOutcome::Insert(bytes) => {
160                self.insert_entry(
161                    SmallBytes::from_slice(key),
162                    Entry::new(Value::Str(SmallBytes::from_slice(&bytes)), None),
163                );
164                Ok(bytes)
165            }
166        }
167    }
168}
169
170enum AppendOutcome {
171    Reweigh(usize),
172    Insert,
173}
174
175enum FloatOutcome {
176    Reweigh(Vec<u8>),
177    Insert(Vec<u8>),
178}