Skip to main content

turtle_build/infrastructure/database/
redb.rs

1use crate::{
2    build_hash::BuildHash,
3    infrastructure::{Database, DatabaseError},
4    ir::BuildId,
5    path_pool::PathPool,
6};
7use alloc::sync::Arc;
8use redb::{
9    Durability, Key, ReadOnlyTable, ReadableDatabase, ReadableTable, TableDefinition, Value,
10};
11use std::{fs::create_dir_all, path::Path};
12
13const HASHES: TableDefinition<[u8; 8], (u64, u64)> = TableDefinition::new("hashes");
14const HEADER_DEPENDENCIES: TableDefinition<[u8; 8], Vec<&str>> =
15    TableDefinition::new("header_dependencies");
16const OUTPUTS: TableDefinition<&str, ()> = TableDefinition::new("outputs");
17const SOURCES: TableDefinition<&str, &str> = TableDefinition::new("sources");
18
19/// A redb database.
20pub struct RedbDatabase {
21    database: redb::Database,
22    path_pool: Arc<PathPool>,
23}
24
25impl RedbDatabase {
26    /// Creates a database.
27    pub fn new(path: &Path, path_pool: Arc<PathPool>) -> Result<Self, DatabaseError> {
28        Ok(Self {
29            database: Self::open(path)?,
30            path_pool,
31        })
32    }
33
34    fn open(path: &Path) -> Result<redb::Database, redb::Error> {
35        if let Some(directory) = path.parent() {
36            create_dir_all(directory)?;
37        }
38
39        let database = redb::Database::create(path)?;
40        let mut transaction = database.begin_write()?;
41
42        transaction.set_durability(Durability::None)?;
43        transaction.open_table(HASHES)?;
44        transaction.open_table(HEADER_DEPENDENCIES)?;
45        transaction.open_table(OUTPUTS)?;
46        transaction.open_table(SOURCES)?;
47        transaction.commit()?;
48
49        Ok(database)
50    }
51
52    fn read<K: Key + 'static, V: Value + 'static, T>(
53        &self,
54        table: TableDefinition<K, V>,
55        read: impl FnOnce(ReadOnlyTable<K, V>) -> Result<T, redb::Error>,
56    ) -> Result<T, redb::Error> {
57        read(self.database.begin_read()?.open_table(table)?)
58    }
59
60    // Writes become durable when the database is closed.
61    fn write<K: Key + 'static, V: Value + 'static>(
62        &self,
63        table: TableDefinition<K, V>,
64        key: K::SelfType<'_>,
65        value: V::SelfType<'_>,
66    ) -> Result<(), redb::Error> {
67        let mut transaction = self.database.begin_write()?;
68
69        transaction.set_durability(Durability::None)?;
70        transaction.open_table(table)?.insert(key, value)?;
71        transaction.commit()?;
72
73        Ok(())
74    }
75}
76
77impl Database for RedbDatabase {
78    fn get_hash(&self, id: BuildId) -> Result<Option<BuildHash>, DatabaseError> {
79        Ok(self.read(HASHES, |table| {
80            Ok(table
81                .get(id.to_bytes())?
82                .map(|hash| hash.value())
83                .map(|(timestamp, content)| BuildHash::new(timestamp, content)))
84        })?)
85    }
86
87    fn set_hash(&self, id: BuildId, hash: BuildHash) -> Result<(), DatabaseError> {
88        Ok(self.write(HASHES, id.to_bytes(), (hash.timestamp(), hash.content()))?)
89    }
90
91    fn get_header_inputs(&self, id: BuildId) -> Result<Vec<Arc<str>>, DatabaseError> {
92        Ok(self.read(HEADER_DEPENDENCIES, |table| {
93            Ok(table
94                .get(id.to_bytes())?
95                .map(|dependencies| {
96                    dependencies
97                        .value()
98                        .into_iter()
99                        .map(|path| self.path_pool.intern(path))
100                        .collect()
101                })
102                .unwrap_or_default())
103        })?)
104    }
105
106    fn set_header_inputs(
107        &self,
108        id: BuildId,
109        dependencies: &[Arc<str>],
110    ) -> Result<(), DatabaseError> {
111        Ok(self.write(
112            HEADER_DEPENDENCIES,
113            id.to_bytes(),
114            dependencies.iter().map(AsRef::as_ref).collect(),
115        )?)
116    }
117
118    fn get_outputs(&self) -> Result<Vec<String>, DatabaseError> {
119        Ok(self.read(OUTPUTS, |table| {
120            table
121                .iter()?
122                .map(|entry| Ok(entry?.0.value().into()))
123                .collect()
124        })?)
125    }
126
127    fn set_output(&self, path: &str) -> Result<(), DatabaseError> {
128        Ok(self.write(OUTPUTS, path, ())?)
129    }
130
131    fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
132        Ok(self.read(SOURCES, |table| {
133            Ok(table.get(output)?.map(|source| source.value().into()))
134        })?)
135    }
136
137    fn set_source(&self, output: &str, source: &str) -> Result<(), DatabaseError> {
138        Ok(self.write(SOURCES, output, source)?)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use tempfile::{TempDir, tempdir};
146
147    const FILENAME: &str = "database";
148
149    fn open() -> (RedbDatabase, TempDir) {
150        let directory = tempdir().unwrap();
151
152        (
153            RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap(),
154            directory,
155        )
156    }
157
158    #[test]
159    fn new() {
160        open();
161    }
162
163    #[test]
164    fn new_in_missing_directory() {
165        let directory = tempdir().unwrap();
166
167        RedbDatabase::new(
168            &directory.path().join("foo").join("bar").join(FILENAME),
169            Default::default(),
170        )
171        .unwrap();
172    }
173
174    #[test]
175    fn hash() {
176        let (database, _directory) = open();
177
178        database
179            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
180            .unwrap();
181
182        assert_eq!(
183            database.get_hash(BuildId::new(0)).unwrap(),
184            Some(BuildHash::new(1, 2))
185        );
186    }
187
188    #[test]
189    fn get_no_hash() {
190        let (database, _directory) = open();
191
192        database
193            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
194            .unwrap();
195
196        assert_eq!(database.get_hash(BuildId::new(1)).unwrap(), None);
197    }
198
199    #[test]
200    fn update_hash() {
201        let (database, _directory) = open();
202
203        database
204            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
205            .unwrap();
206        database
207            .set_hash(BuildId::new(0), BuildHash::new(3, 4))
208            .unwrap();
209
210        assert_eq!(
211            database.get_hash(BuildId::new(0)).unwrap(),
212            Some(BuildHash::new(3, 4))
213        );
214    }
215
216    #[test]
217    fn set_output() {
218        let (database, _directory) = open();
219
220        database.set_output("foo").unwrap();
221    }
222
223    #[test]
224    fn get_output() {
225        let (database, _directory) = open();
226
227        database.set_output("foo").unwrap();
228
229        assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
230    }
231
232    #[test]
233    fn get_no_output() {
234        let (database, _directory) = open();
235
236        assert_eq!(database.get_outputs().unwrap(), Vec::<String>::new());
237    }
238
239    #[test]
240    fn get_output_with_source() {
241        let (database, _directory) = open();
242
243        database.set_output("foo").unwrap();
244        database.set_source("foo", "bar").unwrap();
245
246        assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
247    }
248
249    #[test]
250    fn header_dependencies() {
251        let (database, _directory) = open();
252
253        database
254            .set_header_inputs(BuildId::new(0), &["foo".into(), "bar".into()])
255            .unwrap();
256
257        assert_eq!(
258            database.get_header_inputs(BuildId::new(0)).unwrap(),
259            vec!["foo".into(), "bar".into()]
260        );
261    }
262
263    #[test]
264    fn get_no_header_dependencies() {
265        let (database, _directory) = open();
266
267        assert_eq!(
268            database.get_header_inputs(BuildId::new(0)).unwrap(),
269            Vec::<Arc<str>>::new()
270        );
271    }
272
273    #[test]
274    fn intern_header_dependencies() {
275        let directory = tempdir().unwrap();
276        let path_pool = Arc::new(PathPool::new());
277        let database =
278            RedbDatabase::new(&directory.path().join(FILENAME), path_pool.clone()).unwrap();
279
280        database
281            .set_header_inputs(BuildId::new(0), &["foo".into()])
282            .unwrap();
283
284        assert!(Arc::ptr_eq(
285            &database.get_header_inputs(BuildId::new(0)).unwrap()[0],
286            &path_pool.intern("foo")
287        ));
288    }
289
290    #[test]
291    fn set_source() {
292        let (database, _directory) = open();
293
294        database.set_source("foo", "bar").unwrap();
295    }
296
297    #[test]
298    fn get_source() {
299        let (database, _directory) = open();
300
301        database.set_source("foo", "bar").unwrap();
302
303        assert_eq!(database.get_source("foo").unwrap(), Some("bar".into()));
304    }
305
306    #[test]
307    fn get_no_source() {
308        let (database, _directory) = open();
309
310        assert_eq!(database.get_source("foo").unwrap(), None);
311    }
312
313    #[test]
314    fn reopen() {
315        let (database, directory) = open();
316
317        database
318            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
319            .unwrap();
320        database
321            .set_header_inputs(BuildId::new(0), &["foo".into()])
322            .unwrap();
323        database.set_output("foo").unwrap();
324        database.set_source("foo", "bar").unwrap();
325
326        drop(database);
327
328        let database =
329            RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap();
330
331        assert_eq!(
332            database.get_hash(BuildId::new(0)).unwrap(),
333            Some(BuildHash::new(1, 2))
334        );
335        assert_eq!(
336            database.get_header_inputs(BuildId::new(0)).unwrap(),
337            vec!["foo".into()]
338        );
339        assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
340        assert_eq!(database.get_source("foo").unwrap(), Some("bar".into()));
341    }
342}