Skip to main content

turtle_build/infrastructure/database/
fjall.rs

1use 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
17/// A Fjall database.
18pub struct FjallDatabase {
19    keyspace: Keyspace,
20}
21
22impl FjallDatabase {
23    /// Creates a database.
24    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}