Skip to main content

vibe_ready/store/db/tables/
key_val.rs

1use serde::{Deserialize, Serialize};
2
3#[cfg(feature = "store-diesel-sqlite")]
4pub static TABLE_NAME_KEY_VAL: &str = "vibe_ready_key_val";
5#[cfg(feature = "store-diesel-sqlite")]
6pub static TABLE_NAME_KV_META: &str = "vibe_ready_kv_meta";
7
8pub const DEFAULT_BUCKET: &str = "default";
9
10pub(crate) const VALUE_TYPE_STR: i16 = 1;
11pub(crate) const VALUE_TYPE_BOOL: i16 = 2;
12pub(crate) const VALUE_TYPE_I32: i16 = 3;
13pub(crate) const VALUE_TYPE_I64: i16 = 4;
14pub(crate) const VALUE_TYPE_F64: i16 = 5;
15pub(crate) const VALUE_TYPE_BYTES: i16 = 6;
16pub(crate) const VALUE_TYPE_JSON: i16 = 7;
17
18/// Sentinel meaning "no expiry".
19pub(crate) const EXPIRES_AT_NEVER: i64 = 0;
20
21#[cfg(feature = "store-diesel-sqlite")]
22diesel::table! {
23    vibe_ready_key_val (user_id, bucket, key) {
24        user_id -> Text,
25        bucket -> Text,
26        key -> Text,
27        value_type -> SmallInt,
28        value_str -> Text,
29        value_bool -> Bool,
30        value_i32 -> Integer,
31        value_i64 -> BigInt,
32        value_f64 -> Double,
33        value_bytes -> Binary,
34        value_json -> Text,
35        expires_at_ms -> BigInt,
36    }
37}
38
39/// Value stored in the high-level key-value store.
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub enum VibeKvValue {
42    /// UTF-8 string value.
43    String(String),
44    /// Boolean value.
45    Bool(bool),
46    /// 32-bit signed integer value.
47    I32(i32),
48    /// 64-bit signed integer value.
49    I64(i64),
50    /// 64-bit floating-point value.
51    F64(f64),
52    /// Binary value.
53    Bytes(Vec<u8>),
54    /// JSON value.
55    Json(serde_json::Value),
56}
57
58impl VibeKvValue {
59    /// Returns the inner string when this value is [`VibeKvValue::String`].
60    ///
61    /// # Returns
62    ///
63    /// `Some(&str)` for string values, otherwise `None`.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use vibe_ready::VibeKvValue;
69    ///
70    /// assert_eq!(VibeKvValue::from("ready").as_str(), Some("ready"));
71    /// ```
72    pub fn as_str(&self) -> Option<&str> {
73        match self {
74            Self::String(value) => Some(value.as_str()),
75            _ => None,
76        }
77    }
78
79    /// Returns the inner boolean when this value is [`VibeKvValue::Bool`].
80    ///
81    /// # Returns
82    ///
83    /// `Some(bool)` for boolean values, otherwise `None`.
84    pub fn as_bool(&self) -> Option<bool> {
85        match self {
86            Self::Bool(value) => Some(*value),
87            _ => None,
88        }
89    }
90
91    /// Returns the inner integer when this value is [`VibeKvValue::I32`].
92    ///
93    /// # Returns
94    ///
95    /// `Some(i32)` for `i32` values, otherwise `None`.
96    pub fn as_i32(&self) -> Option<i32> {
97        match self {
98            Self::I32(value) => Some(*value),
99            _ => None,
100        }
101    }
102
103    /// Returns the inner integer when this value is [`VibeKvValue::I64`].
104    ///
105    /// # Returns
106    ///
107    /// `Some(i64)` for `i64` values, otherwise `None`.
108    pub fn as_i64(&self) -> Option<i64> {
109        match self {
110            Self::I64(value) => Some(*value),
111            _ => None,
112        }
113    }
114
115    /// Returns the inner float when this value is [`VibeKvValue::F64`].
116    ///
117    /// # Returns
118    ///
119    /// `Some(f64)` for `f64` values, otherwise `None`.
120    pub fn as_f64(&self) -> Option<f64> {
121        match self {
122            Self::F64(value) => Some(*value),
123            _ => None,
124        }
125    }
126
127    /// Returns the inner bytes when this value is [`VibeKvValue::Bytes`].
128    ///
129    /// # Returns
130    ///
131    /// `Some(&[u8])` for byte values, otherwise `None`.
132    pub fn as_bytes(&self) -> Option<&[u8]> {
133        match self {
134            Self::Bytes(value) => Some(value.as_slice()),
135            _ => None,
136        }
137    }
138
139    /// Returns the inner JSON value when this value is [`VibeKvValue::Json`].
140    ///
141    /// # Returns
142    ///
143    /// `Some(&serde_json::Value)` for JSON values, otherwise `None`.
144    pub fn as_json(&self) -> Option<&serde_json::Value> {
145        match self {
146            Self::Json(value) => Some(value),
147            _ => None,
148        }
149    }
150}
151
152impl From<String> for VibeKvValue {
153    fn from(value: String) -> Self {
154        Self::String(value)
155    }
156}
157
158impl From<&str> for VibeKvValue {
159    fn from(value: &str) -> Self {
160        Self::String(value.to_string())
161    }
162}
163
164impl From<bool> for VibeKvValue {
165    fn from(value: bool) -> Self {
166        Self::Bool(value)
167    }
168}
169
170impl From<i32> for VibeKvValue {
171    fn from(value: i32) -> Self {
172        Self::I32(value)
173    }
174}
175
176impl From<i64> for VibeKvValue {
177    fn from(value: i64) -> Self {
178        Self::I64(value)
179    }
180}
181
182impl From<f64> for VibeKvValue {
183    fn from(value: f64) -> Self {
184        Self::F64(value)
185    }
186}
187
188impl From<Vec<u8>> for VibeKvValue {
189    fn from(value: Vec<u8>) -> Self {
190        Self::Bytes(value)
191    }
192}
193
194impl From<&[u8]> for VibeKvValue {
195    fn from(value: &[u8]) -> Self {
196        Self::Bytes(value.to_vec())
197    }
198}
199
200impl From<serde_json::Value> for VibeKvValue {
201    fn from(value: serde_json::Value) -> Self {
202        Self::Json(value)
203    }
204}
205
206#[cfg_attr(
207    feature = "store-diesel-sqlite",
208    derive(diesel::Queryable, diesel::Selectable, diesel::Insertable)
209)]
210#[cfg_attr(feature = "store-diesel-sqlite", diesel(table_name = vibe_ready_key_val))]
211#[cfg_attr(
212    feature = "store-diesel-sqlite",
213    diesel(primary_key(user_id, bucket, key))
214)]
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216/// Database row representation for a key-value item.
217///
218/// Applications usually work with [`VibeKvStore`](crate::VibeKvStore); this
219/// row type is exposed for advanced database integrations.
220pub struct VibeTableKeyVal {
221    pub(crate) user_id: String,
222    pub(crate) bucket: String,
223    pub(crate) key: String,
224    pub(crate) value_type: i16,
225    pub(crate) value_str: String,
226    pub(crate) value_bool: bool,
227    pub(crate) value_i32: i32,
228    pub(crate) value_i64: i64,
229    pub(crate) value_f64: f64,
230    pub(crate) value_bytes: Vec<u8>,
231    pub(crate) value_json: String,
232    pub(crate) expires_at_ms: i64,
233}
234
235impl VibeTableKeyVal {
236    /// Build a row using the default bucket and no TTL.
237    ///
238    /// # Returns
239    ///
240    /// A [`VibeTableKeyVal`] row targeting the default bucket.
241    pub fn new(user_id: &str, key: &str, value: VibeKvValue) -> Self {
242        Self::new_in_bucket(user_id, DEFAULT_BUCKET, key, value, EXPIRES_AT_NEVER)
243    }
244
245    /// Build a row with explicit bucket and optional `expires_at_ms`
246    /// (`0` means no expiry).
247    ///
248    /// # Returns
249    ///
250    /// A [`VibeTableKeyVal`] row targeting the specified bucket.
251    pub fn new_in_bucket(
252        user_id: &str,
253        bucket: &str,
254        key: &str,
255        value: VibeKvValue,
256        expires_at_ms: i64,
257    ) -> Self {
258        let mut row = Self::empty(user_id, bucket, key);
259        row.expires_at_ms = expires_at_ms;
260        match value {
261            VibeKvValue::String(v) => {
262                row.value_type = VALUE_TYPE_STR;
263                row.value_str = v;
264            }
265            VibeKvValue::Bool(v) => {
266                row.value_type = VALUE_TYPE_BOOL;
267                row.value_bool = v;
268            }
269            VibeKvValue::I32(v) => {
270                row.value_type = VALUE_TYPE_I32;
271                row.value_i32 = v;
272            }
273            VibeKvValue::I64(v) => {
274                row.value_type = VALUE_TYPE_I64;
275                row.value_i64 = v;
276            }
277            VibeKvValue::F64(v) => {
278                row.value_type = VALUE_TYPE_F64;
279                row.value_f64 = v;
280            }
281            VibeKvValue::Bytes(v) => {
282                row.value_type = VALUE_TYPE_BYTES;
283                row.value_bytes = v;
284            }
285            VibeKvValue::Json(v) => {
286                row.value_type = VALUE_TYPE_JSON;
287                row.value_json = v.to_string();
288            }
289        }
290        row
291    }
292
293    fn empty(user_id: &str, bucket: &str, key: &str) -> Self {
294        Self {
295            user_id: user_id.to_string(),
296            bucket: bucket.to_string(),
297            key: key.to_string(),
298            value_type: 0,
299            value_str: String::new(),
300            value_bool: false,
301            value_i32: 0,
302            value_i64: 0,
303            value_f64: 0.0,
304            value_bytes: Vec::new(),
305            value_json: String::new(),
306            expires_at_ms: EXPIRES_AT_NEVER,
307        }
308    }
309
310    /// Builds a default-bucket string row.
311    ///
312    /// # Returns
313    ///
314    /// A [`VibeTableKeyVal`] containing a string value.
315    pub fn new_with_str(user_id: &str, key: &str, val: &str) -> Self {
316        Self::new(user_id, key, VibeKvValue::String(val.to_string()))
317    }
318
319    /// Builds a default-bucket boolean row.
320    ///
321    /// # Returns
322    ///
323    /// A [`VibeTableKeyVal`] containing a boolean value.
324    pub fn new_with_bool(user_id: &str, key: &str, val: bool) -> Self {
325        Self::new(user_id, key, VibeKvValue::Bool(val))
326    }
327
328    /// Builds a default-bucket 32-bit integer row.
329    ///
330    /// # Returns
331    ///
332    /// A [`VibeTableKeyVal`] containing an `i32` value.
333    pub fn new_with_i32(user_id: &str, key: &str, val: i32) -> Self {
334        Self::new(user_id, key, VibeKvValue::I32(val))
335    }
336
337    /// Returns the row key.
338    ///
339    /// # Returns
340    ///
341    /// A borrowed key string.
342    pub fn key(&self) -> &str {
343        self.key.as_str()
344    }
345
346    /// Returns the row user id.
347    ///
348    /// # Returns
349    ///
350    /// A borrowed user id string.
351    pub fn user_id(&self) -> &str {
352        self.user_id.as_str()
353    }
354
355    /// Returns the row bucket name.
356    ///
357    /// # Returns
358    ///
359    /// A borrowed bucket string.
360    pub fn bucket(&self) -> &str {
361        self.bucket.as_str()
362    }
363
364    /// Returns the expiry timestamp.
365    ///
366    /// # Returns
367    ///
368    /// Unix milliseconds, or `0` when the row never expires.
369    pub fn expires_at_ms(&self) -> i64 {
370        self.expires_at_ms
371    }
372
373    /// Checks whether the row is expired at `now_ms`.
374    ///
375    /// # Returns
376    ///
377    /// `true` when the row has an expiry timestamp and `now_ms` is at or past it.
378    pub fn is_expired(&self, now_ms: i64) -> bool {
379        self.expires_at_ms != EXPIRES_AT_NEVER && now_ms >= self.expires_at_ms
380    }
381
382    /// Converts the row payload back into a high-level value.
383    ///
384    /// # Returns
385    ///
386    /// `Some(VibeKvValue)` for known value types, otherwise `None`.
387    pub fn value(&self) -> Option<VibeKvValue> {
388        match self.value_type {
389            VALUE_TYPE_STR => Some(VibeKvValue::String(self.value_str.clone())),
390            VALUE_TYPE_BOOL => Some(VibeKvValue::Bool(self.value_bool)),
391            VALUE_TYPE_I32 => Some(VibeKvValue::I32(self.value_i32)),
392            VALUE_TYPE_I64 => Some(VibeKvValue::I64(self.value_i64)),
393            VALUE_TYPE_F64 => Some(VibeKvValue::F64(self.value_f64)),
394            VALUE_TYPE_BYTES => Some(VibeKvValue::Bytes(self.value_bytes.clone())),
395            VALUE_TYPE_JSON => serde_json::from_str(&self.value_json)
396                .ok()
397                .map(VibeKvValue::Json),
398            _ => None,
399        }
400    }
401
402    /// Returns the row payload as a string if its type is string.
403    ///
404    /// # Returns
405    ///
406    /// `Some(&str)` for string rows, otherwise `None`.
407    pub fn get_value_str(&self) -> Option<&str> {
408        if self.value_type == VALUE_TYPE_STR {
409            Some(self.value_str.as_str())
410        } else {
411            None
412        }
413    }
414
415    /// Returns the row payload as a boolean if its type is boolean.
416    ///
417    /// # Returns
418    ///
419    /// `Some(bool)` for boolean rows, otherwise `None`.
420    pub fn get_value_bool(&self) -> Option<bool> {
421        if self.value_type == VALUE_TYPE_BOOL {
422            Some(self.value_bool)
423        } else {
424            None
425        }
426    }
427
428    /// Returns the row payload as an `i32` if its type is `i32`.
429    ///
430    /// # Returns
431    ///
432    /// `Some(i32)` for `i32` rows, otherwise `None`.
433    pub fn get_value_i32(&self) -> Option<i32> {
434        if self.value_type == VALUE_TYPE_I32 {
435            Some(self.value_i32)
436        } else {
437            None
438        }
439    }
440}
441
442#[cfg(test)]
443mod strict_tests {
444    use super::*;
445    include!(concat!(
446        env!("CARGO_MANIFEST_DIR"),
447        "/test/unit/store/key_val_tests.rs"
448    ));
449}