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