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