aimdb_core/
record_id.rs

1//! Record identification types for stable, O(1) lookups
2//!
3//! This module provides two key types for record identification:
4//!
5//! - [`RecordKey`]: A stable, human-readable identifier for external APIs
6//! - [`RecordId`]: An internal index for O(1) hot-path lookups
7//!
8//! # Design Rationale
9//!
10//! AimDB separates *logical identity* (RecordKey) from *physical identity* (RecordId):
11//!
12//! - **RecordKey** is used by external systems (MCP, CLI, config files) and supports
13//!   multiple records of the same Rust type (e.g., "sensors.outdoor" and "sensors.indoor"
14//!   can both be `Temperature` records).
15//!
16//! - **RecordId** is the hot-path identifier used internally for O(1) Vec indexing
17//!   during produce/consume operations.
18//!
19//! # Examples
20//!
21//! ```rust
22//! use aimdb_core::record_id::RecordKey;
23//!
24//! // Static keys (zero allocation, preferred)
25//! let key: RecordKey = "sensors.temperature".into();
26//! assert!(key.is_static());
27//!
28//! // Dynamic keys (for runtime-generated names)
29//! let tenant_id = "acme";
30//! let key = RecordKey::dynamic(format!("tenant.{}.sensors", tenant_id));
31//! assert!(!key.is_static());
32//!
33//! // RecordId is internal, obtained from the database (not user-constructed)
34//! // let id = db.resolve("sensors.temperature").unwrap();
35//! ```
36
37#[cfg(not(feature = "std"))]
38extern crate alloc;
39
40#[cfg(not(feature = "std"))]
41use alloc::sync::Arc;
42
43#[cfg(feature = "std")]
44use std::sync::Arc;
45
46/// Stable identifier for a record
47///
48/// Supports both static (zero-cost) and dynamic (Arc-allocated) names.
49/// Use string literals for the common case; they auto-convert via `From`.
50///
51/// # Naming Convention
52///
53/// Recommended format: `<namespace>.<category>.<instance>`
54///
55/// Examples:
56/// - `sensors.temperature.outdoor`
57/// - `sensors.temperature.indoor`
58/// - `mesh.weather.sf-bay`
59/// - `config.app.settings`
60/// - `tenant.acme.sensors.temp`
61///
62/// # Examples
63///
64/// ```rust
65/// use aimdb_core::record_id::RecordKey;
66///
67/// // Static (preferred) - zero allocation
68/// let key: RecordKey = "sensors.temperature".into();
69///
70/// // Dynamic - for runtime-generated names
71/// let key = RecordKey::dynamic(format!("tenant.{}.sensors", "acme"));
72/// ```
73#[derive(Clone)]
74pub struct RecordKey(RecordKeyInner);
75
76// Custom Debug to show Static vs Dynamic variant
77impl core::fmt::Debug for RecordKey {
78    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79        match &self.0 {
80            RecordKeyInner::Static(s) => f.debug_tuple("RecordKey::Static").field(s).finish(),
81            RecordKeyInner::Dynamic(s) => f.debug_tuple("RecordKey::Dynamic").field(s).finish(),
82        }
83    }
84}
85
86#[derive(Clone)]
87enum RecordKeyInner {
88    /// Static string literal (zero allocation, pointer comparison possible)
89    Static(&'static str),
90    /// Dynamic runtime string (Arc for cheap cloning)
91    Dynamic(Arc<str>),
92}
93
94impl RecordKey {
95    /// Create from a static string literal
96    ///
97    /// This is a const fn, usable in const contexts.
98    #[inline]
99    #[must_use]
100    pub const fn new(s: &'static str) -> Self {
101        Self(RecordKeyInner::Static(s))
102    }
103
104    /// Create from a runtime-generated string
105    ///
106    /// Use this for dynamic names (multi-tenant, config-driven, etc.).
107    #[inline]
108    #[must_use]
109    pub fn dynamic(s: impl Into<Arc<str>>) -> Self {
110        Self(RecordKeyInner::Dynamic(s.into()))
111    }
112
113    /// Create from a runtime string (alias for `dynamic`)
114    ///
115    /// Allocates an `Arc<str>` to store the string.
116    #[inline]
117    #[must_use]
118    pub fn from_dynamic(s: &str) -> Self {
119        Self(RecordKeyInner::Dynamic(Arc::from(s)))
120    }
121
122    /// Get the string representation
123    #[inline]
124    pub fn as_str(&self) -> &str {
125        match &self.0 {
126            RecordKeyInner::Static(s) => s,
127            RecordKeyInner::Dynamic(s) => s,
128        }
129    }
130
131    /// Returns true if this is a static (zero-allocation) key
132    #[inline]
133    pub fn is_static(&self) -> bool {
134        matches!(self.0, RecordKeyInner::Static(_))
135    }
136}
137
138// ===== Trait Implementations =====
139
140impl core::hash::Hash for RecordKey {
141    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
142        // Hash the string content, not the enum variant
143        self.as_str().hash(state);
144    }
145}
146
147impl PartialEq for RecordKey {
148    fn eq(&self, other: &Self) -> bool {
149        self.as_str() == other.as_str()
150    }
151}
152
153impl Eq for RecordKey {}
154
155/// Enable direct comparison with &str
156impl PartialEq<str> for RecordKey {
157    fn eq(&self, other: &str) -> bool {
158        self.as_str() == other
159    }
160}
161
162/// Enable direct comparison with &str reference
163impl PartialEq<&str> for RecordKey {
164    fn eq(&self, other: &&str) -> bool {
165        self.as_str() == *other
166    }
167}
168
169impl PartialOrd for RecordKey {
170    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
171        Some(self.cmp(other))
172    }
173}
174
175impl Ord for RecordKey {
176    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
177        self.as_str().cmp(other.as_str())
178    }
179}
180
181/// Ergonomic conversion from string literals
182impl From<&'static str> for RecordKey {
183    #[inline]
184    fn from(s: &'static str) -> Self {
185        Self::new(s)
186    }
187}
188
189/// Ergonomic conversion from owned String (no_std with alloc)
190#[cfg(all(feature = "alloc", not(feature = "std")))]
191impl From<alloc::string::String> for RecordKey {
192    #[inline]
193    fn from(s: alloc::string::String) -> Self {
194        Self::dynamic(s)
195    }
196}
197
198/// Ergonomic conversion from owned String (std)
199#[cfg(feature = "std")]
200impl From<String> for RecordKey {
201    #[inline]
202    fn from(s: String) -> Self {
203        Self::dynamic(s)
204    }
205}
206
207impl core::fmt::Display for RecordKey {
208    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
209        f.write_str(self.as_str())
210    }
211}
212
213impl AsRef<str> for RecordKey {
214    fn as_ref(&self) -> &str {
215        self.as_str()
216    }
217}
218
219/// Enable O(1) HashMap lookup by &str
220///
221/// This allows `hashmap.get("string_literal")` without allocating a RecordKey.
222impl core::borrow::Borrow<str> for RecordKey {
223    fn borrow(&self) -> &str {
224        self.as_str()
225    }
226}
227
228// ===== Serde Support (std only) =====
229
230#[cfg(feature = "std")]
231impl serde::Serialize for RecordKey {
232    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
233        serializer.serialize_str(self.as_str())
234    }
235}
236
237#[cfg(feature = "std")]
238impl<'de> serde::Deserialize<'de> for RecordKey {
239    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
240        let s = String::deserialize(deserializer)?;
241        Ok(Self::dynamic(s))
242    }
243}
244
245// ===== RecordId =====
246
247/// Internal record identifier (index into storage Vec)
248///
249/// This is the hot-path identifier used for O(1) lookups during
250/// produce/consume operations. Not exposed to external APIs.
251///
252/// # Why u32?
253///
254/// - 4 billion records is more than enough for any deployment
255/// - Smaller than `usize` on 64-bit systems (cache efficiency)
256/// - Copy-friendly (no clone overhead)
257#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
258pub struct RecordId(pub(crate) u32);
259
260impl RecordId {
261    /// Create a new RecordId from an index
262    ///
263    /// # Arguments
264    /// * `index` - The index into the record storage array
265    #[inline]
266    pub const fn new(index: u32) -> Self {
267        Self(index)
268    }
269
270    /// Get the underlying index
271    #[inline]
272    pub const fn index(self) -> usize {
273        self.0 as usize
274    }
275
276    /// Get the raw u32 value (for serialization)
277    #[inline]
278    pub const fn raw(self) -> u32 {
279        self.0
280    }
281}
282
283impl core::fmt::Display for RecordId {
284    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
285        write!(f, "RecordId({})", self.0)
286    }
287}
288
289#[cfg(feature = "std")]
290impl serde::Serialize for RecordId {
291    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
292        serializer.serialize_u32(self.0)
293    }
294}
295
296#[cfg(feature = "std")]
297impl<'de> serde::Deserialize<'de> for RecordId {
298    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
299        let index = u32::deserialize(deserializer)?;
300        Ok(Self(index))
301    }
302}
303
304// ===== Unit Tests (std only - uses String, HashMap, format!) =====
305
306#[cfg(all(test, feature = "std"))]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn test_record_key_static() {
312        let key: RecordKey = "sensors.temperature".into();
313        assert!(key.is_static());
314        assert_eq!(key.as_str(), "sensors.temperature");
315    }
316
317    #[test]
318    fn test_record_key_dynamic() {
319        let key = RecordKey::dynamic("sensors.temperature".to_string());
320        assert!(!key.is_static());
321        assert_eq!(key.as_str(), "sensors.temperature");
322    }
323
324    #[test]
325    fn test_record_key_equality() {
326        let static_key: RecordKey = "sensors.temp".into();
327        let dynamic_key = RecordKey::dynamic("sensors.temp".to_string());
328
329        // Static and dynamic keys with same content should be equal
330        assert_eq!(static_key, dynamic_key);
331    }
332
333    #[test]
334    fn test_record_key_hash_consistency() {
335        use core::hash::{Hash, Hasher};
336        use std::collections::hash_map::DefaultHasher;
337
338        fn hash_key(key: &RecordKey) -> u64 {
339            let mut hasher = DefaultHasher::new();
340            key.hash(&mut hasher);
341            hasher.finish()
342        }
343
344        let static_key: RecordKey = "sensors.temp".into();
345        let dynamic_key = RecordKey::dynamic("sensors.temp".to_string());
346
347        // Hash should be the same for equal keys
348        assert_eq!(hash_key(&static_key), hash_key(&dynamic_key));
349    }
350
351    #[test]
352    fn test_record_key_borrow() {
353        use std::collections::HashMap;
354
355        let mut map: HashMap<RecordKey, i32> = HashMap::new();
356        map.insert("sensors.temp".into(), 42);
357
358        // Can lookup by &str without allocation
359        assert_eq!(map.get("sensors.temp"), Some(&42));
360    }
361
362    #[test]
363    fn test_record_id_basic() {
364        let id = RecordId::new(42);
365        assert_eq!(id.index(), 42);
366        assert_eq!(id.raw(), 42);
367    }
368
369    #[test]
370    fn test_record_id_copy() {
371        let id1 = RecordId::new(10);
372        let id2 = id1; // Copy, not move
373        assert_eq!(id1, id2);
374    }
375
376    #[test]
377    fn test_record_key_display() {
378        let key: RecordKey = "sensors.temperature".into();
379        assert_eq!(format!("{}", key), "sensors.temperature");
380    }
381
382    #[test]
383    fn test_record_key_debug() {
384        let static_key: RecordKey = "sensors.temp".into();
385        let dynamic_key = RecordKey::dynamic("sensors.temp".to_string());
386
387        // Debug output should distinguish static vs dynamic
388        let static_debug = format!("{:?}", static_key);
389        let dynamic_debug = format!("{:?}", dynamic_key);
390
391        assert!(static_debug.contains("Static"));
392        assert!(dynamic_debug.contains("Dynamic"));
393    }
394
395    #[test]
396    fn test_record_key_partial_eq_str() {
397        let key: RecordKey = "sensors.temperature".into();
398
399        // Direct comparison with &str
400        assert!(key == "sensors.temperature");
401        assert!(key != "other.key");
402
403        // Also works with str reference
404        let s: &str = "sensors.temperature";
405        assert!(key == s);
406    }
407
408    #[test]
409    fn test_record_key_from_string() {
410        let owned = "sensors.temperature".to_string();
411        let key: RecordKey = owned.into();
412
413        assert!(!key.is_static());
414        assert_eq!(key.as_str(), "sensors.temperature");
415    }
416
417    #[test]
418    fn test_record_id_display() {
419        let id = RecordId::new(42);
420        assert_eq!(format!("{}", id), "RecordId(42)");
421    }
422}