Skip to main content

turtle_build/infrastructure/database/
fjall.rs

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