#![cfg(windows)]
#![allow(clippy::expect_used, clippy::panic)]
use tempfile::tempdir;
use zeppelin_embed::ingest::{
DeleteBatch, DocId, DocumentVersion, IngestBatch, IngestDocument, Revision,
};
use zeppelin_embed::lifecycle::durability::{CommitTier, DurabilityMode};
use zeppelin_embed::lifecycle::{OpenOptions, Store};
const DIMENSION: usize = 3;
fn vector(seed: f32) -> Vec<f32> {
vec![seed, seed + 1.0, seed + 2.0]
}
fn document(id: u128, revision: u64, seed: f32) -> IngestDocument {
IngestDocument::new(
DocumentVersion::new(DocId::new(id), Revision::new(revision)),
vector(seed),
)
}
fn options(mode: DurabilityMode, tier: CommitTier) -> OpenOptions {
OpenOptions::new().with_durability(mode, tier)
}
fn ingest(store: &Store, documents: Vec<IngestDocument>) -> u64 {
let acknowledgement = store
.ingest(IngestBatch::new(documents))
.expect("ingest must succeed");
acknowledgement.generation()
}
#[test]
fn every_supported_mode_and_tier_accepts_a_mutation() {
let rows = [
(DurabilityMode::Derived, CommitTier::None),
(DurabilityMode::Derived, CommitTier::Ordered),
(DurabilityMode::Derived, CommitTier::Durable),
(DurabilityMode::Durable, CommitTier::None),
(DurabilityMode::Durable, CommitTier::Ordered),
(DurabilityMode::Durable, CommitTier::Durable),
];
for (index, (mode, tier)) in rows.into_iter().enumerate() {
let directory = tempdir().expect("store directory");
let store = Store::open(directory.path(), options(mode, tier))
.unwrap_or_else(|error| panic!("open {mode:?}/{tier:?}: {error:?}"));
let generation = ingest(&store, vec![document(index as u128 + 1, 1, index as f32)]);
assert!(
generation > 0,
"{mode:?}/{tier:?} must report the generation it changed"
);
store
.close()
.unwrap_or_else(|error| panic!("close {mode:?}/{tier:?}: {error:?}"));
}
}
#[test]
fn attached_mode_remains_typed_unsupported() {
let directory = tempdir().expect("store directory");
let outcome = Store::open(
directory.path(),
options(DurabilityMode::Attached, CommitTier::Durable),
);
match outcome {
Err(error) => {
let rendered = format!("{error:?}");
assert!(
rendered.contains("Attached"),
"expected an Attached-specific refusal, observed {rendered}"
);
}
Ok(store) => {
let outcome = store.ingest(IngestBatch::new(vec![document(1, 1, 0.0)]));
assert!(
outcome.is_err(),
"Attached must not silently accept a mutation"
);
let _ = store.close();
}
}
}
#[test]
fn durable_mutations_survive_close_and_reopen() {
let directory = tempdir().expect("store directory");
let path = directory.path();
let store = Store::open(path, options(DurabilityMode::Durable, CommitTier::Durable))
.expect("open durable store");
let first = ingest(&store, vec![document(1, 1, 1.0), document(2, 1, 2.0)]);
let second = ingest(&store, vec![document(3, 1, 3.0)]);
assert!(
second > first,
"each acknowledged mutation must advance the generation: {first} then {second}"
);
store.close().expect("close durable store");
let reopened = Store::open(path, options(DurabilityMode::Durable, CommitTier::Durable))
.expect("reopen durable store");
let counted = reopened
.count_documents(None, None)
.expect("count after reopen");
assert_eq!(
counted.count, 3,
"every acknowledged document must survive the reopen"
);
reopened.close().expect("close reopened store");
}
#[test]
fn a_sealed_generation_survives_reopen() {
let directory = tempdir().expect("store directory");
let path = directory.path();
let store =
Store::open(path, options(DurabilityMode::Durable, CommitTier::Durable)).expect("open");
ingest(&store, vec![document(10, 1, 1.0), document(11, 1, 2.0)]);
let sealed = store.seal().expect("seal must publish");
assert!(sealed > 0, "seal must report the generation it published");
store.close().expect("close");
let reopened =
Store::open(path, options(DurabilityMode::Durable, CommitTier::Durable)).expect("reopen");
assert_eq!(
reopened.count_documents(None, None).expect("count").count,
2,
"sealed rows must be readable after reopen"
);
reopened.close().expect("close");
}
#[test]
fn revisions_and_deletes_replay_in_order_after_reopen() {
let directory = tempdir().expect("store directory");
let path = directory.path();
let store =
Store::open(path, options(DurabilityMode::Durable, CommitTier::Durable)).expect("open");
ingest(&store, vec![document(20, 1, 1.0), document(21, 1, 2.0)]);
store
.ingest(IngestBatch::new(vec![document(20, 2, 9.0)]))
.expect("supersede revision 1");
store
.delete(DeleteBatch::new(vec![DocId::new(21)]))
.expect("delete the second document");
store.close().expect("close");
let reopened =
Store::open(path, options(DurabilityMode::Durable, CommitTier::Durable)).expect("reopen");
assert_eq!(
reopened.count_documents(None, None).expect("count").count,
1,
"the deleted document must not come back"
);
reopened.close().expect("close");
}
#[test]
fn opening_under_a_missing_parent_fails_loudly() {
let directory = tempdir().expect("scratch");
let absent = directory.path().join("no-such-parent\u{0}bad");
let outcome = Store::open(
&absent,
options(DurabilityMode::Durable, CommitTier::Durable),
);
assert!(
outcome.is_err(),
"a path containing an interior NUL must be refused"
);
}
#[test]
fn a_second_writer_is_refused_while_the_first_is_open() {
let directory = tempdir().expect("store directory");
let path = directory.path();
let first = Store::open(path, options(DurabilityMode::Durable, CommitTier::Durable))
.expect("first writer");
let second = Store::open(path, options(DurabilityMode::Durable, CommitTier::Durable));
assert!(
second.is_err(),
"a second writer must be refused while the first holds the store"
);
first.close().expect("close the first writer");
let third = Store::open(path, options(DurabilityMode::Durable, CommitTier::Durable))
.expect("a writer is admitted once the first closes");
third.close().expect("close");
}
#[test]
fn a_store_under_a_non_ascii_path_completes_a_durable_round_trip() {
let directory = tempdir().expect("scratch");
let path = directory
.path()
.join("Documents et param\u{e8}tres")
.join("zeppelin \u{4e2d}\u{6587} store");
std::fs::create_dir_all(&path).expect("create a non-ASCII store directory");
let store = Store::open(&path, options(DurabilityMode::Durable, CommitTier::Durable))
.expect("open under a non-ASCII path");
ingest(&store, vec![document(30, 1, 1.0)]);
store.seal().expect("seal");
store.close().expect("close");
let reopened = Store::open(&path, options(DurabilityMode::Durable, CommitTier::Durable))
.expect("reopen under a non-ASCII path");
assert_eq!(
reopened.count_documents(None, None).expect("count").count,
1
);
reopened.close().expect("close");
}
#[test]
fn a_mismatched_vector_dimension_is_still_refused() {
let directory = tempdir().expect("store directory");
let store = Store::open(
directory.path(),
options(DurabilityMode::Durable, CommitTier::Durable),
)
.expect("open");
ingest(&store, vec![document(40, 1, 1.0)]);
let wrong = IngestDocument::new(
DocumentVersion::new(DocId::new(41), Revision::new(1)),
vec![1.0_f32; DIMENSION + 2],
);
assert!(
store.ingest(IngestBatch::new(vec![wrong])).is_err(),
"a mismatched dimension must be refused"
);
store.close().expect("close");
}