1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::SystemTime;
3
4use ulid::Ulid;
5
6static COUNTER: AtomicU64 = AtomicU64::new(0);
7
8#[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}