Skip to main content

turtle_build/infrastructure/database/
fjall.rs

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