Skip to main content

fathomdb_engine/
ids.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::SystemTime;
3
4use ulid::Ulid;
5
6static COUNTER: AtomicU64 = AtomicU64::new(0);
7
8/// Generate a new identifier suitable for use as a `row_id`, `logical_id`, or
9/// chunk/run/step/action `id`.
10///
11/// Returns a 26-character ULID (Universally Unique Lexicographically Sortable Identifier).
12/// ULIDs are timestamp-prefixed so IDs generated close in time sort together.
13/// They are case-insensitive and URL-safe.
14///
15/// This function is not part of the write path. Callers that already have stable
16/// identifiers are not required to use it.
17#[must_use]
18pub fn new_id() -> String {
19    Ulid::new().to_string()
20}
21
22pub fn new_row_id() -> String {
23    let now = SystemTime::now()
24        .duration_since(SystemTime::UNIX_EPOCH)
25        .unwrap_or_default();
26    let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
27    format!(
28        "{:016x}-{:08x}-{:016x}",
29        now.as_secs(),
30        now.subsec_nanos(),
31        seq
32    )
33}
34
35#[cfg(test)]
36#[allow(clippy::expect_used)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn new_id_returns_nonempty_string() {
42        let id = new_id();
43        assert!(!id.is_empty(), "new_id must return a non-empty string");
44    }
45
46    #[test]
47    fn new_id_returns_unique_values() {
48        let a = new_id();
49        let b = new_id();
50        assert_ne!(a, b, "consecutive new_id calls must return distinct values");
51    }
52
53    #[test]
54    fn new_id_is_26_characters() {
55        let id = new_id();
56        assert_eq!(
57            id.len(),
58            26,
59            "ULID must be exactly 26 characters, got: {id}"
60        );
61    }
62
63    #[test]
64    fn new_id_is_valid_for_node_insert() {
65        use std::sync::Arc;
66
67        use fathomdb_schema::SchemaManager;
68        use tempfile::NamedTempFile;
69
70        use crate::{
71            ChunkPolicy, NodeInsert, ProvenanceMode, TelemetryCounters, WriteRequest, WriterActor,
72        };
73
74        let db = NamedTempFile::new().expect("temporary db");
75        let writer = WriterActor::start(
76            db.path(),
77            Arc::new(SchemaManager::new()),
78            ProvenanceMode::Warn,
79            Arc::new(TelemetryCounters::default()),
80        )
81        .expect("writer");
82
83        let row_id = new_id();
84        let logical_id = new_id();
85
86        writer
87            .submit(WriteRequest {
88                label: "new_id_test".to_owned(),
89                nodes: vec![NodeInsert {
90                    row_id,
91                    logical_id,
92                    kind: "Note".to_owned(),
93                    properties: "{}".to_owned(),
94                    source_ref: Some("test".to_owned()),
95                    upsert: false,
96                    chunk_policy: ChunkPolicy::Preserve,
97                    content_ref: None,
98                }],
99                node_retires: vec![],
100                edges: vec![],
101                edge_retires: vec![],
102                chunks: vec![],
103                runs: vec![],
104                steps: vec![],
105                actions: vec![],
106                optional_backfills: vec![],
107                vec_inserts: vec![],
108                operational_writes: vec![],
109            })
110            .expect("write with new_id must succeed");
111    }
112
113    #[test]
114    fn new_row_id_returns_unique_ids() {
115        let a = new_row_id();
116        let b = new_row_id();
117        let c = new_row_id();
118        assert_ne!(a, b, "consecutive IDs must be distinct");
119        assert_ne!(b, c, "consecutive IDs must be distinct");
120        assert_ne!(a, c, "consecutive IDs must be distinct");
121    }
122
123    #[test]
124    fn new_row_id_has_expected_format() {
125        let id = new_row_id();
126        assert!(!id.is_empty(), "ID must not be empty");
127        assert!(
128            id.chars().all(|c| c.is_ascii_hexdigit() || c == '-'),
129            "ID must contain only hex digits and dashes, got: {id}"
130        );
131    }
132}