From 05ca2a2ead9b1a3570df2482358ebdfd4f084b29 Mon Sep 17 00:00:00 2001 From: Austin Richardson Date: Fri, 10 Apr 2026 16:02:41 -0700 Subject: [PATCH] =?UTF-8?q?[=F0=9F=A4=96]=20Fix=20remove()=20corrupting=20?= =?UTF-8?q?parent=20indices=20and=20leaking=20data=20array=20(DEV-9780)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base.rs | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/base.rs b/src/base.rs index 77c377a..73c1772 100644 --- a/src/base.rs +++ b/src/base.rs @@ -292,11 +292,14 @@ impl GeneralTaxonomy { self.parent_distances.remove(idx); self.ranks.remove(idx); self.names.remove(idx); - - // everything after `tax_id` in parents needs to get decremented by 1 - // because we've changed the actual array size - for parent in self.parent_ids.iter_mut().skip(idx + 1) { - if *parent > 0 { + self.data.remove(idx); + + // all parent indices pointing to positions after the removed node must be + // decremented by 1, because the array shifted. This applies to every element + // (not just those after idx), since an out-of-order taxonomy can have parents + // with larger indices than their children. + for parent in self.parent_ids.iter_mut() { + if *parent > idx { *parent -= 1; } } @@ -523,6 +526,30 @@ mod tests { assert!(tax.remove("1").is_err()); } + #[test] + fn remove_then_add_then_prune_preserves_nodes() { + use crate::edit::prune_to; + + // Reproduce DEV-9780: removing a node, adding it back under a new parent, + // then pruning should not lose unrelated nodes or panic. + let mut tax = create_test_taxonomy(); + + // Remove "2" (Bacteria), which is a child of root and parent of "562" + tax.remove("2").unwrap(); + + // Add "2" back directly under root + tax.add("1", "2").unwrap(); + + // Prune to keep "562" (E. coli). Before the fix, "562" was unreachable + // from root after the remove+add, so the pruned taxonomy was empty. + let pruned = prune_to(&tax, &["562"], false).unwrap(); + + // "562" must be present and reachable + assert!(pruned.to_internal_index("562").is_ok()); + // Root must be present + assert_eq!(Taxonomy::<&str>::root(&pruned), "1"); + } + #[test] fn errors_on_taxonomy_with_cycle() { let example = r#"{