use topodb::*;
#[test]
#[ignore]
fn regenerate_v9_fixture() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v9.redb");
let _ = std::fs::remove_file(&path);
let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
{
let db = Db::open_with(&path, spec).unwrap();
let s = ScopeId::from_u128(1);
let n1 = NodeId::from_u128(10);
let n2 = NodeId::from_u128(11);
let e1 = EdgeId::from_u128(20);
let e2 = EdgeId::from_u128(21);
let mut p1 = Props::new();
p1.insert("name".into(), PropValue::Str("ada".into()));
let mut p2 = Props::new();
p2.insert(
"content".into(),
PropValue::Str("fixture memory about databases".into()),
);
db.submit(vec![
Op::CreateNode {
id: n1,
scope: Scope::Id(s),
label: "Entity".into(),
props: p1,
},
Op::CreateNode {
id: n2,
scope: Scope::Id(s),
label: "Memory".into(),
props: p2,
},
])
.unwrap();
db.submit(vec![Op::SetEmbedding {
id: n2,
model: "m1".into(),
vector: vec![1.0, 0.0],
}])
.unwrap();
db.submit_at(
vec![Op::CreateEdge {
id: e1,
scope: Scope::Id(s),
ty: "about".into(),
from: n1,
to: n2,
props: Props::new(),
valid_from: Some(1_000),
recorded_at: None, }],
5_000,
)
.unwrap();
db.submit_at(
vec![Op::CreateEdge {
id: e2,
scope: Scope::Id(s),
ty: "cites".into(),
from: n2,
to: n1,
props: Props::new(),
valid_from: None,
recorded_at: None,
}],
2_000,
)
.unwrap();
db.submit_at(
vec![Op::CloseEdge {
id: e2,
valid_to: None,
superseded_at: None,
}],
9_000,
)
.unwrap();
}
{
let mut raw = redb::Database::open(&path).unwrap();
raw.compact().unwrap();
}
assert!(
path.exists(),
"regenerate_v9_fixture: fixture file was not created at {path:?}"
);
}
#[test]
fn v5_fixture_migrates_to_v6_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v5.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v5.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(
db.nodes_by_prop_normalized(&scopes, "Entity", "name", &PropValue::Str(" ADA ".into()))
.unwrap()
.len(),
1,
"v5 normalized equality keys must match case/whitespace variants"
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(
db.nodes_by_label(&scopes, "Entity").len(),
1,
"recipe's known corpus: exactly one Entity node (n1)"
);
assert_eq!(
db.nodes_by_label(&scopes, "Memory").len(),
1,
"recipe's known corpus: exactly one Memory node (n2)"
);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
}
#[test]
fn v6_fixture_opens_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v6.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v6.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(db.nodes_by_label(&scopes, "Entity").len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Memory").len(), 1);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
}
#[test]
fn v6_fixture_migrates_to_v7_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v6.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v6.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(db.nodes_by_label(&scopes, "Entity").len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Memory").len(), 1);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
assert!(db.debug_dump_hnsw_meta().unwrap().is_empty());
assert!(db.debug_dump_hnsw_links().unwrap().is_empty());
}
#[test]
fn v7_fixture_opens_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v7.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v7.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(db.nodes_by_label(&scopes, "Entity").len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Memory").len(), 1);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
assert!(db.debug_dump_hnsw_meta().unwrap().is_empty());
assert!(db.debug_dump_hnsw_links().unwrap().is_empty());
}
fn quantize(v: &[f32]) -> (f32, Vec<i8>) {
let mut maxabs = 0.0f32;
for &x in v {
let a = x.abs();
if a > maxabs {
maxabs = a;
}
}
if maxabs == 0.0 || !maxabs.is_finite() {
return (0.0, vec![0i8; v.len()]);
}
let s = 127.0f32 / maxabs;
let codes = v
.iter()
.map(|&x| ((x * s).round() as i32).clamp(-127, 127) as i8)
.collect();
(maxabs, codes)
}
fn dequantize(scale: f32, codes: &[i8]) -> Vec<f32> {
codes.iter().map(|&c| c as f32 * scale / 127.0).collect()
}
#[test]
fn v8_fixture_migrates_to_v9_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v8.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v8.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(db.nodes_by_label(&scopes, "Entity").len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Memory").len(), 1);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
assert!(db.debug_dump_hnsw_meta().unwrap().is_empty());
assert!(db.debug_dump_hnsw_links().unwrap().is_empty());
let rows = db.debug_dump_vectors().unwrap();
assert_eq!(
rows.len(),
1,
"recipe's known corpus: exactly one embedding"
);
let (scale, codes) = quantize(&[1.0f32, 0.0]);
let expected = dequantize(scale, &codes);
assert_eq!(
rows[0].3, expected,
"stored embedding must dequantize to exactly dequantize(quantize([1.0, 0.0]))"
);
assert!(
db.debug_dump_edges().is_empty(),
"v8.redb's recipe has no edges — the v8->v9 EDGES pass has nothing to backfill here"
);
}
#[test]
fn v8_edges_fixture_migrates_to_v9_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v8-edges.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v8-edges.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![],
text: vec![],
};
let db = Db::open_with(&path, spec).unwrap();
assert_eq!(db.format_version(), 9);
let s = ScopeId::from_u128(1);
let scopes = ScopeSet::of(&[s]);
let n1 = NodeId::from_u128(10);
let n2 = NodeId::from_u128(11);
let e1 = EdgeId::from_u128(20);
let e2 = EdgeId::from_u128(21);
let edges = db.debug_dump_edges();
assert_eq!(edges.len(), 2, "fixture's known corpus: exactly two edges");
let edge1 = edges
.iter()
.find(|e| e.id == e1)
.expect("e1 must be present");
assert_eq!(edge1.from, n1);
assert_eq!(edge1.to, n2);
assert_eq!(edge1.valid_from, 1_000);
assert_eq!(edge1.valid_to, None, "e1 is open");
assert_eq!(
edge1.recorded_at, 1_000,
"copy rule: recorded_at := valid_from — the backdated-edge \
approximation, NOT e1's true write instant (5_000), which a \
pre-v9 op had nowhere to persist"
);
assert_eq!(edge1.superseded_at, None, "e1 was never closed");
let edge2 = edges
.iter()
.find(|e| e.id == e2)
.expect("e2 must be present");
assert_eq!(edge2.from, n2);
assert_eq!(edge2.to, n1);
assert_eq!(edge2.valid_from, 2_000);
assert_eq!(edge2.valid_to, Some(9_000));
assert_eq!(
edge2.recorded_at, 2_000,
"copy rule: recorded_at := valid_from — exact here, since e2 was \
never backdated"
);
assert_eq!(
edge2.superseded_at,
Some(9_000),
"copy rule: superseded_at := valid_to — exact here, since e2's \
close was never backdated either"
);
let changes = db.ops_since(1).unwrap();
let mut saw_create_e1 = false;
let mut saw_create_e2 = false;
let mut saw_close_e2 = false;
for change in &changes {
match change.op.as_ref() {
Op::CreateEdge {
id,
valid_from,
recorded_at,
..
} if *id == e1 => {
assert_eq!(*valid_from, Some(1_000));
assert_eq!(
*recorded_at,
Some(1_000),
"migrated CreateEdge op must carry the same copy-rule \
recorded_at as the EDGES row"
);
saw_create_e1 = true;
}
Op::CreateEdge {
id,
valid_from,
recorded_at,
..
} if *id == e2 => {
assert_eq!(*valid_from, Some(2_000));
assert_eq!(*recorded_at, Some(2_000));
saw_create_e2 = true;
}
Op::CloseEdge {
id,
valid_to,
superseded_at,
} if *id == e2 => {
assert_eq!(*valid_to, Some(9_000));
assert_eq!(*superseded_at, Some(9_000));
saw_close_e2 = true;
}
_ => {}
}
}
assert!(
saw_create_e1,
"migrated CreateEdge(e1) missing from change feed"
);
assert!(
saw_create_e2,
"migrated CreateEdge(e2) missing from change feed"
);
assert!(
saw_close_e2,
"migrated CloseEdge(e2) missing from change feed"
);
let recorded_open_from_n1 = db
.edges_from(&scopes, n1, None, None, true, TimeAxis::Recorded)
.unwrap();
assert_eq!(
recorded_open_from_n1.len(),
1,
"e1 (superseded_at = None) must be open under TimeAxis::Recorded"
);
assert_eq!(recorded_open_from_n1[0].id, e1);
let recorded_open_from_n2 = db
.edges_from(&scopes, n2, None, None, true, TimeAxis::Recorded)
.unwrap();
assert!(
recorded_open_from_n2.is_empty(),
"e2 (superseded_at = Some(9_000)) must NOT be open under TimeAxis::Recorded"
);
let valid_open_from_n1 = db
.edges_from(&scopes, n1, None, None, true, TimeAxis::Valid)
.unwrap();
assert_eq!(valid_open_from_n1.len(), 1);
assert_eq!(valid_open_from_n1[0].id, e1);
}
#[test]
fn v9_fixture_opens_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v9.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v9.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
let n1 = NodeId::from_u128(10);
let n2 = NodeId::from_u128(11);
let e1 = EdgeId::from_u128(20);
let e2 = EdgeId::from_u128(21);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(db.nodes_by_label(&scopes, "Entity").len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Memory").len(), 1);
assert_eq!(db.current_seq().unwrap(), 6);
assert_eq!(db.format_version(), 9);
assert!(db.debug_dump_hnsw_meta().unwrap().is_empty());
assert!(db.debug_dump_hnsw_links().unwrap().is_empty());
let rows = db.debug_dump_vectors().unwrap();
assert_eq!(
rows.len(),
1,
"recipe's known corpus: exactly one embedding"
);
let (scale, codes) = quantize(&[1.0f32, 0.0]);
let expected = dequantize(scale, &codes);
assert_eq!(
rows[0].3, expected,
"stored embedding must dequantize to exactly dequantize(quantize([1.0, 0.0]))"
);
let from_n1 = db
.edges_from(&scopes, n1, None, None, false, TimeAxis::Valid)
.unwrap();
assert_eq!(
from_n1.len(),
1,
"recipe's known corpus: exactly one edge from n1"
);
let edge1 = &from_n1[0];
assert_eq!(edge1.id, e1);
assert_eq!(edge1.to, n2);
assert_eq!(edge1.valid_from, 1_000);
assert_eq!(edge1.valid_to, None, "e1 is open");
assert_eq!(edge1.recorded_at, 5_000);
assert_eq!(edge1.superseded_at, None, "e1 was never closed");
let from_n2 = db
.edges_from(&scopes, n2, None, None, false, TimeAxis::Valid)
.unwrap();
assert_eq!(
from_n2.len(),
1,
"recipe's known corpus: exactly one edge from n2"
);
let edge2 = &from_n2[0];
assert_eq!(edge2.id, e2);
assert_eq!(edge2.to, n1);
assert_eq!(edge2.valid_from, 2_000);
assert_eq!(edge2.valid_to, Some(9_000));
assert_eq!(edge2.recorded_at, 2_000);
assert_eq!(edge2.superseded_at, Some(9_000));
}
#[test]
fn v1_fixture_opens_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v1.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v1.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let s = ScopeId::from_u128(1);
let scopes = ScopeSet::of(&[s]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(
db.nodes_by_label(&scopes, "Entity").len(),
1,
"v1->v6 migration: recipe's known corpus has exactly one Entity node (n1)"
);
assert_eq!(
db.nodes_by_label(&scopes, "Memory").len(),
1,
"v1->v6 migration: recipe's known corpus has exactly one Memory node (n2)"
);
assert_eq!(
db.debug_dump_label_index().unwrap().len(),
2,
"v1->v6 migration: LABEL_INDEX must have exactly one row per node (2 total), not be empty"
);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
drop(db);
let reopened = Db::open_with(
&path,
IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
},
)
.unwrap();
assert_eq!(reopened.current_seq().unwrap(), 3);
}
#[test]
fn v2_fixture_opens_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v2.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v2.redb");
std::fs::copy(&src, &path).unwrap();
let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Entity").len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Memory").len(), 1);
assert_eq!(
db.debug_dump_label_index().unwrap().len(),
2,
"v2->v6 migration: LABEL_INDEX must have exactly one row per node (2 total), not be empty"
);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
}
#[test]
fn v3_fixture_opens_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v3.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v3.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(db.nodes_by_label(&scopes, "Entity").len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Memory").len(), 1);
assert_eq!(
db.debug_dump_label_index().unwrap().len(),
2,
"v3->v6 migration: LABEL_INDEX must have exactly one row per node (2 total), not be empty"
);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
}
#[test]
fn v3_legacy_fixture_migrates_and_reads() {
let src =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v3-legacy.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v3-legacy.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(db.nodes_by_label(&scopes, "Entity").len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Memory").len(), 1);
assert_eq!(
db.debug_dump_label_index().unwrap().len(),
2,
"v3-legacy->v6 migration: LABEL_INDEX must have exactly one row per node (2 total), not be empty"
);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
}
#[test]
fn v4_fixture_opens_and_reads() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v4.redb");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v4.redb");
std::fs::copy(&src, &path).unwrap(); let spec = IndexSpec {
equality: vec![PropIndex {
label: "Entity".into(),
prop: "name".into(),
}],
text: vec![PropIndex {
label: "Memory".into(),
prop: "content".into(),
}],
};
let db = Db::open_with(&path, spec).unwrap();
let scopes = ScopeSet::of(&[ScopeId::from_u128(1)]);
assert_eq!(
db.nodes_by_prop(&scopes, "Entity", "name", &PropValue::Str("ada".into()))
.unwrap()
.len(),
1
);
assert_eq!(db.search_text(&scopes, "databases", 10).unwrap().len(), 1);
assert_eq!(
db.search_vector(&VectorQuery {
scopes: scopes.clone(),
model: "m1".into(),
vector: vec![1.0, 0.0],
k: 1,
candidates: None,
})
.unwrap()
.len(),
1
);
assert_eq!(db.nodes_by_label(&scopes, "Entity").len(), 1);
assert_eq!(db.nodes_by_label(&scopes, "Memory").len(), 1);
assert_eq!(
db.debug_dump_label_index().unwrap().len(),
2,
"v4->v6 migration: LABEL_INDEX must have exactly one row per node (2 total), not be empty"
);
assert_eq!(db.current_seq().unwrap(), 3);
assert_eq!(db.format_version(), 9);
}
#[test]
fn corrupt_hnsw_params_stamp_fails_open_with_encoding_error() {
use redb::TableDefinition;
const META: TableDefinition<&str, &[u8]> = TableDefinition::new("meta");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.redb");
let spec = IndexSpec {
equality: vec![],
text: vec![],
};
{
let db = Db::open_with(&path, spec.clone()).unwrap();
drop(db);
}
{
let raw = redb::Database::open(&path).unwrap();
let tx = raw.begin_write().unwrap();
{
let mut meta = tx.open_table(META).unwrap();
meta.insert("hnsw_params", [0xFFu8].as_slice()).unwrap();
}
tx.commit().unwrap();
}
let err = Db::open_with(&path, spec).unwrap_err();
assert!(
matches!(err, TopoError::Encoding(ref msg) if msg.contains("hnsw_params")),
"corrupt hnsw_params stamp must fail open with an Encoding error, got: {err:?}"
);
}