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