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(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}