From ec1442cf79d3308911ac24136a4569920f66bfca Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 06:40:31 +0700 Subject: [PATCH 1/9] Reuse parent Merk when deleting non-empty subtrees --- grovedb-version/src/version/v3.rs | 2 +- grovedb/src/operations/delete/mod.rs | 285 +++++++++++++++++++++------ 2 files changed, 228 insertions(+), 59 deletions(-) diff --git a/grovedb-version/src/version/v3.rs b/grovedb-version/src/version/v3.rs index 039891bd1..a901ae7e4 100644 --- a/grovedb-version/src/version/v3.rs +++ b/grovedb-version/src/version/v3.rs @@ -117,7 +117,7 @@ pub const GROVE_V3: GroveVersion = GroveVersion { delete_if_empty_tree: 0, delete_if_empty_tree_with_sectional_storage_function: 0, delete_operation_for_delete_internal: 0, - delete_internal_on_transaction: 0, + delete_internal_on_transaction: 1, delete_internal_without_transaction: 0, average_case_delete_operation_for_delete: 0, worst_case_delete_operation_for_delete: 0, diff --git a/grovedb/src/operations/delete/mod.rs b/grovedb/src/operations/delete/mod.rs index 7ea218411..09cf6c279 100644 --- a/grovedb/src/operations/delete/mod.rs +++ b/grovedb/src/operations/delete/mod.rs @@ -44,7 +44,9 @@ use grovedb_path::SubtreePath; use grovedb_storage::{ rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, }; -use grovedb_version::{check_grovedb_v0_with_cost, version::GroveVersion}; +use grovedb_version::{ + check_grovedb_v0_or_v1_with_cost, check_grovedb_v0_with_cost, version::GroveVersion, +}; use crate::util::{compat, TxRef}; #[cfg(feature = "minimal")] @@ -722,13 +724,14 @@ impl GroveDb { batch: &StorageBatch, grove_version: &GroveVersion, ) -> CostResult { - check_grovedb_v0_with_cost!( + let delete_internal_version = grove_version + .grovedb_versions + .operations + .delete + .delete_internal_on_transaction; + check_grovedb_v0_or_v1_with_cost!( "delete_internal_on_transaction", - grove_version - .grovedb_versions - .operations - .delete - .delete_internal_on_transaction + delete_internal_version ); let mut cost = OperationCost::default(); @@ -746,7 +749,7 @@ impl GroveDb { grove_version ) ); - let uses_sum_tree = subtree_to_delete_from.tree_type; + let parent_tree_type = subtree_to_delete_from.tree_type; if let Some(tree_type) = element.tree_type() { let subtree_merk_path = path.derive_owned_with_child(key); let subtree_merk_path_ref = SubtreePath::from(&subtree_merk_path); @@ -838,55 +841,87 @@ impl GroveDb { ); } } - // todo: verify why we need to open the same? merk again - let storage = self - .db - .get_transactional_storage_context(path.clone(), Some(batch), transaction) - .unwrap_add_cost(&mut cost); + if delete_internal_version == 0 { + // Legacy behavior reopened the parent layer using the + // child tree type before deleting the tree element. + let storage = self + .db + .get_transactional_storage_context(path.clone(), Some(batch), transaction) + .unwrap_add_cost(&mut cost); - let mut merk_to_delete_tree_from = cost_return_on_error!( - &mut cost, - Merk::open_layered_with_root_key( - storage, - subtree_to_delete_from.root_key(), - tree_type, - Some(&Element::value_defined_cost_for_serialized_value), - grove_version, - ) - .map_err(|e| { - Error::CorruptedData(format!( - "cannot open a subtree with given root key: {e}" - )) - }) - ); - // We are deleting a tree, a tree uses 3 bytes - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut merk_to_delete_tree_from, - key, - Some(options.as_merk_options()), - true, - uses_sum_tree, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap< - SubtreePath, - Merk, - > = HashMap::default(); - merk_cache.insert(path.clone(), merk_to_delete_tree_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_batch_transaction( - batch, - merk_cache, - &path, - transaction, - grove_version, - ) - ); + let mut merk_to_delete_tree_from = cost_return_on_error!( + &mut cost, + Merk::open_layered_with_root_key( + storage, + subtree_to_delete_from.root_key(), + tree_type, + Some(&Element::value_defined_cost_for_serialized_value), + grove_version, + ) + .map_err(|e| { + Error::CorruptedData(format!( + "cannot open a subtree with given root key: {e}" + )) + }) + ); + // We are deleting a tree, a tree uses 3 bytes + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut merk_to_delete_tree_from, + key, + Some(options.as_merk_options()), + true, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap< + SubtreePath, + Merk, + > = HashMap::default(); + merk_cache.insert(path.clone(), merk_to_delete_tree_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_batch_transaction( + batch, + merk_cache, + &path, + transaction, + grove_version, + ) + ); + } else { + // We are deleting a tree, a tree uses 3 bytes + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut subtree_to_delete_from, + key, + Some(options.as_merk_options()), + true, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap< + SubtreePath, + Merk, + > = HashMap::default(); + merk_cache.insert(path.clone(), subtree_to_delete_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_batch_transaction( + batch, + merk_cache, + &path, + transaction, + grove_version, + ) + ); + } } else { // We are deleting a tree, a tree uses 3 bytes cost_return_on_error_into!( @@ -896,7 +931,7 @@ impl GroveDb { key, Some(options.as_merk_options()), true, - uses_sum_tree, + parent_tree_type, sectioned_removal, grove_version, ) @@ -925,7 +960,7 @@ impl GroveDb { key, Some(options.as_merk_options()), false, - uses_sum_tree, + parent_tree_type, sectioned_removal, grove_version, ) @@ -1511,6 +1546,140 @@ mod tests { .is_ok()); } + #[test] + fn test_non_empty_tree_delete_under_count_tree_parent_updates_count() { + let grove_version = GroveVersion::latest(); + let db = make_test_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"parent", + Element::empty_count_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful count tree insert"); + db.insert( + [TEST_LEAF, b"parent"].as_ref(), + b"child", + Element::empty_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful child tree insert"); + db.insert( + [TEST_LEAF, b"parent", b"child"].as_ref(), + b"leaf", + Element::new_item(b"value".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful child item insert"); + + let before = db + .get([TEST_LEAF].as_ref(), b"parent", None, grove_version) + .unwrap() + .expect("expected parent count tree"); + assert!(matches!(before, Element::CountTree(_, 1, _))); + + db.delete( + [TEST_LEAF, b"parent"].as_ref(), + b"child", + Some(DeleteOptions { + allow_deleting_non_empty_trees: true, + deleting_non_empty_trees_returns_error: false, + ..Default::default() + }), + None, + grove_version, + ) + .unwrap() + .expect("delete non-empty child tree"); + + let after = db + .get([TEST_LEAF].as_ref(), b"parent", None, grove_version) + .unwrap() + .expect("expected parent count tree"); + assert!(matches!(after, Element::CountTree(_, 0, _))); + let issues = db + .verify_grovedb(None, false, true, grove_version) + .expect("verify grovedb"); + assert!(issues.is_empty(), "verification issues: {:?}", issues); + } + + #[test] + fn test_non_empty_tree_delete_under_count_sum_tree_parent_updates_count_and_sum() { + let grove_version = GroveVersion::latest(); + let db = make_test_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"parent", + Element::empty_count_sum_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful count sum tree insert"); + db.insert( + [TEST_LEAF, b"parent"].as_ref(), + b"child", + Element::empty_sum_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful child sum tree insert"); + db.insert( + [TEST_LEAF, b"parent", b"child"].as_ref(), + b"leaf", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful child sum item insert"); + + let before = db + .get([TEST_LEAF].as_ref(), b"parent", None, grove_version) + .unwrap() + .expect("expected parent count sum tree"); + assert!(matches!(before, Element::CountSumTree(_, 1, 7, _))); + + db.delete( + [TEST_LEAF, b"parent"].as_ref(), + b"child", + Some(DeleteOptions { + allow_deleting_non_empty_trees: true, + deleting_non_empty_trees_returns_error: false, + ..Default::default() + }), + None, + grove_version, + ) + .unwrap() + .expect("delete non-empty child tree"); + + let after = db + .get([TEST_LEAF].as_ref(), b"parent", None, grove_version) + .unwrap() + .expect("expected parent count sum tree"); + assert!(matches!(after, Element::CountSumTree(_, 0, 0, _))); + let issues = db + .verify_grovedb(None, false, true, grove_version) + .expect("verify grovedb"); + assert!(issues.is_empty(), "verification issues: {:?}", issues); + } + #[test] fn test_item_deletion() { let grove_version = GroveVersion::latest(); From 03123adc0051122a3840a5144589041a84441fc9 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 18:46:19 +0700 Subject: [PATCH 2/9] test(delete): cover legacy subtree delete path --- grovedb/src/operations/delete/mod.rs | 64 +++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/grovedb/src/operations/delete/mod.rs b/grovedb/src/operations/delete/mod.rs index 09cf6c279..88624eab7 100644 --- a/grovedb/src/operations/delete/mod.rs +++ b/grovedb/src/operations/delete/mod.rs @@ -991,7 +991,7 @@ mod tests { storage_cost::{removal::StorageRemovedBytes::BasicStorageRemoval, StorageCost}, OperationCost, }; - use grovedb_version::version::GroveVersion; + use grovedb_version::version::{v2::GROVE_V2, GroveVersion}; use pretty_assertions::assert_eq; use crate::{ @@ -1613,6 +1613,68 @@ mod tests { assert!(issues.is_empty(), "verification issues: {:?}", issues); } + #[test] + fn test_legacy_non_empty_tree_delete_keeps_version_0_path() { + let grove_version = &GROVE_V2; + let db = make_test_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"parent", + Element::empty_count_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful count tree insert"); + db.insert( + [TEST_LEAF, b"parent"].as_ref(), + b"child", + Element::empty_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful child tree insert"); + db.insert( + [TEST_LEAF, b"parent", b"child"].as_ref(), + b"leaf", + Element::new_item(b"value".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful child item insert"); + + db.delete( + [TEST_LEAF, b"parent"].as_ref(), + b"child", + Some(DeleteOptions { + allow_deleting_non_empty_trees: true, + deleting_non_empty_trees_returns_error: false, + ..Default::default() + }), + None, + grove_version, + ) + .unwrap() + .expect("legacy delete non-empty child tree"); + + assert!(matches!( + db.get( + [TEST_LEAF, b"parent"].as_ref(), + b"child", + None, + grove_version + ) + .unwrap(), + Err(Error::PathKeyNotFound(_)) + )); + } + #[test] fn test_non_empty_tree_delete_under_count_sum_tree_parent_updates_count_and_sum() { let grove_version = GroveVersion::latest(); From cbdded1e30c2449d3c25b20363e43a1f0de86f90 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 19:08:14 +0700 Subject: [PATCH 3/9] test(delete): cover legacy reopen error mapping --- grovedb/src/operations/delete/mod.rs | 33 ++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/grovedb/src/operations/delete/mod.rs b/grovedb/src/operations/delete/mod.rs index 88624eab7..704bed694 100644 --- a/grovedb/src/operations/delete/mod.rs +++ b/grovedb/src/operations/delete/mod.rs @@ -55,6 +55,13 @@ use crate::{ Element, ElementFlags, Error, GroveDb, Transaction, TransactionArg, }; +#[cfg(feature = "minimal")] +fn cannot_open_subtree_with_root_key_error(error: MerkError) -> Error { + Error::CorruptedData(format!( + "cannot open a subtree with given root key: {error}" + )) +} + #[cfg(feature = "minimal")] #[derive(Clone)] /// Clear options @@ -729,10 +736,8 @@ impl GroveDb { .operations .delete .delete_internal_on_transaction; - check_grovedb_v0_or_v1_with_cost!( - "delete_internal_on_transaction", - delete_internal_version - ); + #[rustfmt::skip] + check_grovedb_v0_or_v1_with_cost!("delete_internal_on_transaction", delete_internal_version); let mut cost = OperationCost::default(); @@ -858,11 +863,7 @@ impl GroveDb { Some(&Element::value_defined_cost_for_serialized_value), grove_version, ) - .map_err(|e| { - Error::CorruptedData(format!( - "cannot open a subtree with given root key: {e}" - )) - }) + .map_err(cannot_open_subtree_with_root_key_error) ); // We are deleting a tree, a tree uses 3 bytes cost_return_on_error_into!( @@ -1003,6 +1004,20 @@ mod tests { Element, Error, }; + #[test] + fn test_open_subtree_root_key_error_mapping() { + let err = super::cannot_open_subtree_with_root_key_error( + grovedb_merk::Error::CorruptedState("bad root"), + ); + + assert!(matches!( + err, + Error::CorruptedData(message) + if message.contains("cannot open a subtree with given root key") + && message.contains("bad root") + )); + } + #[test] fn test_empty_subtree_deletion_without_transaction() { let grove_version = GroveVersion::latest(); From 01bc8ceb3a50ca8e0e2edf66cd2da1bce78d9117 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 19:10:03 +0700 Subject: [PATCH 4/9] refactor(delete): mod/v0/v1 dispatcher for delete_internal_on_transaction Replace the inline `if version == 0` branch with the established mod.rs-dispatcher + v0.rs/v1.rs pattern used elsewhere (e.g. merk add_average_case_merk_propagate). v0 preserves grove v1/v2 behavior (reopen parent with child tree type); v1 (grove v3+) reuses the open parent merk so aggregate propagation uses the parent tree type. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../delete_internal_on_transaction/mod.rs | 79 +++++ .../delete_internal_on_transaction/v0.rs | 268 +++++++++++++++++ .../delete_internal_on_transaction/v1.rs | 243 +++++++++++++++ grovedb/src/operations/delete/mod.rs | 276 +----------------- 4 files changed, 593 insertions(+), 273 deletions(-) create mode 100644 grovedb/src/operations/delete/delete_internal_on_transaction/mod.rs create mode 100644 grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs create mode 100644 grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/mod.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/mod.rs new file mode 100644 index 000000000..472af18f0 --- /dev/null +++ b/grovedb/src/operations/delete/delete_internal_on_transaction/mod.rs @@ -0,0 +1,79 @@ +//! Versioned dispatch for [`GroveDb::delete_internal_on_transaction`]. +//! +//! Each `v*.rs` defines one `delete_internal_on_transaction_v*` method. The +//! dispatcher below selects the implementation by +//! `GroveVersion::grovedb_versions.operations.delete.delete_internal_on_transaction`. +//! +//! `v0` preserves the historical (grove v1/v2) behavior: when deleting a +//! non-empty tree it reopened the parent layer using the *child* tree type, +//! which propagated parent aggregate (count/sum) metadata incorrectly. `v1` +//! (grove v3+) reuses the already-open parent merk so propagation uses the +//! parent tree type. See issue #686. + +mod v0; +mod v1; + +use grovedb_costs::{ + storage_cost::removal::StorageRemovedBytes, CostResult, CostsExt, OperationCost, +}; +use grovedb_merk::Error as MerkError; +use grovedb_path::SubtreePath; +use grovedb_storage::StorageBatch; +use grovedb_version::{error::GroveVersionError, version::GroveVersion}; + +use super::DeleteOptions; +use crate::{Error, GroveDb, Transaction}; + +impl GroveDb { + pub(super) fn delete_internal_on_transaction>( + &self, + path: SubtreePath, + key: &[u8], + options: &DeleteOptions, + transaction: &Transaction, + sectioned_removal: &mut impl FnMut( + &Vec, + u32, + u32, + ) -> Result< + (StorageRemovedBytes, StorageRemovedBytes), + MerkError, + >, + batch: &StorageBatch, + grove_version: &GroveVersion, + ) -> CostResult { + match grove_version + .grovedb_versions + .operations + .delete + .delete_internal_on_transaction + { + 0 => self.delete_internal_on_transaction_v0( + path, + key, + options, + transaction, + sectioned_removal, + batch, + grove_version, + ), + 1 => self.delete_internal_on_transaction_v1( + path, + key, + options, + transaction, + sectioned_removal, + batch, + grove_version, + ), + version => Err(Error::VersionError( + GroveVersionError::UnknownVersionMismatch { + method: "delete_internal_on_transaction".to_string(), + known_versions: vec![0, 1], + received: version, + }, + )) + .wrap_with_cost(OperationCost::default()), + } + } +} diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs new file mode 100644 index 000000000..35f5359d0 --- /dev/null +++ b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs @@ -0,0 +1,268 @@ +//! `delete_internal_on_transaction` v0 — grove v1/v2 dispatch path +//! (historical baseline). +//! +//! When deleting a non-empty tree element, this reopens the parent layer using +//! the *child* tree type (`tree_type`) before deleting. This propagates parent +//! aggregate/root metadata incorrectly for aggregate parents (CountTree, +//! SumTree, CountSumTree, ...). It is preserved verbatim for replay +//! compatibility; the corrected behavior lives in +//! [`super::v1`](super::v1) (grove v3+). See issue #686. + +use std::collections::HashMap; + +use grovedb_costs::{ + cost_return_on_error, cost_return_on_error_into, storage_cost::removal::StorageRemovedBytes, + CostResult, CostsExt, OperationCost, +}; +use grovedb_merk::{ + element::{ + costs::ElementCostExtensions, delete::ElementDeleteFromStorageExtensions, + tree_type::ElementTreeTypeExtensions, + }, + Error as MerkError, Merk, +}; +use grovedb_path::SubtreePath; +use grovedb_storage::{ + rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, +}; +use grovedb_version::version::GroveVersion; + +use super::super::DeleteOptions; +use crate::{Element, Error, GroveDb, Transaction}; + +impl GroveDb { + pub(super) fn delete_internal_on_transaction_v0>( + &self, + path: SubtreePath, + key: &[u8], + options: &DeleteOptions, + transaction: &Transaction, + sectioned_removal: &mut impl FnMut( + &Vec, + u32, + u32, + ) -> Result< + (StorageRemovedBytes, StorageRemovedBytes), + MerkError, + >, + batch: &StorageBatch, + grove_version: &GroveVersion, + ) -> CostResult { + let mut cost = OperationCost::default(); + + let element = cost_return_on_error!( + &mut cost, + self.get_raw(path.clone(), key.as_ref(), Some(transaction), grove_version) + ); + let mut subtree_to_delete_from = cost_return_on_error!( + &mut cost, + self.open_transactional_merk_at_path( + path.clone(), + transaction, + Some(batch), + grove_version + ) + ); + let parent_tree_type = subtree_to_delete_from.tree_type; + if let Some(tree_type) = element.tree_type() { + let subtree_merk_path = path.derive_owned_with_child(key); + let subtree_merk_path_ref = SubtreePath::from(&subtree_merk_path); + + // Tree types that store data in the data namespace as non-Merk + // entries (CommitmentTree, MmrTree, BulkAppendTree, DenseTree) + // have an always-empty Merk but may have data. We cannot iterate + // their storage with find_subtrees because the entries are not + // valid Element serializations. + let non_merk_data = element.uses_non_merk_data_storage(); + + let subtree_of_tree_we_are_deleting = cost_return_on_error!( + &mut cost, + self.open_transactional_merk_at_path( + subtree_merk_path_ref.clone(), + transaction, + Some(batch), + grove_version, + ) + ); + + // For non-Merk data trees the raw_iter check would see non-Merk + // keys and wrongly report the tree as non-empty. Use the + // element's own count instead. + let is_empty = if non_merk_data { + element.non_merk_entry_count().unwrap_or(0) == 0 + } else { + subtree_of_tree_we_are_deleting + .is_empty_tree() + .unwrap_add_cost(&mut cost) + }; + + if !options.allow_deleting_non_empty_trees && !is_empty { + return if options.deleting_non_empty_trees_returns_error { + Err(Error::DeletingNonEmptyTree( + "trying to do a delete operation for a non empty tree, but options not \ + allowing this", + )) + .wrap_with_cost(cost) + } else { + Ok(false).wrap_with_cost(cost) + }; + } + + if !is_empty { + if non_merk_data { + // Non-Merk data trees: clear the subtree storage directly. + // These trees never contain child subtrees so we only need + // to clear the one storage context. + let mut storage = self + .db + .get_transactional_storage_context( + subtree_merk_path_ref.clone(), + Some(batch), + transaction, + ) + .unwrap_add_cost(&mut cost); + cost_return_on_error!( + &mut cost, + storage.clear().map_err(|e| { + Error::CorruptedData(format!( + "unable to cleanup non-merk tree data from storage: {e}", + )) + }) + ); + } else { + let subtrees_paths = cost_return_on_error!( + &mut cost, + self.find_subtrees( + &subtree_merk_path_ref, + Some(transaction), + grove_version + ) + ); + for subtree_path in subtrees_paths { + let p: SubtreePath<_> = subtree_path.as_slice().into(); + let mut storage = self + .db + .get_transactional_storage_context(p, Some(batch), transaction) + .unwrap_add_cost(&mut cost); + + cost_return_on_error!( + &mut cost, + storage.clear().map_err(|e| { + Error::CorruptedData(format!( + "unable to cleanup tree from storage: {e}", + )) + }) + ); + } + } + // Legacy behavior reopened the parent layer using the + // child tree type before deleting the tree element. + let storage = self + .db + .get_transactional_storage_context(path.clone(), Some(batch), transaction) + .unwrap_add_cost(&mut cost); + + let mut merk_to_delete_tree_from = cost_return_on_error!( + &mut cost, + Merk::open_layered_with_root_key( + storage, + subtree_to_delete_from.root_key(), + tree_type, + Some(&Element::value_defined_cost_for_serialized_value), + grove_version, + ) + .map_err(|e| { + Error::CorruptedData(format!( + "cannot open a subtree with given root key: {e}" + )) + }) + ); + // We are deleting a tree, a tree uses 3 bytes + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut merk_to_delete_tree_from, + key, + Some(options.as_merk_options()), + true, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap< + SubtreePath, + Merk, + > = HashMap::default(); + merk_cache.insert(path.clone(), merk_to_delete_tree_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_batch_transaction( + batch, + merk_cache, + &path, + transaction, + grove_version, + ) + ); + } else { + // We are deleting a tree, a tree uses 3 bytes + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut subtree_to_delete_from, + key, + Some(options.as_merk_options()), + true, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap< + SubtreePath, + Merk, + > = HashMap::default(); + merk_cache.insert(path.clone(), subtree_to_delete_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_transaction( + merk_cache, + path, + transaction, + batch, + grove_version + ) + ); + } + } else { + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut subtree_to_delete_from, + key, + Some(options.as_merk_options()), + false, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap, Merk> = + HashMap::default(); + merk_cache.insert(path.clone(), subtree_to_delete_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_transaction( + merk_cache, + path, + transaction, + batch, + grove_version + ) + ); + } + + Ok(true).wrap_with_cost(cost) + } +} diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs new file mode 100644 index 000000000..428551e0b --- /dev/null +++ b/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs @@ -0,0 +1,243 @@ +//! `delete_internal_on_transaction` v1 — grove v3+ dispatch path. +//! +//! Fixes issue #686: when deleting a non-empty tree element, the parent layer +//! is no longer reopened with the *child* tree type. Instead the already-open +//! parent merk (`subtree_to_delete_from`) is reused, so aggregate/root +//! propagation uses the parent tree type. The historical behavior is preserved +//! in [`super::v0`](super::v0) for grove v1/v2 replay compatibility. + +use std::collections::HashMap; + +use grovedb_costs::{ + cost_return_on_error, cost_return_on_error_into, storage_cost::removal::StorageRemovedBytes, + CostResult, CostsExt, OperationCost, +}; +use grovedb_merk::{ + element::{delete::ElementDeleteFromStorageExtensions, tree_type::ElementTreeTypeExtensions}, + Error as MerkError, Merk, +}; +use grovedb_path::SubtreePath; +use grovedb_storage::{ + rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, +}; +use grovedb_version::version::GroveVersion; + +use super::super::DeleteOptions; +use crate::{Element, Error, GroveDb, Transaction}; + +impl GroveDb { + pub(super) fn delete_internal_on_transaction_v1>( + &self, + path: SubtreePath, + key: &[u8], + options: &DeleteOptions, + transaction: &Transaction, + sectioned_removal: &mut impl FnMut( + &Vec, + u32, + u32, + ) -> Result< + (StorageRemovedBytes, StorageRemovedBytes), + MerkError, + >, + batch: &StorageBatch, + grove_version: &GroveVersion, + ) -> CostResult { + let mut cost = OperationCost::default(); + + let element = cost_return_on_error!( + &mut cost, + self.get_raw(path.clone(), key.as_ref(), Some(transaction), grove_version) + ); + let mut subtree_to_delete_from = cost_return_on_error!( + &mut cost, + self.open_transactional_merk_at_path( + path.clone(), + transaction, + Some(batch), + grove_version + ) + ); + let parent_tree_type = subtree_to_delete_from.tree_type; + if element.tree_type().is_some() { + let subtree_merk_path = path.derive_owned_with_child(key); + let subtree_merk_path_ref = SubtreePath::from(&subtree_merk_path); + + // Tree types that store data in the data namespace as non-Merk + // entries (CommitmentTree, MmrTree, BulkAppendTree, DenseTree) + // have an always-empty Merk but may have data. We cannot iterate + // their storage with find_subtrees because the entries are not + // valid Element serializations. + let non_merk_data = element.uses_non_merk_data_storage(); + + let subtree_of_tree_we_are_deleting = cost_return_on_error!( + &mut cost, + self.open_transactional_merk_at_path( + subtree_merk_path_ref.clone(), + transaction, + Some(batch), + grove_version, + ) + ); + + // For non-Merk data trees the raw_iter check would see non-Merk + // keys and wrongly report the tree as non-empty. Use the + // element's own count instead. + let is_empty = if non_merk_data { + element.non_merk_entry_count().unwrap_or(0) == 0 + } else { + subtree_of_tree_we_are_deleting + .is_empty_tree() + .unwrap_add_cost(&mut cost) + }; + + if !options.allow_deleting_non_empty_trees && !is_empty { + return if options.deleting_non_empty_trees_returns_error { + Err(Error::DeletingNonEmptyTree( + "trying to do a delete operation for a non empty tree, but options not \ + allowing this", + )) + .wrap_with_cost(cost) + } else { + Ok(false).wrap_with_cost(cost) + }; + } + + if !is_empty { + if non_merk_data { + // Non-Merk data trees: clear the subtree storage directly. + // These trees never contain child subtrees so we only need + // to clear the one storage context. + let mut storage = self + .db + .get_transactional_storage_context( + subtree_merk_path_ref.clone(), + Some(batch), + transaction, + ) + .unwrap_add_cost(&mut cost); + cost_return_on_error!( + &mut cost, + storage.clear().map_err(|e| { + Error::CorruptedData(format!( + "unable to cleanup non-merk tree data from storage: {e}", + )) + }) + ); + } else { + let subtrees_paths = cost_return_on_error!( + &mut cost, + self.find_subtrees( + &subtree_merk_path_ref, + Some(transaction), + grove_version + ) + ); + for subtree_path in subtrees_paths { + let p: SubtreePath<_> = subtree_path.as_slice().into(); + let mut storage = self + .db + .get_transactional_storage_context(p, Some(batch), transaction) + .unwrap_add_cost(&mut cost); + + cost_return_on_error!( + &mut cost, + storage.clear().map_err(|e| { + Error::CorruptedData(format!( + "unable to cleanup tree from storage: {e}", + )) + }) + ); + } + } + // Reuse the already-open parent merk so propagation keeps the + // parent tree type instead of reopening with the child type. + // We are deleting a tree, a tree uses 3 bytes + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut subtree_to_delete_from, + key, + Some(options.as_merk_options()), + true, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap< + SubtreePath, + Merk, + > = HashMap::default(); + merk_cache.insert(path.clone(), subtree_to_delete_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_batch_transaction( + batch, + merk_cache, + &path, + transaction, + grove_version, + ) + ); + } else { + // We are deleting a tree, a tree uses 3 bytes + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut subtree_to_delete_from, + key, + Some(options.as_merk_options()), + true, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap< + SubtreePath, + Merk, + > = HashMap::default(); + merk_cache.insert(path.clone(), subtree_to_delete_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_transaction( + merk_cache, + path, + transaction, + batch, + grove_version + ) + ); + } + } else { + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut subtree_to_delete_from, + key, + Some(options.as_merk_options()), + false, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap, Merk> = + HashMap::default(); + merk_cache.insert(path.clone(), subtree_to_delete_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_transaction( + merk_cache, + path, + transaction, + batch, + grove_version + ) + ); + } + + Ok(true).wrap_with_cost(cost) + } +} diff --git a/grovedb/src/operations/delete/mod.rs b/grovedb/src/operations/delete/mod.rs index 704bed694..f0c19c0c7 100644 --- a/grovedb/src/operations/delete/mod.rs +++ b/grovedb/src/operations/delete/mod.rs @@ -15,6 +15,8 @@ #[cfg(feature = "estimated_costs")] mod average_case; #[cfg(feature = "minimal")] +mod delete_internal_on_transaction; +#[cfg(feature = "minimal")] mod delete_up_tree; #[cfg(feature = "estimated_costs")] mod worst_case; @@ -44,9 +46,7 @@ use grovedb_path::SubtreePath; use grovedb_storage::{ rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, }; -use grovedb_version::{ - check_grovedb_v0_or_v1_with_cost, check_grovedb_v0_with_cost, version::GroveVersion, -}; +use grovedb_version::{check_grovedb_v0_with_cost, version::GroveVersion}; use crate::util::{compat, TxRef}; #[cfg(feature = "minimal")] @@ -713,276 +713,6 @@ impl GroveDb { } } } - - fn delete_internal_on_transaction>( - &self, - path: SubtreePath, - key: &[u8], - options: &DeleteOptions, - transaction: &Transaction, - sectioned_removal: &mut impl FnMut( - &Vec, - u32, - u32, - ) -> Result< - (StorageRemovedBytes, StorageRemovedBytes), - MerkError, - >, - batch: &StorageBatch, - grove_version: &GroveVersion, - ) -> CostResult { - let delete_internal_version = grove_version - .grovedb_versions - .operations - .delete - .delete_internal_on_transaction; - #[rustfmt::skip] - check_grovedb_v0_or_v1_with_cost!("delete_internal_on_transaction", delete_internal_version); - - let mut cost = OperationCost::default(); - - let element = cost_return_on_error!( - &mut cost, - self.get_raw(path.clone(), key.as_ref(), Some(transaction), grove_version) - ); - let mut subtree_to_delete_from = cost_return_on_error!( - &mut cost, - self.open_transactional_merk_at_path( - path.clone(), - transaction, - Some(batch), - grove_version - ) - ); - let parent_tree_type = subtree_to_delete_from.tree_type; - if let Some(tree_type) = element.tree_type() { - let subtree_merk_path = path.derive_owned_with_child(key); - let subtree_merk_path_ref = SubtreePath::from(&subtree_merk_path); - - // Tree types that store data in the data namespace as non-Merk - // entries (CommitmentTree, MmrTree, BulkAppendTree, DenseTree) - // have an always-empty Merk but may have data. We cannot iterate - // their storage with find_subtrees because the entries are not - // valid Element serializations. - let non_merk_data = element.uses_non_merk_data_storage(); - - let subtree_of_tree_we_are_deleting = cost_return_on_error!( - &mut cost, - self.open_transactional_merk_at_path( - subtree_merk_path_ref.clone(), - transaction, - Some(batch), - grove_version, - ) - ); - - // For non-Merk data trees the raw_iter check would see non-Merk - // keys and wrongly report the tree as non-empty. Use the - // element's own count instead. - let is_empty = if non_merk_data { - element.non_merk_entry_count().unwrap_or(0) == 0 - } else { - subtree_of_tree_we_are_deleting - .is_empty_tree() - .unwrap_add_cost(&mut cost) - }; - - if !options.allow_deleting_non_empty_trees && !is_empty { - return if options.deleting_non_empty_trees_returns_error { - Err(Error::DeletingNonEmptyTree( - "trying to do a delete operation for a non empty tree, but options not \ - allowing this", - )) - .wrap_with_cost(cost) - } else { - Ok(false).wrap_with_cost(cost) - }; - } - - if !is_empty { - if non_merk_data { - // Non-Merk data trees: clear the subtree storage directly. - // These trees never contain child subtrees so we only need - // to clear the one storage context. - let mut storage = self - .db - .get_transactional_storage_context( - subtree_merk_path_ref.clone(), - Some(batch), - transaction, - ) - .unwrap_add_cost(&mut cost); - cost_return_on_error!( - &mut cost, - storage.clear().map_err(|e| { - Error::CorruptedData(format!( - "unable to cleanup non-merk tree data from storage: {e}", - )) - }) - ); - } else { - let subtrees_paths = cost_return_on_error!( - &mut cost, - self.find_subtrees( - &subtree_merk_path_ref, - Some(transaction), - grove_version - ) - ); - for subtree_path in subtrees_paths { - let p: SubtreePath<_> = subtree_path.as_slice().into(); - let mut storage = self - .db - .get_transactional_storage_context(p, Some(batch), transaction) - .unwrap_add_cost(&mut cost); - - cost_return_on_error!( - &mut cost, - storage.clear().map_err(|e| { - Error::CorruptedData(format!( - "unable to cleanup tree from storage: {e}", - )) - }) - ); - } - } - if delete_internal_version == 0 { - // Legacy behavior reopened the parent layer using the - // child tree type before deleting the tree element. - let storage = self - .db - .get_transactional_storage_context(path.clone(), Some(batch), transaction) - .unwrap_add_cost(&mut cost); - - let mut merk_to_delete_tree_from = cost_return_on_error!( - &mut cost, - Merk::open_layered_with_root_key( - storage, - subtree_to_delete_from.root_key(), - tree_type, - Some(&Element::value_defined_cost_for_serialized_value), - grove_version, - ) - .map_err(cannot_open_subtree_with_root_key_error) - ); - // We are deleting a tree, a tree uses 3 bytes - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut merk_to_delete_tree_from, - key, - Some(options.as_merk_options()), - true, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap< - SubtreePath, - Merk, - > = HashMap::default(); - merk_cache.insert(path.clone(), merk_to_delete_tree_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_batch_transaction( - batch, - merk_cache, - &path, - transaction, - grove_version, - ) - ); - } else { - // We are deleting a tree, a tree uses 3 bytes - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut subtree_to_delete_from, - key, - Some(options.as_merk_options()), - true, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap< - SubtreePath, - Merk, - > = HashMap::default(); - merk_cache.insert(path.clone(), subtree_to_delete_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_batch_transaction( - batch, - merk_cache, - &path, - transaction, - grove_version, - ) - ); - } - } else { - // We are deleting a tree, a tree uses 3 bytes - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut subtree_to_delete_from, - key, - Some(options.as_merk_options()), - true, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap< - SubtreePath, - Merk, - > = HashMap::default(); - merk_cache.insert(path.clone(), subtree_to_delete_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_transaction( - merk_cache, - path, - transaction, - batch, - grove_version - ) - ); - } - } else { - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut subtree_to_delete_from, - key, - Some(options.as_merk_options()), - false, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap, Merk> = - HashMap::default(); - merk_cache.insert(path.clone(), subtree_to_delete_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_transaction( - merk_cache, - path, - transaction, - batch, - grove_version - ) - ); - } - - Ok(true).wrap_with_cost(cost) - } } #[cfg(feature = "minimal")] From f844b5d561e38c51d16220c6dd01319fdc26215b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 19:15:14 +0700 Subject: [PATCH 5/9] style(delete): drop imports orphaned by the dispatcher split cost_return_on_error_into, ElementCostExtensions and ElementDeleteFromStorageExtensions are no longer used in delete/mod.rs (moved into v0/v1); StorageContext is unused in v0/v1 since clear() is inherent. Required for CI's clippy -D warnings. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../operations/delete/delete_internal_on_transaction/v0.rs | 4 +--- .../operations/delete/delete_internal_on_transaction/v1.rs | 4 +--- grovedb/src/operations/delete/mod.rs | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs index 35f5359d0..d0d21be3d 100644 --- a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs +++ b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs @@ -22,9 +22,7 @@ use grovedb_merk::{ Error as MerkError, Merk, }; use grovedb_path::SubtreePath; -use grovedb_storage::{ - rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, -}; +use grovedb_storage::{rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch}; use grovedb_version::version::GroveVersion; use super::super::DeleteOptions; diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs index 428551e0b..6c6e023c8 100644 --- a/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs +++ b/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs @@ -17,9 +17,7 @@ use grovedb_merk::{ Error as MerkError, Merk, }; use grovedb_path::SubtreePath; -use grovedb_storage::{ - rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, -}; +use grovedb_storage::{rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch}; use grovedb_version::version::GroveVersion; use super::super::DeleteOptions; diff --git a/grovedb/src/operations/delete/mod.rs b/grovedb/src/operations/delete/mod.rs index f0c19c0c7..404fe1af7 100644 --- a/grovedb/src/operations/delete/mod.rs +++ b/grovedb/src/operations/delete/mod.rs @@ -26,7 +26,6 @@ use std::collections::{BTreeSet, HashMap}; #[cfg(feature = "minimal")] pub use delete_up_tree::DeleteUpTreeOptions; -use grovedb_costs::cost_return_on_error_into; #[cfg(feature = "minimal")] use grovedb_costs::{ cost_return_on_error, @@ -34,8 +33,7 @@ use grovedb_costs::{ CostResult, CostsExt, OperationCost, }; use grovedb_merk::element::{ - costs::ElementCostExtensions, decode::ElementDecodeExtensions, - delete::ElementDeleteFromStorageExtensions, tree_type::ElementTreeTypeExtensions, + decode::ElementDecodeExtensions, tree_type::ElementTreeTypeExtensions, }; #[cfg(feature = "minimal")] use grovedb_merk::{proofs::Query, KVIterator, MaybeTree}; From 5bbb7afcc8326f38ae0a7749609e998baa277e1b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 19:19:39 +0700 Subject: [PATCH 6/9] refactor(delete): use cannot_open_subtree_with_root_key_error in v0 The legacy reopen path now lives in v0.rs; route its map_err through the shared helper added in cbdded1e so the helper is exercised by production code (not just its unit test). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../operations/delete/delete_internal_on_transaction/v0.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs index d0d21be3d..b15dc5676 100644 --- a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs +++ b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs @@ -169,11 +169,7 @@ impl GroveDb { Some(&Element::value_defined_cost_for_serialized_value), grove_version, ) - .map_err(|e| { - Error::CorruptedData(format!( - "cannot open a subtree with given root key: {e}" - )) - }) + .map_err(super::super::cannot_open_subtree_with_root_key_error) ); // We are deleting a tree, a tree uses 3 bytes cost_return_on_error_into!( From d76128a6a89ad1b051eac27a976fd4d9bff59424 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 19:34:34 +0700 Subject: [PATCH 7/9] Revert "refactor(delete): use cannot_open_subtree_with_root_key_error in v0" This reverts commit 5bbb7afcc8326f38ae0a7749609e998baa277e1b. --- .../operations/delete/delete_internal_on_transaction/v0.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs index b15dc5676..d0d21be3d 100644 --- a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs +++ b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs @@ -169,7 +169,11 @@ impl GroveDb { Some(&Element::value_defined_cost_for_serialized_value), grove_version, ) - .map_err(super::super::cannot_open_subtree_with_root_key_error) + .map_err(|e| { + Error::CorruptedData(format!( + "cannot open a subtree with given root key: {e}" + )) + }) ); // We are deleting a tree, a tree uses 3 bytes cost_return_on_error_into!( From 6a684b418fb7f1f4b78c87c83e180cd3a5d8f021 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 19:34:34 +0700 Subject: [PATCH 8/9] Revert "style(delete): drop imports orphaned by the dispatcher split" This reverts commit f844b5d561e38c51d16220c6dd01319fdc26215b. --- .../operations/delete/delete_internal_on_transaction/v0.rs | 4 +++- .../operations/delete/delete_internal_on_transaction/v1.rs | 4 +++- grovedb/src/operations/delete/mod.rs | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs index d0d21be3d..35f5359d0 100644 --- a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs +++ b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs @@ -22,7 +22,9 @@ use grovedb_merk::{ Error as MerkError, Merk, }; use grovedb_path::SubtreePath; -use grovedb_storage::{rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch}; +use grovedb_storage::{ + rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, +}; use grovedb_version::version::GroveVersion; use super::super::DeleteOptions; diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs index 6c6e023c8..428551e0b 100644 --- a/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs +++ b/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs @@ -17,7 +17,9 @@ use grovedb_merk::{ Error as MerkError, Merk, }; use grovedb_path::SubtreePath; -use grovedb_storage::{rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch}; +use grovedb_storage::{ + rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, +}; use grovedb_version::version::GroveVersion; use super::super::DeleteOptions; diff --git a/grovedb/src/operations/delete/mod.rs b/grovedb/src/operations/delete/mod.rs index 404fe1af7..f0c19c0c7 100644 --- a/grovedb/src/operations/delete/mod.rs +++ b/grovedb/src/operations/delete/mod.rs @@ -26,6 +26,7 @@ use std::collections::{BTreeSet, HashMap}; #[cfg(feature = "minimal")] pub use delete_up_tree::DeleteUpTreeOptions; +use grovedb_costs::cost_return_on_error_into; #[cfg(feature = "minimal")] use grovedb_costs::{ cost_return_on_error, @@ -33,7 +34,8 @@ use grovedb_costs::{ CostResult, CostsExt, OperationCost, }; use grovedb_merk::element::{ - decode::ElementDecodeExtensions, tree_type::ElementTreeTypeExtensions, + costs::ElementCostExtensions, decode::ElementDecodeExtensions, + delete::ElementDeleteFromStorageExtensions, tree_type::ElementTreeTypeExtensions, }; #[cfg(feature = "minimal")] use grovedb_merk::{proofs::Query, KVIterator, MaybeTree}; From 38944e8cfbb29e2543d5dfe8d61976133521fb46 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 21 May 2026 19:34:34 +0700 Subject: [PATCH 9/9] Revert "refactor(delete): mod/v0/v1 dispatcher for delete_internal_on_transaction" This reverts commit 01bc8ceb3a50ca8e0e2edf66cd2da1bce78d9117. --- .../delete_internal_on_transaction/mod.rs | 79 ----- .../delete_internal_on_transaction/v0.rs | 268 ----------------- .../delete_internal_on_transaction/v1.rs | 243 --------------- grovedb/src/operations/delete/mod.rs | 276 +++++++++++++++++- 4 files changed, 273 insertions(+), 593 deletions(-) delete mode 100644 grovedb/src/operations/delete/delete_internal_on_transaction/mod.rs delete mode 100644 grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs delete mode 100644 grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/mod.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/mod.rs deleted file mode 100644 index 472af18f0..000000000 --- a/grovedb/src/operations/delete/delete_internal_on_transaction/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Versioned dispatch for [`GroveDb::delete_internal_on_transaction`]. -//! -//! Each `v*.rs` defines one `delete_internal_on_transaction_v*` method. The -//! dispatcher below selects the implementation by -//! `GroveVersion::grovedb_versions.operations.delete.delete_internal_on_transaction`. -//! -//! `v0` preserves the historical (grove v1/v2) behavior: when deleting a -//! non-empty tree it reopened the parent layer using the *child* tree type, -//! which propagated parent aggregate (count/sum) metadata incorrectly. `v1` -//! (grove v3+) reuses the already-open parent merk so propagation uses the -//! parent tree type. See issue #686. - -mod v0; -mod v1; - -use grovedb_costs::{ - storage_cost::removal::StorageRemovedBytes, CostResult, CostsExt, OperationCost, -}; -use grovedb_merk::Error as MerkError; -use grovedb_path::SubtreePath; -use grovedb_storage::StorageBatch; -use grovedb_version::{error::GroveVersionError, version::GroveVersion}; - -use super::DeleteOptions; -use crate::{Error, GroveDb, Transaction}; - -impl GroveDb { - pub(super) fn delete_internal_on_transaction>( - &self, - path: SubtreePath, - key: &[u8], - options: &DeleteOptions, - transaction: &Transaction, - sectioned_removal: &mut impl FnMut( - &Vec, - u32, - u32, - ) -> Result< - (StorageRemovedBytes, StorageRemovedBytes), - MerkError, - >, - batch: &StorageBatch, - grove_version: &GroveVersion, - ) -> CostResult { - match grove_version - .grovedb_versions - .operations - .delete - .delete_internal_on_transaction - { - 0 => self.delete_internal_on_transaction_v0( - path, - key, - options, - transaction, - sectioned_removal, - batch, - grove_version, - ), - 1 => self.delete_internal_on_transaction_v1( - path, - key, - options, - transaction, - sectioned_removal, - batch, - grove_version, - ), - version => Err(Error::VersionError( - GroveVersionError::UnknownVersionMismatch { - method: "delete_internal_on_transaction".to_string(), - known_versions: vec![0, 1], - received: version, - }, - )) - .wrap_with_cost(OperationCost::default()), - } - } -} diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs deleted file mode 100644 index 35f5359d0..000000000 --- a/grovedb/src/operations/delete/delete_internal_on_transaction/v0.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! `delete_internal_on_transaction` v0 — grove v1/v2 dispatch path -//! (historical baseline). -//! -//! When deleting a non-empty tree element, this reopens the parent layer using -//! the *child* tree type (`tree_type`) before deleting. This propagates parent -//! aggregate/root metadata incorrectly for aggregate parents (CountTree, -//! SumTree, CountSumTree, ...). It is preserved verbatim for replay -//! compatibility; the corrected behavior lives in -//! [`super::v1`](super::v1) (grove v3+). See issue #686. - -use std::collections::HashMap; - -use grovedb_costs::{ - cost_return_on_error, cost_return_on_error_into, storage_cost::removal::StorageRemovedBytes, - CostResult, CostsExt, OperationCost, -}; -use grovedb_merk::{ - element::{ - costs::ElementCostExtensions, delete::ElementDeleteFromStorageExtensions, - tree_type::ElementTreeTypeExtensions, - }, - Error as MerkError, Merk, -}; -use grovedb_path::SubtreePath; -use grovedb_storage::{ - rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, -}; -use grovedb_version::version::GroveVersion; - -use super::super::DeleteOptions; -use crate::{Element, Error, GroveDb, Transaction}; - -impl GroveDb { - pub(super) fn delete_internal_on_transaction_v0>( - &self, - path: SubtreePath, - key: &[u8], - options: &DeleteOptions, - transaction: &Transaction, - sectioned_removal: &mut impl FnMut( - &Vec, - u32, - u32, - ) -> Result< - (StorageRemovedBytes, StorageRemovedBytes), - MerkError, - >, - batch: &StorageBatch, - grove_version: &GroveVersion, - ) -> CostResult { - let mut cost = OperationCost::default(); - - let element = cost_return_on_error!( - &mut cost, - self.get_raw(path.clone(), key.as_ref(), Some(transaction), grove_version) - ); - let mut subtree_to_delete_from = cost_return_on_error!( - &mut cost, - self.open_transactional_merk_at_path( - path.clone(), - transaction, - Some(batch), - grove_version - ) - ); - let parent_tree_type = subtree_to_delete_from.tree_type; - if let Some(tree_type) = element.tree_type() { - let subtree_merk_path = path.derive_owned_with_child(key); - let subtree_merk_path_ref = SubtreePath::from(&subtree_merk_path); - - // Tree types that store data in the data namespace as non-Merk - // entries (CommitmentTree, MmrTree, BulkAppendTree, DenseTree) - // have an always-empty Merk but may have data. We cannot iterate - // their storage with find_subtrees because the entries are not - // valid Element serializations. - let non_merk_data = element.uses_non_merk_data_storage(); - - let subtree_of_tree_we_are_deleting = cost_return_on_error!( - &mut cost, - self.open_transactional_merk_at_path( - subtree_merk_path_ref.clone(), - transaction, - Some(batch), - grove_version, - ) - ); - - // For non-Merk data trees the raw_iter check would see non-Merk - // keys and wrongly report the tree as non-empty. Use the - // element's own count instead. - let is_empty = if non_merk_data { - element.non_merk_entry_count().unwrap_or(0) == 0 - } else { - subtree_of_tree_we_are_deleting - .is_empty_tree() - .unwrap_add_cost(&mut cost) - }; - - if !options.allow_deleting_non_empty_trees && !is_empty { - return if options.deleting_non_empty_trees_returns_error { - Err(Error::DeletingNonEmptyTree( - "trying to do a delete operation for a non empty tree, but options not \ - allowing this", - )) - .wrap_with_cost(cost) - } else { - Ok(false).wrap_with_cost(cost) - }; - } - - if !is_empty { - if non_merk_data { - // Non-Merk data trees: clear the subtree storage directly. - // These trees never contain child subtrees so we only need - // to clear the one storage context. - let mut storage = self - .db - .get_transactional_storage_context( - subtree_merk_path_ref.clone(), - Some(batch), - transaction, - ) - .unwrap_add_cost(&mut cost); - cost_return_on_error!( - &mut cost, - storage.clear().map_err(|e| { - Error::CorruptedData(format!( - "unable to cleanup non-merk tree data from storage: {e}", - )) - }) - ); - } else { - let subtrees_paths = cost_return_on_error!( - &mut cost, - self.find_subtrees( - &subtree_merk_path_ref, - Some(transaction), - grove_version - ) - ); - for subtree_path in subtrees_paths { - let p: SubtreePath<_> = subtree_path.as_slice().into(); - let mut storage = self - .db - .get_transactional_storage_context(p, Some(batch), transaction) - .unwrap_add_cost(&mut cost); - - cost_return_on_error!( - &mut cost, - storage.clear().map_err(|e| { - Error::CorruptedData(format!( - "unable to cleanup tree from storage: {e}", - )) - }) - ); - } - } - // Legacy behavior reopened the parent layer using the - // child tree type before deleting the tree element. - let storage = self - .db - .get_transactional_storage_context(path.clone(), Some(batch), transaction) - .unwrap_add_cost(&mut cost); - - let mut merk_to_delete_tree_from = cost_return_on_error!( - &mut cost, - Merk::open_layered_with_root_key( - storage, - subtree_to_delete_from.root_key(), - tree_type, - Some(&Element::value_defined_cost_for_serialized_value), - grove_version, - ) - .map_err(|e| { - Error::CorruptedData(format!( - "cannot open a subtree with given root key: {e}" - )) - }) - ); - // We are deleting a tree, a tree uses 3 bytes - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut merk_to_delete_tree_from, - key, - Some(options.as_merk_options()), - true, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap< - SubtreePath, - Merk, - > = HashMap::default(); - merk_cache.insert(path.clone(), merk_to_delete_tree_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_batch_transaction( - batch, - merk_cache, - &path, - transaction, - grove_version, - ) - ); - } else { - // We are deleting a tree, a tree uses 3 bytes - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut subtree_to_delete_from, - key, - Some(options.as_merk_options()), - true, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap< - SubtreePath, - Merk, - > = HashMap::default(); - merk_cache.insert(path.clone(), subtree_to_delete_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_transaction( - merk_cache, - path, - transaction, - batch, - grove_version - ) - ); - } - } else { - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut subtree_to_delete_from, - key, - Some(options.as_merk_options()), - false, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap, Merk> = - HashMap::default(); - merk_cache.insert(path.clone(), subtree_to_delete_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_transaction( - merk_cache, - path, - transaction, - batch, - grove_version - ) - ); - } - - Ok(true).wrap_with_cost(cost) - } -} diff --git a/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs b/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs deleted file mode 100644 index 428551e0b..000000000 --- a/grovedb/src/operations/delete/delete_internal_on_transaction/v1.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! `delete_internal_on_transaction` v1 — grove v3+ dispatch path. -//! -//! Fixes issue #686: when deleting a non-empty tree element, the parent layer -//! is no longer reopened with the *child* tree type. Instead the already-open -//! parent merk (`subtree_to_delete_from`) is reused, so aggregate/root -//! propagation uses the parent tree type. The historical behavior is preserved -//! in [`super::v0`](super::v0) for grove v1/v2 replay compatibility. - -use std::collections::HashMap; - -use grovedb_costs::{ - cost_return_on_error, cost_return_on_error_into, storage_cost::removal::StorageRemovedBytes, - CostResult, CostsExt, OperationCost, -}; -use grovedb_merk::{ - element::{delete::ElementDeleteFromStorageExtensions, tree_type::ElementTreeTypeExtensions}, - Error as MerkError, Merk, -}; -use grovedb_path::SubtreePath; -use grovedb_storage::{ - rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, -}; -use grovedb_version::version::GroveVersion; - -use super::super::DeleteOptions; -use crate::{Element, Error, GroveDb, Transaction}; - -impl GroveDb { - pub(super) fn delete_internal_on_transaction_v1>( - &self, - path: SubtreePath, - key: &[u8], - options: &DeleteOptions, - transaction: &Transaction, - sectioned_removal: &mut impl FnMut( - &Vec, - u32, - u32, - ) -> Result< - (StorageRemovedBytes, StorageRemovedBytes), - MerkError, - >, - batch: &StorageBatch, - grove_version: &GroveVersion, - ) -> CostResult { - let mut cost = OperationCost::default(); - - let element = cost_return_on_error!( - &mut cost, - self.get_raw(path.clone(), key.as_ref(), Some(transaction), grove_version) - ); - let mut subtree_to_delete_from = cost_return_on_error!( - &mut cost, - self.open_transactional_merk_at_path( - path.clone(), - transaction, - Some(batch), - grove_version - ) - ); - let parent_tree_type = subtree_to_delete_from.tree_type; - if element.tree_type().is_some() { - let subtree_merk_path = path.derive_owned_with_child(key); - let subtree_merk_path_ref = SubtreePath::from(&subtree_merk_path); - - // Tree types that store data in the data namespace as non-Merk - // entries (CommitmentTree, MmrTree, BulkAppendTree, DenseTree) - // have an always-empty Merk but may have data. We cannot iterate - // their storage with find_subtrees because the entries are not - // valid Element serializations. - let non_merk_data = element.uses_non_merk_data_storage(); - - let subtree_of_tree_we_are_deleting = cost_return_on_error!( - &mut cost, - self.open_transactional_merk_at_path( - subtree_merk_path_ref.clone(), - transaction, - Some(batch), - grove_version, - ) - ); - - // For non-Merk data trees the raw_iter check would see non-Merk - // keys and wrongly report the tree as non-empty. Use the - // element's own count instead. - let is_empty = if non_merk_data { - element.non_merk_entry_count().unwrap_or(0) == 0 - } else { - subtree_of_tree_we_are_deleting - .is_empty_tree() - .unwrap_add_cost(&mut cost) - }; - - if !options.allow_deleting_non_empty_trees && !is_empty { - return if options.deleting_non_empty_trees_returns_error { - Err(Error::DeletingNonEmptyTree( - "trying to do a delete operation for a non empty tree, but options not \ - allowing this", - )) - .wrap_with_cost(cost) - } else { - Ok(false).wrap_with_cost(cost) - }; - } - - if !is_empty { - if non_merk_data { - // Non-Merk data trees: clear the subtree storage directly. - // These trees never contain child subtrees so we only need - // to clear the one storage context. - let mut storage = self - .db - .get_transactional_storage_context( - subtree_merk_path_ref.clone(), - Some(batch), - transaction, - ) - .unwrap_add_cost(&mut cost); - cost_return_on_error!( - &mut cost, - storage.clear().map_err(|e| { - Error::CorruptedData(format!( - "unable to cleanup non-merk tree data from storage: {e}", - )) - }) - ); - } else { - let subtrees_paths = cost_return_on_error!( - &mut cost, - self.find_subtrees( - &subtree_merk_path_ref, - Some(transaction), - grove_version - ) - ); - for subtree_path in subtrees_paths { - let p: SubtreePath<_> = subtree_path.as_slice().into(); - let mut storage = self - .db - .get_transactional_storage_context(p, Some(batch), transaction) - .unwrap_add_cost(&mut cost); - - cost_return_on_error!( - &mut cost, - storage.clear().map_err(|e| { - Error::CorruptedData(format!( - "unable to cleanup tree from storage: {e}", - )) - }) - ); - } - } - // Reuse the already-open parent merk so propagation keeps the - // parent tree type instead of reopening with the child type. - // We are deleting a tree, a tree uses 3 bytes - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut subtree_to_delete_from, - key, - Some(options.as_merk_options()), - true, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap< - SubtreePath, - Merk, - > = HashMap::default(); - merk_cache.insert(path.clone(), subtree_to_delete_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_batch_transaction( - batch, - merk_cache, - &path, - transaction, - grove_version, - ) - ); - } else { - // We are deleting a tree, a tree uses 3 bytes - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut subtree_to_delete_from, - key, - Some(options.as_merk_options()), - true, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap< - SubtreePath, - Merk, - > = HashMap::default(); - merk_cache.insert(path.clone(), subtree_to_delete_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_transaction( - merk_cache, - path, - transaction, - batch, - grove_version - ) - ); - } - } else { - cost_return_on_error_into!( - &mut cost, - Element::delete_with_sectioned_removal_bytes( - &mut subtree_to_delete_from, - key, - Some(options.as_merk_options()), - false, - parent_tree_type, - sectioned_removal, - grove_version, - ) - ); - let mut merk_cache: HashMap, Merk> = - HashMap::default(); - merk_cache.insert(path.clone(), subtree_to_delete_from); - cost_return_on_error!( - &mut cost, - self.propagate_changes_with_transaction( - merk_cache, - path, - transaction, - batch, - grove_version - ) - ); - } - - Ok(true).wrap_with_cost(cost) - } -} diff --git a/grovedb/src/operations/delete/mod.rs b/grovedb/src/operations/delete/mod.rs index f0c19c0c7..704bed694 100644 --- a/grovedb/src/operations/delete/mod.rs +++ b/grovedb/src/operations/delete/mod.rs @@ -15,8 +15,6 @@ #[cfg(feature = "estimated_costs")] mod average_case; #[cfg(feature = "minimal")] -mod delete_internal_on_transaction; -#[cfg(feature = "minimal")] mod delete_up_tree; #[cfg(feature = "estimated_costs")] mod worst_case; @@ -46,7 +44,9 @@ use grovedb_path::SubtreePath; use grovedb_storage::{ rocksdb_storage::PrefixedRocksDbTransactionContext, Storage, StorageBatch, StorageContext, }; -use grovedb_version::{check_grovedb_v0_with_cost, version::GroveVersion}; +use grovedb_version::{ + check_grovedb_v0_or_v1_with_cost, check_grovedb_v0_with_cost, version::GroveVersion, +}; use crate::util::{compat, TxRef}; #[cfg(feature = "minimal")] @@ -713,6 +713,276 @@ impl GroveDb { } } } + + fn delete_internal_on_transaction>( + &self, + path: SubtreePath, + key: &[u8], + options: &DeleteOptions, + transaction: &Transaction, + sectioned_removal: &mut impl FnMut( + &Vec, + u32, + u32, + ) -> Result< + (StorageRemovedBytes, StorageRemovedBytes), + MerkError, + >, + batch: &StorageBatch, + grove_version: &GroveVersion, + ) -> CostResult { + let delete_internal_version = grove_version + .grovedb_versions + .operations + .delete + .delete_internal_on_transaction; + #[rustfmt::skip] + check_grovedb_v0_or_v1_with_cost!("delete_internal_on_transaction", delete_internal_version); + + let mut cost = OperationCost::default(); + + let element = cost_return_on_error!( + &mut cost, + self.get_raw(path.clone(), key.as_ref(), Some(transaction), grove_version) + ); + let mut subtree_to_delete_from = cost_return_on_error!( + &mut cost, + self.open_transactional_merk_at_path( + path.clone(), + transaction, + Some(batch), + grove_version + ) + ); + let parent_tree_type = subtree_to_delete_from.tree_type; + if let Some(tree_type) = element.tree_type() { + let subtree_merk_path = path.derive_owned_with_child(key); + let subtree_merk_path_ref = SubtreePath::from(&subtree_merk_path); + + // Tree types that store data in the data namespace as non-Merk + // entries (CommitmentTree, MmrTree, BulkAppendTree, DenseTree) + // have an always-empty Merk but may have data. We cannot iterate + // their storage with find_subtrees because the entries are not + // valid Element serializations. + let non_merk_data = element.uses_non_merk_data_storage(); + + let subtree_of_tree_we_are_deleting = cost_return_on_error!( + &mut cost, + self.open_transactional_merk_at_path( + subtree_merk_path_ref.clone(), + transaction, + Some(batch), + grove_version, + ) + ); + + // For non-Merk data trees the raw_iter check would see non-Merk + // keys and wrongly report the tree as non-empty. Use the + // element's own count instead. + let is_empty = if non_merk_data { + element.non_merk_entry_count().unwrap_or(0) == 0 + } else { + subtree_of_tree_we_are_deleting + .is_empty_tree() + .unwrap_add_cost(&mut cost) + }; + + if !options.allow_deleting_non_empty_trees && !is_empty { + return if options.deleting_non_empty_trees_returns_error { + Err(Error::DeletingNonEmptyTree( + "trying to do a delete operation for a non empty tree, but options not \ + allowing this", + )) + .wrap_with_cost(cost) + } else { + Ok(false).wrap_with_cost(cost) + }; + } + + if !is_empty { + if non_merk_data { + // Non-Merk data trees: clear the subtree storage directly. + // These trees never contain child subtrees so we only need + // to clear the one storage context. + let mut storage = self + .db + .get_transactional_storage_context( + subtree_merk_path_ref.clone(), + Some(batch), + transaction, + ) + .unwrap_add_cost(&mut cost); + cost_return_on_error!( + &mut cost, + storage.clear().map_err(|e| { + Error::CorruptedData(format!( + "unable to cleanup non-merk tree data from storage: {e}", + )) + }) + ); + } else { + let subtrees_paths = cost_return_on_error!( + &mut cost, + self.find_subtrees( + &subtree_merk_path_ref, + Some(transaction), + grove_version + ) + ); + for subtree_path in subtrees_paths { + let p: SubtreePath<_> = subtree_path.as_slice().into(); + let mut storage = self + .db + .get_transactional_storage_context(p, Some(batch), transaction) + .unwrap_add_cost(&mut cost); + + cost_return_on_error!( + &mut cost, + storage.clear().map_err(|e| { + Error::CorruptedData(format!( + "unable to cleanup tree from storage: {e}", + )) + }) + ); + } + } + if delete_internal_version == 0 { + // Legacy behavior reopened the parent layer using the + // child tree type before deleting the tree element. + let storage = self + .db + .get_transactional_storage_context(path.clone(), Some(batch), transaction) + .unwrap_add_cost(&mut cost); + + let mut merk_to_delete_tree_from = cost_return_on_error!( + &mut cost, + Merk::open_layered_with_root_key( + storage, + subtree_to_delete_from.root_key(), + tree_type, + Some(&Element::value_defined_cost_for_serialized_value), + grove_version, + ) + .map_err(cannot_open_subtree_with_root_key_error) + ); + // We are deleting a tree, a tree uses 3 bytes + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut merk_to_delete_tree_from, + key, + Some(options.as_merk_options()), + true, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap< + SubtreePath, + Merk, + > = HashMap::default(); + merk_cache.insert(path.clone(), merk_to_delete_tree_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_batch_transaction( + batch, + merk_cache, + &path, + transaction, + grove_version, + ) + ); + } else { + // We are deleting a tree, a tree uses 3 bytes + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut subtree_to_delete_from, + key, + Some(options.as_merk_options()), + true, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap< + SubtreePath, + Merk, + > = HashMap::default(); + merk_cache.insert(path.clone(), subtree_to_delete_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_batch_transaction( + batch, + merk_cache, + &path, + transaction, + grove_version, + ) + ); + } + } else { + // We are deleting a tree, a tree uses 3 bytes + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut subtree_to_delete_from, + key, + Some(options.as_merk_options()), + true, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap< + SubtreePath, + Merk, + > = HashMap::default(); + merk_cache.insert(path.clone(), subtree_to_delete_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_transaction( + merk_cache, + path, + transaction, + batch, + grove_version + ) + ); + } + } else { + cost_return_on_error_into!( + &mut cost, + Element::delete_with_sectioned_removal_bytes( + &mut subtree_to_delete_from, + key, + Some(options.as_merk_options()), + false, + parent_tree_type, + sectioned_removal, + grove_version, + ) + ); + let mut merk_cache: HashMap, Merk> = + HashMap::default(); + merk_cache.insert(path.clone(), subtree_to_delete_from); + cost_return_on_error!( + &mut cost, + self.propagate_changes_with_transaction( + merk_cache, + path, + transaction, + batch, + grove_version + ) + ); + } + + Ok(true).wrap_with_cost(cost) + } } #[cfg(feature = "minimal")]