Skip to main content

pulsedb/
types.rs

1//! Core type definitions for PulseDB identifiers and timestamps.
2//!
3//! This module defines the fundamental ID types used throughout PulseDB.
4//! All ID types use UUID v7 for time-ordered unique identification.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use uuid::Uuid;
9
10/// Collective identifier (UUID v7 for time-ordering).
11///
12/// Collectives are isolated namespaces for agent experiences, typically one per project.
13/// Each collective has its own HNSW index and embedding dimension.
14///
15/// # Example
16/// ```
17/// use pulsedb::CollectiveId;
18///
19/// let id = CollectiveId::new();
20/// println!("Created collective: {}", id);
21/// ```
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct CollectiveId(pub Uuid);
24
25impl CollectiveId {
26    /// Creates a new CollectiveId with a UUID v7 (time-ordered).
27    #[inline]
28    pub fn new() -> Self {
29        Self(Uuid::now_v7())
30    }
31
32    /// Creates a nil (all zeros) CollectiveId.
33    /// Useful for testing or sentinel values.
34    #[inline]
35    pub fn nil() -> Self {
36        Self(Uuid::nil())
37    }
38
39    /// Returns the raw UUID bytes for storage.
40    #[inline]
41    pub fn as_bytes(&self) -> &[u8; 16] {
42        self.0.as_bytes()
43    }
44
45    /// Creates a CollectiveId from raw bytes.
46    #[inline]
47    pub fn from_bytes(bytes: [u8; 16]) -> Self {
48        Self(Uuid::from_bytes(bytes))
49    }
50}
51
52impl Default for CollectiveId {
53    /// Returns a nil (all zeros) CollectiveId.
54    ///
55    /// For a new unique ID, use [`CollectiveId::new()`].
56    fn default() -> Self {
57        Self::nil()
58    }
59}
60
61impl fmt::Display for CollectiveId {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(f, "{}", self.0)
64    }
65}
66
67/// Experience identifier (UUID v7 for time-ordering).
68///
69/// Experiences are the core unit of learned knowledge in PulseDB.
70/// Each experience belongs to exactly one collective.
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
72pub struct ExperienceId(pub Uuid);
73
74impl ExperienceId {
75    /// Creates a new ExperienceId with a UUID v7 (time-ordered).
76    #[inline]
77    pub fn new() -> Self {
78        Self(Uuid::now_v7())
79    }
80
81    /// Creates a nil (all zeros) ExperienceId.
82    #[inline]
83    pub fn nil() -> Self {
84        Self(Uuid::nil())
85    }
86
87    /// Returns the raw UUID bytes for storage.
88    #[inline]
89    pub fn as_bytes(&self) -> &[u8; 16] {
90        self.0.as_bytes()
91    }
92
93    /// Creates an ExperienceId from raw bytes.
94    #[inline]
95    pub fn from_bytes(bytes: [u8; 16]) -> Self {
96        Self(Uuid::from_bytes(bytes))
97    }
98}
99
100impl Default for ExperienceId {
101    /// Returns a nil (all zeros) ExperienceId.
102    ///
103    /// For a new unique ID, use [`ExperienceId::new()`].
104    fn default() -> Self {
105        Self::nil()
106    }
107}
108
109impl fmt::Display for ExperienceId {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}", self.0)
112    }
113}
114
115/// Unix timestamp in milliseconds.
116///
117/// Using i64 allows representing dates far into the future and past.
118/// Millisecond precision is sufficient for agent operations.
119#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
120pub struct Timestamp(pub i64);
121
122impl Timestamp {
123    /// Creates a timestamp for the current moment.
124    ///
125    /// If the system clock is before the Unix epoch (should never happen
126    /// in practice), returns a timestamp of 0 (epoch) rather than panicking.
127    #[inline]
128    pub fn now() -> Self {
129        use std::time::{SystemTime, UNIX_EPOCH};
130        let duration = SystemTime::now()
131            .duration_since(UNIX_EPOCH)
132            .unwrap_or_default();
133        Self(duration.as_millis() as i64)
134    }
135
136    /// Creates a timestamp from Unix milliseconds.
137    #[inline]
138    pub const fn from_millis(millis: i64) -> Self {
139        Self(millis)
140    }
141
142    /// Returns the timestamp as Unix milliseconds.
143    #[inline]
144    pub const fn as_millis(&self) -> i64 {
145        self.0
146    }
147
148    /// Returns big-endian bytes for storage (enables lexicographic ordering).
149    #[inline]
150    pub fn to_be_bytes(&self) -> [u8; 8] {
151        self.0.to_be_bytes()
152    }
153}
154
155impl fmt::Display for Timestamp {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(f, "{}", self.0)
158    }
159}
160
161/// Relation identifier (UUID v7 for time-ordering).
162///
163/// Relations connect two experiences within the same collective,
164/// enabling agents to understand how knowledge connects.
165#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
166pub struct RelationId(pub Uuid);
167
168impl RelationId {
169    /// Creates a new RelationId with a UUID v7 (time-ordered).
170    #[inline]
171    pub fn new() -> Self {
172        Self(Uuid::now_v7())
173    }
174
175    /// Creates a nil (all zeros) RelationId.
176    #[inline]
177    pub fn nil() -> Self {
178        Self(Uuid::nil())
179    }
180
181    /// Returns the raw UUID bytes for storage.
182    #[inline]
183    pub fn as_bytes(&self) -> &[u8; 16] {
184        self.0.as_bytes()
185    }
186
187    /// Creates a RelationId from raw bytes.
188    #[inline]
189    pub fn from_bytes(bytes: [u8; 16]) -> Self {
190        Self(Uuid::from_bytes(bytes))
191    }
192}
193
194impl Default for RelationId {
195    /// Returns a nil (all zeros) RelationId.
196    ///
197    /// For a new unique ID, use [`RelationId::new()`].
198    fn default() -> Self {
199        Self::nil()
200    }
201}
202
203impl fmt::Display for RelationId {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "{}", self.0)
206    }
207}
208
209/// Insight identifier (UUID v7 for time-ordering).
210///
211/// Insights are derived knowledge synthesized from multiple experiences.
212/// Each insight belongs to exactly one collective.
213///
214/// # Example
215/// ```
216/// use pulsedb::InsightId;
217///
218/// let id = InsightId::new();
219/// println!("Created insight: {}", id);
220/// ```
221#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
222pub struct InsightId(pub Uuid);
223
224impl InsightId {
225    /// Creates a new InsightId with a UUID v7 (time-ordered).
226    #[inline]
227    pub fn new() -> Self {
228        Self(Uuid::now_v7())
229    }
230
231    /// Creates a nil (all zeros) InsightId.
232    #[inline]
233    pub fn nil() -> Self {
234        Self(Uuid::nil())
235    }
236
237    /// Returns the raw UUID bytes for storage.
238    #[inline]
239    pub fn as_bytes(&self) -> &[u8; 16] {
240        self.0.as_bytes()
241    }
242
243    /// Creates an InsightId from raw bytes.
244    #[inline]
245    pub fn from_bytes(bytes: [u8; 16]) -> Self {
246        Self(Uuid::from_bytes(bytes))
247    }
248}
249
250impl Default for InsightId {
251    /// Returns a nil (all zeros) InsightId.
252    ///
253    /// For a new unique ID, use [`InsightId::new()`].
254    fn default() -> Self {
255        Self::nil()
256    }
257}
258
259impl fmt::Display for InsightId {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        write!(f, "{}", self.0)
262    }
263}
264
265/// Opaque user identifier.
266///
267/// PulseDB doesn't handle authentication - the consumer provides user IDs.
268/// This allows integration with any auth system (OAuth, API keys, etc.).
269#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
270pub struct UserId(pub String);
271
272impl UserId {
273    /// Creates a new UserId from a string.
274    pub fn new(id: impl Into<String>) -> Self {
275        Self(id.into())
276    }
277
278    /// Returns the user ID as a string slice.
279    pub fn as_str(&self) -> &str {
280        &self.0
281    }
282}
283
284impl fmt::Display for UserId {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        write!(f, "{}", self.0)
287    }
288}
289
290/// Agent identifier.
291///
292/// Identifies a specific AI agent instance within a collective.
293/// Multiple agents can operate on the same collective simultaneously.
294#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
295pub struct AgentId(pub String);
296
297impl AgentId {
298    /// Creates a new AgentId from a string.
299    pub fn new(id: impl Into<String>) -> Self {
300        Self(id.into())
301    }
302
303    /// Returns the agent ID as a string slice.
304    pub fn as_str(&self) -> &str {
305        &self.0
306    }
307}
308
309impl fmt::Display for AgentId {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        write!(f, "{}", self.0)
312    }
313}
314
315/// Task identifier.
316///
317/// Identifies a specific task or job that an agent is working on.
318/// Used for tracking which experiences came from which task.
319#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
320pub struct TaskId(pub String);
321
322impl TaskId {
323    /// Creates a new TaskId from a string.
324    pub fn new(id: impl Into<String>) -> Self {
325        Self(id.into())
326    }
327
328    /// Returns the task ID as a string slice.
329    pub fn as_str(&self) -> &str {
330        &self.0
331    }
332}
333
334impl fmt::Display for TaskId {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        write!(f, "{}", self.0)
337    }
338}
339
340/// Embedding vector type alias.
341///
342/// Embeddings are f32 vectors of fixed dimension (typically 384 or 768).
343pub type Embedding = Vec<f32>;
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_collective_id_new_is_unique() {
351        let id1 = CollectiveId::new();
352        let id2 = CollectiveId::new();
353        assert_ne!(id1, id2);
354    }
355
356    #[test]
357    fn test_collective_id_nil() {
358        let id = CollectiveId::nil();
359        assert_eq!(id.0, Uuid::nil());
360    }
361
362    #[test]
363    fn test_collective_id_bytes_roundtrip() {
364        let id = CollectiveId::new();
365        let bytes = *id.as_bytes();
366        let restored = CollectiveId::from_bytes(bytes);
367        assert_eq!(id, restored);
368    }
369
370    #[test]
371    fn test_collective_id_serialization() {
372        let id = CollectiveId::new();
373        let bytes = bincode::serialize(&id).unwrap();
374        let restored: CollectiveId = bincode::deserialize(&bytes).unwrap();
375        assert_eq!(id, restored);
376    }
377
378    #[test]
379    fn test_experience_id_new_is_unique() {
380        let id1 = ExperienceId::new();
381        let id2 = ExperienceId::new();
382        assert_ne!(id1, id2);
383    }
384
385    #[test]
386    fn test_experience_id_serialization() {
387        let id = ExperienceId::new();
388        let bytes = bincode::serialize(&id).unwrap();
389        let restored: ExperienceId = bincode::deserialize(&bytes).unwrap();
390        assert_eq!(id, restored);
391    }
392
393    #[test]
394    fn test_relation_id_new_is_unique() {
395        let id1 = RelationId::new();
396        let id2 = RelationId::new();
397        assert_ne!(id1, id2);
398    }
399
400    #[test]
401    fn test_relation_id_nil() {
402        let id = RelationId::nil();
403        assert_eq!(id.0, Uuid::nil());
404    }
405
406    #[test]
407    fn test_relation_id_bytes_roundtrip() {
408        let id = RelationId::new();
409        let bytes = *id.as_bytes();
410        let restored = RelationId::from_bytes(bytes);
411        assert_eq!(id, restored);
412    }
413
414    #[test]
415    fn test_relation_id_serialization() {
416        let id = RelationId::new();
417        let bytes = bincode::serialize(&id).unwrap();
418        let restored: RelationId = bincode::deserialize(&bytes).unwrap();
419        assert_eq!(id, restored);
420    }
421
422    #[test]
423    fn test_insight_id_new_is_unique() {
424        let id1 = InsightId::new();
425        let id2 = InsightId::new();
426        assert_ne!(id1, id2);
427    }
428
429    #[test]
430    fn test_insight_id_nil() {
431        let id = InsightId::nil();
432        assert_eq!(id.0, Uuid::nil());
433    }
434
435    #[test]
436    fn test_insight_id_bytes_roundtrip() {
437        let id = InsightId::new();
438        let bytes = *id.as_bytes();
439        let restored = InsightId::from_bytes(bytes);
440        assert_eq!(id, restored);
441    }
442
443    #[test]
444    fn test_insight_id_serialization() {
445        let id = InsightId::new();
446        let bytes = bincode::serialize(&id).unwrap();
447        let restored: InsightId = bincode::deserialize(&bytes).unwrap();
448        assert_eq!(id, restored);
449    }
450
451    #[test]
452    fn test_timestamp_now() {
453        let t1 = Timestamp::now();
454        std::thread::sleep(std::time::Duration::from_millis(1));
455        let t2 = Timestamp::now();
456        assert!(t1 < t2, "Timestamps should be ordered");
457    }
458
459    #[test]
460    fn test_timestamp_ordering() {
461        let t1 = Timestamp::from_millis(1000);
462        let t2 = Timestamp::from_millis(2000);
463        assert!(t1 < t2);
464    }
465
466    #[test]
467    fn test_timestamp_be_bytes() {
468        // Big-endian ensures lexicographic ordering matches numeric ordering
469        let t1 = Timestamp::from_millis(100);
470        let t2 = Timestamp::from_millis(200);
471        assert!(t1.to_be_bytes() < t2.to_be_bytes());
472    }
473
474    #[test]
475    fn test_user_id() {
476        let id = UserId::new("user-123");
477        assert_eq!(id.as_str(), "user-123");
478        assert_eq!(format!("{}", id), "user-123");
479    }
480
481    #[test]
482    fn test_agent_id() {
483        let id = AgentId::new("claude-opus");
484        assert_eq!(id.as_str(), "claude-opus");
485    }
486
487    #[test]
488    fn test_task_id() {
489        let id = TaskId::new("task-456");
490        assert_eq!(id.as_str(), "task-456");
491    }
492}