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}