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