Skip to main content

nodedb_types/
kv.rs

1//! KV engine configuration types.
2//!
3//! Key-Value collections use a hash-indexed primary key for O(1) point lookups.
4//! Value fields are encoded as Binary Tuples (same codec as strict mode).
5
6use serde::{Deserialize, Serialize};
7
8use crate::columnar::ColumnType;
9
10/// Default inline value threshold in bytes. Values at or below this size are
11/// stored directly in the hash entry (no pointer chase).
12pub const KV_DEFAULT_INLINE_THRESHOLD: u16 = 64;
13
14/// Configuration for a Key-Value collection.
15///
16/// KV collections use a hash-indexed primary key for O(1) point lookups.
17/// Value fields are encoded as Binary Tuples (same codec as strict mode)
18/// providing O(1) field extraction by byte offset.
19#[derive(
20    Debug,
21    Clone,
22    PartialEq,
23    Eq,
24    Serialize,
25    Deserialize,
26    zerompk::ToMessagePack,
27    zerompk::FromMessagePack,
28)]
29pub struct KvConfig {
30    /// Typed schema for this KV collection (key + value columns).
31    ///
32    /// The PRIMARY KEY column is the hash key. Remaining columns are value
33    /// fields encoded as Binary Tuples with O(1) field extraction.
34    /// The schema reuses `StrictSchema` from the strict document engine.
35    pub schema: crate::columnar::StrictSchema,
36
37    /// TTL policy for automatic key expiration. `None` = keys never expire.
38    pub ttl: Option<KvTtlPolicy>,
39
40    /// Initial capacity hint for the hash table. Avoids early rehash churn
41    /// for known-size workloads. `0` = use engine default (from `KvTuning`).
42    #[serde(default)]
43    pub capacity_hint: u32,
44
45    /// Inline value threshold in bytes. Values at or below this size are stored
46    /// directly in the hash entry (no pointer chase). Larger values overflow to
47    /// slab-allocated Binary Tuples referenced by slab ID.
48    #[serde(default = "default_inline_threshold")]
49    pub inline_threshold: u16,
50}
51
52fn default_inline_threshold() -> u16 {
53    KV_DEFAULT_INLINE_THRESHOLD
54}
55
56impl KvConfig {
57    /// Get the primary key column from the schema.
58    ///
59    /// KV collections always have exactly one PRIMARY KEY column.
60    pub fn primary_key_column(&self) -> Option<&crate::columnar::ColumnDef> {
61        self.schema.columns.iter().find(|c| c.primary_key)
62    }
63
64    /// Whether this KV collection has TTL enabled.
65    pub fn has_ttl(&self) -> bool {
66        self.ttl.is_some()
67    }
68}
69
70/// TTL policy for KV collection key expiration.
71///
72/// Two modes:
73/// - `FixedDuration`: All keys share the same lifetime from insertion time.
74/// - `FieldBased`: Each key expires when a referenced timestamp field plus an
75///   offset exceeds the current time, allowing per-key variable expiration.
76#[derive(
77    Debug,
78    Clone,
79    PartialEq,
80    Eq,
81    Serialize,
82    Deserialize,
83    zerompk::ToMessagePack,
84    zerompk::FromMessagePack,
85)]
86#[serde(tag = "kind")]
87pub enum KvTtlPolicy {
88    /// Fixed duration from insertion time. All keys share the same lifetime.
89    ///
90    /// Example DDL: `WITH storage = 'kv', ttl = INTERVAL '15 minutes'`
91    FixedDuration {
92        /// Duration in milliseconds.
93        duration_ms: u64,
94    },
95    /// Field-based expiration. Each key expires when the referenced timestamp
96    /// field plus the offset exceeds the current time.
97    ///
98    /// Example DDL: `WITH storage = 'kv', ttl = last_active + INTERVAL '1 hour'`
99    FieldBased {
100        /// Name of the timestamp field in the value schema.
101        field: String,
102        /// Offset in milliseconds added to the field value.
103        offset_ms: u64,
104    },
105}
106
107/// Column types valid as KV primary key (hash key).
108///
109/// Only types with well-defined equality and hash behavior are allowed.
110/// Floating-point types are excluded (equality is unreliable for hashing).
111const KV_VALID_KEY_TYPES: &[ColumnType] = &[
112    ColumnType::String,    // TEXT, VARCHAR
113    ColumnType::Uuid,      // UUID
114    ColumnType::Int64,     // INT, BIGINT, INTEGER
115    ColumnType::Bytes,     // BYTES, BYTEA, BLOB
116    ColumnType::Timestamp, // TIMESTAMP (epoch-based, deterministic equality)
117];
118
119/// Check whether a column type is valid as a KV primary key.
120pub fn is_valid_kv_key_type(ct: &ColumnType) -> bool {
121    KV_VALID_KEY_TYPES.contains(ct)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::columnar::{ColumnDef, ColumnType, StrictSchema};
128
129    #[test]
130    fn kv_config_primary_key() {
131        let schema = StrictSchema::new(vec![
132            ColumnDef::required("session_id", ColumnType::String).with_primary_key(),
133            ColumnDef::required("user_id", ColumnType::Uuid),
134            ColumnDef::nullable("payload", ColumnType::Bytes),
135        ])
136        .unwrap();
137        let config = KvConfig {
138            schema,
139            ttl: None,
140            capacity_hint: 0,
141            inline_threshold: KV_DEFAULT_INLINE_THRESHOLD,
142        };
143        let pk = config.primary_key_column().unwrap();
144        assert_eq!(pk.name, "session_id");
145        assert_eq!(pk.column_type, ColumnType::String);
146        assert!(pk.primary_key);
147    }
148
149    #[test]
150    fn kv_valid_key_types() {
151        assert!(is_valid_kv_key_type(&ColumnType::String));
152        assert!(is_valid_kv_key_type(&ColumnType::Uuid));
153        assert!(is_valid_kv_key_type(&ColumnType::Int64));
154        assert!(is_valid_kv_key_type(&ColumnType::Bytes));
155        assert!(is_valid_kv_key_type(&ColumnType::Timestamp));
156        // Invalid key types:
157        assert!(!is_valid_kv_key_type(&ColumnType::Float64));
158        assert!(!is_valid_kv_key_type(&ColumnType::Bool));
159        assert!(!is_valid_kv_key_type(&ColumnType::Geometry));
160        assert!(!is_valid_kv_key_type(&ColumnType::Vector(128)));
161        assert!(!is_valid_kv_key_type(&ColumnType::Decimal));
162    }
163
164    #[test]
165    fn kv_ttl_policy_serde() {
166        let policies = vec![
167            KvTtlPolicy::FixedDuration {
168                duration_ms: 60_000,
169            },
170            KvTtlPolicy::FieldBased {
171                field: "last_active".into(),
172                offset_ms: 3_600_000,
173            },
174        ];
175        for p in policies {
176            let json = sonic_rs::to_string(&p).unwrap();
177            let back: KvTtlPolicy = sonic_rs::from_str(&json).unwrap();
178            assert_eq!(back, p);
179        }
180    }
181}