use lunaris_core::{Lsn, LunarisError};
use crate::handle::Lunaris;
impl Lunaris {
pub async fn snapshot(&self) -> Result<Lsn, LunarisError> {
self.storage
.atomic_write(&lunaris_core::Scope::dev(), &[])
.await
.map_err(LunarisError::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use async_trait::async_trait;
use bytes::Bytes;
use futures::stream::BoxStream;
use lunaris_core::{
CypherQuery, Embedder, Filter, GraphResult, Hlc, HlcClock, QueueMsg, Row,
StorageCapabilities, StorageError, StoragePort, StubEmbedder, VectorHit, WriteOp,
};
use parking_lot::Mutex;
#[derive(Default)]
struct MonotonicLsnStorage {
counter: Mutex<u32>,
}
#[async_trait]
impl StoragePort for MonotonicLsnStorage {
async fn atomic_write(
&self,
_scope: &lunaris_core::Scope,
_ops: &[WriteOp],
) -> Result<Lsn, StorageError> {
let mut g = self.counter.lock();
*g += 1;
Ok(Lsn { wall_ms: 1, counter: *g })
}
async fn vector_search(
&self,
_scope: &lunaris_core::Scope,
_index: &str,
_query: &[f32],
_k: usize,
_filter: Option<&Filter>,
_as_of: Option<Hlc>,
_rerank: bool,
) -> Result<Vec<VectorHit>, StorageError> {
Err(StorageError::NotSupported("MonotonicLsnStorage::vector_search"))
}
async fn graph_traverse(
&self,
_scope: &lunaris_core::Scope,
_query: &CypherQuery,
_as_of: Option<Hlc>,
) -> Result<GraphResult, StorageError> {
Err(StorageError::NotSupported("MonotonicLsnStorage::graph_traverse"))
}
async fn scan_range(
&self,
_scope: &lunaris_core::Scope,
_prefix: &[u8],
_as_of: Option<Hlc>,
) -> Result<BoxStream<'_, Result<(Bytes, Bytes), StorageError>>, StorageError> {
Err(StorageError::NotSupported("MonotonicLsnStorage::scan_range"))
}
async fn read_as_of(
&self,
_scope: &lunaris_core::Scope,
_key: &[u8],
_as_of: Hlc,
) -> Result<Option<Row<Bytes>>, StorageError> {
Err(StorageError::NotSupported("MonotonicLsnStorage::read_as_of"))
}
async fn publish(
&self,
_scope: &lunaris_core::Scope,
_topic: &str,
_partition: u16,
_payload: Bytes,
) -> Result<u64, StorageError> {
Err(StorageError::NotSupported("MonotonicLsnStorage::publish"))
}
async fn subscribe(
&self,
_scope: &lunaris_core::Scope,
_group: &str,
_topic: &str,
_partition: u16,
) -> Result<BoxStream<'static, Result<QueueMsg, StorageError>>, StorageError> {
Err(StorageError::NotSupported("MonotonicLsnStorage::subscribe"))
}
fn capabilities(&self) -> StorageCapabilities {
StorageCapabilities {
bi_temporal_native: false,
graph_native: false,
rerank_native: false,
queue_native: false,
max_vector_dim: 768,
native_rrf: false,
max_scopes_recommended: 0,
cypher_dialect: lunaris_core::CypherDialect::Legacy,
graph_decay_native: false,
graph_navigate_native: false,
}
}
}
struct FailingStorage;
#[async_trait]
impl StoragePort for FailingStorage {
async fn atomic_write(
&self,
_scope: &lunaris_core::Scope,
_ops: &[WriteOp],
) -> Result<Lsn, StorageError> {
Err(StorageError::NotSupported("snapshot test: atomic_write disabled"))
}
async fn vector_search(
&self,
_scope: &lunaris_core::Scope,
_index: &str,
_query: &[f32],
_k: usize,
_filter: Option<&Filter>,
_as_of: Option<Hlc>,
_rerank: bool,
) -> Result<Vec<VectorHit>, StorageError> {
Err(StorageError::NotSupported("FailingStorage::vector_search"))
}
async fn graph_traverse(
&self,
_scope: &lunaris_core::Scope,
_query: &CypherQuery,
_as_of: Option<Hlc>,
) -> Result<GraphResult, StorageError> {
Err(StorageError::NotSupported("FailingStorage::graph_traverse"))
}
async fn scan_range(
&self,
_scope: &lunaris_core::Scope,
_prefix: &[u8],
_as_of: Option<Hlc>,
) -> Result<BoxStream<'_, Result<(Bytes, Bytes), StorageError>>, StorageError> {
Err(StorageError::NotSupported("FailingStorage::scan_range"))
}
async fn read_as_of(
&self,
_scope: &lunaris_core::Scope,
_key: &[u8],
_as_of: Hlc,
) -> Result<Option<Row<Bytes>>, StorageError> {
Err(StorageError::NotSupported("FailingStorage::read_as_of"))
}
async fn publish(
&self,
_scope: &lunaris_core::Scope,
_topic: &str,
_partition: u16,
_payload: Bytes,
) -> Result<u64, StorageError> {
Err(StorageError::NotSupported("FailingStorage::publish"))
}
async fn subscribe(
&self,
_scope: &lunaris_core::Scope,
_group: &str,
_topic: &str,
_partition: u16,
) -> Result<BoxStream<'static, Result<QueueMsg, StorageError>>, StorageError> {
Err(StorageError::NotSupported("FailingStorage::subscribe"))
}
fn capabilities(&self) -> StorageCapabilities {
StorageCapabilities {
bi_temporal_native: false,
graph_native: false,
rerank_native: false,
queue_native: false,
max_vector_dim: 768,
native_rrf: false,
max_scopes_recommended: 0,
cypher_dialect: lunaris_core::CypherDialect::Legacy,
graph_decay_native: false,
graph_navigate_native: false,
}
}
}
#[tokio::test]
async fn test_snapshot_returns_monotonic_lsn() {
let storage: Arc<dyn StoragePort> = Arc::new(MonotonicLsnStorage::default());
let embedder: Arc<dyn Embedder> = Arc::new(StubEmbedder::new(768));
let clock = HlcClock::new(0);
let handle = Lunaris::with_parts(storage, embedder, clock);
let a = handle.snapshot().await.expect("first snapshot");
let b = handle.snapshot().await.expect("second snapshot");
assert!(b > a, "expected monotonic LSN: a={a:?} b={b:?}");
}
#[tokio::test]
async fn test_snapshot_fallible() {
let storage: Arc<dyn StoragePort> = Arc::new(FailingStorage);
let embedder: Arc<dyn Embedder> = Arc::new(StubEmbedder::new(768));
let clock = HlcClock::new(0);
let handle = Lunaris::with_parts(storage, embedder, clock);
let r = handle.snapshot().await;
assert!(
matches!(r, Err(LunarisError::Storage(_))),
"expected LunarisError::Storage(_); got {r:?}"
);
}
}