use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
pub use powdb_query::executor::{Engine, WalSyncMode};
pub use powdb_query::result::{QueryError, QueryResult};
pub use powdb_storage::types::Value;
#[derive(Debug)]
pub enum Error {
Open(std::io::Error),
Query(QueryError),
Poisoned,
OpenPanicked,
InvalidArgument(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Open(e) => write!(f, "failed to open database: {e}"),
Error::Query(e) => write!(f, "query failed: {e:?}"),
Error::Poisoned => write!(
f,
"database handle is poisoned (a previous call panicked); reopen the database"
),
Error::OpenPanicked => write!(
f,
"opening the database panicked (data directory may be corrupt); restore from a backup"
),
Error::InvalidArgument(msg) => write!(f, "{msg}"),
}
}
}
pub fn parse_sync_mode(mode: &str) -> Option<WalSyncMode> {
match mode.to_ascii_lowercase().as_str() {
"full" => Some(WalSyncMode::Full),
"normal" => Some(WalSyncMode::Normal),
"off" => Some(WalSyncMode::Off),
_ => None,
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Open(e) => Some(e),
_ => None,
}
}
}
pub struct Database {
engine: Option<Engine>,
poisoned: AtomicBool,
}
impl Database {
pub fn open(dir: impl AsRef<Path>) -> Result<Self, Error> {
let dir = dir.as_ref();
Self::wrap_open(catch_unwind(AssertUnwindSafe(|| Engine::new(dir))))
}
pub fn open_with_memory_limit(
dir: impl AsRef<Path>,
limit_bytes: usize,
) -> Result<Self, Error> {
let dir = dir.as_ref();
Self::wrap_open(catch_unwind(AssertUnwindSafe(|| {
Engine::with_memory_limit(dir, limit_bytes)
})))
}
fn wrap_open(result: std::thread::Result<std::io::Result<Engine>>) -> Result<Self, Error> {
let engine = result
.map_err(|_| Error::OpenPanicked)?
.map_err(Error::Open)?;
Ok(Self {
engine: Some(engine),
poisoned: AtomicBool::new(false),
})
}
pub fn query(&mut self, powql: &str) -> Result<QueryResult, Error> {
self.run_mut(|e| e.execute_powql(powql))
}
pub fn query_sql(&mut self, sql: &str) -> Result<QueryResult, Error> {
self.run_mut(|e| e.execute_sql(sql))
}
pub fn query_readonly(&self, powql: &str) -> Result<QueryResult, Error> {
self.run_ref(|e| e.execute_powql_readonly(powql))
}
pub fn set_sync_mode(&mut self, mode: WalSyncMode) {
if let Some(engine) = self.engine.as_mut() {
engine.set_wal_sync_mode(mode);
}
}
pub fn set_sync_mode_str(&mut self, mode: &str) -> Result<(), Error> {
let parsed = parse_sync_mode(mode).ok_or_else(|| {
Error::InvalidArgument(format!(
"unknown sync mode {mode:?}; expected \"full\", \"normal\", or \"off\""
))
})?;
self.set_sync_mode(parsed);
Ok(())
}
pub fn is_poisoned(&self) -> bool {
self.poisoned.load(Ordering::Acquire)
}
pub fn close(self) {
}
fn run_mut(
&mut self,
f: impl FnOnce(&mut Engine) -> Result<QueryResult, QueryError>,
) -> Result<QueryResult, Error> {
if self.poisoned.load(Ordering::Acquire) {
return Err(Error::Poisoned);
}
let engine = self.engine.as_mut().expect("engine present until drop");
match catch_unwind(AssertUnwindSafe(|| f(engine))) {
Ok(inner) => inner.map_err(Error::Query),
Err(_) => {
self.poisoned.store(true, Ordering::Release);
Err(Error::Poisoned)
}
}
}
fn run_ref(
&self,
f: impl FnOnce(&Engine) -> Result<QueryResult, QueryError>,
) -> Result<QueryResult, Error> {
if self.poisoned.load(Ordering::Acquire) {
return Err(Error::Poisoned);
}
let engine = self.engine.as_ref().expect("engine present until drop");
match catch_unwind(AssertUnwindSafe(|| f(engine))) {
Ok(inner) => inner.map_err(Error::Query),
Err(_) => {
self.poisoned.store(true, Ordering::Release);
Err(Error::Poisoned)
}
}
}
}
impl Drop for Database {
fn drop(&mut self) {
if let Some(engine) = self.engine.take() {
if self.poisoned.load(Ordering::Acquire) {
std::mem::forget(engine);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn caught_panic_poisons_and_returns_error() {
let poisoned = AtomicBool::new(false);
let result: Result<(), Error> = (|| {
if poisoned.load(Ordering::Acquire) {
return Err(Error::Poisoned);
}
match catch_unwind(AssertUnwindSafe(|| panic!("boom"))) {
Ok(()) => Ok(()),
Err(_) => {
poisoned.store(true, Ordering::Release);
Err(Error::Poisoned)
}
}
})();
assert!(matches!(result, Err(Error::Poisoned)));
assert!(poisoned.load(Ordering::Acquire));
}
#[test]
fn poisoned_handle_rejects_then_reopen_recovers() {
let dir = std::env::temp_dir().join(format!("powdb_facade_poison_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
{
let mut db = Database::open(&dir).unwrap();
db.query("type T { required id: int }").unwrap();
db.query("insert T { id := 1 }").unwrap(); db.poisoned.store(true, Ordering::Release);
assert!(db.is_poisoned());
assert!(matches!(db.query("count(T)"), Err(Error::Poisoned)));
}
let mut db = Database::open(&dir).unwrap();
match db.query("count(T)").unwrap() {
QueryResult::Scalar(Value::Int(n)) => assert_eq!(n, 1),
other => panic!("expected 1 row after poisoned-drop reopen, got {other:?}"),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn parse_sync_mode_is_case_insensitive_and_rejects_unknown() {
assert!(matches!(parse_sync_mode("full"), Some(WalSyncMode::Full)));
assert!(matches!(
parse_sync_mode("Normal"),
Some(WalSyncMode::Normal)
));
assert!(matches!(parse_sync_mode("OFF"), Some(WalSyncMode::Off)));
assert!(parse_sync_mode("bogus").is_none());
}
#[test]
fn set_sync_mode_str_sets_known_and_errors_on_unknown() {
let dir = std::env::temp_dir().join(format!("powdb_syncstr_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let mut db = Database::open(&dir).unwrap();
db.query("type T { required id: int }").unwrap();
db.set_sync_mode_str("normal").unwrap();
db.query("insert T { id := 1 }").unwrap();
assert!(db.set_sync_mode_str("bogus").is_err());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn open_with_corrupt_heap_returns_error_not_panic() {
let dir = std::env::temp_dir().join(format!("powdb_corrupt_heap_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
{
let mut db = Database::open(&dir).unwrap();
db.query("type T { required id: int }").unwrap();
db.query("insert T { id := 1 }").unwrap();
}
let heap = dir.join("T.heap");
let mut bytes = std::fs::read(&heap).unwrap();
for b in bytes.iter_mut().take(20) {
*b = 0xFF;
}
std::fs::write(&heap, &bytes).unwrap();
let result = Database::open(&dir);
let _ = std::fs::remove_dir_all(&dir);
assert!(
result.is_err(),
"corrupt heap must return Err, not panic/abort the host"
);
}
}