Skip to main content

turtle_build/infrastructure/database/
redb.rs

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