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 /// Returns every link in the store.
84 fn all_links(&self) -> Vec<GenericLink<T>>;
85
86 /// Returns every link matching the (optional) index/source/target pattern.
87 fn query_links(
88 &self,
89 index: Option<T>,
90 source: Option<T>,
91 target: Option<T>,
92 ) -> Vec<GenericLink<T>> {
93 self.all_links()
94 .into_iter()
95 .filter(|link| {
96 index.is_none_or(|value| value == link.index)
97 && source.is_none_or(|value| value == link.source)
98 && target.is_none_or(|value| value == link.target)
99 })
100 .collect()
101 }
102
103 /// Finds the address of a link with the given source and target.
104 fn search_link(&self, source: T, target: T) -> Option<T> {
105 self.all_links()
106 .into_iter()
107 .find(|link| link.source == source && link.target == target)
108 .map(|link| link.index)
109 }
110
111 /// Returns the address of an existing `(source, target)` link,
112 /// creating it when it does not exist yet.
113 fn get_or_create_link(&mut self, source: T, target: T) -> Result<T, LinkError> {
114 match self.search_link(source, target) {
115 Some(index) => Ok(index),
116 None => self.create_link(source, target),
117 }
118 }
119
120 /// Number of links currently stored.
121 fn links_count(&self) -> usize {
122 self.all_links().len()
123 }
124
125 /// Makes every write durable on disk.
126 ///
127 /// Implementations that keep state in memory write it out here;
128 /// memory-mapped implementations `fsync` the backing file. Callers
129 /// that need crash-consistency guarantees must call this — see the
130 /// durability notes on each implementation.
131 fn flush(&mut self) -> Result<(), LinkError>;
132
133 /// Cheap check for "did another process write to this database since
134 /// we last read or wrote it?".
135 ///
136 /// The default implementation reports `false`, which is correct for
137 /// stores that are not shared through the filesystem.
138 fn has_external_changes(&self) -> Result<bool, LinkError> {
139 Ok(false)
140 }
141
142 /// Re-reads the database from disk, discarding cached state.
143 ///
144 /// The default implementation is a no-op for stores that always read
145 /// through to their backing storage.
146 fn reload(&mut self) -> Result<(), LinkError> {
147 Ok(())
148 }
149}
150
151/// Extension implemented by stores that keep links resident in memory and
152/// can therefore lend out references instead of copies.
153///
154/// Memory-mapped stores deliberately do **not** implement this: their
155/// links are decoded from raw memory on read, so there is no stable
156/// `&GenericLink<T>` to borrow.
157pub trait LinksStorageRef<T: LinkReference>: LinksStorage<T> {
158 /// Borrows the link stored at `index`.
159 fn get_link_ref(&self, index: T) -> Option<&GenericLink<T>>;
160
161 /// Borrows every link in the store.
162 fn all_link_refs(&self) -> Vec<&GenericLink<T>>;
163
164 /// Borrows every link matching the (optional) index/source/target pattern.
165 fn query_link_refs(
166 &self,
167 index: Option<T>,
168 source: Option<T>,
169 target: Option<T>,
170 ) -> Vec<&GenericLink<T>> {
171 self.all_link_refs()
172 .into_iter()
173 .filter(|link| {
174 index.is_none_or(|value| value == link.index)
175 && source.is_none_or(|value| value == link.source)
176 && target.is_none_or(|value| value == link.target)
177 })
178 .collect()
179 }
180}