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)]
29pub struct ChangeLog {
30 pub(crate) changes: Arc<Mutex<Vec<Change>>>,
31}
32
33impl Default for ChangeLog {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl ChangeLog {
40 /// Creates a new, empty `ChangeLog`.
41 pub fn new() -> Self {
42 Self { changes: Arc::new(Mutex::new(Vec::new())) }
43 }
44
45 /// Records a request to add a new file.
46 ///
47 /// # Errors
48 ///
49 /// Returns a `DatabaseError::PoisonedLogMutex` if another thread panicked
50 /// while holding the lock, leaving the change log in an unusable state.
51 pub fn add(&self, file: File) -> Result<(), DatabaseError> {
52 self.changes.lock().map_err(|_| DatabaseError::PoisonedLogMutex)?.push(Change::Add(file));
53 Ok(())
54 }
55
56 /// Records a request to update an existing file's content by its `FileId`.
57 ///
58 /// # Errors
59 ///
60 /// Returns a `DatabaseError::PoisonedLogMutex` if another thread panicked
61 /// while holding the lock, leaving the change log in an unusable state.
62 pub fn update(&self, id: FileId, new_contents: Cow<'static, str>) -> Result<(), DatabaseError> {
63 self.changes.lock().map_err(|_| DatabaseError::PoisonedLogMutex)?.push(Change::Update(id, new_contents));
64 Ok(())
65 }
66
67 /// Records a request to delete a file by its `FileId`.
68 ///
69 /// # Errors
70 ///
71 /// Returns a `DatabaseError::PoisonedLogMutex` if another thread panicked
72 /// while holding the lock, leaving the change log in an unusable state.
73 pub fn delete(&self, id: FileId) -> Result<(), DatabaseError> {
74 self.changes.lock().map_err(|_| DatabaseError::PoisonedLogMutex)?.push(Change::Delete(id));
75 Ok(())
76 }
77
78 /// Consumes the change log and returns the vector of collected changes.
79 ///
80 /// This operation safely unwraps the underlying list of changes. It will
81 /// only succeed if called on the last remaining reference to the change log,
82 /// which guarantees that no other threads can be modifying the list.
83 ///
84 /// # Errors
85 ///
86 /// - `DatabaseError::ChangeLogInUse`: Returned if other `Arc` references to this change log still exist.
87 /// - `DatabaseError::PoisonedLogMutex`: Returned if the internal lock was poisoned because another thread panicked while holding it.
88 pub fn into_inner(self) -> Result<Vec<Change>, DatabaseError> {
89 match Arc::try_unwrap(self.changes) {
90 Ok(mutex) => mutex.into_inner().map_err(|_| DatabaseError::PoisonedLogMutex),
91 Err(_) => Err(DatabaseError::ChangeLogInUse),
92 }
93 }
94}