use crate::disk::NodeRecordDiskV3;
use crate::error::{storage_err, TopoError};
use crate::storage::{label_index_key, LABEL_INDEX, NODES};
use redb::ReadableTable;
pub(crate) fn migrate_v5_to_v6(tx: &redb::WriteTransaction) -> Result<(), TopoError> {
let nodes = tx.open_table(NODES).map_err(storage_err)?;
let mut label_index = tx.open_table(LABEL_INDEX).map_err(storage_err)?;
for entry in nodes.iter().map_err(storage_err)? {
let (k, v) = entry.map_err(storage_err)?;
let slot_bytes: [u8; 8] = k
.value()
.try_into()
.map_err(|_| TopoError::Encoding("bad node slot key".into()))?;
let slot = u64::from_be_bytes(slot_bytes);
let raw = crate::codec::unframe_value(v.value())?;
let disk: NodeRecordDiskV3 =
postcard::from_bytes(raw.as_ref()).map_err(|e| TopoError::Encoding(e.to_string()))?;
let key = label_index_key(disk.label, disk.scope, disk.id);
label_index
.insert(key.as_slice(), slot)
.map_err(storage_err)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dict::{DictKind, Dicts, InternJournal};
use crate::disk::node_to_disk_v3;
use crate::ids::{NodeId, Scope, ScopeId};
use crate::props::Props;
use crate::scopes::{seed_shared, ScopeRegistry, SCOPES};
use crate::slots::{alloc_node_slot, EDGE_IDS, EDGE_SLOTS, NODE_IDS, NODE_SLOTS};
use crate::state::NodeRecord;
use redb::ReadableTableMetadata;
#[test]
fn migrate_v5_to_v6_builds_label_index_from_nodes_scan() {
let dir = tempfile::tempdir().unwrap();
let db = redb::Database::create(dir.path().join("t.redb")).unwrap();
let n1 = NodeId::from_u128(1);
let n2 = NodeId::from_u128(2);
let scope = Scope::Id(ScopeId::from_u128(9));
{
let tx = db.begin_write().unwrap();
{
let mut nodes = tx.open_table(crate::storage::NODES).unwrap();
let mut dict = tx.open_table(crate::dict::DICT).unwrap();
let mut scopes = tx.open_table(SCOPES).unwrap();
seed_shared(&mut scopes).unwrap();
let mut scope_registry = ScopeRegistry::load_table_for_rebuild(&scopes).unwrap();
let mut dicts = Dicts::default();
let mut journal = InternJournal::default();
let mut slot_meta = tx.open_table(crate::storage::META).unwrap();
let mut node_slots = tx.open_table(NODE_SLOTS).unwrap();
let mut node_ids = tx.open_table(NODE_IDS).unwrap();
let _ = tx.open_table(EDGE_SLOTS).unwrap();
let _ = tx.open_table(EDGE_IDS).unwrap();
for (id, label) in [(n1, "Entity"), (n2, "Memory")] {
let slot = alloc_node_slot(&mut slot_meta, &mut node_slots, &mut node_ids, id)
.unwrap();
let rec = NodeRecord {
id,
scope,
label: label.into(),
props: Props::new(),
embedding: None,
};
let disk = node_to_disk_v3(
&rec,
&mut dict,
&mut dicts,
&mut scopes,
&mut scope_registry,
&mut journal,
)
.unwrap();
let raw = postcard::to_allocvec(&disk).unwrap();
let framed = crate::codec::frame_value(raw);
nodes
.insert(crate::storage::slot_key(slot).as_slice(), framed.as_slice())
.unwrap();
}
}
migrate_v5_to_v6(&tx).unwrap();
tx.commit().unwrap();
}
let tx = db.begin_read().unwrap();
let label_index = tx.open_table(LABEL_INDEX).unwrap();
assert_eq!(label_index.len().unwrap(), 2, "one row per node");
let dict = tx.open_table(crate::dict::DICT).unwrap();
let dicts = Dicts::load_from_table(&dict).unwrap();
let scopes_table = tx.open_table(SCOPES).unwrap();
let scope_registry = ScopeRegistry::load_table_for_rebuild(&scopes_table).unwrap();
let label_id = dicts.id_of(DictKind::Label, "Entity").unwrap();
let scope_id = scope_registry.id_of(scope).unwrap();
let key = label_index_key(label_id, scope_id, n1);
assert!(
label_index.get(key.as_slice()).unwrap().is_some(),
"n1's LABEL_INDEX row must exist under (Entity, scope, n1)"
);
}
}