use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::path::{Path, PathBuf};
use crate::bytes::Bytes;
use crate::future::block_on;
use crate::persistence::NamespaceSummary;
use crate::persistence::database::{self, SCHEMA_VERSION, SCHEMA_VERSION_KEY, connect};
use crate::persistence::sql::{self, Opening, SqlError};
use super::{Bundle, BundleError, BundleManifest};
const MAGIC: &[u8; 16] = b"SQLite format 3\0";
#[derive(Debug)]
pub struct SqliteBundle {
database: sql::Database,
manifest: BundleManifest,
path: PathBuf,
}
impl SqliteBundle {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, BundleError> {
let path = path.as_ref();
if !is_sqlite(path)? {
return Err(BundleError::NotABundle);
}
let location = path.to_str().ok_or_else(|| {
BundleError::Storage(std::format!("bundle path {path:?} is not valid UTF-8"))
})?;
let database = block_on(sql::Database::open(location, Opening::ReadOnly))?;
let connection = connect(&database)?;
let expected = SCHEMA_VERSION.to_string();
match database::meta_get(&connection, SCHEMA_VERSION_KEY).map_err(missing_meta)? {
Some(found) if found == expected => {}
Some(found) => return Err(BundleError::UnsupportedDatabase(found)),
None => return Err(BundleError::NotABundle),
}
let manifest = BundleManifest::read(&connection)?;
manifest.warn_on_version_mismatch();
Ok(Self {
database,
manifest,
path: path.to_path_buf(),
})
}
pub fn manifest(&self) -> &BundleManifest {
&self.manifest
}
pub fn summary(&self) -> Vec<NamespaceSummary> {
self.read("summarize", database::summarize)
.unwrap_or_default()
}
fn read<T>(
&self,
name: &str,
read: impl FnOnce(&sql::Connection) -> Result<T, SqlError>,
) -> Option<T> {
connect(&self.database)
.and_then(|connection| read(&connection))
.inspect_err(|err| log::warn!("Unable to {name} {}: {err}", self.describe()))
.ok()
}
}
impl Bundle for SqliteBundle {
fn get(&self, namespace: &str, key: &[u8]) -> Option<Bytes> {
self.read("read", |connection| {
let mut value = None;
connection.query(
"SELECT value FROM entries WHERE namespace = ?1 AND key = ?2",
&[namespace.into(), key.into()],
&mut |row| {
value = Some(Bytes::from_bytes_vec(row.blob(0)?));
Ok::<_, SqlError>(())
},
)?;
Ok(value)
})
.flatten()
}
fn scan(&self, namespace: &str, visit: &mut dyn FnMut(&[u8], &[u8])) {
self.read("scan", |connection| {
connection.query(
"SELECT key, value FROM entries WHERE namespace = ?1",
&[namespace.into()],
&mut |row| {
visit(&row.blob(0)?, &row.blob(1)?);
Ok::<_, SqlError>(())
},
)
});
}
fn namespaces(&self) -> Vec<String> {
self.read("list", |connection| {
let mut namespaces = Vec::new();
connection.query(
"SELECT DISTINCT namespace FROM entries ORDER BY namespace",
&[],
&mut |row| {
namespaces.push(row.text(0)?);
Ok::<_, SqlError>(())
},
)?;
Ok(namespaces)
})
.unwrap_or_default()
}
fn describe(&self) -> String {
alloc::format!("bundle '{}' at {:?}", self.manifest.name, self.path)
}
}
pub(super) fn missing_meta(err: SqlError) -> BundleError {
let message = err.to_string();
if message.contains("no such table") {
BundleError::NotABundle
} else {
BundleError::Storage(message)
}
}
fn is_sqlite(path: &Path) -> Result<bool, BundleError> {
use std::io::Read;
let mut header = [0u8; 16];
match std::fs::File::open(path)?.read_exact(&mut header) {
Ok(()) => Ok(&header == MAGIC),
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => Ok(false),
Err(err) => Err(err.into()),
}
}