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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
//! Cross-key operations: `copy`, `randomkey`, `unlink`, `touch`.
//!
//! These compose existing `kevy_store::Store` primitives at the
//! embedded layer:
//!
//! - `copy` clones the source's value and TTL and places them at dst.
//! - `randomkey` collects matching keys and picks one by index.
//! - `unlink` is an alias for `del`; kevy has no async deletion, so
//! sync delete is the unblocking semantic.
//! - `touch` counts existing keys and reads bump LRU/LFU bookkeeping
//! as a side effect.
use crate::KevyResult;
use crate::CopyMode;
use crate::store::ensure_writable;
use crate::store::{Inner, Store, commit_write};
impl Store {
/// `COPY src dst [REPLACE]` — copy `src`'s value, of any type, and
/// its remaining TTL to `dst`. Returns `true` when the copy happened.
///
/// Semantics:
/// - `false` if `src` doesn't exist.
/// - `false` if `dst` exists and `mode` is [`CopyMode::IfAbsent`].
/// - A source with a TTL gives the destination the same deadline.
///
/// ```
/// use kevy_embedded::{Config, CopyMode, Store};
///
/// let s = Store::open(Config::default())?;
/// s.set(b"a", b"1")?;
/// assert!(s.copy(b"a", b"b", CopyMode::IfAbsent)?);
/// assert_eq!(s.get(b"b")?.as_deref(), Some(&b"1"[..]));
/// s.hset(b"h", &[(b"f", b"v")])?;
/// assert!(s.copy(b"h", b"b", CopyMode::Replace)?);
/// assert_eq!(s.hget(b"b", b"f")?.as_deref(), Some(&b"v"[..]));
/// # Ok::<(), kevy_embedded::KevyError>(())
/// ```
pub fn copy(&self, src: &[u8], dst: &[u8], mode: CopyMode) -> KevyResult<bool> {
ensure_writable(self)?;
// the source's lock is released before the destination's is
// taken: the two keys may live on one shard
let cloned = self.wshard(src).store.clone_with_ttl(src);
let Some((value, ttl_ms)) = cloned else {
return Ok(false);
};
let mut g = self.wshard(dst);
let replaced = g.store.key_exists(dst);
if replaced && !matches!(mode, CopyMode::Replace) {
return Ok(false);
}
let frames = placed_frames(&g, dst, &value, ttl_ms);
g.store.put_with_ttl(dst.to_vec(), value, ttl_ms);
commit_copy(&mut g, [b"COPY", src, dst], frames, replaced)?;
Ok(true)
}
/// `RANDOMKEY` — return a randomly-chosen existing key, or
/// `None` when the keyspace is empty.
///
/// A shard is drawn in proportion to how many keys it holds, and that
/// shard picks from a random point in its table, so the cost does not
/// grow with the keyspace.
///
/// ```
/// # use kevy_embedded::{Config, Store};
/// let s = Store::open(Config::default())?;
/// assert_eq!(s.randomkey(), None);
/// s.set(b"only", b"1")?;
/// assert_eq!(s.randomkey(), Some(b"only".to_vec()));
/// # Ok::<(), kevy_embedded::KevyError>(())
/// ```
pub fn randomkey(&self) -> Option<Vec<u8>> {
let sizes: Vec<usize> =
self.shards.iter().map(|sh| crate::store_glue::lock_read(sh).store.dbsize()).collect();
let total: usize = sizes.iter().sum();
if total == 0 {
return None;
}
let mut draw = {
let mut g = crate::store_glue::lock_write(&self.shards[0]);
g.store.rand_draw() as usize % total
};
for (i, n) in sizes.iter().enumerate() {
if draw < *n {
if let Some(k) = crate::store_glue::lock_write(&self.shards[i]).store.random_key() {
return Some(k);
}
break;
}
draw -= n;
}
// the shard drawn emptied since it was counted: any key will do
self.shards.iter().find_map(|sh| crate::store_glue::lock_write(sh).store.random_key())
}
/// `UNLINK key [key ...]` — alias for [`Self::del`]. In Redis
/// this is the async (non-blocking) variant; kevy is in-process
/// so the sync `del` IS the unblocking semantic. Returns count
/// actually removed.
pub fn unlink(&self, keys: &[&[u8]]) -> KevyResult<usize> {
self.del(keys)
}
/// `TOUCH key [key ...]` — count keys that exist. Side effect:
/// the existence check refreshes LRU/LFU bookkeeping on the
/// touched shards, matching Redis semantics.
pub fn touch(&self, keys: &[&[u8]]) -> KevyResult<usize> {
self.exists(keys)
}
}
impl crate::Store {
/// Order-insensitive prefix checksum for migration
/// verification: `(row_count, xor_of_row_digests)`. Matches the
/// server's `PREFIX.DIGEST` bit for bit (same canonicalization).
pub fn prefix_digest(&self, prefix: &[u8]) -> (u64, u64) {
let mut pat = prefix.to_vec();
pat.push(b'*');
let keys = self.keys(Some(&pat), None);
let mut xor = 0u64;
for key in &keys {
xor ^= self.row_digest_embedded(key);
}
(keys.len() as u64, xor)
}
/// One row's digest under a SINGLE shard write-lock acquisition,
/// with the row reads inside the store's bulk-read peek scope
/// — a cold row hashes from ONE record read, never promotes
/// and never advances the 2nd-touch gate — a full-prefix digest
/// must not thrash the hot tier (server twin: `cmd_digest`).
fn row_digest_embedded(&self, key: &[u8]) -> u64 {
let mut g = self.wshard(key);
g.store.peek_scope(|s| {
let mut h = FNV_OFFSET;
fnv(&mut h, key);
let ty = s.type_of(key);
fnv(&mut h, ty.as_bytes());
digest_row_body(s, key, ty, &mut h);
h
})
}
}
/// Fold one row's canonicalized value into the FNV state, per type
/// (hash fields and set members sort first; zset folds score bits
/// then member, rank order). Reads the store directly — the caller
/// already holds the shard lock and the peek scope.
fn digest_row_body(s: &mut kevy_store::Store, key: &[u8], ty: &str, h: &mut u64) {
match ty {
"string" => {
if let Ok(Some(v)) = s.get(key) {
let v = v.to_vec();
fnv(h, &v);
}
}
"hash" => {
if let Ok(flat) = s.hgetall(key) {
let mut pairs: Vec<(&[u8], &[u8])> =
flat.chunks(2).map(|c| (c[0].as_slice(), c[1].as_slice())).collect();
pairs.sort();
for (f, v) in pairs {
fnv(h, f);
fnv(h, v);
}
}
}
"list" => {
if let Ok(items) = s.lrange(key, 0, -1) {
for i in items {
fnv(h, &i);
}
}
}
"set" => {
if let Ok(mut ms) = s.smembers(key) {
ms.sort();
for m in ms {
fnv(h, &m);
}
}
}
"zset" => {
if let Ok(items) = s.zrange(key, 0, -1) {
for (member, score) in items {
fnv(h, &score.to_bits().to_le_bytes());
fnv(h, &member);
}
}
}
_ => {}
}
}
const FNV_OFFSET: u64 = 0xCBF2_9CE4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01B3;
fn fnv(h: &mut u64, bytes: &[u8]) {
for &b in bytes {
*h ^= u64::from(b);
*h = h.wrapping_mul(FNV_PRIME);
}
}
/// The commands that rebuild a copied value at `dst`, when this shard
/// records its writes anywhere
#[cfg(feature = "persist")]
fn placed_frames(
g: &Inner,
dst: &[u8],
value: &kevy_store::Value,
ttl_ms: Option<u64>,
) -> Option<Vec<u8>> {
crate::store_glue::records_writes(g)
.then(|| kevy_persist::value_as_v1_frames(dst, value, ttl_ms))
}
#[cfg(not(feature = "persist"))]
fn placed_frames(_: &Inner, _: &[u8], _: &kevy_store::Value, _: Option<u64>) -> Option<Vec<u8>> {
None
}
/// Record a copy as the commands that rebuild its value, after a DEL
/// when it replaced a key, so a replay does not merge into what was
/// there; with nothing to record into, the argv runs the commit's other
/// steps
fn commit_copy(
g: &mut Inner,
argv: [&[u8]; 3],
frames: Option<Vec<u8>>,
replaced: bool,
) -> KevyResult<()> {
let Some(frames) = frames else {
return commit_write(g, &argv);
};
if replaced {
commit_write(g, &[b"DEL", argv[2]])?;
}
let (mut pos, mut cmd) = (0, kevy_resp::Argv::default());
while pos < frames.len() {
cmd.clear();
pos += kevy_resp::parse_command_into(&frames[pos..], &mut cmd)
.ok()
.flatten()
.expect("the value serializer writes whole commands");
let parts: Vec<&[u8]> = cmd.iter().collect();
commit_write(g, &parts)?;
}
Ok(())
}