Skip to main content

turtle_build/infrastructure/database/
redb.rs

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