Skip to main content

link_cli/storage/
traits.rs

1//! Storage abstraction shared by every links backend.
2//!
3//! [`LinksStorage`] is the trait the transactions layer is written
4//! against, so any doublets-compatible store — the CLI's own
5//! [`LinkStorage`](crate::LinkStorage), a file-mapped
6//! [`DoubletsStorage`](super::DoubletsStorage), or an externally owned
7//! `doublets::unit::Store` wrapped with
8//! [`DoubletsStorage::wrap`](super::DoubletsStorage::wrap) — can be
9//! composed under [`GenericTransactionsDecorator`](crate::transactions::GenericTransactionsDecorator).
10//!
11//! The trait is generic over the doublets address type `T`, so external
12//! consumers using `usize`-addressed stores are first-class.
13
14use std::fs;
15use std::path::Path;
16
17use doublets::data::LinkReference;
18
19use crate::error::LinkError;
20use crate::link::GenericLink;
21
22/// Cheap fingerprint of a database file, used to answer
23/// "has anyone else written since I last looked?" without reparsing.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub struct StorageRevision {
26    /// Size of the database file in bytes (0 when it does not exist).
27    pub len: u64,
28    /// Last modification time in nanoseconds since the Unix epoch.
29    pub modified_nanos: u128,
30}
31
32impl StorageRevision {
33    /// Reads the current revision of `path`.
34    ///
35    /// A missing file is reported as [`StorageRevision::default`] rather
36    /// than an error, so callers can fingerprint a database before it
37    /// has been created.
38    pub fn of<P: AsRef<Path>>(path: P) -> Result<Self, LinkError> {
39        let metadata = match fs::metadata(path.as_ref()) {
40            Ok(metadata) => metadata,
41            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
42                return Ok(Self::default())
43            }
44            Err(error) => return Err(LinkError::Io(error)),
45        };
46        let modified_nanos = metadata
47            .modified()
48            .ok()
49            .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
50            .map(|duration| duration.as_nanos())
51            .unwrap_or(0);
52        Ok(Self {
53            len: metadata.len(),
54            modified_nanos,
55        })
56    }
57}
58
59/// A store of links addressed by `T`, expressed in terms of owned
60/// [`GenericLink<T>`] values so that both in-memory maps and
61/// memory-mapped doublets stores can implement it.
62pub trait LinksStorage<T: LinkReference> {
63    /// Creates a new link and returns its address.
64    fn create_link(&mut self, source: T, target: T) -> Result<T, LinkError>;
65
66    /// Ensures a link exists at `index`, creating placeholders as needed.
67    fn ensure_link_created(&mut self, index: T) -> Result<T, LinkError>;
68
69    /// Returns the link stored at `index`, if any.
70    fn get_link(&self, index: T) -> Option<GenericLink<T>>;
71
72    /// Returns `true` when a link exists at `index`.
73    fn link_exists(&self, index: T) -> bool {
74        self.get_link(index).is_some()
75    }
76
77    /// Repoints `index` at `source`/`target`, returning the previous state.
78    fn update_link(&mut self, index: T, source: T, target: T) -> Result<GenericLink<T>, LinkError>;
79
80    /// Deletes `index`, returning the link that was removed.
81    fn delete_link(&mut self, index: T) -> Result<GenericLink<T>, LinkError>;
82
83    /// [`Self::update_link`], reporting every `(before, after)` change it made.
84    ///
85    /// A store that resolves duplicate doublets turns one write into a cascade
86    /// of changes, and the transactions layer has to log each of them or a
87    /// rollback cannot restore what the write actually did. This is the
88    /// equivalent of the `WriteHandler` the C# decorators forward to.
89    ///
90    /// The default implementation reports the single change a store without any
91    /// policy of its own makes, so implementors only override it when they
92    /// really can cascade.
93    fn update_link_observed(
94        &mut self,
95        index: T,
96        source: T,
97        target: T,
98        observer: &mut dyn FnMut(GenericLink<T>, GenericLink<T>),
99    ) -> Result<GenericLink<T>, LinkError> {
100        let previous = self.update_link(index, source, target)?;
101        let after = self
102            .get_link(index)
103            .unwrap_or_else(|| GenericLink::new(index, source, target));
104        observer(previous, after);
105        Ok(previous)
106    }
107
108    /// [`Self::delete_link`], reporting every `(before, after)` change it made.
109    ///
110    /// See [`Self::update_link_observed`]; a cascading delete removes every link
111    /// that still referenced `index`.
112    fn delete_link_observed(
113        &mut self,
114        index: T,
115        observer: &mut dyn FnMut(GenericLink<T>, GenericLink<T>),
116    ) -> Result<GenericLink<T>, LinkError> {
117        let deleted = self.delete_link(index)?;
118        observer(deleted, GenericLink::null());
119        Ok(deleted)
120    }
121
122    /// Returns every link in the store.
123    fn all_links(&self) -> Vec<GenericLink<T>>;
124
125    /// Returns every link matching the (optional) index/source/target pattern.
126    fn query_links(
127        &self,
128        index: Option<T>,
129        source: Option<T>,
130        target: Option<T>,
131    ) -> Vec<GenericLink<T>> {
132        self.all_links()
133            .into_iter()
134            .filter(|link| {
135                index.is_none_or(|value| value == link.index)
136                    && source.is_none_or(|value| value == link.source)
137                    && target.is_none_or(|value| value == link.target)
138            })
139            .collect()
140    }
141
142    /// Finds the address of a link with the given source and target.
143    fn search_link(&self, source: T, target: T) -> Option<T> {
144        self.all_links()
145            .into_iter()
146            .find(|link| link.source == source && link.target == target)
147            .map(|link| link.index)
148    }
149
150    /// Returns the address of an existing `(source, target)` link,
151    /// creating it when it does not exist yet.
152    fn get_or_create_link(&mut self, source: T, target: T) -> Result<T, LinkError> {
153        match self.search_link(source, target) {
154            Some(index) => Ok(index),
155            None => self.create_link(source, target),
156        }
157    }
158
159    /// Number of links currently stored.
160    fn links_count(&self) -> usize {
161        self.all_links().len()
162    }
163
164    /// Makes every write durable on disk.
165    ///
166    /// Implementations that keep state in memory write it out here;
167    /// memory-mapped implementations `fsync` the backing file. Callers
168    /// that need crash-consistency guarantees must call this — see the
169    /// durability notes on each implementation.
170    fn flush(&mut self) -> Result<(), LinkError>;
171
172    /// Cheap check for "did another process write to this database since
173    /// we last read or wrote it?".
174    ///
175    /// The default implementation reports `false`, which is correct for
176    /// stores that are not shared through the filesystem.
177    fn has_external_changes(&self) -> Result<bool, LinkError> {
178        Ok(false)
179    }
180
181    /// Re-reads the database from disk, discarding cached state.
182    ///
183    /// The default implementation is a no-op for stores that always read
184    /// through to their backing storage.
185    fn reload(&mut self) -> Result<(), LinkError> {
186        Ok(())
187    }
188}
189
190/// Extension implemented by stores that keep links resident in memory and
191/// can therefore lend out references instead of copies.
192///
193/// Memory-mapped stores deliberately do **not** implement this: their
194/// links are decoded from raw memory on read, so there is no stable
195/// `&GenericLink<T>` to borrow.
196pub trait LinksStorageRef<T: LinkReference>: LinksStorage<T> {
197    /// Borrows the link stored at `index`.
198    fn get_link_ref(&self, index: T) -> Option<&GenericLink<T>>;
199
200    /// Borrows every link in the store.
201    fn all_link_refs(&self) -> Vec<&GenericLink<T>>;
202
203    /// Borrows every link matching the (optional) index/source/target pattern.
204    fn query_link_refs(
205        &self,
206        index: Option<T>,
207        source: Option<T>,
208        target: Option<T>,
209    ) -> Vec<&GenericLink<T>> {
210        self.all_link_refs()
211            .into_iter()
212            .filter(|link| {
213                index.is_none_or(|value| value == link.index)
214                    && source.is_none_or(|value| value == link.source)
215                    && target.is_none_or(|value| value == link.target)
216            })
217            .collect()
218    }
219}