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    /// 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}