turtle_build/infrastructure/database/
fjall.rs1use crate::{
2 build_hash::BuildHash,
3 infrastructure::{Database, DatabaseError},
4 ir::BuildId,
5 path_pool::PathPool,
6};
7use alloc::sync::Arc;
8use async_trait::async_trait;
9use core::str;
10use fjall::Keyspace;
11use rkyv::{Archived, access, from_bytes, rancor, to_bytes};
12
13const HASH_TAG: u8 = 0;
14const HEADER_DEPENDENCY_TAG: u8 = 1;
15const OUTPUT_TAG: u8 = 2;
16
17pub struct FjallDatabase {
19 keyspace: Keyspace,
20 path_pool: Arc<PathPool>,
21}
22
23impl FjallDatabase {
24 pub const fn new(keyspace: Keyspace, path_pool: Arc<PathPool>) -> Self {
26 Self {
27 keyspace,
28 path_pool,
29 }
30 }
31
32 fn insert(&self, key: &[u8], value: &[u8]) -> Result<(), DatabaseError> {
33 self.keyspace.insert(key, value)?;
34
35 Ok(())
36 }
37}
38
39fn key(tag: u8, payload: &[u8]) -> Vec<u8> {
40 [[tag].as_slice(), payload].concat()
41}
42
43#[async_trait]
44impl Database for FjallDatabase {
45 async fn get_hash(&self, id: BuildId) -> Result<Option<BuildHash>, DatabaseError> {
46 Ok(self
47 .keyspace
48 .get(key(HASH_TAG, &id.to_bytes()))?
49 .map(|value| {
50 from_bytes::<(u64, u64), rancor::Error>(&value)
51 .map(|(timestamp, content)| BuildHash::new(timestamp, content))
52 })
53 .transpose()?)
54 }
55
56 async fn set_hash(&self, id: BuildId, hash: BuildHash) -> Result<(), DatabaseError> {
57 self.insert(
58 &key(HASH_TAG, &id.to_bytes()),
59 &to_bytes::<rancor::Error>(&(hash.timestamp(), hash.content()))?,
60 )
61 }
62
63 async fn get_header_inputs(&self, id: BuildId) -> Result<Vec<Arc<str>>, DatabaseError> {
64 Ok(self
65 .keyspace
66 .get(key(HEADER_DEPENDENCY_TAG, &id.to_bytes()))?
67 .map(|value| {
68 access::<Archived<Vec<Arc<str>>>, rancor::Error>(&value).map(|dependencies| {
69 dependencies
70 .iter()
71 .map(|path| self.path_pool.intern(path))
72 .collect()
73 })
74 })
75 .transpose()?
76 .unwrap_or_default())
77 }
78
79 async fn set_header_inputs(
80 &self,
81 id: BuildId,
82 dependencies: &[Arc<str>],
83 ) -> Result<(), DatabaseError> {
84 self.insert(
85 &key(HEADER_DEPENDENCY_TAG, &id.to_bytes()),
86 &to_bytes::<rancor::Error>(&dependencies.to_vec())?,
87 )
88 }
89
90 async fn get_outputs(&self) -> Result<Vec<String>, DatabaseError> {
91 self.keyspace
92 .prefix([OUTPUT_TAG])
93 .map(|guard| Ok(str::from_utf8(&guard.key()?[1..])?.into()))
94 .collect::<Result<_, _>>()
95 }
96
97 async fn set_output(&self, path: &str, source: Option<&str>) -> Result<(), DatabaseError> {
98 self.insert(
99 &key(OUTPUT_TAG, path.as_bytes()),
100 &to_bytes::<rancor::Error>(&source.map(String::from))?,
101 )
102 }
103
104 async fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
105 Ok(self
106 .keyspace
107 .get(key(OUTPUT_TAG, output.as_bytes()))?
108 .map(|value| from_bytes::<Option<String>, rancor::Error>(&value))
109 .transpose()?
110 .flatten())
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use fjall::KeyspaceCreateOptions;
118 use std::path::Path;
119 use tempfile::tempdir;
120
121 fn open(path: &Path) -> (FjallDatabase, fjall::Database) {
122 let database = fjall::Database::builder(path).open().unwrap();
123
124 (
125 FjallDatabase::new(
126 database
127 .keyspace("build", KeyspaceCreateOptions::default)
128 .unwrap(),
129 Default::default(),
130 ),
131 database,
132 )
133 }
134
135 #[test]
136 fn new() {
137 open(tempdir().unwrap().path());
138 }
139
140 #[tokio::test]
141 async fn hash() {
142 let (database, _fjall) = open(tempdir().unwrap().path());
143
144 database
145 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
146 .await
147 .unwrap();
148
149 assert_eq!(
150 database.get_hash(BuildId::new(0)).await.unwrap(),
151 Some(BuildHash::new(1, 2))
152 );
153 }
154
155 #[tokio::test]
156 async fn get_no_hash() {
157 let (database, _fjall) = open(tempdir().unwrap().path());
158
159 database
160 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
161 .await
162 .unwrap();
163
164 assert_eq!(database.get_hash(BuildId::new(1)).await.unwrap(), None);
165 }
166
167 #[tokio::test]
168 async fn update_hash() {
169 let (database, _fjall) = open(tempdir().unwrap().path());
170
171 database
172 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
173 .await
174 .unwrap();
175 database
176 .set_hash(BuildId::new(0), BuildHash::new(3, 4))
177 .await
178 .unwrap();
179
180 assert_eq!(
181 database.get_hash(BuildId::new(0)).await.unwrap(),
182 Some(BuildHash::new(3, 4))
183 );
184 }
185
186 #[tokio::test]
187 async fn fail_to_get_invalid_hash() {
188 let (database, _fjall) = open(tempdir().unwrap().path());
189
190 database
191 .insert(&key(HASH_TAG, &BuildId::new(0).to_bytes()), &[0])
192 .unwrap();
193
194 assert!(database.get_hash(BuildId::new(0)).await.is_err());
195 }
196
197 #[tokio::test]
198 async fn set_output() {
199 let (database, _fjall) = open(tempdir().unwrap().path());
200
201 database.set_output("foo", None).await.unwrap();
202 }
203
204 #[tokio::test]
205 async fn get_output() {
206 let (database, _fjall) = open(tempdir().unwrap().path());
207
208 database.set_output("foo", None).await.unwrap();
209
210 assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
211 }
212
213 #[tokio::test]
214 async fn get_output_with_source() {
215 let (database, _fjall) = open(tempdir().unwrap().path());
216
217 database.set_output("foo", Some("bar")).await.unwrap();
218
219 assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
220 }
221
222 #[tokio::test]
223 async fn header_dependencies() {
224 let (database, _fjall) = open(tempdir().unwrap().path());
225
226 database
227 .set_header_inputs(BuildId::new(0), &["foo".into(), "bar".into()])
228 .await
229 .unwrap();
230
231 assert_eq!(
232 database.get_header_inputs(BuildId::new(0)).await.unwrap(),
233 vec!["foo".into(), "bar".into()]
234 );
235 }
236
237 #[tokio::test]
238 async fn empty_header_dependencies() {
239 let (database, _fjall) = open(tempdir().unwrap().path());
240
241 database
242 .set_header_inputs(BuildId::new(0), &[])
243 .await
244 .unwrap();
245
246 assert_eq!(
247 database.get_header_inputs(BuildId::new(0)).await.unwrap(),
248 Vec::<Arc<str>>::new()
249 );
250 }
251
252 #[tokio::test]
253 async fn long_header_dependency() {
254 let (database, _fjall) = open(tempdir().unwrap().path());
255
256 database
257 .set_header_inputs(BuildId::new(0), &["foo/bar/baz/qux.h".into()])
258 .await
259 .unwrap();
260
261 assert_eq!(
262 database.get_header_inputs(BuildId::new(0)).await.unwrap(),
263 vec!["foo/bar/baz/qux.h".into()]
264 );
265 }
266
267 #[tokio::test]
268 async fn duplicate_header_dependencies() {
269 let (database, _fjall) = open(tempdir().unwrap().path());
270 let path = Arc::<str>::from("foo");
271
272 database
273 .set_header_inputs(BuildId::new(0), &[path.clone(), path])
274 .await
275 .unwrap();
276
277 assert_eq!(
278 database.get_header_inputs(BuildId::new(0)).await.unwrap(),
279 vec!["foo".into(), "foo".into()]
280 );
281 }
282
283 #[tokio::test]
284 async fn intern_header_dependencies() {
285 let directory = tempdir().unwrap();
286 let path_pool = Arc::new(PathPool::new());
287 let database = FjallDatabase::new(
288 fjall::Database::builder(directory.path())
289 .open()
290 .unwrap()
291 .keyspace("build", KeyspaceCreateOptions::default)
292 .unwrap(),
293 path_pool.clone(),
294 );
295
296 database
297 .set_header_inputs(BuildId::new(0), &["foo".into()])
298 .await
299 .unwrap();
300
301 assert!(Arc::ptr_eq(
302 &database.get_header_inputs(BuildId::new(0)).await.unwrap()[0],
303 &path_pool.intern("foo")
304 ));
305 }
306
307 #[tokio::test]
308 async fn fail_to_get_invalid_header_dependencies() {
309 let (database, _fjall) = open(tempdir().unwrap().path());
310
311 database
312 .insert(
313 &key(HEADER_DEPENDENCY_TAG, &BuildId::new(0).to_bytes()),
314 &[0xff; 8],
315 )
316 .unwrap();
317
318 assert!(database.get_header_inputs(BuildId::new(0)).await.is_err());
319 }
320
321 #[tokio::test]
322 async fn get_source() {
323 let (database, _fjall) = open(tempdir().unwrap().path());
324
325 database.set_output("foo", Some("bar")).await.unwrap();
326
327 assert_eq!(
328 database.get_source("foo").await.unwrap(),
329 Some("bar".into())
330 );
331 }
332
333 #[tokio::test]
334 async fn get_no_source() {
335 let (database, _fjall) = open(tempdir().unwrap().path());
336
337 database.set_output("foo", None).await.unwrap();
338
339 assert_eq!(database.get_source("foo").await.unwrap(), None);
340 }
341
342 #[tokio::test]
343 async fn update_source() {
344 let (database, _fjall) = open(tempdir().unwrap().path());
345
346 database.set_output("foo", Some("bar")).await.unwrap();
347 database.set_output("foo", Some("baz")).await.unwrap();
348
349 assert_eq!(
350 database.get_source("foo").await.unwrap(),
351 Some("baz".into())
352 );
353 }
354
355 #[tokio::test]
356 async fn remove_source() {
357 let (database, _fjall) = open(tempdir().unwrap().path());
358
359 database.set_output("foo", Some("bar")).await.unwrap();
360 database.set_output("foo", None).await.unwrap();
361
362 assert_eq!(database.get_source("foo").await.unwrap(), None);
363 }
364
365 #[tokio::test]
366 async fn fail_to_get_invalid_source() {
367 let (database, _fjall) = open(tempdir().unwrap().path());
368
369 database.insert(&key(OUTPUT_TAG, b"foo"), &[0]).unwrap();
370
371 assert!(database.get_source("foo").await.is_err());
372 }
373
374 #[tokio::test]
375 async fn reopen() {
376 let directory = tempdir().unwrap();
377
378 let (database, fjall) = open(directory.path());
379
380 database
381 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
382 .await
383 .unwrap();
384 database
385 .set_header_inputs(BuildId::new(0), &["foo".into()])
386 .await
387 .unwrap();
388 database.set_output("foo", Some("bar")).await.unwrap();
389
390 drop(database);
391 drop(fjall);
392
393 let (database, _fjall) = open(directory.path());
394
395 assert_eq!(
396 database.get_hash(BuildId::new(0)).await.unwrap(),
397 Some(BuildHash::new(1, 2))
398 );
399 assert_eq!(
400 database.get_header_inputs(BuildId::new(0)).await.unwrap(),
401 vec!["foo".into()]
402 );
403 assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
404 assert_eq!(
405 database.get_source("foo").await.unwrap(),
406 Some("bar".into())
407 );
408 }
409}