kevy_embedded/ops_bonus.rs
1//! String `SET` variants (`SETNX`, SET with a TTL, `APPEND`, `STRLEN`), hash
2//! conditional set (`HSETNX`), decrement helpers (`DECR`, `DECRBY`,
3//! `INCRBYFLOAT`), and the seconds-precision TTL accessor
4//! (`ttl_secs`).
5
6use std::time::Duration;
7
8use crate::{KevyError, KevyResult};
9
10use crate::store::ensure_writable;
11use crate::store::{Store, commit_write, store_err};
12use crate::store_glue::commit_deadline;
13
14impl Store {
15 // ---- string SET variants ----------------------------------------
16
17 /// `SETNX key value` — set only if the key does not exist.
18 /// Returns `true` when the SET succeeded; `false` when it was
19 /// vetoed by an existing value.
20 pub fn setnx(&self, key: &[u8], value: &[u8]) -> KevyResult<bool> {
21 ensure_writable(self)?;
22 let mut g = self.wshard(key);
23 let ok = g.store.set(key, value.to_vec(), None, kevy_store::SetCondition::IfAbsent);
24 if ok {
25 commit_write(&mut g, &[b"SET", key, value, b"NX"])?;
26 }
27 Ok(ok)
28 }
29
30 /// `SET key value PX ms` — overwrites + sets TTL. The AOF records an
31 /// **absolute** `PEXPIREAT` deadline (not the relative `ttl`) so the key
32 /// expires at the same wall-clock instant after a restart — a relative
33 /// `PEXPIRE` would be re-anchored to replay-time, resetting the TTL to a
34 /// fresh full duration on every restart (seen as a production
35 /// incident: cache keys never expired across restarts).
36 /// The value and its TTL change under one lock, so no other thread sees
37 /// the value without its TTL. The AOF gets the server's shape: `SET key
38 /// value PX ms` carries the TTL in the value's own frame, so a crash
39 /// before the second frame still leaves an expiring key, and
40 /// `PEXPIREAT` then pins the absolute deadline.
41 pub fn set_with_ttl(&self, key: &[u8], value: &[u8], ttl: Duration) -> KevyResult<bool> {
42 ensure_writable(self)?;
43 let mut g = self.wshard(key);
44 let ok = g.store.set(key, value.to_vec(), Some(ttl), kevy_store::SetCondition::Always);
45 let ms = ttl.as_millis().min(u128::from(u64::MAX)).to_string();
46 commit_write(&mut g, &[b"SET", key, value, b"PX", ms.as_bytes()])?;
47 commit_deadline(&mut g, key)?;
48 Ok(ok)
49 }
50
51 /// `INCRBYFLOAT key delta` — atomic float increment of a string
52 /// value. Returns the post-increment value parsed as f64.
53 pub fn incrbyfloat(&self, key: &[u8], delta: f64) -> KevyResult<f64> {
54 ensure_writable(self)?;
55 let mut g = self.wshard(key);
56 let new_bytes = g.store.incr_by_float(key, delta).map_err(store_err)?;
57 let delta_str = format!("{delta}");
58 commit_write(&mut g, &[b"INCRBYFLOAT", key, delta_str.as_bytes()])?;
59 std::str::from_utf8(&new_bytes)
60 .ok()
61 .and_then(|s| s.parse::<f64>().ok())
62 .ok_or_else(|| KevyError::Protocol("incrbyfloat result not parseable".into()))
63 }
64
65 /// `DECR key` — atomic decrement by 1.
66 pub fn decr(&self, key: &[u8]) -> KevyResult<i64> {
67 self.incr_by(key, -1)
68 }
69
70 /// `DECRBY key delta` — atomic decrement by `delta`.
71 pub fn decrby(&self, key: &[u8], delta: i64) -> KevyResult<i64> {
72 self.incr_by(key, delta.checked_neg().unwrap_or(i64::MIN.saturating_add(1)))
73 }
74
75 /// `STRLEN key` — length of the string value at `key`; 0 if
76 /// absent. Errors on wrong type.
77 pub fn strlen(&self, key: &[u8]) -> KevyResult<usize> {
78 self.wshard(key).store.strlen(key).map_err(store_err)
79 }
80
81 /// `APPEND key data` — append `data` to the string at `key`.
82 /// Creates the key if absent. Returns the new total length.
83 pub fn append(&self, key: &[u8], data: &[u8]) -> KevyResult<usize> {
84 ensure_writable(self)?;
85 let mut g = self.wshard(key);
86 let new_len = g.store.append(key, data).map_err(store_err)?;
87 commit_write(&mut g, &[b"APPEND", key, data])?;
88 Ok(new_len)
89 }
90
91 // ---- hash conditional set ---------------------------------------
92
93 /// `HSETNX key field value` — set the hash field only if it
94 /// does not already exist. Returns `true` when set; `false`
95 /// when the field existed.
96 pub fn hsetnx(&self, key: &[u8], field: &[u8], value: &[u8]) -> KevyResult<bool> {
97 ensure_writable(self)?;
98 let mut g = self.wshard(key);
99 let ok = g.store.hsetnx(key, field, value).map_err(store_err)?;
100 if ok {
101 commit_write(&mut g, &[b"HSETNX", key, field, value])?;
102 }
103 Ok(ok)
104 }
105
106 // ---- TTL units --------------------------------------------------
107
108 /// `TTL key` — TTL in **seconds** (truncated from ms). `-1`
109 /// when the key has no TTL; `-2` when absent. Matches Redis
110 /// wire semantics for the integer reply.
111 pub fn ttl_secs(&self, key: &[u8]) -> i64 {
112 let ms = self.ttl_ms(key);
113 if ms <= 0 { ms } else { ms / 1000 }
114 }
115}