Skip to main content

mago_database/
change.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3use std::sync::Mutex;
4
5use crate::error::DatabaseError;
6use crate::file::File;
7use crate::file::FileId;
8
9/// Represents a single, deferred database operation.
10///
11/// An instruction to be applied to a `Database` as part of a [`ChangeLog`].
12#[derive(Debug)]
13pub enum Change {
14    /// An instruction to add a new file.
15    Add(File),
16    /// An instruction to update an existing file, identified by its `FileId`.
17    Update(FileId, Cow<'static, str>),
18    /// An instruction to delete an existing file, identified by its `FileId`.
19    Delete(FileId),
20}
21
22/// A thread-safe, cloneable transaction log for collecting database operations.
23///
24/// This struct acts as a "Unit of Work," allowing multiple threads to concurrently
25/// record operations without directly mutating the `Database`. The collected changes
26/// can then be applied later in a single batch operation. This pattern avoids lock
27/// contention on the main database during processing.
28#[derive(Clone, Debug)]
29#[allow(clippy::field_scoped_visibility_modifiers)]
30pub struct ChangeLog {
31    pub(crate) changes: Arc<Mutex<Vec<Change>>>,
32}
33
34impl Default for ChangeLog {
35    #[inline]
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl ChangeLog {
42    /// Creates a new, empty `ChangeLog`.
43    #[inline]
44    #[must_use]
45    pub fn new() -> Self {
46        Self { changes: Arc::new(Mutex::new(Vec::new())) }
47    }
48
49    /// Records a request to add a new file.
50    ///
51    /// # Errors
52    ///
53    /// Returns a `DatabaseError::PoisonedLogMutex` if another thread panicked
54    /// while holding the lock, leaving the change log in an unusable state.
55    #[inline]
56    pub fn add(&self, file: File) -> Result<(), DatabaseError> {
57        self.changes.lock().map_err(|_| DatabaseError::PoisonedLogMutex)?.push(Change::Add(file));
58        Ok(())
59    }
60
61    /// Records a request to update an existing file's content by its `FileId`.
62    ///
63    /// # Errors
64    ///
65    /// Returns a `DatabaseError::PoisonedLogMutex` if another thread panicked
66    /// while holding the lock, leaving the change log in an unusable state.
67    #[inline]
68    pub fn update(&self, id: FileId, new_contents: Cow<'static, str>) -> Result<(), DatabaseError> {
69        self.changes.lock().map_err(|_| DatabaseError::PoisonedLogMutex)?.push(Change::Update(id, new_contents));
70        Ok(())
71    }
72
73    /// Records a request to delete a file by its `FileId`.
74    ///
75    /// # Errors
76    ///
77    /// Returns a `DatabaseError::PoisonedLogMutex` if another thread panicked
78    /// while holding the lock, leaving the change log in an unusable state.
79    #[inline]
80    pub fn delete(&self, id: FileId) -> Result<(), DatabaseError> {
81        self.changes.lock().map_err(|_| DatabaseError::PoisonedLogMutex)?.push(Change::Delete(id));
82        Ok(())
83    }
84
85    /// Returns a vector of all `FileId`s affected by the recorded changes.
86    ///
87    /// This includes IDs for files being added, updated, or deleted.
88    ///
89    /// # Errors
90    ///
91    /// - `DatabaseError::PoisonedLogMutex`: Returned if the internal lock was poisoned
92    #[inline]
93    pub fn changed_file_ids(&self) -> Result<Vec<FileId>, DatabaseError> {
94        let changes = self.changes.lock().map_err(|_| DatabaseError::PoisonedLogMutex)?;
95        let mut ids = Vec::new();
96        for change in &*changes {
97            match change {
98                Change::Add(file) => ids.push(file.id),
99                Change::Update(id, _) => ids.push(*id),
100                Change::Delete(id) => ids.push(*id),
101            }
102        }
103
104        Ok(ids)
105    }
106
107    /// Consumes the change log and returns the vector of collected changes.
108    ///
109    /// This operation safely unwraps the underlying list of changes. It will
110    /// only succeed if called on the last remaining reference to the change log,
111    /// which guarantees that no other threads can be modifying the list.
112    ///
113    /// # Errors
114    ///
115    /// - `DatabaseError::ChangeLogInUse`: Returned if other `Arc` references to this change log still exist.
116    /// - `DatabaseError::PoisonedLogMutex`: Returned if the internal lock was poisoned because another thread panicked while holding it.
117    #[inline]
118    pub fn into_inner(self) -> Result<Vec<Change>, DatabaseError> {
119        match Arc::try_unwrap(self.changes) {
120            Ok(mutex) => mutex.into_inner().map_err(|_| DatabaseError::PoisonedLogMutex),
121            Err(_) => Err(DatabaseError::ChangeLogInUse),
122        }
123    }
124}