Skip to main content

turtle_build/infrastructure/database/
fjall.rs

1use crate::{
2    hash_type::HashType,
3    infrastructure::{Database, DatabaseError},
4    ir::BuildId,
5    path_pool::PathPool,
6};
7use alloc::sync::Arc;
8use core::str;
9use fjall::Keyspace;
10
11const TIMESTAMP_HASH_TAG: u8 = 0;
12const CONTENT_HASH_TAG: u8 = 1;
13const HEADER_DEPENDENCY_TAG: u8 = 2;
14const OUTPUT_TAG: u8 = 3;
15const SOURCE_TAG: u8 = 4;
16
17const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
18
19/// A Fjall database.
20pub struct FjallDatabase {
21    keyspace: Keyspace,
22    path_pool: Arc<PathPool>,
23}
24
25impl FjallDatabase {
26    /// Creates a database.
27    pub const fn new(keyspace: Keyspace, path_pool: Arc<PathPool>) -> Self {
28        Self {
29            keyspace,
30            path_pool,
31        }
32    }
33
34    fn insert(&self, key: &[u8], value: &[u8]) -> Result<(), DatabaseError> {
35        self.keyspace.insert(key, value)?;
36
37        Ok(())
38    }
39}
40
41const fn hash_tag(r#type: HashType) -> u8 {
42    match r#type {
43        HashType::Content => CONTENT_HASH_TAG,
44        HashType::Timestamp => TIMESTAMP_HASH_TAG,
45    }
46}
47
48fn key(tag: u8, payload: &[u8]) -> Vec<u8> {
49    [[tag].as_slice(), payload].concat()
50}
51
52impl Database for FjallDatabase {
53    fn get_hash(&self, r#type: HashType, id: BuildId) -> Result<Option<u64>, DatabaseError> {
54        Ok(self
55            .keyspace
56            .get(key(hash_tag(r#type), &id.to_bytes()))?
57            .map(|value| bincode::decode_from_slice(&value, BINCODE_CONFIG).map(|(value, _)| value))
58            .transpose()?)
59    }
60
61    fn set_hash(&self, r#type: HashType, id: BuildId, hash: u64) -> Result<(), DatabaseError> {
62        self.insert(
63            &key(hash_tag(r#type), &id.to_bytes()),
64            &bincode::encode_to_vec(hash, BINCODE_CONFIG)?,
65        )
66    }
67
68    fn get_header_dependencies(&self, id: BuildId) -> Result<Vec<Arc<str>>, DatabaseError> {
69        Ok(self
70            .keyspace
71            .get(key(HEADER_DEPENDENCY_TAG, &id.to_bytes()))?
72            .map(|value| {
73                bincode::borrow_decode_from_slice::<Vec<&str>, _>(&value, BINCODE_CONFIG).map(
74                    |(dependencies, _)| {
75                        dependencies
76                            .into_iter()
77                            .map(|path| self.path_pool.intern(path))
78                            .collect()
79                    },
80                )
81            })
82            .transpose()?
83            .unwrap_or_default())
84    }
85
86    fn set_header_dependencies(
87        &self,
88        id: BuildId,
89        dependencies: &[Arc<str>],
90    ) -> Result<(), DatabaseError> {
91        self.insert(
92            &key(HEADER_DEPENDENCY_TAG, &id.to_bytes()),
93            &bincode::encode_to_vec(dependencies, BINCODE_CONFIG)?,
94        )
95    }
96
97    fn get_outputs(&self) -> Result<Vec<String>, DatabaseError> {
98        self.keyspace
99            .prefix([OUTPUT_TAG])
100            .map(|guard| Ok(str::from_utf8(&guard.key()?[1..])?.into()))
101            .collect::<Result<_, _>>()
102    }
103
104    fn set_output(&self, path: &str) -> Result<(), DatabaseError> {
105        self.insert(&key(OUTPUT_TAG, path.as_bytes()), &[])
106    }
107
108    fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
109        self.keyspace
110            .get(key(SOURCE_TAG, output.as_bytes()))?
111            .map(|source| Ok::<_, DatabaseError>(str::from_utf8(&source)?.into()))
112            .transpose()
113    }
114
115    fn set_source(&self, output: &str, source: &str) -> Result<(), DatabaseError> {
116        self.insert(&key(SOURCE_TAG, output.as_bytes()), source.as_bytes())
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use fjall::KeyspaceCreateOptions;
124    use std::path::Path;
125    use tempfile::tempdir;
126
127    fn open(path: &Path) -> (FjallDatabase, fjall::Database) {
128        let database = fjall::Database::builder(path).open().unwrap();
129
130        (
131            FjallDatabase::new(
132                database
133                    .keyspace("build", KeyspaceCreateOptions::default)
134                    .unwrap(),
135                Default::default(),
136            ),
137            database,
138        )
139    }
140
141    #[test]
142    fn new() {
143        open(tempdir().unwrap().path());
144    }
145
146    #[test]
147    fn timestamp_hash() {
148        let (database, _fjall) = open(tempdir().unwrap().path());
149
150        database
151            .set_hash(HashType::Timestamp, BuildId::new(0), 42)
152            .unwrap();
153
154        assert_eq!(
155            database
156                .get_hash(HashType::Timestamp, BuildId::new(0))
157                .unwrap(),
158            Some(42)
159        );
160        assert_eq!(
161            database
162                .get_hash(HashType::Content, BuildId::new(0))
163                .unwrap(),
164            None,
165        );
166    }
167
168    #[test]
169    fn content_hash() {
170        let (database, _fjall) = open(tempdir().unwrap().path());
171
172        database
173            .set_hash(HashType::Content, BuildId::new(0), 42)
174            .unwrap();
175
176        assert_eq!(
177            database
178                .get_hash(HashType::Content, BuildId::new(0))
179                .unwrap(),
180            Some(42)
181        );
182        assert_eq!(
183            database
184                .get_hash(HashType::Timestamp, BuildId::new(0))
185                .unwrap(),
186            None,
187        );
188    }
189
190    #[test]
191    fn set_output() {
192        let (database, _fjall) = open(tempdir().unwrap().path());
193
194        database.set_output("foo").unwrap();
195    }
196
197    #[test]
198    fn get_output() {
199        let (database, _fjall) = open(tempdir().unwrap().path());
200
201        database.set_output("foo").unwrap();
202
203        assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
204    }
205
206    #[test]
207    fn get_output_with_source() {
208        let (database, _fjall) = open(tempdir().unwrap().path());
209
210        database.set_output("foo").unwrap();
211        database.set_source("foo", "bar").unwrap();
212
213        assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
214    }
215
216    #[test]
217    fn header_dependencies() {
218        let (database, _fjall) = open(tempdir().unwrap().path());
219
220        database
221            .set_header_dependencies(BuildId::new(0), &["foo".into(), "bar".into()])
222            .unwrap();
223
224        assert_eq!(
225            database.get_header_dependencies(BuildId::new(0)).unwrap(),
226            vec!["foo".into(), "bar".into()]
227        );
228    }
229
230    #[test]
231    fn intern_header_dependencies() {
232        let directory = tempdir().unwrap();
233        let path_pool = Arc::new(PathPool::new());
234        let database = FjallDatabase::new(
235            fjall::Database::builder(directory.path())
236                .open()
237                .unwrap()
238                .keyspace("build", KeyspaceCreateOptions::default)
239                .unwrap(),
240            path_pool.clone(),
241        );
242
243        database
244            .set_header_dependencies(BuildId::new(0), &["foo".into()])
245            .unwrap();
246
247        assert!(Arc::ptr_eq(
248            &database.get_header_dependencies(BuildId::new(0)).unwrap()[0],
249            &path_pool.intern("foo")
250        ));
251    }
252
253    #[test]
254    fn set_source() {
255        let (database, _fjall) = open(tempdir().unwrap().path());
256
257        database.set_source("foo", "bar").unwrap();
258    }
259
260    #[test]
261    fn get_source() {
262        let (database, _fjall) = open(tempdir().unwrap().path());
263
264        database.set_source("foo", "bar").unwrap();
265
266        assert_eq!(database.get_source("foo").unwrap(), Some("bar".into()));
267    }
268
269    #[test]
270    fn reopen() {
271        let directory = tempdir().unwrap();
272
273        let (database, fjall) = open(directory.path());
274
275        database
276            .set_hash(HashType::Timestamp, BuildId::new(0), 42)
277            .unwrap();
278        database
279            .set_header_dependencies(BuildId::new(0), &["foo".into()])
280            .unwrap();
281        database.set_output("foo").unwrap();
282        database.set_source("foo", "bar").unwrap();
283
284        drop(database);
285        drop(fjall);
286
287        let (database, _fjall) = open(directory.path());
288
289        assert_eq!(
290            database
291                .get_hash(HashType::Timestamp, BuildId::new(0))
292                .unwrap(),
293            Some(42)
294        );
295        assert_eq!(
296            database.get_header_dependencies(BuildId::new(0)).unwrap(),
297            vec!["foo".into()]
298        );
299        assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
300        assert_eq!(database.get_source("foo").unwrap(), Some("bar".into()));
301    }
302}