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