Skip to main content

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}