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 redb::{
9 Durability, Key, ReadOnlyTable, ReadableDatabase, ReadableTable, TableDefinition, Value,
10};
11use std::{fs::create_dir_all, path::Path};
12
13const HASHES: TableDefinition<[u8; 8], (u64, u64)> = TableDefinition::new("hashes");
14const HEADER_DEPENDENCIES: TableDefinition<[u8; 8], Vec<&str>> =
15 TableDefinition::new("header_dependencies");
16const OUTPUTS: TableDefinition<&str, ()> = TableDefinition::new("outputs");
17const SOURCES: TableDefinition<&str, &str> = TableDefinition::new("sources");
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.open_table(SOURCES)?;
47 transaction.commit()?;
48
49 Ok(database)
50 }
51
52 fn read<K: Key + 'static, V: Value + 'static, T>(
53 &self,
54 table: TableDefinition<K, V>,
55 read: impl FnOnce(ReadOnlyTable<K, V>) -> Result<T, redb::Error>,
56 ) -> Result<T, redb::Error> {
57 read(self.database.begin_read()?.open_table(table)?)
58 }
59
60 fn write<K: Key + 'static, V: Value + 'static>(
62 &self,
63 table: TableDefinition<K, V>,
64 key: K::SelfType<'_>,
65 value: V::SelfType<'_>,
66 ) -> Result<(), redb::Error> {
67 let mut transaction = self.database.begin_write()?;
68
69 transaction.set_durability(Durability::None)?;
70 transaction.open_table(table)?.insert(key, value)?;
71 transaction.commit()?;
72
73 Ok(())
74 }
75}
76
77impl Database for RedbDatabase {
78 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 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 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 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 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 fn set_output(&self, path: &str) -> Result<(), DatabaseError> {
128 Ok(self.write(OUTPUTS, path, ())?)
129 }
130
131 fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
132 Ok(self.read(SOURCES, |table| {
133 Ok(table.get(output)?.map(|source| source.value().into()))
134 })?)
135 }
136
137 fn set_source(&self, output: &str, source: &str) -> Result<(), DatabaseError> {
138 Ok(self.write(SOURCES, output, source)?)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use tempfile::{TempDir, tempdir};
146
147 const FILENAME: &str = "database";
148
149 fn open() -> (RedbDatabase, TempDir) {
150 let directory = tempdir().unwrap();
151
152 (
153 RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap(),
154 directory,
155 )
156 }
157
158 #[test]
159 fn new() {
160 open();
161 }
162
163 #[test]
164 fn new_in_missing_directory() {
165 let directory = tempdir().unwrap();
166
167 RedbDatabase::new(
168 &directory.path().join("foo").join("bar").join(FILENAME),
169 Default::default(),
170 )
171 .unwrap();
172 }
173
174 #[test]
175 fn hash() {
176 let (database, _directory) = open();
177
178 database
179 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
180 .unwrap();
181
182 assert_eq!(
183 database.get_hash(BuildId::new(0)).unwrap(),
184 Some(BuildHash::new(1, 2))
185 );
186 }
187
188 #[test]
189 fn get_no_hash() {
190 let (database, _directory) = open();
191
192 database
193 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
194 .unwrap();
195
196 assert_eq!(database.get_hash(BuildId::new(1)).unwrap(), None);
197 }
198
199 #[test]
200 fn update_hash() {
201 let (database, _directory) = open();
202
203 database
204 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
205 .unwrap();
206 database
207 .set_hash(BuildId::new(0), BuildHash::new(3, 4))
208 .unwrap();
209
210 assert_eq!(
211 database.get_hash(BuildId::new(0)).unwrap(),
212 Some(BuildHash::new(3, 4))
213 );
214 }
215
216 #[test]
217 fn set_output() {
218 let (database, _directory) = open();
219
220 database.set_output("foo").unwrap();
221 }
222
223 #[test]
224 fn get_output() {
225 let (database, _directory) = open();
226
227 database.set_output("foo").unwrap();
228
229 assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
230 }
231
232 #[test]
233 fn get_no_output() {
234 let (database, _directory) = open();
235
236 assert_eq!(database.get_outputs().unwrap(), Vec::<String>::new());
237 }
238
239 #[test]
240 fn get_output_with_source() {
241 let (database, _directory) = open();
242
243 database.set_output("foo").unwrap();
244 database.set_source("foo", "bar").unwrap();
245
246 assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
247 }
248
249 #[test]
250 fn header_dependencies() {
251 let (database, _directory) = open();
252
253 database
254 .set_header_inputs(BuildId::new(0), &["foo".into(), "bar".into()])
255 .unwrap();
256
257 assert_eq!(
258 database.get_header_inputs(BuildId::new(0)).unwrap(),
259 vec!["foo".into(), "bar".into()]
260 );
261 }
262
263 #[test]
264 fn get_no_header_dependencies() {
265 let (database, _directory) = open();
266
267 assert_eq!(
268 database.get_header_inputs(BuildId::new(0)).unwrap(),
269 Vec::<Arc<str>>::new()
270 );
271 }
272
273 #[test]
274 fn intern_header_dependencies() {
275 let directory = tempdir().unwrap();
276 let path_pool = Arc::new(PathPool::new());
277 let database =
278 RedbDatabase::new(&directory.path().join(FILENAME), path_pool.clone()).unwrap();
279
280 database
281 .set_header_inputs(BuildId::new(0), &["foo".into()])
282 .unwrap();
283
284 assert!(Arc::ptr_eq(
285 &database.get_header_inputs(BuildId::new(0)).unwrap()[0],
286 &path_pool.intern("foo")
287 ));
288 }
289
290 #[test]
291 fn set_source() {
292 let (database, _directory) = open();
293
294 database.set_source("foo", "bar").unwrap();
295 }
296
297 #[test]
298 fn get_source() {
299 let (database, _directory) = open();
300
301 database.set_source("foo", "bar").unwrap();
302
303 assert_eq!(database.get_source("foo").unwrap(), Some("bar".into()));
304 }
305
306 #[test]
307 fn get_no_source() {
308 let (database, _directory) = open();
309
310 assert_eq!(database.get_source("foo").unwrap(), None);
311 }
312
313 #[test]
314 fn reopen() {
315 let (database, directory) = open();
316
317 database
318 .set_hash(BuildId::new(0), BuildHash::new(1, 2))
319 .unwrap();
320 database
321 .set_header_inputs(BuildId::new(0), &["foo".into()])
322 .unwrap();
323 database.set_output("foo").unwrap();
324 database.set_source("foo", "bar").unwrap();
325
326 drop(database);
327
328 let database =
329 RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap();
330
331 assert_eq!(
332 database.get_hash(BuildId::new(0)).unwrap(),
333 Some(BuildHash::new(1, 2))
334 );
335 assert_eq!(
336 database.get_header_inputs(BuildId::new(0)).unwrap(),
337 vec!["foo".into()]
338 );
339 assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
340 assert_eq!(database.get_source("foo").unwrap(), Some("bar".into()));
341 }
342}