use std::{sync::Arc, time::Duration};
use object_store::ObjectStore;
use slatedb::{Db, DbReader, DbTransaction, IsolationLevel};
use crate::{
catalog::{CatalogSnapshot, SnapshotId, projection::ProjectionCache},
error::{Error, Result},
store::{handle::ReadSession, open::StoreBuilder},
transaction::{Transaction, commit},
};
enum Store {
Writer(Db),
Reader(Arc<DbReader>),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CatalogOptions {
pub path: String,
pub encrypted: bool,
pub flush_interval: Duration,
pub cache_dir: Option<std::path::PathBuf>,
}
impl Default for CatalogOptions {
fn default() -> Self {
Self {
path: String::new(),
encrypted: false,
flush_interval: Duration::from_millis(100),
cache_dir: None,
}
}
}
#[derive(Clone)]
pub struct Catalog {
store: Arc<Store>,
projections: Arc<std::sync::RwLock<ProjectionCache>>,
}
impl std::fmt::Debug for Catalog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Catalog").finish_non_exhaustive()
}
}
impl Catalog {
pub async fn open(object_store: Arc<dyn ObjectStore>, options: CatalogOptions) -> Result<Self> {
let store = StoreBuilder::new(&options.path, object_store)
.flush_interval(options.flush_interval)
.cache_dir(options.cache_dir.clone());
let db = commit::open_initialized(store, options.encrypted).await?;
Ok(Self {
store: Arc::new(Store::Writer(db)),
projections: Arc::new(std::sync::RwLock::new(ProjectionCache::empty())),
})
}
pub async fn open_read_only(
object_store: Arc<dyn ObjectStore>,
options: CatalogOptions,
) -> Result<Self> {
let store =
StoreBuilder::new(&options.path, object_store).cache_dir(options.cache_dir.clone());
let reader = commit::open_reader_initialized(store).await?;
Ok(Self {
store: Arc::new(Store::Reader(Arc::new(reader))),
projections: Arc::new(std::sync::RwLock::new(ProjectionCache::empty())),
})
}
pub(crate) fn projections(&self) -> &Arc<std::sync::RwLock<ProjectionCache>> {
&self.projections
}
pub(crate) fn maintains_projections(&self) -> bool {
matches!(self.store.as_ref(), Store::Writer(_))
}
fn writer(&self) -> Result<&Db> {
match self.store.as_ref() {
Store::Writer(db) => Ok(db),
Store::Reader(_) => Err(Error::Constraint(
"catalog opened read-only; writes are unavailable".to_string(),
)),
}
}
pub async fn snapshot(&self) -> Result<CatalogSnapshot> {
self.view(None).await
}
pub async fn snapshot_at(&self, snapshot: SnapshotId) -> Result<CatalogSnapshot> {
self.view(Some(snapshot.get())).await
}
async fn view(&self, at: Option<u64>) -> Result<CatalogSnapshot> {
let session = self.begin_read().await?;
let view = commit::materialize(session.handle(), at).await;
session.finish();
view
}
pub(crate) async fn begin_read(&self) -> Result<ReadSession> {
match self.store.as_ref() {
Store::Writer(db) => Ok(ReadSession::Tx(
db.begin(IsolationLevel::Snapshot)
.await
.map_err(Error::from)?,
)),
Store::Reader(reader) => Ok(ReadSession::Reader(reader.clone())),
}
}
pub(crate) async fn begin_write_tx(&self) -> Result<DbTransaction> {
self.writer()?
.begin(IsolationLevel::Snapshot)
.await
.map_err(Error::from)
}
pub async fn close(&self) -> Result<()> {
match self.store.as_ref() {
Store::Writer(db) => db.close().await.map_err(Error::from),
Store::Reader(reader) => reader.close().await.map_err(Error::from),
}
}
pub async fn commit<F>(&self, f: F) -> Result<SnapshotId>
where
F: Fn(&mut Transaction) -> Result<()>,
{
commit::commit_cycle(self.writer()?, &f, &self.projections).await
}
}