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