Skip to main content

turtle_build/infrastructure/database/
fjall.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 core::str;
10use fjall::Keyspace;
11use rkyv::{Archived, access, from_bytes, rancor, to_bytes};
12
13const HASH_TAG: u8 = 0;
14const HEADER_DEPENDENCY_TAG: u8 = 1;
15const OUTPUT_TAG: u8 = 2;
16
17/// A Fjall database.
18pub struct FjallDatabase {
19    keyspace: Keyspace,
20    path_pool: Arc<PathPool>,
21}
22
23impl FjallDatabase {
24    /// Creates a database.
25    pub const fn new(keyspace: Keyspace, path_pool: Arc<PathPool>) -> Self {
26        Self {
27            keyspace,
28            path_pool,
29        }
30    }
31
32    fn insert(&self, key: &[u8], value: &[u8]) -> Result<(), DatabaseError> {
33        self.keyspace.insert(key, value)?;
34
35        Ok(())
36    }
37}
38
39fn key(tag: u8, payload: &[u8]) -> Vec<u8> {
40    [[tag].as_slice(), payload].concat()
41}
42
43#[async_trait]
44impl Database for FjallDatabase {
45    async fn get_hash(&self, id: BuildId) -> Result<Option<BuildHash>, DatabaseError> {
46        Ok(self
47            .keyspace
48            .get(key(HASH_TAG, &id.to_bytes()))?
49            .map(|value| {
50                from_bytes::<(u64, u64), rancor::Error>(&value)
51                    .map(|(timestamp, content)| BuildHash::new(timestamp, content))
52            })
53            .transpose()?)
54    }
55
56    async fn set_hash(&self, id: BuildId, hash: BuildHash) -> Result<(), DatabaseError> {
57        self.insert(
58            &key(HASH_TAG, &id.to_bytes()),
59            &to_bytes::<rancor::Error>(&(hash.timestamp(), hash.content()))?,
60        )
61    }
62
63    async fn get_header_inputs(&self, id: BuildId) -> Result<Vec<Arc<str>>, DatabaseError> {
64        Ok(self
65            .keyspace
66            .get(key(HEADER_DEPENDENCY_TAG, &id.to_bytes()))?
67            .map(|value| {
68                access::<Archived<Vec<Arc<str>>>, rancor::Error>(&value).map(|dependencies| {
69                    dependencies
70                        .iter()
71                        .map(|path| self.path_pool.intern(path))
72                        .collect()
73                })
74            })
75            .transpose()?
76            .unwrap_or_default())
77    }
78
79    async fn set_header_inputs(
80        &self,
81        id: BuildId,
82        dependencies: &[Arc<str>],
83    ) -> Result<(), DatabaseError> {
84        self.insert(
85            &key(HEADER_DEPENDENCY_TAG, &id.to_bytes()),
86            &to_bytes::<rancor::Error>(&dependencies.to_vec())?,
87        )
88    }
89
90    async fn get_outputs(&self) -> Result<Vec<String>, DatabaseError> {
91        self.keyspace
92            .prefix([OUTPUT_TAG])
93            .map(|guard| Ok(str::from_utf8(&guard.key()?[1..])?.into()))
94            .collect::<Result<_, _>>()
95    }
96
97    async fn set_output(&self, path: &str, source: Option<&str>) -> Result<(), DatabaseError> {
98        self.insert(
99            &key(OUTPUT_TAG, path.as_bytes()),
100            &to_bytes::<rancor::Error>(&source.map(String::from))?,
101        )
102    }
103
104    async fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
105        Ok(self
106            .keyspace
107            .get(key(OUTPUT_TAG, output.as_bytes()))?
108            .map(|value| from_bytes::<Option<String>, rancor::Error>(&value))
109            .transpose()?
110            .flatten())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use fjall::KeyspaceCreateOptions;
118    use std::path::Path;
119    use tempfile::tempdir;
120
121    fn open(path: &Path) -> (FjallDatabase, fjall::Database) {
122        let database = fjall::Database::builder(path).open().unwrap();
123
124        (
125            FjallDatabase::new(
126                database
127                    .keyspace("build", KeyspaceCreateOptions::default)
128                    .unwrap(),
129                Default::default(),
130            ),
131            database,
132        )
133    }
134
135    #[test]
136    fn new() {
137        open(tempdir().unwrap().path());
138    }
139
140    #[tokio::test]
141    async fn hash() {
142        let (database, _fjall) = open(tempdir().unwrap().path());
143
144        database
145            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
146            .await
147            .unwrap();
148
149        assert_eq!(
150            database.get_hash(BuildId::new(0)).await.unwrap(),
151            Some(BuildHash::new(1, 2))
152        );
153    }
154
155    #[tokio::test]
156    async fn get_no_hash() {
157        let (database, _fjall) = open(tempdir().unwrap().path());
158
159        database
160            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
161            .await
162            .unwrap();
163
164        assert_eq!(database.get_hash(BuildId::new(1)).await.unwrap(), None);
165    }
166
167    #[tokio::test]
168    async fn update_hash() {
169        let (database, _fjall) = open(tempdir().unwrap().path());
170
171        database
172            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
173            .await
174            .unwrap();
175        database
176            .set_hash(BuildId::new(0), BuildHash::new(3, 4))
177            .await
178            .unwrap();
179
180        assert_eq!(
181            database.get_hash(BuildId::new(0)).await.unwrap(),
182            Some(BuildHash::new(3, 4))
183        );
184    }
185
186    #[tokio::test]
187    async fn fail_to_get_invalid_hash() {
188        let (database, _fjall) = open(tempdir().unwrap().path());
189
190        database
191            .insert(&key(HASH_TAG, &BuildId::new(0).to_bytes()), &[0])
192            .unwrap();
193
194        assert!(database.get_hash(BuildId::new(0)).await.is_err());
195    }
196
197    #[tokio::test]
198    async fn set_output() {
199        let (database, _fjall) = open(tempdir().unwrap().path());
200
201        database.set_output("foo", None).await.unwrap();
202    }
203
204    #[tokio::test]
205    async fn get_output() {
206        let (database, _fjall) = open(tempdir().unwrap().path());
207
208        database.set_output("foo", None).await.unwrap();
209
210        assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
211    }
212
213    #[tokio::test]
214    async fn get_output_with_source() {
215        let (database, _fjall) = open(tempdir().unwrap().path());
216
217        database.set_output("foo", Some("bar")).await.unwrap();
218
219        assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
220    }
221
222    #[tokio::test]
223    async fn header_dependencies() {
224        let (database, _fjall) = open(tempdir().unwrap().path());
225
226        database
227            .set_header_inputs(BuildId::new(0), &["foo".into(), "bar".into()])
228            .await
229            .unwrap();
230
231        assert_eq!(
232            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
233            vec!["foo".into(), "bar".into()]
234        );
235    }
236
237    #[tokio::test]
238    async fn empty_header_dependencies() {
239        let (database, _fjall) = open(tempdir().unwrap().path());
240
241        database
242            .set_header_inputs(BuildId::new(0), &[])
243            .await
244            .unwrap();
245
246        assert_eq!(
247            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
248            Vec::<Arc<str>>::new()
249        );
250    }
251
252    #[tokio::test]
253    async fn long_header_dependency() {
254        let (database, _fjall) = open(tempdir().unwrap().path());
255
256        database
257            .set_header_inputs(BuildId::new(0), &["foo/bar/baz/qux.h".into()])
258            .await
259            .unwrap();
260
261        assert_eq!(
262            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
263            vec!["foo/bar/baz/qux.h".into()]
264        );
265    }
266
267    #[tokio::test]
268    async fn duplicate_header_dependencies() {
269        let (database, _fjall) = open(tempdir().unwrap().path());
270        let path = Arc::<str>::from("foo");
271
272        database
273            .set_header_inputs(BuildId::new(0), &[path.clone(), path])
274            .await
275            .unwrap();
276
277        assert_eq!(
278            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
279            vec!["foo".into(), "foo".into()]
280        );
281    }
282
283    #[tokio::test]
284    async fn intern_header_dependencies() {
285        let directory = tempdir().unwrap();
286        let path_pool = Arc::new(PathPool::new());
287        let database = FjallDatabase::new(
288            fjall::Database::builder(directory.path())
289                .open()
290                .unwrap()
291                .keyspace("build", KeyspaceCreateOptions::default)
292                .unwrap(),
293            path_pool.clone(),
294        );
295
296        database
297            .set_header_inputs(BuildId::new(0), &["foo".into()])
298            .await
299            .unwrap();
300
301        assert!(Arc::ptr_eq(
302            &database.get_header_inputs(BuildId::new(0)).await.unwrap()[0],
303            &path_pool.intern("foo")
304        ));
305    }
306
307    #[tokio::test]
308    async fn fail_to_get_invalid_header_dependencies() {
309        let (database, _fjall) = open(tempdir().unwrap().path());
310
311        database
312            .insert(
313                &key(HEADER_DEPENDENCY_TAG, &BuildId::new(0).to_bytes()),
314                &[0xff; 8],
315            )
316            .unwrap();
317
318        assert!(database.get_header_inputs(BuildId::new(0)).await.is_err());
319    }
320
321    #[tokio::test]
322    async fn get_source() {
323        let (database, _fjall) = open(tempdir().unwrap().path());
324
325        database.set_output("foo", Some("bar")).await.unwrap();
326
327        assert_eq!(
328            database.get_source("foo").await.unwrap(),
329            Some("bar".into())
330        );
331    }
332
333    #[tokio::test]
334    async fn get_no_source() {
335        let (database, _fjall) = open(tempdir().unwrap().path());
336
337        database.set_output("foo", None).await.unwrap();
338
339        assert_eq!(database.get_source("foo").await.unwrap(), None);
340    }
341
342    #[tokio::test]
343    async fn update_source() {
344        let (database, _fjall) = open(tempdir().unwrap().path());
345
346        database.set_output("foo", Some("bar")).await.unwrap();
347        database.set_output("foo", Some("baz")).await.unwrap();
348
349        assert_eq!(
350            database.get_source("foo").await.unwrap(),
351            Some("baz".into())
352        );
353    }
354
355    #[tokio::test]
356    async fn remove_source() {
357        let (database, _fjall) = open(tempdir().unwrap().path());
358
359        database.set_output("foo", Some("bar")).await.unwrap();
360        database.set_output("foo", None).await.unwrap();
361
362        assert_eq!(database.get_source("foo").await.unwrap(), None);
363    }
364
365    #[tokio::test]
366    async fn fail_to_get_invalid_source() {
367        let (database, _fjall) = open(tempdir().unwrap().path());
368
369        database.insert(&key(OUTPUT_TAG, b"foo"), &[0]).unwrap();
370
371        assert!(database.get_source("foo").await.is_err());
372    }
373
374    #[tokio::test]
375    async fn reopen() {
376        let directory = tempdir().unwrap();
377
378        let (database, fjall) = open(directory.path());
379
380        database
381            .set_hash(BuildId::new(0), BuildHash::new(1, 2))
382            .await
383            .unwrap();
384        database
385            .set_header_inputs(BuildId::new(0), &["foo".into()])
386            .await
387            .unwrap();
388        database.set_output("foo", Some("bar")).await.unwrap();
389
390        drop(database);
391        drop(fjall);
392
393        let (database, _fjall) = open(directory.path());
394
395        assert_eq!(
396            database.get_hash(BuildId::new(0)).await.unwrap(),
397            Some(BuildHash::new(1, 2))
398        );
399        assert_eq!(
400            database.get_header_inputs(BuildId::new(0)).await.unwrap(),
401            vec!["foo".into()]
402        );
403        assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
404        assert_eq!(
405            database.get_source("foo").await.unwrap(),
406            Some("bar".into())
407        );
408    }
409}