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 async_trait::async_trait;
9use redb::{
10    Durability, Key, ReadOnlyTable, ReadableDatabase, ReadableTable, TableDefinition, Value,
11};
12use std::{fs::create_dir_all, path::Path};
13
14const HASHES: TableDefinition<[u8; 8], (u64, u64)> = TableDefinition::new("hashes");
15const HEADER_DEPENDENCIES: TableDefinition<[u8; 8], Vec<&str>> =
16    TableDefinition::new("header_dependencies");
17const OUTPUTS: TableDefinition<&str, Option<&str>> = TableDefinition::new("outputs");
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.commit()?;
47
48        Ok(database)
49    }
50
51    fn read<K: Key + 'static, V: Value + 'static, T>(
52        &self,
53        table: TableDefinition<K, V>,
54        read: impl FnOnce(ReadOnlyTable<K, V>) -> Result<T, redb::Error>,
55    ) -> Result<T, redb::Error> {
56        read(self.database.begin_read()?.open_table(table)?)
57    }
58
59    // Writes become durable when the database is closed.
60    fn write<K: Key + 'static, V: Value + 'static>(
61        &self,
62        table: TableDefinition<K, V>,
63        key: K::SelfType<'_>,
64        value: V::SelfType<'_>,
65    ) -> Result<(), redb::Error> {
66        let mut transaction = self.database.begin_write()?;
67
68        transaction.set_durability(Durability::None)?;
69        transaction.open_table(table)?.insert(key, value)?;
70        transaction.commit()?;
71
72        Ok(())
73    }
74}
75
76#[async_trait]
77impl Database for RedbDatabase {
78    async 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    async 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    async 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    async 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    async 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    async fn set_output(&self, path: &str, source: Option<&str>) -> Result<(), DatabaseError> {
128        Ok(self.write(OUTPUTS, path, source)?)
129    }
130
131    async fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
132        Ok(self.read(OUTPUTS, |table| {
133            Ok(table
134                .get(output)?
135                .and_then(|source| source.value().map(Into::into)))
136        })?)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use tempfile::{TempDir, tempdir};
144
145    const FILENAME: &str = "database";
146
147    fn open() -> (RedbDatabase, TempDir) {
148        let directory = tempdir().unwrap();
149
150        (
151            RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap(),
152            directory,
153        )
154    }
155
156    #[test]
157    fn new() {
158        open();
159    }
160
161    #[test]
162    fn new_in_missing_directory() {
163        let directory = tempdir().unwrap();
164
165        RedbDatabase::new(
166            &directory.path().join("foo").join("bar").join(FILENAME),
167            Default::default(),
168        )
169        .unwrap();
170    }
171
172    #[tokio::test]
173    async fn hash() {
174        let (database, _directory) = open();
175
176        database
177            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
178            .await
179            .unwrap();
180
181        assert_eq!(
182            database.get_hash(BuildId::new(0)).await.unwrap(),
183            Some(BuildHash::new(1, 2))
184        );
185    }
186
187    #[tokio::test]
188    async fn get_no_hash() {
189        let (database, _directory) = open();
190
191        database
192            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
193            .await
194            .unwrap();
195
196        assert_eq!(database.get_hash(BuildId::new(1)).await.unwrap(), None);
197    }
198
199    #[tokio::test]
200    async fn update_hash() {
201        let (database, _directory) = open();
202
203        database
204            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
205            .await
206            .unwrap();
207        database
208            .set_hash(BuildId::new(0), BuildHash::new(3, 4))
209            .await
210            .unwrap();
211
212        assert_eq!(
213            database.get_hash(BuildId::new(0)).await.unwrap(),
214            Some(BuildHash::new(3, 4))
215        );
216    }
217
218    #[tokio::test]
219    async fn set_output() {
220        let (database, _directory) = open();
221
222        database.set_output("foo", None).await.unwrap();
223    }
224
225    #[tokio::test]
226    async fn get_output() {
227        let (database, _directory) = open();
228
229        database.set_output("foo", None).await.unwrap();
230
231        assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
232    }
233
234    #[tokio::test]
235    async fn get_no_output() {
236        let (database, _directory) = open();
237
238        assert_eq!(database.get_outputs().await.unwrap(), Vec::<String>::new());
239    }
240
241    #[tokio::test]
242    async fn get_output_with_source() {
243        let (database, _directory) = open();
244
245        database.set_output("foo", Some("bar")).await.unwrap();
246
247        assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
248    }
249
250    #[tokio::test]
251    async fn header_dependencies() {
252        let (database, _directory) = open();
253
254        database
255            .set_header_inputs(BuildId::new(0), &["foo".into(), "bar".into()])
256            .await
257            .unwrap();
258
259        assert_eq!(
260            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
261            vec!["foo".into(), "bar".into()]
262        );
263    }
264
265    #[tokio::test]
266    async fn get_no_header_dependencies() {
267        let (database, _directory) = open();
268
269        assert_eq!(
270            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
271            Vec::<Arc<str>>::new()
272        );
273    }
274
275    #[tokio::test]
276    async fn intern_header_dependencies() {
277        let directory = tempdir().unwrap();
278        let path_pool = Arc::new(PathPool::new());
279        let database =
280            RedbDatabase::new(&directory.path().join(FILENAME), path_pool.clone()).unwrap();
281
282        database
283            .set_header_inputs(BuildId::new(0), &["foo".into()])
284            .await
285            .unwrap();
286
287        assert!(Arc::ptr_eq(
288            &database.get_header_inputs(BuildId::new(0)).await.unwrap()[0],
289            &path_pool.intern("foo")
290        ));
291    }
292
293    #[tokio::test]
294    async fn get_source() {
295        let (database, _directory) = open();
296
297        database.set_output("foo", Some("bar")).await.unwrap();
298
299        assert_eq!(
300            database.get_source("foo").await.unwrap(),
301            Some("bar".into())
302        );
303    }
304
305    #[tokio::test]
306    async fn get_no_source() {
307        let (database, _directory) = open();
308
309        database.set_output("foo", None).await.unwrap();
310
311        assert_eq!(database.get_source("foo").await.unwrap(), None);
312    }
313
314    #[tokio::test]
315    async fn get_no_source_of_missing_output() {
316        let (database, _directory) = open();
317
318        assert_eq!(database.get_source("foo").await.unwrap(), None);
319    }
320
321    #[tokio::test]
322    async fn update_source() {
323        let (database, _directory) = open();
324
325        database.set_output("foo", Some("bar")).await.unwrap();
326        database.set_output("foo", Some("baz")).await.unwrap();
327
328        assert_eq!(
329            database.get_source("foo").await.unwrap(),
330            Some("baz".into())
331        );
332    }
333
334    #[tokio::test]
335    async fn remove_source() {
336        let (database, _directory) = open();
337
338        database.set_output("foo", Some("bar")).await.unwrap();
339        database.set_output("foo", None).await.unwrap();
340
341        assert_eq!(database.get_source("foo").await.unwrap(), None);
342    }
343
344    #[tokio::test]
345    async fn reopen() {
346        let (database, directory) = open();
347
348        database
349            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
350            .await
351            .unwrap();
352        database
353            .set_header_inputs(BuildId::new(0), &["foo".into()])
354            .await
355            .unwrap();
356        database.set_output("foo", Some("bar")).await.unwrap();
357
358        drop(database);
359
360        let database =
361            RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap();
362
363        assert_eq!(
364            database.get_hash(BuildId::new(0)).await.unwrap(),
365            Some(BuildHash::new(1, 2))
366        );
367        assert_eq!(
368            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
369            vec!["foo".into()]
370        );
371        assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
372        assert_eq!(
373            database.get_source("foo").await.unwrap(),
374            Some("bar".into())
375        );
376    }
377}