use crate::{
build_hash::BuildHash,
infrastructure::{Database, DatabaseError},
ir::BuildId,
path_pool::PathPool,
};
use alloc::sync::Arc;
use async_trait::async_trait;
use redb::{
Durability, Key, ReadOnlyTable, ReadableDatabase, ReadableTable, TableDefinition, Value,
};
use std::{fs::create_dir_all, path::Path};
const HASHES: TableDefinition<[u8; 8], (u64, u64)> = TableDefinition::new("hashes");
const HEADER_DEPENDENCIES: TableDefinition<[u8; 8], Vec<&str>> =
TableDefinition::new("header_dependencies");
const OUTPUTS: TableDefinition<&str, Option<&str>> = TableDefinition::new("outputs");
pub struct RedbDatabase {
database: redb::Database,
path_pool: Arc<PathPool>,
}
impl RedbDatabase {
pub fn new(path: &Path, path_pool: Arc<PathPool>) -> Result<Self, DatabaseError> {
Ok(Self {
database: Self::open(path)?,
path_pool,
})
}
fn open(path: &Path) -> Result<redb::Database, redb::Error> {
if let Some(directory) = path.parent() {
create_dir_all(directory)?;
}
let database = redb::Database::create(path)?;
let mut transaction = database.begin_write()?;
transaction.set_durability(Durability::None)?;
transaction.open_table(HASHES)?;
transaction.open_table(HEADER_DEPENDENCIES)?;
transaction.open_table(OUTPUTS)?;
transaction.commit()?;
Ok(database)
}
fn read<K: Key + 'static, V: Value + 'static, T>(
&self,
table: TableDefinition<K, V>,
read: impl FnOnce(ReadOnlyTable<K, V>) -> Result<T, redb::Error>,
) -> Result<T, redb::Error> {
read(self.database.begin_read()?.open_table(table)?)
}
fn write<K: Key + 'static, V: Value + 'static>(
&self,
table: TableDefinition<K, V>,
key: K::SelfType<'_>,
value: V::SelfType<'_>,
) -> Result<(), redb::Error> {
let mut transaction = self.database.begin_write()?;
transaction.set_durability(Durability::None)?;
transaction.open_table(table)?.insert(key, value)?;
transaction.commit()?;
Ok(())
}
}
#[async_trait]
impl Database for RedbDatabase {
async fn get_hash(&self, id: BuildId) -> Result<Option<BuildHash>, DatabaseError> {
Ok(self.read(HASHES, |table| {
Ok(table
.get(id.to_bytes())?
.map(|hash| hash.value())
.map(|(timestamp, content)| BuildHash::new(timestamp, content)))
})?)
}
async fn set_hash(&self, id: BuildId, hash: BuildHash) -> Result<(), DatabaseError> {
Ok(self.write(HASHES, id.to_bytes(), (hash.timestamp(), hash.content()))?)
}
async fn get_header_inputs(&self, id: BuildId) -> Result<Vec<Arc<str>>, DatabaseError> {
Ok(self.read(HEADER_DEPENDENCIES, |table| {
Ok(table
.get(id.to_bytes())?
.map(|dependencies| {
dependencies
.value()
.into_iter()
.map(|path| self.path_pool.intern(path))
.collect()
})
.unwrap_or_default())
})?)
}
async fn set_header_inputs(
&self,
id: BuildId,
dependencies: &[Arc<str>],
) -> Result<(), DatabaseError> {
Ok(self.write(
HEADER_DEPENDENCIES,
id.to_bytes(),
dependencies.iter().map(AsRef::as_ref).collect(),
)?)
}
async fn get_outputs(&self) -> Result<Vec<String>, DatabaseError> {
Ok(self.read(OUTPUTS, |table| {
table
.iter()?
.map(|entry| Ok(entry?.0.value().into()))
.collect()
})?)
}
async fn set_output(&self, path: &str, source: Option<&str>) -> Result<(), DatabaseError> {
Ok(self.write(OUTPUTS, path, source)?)
}
async fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
Ok(self.read(OUTPUTS, |table| {
Ok(table
.get(output)?
.and_then(|source| source.value().map(Into::into)))
})?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::{TempDir, tempdir};
const FILENAME: &str = "database";
fn open() -> (RedbDatabase, TempDir) {
let directory = tempdir().unwrap();
(
RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap(),
directory,
)
}
#[test]
fn new() {
open();
}
#[test]
fn new_in_missing_directory() {
let directory = tempdir().unwrap();
RedbDatabase::new(
&directory.path().join("foo").join("bar").join(FILENAME),
Default::default(),
)
.unwrap();
}
#[tokio::test]
async fn hash() {
let (database, _directory) = open();
database
.set_hash(BuildId::new(0), BuildHash::new(1, 2))
.await
.unwrap();
assert_eq!(
database.get_hash(BuildId::new(0)).await.unwrap(),
Some(BuildHash::new(1, 2))
);
}
#[tokio::test]
async fn get_no_hash() {
let (database, _directory) = open();
database
.set_hash(BuildId::new(0), BuildHash::new(1, 2))
.await
.unwrap();
assert_eq!(database.get_hash(BuildId::new(1)).await.unwrap(), None);
}
#[tokio::test]
async fn update_hash() {
let (database, _directory) = open();
database
.set_hash(BuildId::new(0), BuildHash::new(1, 2))
.await
.unwrap();
database
.set_hash(BuildId::new(0), BuildHash::new(3, 4))
.await
.unwrap();
assert_eq!(
database.get_hash(BuildId::new(0)).await.unwrap(),
Some(BuildHash::new(3, 4))
);
}
#[tokio::test]
async fn set_output() {
let (database, _directory) = open();
database.set_output("foo", None).await.unwrap();
}
#[tokio::test]
async fn get_output() {
let (database, _directory) = open();
database.set_output("foo", None).await.unwrap();
assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
}
#[tokio::test]
async fn get_no_output() {
let (database, _directory) = open();
assert_eq!(database.get_outputs().await.unwrap(), Vec::<String>::new());
}
#[tokio::test]
async fn get_output_with_source() {
let (database, _directory) = open();
database.set_output("foo", Some("bar")).await.unwrap();
assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
}
#[tokio::test]
async fn header_dependencies() {
let (database, _directory) = open();
database
.set_header_inputs(BuildId::new(0), &["foo".into(), "bar".into()])
.await
.unwrap();
assert_eq!(
database.get_header_inputs(BuildId::new(0)).await.unwrap(),
vec!["foo".into(), "bar".into()]
);
}
#[tokio::test]
async fn get_no_header_dependencies() {
let (database, _directory) = open();
assert_eq!(
database.get_header_inputs(BuildId::new(0)).await.unwrap(),
Vec::<Arc<str>>::new()
);
}
#[tokio::test]
async fn intern_header_dependencies() {
let directory = tempdir().unwrap();
let path_pool = Arc::new(PathPool::new());
let database =
RedbDatabase::new(&directory.path().join(FILENAME), path_pool.clone()).unwrap();
database
.set_header_inputs(BuildId::new(0), &["foo".into()])
.await
.unwrap();
assert!(Arc::ptr_eq(
&database.get_header_inputs(BuildId::new(0)).await.unwrap()[0],
&path_pool.intern("foo")
));
}
#[tokio::test]
async fn get_source() {
let (database, _directory) = open();
database.set_output("foo", Some("bar")).await.unwrap();
assert_eq!(
database.get_source("foo").await.unwrap(),
Some("bar".into())
);
}
#[tokio::test]
async fn get_no_source() {
let (database, _directory) = open();
database.set_output("foo", None).await.unwrap();
assert_eq!(database.get_source("foo").await.unwrap(), None);
}
#[tokio::test]
async fn get_no_source_of_missing_output() {
let (database, _directory) = open();
assert_eq!(database.get_source("foo").await.unwrap(), None);
}
#[tokio::test]
async fn update_source() {
let (database, _directory) = open();
database.set_output("foo", Some("bar")).await.unwrap();
database.set_output("foo", Some("baz")).await.unwrap();
assert_eq!(
database.get_source("foo").await.unwrap(),
Some("baz".into())
);
}
#[tokio::test]
async fn remove_source() {
let (database, _directory) = open();
database.set_output("foo", Some("bar")).await.unwrap();
database.set_output("foo", None).await.unwrap();
assert_eq!(database.get_source("foo").await.unwrap(), None);
}
#[tokio::test]
async fn reopen() {
let (database, directory) = open();
database
.set_hash(BuildId::new(0), BuildHash::new(1, 2))
.await
.unwrap();
database
.set_header_inputs(BuildId::new(0), &["foo".into()])
.await
.unwrap();
database.set_output("foo", Some("bar")).await.unwrap();
drop(database);
let database =
RedbDatabase::new(&directory.path().join(FILENAME), Default::default()).unwrap();
assert_eq!(
database.get_hash(BuildId::new(0)).await.unwrap(),
Some(BuildHash::new(1, 2))
);
assert_eq!(
database.get_header_inputs(BuildId::new(0)).await.unwrap(),
vec!["foo".into()]
);
assert_eq!(database.get_outputs().await.unwrap(), vec!["foo"]);
assert_eq!(
database.get_source("foo").await.unwrap(),
Some("bar".into())
);
}
}