mago_database/
change.rs

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