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