use topodb::*;
#[test]
fn subscriber_receives_applied_ops_with_monotonic_seq() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
let rx = db.subscribe(16);
let scope = Scope::Id(ScopeId::new());
let (a, b) = (NodeId::new(), NodeId::new());
db.submit(vec![
Op::CreateNode {
id: a,
scope,
label: "M".into(),
props: Default::default(),
},
Op::CreateNode {
id: b,
scope,
label: "M".into(),
props: Default::default(),
},
])
.unwrap();
let e1 = rx.recv().unwrap();
let e2 = rx.recv().unwrap();
assert_eq!((e1.seq, e2.seq), (1, 2));
assert!(matches!(e1.op.as_ref(), Op::CreateNode { id, .. } if *id == a));
}
#[test]
fn rejected_batches_and_reads_produce_no_events() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
let rx = db.subscribe(16);
assert!(db
.submit(vec![Op::CloseEdge {
id: EdgeId::new(),
valid_to: None
}])
.is_err());
let _ = db.nodes_by_label(&ScopeSet::of(&[ScopeId::new()]), "M");
assert!(
rx.try_recv().is_err(),
"no events for rejected batches or reads"
);
}
#[test]
fn ops_since_replays_the_log_and_covers_dropped_events() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
let rx = db.subscribe(1); let scope = Scope::Id(ScopeId::new());
db.submit(vec![
Op::CreateNode {
id: NodeId::new(),
scope,
label: "M".into(),
props: Default::default(),
},
Op::CreateNode {
id: NodeId::new(),
scope,
label: "M".into(),
props: Default::default(),
},
Op::CreateNode {
id: NodeId::new(),
scope,
label: "M".into(),
props: Default::default(),
},
])
.unwrap();
let first = rx.recv().unwrap();
assert_eq!(first.seq, 1);
let replay = db.ops_since(first.seq + 1).unwrap();
let seqs: Vec<u64> = replay.iter().map(|e| e.seq).collect();
assert_eq!(seqs, vec![2, 3]);
}
#[test]
fn subscribe_zero_capacity_still_delivers() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
let rx = db.subscribe(0); db.submit(vec![Op::CreateNode {
id: NodeId::new(),
scope: Scope::Id(ScopeId::new()),
label: "M".into(),
props: Default::default(),
}])
.unwrap();
assert_eq!(rx.recv().unwrap().seq, 1);
}
#[test]
fn rebuild_broadcasts_nothing() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
let rx = db.subscribe(16);
db.submit(vec![Op::CreateNode {
id: NodeId::new(),
scope: Scope::Id(ScopeId::new()),
label: "M".into(),
props: Default::default(),
}])
.unwrap();
let _ = rx.recv().unwrap();
db.rebuild_state_from_ops().unwrap();
assert!(rx.try_recv().is_err(), "rebuild must not broadcast");
}
#[test]
fn compaction_enforces_ops_since_contract() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
let scope = Scope::Id(ScopeId::new());
for _ in 0..5 {
db.submit(vec![Op::CreateNode {
id: NodeId::new(),
scope,
label: "M".into(),
props: Default::default(),
}])
.unwrap();
}
assert_eq!(db.current_seq().unwrap(), 5);
db.compact_ops(4).unwrap(); assert_eq!(db.ops_since(4).unwrap().len(), 2);
match db.ops_since(2) {
Err(TopoError::Compacted { oldest: 4 }) => {}
other => panic!("expected Compacted{{oldest:4}}, got {other:?}"),
}
match db.rebuild_state_from_ops() {
Err(TopoError::Compacted { oldest: 4 }) => {}
other => panic!("expected Compacted, got {other:?}"),
}
assert_eq!(db.current_seq().unwrap(), 5);
db.compact_ops(2).unwrap(); assert!(db.compact_ops(7).is_err()); db.compact_ops(6).unwrap(); assert!(db.ops_since(6).unwrap().is_empty());
}
#[test]
fn append_after_empty_compaction_resumes_at_the_floor() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
let scope = Scope::Id(ScopeId::new());
for _ in 0..5 {
db.submit(vec![Op::CreateNode {
id: NodeId::new(),
scope,
label: "M".into(),
props: Default::default(),
}])
.unwrap();
}
db.compact_ops(6).unwrap();
db.submit(vec![Op::CreateNode {
id: NodeId::new(),
scope,
label: "M".into(),
props: Default::default(),
}])
.unwrap();
assert_eq!(db.current_seq().unwrap(), 6);
let replay = db.ops_since(6).unwrap();
assert_eq!(replay.len(), 1);
assert_eq!(replay[0].seq, 6);
}
#[test]
fn ops_since_zero_replays_everything_on_uncompacted_log() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
db.submit(vec![Op::CreateNode {
id: NodeId::new(),
scope: Scope::Id(ScopeId::new()),
label: "M".into(),
props: Default::default(),
}])
.unwrap();
assert_eq!(db.ops_since(0).unwrap().len(), 1);
}
#[test]
fn current_seq_survives_empty_compaction_for_anchoring() {
let dir = tempfile::tempdir().unwrap();
let db = Db::open(dir.path().join("t.redb")).unwrap();
let scope = Scope::Id(ScopeId::new());
for _ in 0..3 {
db.submit(vec![Op::CreateNode {
id: NodeId::new(),
scope,
label: "M".into(),
props: Default::default(),
}])
.unwrap();
}
db.compact_ops(4).unwrap(); assert_eq!(db.current_seq().unwrap(), 3, "high-water mark survives");
assert_eq!(
db.ops_since(db.current_seq().unwrap() + 1).unwrap().len(),
0,
"anchor recipe must not spuriously Compacted"
);
db.submit(vec![Op::CreateNode {
id: NodeId::new(),
scope,
label: "M".into(),
props: Default::default(),
}])
.unwrap();
assert_eq!(db.current_seq().unwrap(), 4);
}
#[test]
fn unsupported_format_version_errors_at_open() {
use redb::{Database, TableDefinition};
const META: TableDefinition<&str, &[u8]> = TableDefinition::new("meta");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.redb");
{
let _ = Db::open(path.clone()).unwrap();
} {
let raw = Database::open(&path).unwrap();
let tx = raw.begin_write().unwrap();
{
let mut t = tx.open_table(META).unwrap();
t.insert("format_version", 999u32.to_le_bytes().as_slice())
.unwrap();
}
tx.commit().unwrap();
}
match Db::open(path) {
Err(TopoError::UnsupportedFormat {
found: 999,
supported: 1,
}) => {}
other => panic!("expected UnsupportedFormat, got {other:?}"),
}
}
#[test]
fn pre_v1_format_version_errors_at_open() {
use redb::{Database, TableDefinition};
const META: TableDefinition<&str, &[u8]> = TableDefinition::new("meta");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.redb");
{
let _ = Db::open(path.clone()).unwrap();
} {
let raw = Database::open(&path).unwrap();
let tx = raw.begin_write().unwrap();
{
let mut t = tx.open_table(META).unwrap();
t.insert("format_version", 0u32.to_le_bytes().as_slice())
.unwrap();
}
tx.commit().unwrap();
}
match Db::open(path) {
Err(TopoError::Encoding(_)) => {}
other => panic!("expected Encoding (guard-behavior pin, see comment), got {other:?}"),
}
}