use std::{sync::Arc, time::Duration};
use object_store::ObjectStore;
use slatedb::{Db, DbReader, config::Settings};
use crate::{
error::{Error, Result},
store::segment::TagSegmentExtractor,
};
fn settings(flush_interval: Duration) -> Result<Settings> {
if flush_interval.is_zero() {
return Err(Error::Configuration(
"flush_interval must be nonzero; zero would disable automatic flushing and \
hang every durable commit"
.to_string(),
));
}
Ok(Settings {
flush_interval: Some(flush_interval),
..Default::default()
})
}
pub(crate) async fn open_store(
path: &str,
object_store: Arc<dyn ObjectStore>,
flush_interval: Duration,
) -> Result<Db> {
Db::builder(path, object_store)
.with_settings(settings(flush_interval)?)
.with_segment_extractor(Arc::new(TagSegmentExtractor))
.build()
.await
.map_err(Error::from)
}
pub(crate) async fn open_reader(
path: &str,
object_store: Arc<dyn ObjectStore>,
) -> Result<DbReader> {
DbReader::builder(path, object_store)
.with_segment_extractor(Arc::new(crate::store::segment::TagSegmentExtractor))
.build()
.await
.map_err(Error::from)
}
#[cfg(test)]
mod tests {
use object_store::memory::InMemory;
use slatedb::{IsolationLevel, config::WriteOptions};
use super::*;
use crate::store::key::{self, Key, SysKey};
fn memory_store() -> Arc<dyn ObjectStore> {
Arc::new(InMemory::new())
}
#[tokio::test]
async fn multi_subspace_transaction_and_prefix_scans() {
let db = open_store("test/store", memory_store(), Duration::from_millis(100))
.await
.unwrap();
let head = Key::Sys(SysKey::Head).encode();
let snapshot = Key::Snapshot { snapshot_id: 1 }.encode();
let table = Key::current(key::EntityKey::Table { table_id: 7 }).encode();
let tx = db.begin(IsolationLevel::Snapshot).await.unwrap();
tx.put(&head, b"head").unwrap();
tx.put(&snapshot, b"snap").unwrap();
tx.put(&table, b"table").unwrap();
tx.commit_with_options(&WriteOptions {
await_durable: true,
..Default::default()
})
.await
.unwrap();
assert_eq!(db.get(&head).await.unwrap().unwrap().as_ref(), b"head");
let mut iter = db
.scan_prefix(key::subspace_prefix(key::Subspace::Current), ..)
.await
.unwrap();
let entry = iter.next().await.unwrap().unwrap();
assert_eq!(entry.key.as_ref(), table.as_slice());
assert!(iter.next().await.unwrap().is_none());
let mut iter = db
.scan_prefix(key::subspace_prefix(key::Subspace::Snapshot), ..)
.await
.unwrap();
let entry = iter.next().await.unwrap().unwrap();
assert_eq!(entry.key.as_ref(), snapshot.as_slice());
assert!(iter.next().await.unwrap().is_none());
db.close().await.unwrap();
}
#[tokio::test]
async fn zero_flush_interval_is_refused() {
match open_store("test/store", memory_store(), Duration::ZERO).await {
Err(Error::Configuration(_)) => {}
Err(err) => panic!("expected a configuration error, got {err:?}"),
Ok(_) => panic!("a zero flush interval unexpectedly opened a store"),
}
}
#[tokio::test]
async fn explicit_flush_interval_opens_a_working_store() {
let db = open_store("test/store", memory_store(), Duration::from_millis(1))
.await
.unwrap();
let head = Key::Sys(SysKey::Head).encode();
db.put(&head, b"head").await.unwrap();
assert_eq!(db.get(&head).await.unwrap().unwrap().as_ref(), b"head");
db.close().await.unwrap();
}
#[tokio::test]
async fn reopen_without_extractor_is_refused() {
let object_store = memory_store();
let db = open_store(
"test/store",
object_store.clone(),
Duration::from_millis(100),
)
.await
.unwrap();
db.put(&Key::Sys(SysKey::Head).encode(), b"head")
.await
.unwrap();
db.close().await.unwrap();
let bare = Db::builder("test/store", object_store).build().await;
assert!(bare.is_err(), "unsegmented reopen must be refused");
}
}