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