admin: add bulk-rename command, remove # from all keys
Add `poc-memory admin bulk-rename FROM TO [--apply]` for bulk key character replacement. Uses rename_node() per key for proper capnp log persistence. Collision detection, progress reporting, auto-fsck. Applied: renamed 13,042 keys from # to - separator. This fixes the Claude Bash tool's inability to pass # in command arguments (the model confabulates that quoting doesn't work and gives up). 7 collision pairs resolved by deleting the # version before rename. 209 orphan edges pruned by fsck. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
e74f403192
commit
f8221286da
1 changed files with 79 additions and 0 deletions
|
|
@ -608,6 +608,17 @@ enum AdminCmd {
|
|||
#[arg(long)]
|
||||
apply: bool,
|
||||
},
|
||||
/// Bulk rename: replace a character in all keys
|
||||
#[command(name = "bulk-rename")]
|
||||
BulkRename {
|
||||
/// Character to replace
|
||||
from: String,
|
||||
/// Replacement character
|
||||
to: String,
|
||||
/// Apply changes (default: dry run)
|
||||
#[arg(long)]
|
||||
apply: bool,
|
||||
},
|
||||
/// Brief metrics check (for cron/notifications)
|
||||
#[command(name = "daily-check")]
|
||||
DailyCheck,
|
||||
|
|
@ -809,6 +820,7 @@ fn main() {
|
|||
AdminCmd::Health => cmd_health(),
|
||||
AdminCmd::Fsck => cmd_fsck(),
|
||||
AdminCmd::Dedup { apply } => cmd_dedup(apply),
|
||||
AdminCmd::BulkRename { from, to, apply } => cmd_bulk_rename(&from, &to, apply),
|
||||
AdminCmd::DailyCheck => cmd_daily_check(),
|
||||
AdminCmd::Import { files } => cmd_import(&files),
|
||||
AdminCmd::Export { files, all } => cmd_export(&files, all),
|
||||
|
|
@ -1014,6 +1026,73 @@ fn cmd_init() -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_bulk_rename(from: &str, to: &str, apply: bool) -> Result<(), String> {
|
||||
let mut store = store::Store::load()?;
|
||||
|
||||
// Find all keys that need renaming
|
||||
let renames: Vec<(String, String)> = store.nodes.keys()
|
||||
.filter(|k| k.contains(from))
|
||||
.map(|k| (k.clone(), k.replace(from, to)))
|
||||
.collect();
|
||||
|
||||
// Check for collisions
|
||||
let existing: std::collections::HashSet<&String> = store.nodes.keys().collect();
|
||||
let mut collisions = 0;
|
||||
for (old, new) in &renames {
|
||||
if existing.contains(new) && old != new {
|
||||
eprintln!("COLLISION: {} -> {} (target exists)", old, new);
|
||||
collisions += 1;
|
||||
}
|
||||
}
|
||||
|
||||
println!("Bulk rename '{}' -> '{}'", from, to);
|
||||
println!(" Keys to rename: {}", renames.len());
|
||||
println!(" Collisions: {}", collisions);
|
||||
|
||||
if collisions > 0 {
|
||||
return Err(format!("{} collisions — aborting", collisions));
|
||||
}
|
||||
|
||||
if !apply {
|
||||
// Show a sample
|
||||
for (old, new) in renames.iter().take(10) {
|
||||
println!(" {} -> {}", old, new);
|
||||
}
|
||||
if renames.len() > 10 {
|
||||
println!(" ... and {} more", renames.len() - 10);
|
||||
}
|
||||
println!("\nDry run. Use --apply to execute.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Apply renames using rename_node() which properly appends to capnp logs.
|
||||
// Process in batches to avoid holding the lock too long.
|
||||
let mut renamed_count = 0;
|
||||
let mut errors = 0;
|
||||
let total = renames.len();
|
||||
for (i, (old_key, new_key)) in renames.iter().enumerate() {
|
||||
match store.rename_node(old_key, new_key) {
|
||||
Ok(()) => renamed_count += 1,
|
||||
Err(e) => {
|
||||
eprintln!(" RENAME ERROR: {} -> {}: {}", old_key, new_key, e);
|
||||
errors += 1;
|
||||
}
|
||||
}
|
||||
if (i + 1) % 1000 == 0 {
|
||||
println!(" {}/{} ({} errors)", i + 1, total, errors);
|
||||
}
|
||||
}
|
||||
store.save()?;
|
||||
println!("Renamed {} nodes ({} errors).", renamed_count, errors);
|
||||
|
||||
// Run fsck to verify
|
||||
println!("\nRunning fsck...");
|
||||
drop(store);
|
||||
cmd_fsck()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_default_file(data_dir: &std::path::Path, name: &str, content: &str) -> Result<(), String> {
|
||||
let path = data_dir.join(name);
|
||||
if !path.exists() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue