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