turtle_build/infrastructure/database/
fjall.rs1use 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#[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 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}