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}