use std::sync::Arc;
use laurus::lexical::TextOption;
use laurus::storage::Storage;
use laurus::storage::memory::{MemoryStorage, MemoryStorageConfig};
use laurus::{DataValue, Document, Engine, FieldOption, LaurusError, Schema};
fn batch_entry(id: &str, title: &str) -> (String, Document) {
let doc = Document::builder()
.add_field("title", DataValue::Text(title.into()))
.build();
(id.to_string(), doc)
}
fn title_schema() -> Schema {
Schema::builder()
.add_field("title", FieldOption::Text(TextOption::default()))
.build()
}
#[tokio::test(flavor = "multi_thread")]
async fn test_put_documents_uncommitted_batch_recovers_after_reopen() -> laurus::Result<()> {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let schema = title_schema();
{
let engine = Engine::new(storage.clone(), schema.clone()).await?;
let docs: Vec<_> = (0..25)
.map(|i| batch_entry(&format!("id{i}"), &format!("title-{i}")))
.collect();
engine.put_documents(docs).await?;
}
{
let engine = Engine::new(storage.clone(), schema.clone()).await?;
engine.commit().await?;
let stats = engine.stats()?;
assert_eq!(
stats.document_count, 25,
"every batched doc must be replayed from the WAL after reopen"
);
for i in 0..25 {
let docs = engine.get_documents(&format!("id{i}")).await?;
assert_eq!(docs.len(), 1, "id{i} must survive the reopen");
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_put_documents_failed_batch_recovers_applied_prefix() -> laurus::Result<()> {
use laurus::DynamicFieldPolicy;
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let schema = Schema::builder()
.add_field("title", FieldOption::Text(TextOption::default()))
.dynamic_field_policy(DynamicFieldPolicy::Strict)
.build();
{
let engine = Engine::new(storage.clone(), schema.clone()).await?;
let mut docs: Vec<_> = (0..3)
.map(|i| batch_entry(&format!("ok{i}"), &format!("title-{i}")))
.collect();
docs.push((
"bad".to_string(),
Document::builder()
.add_field("undeclared", DataValue::Text("boom".into()))
.build(),
));
docs.push(batch_entry("never", "never-applied"));
let err = engine
.put_documents(docs)
.await
.expect_err("Strict policy must fail the batch");
assert!(
matches!(
err,
LaurusError::BatchIngest {
failed_index: 3,
applied: 3,
..
}
),
"expected BatchIngest at index 3, got: {err}"
);
}
{
let engine = Engine::new(storage.clone(), schema.clone()).await?;
engine.commit().await?;
assert_eq!(
engine.stats()?.document_count,
3,
"exactly the applied prefix must survive the reopen"
);
for i in 0..3 {
let docs = engine.get_documents(&format!("ok{i}")).await?;
assert_eq!(docs.len(), 1, "applied doc ok{i} must be recovered");
}
assert!(
engine.get_documents("bad").await?.is_empty(),
"the failing doc must not exist after recovery"
);
assert!(
engine.get_documents("never").await?.is_empty(),
"docs after the failing one must not exist after recovery"
);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_add_documents_chunks_recover_after_reopen() -> laurus::Result<()> {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let schema = title_schema();
{
let engine = Engine::new(storage.clone(), schema.clone()).await?;
let docs: Vec<_> = (0..4)
.map(|i| batch_entry("doc", &format!("chunk-{i}")))
.collect();
engine.add_documents(docs).await?;
}
{
let engine = Engine::new(storage.clone(), schema.clone()).await?;
engine.commit().await?;
let chunks = engine.get_documents("doc").await?;
assert_eq!(
chunks.len(),
4,
"all four chunks must be replayed as chunks, not deduped"
);
}
Ok(())
}