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}