diff --git a/grovedb-query/src/aggregate_sum_query/insert.rs b/grovedb-query/src/aggregate_sum_query/insert.rs new file mode 100644 index 000000000..1745a98cb --- /dev/null +++ b/grovedb-query/src/aggregate_sum_query/insert.rs @@ -0,0 +1,176 @@ +use std::ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive}; + +use super::AggregateSumQuery; +use crate::QueryItem; + +impl AggregateSumQuery { + /// Adds an individual key to the query, so that its value (or its absence) + /// in the tree will be included in the resulting proof. + /// + /// If the key or a range including the key already exists in the query, + /// this will have no effect. If the query already includes a range that has + /// a non-inclusive bound equal to the key, the bound will be changed to be + /// inclusive. + pub fn insert_key(&mut self, key: Vec) { + let key = QueryItem::Key(key); + self.insert_item(key); + } + + /// Adds multiple individual keys to the query, so that its value (or its + /// absence) in the tree will be included in the resulting proof. + /// + /// If the key or a range including the key already exists in the query, + /// this will have no effect. If the query already includes a range that has + /// a non-inclusive bound equal to the key, the bound will be changed to be + /// inclusive. + pub fn insert_keys(&mut self, keys: Vec>) { + for key in keys { + let key = QueryItem::Key(key); + self.insert_item(key); + } + } + + /// Adds a range to the query, so that all the entries in the tree with keys + /// in the range will be included in the resulting proof. + /// + /// If a range including the range already exists in the query, this will + /// have no effect. If the query already includes a range that overlaps with + /// the range, the ranges will be joined together. + pub fn insert_range(&mut self, range: Range>) { + let range = QueryItem::Range(range); + self.insert_item(range); + } + + /// Adds an inclusive range to the query, so that all the entries in the + /// tree with keys in the range will be included in the resulting proof. + /// + /// If a range including the range already exists in the query, this will + /// have no effect. If the query already includes a range that overlaps with + /// the range, the ranges will be merged together. + pub fn insert_range_inclusive(&mut self, range: RangeInclusive>) { + let range = QueryItem::RangeInclusive(range); + self.insert_item(range); + } + + /// Adds a range until a certain included value to the query, so that all + /// the entries in the tree with keys in the range will be included in the + /// resulting proof. + /// + /// If a range including the range already exists in the query, this will + /// have no effect. If the query already includes a range that overlaps with + /// the range, the ranges will be joined together. + pub fn insert_range_to_inclusive(&mut self, range: RangeToInclusive>) { + let range = QueryItem::RangeToInclusive(range); + self.insert_item(range); + } + + /// Adds a range from a certain included value to the query, so that all + /// the entries in the tree with keys in the range will be included in the + /// resulting proof. + /// + /// If a range including the range already exists in the query, this will + /// have no effect. If the query already includes a range that overlaps with + /// the range, the ranges will be joined together. + pub fn insert_range_from(&mut self, range: RangeFrom>) { + let range = QueryItem::RangeFrom(range); + self.insert_item(range); + } + + /// Adds a range until a certain non included value to the query, so that + /// all the entries in the tree with keys in the range will be included + /// in the resulting proof. + /// + /// If a range including the range already exists in the query, this will + /// have no effect. If the query already includes a range that overlaps with + /// the range, the ranges will be joined together. + pub fn insert_range_to(&mut self, range: RangeTo>) { + let range = QueryItem::RangeTo(range); + self.insert_item(range); + } + + /// Adds a range after the first value, so that all the entries in the tree + /// with keys in the range will be included in the resulting proof. + /// + /// If a range including the range already exists in the query, this will + /// have no effect. If the query already includes a range that overlaps with + /// the range, the ranges will be joined together. + pub fn insert_range_after(&mut self, range: RangeFrom>) { + let range = QueryItem::RangeAfter(range); + self.insert_item(range); + } + + /// Adds a range after the first value, until a certain non included value + /// to the query, so that all the entries in the tree with keys in the + /// range will be included in the resulting proof. + /// + /// If a range including the range already exists in the query, this will + /// have no effect. If the query already includes a range that overlaps with + /// the range, the ranges will be joined together. + pub fn insert_range_after_to(&mut self, range: Range>) { + let range = QueryItem::RangeAfterTo(range); + self.insert_item(range); + } + + /// Adds a range after the first value, until a certain included value to + /// the query, so that all the entries in the tree with keys in the + /// range will be included in the resulting proof. + /// + /// If a range including the range already exists in the query, this will + /// have no effect. If the query already includes a range that overlaps with + /// the range, the ranges will be joined together. + pub fn insert_range_after_to_inclusive(&mut self, range: RangeInclusive>) { + let range = QueryItem::RangeAfterToInclusive(range); + self.insert_item(range); + } + + /// Adds a range of all potential values to the query, so that the query + /// will return all values + /// + /// All other items in the query will be discarded as you are now getting + /// back all elements. + pub fn insert_all(&mut self) { + let range = QueryItem::RangeFull(RangeFull); + self.insert_item(range); + } + + /// Adds the `QueryItem` to the query, first checking to see if it collides + /// with any existing ranges or keys. All colliding items will be removed + /// then merged together so that the query includes the minimum number of + /// items (with no items covering any duplicate parts of keyspace) while + /// still including every key or range that has been added to the query. + pub fn insert_item(&mut self, mut item: QueryItem) { + // since `QueryItem::eq` considers items equal if they collide at all + // (including keys within ranges or ranges which partially overlap), + // `items.take` will remove the first item which collides + + self.items = self + .items + .iter() + .filter_map(|our_item| { + if our_item.is_key() && item.is_key() && our_item == &item { + None + } else if our_item.collides_with(&item) { + item.merge_assign(our_item); + None + } else { + Some(our_item.clone()) // todo: manage this without a clone + } + }) + .collect(); + + // since we need items to be sorted we do + match self.items.binary_search(&item) { + Ok(_) => { + unreachable!("this shouldn't be possible") + } + Err(pos) => self.items.insert(pos, item), + } + } + + /// Performs an insert_item on each item in the vector. + pub fn insert_items(&mut self, items: Vec) { + for item in items { + self.insert_item(item) + } + } +} diff --git a/grovedb-query/src/aggregate_sum_query/merge.rs b/grovedb-query/src/aggregate_sum_query/merge.rs new file mode 100644 index 000000000..2e77fddf4 --- /dev/null +++ b/grovedb-query/src/aggregate_sum_query/merge.rs @@ -0,0 +1,72 @@ +use super::AggregateSumQuery; +use crate::error::Error; + +impl AggregateSumQuery { + /// Merge multiple aggregate sum queries into one. + pub fn merge_multiple(mut queries: Vec) -> Result { + if queries.is_empty() { + // We put sum 0 and limit 0 to represent a no-op query + return Ok(AggregateSumQuery::new(0, Some(0))); + } + + // Slight performance improvement via swap_remove + let mut merged_query = queries.swap_remove(0); + let mut aggregate_sum_limit = merged_query.sum_limit; + let expected_left_to_right = merged_query.left_to_right; + let mut merged_limit: Option = merged_query.limit_of_items_to_check; + + for query in queries { + if query.left_to_right != expected_left_to_right { + return Err(Error::NotSupported( + "Cannot merge queries with differing left_to_right values".to_string(), + )); + } + + aggregate_sum_limit = aggregate_sum_limit + .checked_add(query.sum_limit) + .ok_or(Error::Overflow("Overflow when merging sum limits"))?; + + merged_limit = match (merged_limit, query.limit_of_items_to_check) { + (Some(a), Some(b)) => Some( + a.checked_add(b) + .ok_or(Error::Overflow("Overflow when merging item check limits"))?, + ), + _ => None, // if either is None, result is None + }; + + merged_query.insert_items(query.items); + } + + merged_query.sum_limit = aggregate_sum_limit; + merged_query.limit_of_items_to_check = merged_limit; + + Ok(merged_query) + } + + /// Merge another aggregate sum query into this one. + pub fn merge_with(&mut self, other: AggregateSumQuery) -> Result<(), Error> { + if self.left_to_right != other.left_to_right { + return Err(Error::NotSupported( + "Cannot merge queries with differing left_to_right values".to_string(), + )); + } + + self.sum_limit = self + .sum_limit + .checked_add(other.sum_limit) + .ok_or(Error::Overflow("Overflow when merging sum limits"))?; + + self.limit_of_items_to_check = + match (self.limit_of_items_to_check, other.limit_of_items_to_check) { + (Some(a), Some(b)) => Some( + a.checked_add(b) + .ok_or(Error::Overflow("Overflow when merging item check limits"))?, + ), + _ => None, + }; + + self.insert_items(other.items); + + Ok(()) + } +} diff --git a/grovedb-query/src/aggregate_sum_query/mod.rs b/grovedb-query/src/aggregate_sum_query/mod.rs new file mode 100644 index 000000000..9b6d4e521 --- /dev/null +++ b/grovedb-query/src/aggregate_sum_query/mod.rs @@ -0,0 +1,183 @@ +mod insert; +mod merge; + +use crate::{Key, QueryItem}; +use bincode::{Decode, Encode}; +use std::fmt; +use std::ops::RangeFull; + +/// `AggregateSumQuery` represents one or more keys or ranges of keys, which can be used to +/// resolve a proof which will include all the requested values +#[derive(Debug, Default, Clone, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct AggregateSumQuery { + /// Items + pub items: Vec, + /// Left to right? + pub left_to_right: bool, + /// The amount above which we should stop + /// For example if we have sum nodes with 5 and 10, and we have 15, we should stop looking + /// At elements when we get to 15 + pub sum_limit: u64, + /// The max amount of nodes we should check + pub limit_of_items_to_check: Option, +} + +impl fmt::Display for AggregateSumQuery { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let direction = if self.left_to_right { "→" } else { "←" }; + writeln!( + f, + "AggregateSumQuery [direction: {}, sum_limit: {}]", + direction, self.sum_limit + )?; + writeln!(f, "Items:")?; + for item in &self.items { + writeln!(f, " - {}", item)?; + } + Ok(()) + } +} + +impl AggregateSumQuery { + /// Creates a new query which contains all items. + pub fn new(sum_limit: u64, limit_of_items_to_check: Option) -> Self { + Self::new_range_full(sum_limit, limit_of_items_to_check) + } + + /// Creates a new query which contains all items and ordered by keys descending + pub fn new_descending(sum_limit: u64, limit_of_items_to_check: Option) -> Self { + Self::new_range_full_descending(sum_limit, limit_of_items_to_check) + } + + /// Creates a new query which contains all items. + pub fn new_range_full(sum_limit: u64, limit_of_items_to_check: Option) -> Self { + Self { + items: vec![QueryItem::RangeFull(RangeFull)], + left_to_right: true, + sum_limit, + limit_of_items_to_check, + } + } + + /// Creates a new query which contains all items and ordered by keys descending + pub fn new_range_full_descending(sum_limit: u64, limit_of_items_to_check: Option) -> Self { + Self { + items: vec![QueryItem::RangeFull(RangeFull)], + left_to_right: false, + sum_limit, + limit_of_items_to_check, + } + } + + /// Creates a new query which contains only one key. + /// We will basically only check this key to see if we are hitting the sum limit in one element + pub fn new_single_key(key: Vec, sum_limit: u64) -> Self { + Self { + items: vec![QueryItem::Key(key)], + left_to_right: true, + sum_limit, + limit_of_items_to_check: Some(1), + } + } + + /// Creates a new query which contains only one item. + pub fn new_single_query_item( + query_item: QueryItem, + sum_limit: u64, + limit_of_items_to_check: Option, + ) -> Self { + Self { + items: vec![query_item], + left_to_right: true, + sum_limit, + limit_of_items_to_check, + } + } + + /// Creates a new query which contains multiple items. + pub fn new_with_query_items( + query_items: Vec, + sum_limit: u64, + limit_of_items_to_check: Option, + ) -> Self { + Self { + items: query_items, + left_to_right: true, + sum_limit, + limit_of_items_to_check, + } + } + + /// Creates a new query which contains multiple items. + pub fn new_with_keys( + keys: Vec, + sum_limit: u64, + limit_of_items_to_check: Option, + ) -> Self { + Self { + items: keys.into_iter().map(QueryItem::Key).collect(), + left_to_right: true, + sum_limit, + limit_of_items_to_check, + } + } + + /// Creates a new query which contains multiple keys. + pub fn new_with_keys_reversed( + keys: Vec, + sum_limit: u64, + limit_of_items_to_check: Option, + ) -> Self { + Self { + items: keys.into_iter().map(QueryItem::Key).collect(), + left_to_right: false, + sum_limit, + limit_of_items_to_check, + } + } + + /// Creates a new query which contains only one item with the specified + /// direction. + pub fn new_single_query_item_with_direction( + query_item: QueryItem, + left_to_right: bool, + sum_limit: u64, + limit_of_items_to_check: Option, + ) -> Self { + Self { + items: vec![query_item], + left_to_right, + sum_limit, + limit_of_items_to_check, + } + } + + /// Iterate through query items + pub fn iter(&self) -> impl Iterator { + self.items.iter() + } + + /// Iterate through query items in reverse + pub fn rev_iter(&self) -> impl Iterator { + self.items.iter().rev() + } + + /// Iterate with direction specified + pub fn directional_iter( + &self, + left_to_right: bool, + ) -> Box + '_> { + if left_to_right { + Box::new(self.iter()) + } else { + Box::new(self.rev_iter()) + } + } + + /// Check if there are only keys + pub fn has_only_keys(&self) -> bool { + // checks if all searched for items are keys + self.items.iter().all(|a| a.is_key()) + } +} diff --git a/grovedb-query/src/error.rs b/grovedb-query/src/error.rs index e6e5efdd9..9f4052edc 100644 --- a/grovedb-query/src/error.rs +++ b/grovedb-query/src/error.rs @@ -33,4 +33,8 @@ pub enum Error { /// Ed encoding/decoding error #[error("ed error: {0}")] EdError(ed::Error), + + /// Arithmetic overflow during query merging or aggregation. + #[error("overflow error: {0}")] + Overflow(&'static str), } diff --git a/grovedb-query/src/lib.rs b/grovedb-query/src/lib.rs index 48c61cc89..4d4cac205 100644 --- a/grovedb-query/src/lib.rs +++ b/grovedb-query/src/lib.rs @@ -9,6 +9,9 @@ /// Error types for query operations. pub mod error; +/// Aggregate sum query for sum-up-to style queries. +pub mod aggregate_sum_query; + mod common_path; mod insert; @@ -29,6 +32,7 @@ mod query; mod subquery_branch; +pub use aggregate_sum_query::AggregateSumQuery; pub use proof_items::ProofItems; pub use proof_status::ProofStatus; pub use query::Query; diff --git a/grovedb-query/src/query_item/merge.rs b/grovedb-query/src/query_item/merge.rs index d9b8123c6..4b42b6b19 100644 --- a/grovedb-query/src/query_item/merge.rs +++ b/grovedb-query/src/query_item/merge.rs @@ -71,7 +71,8 @@ impl QueryItem { } } - pub(crate) fn merge_assign(&mut self, other: &Self) { + /// Merges another QueryItem into this one in-place. + pub fn merge_assign(&mut self, other: &Self) { *self = self.merge(other); } } diff --git a/grovedb-query/src/query_item/mod.rs b/grovedb-query/src/query_item/mod.rs index 3cea999d3..6525f2ad5 100644 --- a/grovedb-query/src/query_item/mod.rs +++ b/grovedb-query/src/query_item/mod.rs @@ -798,14 +798,16 @@ impl QueryItem { &self, iter: &I, limit: Option, + aggregate_limit: Option, left_to_right: bool, ) -> CostContext { let mut cost = OperationCost::default(); // Check that if limit is set it's greater than 0 and iterator points to a valid // place. - let basic_valid = - limit.map(|l| l > 0).unwrap_or(true) && iter.valid().unwrap_add_cost(&mut cost); + let basic_valid = limit.map(|l| l > 0).unwrap_or(true) + && aggregate_limit.map(|l| l > 0).unwrap_or(true) + && iter.valid().unwrap_add_cost(&mut cost); if !basic_valid { return false.wrap_with_cost(cost); diff --git a/grovedb-query/tests/aggregate_sum_query_tests.rs b/grovedb-query/tests/aggregate_sum_query_tests.rs new file mode 100644 index 000000000..ce191704c --- /dev/null +++ b/grovedb-query/tests/aggregate_sum_query_tests.rs @@ -0,0 +1,404 @@ +use bincode::{config::standard, decode_from_slice, encode_to_vec}; + +use grovedb_query::{AggregateSumQuery, QueryItem}; + +// --------------------------------------------------------------------------- +// Constructor tests (mod.rs) +// --------------------------------------------------------------------------- + +#[test] +fn new_and_new_range_full_are_equivalent() { + let q = AggregateSumQuery::new(100, Some(5)); + assert_eq!(q.items, vec![QueryItem::RangeFull(..)]); + assert!(q.left_to_right); + assert_eq!(q.sum_limit, 100); + assert_eq!(q.limit_of_items_to_check, Some(5)); + + let q2 = AggregateSumQuery::new_range_full(100, Some(5)); + assert_eq!(q, q2); +} + +#[test] +fn new_descending_and_new_range_full_descending_are_equivalent() { + let q = AggregateSumQuery::new_descending(42, None); + assert!(!q.left_to_right); + assert_eq!(q.items, vec![QueryItem::RangeFull(..)]); + assert_eq!(q.sum_limit, 42); + assert_eq!(q.limit_of_items_to_check, None); + + let q2 = AggregateSumQuery::new_range_full_descending(42, None); + assert_eq!(q, q2); +} + +#[test] +fn new_single_key() { + let q = AggregateSumQuery::new_single_key(vec![1, 2, 3], 99); + assert_eq!(q.items, vec![QueryItem::Key(vec![1, 2, 3])]); + assert!(q.left_to_right); + assert_eq!(q.sum_limit, 99); + assert_eq!(q.limit_of_items_to_check, Some(1)); +} + +#[test] +fn new_single_query_item() { + let item = QueryItem::Range(vec![0]..vec![10]); + let q = AggregateSumQuery::new_single_query_item(item.clone(), 50, Some(3)); + assert_eq!(q.items, vec![item]); + assert!(q.left_to_right); + assert_eq!(q.sum_limit, 50); + assert_eq!(q.limit_of_items_to_check, Some(3)); +} + +#[test] +fn new_with_query_items() { + let items = vec![ + QueryItem::Key(vec![1]), + QueryItem::Key(vec![2]), + QueryItem::Key(vec![3]), + ]; + let q = AggregateSumQuery::new_with_query_items(items.clone(), 10, Some(10)); + assert_eq!(q.items, items); + assert!(q.left_to_right); +} + +#[test] +fn new_with_keys() { + let keys = vec![vec![10], vec![20]]; + let q = AggregateSumQuery::new_with_keys(keys, 77, None); + assert_eq!( + q.items, + vec![QueryItem::Key(vec![10]), QueryItem::Key(vec![20])] + ); + assert!(q.left_to_right); + assert_eq!(q.sum_limit, 77); + assert_eq!(q.limit_of_items_to_check, None); +} + +#[test] +fn new_with_keys_reversed() { + let keys = vec![vec![10], vec![20]]; + let q = AggregateSumQuery::new_with_keys_reversed(keys, 55, Some(4)); + assert!(!q.left_to_right); + assert_eq!(q.sum_limit, 55); + assert_eq!(q.limit_of_items_to_check, Some(4)); +} + +#[test] +fn new_single_query_item_with_direction() { + let item = QueryItem::Key(vec![7]); + let q = AggregateSumQuery::new_single_query_item_with_direction(item.clone(), false, 8, None); + assert!(!q.left_to_right); + assert_eq!(q.items, vec![item]); + + let q2 = AggregateSumQuery::new_single_query_item_with_direction( + QueryItem::Key(vec![7]), + true, + 8, + None, + ); + assert!(q2.left_to_right); +} + +// --------------------------------------------------------------------------- +// Iterator tests +// --------------------------------------------------------------------------- + +#[test] +fn iter_forward() { + let q = AggregateSumQuery::new_with_keys(vec![vec![1], vec![2], vec![3]], 100, None); + let collected: Vec<_> = q.iter().cloned().collect(); + assert_eq!( + collected, + vec![ + QueryItem::Key(vec![1]), + QueryItem::Key(vec![2]), + QueryItem::Key(vec![3]), + ] + ); +} + +#[test] +fn rev_iter_reverse() { + let q = AggregateSumQuery::new_with_keys(vec![vec![1], vec![2], vec![3]], 100, None); + let collected: Vec<_> = q.rev_iter().cloned().collect(); + assert_eq!( + collected, + vec![ + QueryItem::Key(vec![3]), + QueryItem::Key(vec![2]), + QueryItem::Key(vec![1]), + ] + ); +} + +#[test] +fn directional_iter_delegates_correctly() { + let q = AggregateSumQuery::new_with_keys(vec![vec![1], vec![2]], 100, None); + + let fwd: Vec<_> = q.directional_iter(true).cloned().collect(); + let rev: Vec<_> = q.directional_iter(false).cloned().collect(); + + assert_eq!(fwd, vec![QueryItem::Key(vec![1]), QueryItem::Key(vec![2])]); + assert_eq!(rev, vec![QueryItem::Key(vec![2]), QueryItem::Key(vec![1])]); +} + +#[test] +fn has_only_keys_true_for_keys() { + let q = AggregateSumQuery::new_with_keys(vec![vec![1], vec![2]], 10, None); + assert!(q.has_only_keys()); +} + +#[test] +fn has_only_keys_false_with_range() { + let mut q = AggregateSumQuery::new_with_keys(vec![vec![1]], 10, None); + q.insert_range(vec![5]..vec![10]); + assert!(!q.has_only_keys()); +} + +// --------------------------------------------------------------------------- +// Display test +// --------------------------------------------------------------------------- + +#[test] +fn display_includes_direction_and_sum_limit() { + let q = AggregateSumQuery::new(42, None); + let s = format!("{}", q); + assert!(s.contains("→"), "ascending should have right arrow"); + assert!(s.contains("42"), "should contain sum_limit"); + + let q2 = AggregateSumQuery::new_descending(99, None); + let s2 = format!("{}", q2); + assert!(s2.contains("←"), "descending should have left arrow"); + assert!(s2.contains("99")); +} + +// --------------------------------------------------------------------------- +// Insert tests (insert.rs) +// --------------------------------------------------------------------------- + +#[test] +fn insert_key() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_key(vec![5]); + assert_eq!(q.items, vec![QueryItem::Key(vec![5])]); +} + +#[test] +fn insert_keys() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_keys(vec![vec![1], vec![2], vec![3]]); + assert_eq!( + q.items, + vec![ + QueryItem::Key(vec![1]), + QueryItem::Key(vec![2]), + QueryItem::Key(vec![3]), + ] + ); +} + +#[test] +fn insert_range() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_range(vec![1]..vec![5]); + assert_eq!(q.items, vec![QueryItem::Range(vec![1]..vec![5])]); +} + +#[test] +fn insert_range_inclusive() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_range_inclusive(vec![1]..=vec![5]); + assert_eq!(q.items, vec![QueryItem::RangeInclusive(vec![1]..=vec![5])]); +} + +#[test] +fn insert_range_from() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_range_from(vec![3]..); + assert_eq!(q.items, vec![QueryItem::RangeFrom(vec![3]..)]); +} + +#[test] +fn insert_range_to() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_range_to(..vec![7]); + assert_eq!(q.items, vec![QueryItem::RangeTo(..vec![7])]); +} + +#[test] +fn insert_range_to_inclusive() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_range_to_inclusive(..=vec![7]); + assert_eq!(q.items, vec![QueryItem::RangeToInclusive(..=vec![7])]); +} + +#[test] +fn insert_range_after() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_range_after(vec![2]..); + assert_eq!(q.items, vec![QueryItem::RangeAfter(vec![2]..)]); +} + +#[test] +fn insert_range_after_to() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_range_after_to(vec![2]..vec![5]); + assert_eq!(q.items, vec![QueryItem::RangeAfterTo(vec![2]..vec![5])]); +} + +#[test] +fn insert_range_after_to_inclusive() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_range_after_to_inclusive(vec![2]..=vec![5]); + assert_eq!( + q.items, + vec![QueryItem::RangeAfterToInclusive(vec![2]..=vec![5])] + ); +} + +#[test] +fn insert_all_replaces_items_with_range_full() { + let mut q = AggregateSumQuery::new_with_keys(vec![vec![1], vec![2]], 10, None); + q.insert_all(); + assert_eq!(q.items, vec![QueryItem::RangeFull(..)]); +} + +#[test] +fn insert_item_merges_overlapping_ranges() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_range(vec![1]..vec![5]); + q.insert_range(vec![3]..vec![8]); + // The two overlapping ranges should be merged into one + assert_eq!(q.items.len(), 1); + assert_eq!(q.items[0], QueryItem::Range(vec![1]..vec![8])); +} + +#[test] +fn insert_items_batch() { + let mut q = AggregateSumQuery::new_with_keys(vec![], 10, None); + q.insert_items(vec![ + QueryItem::Key(vec![1]), + QueryItem::Key(vec![2]), + QueryItem::Key(vec![3]), + ]); + assert_eq!(q.items.len(), 3); +} + +// --------------------------------------------------------------------------- +// Merge tests (merge.rs) +// --------------------------------------------------------------------------- + +#[test] +fn merge_multiple_empty_returns_noop() { + let result = AggregateSumQuery::merge_multiple(vec![]).unwrap(); + assert_eq!(result.sum_limit, 0); + assert_eq!(result.limit_of_items_to_check, Some(0)); +} + +#[test] +fn merge_multiple_single_returns_equivalent() { + let q = AggregateSumQuery::new_with_keys(vec![vec![1]], 42, Some(3)); + let result = AggregateSumQuery::merge_multiple(vec![q.clone()]).unwrap(); + assert_eq!(result.items, q.items); + assert_eq!(result.sum_limit, q.sum_limit); + assert_eq!(result.limit_of_items_to_check, q.limit_of_items_to_check); + assert_eq!(result.left_to_right, q.left_to_right); +} + +#[test] +fn merge_multiple_two_queries() { + let q1 = AggregateSumQuery::new_with_keys(vec![vec![1]], 10, Some(2)); + let q2 = AggregateSumQuery::new_with_keys(vec![vec![2]], 20, Some(3)); + let result = AggregateSumQuery::merge_multiple(vec![q1, q2]).unwrap(); + assert_eq!(result.sum_limit, 30); + assert_eq!(result.limit_of_items_to_check, Some(5)); + assert_eq!(result.items.len(), 2); +} + +#[test] +fn merge_multiple_differing_left_to_right_errors() { + let q1 = AggregateSumQuery::new(10, None); + let q2 = AggregateSumQuery::new_descending(20, None); + let err = AggregateSumQuery::merge_multiple(vec![q1, q2]).unwrap_err(); + assert!(err.to_string().contains("left_to_right")); +} + +#[test] +fn merge_multiple_sum_limit_overflow_errors() { + let q1 = AggregateSumQuery::new(u64::MAX, None); + let q2 = AggregateSumQuery::new(1, None); + let err = AggregateSumQuery::merge_multiple(vec![q1, q2]).unwrap_err(); + assert!(err.to_string().contains("overflow") || err.to_string().contains("Overflow")); +} + +#[test] +fn merge_multiple_none_limit_plus_some_limit_gives_none() { + let q1 = AggregateSumQuery::new(10, None); + let q2 = AggregateSumQuery::new(10, Some(5)); + let result = AggregateSumQuery::merge_multiple(vec![q1, q2]).unwrap(); + assert_eq!(result.limit_of_items_to_check, None); +} + +#[test] +fn merge_multiple_limit_overflow_errors() { + let q1 = AggregateSumQuery::new(1, Some(u16::MAX)); + let q2 = AggregateSumQuery::new(1, Some(1)); + let err = AggregateSumQuery::merge_multiple(vec![q1, q2]).unwrap_err(); + assert!(err.to_string().contains("overflow") || err.to_string().contains("Overflow")); +} + +#[test] +fn merge_with_adds_items_and_limits() { + let mut q1 = AggregateSumQuery::new_with_keys(vec![vec![1]], 10, Some(2)); + let q2 = AggregateSumQuery::new_with_keys(vec![vec![2]], 20, Some(3)); + q1.merge_with(q2).unwrap(); + assert_eq!(q1.sum_limit, 30); + assert_eq!(q1.limit_of_items_to_check, Some(5)); + assert_eq!(q1.items.len(), 2); +} + +#[test] +fn merge_with_differing_direction_errors() { + let mut q1 = AggregateSumQuery::new(10, None); + let q2 = AggregateSumQuery::new_descending(20, None); + let err = q1.merge_with(q2).unwrap_err(); + assert!(err.to_string().contains("left_to_right")); +} + +#[test] +fn merge_with_sum_limit_overflow_errors() { + let mut q1 = AggregateSumQuery::new(u64::MAX, None); + let q2 = AggregateSumQuery::new(1, None); + let err = q1.merge_with(q2).unwrap_err(); + assert!(err.to_string().contains("overflow") || err.to_string().contains("Overflow")); +} + +#[test] +fn merge_with_limit_overflow_errors() { + let mut q1 = AggregateSumQuery::new(1, Some(u16::MAX)); + let q2 = AggregateSumQuery::new(1, Some(1)); + let err = q1.merge_with(q2).unwrap_err(); + assert!(err.to_string().contains("overflow") || err.to_string().contains("Overflow")); +} + +#[test] +fn merge_with_none_limit_gives_none() { + let mut q1 = AggregateSumQuery::new(5, Some(3)); + let q2 = AggregateSumQuery::new(5, None); + q1.merge_with(q2).unwrap(); + assert_eq!(q1.limit_of_items_to_check, None); +} + +// --------------------------------------------------------------------------- +// Encode / decode round-trip +// --------------------------------------------------------------------------- + +#[test] +fn encode_decode_round_trip() { + let q = AggregateSumQuery::new_with_keys(vec![vec![1], vec![2]], 42, Some(7)); + let encoded = encode_to_vec(&q, standard()).expect("encode"); + let (decoded, consumed): (AggregateSumQuery, usize) = + decode_from_slice(&encoded, standard()).expect("decode"); + assert_eq!(consumed, encoded.len()); + assert_eq!(decoded, q); +} diff --git a/grovedb-version/src/version/grovedb_versions.rs b/grovedb-version/src/version/grovedb_versions.rs index 8d10039d5..71fe2474b 100644 --- a/grovedb-version/src/version/grovedb_versions.rs +++ b/grovedb-version/src/version/grovedb_versions.rs @@ -5,8 +5,28 @@ pub struct GroveDBVersions { pub apply_batch: GroveDBApplyBatchVersions, pub element: GroveDBElementMethodVersions, pub operations: GroveDBOperationsVersions, + pub aggregate_sum_path_query_methods: GroveDBAggregateSumPathQueryMethodVersions, pub path_query_methods: GroveDBPathQueryMethodVersions, pub replication: GroveDBReplicationVersions, + pub query_limits: GroveDBQueryLimits, +} + +#[derive(Clone, Debug)] +pub struct GroveDBQueryLimits { + pub max_aggregate_sum_query_elements_scanned: u16, +} + +impl Default for GroveDBQueryLimits { + fn default() -> Self { + Self { + max_aggregate_sum_query_elements_scanned: 1024, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct GroveDBAggregateSumPathQueryMethodVersions { + pub merge: FeatureVersion, } #[derive(Clone, Debug, Default)] @@ -90,6 +110,7 @@ pub struct GroveDBOperationsQueryVersions { pub query: FeatureVersion, pub query_item_value: FeatureVersion, pub query_item_value_or_sum: FeatureVersion, + pub query_aggregate_sums: FeatureVersion, pub query_sums: FeatureVersion, pub query_raw: FeatureVersion, pub query_keys_optional: FeatureVersion, @@ -211,16 +232,21 @@ pub struct GroveDBElementMethodVersions { pub insert_subtree: FeatureVersion, pub insert_subtree_into_batch_operations: FeatureVersion, pub get_query: FeatureVersion, + pub get_aggregate_sum_query: FeatureVersion, pub get_query_values: FeatureVersion, pub get_query_apply_function: FeatureVersion, pub get_path_query: FeatureVersion, pub get_sized_query: FeatureVersion, + pub get_aggregate_sum_query_apply_function: FeatureVersion, pub path_query_push: FeatureVersion, + pub aggregate_sum_path_query_push: FeatureVersion, pub query_item: FeatureVersion, pub basic_push: FeatureVersion, + pub basic_aggregate_sum_push: FeatureVersion, pub serialize: FeatureVersion, pub serialized_size: FeatureVersion, pub deserialize: FeatureVersion, + pub aggregate_sum_query_item: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/grovedb-version/src/version/v1.rs b/grovedb-version/src/version/v1.rs index cdca816e8..fb580aa06 100644 --- a/grovedb-version/src/version/v1.rs +++ b/grovedb-version/src/version/v1.rs @@ -1,3 +1,4 @@ +use crate::version::grovedb_versions::GroveDBAggregateSumPathQueryMethodVersions; use crate::version::{ grovedb_versions::{ GroveDBApplyBatchVersions, GroveDBElementMethodVersions, @@ -5,7 +6,7 @@ use crate::version::{ GroveDBOperationsDeleteVersions, GroveDBOperationsGetVersions, GroveDBOperationsInsertVersions, GroveDBOperationsProofVersions, GroveDBOperationsQueryVersions, GroveDBOperationsVersions, - GroveDBOperationsWorstCaseVersions, GroveDBPathQueryMethodVersions, + GroveDBOperationsWorstCaseVersions, GroveDBPathQueryMethodVersions, GroveDBQueryLimits, GroveDBReplicationVersions, GroveDBVersions, }, merk_versions::{MerkAverageCaseCostsVersions, MerkVersions}, @@ -55,18 +56,23 @@ pub const GROVE_V1: GroveVersion = GroveVersion { insert_subtree: 0, insert_subtree_into_batch_operations: 0, get_query: 0, + get_aggregate_sum_query: 0, get_query_values: 0, get_query_apply_function: 0, get_path_query: 0, get_sized_query: 0, + get_aggregate_sum_query_apply_function: 0, path_query_push: 0, + aggregate_sum_path_query_push: 0, query_item: 0, basic_push: 0, + basic_aggregate_sum_push: 0, serialize: 0, serialized_size: 0, deserialize: 0, get_with_value_hash: 0, insert_reference_if_changed_value: 0, + aggregate_sum_query_item: 0, }, operations: GroveDBOperationsVersions { get: GroveDBOperationsGetVersions { @@ -127,6 +133,7 @@ pub const GROVE_V1: GroveVersion = GroveVersion { query: 0, query_item_value: 0, query_item_value_or_sum: 0, + query_aggregate_sums: 0, query_sums: 0, query_raw: 0, query_keys_optional: 0, @@ -176,6 +183,7 @@ pub const GROVE_V1: GroveVersion = GroveVersion { add_worst_case_get_cost: 0, }, }, + aggregate_sum_path_query_methods: GroveDBAggregateSumPathQueryMethodVersions { merge: 0 }, path_query_methods: GroveDBPathQueryMethodVersions { terminal_keys: 0, merge: 0, @@ -188,6 +196,9 @@ pub const GROVE_V1: GroveVersion = GroveVersion { start_snapshot_syncing: 0, apply_chunk: 0, }, + query_limits: GroveDBQueryLimits { + max_aggregate_sum_query_elements_scanned: 1024, + }, }, merk_versions: MerkVersions { average_case_costs: MerkAverageCaseCostsVersions { diff --git a/grovedb-version/src/version/v2.rs b/grovedb-version/src/version/v2.rs index 7556f5363..d8b45290b 100644 --- a/grovedb-version/src/version/v2.rs +++ b/grovedb-version/src/version/v2.rs @@ -1,3 +1,4 @@ +use crate::version::grovedb_versions::GroveDBAggregateSumPathQueryMethodVersions; use crate::version::{ grovedb_versions::{ GroveDBApplyBatchVersions, GroveDBElementMethodVersions, @@ -5,7 +6,7 @@ use crate::version::{ GroveDBOperationsDeleteVersions, GroveDBOperationsGetVersions, GroveDBOperationsInsertVersions, GroveDBOperationsProofVersions, GroveDBOperationsQueryVersions, GroveDBOperationsVersions, - GroveDBOperationsWorstCaseVersions, GroveDBPathQueryMethodVersions, + GroveDBOperationsWorstCaseVersions, GroveDBPathQueryMethodVersions, GroveDBQueryLimits, GroveDBReplicationVersions, GroveDBVersions, }, merk_versions::{MerkAverageCaseCostsVersions, MerkVersions}, @@ -55,18 +56,23 @@ pub const GROVE_V2: GroveVersion = GroveVersion { insert_subtree: 0, insert_subtree_into_batch_operations: 0, get_query: 0, + get_aggregate_sum_query: 0, get_query_values: 0, get_query_apply_function: 0, get_path_query: 0, get_sized_query: 0, + get_aggregate_sum_query_apply_function: 0, path_query_push: 0, + aggregate_sum_path_query_push: 0, query_item: 0, basic_push: 0, + basic_aggregate_sum_push: 0, serialize: 0, serialized_size: 0, deserialize: 0, get_with_value_hash: 0, insert_reference_if_changed_value: 0, + aggregate_sum_query_item: 0, }, operations: GroveDBOperationsVersions { get: GroveDBOperationsGetVersions { @@ -127,6 +133,7 @@ pub const GROVE_V2: GroveVersion = GroveVersion { query: 0, query_item_value: 0, query_item_value_or_sum: 0, + query_aggregate_sums: 0, query_sums: 0, query_raw: 0, query_keys_optional: 0, @@ -176,6 +183,7 @@ pub const GROVE_V2: GroveVersion = GroveVersion { add_worst_case_get_cost: 0, }, }, + aggregate_sum_path_query_methods: GroveDBAggregateSumPathQueryMethodVersions { merge: 0 }, path_query_methods: GroveDBPathQueryMethodVersions { terminal_keys: 0, merge: 0, @@ -188,6 +196,9 @@ pub const GROVE_V2: GroveVersion = GroveVersion { start_snapshot_syncing: 0, apply_chunk: 0, }, + query_limits: GroveDBQueryLimits { + max_aggregate_sum_query_elements_scanned: 1024, + }, }, merk_versions: MerkVersions { average_case_costs: MerkAverageCaseCostsVersions { diff --git a/grovedb/src/element/aggregate_sum_query/mod.rs b/grovedb/src/element/aggregate_sum_query/mod.rs new file mode 100644 index 000000000..54e41bf29 --- /dev/null +++ b/grovedb/src/element/aggregate_sum_query/mod.rs @@ -0,0 +1,771 @@ +//! Query +//! Implements functions in Element for querying + +use std::fmt; + +use crate::element::SumValue; +#[cfg(feature = "minimal")] +use crate::operations::proof::util::hex_to_ascii; +use crate::operations::proof::util::path_as_slices_hex_to_ascii; +use crate::query_result_type::KeySumValuePair; +use crate::{AggregateSumPathQuery, Element}; +#[cfg(feature = "minimal")] +use crate::{Error, TransactionArg}; +#[cfg(feature = "minimal")] +use grovedb_costs::{ + cost_return_on_error, cost_return_on_error_into_no_add, cost_return_on_error_no_add, + CostResult, CostsExt, OperationCost, +}; +#[cfg(feature = "minimal")] +use grovedb_merk::element::decode::ElementDecodeExtensions; +#[cfg(feature = "minimal")] +use grovedb_merk::element::get::ElementFetchFromStorageExtensions; +#[cfg(feature = "minimal")] +use grovedb_merk::error::MerkErrorExt; +#[cfg(feature = "minimal")] +use grovedb_merk::proofs::query::query_item::QueryItem; +use grovedb_merk::proofs::query::AggregateSumQuery; +#[cfg(feature = "minimal")] +use grovedb_path::SubtreePath; +#[cfg(feature = "minimal")] +use grovedb_storage::{rocksdb_storage::RocksDbStorage, RawIterator, StorageContext}; +#[cfg(feature = "minimal")] +use grovedb_version::{check_grovedb_v0, check_grovedb_v0_with_cost, version::GroveVersion}; + +#[cfg(feature = "minimal")] +const MAX_AGGREGATE_REFERENCE_HOPS: usize = 3; + +/// Result of an aggregate sum query, including metadata about whether the +/// query was truncated. +#[derive(Clone, Debug, PartialEq)] +pub struct AggregateSumQueryResult { + /// The matching key-sum pairs returned by the query. + pub results: Vec, + /// True if the system hard limit on elements scanned was reached before + /// the query completed naturally. When true, more results may exist + /// beyond what was returned. + pub hard_limit_reached: bool, +} + +/// Options controlling how an aggregate sum query is executed. +#[derive(Copy, Clone, Debug)] +pub struct AggregateSumQueryOptions { + /// If true, allows reading from cache instead of forcing fresh disk reads. + pub allow_cache: bool, + /// If true, returns an error when an intermediate path tree does not exist. + /// When false, a missing intermediate tree is silently treated as empty. + pub error_if_intermediate_path_tree_not_present: bool, + /// If true (default), returns an error when a non-sum-item element is + /// encountered (e.g. `Item`, `Tree`). When false, such elements are + /// silently skipped without consuming a user limit slot. + /// `ItemWithSumItem` elements are always processed regardless of this + /// setting. References are handled separately by `ignore_references`. + pub error_if_non_sum_item_found: bool, + /// If true, silently skips `Reference` elements instead of following them. + /// When false (default), references are followed up to 3 hops to resolve + /// the target element. + pub ignore_references: bool, +} + +impl fmt::Display for AggregateSumQueryOptions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "AggregateSumQueryOptions {{")?; + writeln!(f, " allow_cache: {}", self.allow_cache)?; + writeln!( + f, + " error_if_intermediate_path_tree_not_present: {}", + self.error_if_intermediate_path_tree_not_present + )?; + writeln!( + f, + " error_if_non_sum_item_found: {}", + self.error_if_non_sum_item_found + )?; + writeln!(f, " ignore_references: {}", self.ignore_references)?; + write!(f, "}}") + } +} + +impl Default for AggregateSumQueryOptions { + fn default() -> Self { + AggregateSumQueryOptions { + allow_cache: true, + error_if_intermediate_path_tree_not_present: true, + error_if_non_sum_item_found: true, + ignore_references: false, + } + } +} + +#[cfg(feature = "minimal")] +/// Aggregate Sum Path query push arguments +#[allow(dead_code)] +pub struct AggregateSumPathQueryPushArgs<'db, 'ctx, 'a> +where + 'db: 'ctx, +{ + pub storage: &'db RocksDbStorage, + pub transaction: TransactionArg<'db, 'ctx>, + pub key: Option<&'a [u8]>, + pub element: Element, + pub path: &'a [&'a [u8]], + pub left_to_right: bool, + pub query_options: AggregateSumQueryOptions, + pub results: &'a mut Vec, + pub limit: &'a mut Option, + pub sum_limit_left: &'a mut SumValue, + pub elements_scanned: &'a mut u16, + pub max_elements_scanned: u16, +} + +#[cfg(feature = "minimal")] +impl<'db, 'ctx> fmt::Display for AggregateSumPathQueryPushArgs<'db, 'ctx, '_> +where + 'db: 'ctx, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "AggregateSumPathQueryPushArgs {{")?; + writeln!( + f, + " key: {}", + self.key.map_or("None".to_string(), hex_to_ascii) + )?; + writeln!(f, " element: {}", self.element)?; + writeln!( + f, + " path: [{}]", + self.path + .iter() + .map(|p| hex_to_ascii(p)) + .collect::>() + .join(", ") + )?; + writeln!(f, " left_to_right: {}", self.left_to_right)?; + writeln!(f, " query_options: {}", self.query_options)?; + writeln!( + f, + " results: [{}]", + self.results + .iter() + .map(|(key, value)| format!("0x{}: {}", hex::encode(key), value)) + .collect::>() + .join(", ") + )?; + writeln!(f, " limit: {:?}", self.limit)?; + writeln!(f, " sum_limit: {}", self.sum_limit_left)?; + writeln!(f, " elements_scanned: {}", self.elements_scanned)?; + writeln!(f, " max_elements_scanned: {}", self.max_elements_scanned)?; + write!(f, "}}") + } +} + +#[cfg(feature = "minimal")] +pub trait ElementAggregateSumQueryExtensions { + fn get_aggregate_sum_query( + storage: &RocksDbStorage, + aggregate_sum_path_query: &AggregateSumPathQuery, + query_options: AggregateSumQueryOptions, + transaction: TransactionArg, + grove_version: &GroveVersion, + ) -> CostResult; + fn get_aggregate_sum_query_apply_function( + storage: &RocksDbStorage, + path: &[&[u8]], + aggregate_sum_query: &AggregateSumQuery, + query_options: AggregateSumQueryOptions, + transaction: TransactionArg, + add_element_function: fn( + AggregateSumPathQueryPushArgs, + &GroveVersion, + ) -> CostResult<(), Error>, + grove_version: &GroveVersion, + ) -> CostResult; + fn aggregate_sum_path_query_push( + args: AggregateSumPathQueryPushArgs, + grove_version: &GroveVersion, + ) -> CostResult<(), Error>; + fn aggregate_sum_query_item( + storage: &RocksDbStorage, + item: &QueryItem, + results: &mut Vec, + path: &[&[u8]], + left_to_right: bool, + transaction: TransactionArg, + limit: &mut Option, + sum_limit_left: &mut SumValue, + query_options: AggregateSumQueryOptions, + add_element_function: fn( + AggregateSumPathQueryPushArgs, + &GroveVersion, + ) -> CostResult<(), Error>, + elements_scanned: &mut u16, + max_elements_scanned: u16, + grove_version: &GroveVersion, + ) -> CostResult<(), Error>; + fn basic_aggregate_sum_push( + args: AggregateSumPathQueryPushArgs, + grove_version: &GroveVersion, + ) -> Result<(), Error>; +} + +#[cfg(feature = "minimal")] +impl ElementAggregateSumQueryExtensions for Element { + /// Returns a vector of result elements based on given query + fn get_aggregate_sum_query( + storage: &RocksDbStorage, + aggregate_sum_path_query: &AggregateSumPathQuery, + query_options: AggregateSumQueryOptions, + transaction: TransactionArg, + grove_version: &GroveVersion, + ) -> CostResult { + check_grovedb_v0_with_cost!( + "get_aggregate_sum_query", + grove_version + .grovedb_versions + .element + .get_aggregate_sum_query + ); + + let path_slices = aggregate_sum_path_query + .path + .iter() + .map(|x| x.as_slice()) + .collect::>(); + Element::get_aggregate_sum_query_apply_function( + storage, + path_slices.as_slice(), + &aggregate_sum_path_query.aggregate_sum_query, + query_options, + transaction, + Element::aggregate_sum_path_query_push, + grove_version, + ) + } + + /// Returns a vector of result sum items with keys + /// based on given aggregate sum query + fn get_aggregate_sum_query_apply_function( + storage: &RocksDbStorage, + path: &[&[u8]], + aggregate_sum_query: &AggregateSumQuery, + query_options: AggregateSumQueryOptions, + transaction: TransactionArg, + add_element_function: fn( + AggregateSumPathQueryPushArgs, + &GroveVersion, + ) -> CostResult<(), Error>, + grove_version: &GroveVersion, + ) -> CostResult { + check_grovedb_v0_with_cost!( + "get_aggregate_sum_query_apply_function", + grove_version + .grovedb_versions + .element + .get_aggregate_sum_query_apply_function + ); + + let mut cost = OperationCost::default(); + + let mut results = Vec::new(); + + let mut limit = aggregate_sum_query.limit_of_items_to_check; + + let mut sum_limit: SumValue = cost_return_on_error_no_add!( + cost, + aggregate_sum_query + .sum_limit + .try_into() + .map_err(|_| Error::Overflow("sum_limit exceeds i64::MAX")) + ); + + if sum_limit <= 0 || limit == Some(0) { + return Ok(AggregateSumQueryResult { + results, + hard_limit_reached: false, + }) + .wrap_with_cost(cost); + } + + let mut elements_scanned: u16 = 0; + let max_elements_scanned = grove_version + .grovedb_versions + .query_limits + .max_aggregate_sum_query_elements_scanned; + + if aggregate_sum_query.left_to_right { + for item in aggregate_sum_query.iter() { + cost_return_on_error!( + &mut cost, + Self::aggregate_sum_query_item( + storage, + item, + &mut results, + path, + aggregate_sum_query.left_to_right, + transaction, + &mut limit, + &mut sum_limit, + query_options, + add_element_function, + &mut elements_scanned, + max_elements_scanned, + grove_version, + ) + ); + if sum_limit <= 0 { + break; + } + if limit == Some(0) { + break; + } + if elements_scanned > max_elements_scanned { + break; + } + } + } else { + for item in aggregate_sum_query.rev_iter() { + cost_return_on_error!( + &mut cost, + Self::aggregate_sum_query_item( + storage, + item, + &mut results, + path, + aggregate_sum_query.left_to_right, + transaction, + &mut limit, + &mut sum_limit, + query_options, + add_element_function, + &mut elements_scanned, + max_elements_scanned, + grove_version, + ) + ); + if sum_limit <= 0 { + break; + } + if limit == Some(0) { + break; + } + if elements_scanned > max_elements_scanned { + break; + } + } + } + + Ok(AggregateSumQueryResult { + hard_limit_reached: elements_scanned > max_elements_scanned, + results, + }) + .wrap_with_cost(cost) + } + + /// Push arguments to path query + fn aggregate_sum_path_query_push( + args: AggregateSumPathQueryPushArgs, + grove_version: &GroveVersion, + ) -> CostResult<(), Error> { + use crate::reference_path::{path_from_reference_qualified_path_type, ReferencePathType}; + use crate::util::{compat, TxRef}; + + check_grovedb_v0_with_cost!( + "path_query_push", + grove_version + .grovedb_versions + .element + .aggregate_sum_path_query_push + ); + + let mut cost = OperationCost::default(); + + if args.element.is_reference() { + if args.query_options.ignore_references { + // Silently skip references when ignore_references is enabled + return Ok(()).wrap_with_cost(cost); + } + // Follow the reference chain up to MAX_AGGREGATE_REFERENCE_HOPS + let element = args + .element + .convert_if_reference_to_absolute_reference(args.path, args.key); + let element = cost_return_on_error_into_no_add!(cost, element); + + let Element::Reference(ref_path, _, _) = element else { + return Err(Error::InternalError( + "expected a reference after conversion".to_string(), + )) + .wrap_with_cost(cost); + }; + + let mut current_qualified_path = match ref_path { + ReferencePathType::AbsolutePathReference(path) => path, + _ => { + return Err(Error::InternalError( + "expected absolute reference after conversion".to_string(), + )) + .wrap_with_cost(cost); + } + }; + + let tx = TxRef::new(args.storage, args.transaction); + let mut hops_left = MAX_AGGREGATE_REFERENCE_HOPS; + + loop { + let Some((key, ref_path_slices)) = current_qualified_path.split_last() else { + return Err(Error::CorruptedData("empty reference path".to_string())) + .wrap_with_cost(cost); + }; + + let ref_path_refs: Vec<&[u8]> = + ref_path_slices.iter().map(|s| s.as_slice()).collect(); + let subtree_path: SubtreePath<_> = ref_path_refs.as_slice().into(); + + let merk_res = compat::merk_optional_tx( + args.storage, + subtree_path, + tx.as_ref(), + None, + grove_version, + ); + + let merk = cost_return_on_error!(&mut cost, merk_res); + + let resolved = cost_return_on_error!( + &mut cost, + Element::get(&merk, key, args.query_options.allow_cache, grove_version) + .map_err(|e| e.into()) + ); + + match resolved { + Element::Reference(next_ref_path, _, _) => { + if hops_left == 0 { + return Err(Error::ReferenceLimit).wrap_with_cost(cost); + } + hops_left -= 1; + current_qualified_path = cost_return_on_error_into_no_add!( + cost, + path_from_reference_qualified_path_type( + next_ref_path, + ¤t_qualified_path + ) + .map_err(|e| Error::CorruptedData( + format!("failed to resolve reference path: {}", e) + )) + ); + } + resolved_element => { + // We followed the reference to its target. + // Replace the element in args and process it. + let new_args = AggregateSumPathQueryPushArgs { + element: resolved_element, + ..args + }; + if !new_args.element.is_sum_item() { + if !args.query_options.error_if_non_sum_item_found { + return Ok(()).wrap_with_cost(cost); + } + return Err(Error::InvalidPath( + "reference target is not a sum item".to_owned(), + )) + .wrap_with_cost(cost); + } + cost_return_on_error_no_add!( + cost, + Element::basic_aggregate_sum_push(new_args, grove_version) + ); + return Ok(()).wrap_with_cost(cost); + } + } + } + } else if !args.element.is_sum_item() { + if !args.query_options.error_if_non_sum_item_found { + // Silently skip non-sum, non-reference elements + return Ok(()).wrap_with_cost(cost); + } + return Err(Error::InvalidPath( + "we are only expecting sum items in this path".to_owned(), + )) + .wrap_with_cost(cost); + } else { + cost_return_on_error_no_add!( + cost, + Element::basic_aggregate_sum_push(args, grove_version) + ); + } + Ok(()).wrap_with_cost(cost) + } + + /// `decrease_limit_on_range_with_no_sub_elements` should generally be set + /// to true, as having it false could mean very expensive queries. + /// The queries would be expensive because we could go through many many + /// trees where the sub elements have no matches, hence the limit would + /// not decrease and hence we would continue on the increasingly + /// expensive query. + fn aggregate_sum_query_item( + storage: &RocksDbStorage, + item: &QueryItem, + results: &mut Vec, + path: &[&[u8]], + left_to_right: bool, + transaction: TransactionArg, + limit: &mut Option, + sum_limit_left: &mut SumValue, + query_options: AggregateSumQueryOptions, + add_element_function: fn( + AggregateSumPathQueryPushArgs, + &GroveVersion, + ) -> CostResult<(), Error>, + elements_scanned: &mut u16, + max_elements_scanned: u16, + grove_version: &GroveVersion, + ) -> CostResult<(), Error> { + use grovedb_storage::Storage; + + use crate::util::{compat, TxRef}; + + check_grovedb_v0_with_cost!( + "aggregate_sum_query_item", + grove_version + .grovedb_versions + .element + .aggregate_sum_query_item + ); + + let mut cost = OperationCost::default(); + let tx = TxRef::new(storage, transaction); + + let subtree_path: SubtreePath<_> = path.into(); + + if !item.is_range() { + // this is a query on a key + if let QueryItem::Key(key) = item { + let subtree_res = compat::merk_optional_tx( + storage, + subtree_path, + tx.as_ref(), + None, + grove_version, + ); + + if subtree_res.value().is_err() + && !matches!(subtree_res.value(), Err(Error::PathParentLayerNotFound(..))) + { + // simulating old macro's behavior by letting this particular kind of error to + // pass and to short circuit with the rest + return subtree_res.map_ok(|_| ()); + } + + let element_res = subtree_res + .flat_map_ok(|subtree| { + Element::get(&subtree, key, query_options.allow_cache, grove_version) + .add_context(format!("path is {}", path_as_slices_hex_to_ascii(path))) + .map_err(|e| e.into()) + }) + .unwrap_add_cost(&mut cost); + + match element_res { + Ok(element) => { + *elements_scanned = elements_scanned.saturating_add(1); + if *elements_scanned > max_elements_scanned { + return Ok(()).wrap_with_cost(cost); + } + // Check if we should skip this element type + if (!element.is_sum_item() + && !element.is_reference() + && !query_options.error_if_non_sum_item_found) + || (element.is_reference() && query_options.ignore_references) + { + return Ok(()).wrap_with_cost(cost); + } + match add_element_function( + AggregateSumPathQueryPushArgs { + storage, + transaction, + key: Some(key.as_slice()), + element, + path, + left_to_right, + query_options, + results, + limit, + sum_limit_left, + elements_scanned, + max_elements_scanned, + }, + grove_version, + ) + .unwrap_add_cost(&mut cost) + { + Ok(_) => Ok(()), + Err(e) => { + if !query_options.error_if_intermediate_path_tree_not_present { + match e { + Error::PathParentLayerNotFound(_) => Ok(()), + _ => Err(e), + } + } else { + Err(e) + } + } + } + } + Err(Error::PathKeyNotFound(_)) => Ok(()), + Err(e) => { + if !query_options.error_if_intermediate_path_tree_not_present { + match e { + Error::PathParentLayerNotFound(_) => Ok(()), + _ => Err(e), + } + } else { + Err(e) + } + } + } + } else { + Err(Error::InternalError( + "QueryItem must be a Key if not a range".to_string(), + )) + } + } else { + // this is a query on a range + let ctx = storage + .get_transactional_storage_context(subtree_path, None, tx.as_ref()) + .unwrap_add_cost(&mut cost); + + let mut iter = ctx.raw_iter(); + + item.seek_for_iter(&mut iter, left_to_right) + .unwrap_add_cost(&mut cost); + + while item + .iter_is_valid_for_type(&iter, *limit, Some(*sum_limit_left), left_to_right) + .unwrap_add_cost(&mut cost) + { + let value_bytes = cost_return_on_error_no_add!( + cost, + iter.value() + .unwrap_add_cost(&mut cost) + .ok_or(Error::CorruptedData( + "expected iterator value but got None".to_string(), + )) + ); + let element = cost_return_on_error_into_no_add!( + cost, + Element::raw_decode(value_bytes, grove_version) + ); + *elements_scanned = elements_scanned.saturating_add(1); + if *elements_scanned > max_elements_scanned { + break; + } + // Check if we should skip this element type + if (!element.is_sum_item() + && !element.is_reference() + && !query_options.error_if_non_sum_item_found) + || (element.is_reference() && query_options.ignore_references) + { + if left_to_right { + iter.next().unwrap_add_cost(&mut cost); + } else { + iter.prev().unwrap_add_cost(&mut cost); + } + cost.seek_count += 1; + continue; + } + let key = cost_return_on_error_no_add!( + cost, + iter.key() + .unwrap_add_cost(&mut cost) + .ok_or(Error::CorruptedData( + "expected iterator key but got None".to_string(), + )) + ); + let result_with_cost = add_element_function( + AggregateSumPathQueryPushArgs { + storage, + transaction, + key: Some(key), + element, + path, + left_to_right, + query_options, + results, + limit, + sum_limit_left, + elements_scanned, + max_elements_scanned, + }, + grove_version, + ); + let result = result_with_cost.unwrap_add_cost(&mut cost); + match result { + Ok(x) => x, + Err(e) => { + if !query_options.error_if_intermediate_path_tree_not_present { + match e { + Error::PathKeyNotFound(_) | Error::PathParentLayerNotFound(_) => (), + _ => return Err(e).wrap_with_cost(cost), + } + } else { + return Err(e).wrap_with_cost(cost); + } + } + } + if left_to_right { + iter.next().unwrap_add_cost(&mut cost); + } else { + iter.prev().unwrap_add_cost(&mut cost); + } + cost.seek_count += 1; + } + Ok(()) + } + .wrap_with_cost(cost) + } + + fn basic_aggregate_sum_push( + args: AggregateSumPathQueryPushArgs, + grove_version: &GroveVersion, + ) -> Result<(), Error> { + check_grovedb_v0!( + "basic_aggregate_sum_push", + grove_version + .grovedb_versions + .element + .basic_aggregate_sum_push + ); + + let AggregateSumPathQueryPushArgs { + path, + key, + element, + results, + limit, + sum_limit_left, + .. + } = args; + + let element = element.convert_if_reference_to_absolute_reference(path, key)?; + + let value = match element { + Element::SumItem(value, _) => value, + Element::ItemWithSumItem(_, value, _) => value, + _ => return Err(Error::InvalidInput("Only sum items are allowed")), + }; + + let key = key.ok_or(Error::CorruptedPath( + "basic push must have a key".to_string(), + ))?; + results.push((key.to_vec(), value)); + if let Some(limit) = limit { + *limit = limit.saturating_sub(1); + } + + *sum_limit_left = sum_limit_left.saturating_sub(value); + + Ok(()) + } +} + +#[cfg(feature = "minimal")] +#[cfg(test)] +mod tests; diff --git a/grovedb/src/element/aggregate_sum_query/tests.rs b/grovedb/src/element/aggregate_sum_query/tests.rs new file mode 100644 index 000000000..f032fd43d --- /dev/null +++ b/grovedb/src/element/aggregate_sum_query/tests.rs @@ -0,0 +1,2963 @@ +use grovedb_merk::proofs::query::AggregateSumQuery; +use grovedb_merk::proofs::query::QueryItem; +use grovedb_version::version::GroveVersion; + +use crate::element::aggregate_sum_query::{ + AggregateSumQueryOptions, ElementAggregateSumQueryExtensions, +}; +use crate::reference_path::ReferencePathType; +use crate::{ + tests::{make_test_sum_tree_grovedb, TEST_LEAF}, + AggregateSumPathQuery, Element, +}; + +#[test] +fn test_get_aggregate_sum_query_full_range() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"d", + Element::new_sum_item(11), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Test queries by full range up to 10 + let aggregate_sum_query = AggregateSumQuery::new(10, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"a".to_vec(), 7), (b"b".to_vec(), 5)] + ); + + // Test queries by full range up to 12 + let aggregate_sum_query = AggregateSumQuery::new(12, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"a".to_vec(), 7), (b"b".to_vec(), 5)] + ); + + // Test queries by full range up to 13 + let aggregate_sum_query = AggregateSumQuery::new(13, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"a".to_vec(), 7), (b"b".to_vec(), 5), (b"c".to_vec(), 3)] + ); + + // Test queries by full range up to 0 + let aggregate_sum_query = AggregateSumQuery::new(0, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![] + ); + + // Test queries by full range up to 100 + let aggregate_sum_query = AggregateSumQuery::new(100, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![ + (b"a".to_vec(), 7), + (b"b".to_vec(), 5), + (b"c".to_vec(), 3), + (b"d".to_vec(), 11), + ] + ); +} + +#[test] +fn test_get_aggregate_sum_query_full_range_descending() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"d", + Element::new_sum_item(11), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Test queries by full range up to 10 + let aggregate_sum_query = AggregateSumQuery::new_descending(10, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"d".to_vec(), 11)] + ); + + // Test queries by full range up to 12 + let aggregate_sum_query = AggregateSumQuery::new_descending(12, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"d".to_vec(), 11), (b"c".to_vec(), 3)] + ); + + // Test queries by full range up to 0 + let aggregate_sum_query = AggregateSumQuery::new_descending(0, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![] + ); + + // Test queries by full range up to 100 + let aggregate_sum_query = AggregateSumQuery::new_descending(100, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![ + (b"d".to_vec(), 11), + (b"c".to_vec(), 3), + (b"b".to_vec(), 5), + (b"a".to_vec(), 7), + ] + ); +} + +#[test] +fn test_get_aggregate_sum_query_sub_ranges() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"d", + Element::new_sum_item(11), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"e", + Element::new_sum_item(14), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"f", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Test queries by sub range up to 3 + let aggregate_sum_query = AggregateSumQuery::new_single_query_item( + QueryItem::Range(b"b".to_vec()..b"e".to_vec()), + 3, + None, + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"b".to_vec(), 5)] + ); + + // Test queries by sub range up to 0 + let aggregate_sum_query = AggregateSumQuery::new_single_query_item( + QueryItem::Range(b"b".to_vec()..b"e".to_vec()), + 0, + None, + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![] + ); + + // Test queries by sub range up to 100 + let aggregate_sum_query = AggregateSumQuery::new_single_query_item( + QueryItem::Range(b"b".to_vec()..b"e".to_vec()), + 100, + None, + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"b".to_vec(), 5), (b"c".to_vec(), 3), (b"d".to_vec(), 11),] + ); + + // Test queries by sub range inclusive up to 100 + let aggregate_sum_query = AggregateSumQuery::new_single_query_item( + QueryItem::RangeInclusive(b"b".to_vec()..=b"e".to_vec()), + 100, + None, + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![ + (b"b".to_vec(), 5), + (b"c".to_vec(), 3), + (b"d".to_vec(), 11), + (b"e".to_vec(), 14), + ] + ); +} + +#[test] +fn test_get_aggregate_sum_query_on_keys() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"d", + Element::new_sum_item(11), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"e", + Element::new_sum_item(14), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"f", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Test queries by sub range up to 50 + let aggregate_sum_query = AggregateSumQuery::new_with_keys( + vec![b"b".to_vec(), b"e".to_vec(), b"c".to_vec()], + 50, + None, + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + // We should get them back in the same order we asked + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"b".to_vec(), 5), (b"e".to_vec(), 14), (b"c".to_vec(), 3),] + ); + + // Test queries by sub range up to 6 + let aggregate_sum_query = AggregateSumQuery::new_with_keys( + vec![b"b".to_vec(), b"e".to_vec(), b"c".to_vec()], + 6, + None, + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + // We should get only the first 2 + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"b".to_vec(), 5), (b"e".to_vec(), 14),] + ); + + // Test queries by sub range up to 5 + let aggregate_sum_query = AggregateSumQuery::new_with_keys( + vec![b"b".to_vec(), b"e".to_vec(), b"c".to_vec()], + 5, + None, + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + // We should get only the first one + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"b".to_vec(), 5),] + ); + + // Test queries by sub range up to 50, but we make sure to only allow two elements to come back + let aggregate_sum_query = AggregateSumQuery::new_with_keys( + vec![b"b".to_vec(), b"e".to_vec(), b"c".to_vec()], + 50, + Some(2), + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + // We should get only the first two + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"b".to_vec(), 5), (b"e".to_vec(), 14),] + ); + + // Test queries by sub range up to 50, but we make sure to only allow two elements to come back, descending + let aggregate_sum_query = AggregateSumQuery::new_with_keys_reversed( + vec![b"b".to_vec(), b"e".to_vec(), b"c".to_vec()], + 50, + Some(2), + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + // We should get only the first two in reverse order + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"c".to_vec(), 3), (b"e".to_vec(), 14),] + ); + + // Test queries by sub range up to 3, descending + let aggregate_sum_query = AggregateSumQuery::new_with_keys_reversed( + vec![b"b".to_vec(), b"e".to_vec(), b"c".to_vec()], + 3, + None, + ); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + // We should get only the first one + assert_eq!( + Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version + ) + .unwrap() + .expect("expected successful get_query") + .results, + vec![(b"c".to_vec(), 3),] + ); +} + +#[test] +fn display_aggregate_sum_query_options_default() { + let opts = AggregateSumQueryOptions::default(); + let s = format!("{}", opts); + assert!(s.contains("allow_cache: true")); + assert!(s.contains("error_if_intermediate_path_tree_not_present: true")); + assert!(s.contains("error_if_non_sum_item_found: true")); + assert!(s.contains("ignore_references: false")); +} + +#[test] +fn display_aggregate_sum_query_options_custom() { + let opts = AggregateSumQueryOptions { + allow_cache: false, + error_if_intermediate_path_tree_not_present: false, + error_if_non_sum_item_found: false, + ignore_references: true, + }; + let s = format!("{}", opts); + assert!(s.contains("allow_cache: false")); + assert!(s.contains("error_if_intermediate_path_tree_not_present: false")); + assert!(s.contains("error_if_non_sum_item_found: false")); + assert!(s.contains("ignore_references: true")); +} + +#[test] +fn display_aggregate_sum_path_query_push_args() { + use crate::element::aggregate_sum_query::AggregateSumPathQueryPushArgs; + + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + let path: &[&[u8]] = &[TEST_LEAF]; + let mut results = vec![(b"prev".to_vec(), 42i64)]; + let mut limit = Some(5u16); + let mut sum_limit_left = 100i64; + let mut elements_scanned = 3u16; + + let args = AggregateSumPathQueryPushArgs { + storage: &db.db, + transaction: None, + key: Some(b"mykey"), + element: Element::new_sum_item(7), + path, + left_to_right: true, + query_options: AggregateSumQueryOptions::default(), + results: &mut results, + limit: &mut limit, + sum_limit_left: &mut sum_limit_left, + elements_scanned: &mut elements_scanned, + max_elements_scanned: 1024, + }; + + let s = format!("{}", args); + assert!(s.contains("AggregateSumPathQueryPushArgs")); + assert!(s.contains("key:")); + assert!(s.contains("left_to_right: true")); + assert!(s.contains("limit: Some(5)")); + assert!(s.contains("sum_limit: 100")); + assert!(s.contains("elements_scanned: 3")); + assert!(s.contains("max_elements_scanned: 1024")); + assert!(s.contains("0x70726576: 42")); // "prev" in hex +} + +#[test] +fn test_key_not_found_returns_empty() { + // Exercises line 417: Err(Error::PathKeyNotFound(_)) => Ok(()) + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Query for a key that doesn't exist + let aggregate_sum_query = AggregateSumQuery::new_single_key(b"nonexistent".to_vec(), 100); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert!(result.results.is_empty()); +} + +#[test] +fn test_non_sum_item_in_range_query_errors() { + // A range query encountering a non-SumItem element should error + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_item(b"not_a_sum_item".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Range query with error_if_non_sum_item_found=true (default) should error on the Item + let aggregate_sum_query = AggregateSumQuery::new(100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap(); + + assert!( + result.is_err(), + "expected error on non-SumItem in range query" + ); +} + +#[test] +fn test_query_with_limit_of_items_to_check() { + // Exercises line 256-258: limit == Some(0) break path in ascending + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(1), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(2), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // sum_limit is high but limit_of_items_to_check is 1 + let aggregate_sum_query = AggregateSumQuery::new(1000, Some(1)); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0], (b"a".to_vec(), 1)); +} + +#[test] +fn test_query_multiple_keys_with_some_missing() { + // Exercises line 417 PathKeyNotFound for some keys, success for others + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Query for 3 keys, but "b" doesn't exist + let aggregate_sum_query = AggregateSumQuery::new_with_keys( + vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()], + 100, + None, + ); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Should return only a and c, skipping the missing b + assert_eq!(result.results, vec![(b"a".to_vec(), 7), (b"c".to_vec(), 3)]); +} + +#[test] +fn test_descending_query_with_limit_break() { + // Exercises line 281-283: limit == Some(0) break path in descending branch + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(1), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(2), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // descending with items-to-check limit of 1 + let aggregate_sum_query = AggregateSumQuery::new_descending(1000, Some(1)); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!(result.results.len(), 1); + assert_eq!(result.results[0], (b"c".to_vec(), 3)); +} + +#[test] +fn test_range_query_skips_non_sum_items() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + // Insert a mix of Items and SumItems + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_item(b"regular_item".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"d", + Element::new_item(b"another_item".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"e", + Element::new_sum_item(11), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Query with error_if_non_sum_item_found=false should return only SumItems + let aggregate_sum_query = AggregateSumQuery::new(100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_non_sum_item_found: false, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!( + result.results, + vec![(b"a".to_vec(), 7), (b"c".to_vec(), 3), (b"e".to_vec(), 11),] + ); +} + +#[test] +fn test_range_query_skipped_items_do_not_decrement_limit() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_item(b"regular_item".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // With limit_of_items_to_check=2 and error_if_non_sum_item_found=false, + // b (item) is skipped without consuming a limit slot. + // a (sum_item, limit→1), b (item, skipped), c (sum_item, limit→0) + let aggregate_sum_query = AggregateSumQuery::new(100, Some(2)); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_non_sum_item_found: false, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Both "a" and "c" returned — skipped "b" didn't consume a limit slot + assert_eq!(result.results, vec![(b"a".to_vec(), 7), (b"c".to_vec(), 3)]); +} + +#[test] +fn test_key_query_skips_non_sum_items() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_item(b"regular_item".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Key query with error_if_non_sum_item_found=false: Item key "a" silently produces no result + let aggregate_sum_query = + AggregateSumQuery::new_with_keys(vec![b"a".to_vec(), b"b".to_vec()], 100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_non_sum_item_found: false, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!(result.results, vec![(b"b".to_vec(), 5)]); +} + +#[test] +fn test_hard_limit_returns_partial_results() { + // Create a custom grove version with max_elements_scanned=3 + let mut custom_version = GroveVersion::latest().clone(); + custom_version + .grovedb_versions + .query_limits + .max_aggregate_sum_query_elements_scanned = 3; + + let db = make_test_sum_tree_grovedb(&custom_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(1), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(2), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"d", + Element::new_sum_item(4), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"e", + Element::new_sum_item(5), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Query with sum_limit high enough to get all, but hard limit is 3 + let aggregate_sum_query = AggregateSumQuery::new(1000, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + &custom_version, + ) + .unwrap() + .expect("expected successful get_query (partial results, not error)"); + + // Should return only first 3 elements due to hard limit + assert_eq!( + result.results, + vec![(b"a".to_vec(), 1), (b"b".to_vec(), 2), (b"c".to_vec(), 3),] + ); + assert!(result.hard_limit_reached, "hard limit should be flagged"); +} + +#[test] +fn test_error_if_non_sum_item_found_default() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_item(b"regular_item".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Query with error_if_non_sum_item_found=true (default) should error on non-SumItem + let aggregate_sum_query = AggregateSumQuery::new_single_key(b"a".to_vec(), 100); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap(); + + assert!( + result.is_err(), + "expected error on non-SumItem with error_if_non_sum_item_found=true" + ); +} + +#[test] +fn test_zero_sum_limit_with_key_query_returns_empty() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // sum_limit = 0 with a single key query should return empty + let aggregate_sum_query = AggregateSumQuery::new_with_keys(vec![b"a".to_vec()], 0, None); + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert!( + result.results.is_empty(), + "sum_limit=0 should return no results, got: {:?}", + result + ); +} + +#[test] +fn test_item_with_sum_item_in_range_query() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_item_with_sum_item(b"payload".to_vec(), 10), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Full range query should include ItemWithSumItem using its sum value + let aggregate_sum_query = AggregateSumQuery::new(100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!( + result.results, + vec![(b"a".to_vec(), 7), (b"b".to_vec(), 10), (b"c".to_vec(), 3),] + ); +} + +#[test] +fn test_item_with_sum_item_in_key_query() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_item_with_sum_item(b"data_a".to_vec(), 15), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Key query for both types + let aggregate_sum_query = + AggregateSumQuery::new_with_keys(vec![b"a".to_vec(), b"b".to_vec()], 100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!( + result.results, + vec![(b"a".to_vec(), 15), (b"b".to_vec(), 5)] + ); +} + +#[test] +fn test_mixed_item_with_sum_item_and_sum_items_with_sum_limit() { + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(4), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_item_with_sum_item(b"payload".to_vec(), 6), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(8), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"d", + Element::new_item_with_sum_item(b"more_data".to_vec(), 12), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Sum limit of 10: a(4) + b(6) = 10, should stop after b + let aggregate_sum_query = AggregateSumQuery::new(10, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!(result.results, vec![(b"a".to_vec(), 4), (b"b".to_vec(), 6)]); +} + +#[test] +fn test_item_with_sum_item_not_skipped_when_error_disabled() { + // error_if_non_sum_item_found=false should only skip basic Items, not ItemWithSumItem + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_item(b"plain_item".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_item_with_sum_item(b"hybrid".to_vec(), 9), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // error_if_non_sum_item_found=false should skip "a" (basic Item) but keep "b" (ItemWithSumItem) + let aggregate_sum_query = AggregateSumQuery::new(100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_non_sum_item_found: false, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!(result.results, vec![(b"b".to_vec(), 9), (b"c".to_vec(), 3)]); +} + +#[test] +fn test_reference_to_sum_item_followed() { + // A reference to a SumItem should be followed and resolve to the target's sum value + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + // Insert a reference pointing to the sum item "a" + db.insert( + [TEST_LEAF].as_ref(), + b"ref_a", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"a".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + // Query for the reference key - should follow it and return the target's sum value + let aggregate_sum_query = AggregateSumQuery::new_single_key(b"ref_a".to_vec(), 100); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!(result.results, vec![(b"ref_a".to_vec(), 7)]); +} + +#[test] +fn test_reference_to_item_with_sum_item_followed() { + // A reference to an ItemWithSumItem should be followed and resolve to the target's sum value + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"hybrid", + Element::new_item_with_sum_item(b"data".to_vec(), 15), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"ref_hybrid", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"hybrid".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + let aggregate_sum_query = AggregateSumQuery::new_single_key(b"ref_hybrid".to_vec(), 100); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!(result.results, vec![(b"ref_hybrid".to_vec(), 15)]); +} + +#[test] +fn test_reference_to_regular_item_errors() { + // A reference that resolves to a regular Item (not a sum item) should error + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"item", + Element::new_item(b"not_a_sum_item".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"ref_item", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"item".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + let aggregate_sum_query = AggregateSumQuery::new_single_key(b"ref_item".to_vec(), 100); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap(); + + assert!( + result.is_err(), + "expected error when reference target is not a sum item" + ); +} + +#[test] +fn test_reference_to_sum_item_skipped_with_ignore_references() { + // With ignore_references=true, references are silently dropped + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + // Reference to sum item "a" + db.insert( + [TEST_LEAF].as_ref(), + b"ref_a", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"a".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + // Range query with ignore_references=true + let aggregate_sum_query = AggregateSumQuery::new(100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + ignore_references: true, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Only sum items returned, reference silently skipped + assert_eq!(result.results, vec![(b"a".to_vec(), 7), (b"b".to_vec(), 3)]); +} + +#[test] +fn test_reference_to_item_skipped_with_ignore_references() { + // Reference to a regular Item is also skipped with ignore_references=true + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"item", + Element::new_item(b"regular".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + // Reference to the regular item + db.insert( + [TEST_LEAF].as_ref(), + b"ref_item", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"item".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + // Range query skipping both items and references + let aggregate_sum_query = AggregateSumQuery::new(100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_non_sum_item_found: false, + ignore_references: true, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Only sum item "a" returned + assert_eq!(result.results, vec![(b"a".to_vec(), 7)]); +} + +#[test] +fn test_reference_to_item_with_sum_item_skipped_with_ignore_references() { + // Reference to an ItemWithSumItem is also skipped with ignore_references=true + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"hybrid", + Element::new_item_with_sum_item(b"data".to_vec(), 10), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + // Reference to the ItemWithSumItem + db.insert( + [TEST_LEAF].as_ref(), + b"ref_hybrid", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"hybrid".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + // Range query skipping references only + let aggregate_sum_query = AggregateSumQuery::new(100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + ignore_references: true, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Sum item and ItemWithSumItem returned, reference skipped + assert_eq!( + result.results, + vec![(b"a".to_vec(), 5), (b"hybrid".to_vec(), 10)] + ); +} + +#[test] +fn test_key_query_reference_skipped_with_ignore_references() { + // Key query targeting a reference key with ignore_references=true + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"ref_a", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"a".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + // Key query for both the sum item and the reference + let aggregate_sum_query = + AggregateSumQuery::new_with_keys(vec![b"ref_a".to_vec(), b"a".to_vec()], 100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + ignore_references: true, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Only sum item "a" returned, reference silently skipped + assert_eq!(result.results, vec![(b"a".to_vec(), 7)]); +} + +#[test] +fn test_reference_does_not_decrement_limit_when_skipped() { + // Skipped references should NOT count against the user limit + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"a".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // limit=2 with ignore_references: a (sum_item, limit→1), b (ref, skipped), c (sum_item, limit→0) + let aggregate_sum_query = AggregateSumQuery::new(100, Some(2)); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + ignore_references: true, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Both "a" and "c" returned — skipped ref "b" didn't consume a limit slot + assert_eq!(result.results, vec![(b"a".to_vec(), 5), (b"c".to_vec(), 3)]); +} + +#[test] +fn test_reference_followed_in_range_query() { + // References encountered during range iteration should be followed, not just in key queries + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"a".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Range query should follow the reference at "b" and resolve to sum value 7 + let aggregate_sum_query = AggregateSumQuery::new(100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!( + result.results, + vec![(b"a".to_vec(), 7), (b"b".to_vec(), 7), (b"c".to_vec(), 3)] + ); +} + +#[test] +fn test_multi_hop_reference_chain() { + // ref_c → ref_b → sum_item_a: should follow 2 hops and resolve + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(42), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + // ref_b points to sum_item "a" + db.insert( + [TEST_LEAF].as_ref(), + b"ref_b", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"a".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + // ref_c points to ref_b (chain: ref_c → ref_b → a) + db.insert( + [TEST_LEAF].as_ref(), + b"ref_c", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"ref_b".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + // Key query for the double-hop reference + let aggregate_sum_query = AggregateSumQuery::new_single_key(b"ref_c".to_vec(), 100); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!(result.results, vec![(b"ref_c".to_vec(), 42)]); +} + +#[test] +fn test_reference_limit_exceeded() { + // Chain of 5 references exceeds MAX_AGGREGATE_REFERENCE_HOPS (3). + // ref_a → ref_b → ref_c → ref_d → ref_e → target + // The initial convert gives us ref_b's path. Then in the loop: + // ref_b (hop 3→2), ref_c (2→1), ref_d (1→0), ref_e (hops_left==0 → error) + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"target", + Element::new_sum_item(99), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + // ref_e → target + db.insert( + [TEST_LEAF].as_ref(), + b"ref_e", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"target".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + // ref_d → ref_e + db.insert( + [TEST_LEAF].as_ref(), + b"ref_d", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"ref_e".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + // ref_c → ref_d + db.insert( + [TEST_LEAF].as_ref(), + b"ref_c", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"ref_d".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + // ref_b → ref_c + db.insert( + [TEST_LEAF].as_ref(), + b"ref_b", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"ref_c".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + // ref_a → ref_b + db.insert( + [TEST_LEAF].as_ref(), + b"ref_a", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"ref_b".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + let aggregate_sum_query = AggregateSumQuery::new_single_key(b"ref_a".to_vec(), 100); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap(); + + assert!( + result.is_err(), + "expected ReferenceLimit error for 5-reference chain (4 intermediate hops)" + ); +} + +#[test] +fn test_reference_at_max_hops_succeeds() { + // Chain of exactly MAX_AGGREGATE_REFERENCE_HOPS (3) intermediate hops should succeed. + // ref_a → ref_b → ref_c → ref_d → target + // The initial convert gives us ref_b's path. Then: + // ref_b (hop 3→2), ref_c (2→1), ref_d (1→0), target resolved → success + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"target", + Element::new_sum_item(99), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"ref_d", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"target".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + db.insert( + [TEST_LEAF].as_ref(), + b"ref_c", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"ref_d".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + db.insert( + [TEST_LEAF].as_ref(), + b"ref_b", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"ref_c".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + db.insert( + [TEST_LEAF].as_ref(), + b"ref_a", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"ref_b".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + let aggregate_sum_query = AggregateSumQuery::new_single_key(b"ref_a".to_vec(), 100); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("4-reference chain (3 intermediate hops) should succeed"); + + assert_eq!(result.results, vec![(b"ref_a".to_vec(), 99)]); +} + +#[test] +fn test_reference_to_item_skipped_after_following_when_error_disabled() { + // Reference → regular Item with error_if_non_sum_item_found=false should silently skip + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"item", + Element::new_item(b"not_a_sum".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + // Reference to the regular Item + db.insert( + [TEST_LEAF].as_ref(), + b"ref_item", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"item".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + // With error_if_non_sum_item_found=false: following the reference resolves to a regular Item, + // which should be silently skipped + let aggregate_sum_query = + AggregateSumQuery::new_with_keys(vec![b"a".to_vec(), b"ref_item".to_vec()], 100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_non_sum_item_found: false, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Only "a" returned; reference to Item was followed then skipped + assert_eq!(result.results, vec![(b"a".to_vec(), 5)]); +} + +#[test] +fn test_negative_sum_values() { + // Negative SumItem values should work correctly with sum_limit + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(10), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(-3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // sum_limit=12: a(10) leaves 2, b(-3) increases remaining to 5, c(5) leaves 0 + // All three should be returned + let aggregate_sum_query = AggregateSumQuery::new(12, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!( + result.results, + vec![(b"a".to_vec(), 10), (b"b".to_vec(), -3), (b"c".to_vec(), 5),] + ); + + // sum_limit=8: a(10) leaves -2, which is <= 0, so stop after a + let aggregate_sum_query = AggregateSumQuery::new(8, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + assert_eq!(result.results, vec![(b"a".to_vec(), 10)]); +} + +#[test] +fn test_hard_limit_with_key_queries() { + // Hard limit should also work with key queries + let mut custom_version = GroveVersion::latest().clone(); + custom_version + .grovedb_versions + .query_limits + .max_aggregate_sum_query_elements_scanned = 2; + + let db = make_test_sum_tree_grovedb(&custom_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(1), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(2), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Key query for 3 keys with hard limit of 2 + let aggregate_sum_query = AggregateSumQuery::new_with_keys( + vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()], + 100, + None, + ); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + &custom_version, + ) + .unwrap() + .expect("expected successful get_query (partial results)"); + + // Should return only first 2 elements due to hard limit + assert_eq!(result.results, vec![(b"a".to_vec(), 1), (b"b".to_vec(), 2)]); + assert!(result.hard_limit_reached, "hard limit should be flagged"); +} + +#[test] +fn test_error_if_intermediate_path_tree_not_present_false() { + // With error_if_intermediate_path_tree_not_present=false, a missing path + // should be treated as empty rather than erroring + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + // Query a path that doesn't exist + let aggregate_sum_query = AggregateSumQuery::new_single_key(b"a".to_vec(), 100); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![b"nonexistent_path".to_vec()], + aggregate_sum_query, + }; + + // With default (true), this should error + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap(); + assert!(result.is_err(), "expected error with default options"); + + // With error_if_intermediate_path_tree_not_present=false, should return empty + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_intermediate_path_tree_not_present: false, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query with missing path treated as empty"); + + assert!(result.results.is_empty()); +} + +#[test] +fn test_descending_hard_limit() { + // Hard limit should work in descending (right-to-left) queries + let mut custom_version = GroveVersion::latest().clone(); + custom_version + .grovedb_versions + .query_limits + .max_aggregate_sum_query_elements_scanned = 2; + + let db = make_test_sum_tree_grovedb(&custom_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(1), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(2), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"d", + Element::new_sum_item(4), + None, + None, + &custom_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Descending range query with hard limit of 2 + let mut aggregate_sum_query = AggregateSumQuery::new(100, None); + aggregate_sum_query.left_to_right = false; + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + &custom_version, + ) + .unwrap() + .expect("expected successful get_query (partial results)"); + + // Descending: d(4), c(3) — hard limit reached after 2 elements + assert_eq!(result.results, vec![(b"d".to_vec(), 4), (b"c".to_vec(), 3)]); + assert!(result.hard_limit_reached, "hard limit should be flagged"); +} + +#[test] +fn test_descending_range_skip_non_sum_items() { + // Descending range query should skip non-sum items correctly + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(1), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_item(b"regular".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + let mut aggregate_sum_query = AggregateSumQuery::new(100, None); + aggregate_sum_query.left_to_right = false; + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_non_sum_item_found: false, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Descending: c(3), b(item, skipped), a(1) + assert_eq!(result.results, vec![(b"c".to_vec(), 3), (b"a".to_vec(), 1)]); +} + +#[test] +fn test_key_query_skip_with_limit() { + // Key query with error_if_non_sum_item_found=false and limit: + // skipped elements should not consume limit slots + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_item(b"regular".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + // Key query for all three with limit=1 + let aggregate_sum_query = AggregateSumQuery::new_with_keys( + vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()], + 100, + Some(1), + ); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_non_sum_item_found: false, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // "a" is skipped (Item, no limit consumed), "b" returned (limit→0), "c" not reached + assert_eq!(result.results, vec![(b"b".to_vec(), 5)]); +} + +#[test] +fn test_descending_reference_followed() { + // References should be followed in descending range queries too + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"target", + Element::new_sum_item(42), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(1), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"r", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + TEST_LEAF.to_vec(), + b"target".to_vec(), + ])), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert reference"); + + let mut aggregate_sum_query = AggregateSumQuery::new(100, None); + aggregate_sum_query.left_to_right = false; + + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = Element::get_aggregate_sum_query( + &db.db, + &aggregate_sum_path_query, + AggregateSumQueryOptions::default(), + None, + grove_version, + ) + .unwrap() + .expect("expected successful get_query"); + + // Descending: target(42), r(ref→target=42), a(1) + assert_eq!( + result.results, + vec![ + (b"target".to_vec(), 42), + (b"r".to_vec(), 42), + (b"a".to_vec(), 1), + ] + ); +} diff --git a/grovedb/src/element/mod.rs b/grovedb/src/element/mod.rs index c8104833d..78628eb74 100644 --- a/grovedb/src/element/mod.rs +++ b/grovedb/src/element/mod.rs @@ -2,6 +2,8 @@ //! Subtrees handling is isolated so basically this module is about adapting //! Merk API to GroveDB needs. +#[cfg(any(feature = "minimal", feature = "verify"))] +pub mod aggregate_sum_query; #[cfg(feature = "minimal")] pub mod elements_iterator; #[cfg(feature = "minimal")] diff --git a/grovedb/src/element/query.rs b/grovedb/src/element/query.rs index 045590950..50980d209 100644 --- a/grovedb/src/element/query.rs +++ b/grovedb/src/element/query.rs @@ -703,7 +703,7 @@ impl ElementQueryExtensions for Element { .unwrap_add_cost(&mut cost); while item - .iter_is_valid_for_type(&iter, *limit, sized_query.query.left_to_right) + .iter_is_valid_for_type(&iter, *limit, None, sized_query.query.left_to_right) .unwrap_add_cost(&mut cost) { let element = cost_return_on_error_into_no_add!( diff --git a/grovedb/src/error.rs b/grovedb/src/error.rs index 6ce8c09c1..f65d30ea2 100644 --- a/grovedb/src/error.rs +++ b/grovedb/src/error.rs @@ -162,6 +162,10 @@ pub enum Error { /// Cyclic reference CyclicError(&'static str), + #[error("overflow error: {0}")] + /// Overflow error + Overflow(&'static str), + #[error("commitment tree error: {0}")] /// Commitment tree operation error CommitmentTreeError(String), @@ -242,3 +246,9 @@ impl From for Error { Error::ElementError(value) } } + +impl From for Error { + fn from(value: grovedb_query::error::Error) -> Self { + Error::QueryError(value) + } +} diff --git a/grovedb/src/lib.rs b/grovedb/src/lib.rs index f2854ba0e..79787482c 100644 --- a/grovedb/src/lib.rs +++ b/grovedb/src/lib.rs @@ -172,6 +172,8 @@ use std::{collections::HashMap, option::Option::None, path::Path}; #[cfg(feature = "grovedbg")] use debugger::start_visualizer; #[cfg(any(feature = "minimal", feature = "verify"))] +pub use element::aggregate_sum_query::{AggregateSumQueryOptions, AggregateSumQueryResult}; +#[cfg(any(feature = "minimal", feature = "verify"))] pub use element::Element; #[cfg(any(feature = "minimal", feature = "verify"))] pub use element::ElementFlags; @@ -237,8 +239,8 @@ use grovedb_version::version::GroveVersion; use grovedb_visualize::DebugByteVectors; #[cfg(any(feature = "minimal", feature = "verify"))] pub use query::{ - GroveBranchQueryResult, GroveTrunkQueryResult, LeafInfo, PathBranchChunkQuery, PathQuery, - PathTrunkChunkQuery, SizedQuery, + aggregate_sum_path_query::AggregateSumPathQuery, GroveBranchQueryResult, GroveTrunkQueryResult, + LeafInfo, PathBranchChunkQuery, PathQuery, PathTrunkChunkQuery, SizedQuery, }; #[cfg(feature = "minimal")] use reference_path::path_from_reference_path_type; diff --git a/grovedb/src/operations/get/query.rs b/grovedb/src/operations/get/query.rs index 0e3cf06f9..4317f2a85 100644 --- a/grovedb/src/operations/get/query.rs +++ b/grovedb/src/operations/get/query.rs @@ -1,22 +1,19 @@ //! Query operations -use grovedb_costs::cost_return_on_error_default; -#[cfg(feature = "minimal")] -use grovedb_costs::{ - cost_return_on_error, cost_return_on_error_no_add, CostResult, CostsExt, OperationCost, -}; -use grovedb_version::{check_grovedb_v0, check_grovedb_v0_with_cost, version::GroveVersion}; -#[cfg(feature = "minimal")] -use integer_encoding::VarInt; - #[cfg(feature = "minimal")] use crate::element::SumValue; use crate::{ element::{ - query::ElementQueryExtensions, query_options::QueryOptions, BigSumValue, CountValue, + aggregate_sum_query::{ + AggregateSumQueryOptions, AggregateSumQueryResult, ElementAggregateSumQueryExtensions, + }, + query::ElementQueryExtensions, + query_options::QueryOptions, + BigSumValue, CountValue, }, operations::proof::ProveOptions, query_result_type::PathKeyOptionalElementTrio, + AggregateSumPathQuery, }; #[cfg(feature = "minimal")] use crate::{ @@ -24,6 +21,14 @@ use crate::{ reference_path::ReferencePathType, Element, Error, GroveDb, PathQuery, TransactionArg, }; +use grovedb_costs::cost_return_on_error_default; +#[cfg(feature = "minimal")] +use grovedb_costs::{ + cost_return_on_error, cost_return_on_error_no_add, CostResult, CostsExt, OperationCost, +}; +use grovedb_version::{check_grovedb_v0, check_grovedb_v0_with_cost, version::GroveVersion}; +#[cfg(feature = "minimal")] +use integer_encoding::VarInt; #[cfg(feature = "minimal")] #[derive(Debug, Eq, PartialEq, Clone)] @@ -542,6 +547,68 @@ where { Ok((results, skipped)).wrap_with_cost(cost) } + /// Retrieves only SumItem elements that match a path query. + /// Uses default options: errors on non-sum items, follows references. + /// For full control over skip/ignore behavior, use + /// `query_aggregate_sums_with_options`. + pub fn query_aggregate_sums( + &self, + aggregate_sum_path_query: &AggregateSumPathQuery, + allow_cache: bool, + error_if_intermediate_path_tree_not_present: bool, + transaction: TransactionArg, + grove_version: &GroveVersion, + ) -> CostResult { + check_grovedb_v0_with_cost!( + "query_sums", + grove_version + .grovedb_versions + .operations + .query + .query_aggregate_sums + ); + + Element::get_aggregate_sum_query( + &self.db, + aggregate_sum_path_query, + AggregateSumQueryOptions { + allow_cache, + error_if_intermediate_path_tree_not_present, + error_if_non_sum_item_found: true, + ignore_references: false, + }, + transaction, + grove_version, + ) + } + + /// Retrieves SumItem elements matching a path query with full control + /// over query behavior via `AggregateSumQueryOptions`. + pub fn query_aggregate_sums_with_options( + &self, + aggregate_sum_path_query: &AggregateSumPathQuery, + query_options: AggregateSumQueryOptions, + transaction: TransactionArg, + grove_version: &GroveVersion, + ) -> CostResult { + check_grovedb_v0_with_cost!( + "query_sums", + grove_version + .grovedb_versions + .operations + .query + .query_aggregate_sums + ); + + Element::get_aggregate_sum_query( + &self.db, + aggregate_sum_path_query, + query_options, + transaction, + grove_version, + ) + } + /// Retrieves only SumItem elements that match a path query pub fn query_sums( &self, @@ -2158,4 +2225,120 @@ mod tests { None ); // because we didn't query for it } + + #[test] + fn test_query_aggregate_sums() { + use grovedb_merk::proofs::query::AggregateSumQuery; + + use crate::{ + tests::{make_test_sum_tree_grovedb, TEST_LEAF}, + AggregateSumPathQuery, + }; + + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_sum_item(5), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + let aggregate_sum_query = AggregateSumQuery::new(20, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + let result = db + .query_aggregate_sums(&aggregate_sum_path_query, true, true, None, grove_version) + .unwrap() + .expect("expected successful query"); + + assert_eq!(result.results, vec![(b"a".to_vec(), 7), (b"b".to_vec(), 5)]); + assert!(!result.hard_limit_reached); + } + + #[test] + fn test_query_aggregate_sums_with_options() { + use grovedb_merk::proofs::query::AggregateSumQuery; + + use crate::{ + element::aggregate_sum_query::AggregateSumQueryOptions, + tests::{make_test_sum_tree_grovedb, TEST_LEAF}, + AggregateSumPathQuery, + }; + + let grove_version = GroveVersion::latest(); + let db = make_test_sum_tree_grovedb(grove_version); + + db.insert( + [TEST_LEAF].as_ref(), + b"a", + Element::new_sum_item(7), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"b", + Element::new_item(b"not_a_sum".to_vec()), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + db.insert( + [TEST_LEAF].as_ref(), + b"c", + Element::new_sum_item(3), + None, + None, + grove_version, + ) + .unwrap() + .expect("cannot insert element"); + + let aggregate_sum_query = AggregateSumQuery::new(100, None); + let aggregate_sum_path_query = AggregateSumPathQuery { + path: vec![TEST_LEAF.to_vec()], + aggregate_sum_query, + }; + + // With error_if_non_sum_item_found=false, Item "b" is skipped + let result = db + .query_aggregate_sums_with_options( + &aggregate_sum_path_query, + AggregateSumQueryOptions { + error_if_non_sum_item_found: false, + ..AggregateSumQueryOptions::default() + }, + None, + grove_version, + ) + .unwrap() + .expect("expected successful query"); + + assert_eq!(result.results, vec![(b"a".to_vec(), 7), (b"c".to_vec(), 3)]); + assert!(!result.hard_limit_reached); + } } diff --git a/grovedb/src/query/aggregate_sum_path_query.rs b/grovedb/src/query/aggregate_sum_path_query.rs new file mode 100644 index 000000000..c605ed8b6 --- /dev/null +++ b/grovedb/src/query/aggregate_sum_path_query.rs @@ -0,0 +1,215 @@ +use crate::operations::proof::util::hex_to_ascii; +use crate::Error; +use bincode::{Decode, Encode}; +use grovedb_merk::proofs::query::AggregateSumQuery; +use grovedb_merk::proofs::query::QueryItem; +use grovedb_version::check_grovedb_v0; +use grovedb_version::version::GroveVersion; +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Encode, Decode)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +/// Path query +/// +/// Represents a path to a specific GroveDB tree and a corresponding query to +/// apply to the given tree. +pub struct AggregateSumPathQuery { + /// Path + pub path: Vec>, + /// The aggregate sum query + pub aggregate_sum_query: AggregateSumQuery, +} + +impl fmt::Display for AggregateSumPathQuery { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "AggregateSumPathQuery {{ path: [")?; + for (i, path_element) in self.path.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", hex_to_ascii(path_element))?; + } + write!(f, "], aggregate sum query: {} }}", self.aggregate_sum_query) + } +} + +impl AggregateSumPathQuery { + /// New path query + pub const fn new(path: Vec>, aggregate_sum_query: AggregateSumQuery) -> Self { + Self { + path, + aggregate_sum_query, + } + } + + /// New path query with a single key + pub fn new_single_key(path: Vec>, key: Vec, sum_limit: u64) -> Self { + Self { + path, + aggregate_sum_query: AggregateSumQuery::new_single_key(key, sum_limit), + } + } + + /// New path query with a single query item + pub fn new_single_query_item( + path: Vec>, + query_item: QueryItem, + sum_limit: u64, + limit_of_items_to_check: Option, + ) -> Self { + Self { + path, + aggregate_sum_query: AggregateSumQuery::new_single_query_item( + query_item, + sum_limit, + limit_of_items_to_check, + ), + } + } + + /// Combines multiple aggregate sum path queries into one equivalent aggregate sum path query. + /// All path queries must share the same path. + pub fn merge( + mut path_queries: Vec<&AggregateSumPathQuery>, + grove_version: &GroveVersion, + ) -> Result { + check_grovedb_v0!( + "merge", + grove_version + .grovedb_versions + .aggregate_sum_path_query_methods + .merge + ); + if path_queries.is_empty() { + return Err(Error::InvalidInput( + "merge function requires at least 1 path query", + )); + } + if path_queries.len() == 1 { + return Ok(path_queries.remove(0).clone()); + } + + // Use the path from the first query as the reference + let common_path = &path_queries[0].path; + + // Verify all paths are equal + if !path_queries.iter().all(|q| &q.path == common_path) { + return Err(Error::InvalidInput( + "all path queries must have the same path", + )); + } + + // Extract aggregate_sum_query values and clone them + let aggregate_queries: Vec = path_queries + .iter() + .map(|q| q.aggregate_sum_query.clone()) + .collect(); + + // Merge all aggregate_sum_query values + let merged_query = AggregateSumQuery::merge_multiple(aggregate_queries)?; + + // Return a new AggregateSumPathQuery with the common path and merged query + Ok(AggregateSumPathQuery { + path: common_path.clone(), + aggregate_sum_query: merged_query, + }) + } +} + +#[cfg(test)] +mod tests { + use grovedb_merk::proofs::query::AggregateSumQuery; + use grovedb_merk::proofs::query::QueryItem; + use grovedb_version::version::GroveVersion; + + use super::*; + + #[test] + fn display_includes_path_and_query() { + let q = AggregateSumPathQuery::new( + vec![b"root".to_vec(), b"leaf".to_vec()], + AggregateSumQuery::new(42, None), + ); + let s = format!("{}", q); + assert!(s.contains("AggregateSumPathQuery")); + assert!(s.contains("root")); + assert!(s.contains("leaf")); + assert!(s.contains("42")); + } + + #[test] + fn new_single_key_constructor() { + let q = AggregateSumPathQuery::new_single_key(vec![b"p".to_vec()], b"mykey".to_vec(), 100); + assert_eq!(q.path, vec![b"p".to_vec()]); + assert_eq!(q.aggregate_sum_query.sum_limit, 100); + assert_eq!( + q.aggregate_sum_query.items, + vec![QueryItem::Key(b"mykey".to_vec())] + ); + } + + #[test] + fn new_single_query_item_constructor() { + let q = AggregateSumPathQuery::new_single_query_item( + vec![b"p".to_vec()], + QueryItem::RangeFull(..), + 50, + Some(10), + ); + assert_eq!(q.aggregate_sum_query.sum_limit, 50); + assert_eq!(q.aggregate_sum_query.limit_of_items_to_check, Some(10)); + } + + #[test] + fn merge_empty_returns_error() { + let grove_version = GroveVersion::latest(); + let err = AggregateSumPathQuery::merge(vec![], grove_version).unwrap_err(); + assert!( + format!("{}", err).contains("at least 1"), + "expected 'at least 1' error, got: {}", + err + ); + } + + #[test] + fn merge_single_returns_clone() { + let grove_version = GroveVersion::latest(); + let q = + AggregateSumPathQuery::new(vec![b"path".to_vec()], AggregateSumQuery::new(42, Some(5))); + let merged = AggregateSumPathQuery::merge(vec![&q], grove_version).unwrap(); + assert_eq!(merged, q); + } + + #[test] + fn merge_mismatched_paths_returns_error() { + let grove_version = GroveVersion::latest(); + let q1 = + AggregateSumPathQuery::new(vec![b"path_a".to_vec()], AggregateSumQuery::new(10, None)); + let q2 = + AggregateSumPathQuery::new(vec![b"path_b".to_vec()], AggregateSumQuery::new(10, None)); + let err = AggregateSumPathQuery::merge(vec![&q1, &q2], grove_version).unwrap_err(); + assert!( + format!("{}", err).contains("same path"), + "expected 'same path' error, got: {}", + err + ); + } + + #[test] + fn merge_two_queries_sums_limits() { + let grove_version = GroveVersion::latest(); + let q1 = AggregateSumPathQuery::new( + vec![b"p".to_vec()], + AggregateSumQuery::new_with_keys(vec![vec![1]], 10, Some(2)), + ); + let q2 = AggregateSumPathQuery::new( + vec![b"p".to_vec()], + AggregateSumQuery::new_with_keys(vec![vec![2]], 20, Some(3)), + ); + let merged = AggregateSumPathQuery::merge(vec![&q1, &q2], grove_version).unwrap(); + assert_eq!(merged.path, vec![b"p".to_vec()]); + assert_eq!(merged.aggregate_sum_query.sum_limit, 30); + assert_eq!(merged.aggregate_sum_query.limit_of_items_to_check, Some(5)); + assert_eq!(merged.aggregate_sum_query.items.len(), 2); + } +} diff --git a/grovedb/src/query/mod.rs b/grovedb/src/query/mod.rs index 78b54deee..70bc4f5ed 100644 --- a/grovedb/src/query/mod.rs +++ b/grovedb/src/query/mod.rs @@ -1,5 +1,6 @@ //! Queries +pub mod aggregate_sum_path_query; mod grove_branch_query_result; mod grove_trunk_query_result; mod path_branch_chunk_query; @@ -19,7 +20,7 @@ pub use grove_trunk_query_result::{GroveTrunkQueryResult, LeafInfo}; #[cfg(any(feature = "minimal", feature = "verify"))] use grovedb_merk::proofs::query::query_item::QueryItem; use grovedb_merk::proofs::query::{Key, SubqueryBranch}; -#[cfg(any(feature = "minimal", feature = "verify"))] + use grovedb_merk::proofs::Query; use grovedb_version::{check_grovedb_v0, version::GroveVersion}; use indexmap::IndexMap; @@ -29,12 +30,11 @@ pub use path_branch_chunk_query::PathBranchChunkQuery; pub use path_trunk_chunk_query::PathTrunkChunkQuery; use crate::operations::proof::util::hex_to_ascii; -#[cfg(any(feature = "minimal", feature = "verify"))] + use crate::query_result_type::PathKey; -#[cfg(any(feature = "minimal", feature = "verify"))] + use crate::Error; -#[cfg(any(feature = "minimal", feature = "verify"))] #[derive(Debug, Clone, PartialEq, Encode, Decode)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Path query @@ -48,7 +48,6 @@ pub struct PathQuery { pub query: SizedQuery, } -#[cfg(any(feature = "minimal", feature = "verify"))] impl fmt::Display for PathQuery { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "PathQuery {{ path: [")?; @@ -62,7 +61,6 @@ impl fmt::Display for PathQuery { } } -#[cfg(any(feature = "minimal", feature = "verify"))] #[derive(Debug, Clone, PartialEq, Encode, Decode)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Holds a query to apply to a tree and an optional limit/offset value. @@ -76,7 +74,6 @@ pub struct SizedQuery { pub offset: Option, } -#[cfg(any(feature = "minimal", feature = "verify"))] impl fmt::Display for SizedQuery { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "SizedQuery {{ query: {}", self.query)?; @@ -90,7 +87,6 @@ impl fmt::Display for SizedQuery { } } -#[cfg(any(feature = "minimal", feature = "verify"))] impl SizedQuery { /// New sized query pub const fn new(query: Query, limit: Option, offset: Option) -> Self { @@ -120,7 +116,6 @@ impl SizedQuery { } } -#[cfg(any(feature = "minimal", feature = "verify"))] impl PathQuery { /// New path query pub const fn new(path: Vec>, query: SizedQuery) -> Self { @@ -590,7 +585,6 @@ impl PathQuery { } } -#[cfg(any(feature = "minimal", feature = "verify"))] #[derive(Debug, Clone, PartialEq)] pub enum HasSubquery<'a> { NoSubquery, @@ -598,7 +592,6 @@ pub enum HasSubquery<'a> { Conditionally(Cow<'a, IndexMap>), } -#[cfg(any(feature = "minimal", feature = "verify"))] impl fmt::Display for HasSubquery<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -630,7 +623,7 @@ impl HasSubquery<'_> { /// This represents a query where the items might be borrowed, it is used to get /// subquery information -#[cfg(any(feature = "minimal", feature = "verify"))] + #[derive(Debug, Clone, PartialEq)] pub struct SinglePathSubquery<'a> { /// Items @@ -644,7 +637,6 @@ pub struct SinglePathSubquery<'a> { pub in_path: Option>, } -#[cfg(any(feature = "minimal", feature = "verify"))] impl fmt::Display for SinglePathSubquery<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "InternalCowItemsQuery {{")?; diff --git a/grovedb/src/query_result_type.rs b/grovedb/src/query_result_type.rs index a8d329f7b..2b01fe6ae 100644 --- a/grovedb/src/query_result_type.rs +++ b/grovedb/src/query_result_type.rs @@ -9,6 +9,7 @@ use std::{ pub use grovedb_merk::proofs::query::{Key, Path, PathKey}; use grovedb_version::{version::GroveVersion, TryFromVersioned}; +use crate::element::SumValue; use crate::{ operations::proof::util::{ hex_to_ascii, path_hex_to_ascii, ProvedPathKeyOptionalValue, ProvedPathKeyValue, @@ -491,6 +492,10 @@ impl QueryResultElement { /// Type alias for key-element common pattern. pub type KeyElementPair = (Key, Element); +#[cfg(any(feature = "minimal", feature = "verify"))] +/// Type alias for key-sum value common pattern. +pub type KeySumValuePair = (Key, SumValue); + #[cfg(any(feature = "minimal", feature = "verify"))] /// Type alias for key optional_element common pattern. pub type KeyOptionalElementPair = (Key, Option); diff --git a/grovedb/src/tests/mod.rs b/grovedb/src/tests/mod.rs index e5a11a759..af16231c4 100644 --- a/grovedb/src/tests/mod.rs +++ b/grovedb/src/tests/mod.rs @@ -116,6 +116,21 @@ pub fn make_test_grovedb(grove_version: &GroveVersion) -> TempGroveDb { } } +/// A helper method to create GroveDB with one leaf for a root tree +pub fn make_test_sum_tree_grovedb(grove_version: &GroveVersion) -> TempGroveDb { + // Tree Structure + // root + // test_leaf + // another_test_leaf + let tmp_dir = TempDir::new().unwrap(); + let mut db = GroveDb::open(tmp_dir.path()).unwrap(); + add_test_sum_tree_leaves(&mut db, grove_version); + TempGroveDb { + _tmp_dir: tmp_dir, + grove_db: db, + } +} + fn add_test_leaves(db: &mut GroveDb, grove_version: &GroveVersion) { db.insert( EMPTY_PATH, @@ -139,6 +154,29 @@ fn add_test_leaves(db: &mut GroveDb, grove_version: &GroveVersion) { .expect("successful root tree leaf 2 insert"); } +fn add_test_sum_tree_leaves(db: &mut GroveDb, grove_version: &GroveVersion) { + db.insert( + EMPTY_PATH, + TEST_LEAF, + Element::empty_sum_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful root tree leaf insert"); + db.insert( + EMPTY_PATH, + ANOTHER_TEST_LEAF, + Element::empty_sum_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("successful root tree leaf 2 insert"); +} + pub fn make_deep_tree(grove_version: &GroveVersion) -> TempGroveDb { // Tree Structure // root diff --git a/merk/src/error.rs b/merk/src/error.rs index 37004545a..34e378d22 100644 --- a/merk/src/error.rs +++ b/merk/src/error.rs @@ -203,6 +203,7 @@ impl From for Error { grovedb_query::error::Error::InvalidProofError(s) => Error::InvalidProofError(s), grovedb_query::error::Error::KeyOrderingError(s) => Error::KeyOrderingError(s), grovedb_query::error::Error::EdError(e) => Error::EdError(e), + grovedb_query::error::Error::Overflow(s) => Error::Overflow(s), } } } diff --git a/merk/src/merk/mod.rs b/merk/src/merk/mod.rs index 84c007183..91758dca4 100644 --- a/merk/src/merk/mod.rs +++ b/merk/src/merk/mod.rs @@ -149,7 +149,7 @@ impl<'a, I: RawIterator> KVIterator<'a, I> { let mut cost = OperationCost::default(); if query_item - .iter_is_valid_for_type(&self.raw_iter, None, self.left_to_right) + .iter_is_valid_for_type(&self.raw_iter, None, None, self.left_to_right) .unwrap_add_cost(&mut cost) { let kv = (