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