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