turtle_build/infrastructure/database/
redb.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 redb::{
10 Durability, Key, ReadOnlyTable, ReadableDatabase, ReadableTable, TableDefinition, Value,
11};
12use std::{fs::create_dir_all, path::Path};
13
14const HASHES: TableDefinition<[u8; 8], (u64, u64)> = TableDefinition::new("hashes");
15const HEADER_DEPENDENCIES: TableDefinition<[u8; 8], Vec<&str>> =
16 TableDefinition::new("header_dependencies");
17const OUTPUTS: TableDefinition<&str, Option<&str>> = TableDefinition::new("outputs");
18
19pub struct RedbDatabase {
21 database: redb::Database,
22 path_pool: Arc<PathPool>,
23}
24
25impl RedbDatabase {
26 pub fn new(path: &Path, path_pool: Arc<PathPool>) -> Result<Self, DatabaseError> {
28 Ok(Self {
29 database: Self::open(path)?,
30 path_pool,
31 })
32 }
33
34 fn open(path: &Path) -> Result<redb::Database, redb::Error> {
35 if let Some(directory) = path.parent() {
36 create_dir_all(directory)?;
37 }
38
39 let database = redb::Database::create(path)?;
40 let mut transaction = database.begin_write()?;
41
42 transaction.set_durability(Durability::None)?;
43 transaction.open_table(HASHES)?;
44 transaction.open_table(HEADER_DEPENDENCIES)?;
45 transaction.open_table(OUTPUTS)?;
46 transaction.commit()?;
47
48 Ok(database)
49 }
50
51 fn read<K: Key + 'static, V: Value + 'static, T>(
52 &self,
53 table: TableDefinition<K, V>,
54 read: impl FnOnce(ReadOnlyTable<K, V>) -> Result<T, redb::Error>,
55 ) -> Result<T, redb::Error> {
56 read(self.database.begin_read()?.open_table(table)?)
57 }
58
59 fn write<K: Key + 'static, V: Value + 'static>(
61 &self,
62 table: TableDefinition<K, V>,
63 key: K::SelfType<'_>,
64 value: V::SelfType<'_>,
65 ) -> Result<(), redb::Error> {
66 let mut transaction = self.database.begin_write()?;
67
68 transaction.set_durability(Durability::None)?;
69 transaction.open_table(table)?.insert(key, value)?;
70 transaction.commit()?;
71
72 Ok(())
73 }
74}
75
76#[async_trait]
77impl Database for RedbDatabase {
78 async fn get_hash(&self, id: BuildId) -> Result<Option<BuildHash>, DatabaseError> {
79 Ok(self.read(HASHES, |table| {
80 Ok(table
81 .get(id.to_bytes())?
82 .map(|hash| hash.value())
83 .map(|(timestamp, content)| BuildHash::new(timestamp, content)))
84 })?)
85 }
86
87 async fn set_hash(&self, id: BuildId, hash: BuildHash) -> Result<(), DatabaseError> {
88 Ok(self.write(HASHES, id.to_bytes(), (hash.timestamp(), hash.content()))?)
89 }
90
91 async fn get_header_inputs(&self, id: BuildId) -> Result<Vec<Arc<str>>, DatabaseError> {
92 Ok(self.read(HEADER_DEPENDENCIES, |table| {
93 Ok(table
94 .get(id.to_bytes())?
95 .map(|dependencies| {
96 dependencies
97 .value()
98 .into_iter()
99 .map(|path| self.path_pool.intern(path))
100 .collect()
101 })
102 .unwrap_or_default())
103 })?)
104 }
105
106 async fn set_header_inputs(
107 &self,
108 id: BuildId,
109 dependencies: &[Arc<str>],
110 ) -> Result<(), DatabaseError> {
111 Ok(self.write(
112 HEADER_DEPENDENCIES,
113 id.to_bytes(),
114 dependencies.iter().map(AsRef::as_ref).collect(),
115 )?)
116 }
117
118 async fn get_outputs(&self) -> Result<Vec<String>, DatabaseError> {
119 Ok(self.read(OUTPUTS, |table| {
120 table
121 .iter()?
122 .map(|entry| Ok(entry?.0.value().into()))
123 .collect()
124 })?)
125 }
126
127 async fn set_output(&self, path: &str, source: Option<&str>) -> Result<(), DatabaseError> {
128 Ok(self.write(OUTPUTS, path, source)?)
129 }
130
131 async fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
132 Ok(self.read(OUTPUTS, |table| {
133 Ok(table
134 .get(output)?
135 .and_then(|source| source.value().map(Into::into)))
136 })?)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use tempfile::{TempDir, tempdir};
144
145 const FILENAME: &str = "database";
146
147 fn open() -> (RedbDatabase, TempDir) {
148 let directory = tempdir().unwrap();
149
150 (
151 RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap(),
152 directory,
153 )
154 }
155
156 #[test]
157 fn new() {
158 open();
159 }
160
161 #[test]
162 fn new_in_missing_directory() {
163 let directory = tempdir().unwrap();
164
165 RedbDatabase::new(
166 &directory.path().join("foo").join("bar").join(FILENAME),
167 Default::default(),
168 )
169 .unwrap();
170 }
171
172 #[tokio::test]
173 async fn hash() {
174 let (database, _directory) = open();
175
176 database
177 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
178 .await
179 .unwrap();
180
181 assert_eq!(
182 database.get_hash(BuildId::new(0)).await.unwrap(),
183 Some(BuildHash::new(1, 2))
184 );
185 }
186
187 #[tokio::test]
188 async fn get_no_hash() {
189 let (database, _directory) = open();
190
191 database
192 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
193 .await
194 .unwrap();
195
196 assert_eq!(database.get_hash(BuildId::new(1)).await.unwrap(), None);
197 }
198
199 #[tokio::test]
200 async fn update_hash() {
201 let (database, _directory) = open();
202
203 database
204 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
205 .await
206 .unwrap();
207 database
208 .set_hash(BuildId::new(0), BuildHash::new(3, 4))
209 .await
210 .unwrap();
211
212 assert_eq!(
213 database.get_hash(BuildId::new(0)).await.unwrap(),
214 Some(BuildHash::new(3, 4))
215 );
216 }
217
218 #[tokio::test]
219 async fn set_output() {
220 let (database, _directory) = open();
221
222 database.set_output("foo", None).await.unwrap();
223 }
224
225 #[tokio::test]
226 async fn get_output() {
227 let (database, _directory) = open();
228
229 database.set_output("foo", None).await.unwrap();
230
231 assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
232 }
233
234 #[tokio::test]
235 async fn get_no_output() {
236 let (database, _directory) = open();
237
238 assert_eq!(database.get_outputs().await.unwrap(), Vec::<String>::new());
239 }
240
241 #[tokio::test]
242 async fn get_output_with_source() {
243 let (database, _directory) = open();
244
245 database.set_output("foo", Some("bar")).await.unwrap();
246
247 assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
248 }
249
250 #[tokio::test]
251 async fn header_dependencies() {
252 let (database, _directory) = open();
253
254 database
255 .set_header_inputs(BuildId::new(0), &["foo".into(), "bar".into()])
256 .await
257 .unwrap();
258
259 assert_eq!(
260 database.get_header_inputs(BuildId::new(0)).await.unwrap(),
261 vec!["foo".into(), "bar".into()]
262 );
263 }
264
265 #[tokio::test]
266 async fn get_no_header_dependencies() {
267 let (database, _directory) = open();
268
269 assert_eq!(
270 database.get_header_inputs(BuildId::new(0)).await.unwrap(),
271 Vec::<Arc<str>>::new()
272 );
273 }
274
275 #[tokio::test]
276 async fn intern_header_dependencies() {
277 let directory = tempdir().unwrap();
278 let path_pool = Arc::new(PathPool::new());
279 let database =
280 RedbDatabase::new(&directory.path().join(FILENAME), path_pool.clone()).unwrap();
281
282 database
283 .set_header_inputs(BuildId::new(0), &["foo".into()])
284 .await
285 .unwrap();
286
287 assert!(Arc::ptr_eq(
288 &database.get_header_inputs(BuildId::new(0)).await.unwrap()[0],
289 &path_pool.intern("foo")
290 ));
291 }
292
293 #[tokio::test]
294 async fn get_source() {
295 let (database, _directory) = open();
296
297 database.set_output("foo", Some("bar")).await.unwrap();
298
299 assert_eq!(
300 database.get_source("foo").await.unwrap(),
301 Some("bar".into())
302 );
303 }
304
305 #[tokio::test]
306 async fn get_no_source() {
307 let (database, _directory) = open();
308
309 database.set_output("foo", None).await.unwrap();
310
311 assert_eq!(database.get_source("foo").await.unwrap(), None);
312 }
313
314 #[tokio::test]
315 async fn get_no_source_of_missing_output() {
316 let (database, _directory) = open();
317
318 assert_eq!(database.get_source("foo").await.unwrap(), None);
319 }
320
321 #[tokio::test]
322 async fn update_source() {
323 let (database, _directory) = open();
324
325 database.set_output("foo", Some("bar")).await.unwrap();
326 database.set_output("foo", Some("baz")).await.unwrap();
327
328 assert_eq!(
329 database.get_source("foo").await.unwrap(),
330 Some("baz".into())
331 );
332 }
333
334 #[tokio::test]
335 async fn remove_source() {
336 let (database, _directory) = open();
337
338 database.set_output("foo", Some("bar")).await.unwrap();
339 database.set_output("foo", None).await.unwrap();
340
341 assert_eq!(database.get_source("foo").await.unwrap(), None);
342 }
343
344 #[tokio::test]
345 async fn reopen() {
346 let (database, directory) = open();
347
348 database
349 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
350 .await
351 .unwrap();
352 database
353 .set_header_inputs(BuildId::new(0), &["foo".into()])
354 .await
355 .unwrap();
356 database.set_output("foo", Some("bar")).await.unwrap();
357
358 drop(database);
359
360 let database =
361 RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap();
362
363 assert_eq!(
364 database.get_hash(BuildId::new(0)).await.unwrap(),
365 Some(BuildHash::new(1, 2))
366 );
367 assert_eq!(
368 database.get_header_inputs(BuildId::new(0)).await.unwrap(),
369 vec!["foo".into()]
370 );
371 assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
372 assert_eq!(
373 database.get_source("foo").await.unwrap(),
374 Some("bar".into())
375 );
376 }
377}