use anyhow::{Context, Result};
use redb::{
Database, DatabaseError, ReadableDatabase, ReadableTable, ReadableTableMetadata, StorageError,
TableDefinition, TableError,
};
use super::config::DEFAULT_DB_FILENAME;
use super::Config;
use crate::domain::RustRelease;
pub struct Db {
db: Database,
table_prefix: String,
}
impl Db {
pub fn open(config: &Config) -> Result<Self> {
let db_path = config.db_path();
if let Some(parent) = db_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
let db = if config.database.shared {
Self::open_shared(db_path)?
} else {
Self::open_standalone(db_path)?
};
let write_txn = db.begin_write()?;
for table_name in config.database.all_table_names() {
let table_def: TableDefinition<&str, &str> = TableDefinition::new(&table_name);
write_txn.open_table(table_def)?;
}
write_txn.commit()?;
Ok(Self {
db,
table_prefix: config.database.table_prefix.clone(),
})
}
fn open_shared(db_path: &std::path::Path) -> Result<Database> {
if db_path.exists() {
match Database::open(db_path) {
Ok(db) => Ok(db),
Err(e) => {
let db_err: anyhow::Error = e.into();
let is_redb_error = db_err.downcast_ref::<DatabaseError>().is_some_and(|err| {
matches!(
err,
DatabaseError::UpgradeRequired(_)
| DatabaseError::Storage(StorageError::Corrupted(_))
)
});
if is_redb_error {
eprintln!(
"Warning: '{}' is not a valid redb database. \
Falling back to standalone mode.",
db_path.display()
);
} else {
eprintln!(
"Warning: '{}' could not be opened ({}). \
Falling back to standalone mode.",
db_path.display(),
db_err
);
}
let standalone_path = db_path.with_file_name(DEFAULT_DB_FILENAME);
Self::open_standalone(&standalone_path)
}
}
} else {
Database::create(db_path).with_context(|| {
format!("Failed to create shared database: {}", db_path.display())
})?;
let db = Database::open(db_path).context(format!(
"Failed to open shared database: {}",
db_path.display()
))?;
Ok(db)
}
}
fn open_standalone(db_path: &std::path::Path) -> Result<Database> {
match Database::create(db_path) {
Ok(db) => Ok(db),
Err(e) => {
let anyhow_err: anyhow::Error = e.into();
let needs_recreate = anyhow_err
.downcast_ref::<DatabaseError>()
.is_some_and(|err| matches!(err, DatabaseError::UpgradeRequired(_)));
if needs_recreate {
eprintln!(
"Warning: Incompatible database format, recreating: {}",
db_path.display()
);
let _ = std::fs::remove_file(db_path);
Ok(Database::create(db_path).with_context(|| {
format!("Failed to create database: {}", db_path.display())
})?)
} else {
Err(anyhow_err)
.context(format!("Failed to open database: {}", db_path.display()))
}
}
}
}
fn table_name_for(&self, channel: &str) -> String {
format!("{}_{}", self.table_prefix, channel)
}
pub fn upsert_all(&self, releases: &[RustRelease]) -> Result<u64> {
if releases.is_empty() {
return Ok(0);
}
let mut count = 0u64;
let write_txn = self.db.begin_write()?;
for channel in &["stable", "beta", "nightly"] {
let channel_releases: Vec<_> =
releases.iter().filter(|r| r.channel == *channel).collect();
if channel_releases.is_empty() {
continue;
}
let table_name = self.table_name_for(channel);
let mut table =
write_txn.open_table::<&str, &str>(TableDefinition::new(&table_name))?;
for r in &channel_releases {
table.insert(r.date.as_str(), r.version.as_str())?;
count += 1;
}
}
write_txn.commit()?;
Ok(count)
}
fn read_channel(&self, channel: &str) -> Result<Vec<RustRelease>> {
let table_name = self.table_name_for(channel);
let read_txn = self.db.begin_read()?;
let table = match read_txn.open_table::<&str, &str>(TableDefinition::new(&table_name)) {
Ok(t) => t,
Err(e) => {
let anyhow_err: anyhow::Error = e.into();
let table_not_found = anyhow_err
.downcast_ref::<TableError>()
.is_some_and(|err| matches!(err, TableError::TableDoesNotExist(_)));
if table_not_found {
return Ok(Vec::new());
}
return Err(anyhow_err).context(format!("Failed to open table: {}", table_name));
}
};
let mut releases = Vec::new();
for entry in table.iter()? {
let (key, val) = entry?;
releases.push(RustRelease {
version: val.value().to_string(),
date: key.value().to_string(),
channel: channel.to_string(),
});
}
Ok(releases)
}
fn read_all(&self, channel: Option<&str>) -> Result<Vec<RustRelease>> {
let mut releases = Vec::new();
match channel {
Some(ch) => {
releases = self.read_channel(ch)?;
}
None => {
for ch in &["stable", "beta", "nightly"] {
releases.extend(self.read_channel(ch)?);
}
}
}
Ok(releases)
}
pub fn list_all(&self, channel: Option<&str>) -> Result<Vec<RustRelease>> {
let mut releases = self.read_all(channel)?;
releases.sort_by(|a, b| b.date.cmp(&a.date));
Ok(releases)
}
pub fn search(&self, keyword: &str, channel: Option<&str>) -> Result<Vec<RustRelease>> {
let all = self.read_all(channel)?;
let kw = keyword.to_lowercase();
let mut results: Vec<RustRelease> = all
.into_iter()
.filter(|r| r.version.to_lowercase().contains(&kw) || r.date.contains(&kw))
.collect();
results.sort_by(|a, b| b.date.cmp(&a.date));
Ok(results)
}
pub fn count(&self, channel: Option<&str>) -> Result<u64> {
if let Some(ch) = channel {
let table_name = self.table_name_for(ch);
let read_txn = self.db.begin_read()?;
let count = match read_txn.open_table::<&str, &str>(TableDefinition::new(&table_name)) {
Ok(table) => table.len()?,
Err(e) => {
let anyhow_err: anyhow::Error = e.into();
let table_not_found = anyhow_err
.downcast_ref::<TableError>()
.is_some_and(|err| matches!(err, TableError::TableDoesNotExist(_)));
if table_not_found {
0
} else {
return Err(anyhow_err)
.context(format!("Failed to open table: {}", table_name));
}
}
};
Ok(count)
} else {
let mut total = 0u64;
for ch in &["stable", "beta", "nightly"] {
let table_name = self.table_name_for(ch);
let read_txn = self.db.begin_read()?;
let count =
match read_txn.open_table::<&str, &str>(TableDefinition::new(&table_name)) {
Ok(table) => table.len()?,
Err(e) => {
let anyhow_err: anyhow::Error = e.into();
let table_not_found = anyhow_err
.downcast_ref::<TableError>()
.is_some_and(|err| matches!(err, TableError::TableDoesNotExist(_)));
if table_not_found {
0
} else {
return Err(anyhow_err)
.context(format!("Failed to open table: {}", table_name));
}
}
};
total += count;
}
Ok(total)
}
}
}