1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use rusqlite::DropBehavior;
use crate::{Filesystem, RootId, settings::Settings, sql::SqlStore};
/// The behavior of a SQLite transaction.
///
/// See the [SQLite documentation](https://sqlite.org/lang_transaction.html) for more information.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransactionBehavior {
/// `DEFERRED` means that the transaction does not actually start until the database is first
/// accessed.
Deferred,
/// `IMMEDIATE` cause the database connection to start a new write immediately, without waiting
/// for a writes statement.
Immediate,
/// `EXCLUSIVE` prevents other database connections from reading the database while the
/// transaction is underway.
Exclusive,
}
#[doc(hidden)]
impl From<TransactionBehavior> for rusqlite::TransactionBehavior {
fn from(value: TransactionBehavior) -> Self {
match value {
TransactionBehavior::Deferred => rusqlite::TransactionBehavior::Deferred,
TransactionBehavior::Immediate => rusqlite::TransactionBehavior::Immediate,
TransactionBehavior::Exclusive => rusqlite::TransactionBehavior::Exclusive,
}
}
}
/// An open transaction on a [`Filesystem`].
///
/// If a [`Transaction`] is dropped without committing, the transaction is rolled back.
#[derive(Debug)]
pub struct Transaction<'conn> {
tx: rusqlite::Transaction<'conn>,
settings: Settings,
default_root_id: RootId,
changes_at_start: u64,
}
impl<'conn> Transaction<'conn> {
pub(super) fn new(
tx: rusqlite::Transaction<'conn>,
settings: Settings,
default_root_id: RootId,
) -> Self {
let changes_at_start = tx.total_changes();
Self {
tx,
settings,
default_root_id,
changes_at_start,
}
}
/// Delete orphaned blocks.
///
/// We don't need to do this on every write, because data is only freed up on disk when the
/// transaction commits anyways. As a performance optimization, we can defer this until right
/// before we commit the transaction.
fn clean(&self) -> crate::Result<()> {
let mut stmt = self.tx.prepare_cached(
r#"
DELETE FROM
liteboxfs_blocks
WHERE
id NOT IN (SELECT block FROM liteboxfs_file_blocks);
"#,
)?;
stmt.execute([])?;
Ok(())
}
/// Execute the given function within this transaction.
///
/// This calls the given function, passing a [`Filesystem`] holding this transaction. If the
/// function returns [`Ok`], this transaction is committed. If the function returns [`Err`],
/// this transaction is rolled back.
pub fn exec<T, E, F>(mut self, f: F) -> Result<T, E>
where
F: FnOnce(&mut Filesystem) -> Result<T, E>,
E: From<crate::Error>,
{
let result = {
let mut savepoint = self.tx.savepoint().map_err(crate::Error::from)?;
savepoint.set_drop_behavior(DropBehavior::Commit);
let store = SqlStore::new(savepoint, self.settings.clone());
let mut fs = Filesystem::new(self.default_root_id, self.settings.clone(), store);
f(&mut fs)
}?;
if self.tx.total_changes() != self.changes_at_start {
self.clean()?;
}
self.tx.commit().map_err(crate::Error::from)?;
Ok(result)
}
/// Get the [`Filesystem`] holding this transaction.
pub fn fs(&mut self) -> crate::Result<Filesystem<'_>> {
let mut savepoint = self.tx.savepoint().map_err(crate::Error::from)?;
savepoint.set_drop_behavior(DropBehavior::Commit);
let store = SqlStore::new(savepoint, self.settings.clone());
Ok(Filesystem::new(
self.default_root_id,
self.settings.clone(),
store,
))
}
/// Roll back this transaction.
pub fn rollback(self) -> crate::Result<()> {
Ok(self.tx.rollback()?)
}
/// Commit this transaction.
pub fn commit(self) -> crate::Result<()> {
if self.tx.total_changes() != self.changes_at_start {
self.clean()?;
}
Ok(self.tx.commit()?)
}
}