store: strip .md suffix from all keys
Keys were a vestige of the file-based era. resolve_key() added .md to lookups while upsert() used bare keys, creating phantom duplicate nodes (the instructions bug: writes went to "instructions", reads found "instructions.md"). - Remove .md normalization from resolve_key, strip instead - Update all hardcoded key patterns (journal.md# → journal#, etc) - Add strip_md_keys() migration to fsck: renames nodes and relations - Add broken link detection to health report - Delete redirect table (no longer needed) - Update config defaults and config.jsonl Migration: run `poc-memory fsck` to rename existing keys. Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
This commit is contained in:
parent
77fc533631
commit
46f8fe662e
12 changed files with 289 additions and 132 deletions
|
|
@ -340,6 +340,138 @@ impl Store {
|
|||
}
|
||||
}
|
||||
|
||||
/// Strip .md suffix from all node keys and relation key strings.
|
||||
/// Merges duplicates (bare key + .md key) by keeping the latest version.
|
||||
pub fn strip_md_keys() -> Result<(), String> {
|
||||
use super::strip_md_suffix;
|
||||
|
||||
let mut store = Store::load()?;
|
||||
let mut renamed_nodes = 0usize;
|
||||
let mut renamed_rels = 0usize;
|
||||
let mut merged = 0usize;
|
||||
|
||||
// Collect keys that need renaming
|
||||
let old_keys: Vec<String> = store.nodes.keys()
|
||||
.filter(|k| k.ends_with(".md") || k.contains(".md#"))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for old_key in &old_keys {
|
||||
let new_key = strip_md_suffix(old_key);
|
||||
if new_key == *old_key { continue; }
|
||||
|
||||
let mut node = store.nodes.remove(old_key).unwrap();
|
||||
store.uuid_to_key.remove(&node.uuid);
|
||||
|
||||
if let Some(existing) = store.nodes.get(&new_key) {
|
||||
// Merge: keep whichever has the higher version
|
||||
if existing.version >= node.version {
|
||||
eprintln!(" merge {} → {} (keeping existing v{})",
|
||||
old_key, new_key, existing.version);
|
||||
merged += 1;
|
||||
continue;
|
||||
}
|
||||
eprintln!(" merge {} → {} (replacing v{} with v{})",
|
||||
old_key, new_key, existing.version, node.version);
|
||||
merged += 1;
|
||||
}
|
||||
|
||||
node.key = new_key.clone();
|
||||
node.version += 1;
|
||||
store.uuid_to_key.insert(node.uuid, new_key.clone());
|
||||
store.nodes.insert(new_key, node);
|
||||
renamed_nodes += 1;
|
||||
}
|
||||
|
||||
// Fix relation key strings
|
||||
for rel in &mut store.relations {
|
||||
let new_source = strip_md_suffix(&rel.source_key);
|
||||
let new_target = strip_md_suffix(&rel.target_key);
|
||||
if new_source != rel.source_key || new_target != rel.target_key {
|
||||
rel.source_key = new_source;
|
||||
rel.target_key = new_target;
|
||||
rel.version += 1;
|
||||
renamed_rels += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if renamed_nodes == 0 && renamed_rels == 0 && merged == 0 {
|
||||
eprintln!("No .md suffixes found — store is clean");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
eprintln!("Renamed {} nodes, {} relations, merged {} duplicates",
|
||||
renamed_nodes, renamed_rels, merged);
|
||||
|
||||
// Write fresh logs from the migrated state
|
||||
rewrite_store(&store)?;
|
||||
|
||||
eprintln!("Store rewritten successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rewrite the entire store from scratch (fresh logs + caches).
|
||||
/// Used after migrations that change keys across all nodes/relations.
|
||||
fn rewrite_store(store: &Store) -> Result<(), String> {
|
||||
let _lock = StoreLock::acquire()?;
|
||||
|
||||
// Write fresh node log
|
||||
let nodes: Vec<_> = store.nodes.values().cloned().collect();
|
||||
let nodes_path = nodes_path();
|
||||
{
|
||||
let file = fs::File::create(&nodes_path)
|
||||
.map_err(|e| format!("create {}: {}", nodes_path.display(), e))?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
// Write in chunks to keep message sizes reasonable
|
||||
for chunk in nodes.chunks(100) {
|
||||
let mut msg = message::Builder::new_default();
|
||||
{
|
||||
let log = msg.init_root::<memory_capnp::node_log::Builder>();
|
||||
let mut list = log.init_nodes(chunk.len() as u32);
|
||||
for (i, node) in chunk.iter().enumerate() {
|
||||
node.to_capnp(list.reborrow().get(i as u32));
|
||||
}
|
||||
}
|
||||
serialize::write_message(&mut writer, &msg)
|
||||
.map_err(|e| format!("write nodes: {}", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
// Write fresh relation log
|
||||
let rels_path = relations_path();
|
||||
{
|
||||
let file = fs::File::create(&rels_path)
|
||||
.map_err(|e| format!("create {}: {}", rels_path.display(), e))?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
let rels: Vec<_> = store.relations.iter().filter(|r| !r.deleted).cloned().collect();
|
||||
if !rels.is_empty() {
|
||||
for chunk in rels.chunks(100) {
|
||||
let mut msg = message::Builder::new_default();
|
||||
{
|
||||
let log = msg.init_root::<memory_capnp::relation_log::Builder>();
|
||||
let mut list = log.init_relations(chunk.len() as u32);
|
||||
for (i, rel) in chunk.iter().enumerate() {
|
||||
rel.to_capnp(list.reborrow().get(i as u32));
|
||||
}
|
||||
}
|
||||
serialize::write_message(&mut writer, &msg)
|
||||
.map_err(|e| format!("write relations: {}", e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nuke caches so next load rebuilds from fresh logs
|
||||
for p in [state_path(), snapshot_path()] {
|
||||
if p.exists() {
|
||||
fs::remove_file(&p).ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check and repair corrupt capnp log files.
|
||||
///
|
||||
/// Reads each message sequentially, tracking file position. On the first
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue