Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/
snapshot.rs

1//! Snapshot — an immutable identifier of a database state at a point in time.
2//!
3//! A `Snapshot` is the canonical (sorted, deduplicated) set of DAG tip IDs
4//! that fully identifies the state of a `Database` or a `Store` within one.
5//! Content-addressing makes the mapping bijective: given a snapshot and the
6//! database root it belongs to, the entries (and therefore all reachable
7//! content) are uniquely determined.
8//!
9//! Use a `Snapshot` to pin a read view, anchor a transaction, or describe
10//! a state transition (e.g. `WriteEvent { from, to }`).
11//!
12//! `Snapshot` is intentionally scope-free: it carries *only* a set of tips.
13//! The scope — a database root, or a `(database root, store name)` pair — is
14//! contextual and supplied alongside the snapshot at API boundaries. A
15//! snapshot can therefore describe either a database state or a store state;
16//! the distinction lives in the API method consuming it, not on the snapshot
17//! itself. Keeping `Snapshot` as just a canonical tip-set means no optional
18//! fields, no runtime "is the root set?" checks, and a wire format identical
19//! to `Vec<ID>`.
20
21use serde::{Deserialize, Serialize, Serializer};
22
23use crate::entry::ID;
24
25/// Identifier for a database state — a sorted, deduplicated set of DAG tips.
26///
27/// Equality and hashing are set-equality on the tips. Serialization is
28/// transparent: the wire form is a bare array of IDs, identical to a
29/// `Vec<ID>`. Deserialization normalizes (sort + dedup), so unsorted or
30/// duplicated wire input is canonicalized on read.
31#[derive(Debug, Clone, Default)]
32pub struct Snapshot {
33    /// Sorted, deduplicated set of tip IDs.
34    tips: Vec<ID>,
35}
36
37impl Serialize for Snapshot {
38    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
39        self.tips.serialize(serializer)
40    }
41}
42
43impl<'de> Deserialize<'de> for Snapshot {
44    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
45    where
46        D: serde::Deserializer<'de>,
47    {
48        let tips = Vec::<ID>::deserialize(deserializer)?;
49        Ok(Self::new(tips))
50    }
51}
52
53impl Snapshot {
54    /// A snapshot containing no tips — the state of a database with no entries.
55    pub const EMPTY: Snapshot = Snapshot { tips: Vec::new() };
56
57    /// Construct a snapshot from a vector of tips.
58    ///
59    /// Tips are sorted and deduplicated.
60    pub fn new(mut tips: Vec<ID>) -> Self {
61        tips.sort();
62        tips.dedup();
63        Self { tips }
64    }
65
66    /// Borrow the tips as a sorted, deduplicated slice.
67    pub fn tips(&self) -> &[ID] {
68        &self.tips
69    }
70
71    /// Consume the snapshot and return the underlying tips.
72    pub fn into_tips(self) -> Vec<ID> {
73        self.tips
74    }
75
76    /// Returns true if this snapshot contains no tips.
77    pub fn is_empty(&self) -> bool {
78        self.tips.is_empty()
79    }
80
81    /// Number of tips in this snapshot.
82    pub fn len(&self) -> usize {
83        self.tips.len()
84    }
85}
86
87/// Equality is set-equality on tips.
88impl PartialEq for Snapshot {
89    fn eq(&self, other: &Self) -> bool {
90        // Set-equality holds because `tips` is canonical (sorted + deduped on construction).
91        self.tips == other.tips
92    }
93}
94
95impl Eq for Snapshot {}
96
97impl std::hash::Hash for Snapshot {
98    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
99        self.tips.hash(state);
100    }
101}
102
103impl From<Vec<ID>> for Snapshot {
104    fn from(tips: Vec<ID>) -> Self {
105        Self::new(tips)
106    }
107}
108
109impl From<&[ID]> for Snapshot {
110    fn from(tips: &[ID]) -> Self {
111        Self::new(tips.to_vec())
112    }
113}
114
115impl<const N: usize> From<[ID; N]> for Snapshot {
116    fn from(tips: [ID; N]) -> Self {
117        Self::new(tips.to_vec())
118    }
119}
120
121impl<const N: usize> From<&[ID; N]> for Snapshot {
122    fn from(tips: &[ID; N]) -> Self {
123        Self::new(tips.to_vec())
124    }
125}
126
127impl From<&Vec<ID>> for Snapshot {
128    fn from(tips: &Vec<ID>) -> Self {
129        Self::new(tips.clone())
130    }
131}
132
133impl AsRef<[ID]> for Snapshot {
134    fn as_ref(&self) -> &[ID] {
135        &self.tips
136    }
137}
138
139impl std::ops::Deref for Snapshot {
140    type Target = [ID];
141    fn deref(&self) -> &[ID] {
142        &self.tips
143    }
144}
145
146impl<'a> IntoIterator for &'a Snapshot {
147    type Item = &'a ID;
148    type IntoIter = std::slice::Iter<'a, ID>;
149
150    fn into_iter(self) -> Self::IntoIter {
151        self.tips.iter()
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    fn id(byte: u8) -> ID {
160        ID::from_bytes([byte])
161    }
162
163    #[test]
164    fn empty_const_is_empty() {
165        assert!(Snapshot::EMPTY.is_empty());
166        assert_eq!(Snapshot::EMPTY.len(), 0);
167        assert_eq!(Snapshot::EMPTY.tips(), &[] as &[ID]);
168    }
169
170    #[test]
171    fn default_equals_empty() {
172        assert_eq!(Snapshot::default(), Snapshot::EMPTY);
173    }
174
175    #[test]
176    fn new_sorts_input() {
177        let a = id(1);
178        let b = id(2);
179        let c = id(3);
180        let unsorted = Snapshot::new(vec![c.clone(), a.clone(), b.clone()]);
181        let sorted = Snapshot::new(vec![a, b, c]);
182        assert_eq!(unsorted.tips(), sorted.tips());
183    }
184
185    #[test]
186    fn new_dedups_input() {
187        let a = id(1);
188        let b = id(2);
189        let with_dupes = Snapshot::new(vec![a.clone(), b.clone(), a.clone(), b.clone(), a.clone()]);
190        let mut expected = vec![a, b];
191        expected.sort();
192        assert_eq!(with_dupes.len(), 2);
193        assert_eq!(with_dupes.tips(), expected.as_slice());
194    }
195
196    #[test]
197    fn set_equality_holds_via_canonical_form() {
198        let a = id(1);
199        let b = id(2);
200        let s1: Snapshot = vec![a.clone(), b.clone()].into();
201        let s2: Snapshot = vec![b, a].into();
202        assert_eq!(s1, s2);
203    }
204
205    #[test]
206    fn hash_matches_for_set_equal_snapshots() {
207        use std::collections::hash_map::DefaultHasher;
208        use std::hash::{Hash, Hasher};
209        let a = id(1);
210        let b = id(2);
211        let s1: Snapshot = vec![a.clone(), b.clone()].into();
212        let s2: Snapshot = vec![b, a].into();
213        let mut h1 = DefaultHasher::new();
214        s1.hash(&mut h1);
215        let mut h2 = DefaultHasher::new();
216        s2.hash(&mut h2);
217        assert_eq!(h1.finish(), h2.finish());
218    }
219
220    #[test]
221    fn from_slice_sorts_and_dedups() {
222        let a = id(1);
223        let b = id(2);
224        let snap = Snapshot::from(&[b.clone(), a.clone(), a.clone()][..]);
225        let mut expected = vec![a, b];
226        expected.sort();
227        assert_eq!(snap.tips(), expected.as_slice());
228    }
229
230    #[test]
231    fn from_array_sorts_and_dedups() {
232        let a = id(1);
233        let b = id(2);
234        let snap = Snapshot::from([b.clone(), a.clone(), a.clone()]);
235        let mut expected = vec![a, b];
236        expected.sort();
237        assert_eq!(snap.tips(), expected.as_slice());
238    }
239
240    #[test]
241    fn into_tips_returns_sorted_vec() {
242        let mut expected = vec![id(1), id(2), id(3)];
243        expected.sort();
244        let snap = Snapshot::new(vec![id(3), id(1), id(2)]);
245        assert_eq!(snap.into_tips(), expected);
246    }
247
248    #[test]
249    fn iter_yields_sorted_tips() {
250        let mut expected = [id(1), id(2), id(3)];
251        expected.sort();
252        let snap = Snapshot::new(vec![id(3), id(1), id(2)]);
253        let collected: Vec<&ID> = (&snap).into_iter().collect();
254        let expected_refs: Vec<&ID> = expected.iter().collect();
255        assert_eq!(collected, expected_refs);
256    }
257
258    #[test]
259    fn as_ref_exposes_sorted_slice() {
260        let a = id(1);
261        let b = id(2);
262        let snap = Snapshot::new(vec![b.clone(), a.clone()]);
263        let slice: &[ID] = snap.as_ref();
264        let mut expected = vec![a, b];
265        expected.sort();
266        assert_eq!(slice, expected.as_slice());
267    }
268
269    #[test]
270    fn serde_roundtrip_preserves_invariant() {
271        let a = id(1);
272        let b = id(2);
273        let snap = Snapshot::new(vec![b, a]);
274        let json = serde_json::to_string(&snap).unwrap();
275        let parsed: Snapshot = serde_json::from_str(&json).unwrap();
276        assert_eq!(parsed, snap);
277    }
278
279    #[test]
280    fn deserialize_normalizes_unsorted_wire_data() {
281        // Simulate wire data written without the sorted invariant
282        // (e.g. by an older client). The Snapshot deserializer must canonicalize.
283        let a = id(1);
284        let b = id(2);
285        let canonical = Snapshot::new(vec![a.clone(), b.clone()]);
286        let unsorted_json = serde_json::to_string(&vec![b, a]).unwrap();
287        let parsed: Snapshot = serde_json::from_str(&unsorted_json).unwrap();
288        assert_eq!(parsed, canonical);
289    }
290
291    #[test]
292    fn deserialize_dedups_wire_data() {
293        let a = id(1);
294        let b = id(2);
295        let canonical = Snapshot::new(vec![a.clone(), b.clone()]);
296        let duped_json = serde_json::to_string(&vec![a.clone(), b, a]).unwrap();
297        let parsed: Snapshot = serde_json::from_str(&duped_json).unwrap();
298        assert_eq!(parsed, canonical);
299    }
300
301    #[test]
302    fn serializes_as_bare_id_array() {
303        // Snapshot must be wire-compatible with Vec<ID> — same JSON shape.
304        // This matters for in-place migration of fields that were previously
305        // typed `Vec<ID>` (e.g. EntryMetadata.settings_tips).
306        let a = id(1);
307        let b = id(2);
308        let snap = Snapshot::new(vec![a.clone(), b.clone()]);
309        let snap_json = serde_json::to_string(&snap).unwrap();
310        let vec_json = serde_json::to_string(snap.tips()).unwrap();
311        assert_eq!(snap_json, vec_json);
312    }
313}