diff --git a/core/src/dbs/group.rs b/core/src/dbs/group.rs new file mode 100644 index 00000000..bfa02839 --- /dev/null +++ b/core/src/dbs/group.rs @@ -0,0 +1,367 @@ +use crate::ctx::Context; +use crate::dbs::plan::Explanation; +use crate::dbs::store::StoreCollector; +use crate::dbs::{Options, Statement, Transaction}; +use crate::err::Error; +use crate::sql::function::OptimisedAggregate; +use crate::sql::value::{TryAdd, TryDiv, Value}; +use crate::sql::{Array, Field, Idiom}; +use std::borrow::Cow; +use std::collections::{BTreeMap, HashMap}; +use std::mem; + +pub(super) struct GroupsCollector { + base: Vec, + idioms: Vec, + grp: BTreeMap>, +} + +#[derive(Default)] +struct Aggregator { + array: Option, + first_val: Option, + count: Option, + math_max: Option, + math_min: Option, + math_sum: Option, + math_mean: Option<(Value, usize)>, + time_max: Option, + time_min: Option, +} + +impl GroupsCollector { + pub(super) fn new(stm: &Statement<'_>) -> Self { + let mut idioms_agr: HashMap = HashMap::new(); + if let Some(fields) = stm.expr() { + for field in fields.other() { + if let Field::Single { + expr, + alias, + } = field + { + let idiom = alias.as_ref().cloned().unwrap_or_else(|| expr.to_idiom()); + idioms_agr.entry(idiom).or_default().prepare(expr); + } + } + } + let mut base = Vec::with_capacity(idioms_agr.len()); + let mut idioms = Vec::with_capacity(idioms_agr.len()); + for (idiom, agr) in idioms_agr { + base.push(agr); + idioms.push(idiom); + } + Self { + base, + idioms, + grp: Default::default(), + } + } + + pub(super) async fn push( + &mut self, + ctx: &Context<'_>, + opt: &Options, + txn: &Transaction, + stm: &Statement<'_>, + obj: Value, + ) -> Result<(), Error> { + if let Some(groups) = stm.group() { + // Create a new column set + let mut arr = Array::with_capacity(groups.len()); + // Loop over each group clause + for group in groups.iter() { + // Get the value at the path + let val = obj.pick(group); + // Set the value at the path + arr.push(val); + } + // Add to grouped collection + let agr = self + .grp + .entry(arr) + .or_insert_with(|| self.base.iter().map(|a| a.new_instance()).collect()); + Self::pushes(ctx, opt, txn, agr, &self.idioms, obj).await? + } + Ok(()) + } + + async fn pushes( + ctx: &Context<'_>, + opt: &Options, + txn: &Transaction, + agrs: &mut [Aggregator], + idioms: &[Idiom], + obj: Value, + ) -> Result<(), Error> { + for (agr, idiom) in agrs.iter_mut().zip(idioms) { + let val = obj.get(ctx, opt, txn, None, idiom).await?; + agr.push(val)?; + } + Ok(()) + } + + pub(super) fn len(&self) -> usize { + self.grp.len() + } + + pub(super) async fn output( + &mut self, + ctx: &Context<'_>, + opt: &Options, + txn: &Transaction, + stm: &Statement<'_>, + ) -> Result { + let mut results = StoreCollector::default(); + if let Some(fields) = stm.expr() { + let grp = mem::take(&mut self.grp); + // Loop over each grouped collection + for (_, mut aggregator) in grp { + // Create a new value + let mut obj = Value::base(); + // Loop over each group clause + for field in fields.other() { + // Process the field + if let Field::Single { + expr, + alias, + } = field + { + let idiom = alias + .as_ref() + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(expr.to_idiom())); + if let Some(idioms_pos) = + self.idioms.iter().position(|i| i.eq(idiom.as_ref())) + { + let agr = &mut aggregator[idioms_pos]; + match expr { + Value::Function(f) if f.is_aggregate() => { + let a = f.get_optimised_aggregate(); + let x = if matches!(a, OptimisedAggregate::None) { + // The aggregation is not optimised, let's compute it with the values + let vals = agr.take(); + let x = vals + .all() + .get(ctx, opt, txn, None, idiom.as_ref()) + .await?; + f.aggregate(x).compute(ctx, opt, txn, None).await? + } else { + // The aggregation is optimised, just get the value + agr.compute(a)? + }; + obj.set(ctx, opt, txn, idiom.as_ref(), x).await?; + } + _ => { + let x = agr.take().first(); + obj.set(ctx, opt, txn, idiom.as_ref(), x).await?; + } + } + } + } + } + // Add the object to the results + results.push(obj); + } + } + Ok(results) + } + + pub(super) fn explain(&self, exp: &mut Explanation) { + let mut explain = BTreeMap::new(); + let idioms: Vec = + self.idioms.iter().cloned().map(|i| Value::from(i).to_string()).collect(); + for (i, a) in idioms.into_iter().zip(&self.base) { + explain.insert(i, a.explain()); + } + exp.add_collector("Group", vec![("idioms", explain.into())]); + } +} + +impl Aggregator { + fn prepare(&mut self, expr: &Value) { + let a = match expr { + Value::Function(f) => f.get_optimised_aggregate(), + _ => { + // We set it only if we don't already have an array + if self.array.is_none() && self.first_val.is_none() { + self.first_val = Some(Value::None); + return; + } + OptimisedAggregate::None + } + }; + match a { + OptimisedAggregate::None => { + if self.array.is_none() { + self.array = Some(Array::new()); + // We don't need both the array and the first val + self.first_val = None; + } + } + OptimisedAggregate::Count => { + if self.count.is_none() { + self.count = Some(0); + } + } + OptimisedAggregate::MathMax => { + if self.math_max.is_none() { + self.math_max = Some(Value::None); + } + } + OptimisedAggregate::MathMin => { + if self.math_min.is_none() { + self.math_min = Some(Value::None); + } + } + OptimisedAggregate::MathSum => { + if self.math_sum.is_none() { + self.math_sum = Some(0.into()); + } + } + OptimisedAggregate::MathMean => { + if self.math_mean.is_none() { + self.math_mean = Some((0.into(), 0)); + } + } + OptimisedAggregate::TimeMax => { + if self.time_max.is_none() { + self.time_max = Some(Value::None); + } + } + OptimisedAggregate::TimeMin => { + if self.time_min.is_none() { + self.time_min = Some(Value::None); + } + } + } + } + + fn new_instance(&self) -> Self { + Self { + array: self.array.as_ref().map(|_| Array::new()), + first_val: self.first_val.as_ref().map(|_| Value::None), + count: self.count.as_ref().map(|_| 0), + math_max: self.math_max.as_ref().map(|_| Value::None), + math_min: self.math_min.as_ref().map(|_| Value::None), + math_sum: self.math_sum.as_ref().map(|_| 0.into()), + math_mean: self.math_mean.as_ref().map(|_| (0.into(), 0)), + time_max: self.time_max.as_ref().map(|_| Value::None), + time_min: self.time_min.as_ref().map(|_| Value::None), + } + } + + fn push(&mut self, val: Value) -> Result<(), Error> { + if let Some(ref mut c) = self.count { + *c += 1; + } + if val.is_number() { + if let Some(s) = self.math_sum.take() { + self.math_sum = Some(s.try_add(val.clone())?); + } + if let Some((s, i)) = self.math_mean.take() { + let s = s.try_add(val.clone())?; + self.math_mean = Some((s, i + 1)); + } + if let Some(m) = self.math_min.take() { + self.math_min = Some(if m.is_none() { + val.clone() + } else { + m.min(val.clone()) + }); + } + if let Some(m) = self.math_max.take() { + self.math_max = Some(if m.is_none() { + val.clone() + } else { + m.max(val.clone()) + }); + } + } + if val.is_datetime() { + if let Some(m) = self.time_min.take() { + self.time_min = Some(if m.is_none() { + val.clone() + } else { + m.min(val.clone()) + }); + } + if let Some(m) = self.time_max.take() { + self.time_max = Some(if m.is_none() { + val.clone() + } else { + m.max(val.clone()) + }); + } + } + if let Some(ref mut a) = self.array { + a.0.push(val); + } else if let Some(ref mut v) = self.first_val { + if v.is_none() { + *v = val; + } + } + Ok(()) + } + + fn compute(&mut self, a: OptimisedAggregate) -> Result { + Ok(match a { + OptimisedAggregate::None => Value::None, + OptimisedAggregate::Count => self.count.take().map(|v| v.into()).unwrap_or(Value::None), + OptimisedAggregate::MathMax => self.math_max.take().unwrap_or(Value::None), + OptimisedAggregate::MathMin => self.math_min.take().unwrap_or(Value::None), + OptimisedAggregate::MathSum => self.math_sum.take().unwrap_or(Value::None), + OptimisedAggregate::MathMean => { + if let Some((v, i)) = self.math_mean.take() { + v.try_div(i.into())? + } else { + Value::None + } + } + OptimisedAggregate::TimeMax => self.time_max.take().unwrap_or(Value::None), + OptimisedAggregate::TimeMin => self.time_min.take().unwrap_or(Value::None), + }) + } + + fn take(&mut self) -> Value { + // We return a clone because the same value may be returned for different groups + if let Some(v) = self.first_val.as_ref().cloned() { + Array::from(v).into() + } else if let Some(a) = self.array.as_ref().cloned() { + a.into() + } else { + Value::None + } + } + + fn explain(&self) -> Value { + let mut collections: Vec = vec![]; + if self.array.is_some() { + collections.push("array".into()); + } + if self.first_val.is_some() { + collections.push("first".into()); + } + if self.count.is_some() { + collections.push("count".into()); + } + if self.math_mean.is_some() { + collections.push("math::mean".into()); + } + if self.math_max.is_some() { + collections.push("math::max".into()); + } + if self.math_min.is_some() { + collections.push("math::min".into()); + } + if self.math_sum.is_some() { + collections.push("math::sun".into()); + } + if self.time_max.is_some() { + collections.push("time::max".into()); + } + if self.time_min.is_some() { + collections.push("time::min".into()); + } + collections.into() + } +} diff --git a/core/src/dbs/iterator.rs b/core/src/dbs/iterator.rs index 9bdf851f..6a5ac839 100644 --- a/core/src/dbs/iterator.rs +++ b/core/src/dbs/iterator.rs @@ -3,7 +3,8 @@ use crate::ctx::Context; #[cfg(not(target_arch = "wasm32"))] use crate::dbs::distinct::AsyncDistinct; use crate::dbs::distinct::SyncDistinct; -use crate::dbs::explanation::Explanation; +use crate::dbs::plan::Plan; +use crate::dbs::result::Results; use crate::dbs::Statement; use crate::dbs::{Options, Transaction}; use crate::doc::Document; @@ -11,17 +12,13 @@ use crate::err::Error; use crate::idx::docids::DocId; use crate::idx::planner::executor::IteratorRef; use crate::idx::planner::IterationStage; -use crate::sql::array::Array; use crate::sql::edges::Edges; -use crate::sql::field::Field; use crate::sql::range::Range; use crate::sql::table::Table; use crate::sql::thing::Thing; use crate::sql::value::Value; use async_recursion::async_recursion; -use std::borrow::Cow; use std::cmp::Ordering; -use std::collections::BTreeMap; use std::mem; #[derive(Clone)] @@ -67,8 +64,7 @@ pub(crate) struct Iterator { // Iterator runtime error error: Option, // Iterator output results - // TODO: Should be stored on disk / (mmap?) - results: Vec, + results: Results, // Iterator input values entries: Vec, } @@ -80,7 +76,7 @@ impl Clone for Iterator { limit: self.limit, start: self.start, error: None, - results: vec![], + results: Results::default(), entries: self.entries.clone(), } } @@ -299,10 +295,11 @@ impl Iterator { self.setup_limit(&cancel_ctx, opt, txn, stm).await?; // Process the query START clause self.setup_start(&cancel_ctx, opt, txn, stm).await?; + // Prepare the results with possible optimisations on groups + self.results = self.results.prepare(stm); // Extract the expected behaviour depending on the presence of EXPLAIN with or without FULL - let (do_iterate, mut explanation) = Explanation::new(ctx, stm.explain(), &self.entries); - - if do_iterate { + let mut plan = Plan::new(ctx, stm, &self.entries, &self.results); + if plan.do_iterate { // Process prepared values if let Some(qp) = ctx.get_query_planner() { while let Some(s) = qp.next_iteration_stage().await { @@ -321,30 +318,33 @@ impl Iterator { // Process any SPLIT clause self.output_split(ctx, opt, txn, stm).await?; // Process any GROUP clause - self.output_group(ctx, opt, txn, stm).await?; + self.results = self.results.group(ctx, opt, txn, stm).await?; // Process any ORDER clause self.output_order(ctx, opt, txn, stm).await?; - // Process any START clause - self.output_start(ctx, opt, txn, stm).await?; - // Process any LIMIT clause - self.output_limit(ctx, opt, txn, stm).await?; + // Process any START & LIMIT clause + self.results.start_limit(self.start.as_ref(), self.limit.as_ref()); - if let Some(e) = &mut explanation { + if let Some(e) = &mut plan.explanation { e.add_fetch(self.results.len()); - self.results.clear(); } else { // Process any FETCH clause self.output_fetch(ctx, opt, txn, stm).await?; } } + // Extract the output from the result + let mut results = self.results.take(); + // Output the explanation if any - if let Some(e) = explanation { - e.output(&mut self.results); + if let Some(e) = plan.explanation { + results.clear(); + for v in e.output() { + results.push(v) + } } // Output the results - Ok(mem::take(&mut self.results).into()) + Ok(results.into()) } #[inline] @@ -387,7 +387,7 @@ impl Iterator { // Loop over each split clause for split in splits.iter() { // Get the query result - let res = mem::take(&mut self.results); + let res = self.results.take(); // Loop over each value for obj in &res { // Get the value at the path @@ -401,7 +401,7 @@ impl Iterator { // Set the value at the path obj.set(ctx, opt, txn, split, val).await?; // Add the object to the results - self.results.push(obj); + self.results.push(ctx, opt, txn, stm, obj).await?; } } _ => { @@ -410,7 +410,7 @@ impl Iterator { // Set the value at the path obj.set(ctx, opt, txn, split, val).await?; // Add the object to the results - self.results.push(obj); + self.results.push(ctx, opt, txn, stm, obj).await?; } } } @@ -418,87 +418,6 @@ impl Iterator { } Ok(()) } - - #[inline] - async fn output_group( - &mut self, - ctx: &Context<'_>, - opt: &Options, - txn: &Transaction, - stm: &Statement<'_>, - ) -> Result<(), Error> { - if let Some(fields) = stm.expr() { - if let Some(groups) = stm.group() { - // Create the new grouped collection - let mut grp: BTreeMap = BTreeMap::new(); - // Get the query result - let res = mem::take(&mut self.results); - // Loop over each value - for obj in res { - // Create a new column set - let mut arr = Array::with_capacity(groups.len()); - // Loop over each group clause - for group in groups.iter() { - // Get the value at the path - let val = obj.pick(group); - // Set the value at the path - arr.push(val); - } - // Add to grouped collection - match grp.get_mut(&arr) { - Some(v) => v.push(obj), - None => { - grp.insert(arr, Array::from(obj)); - } - } - } - // Loop over each grouped collection - for (_, vals) in grp { - // Create a new value - let mut obj = Value::base(); - // Save the collected values - let vals = Value::from(vals); - // Loop over each group clause - for field in fields.other() { - // Process the field - if let Field::Single { - expr, - alias, - } = field - { - let idiom = alias - .as_ref() - .map(Cow::Borrowed) - .unwrap_or_else(|| Cow::Owned(expr.to_idiom())); - match expr { - Value::Function(f) if f.is_aggregate() => { - let x = - vals.all().get(ctx, opt, txn, None, idiom.as_ref()).await?; - let x = f.aggregate(x).compute(ctx, opt, txn, None).await?; - obj.set(ctx, opt, txn, idiom.as_ref(), x).await?; - } - _ => { - let x = vals.first(); - let x = if let Some(alias) = alias { - let cur = (&x).into(); - alias.compute(ctx, opt, txn, Some(&cur)).await? - } else { - let cur = (&x).into(); - expr.compute(ctx, opt, txn, Some(&cur)).await? - }; - obj.set(ctx, opt, txn, idiom.as_ref(), x).await?; - } - } - } - } - // Add the object to the results - self.results.push(obj); - } - } - } - Ok(()) - } - #[inline] async fn output_order( &mut self, @@ -538,34 +457,6 @@ impl Iterator { Ok(()) } - #[inline] - async fn output_start( - &mut self, - _ctx: &Context<'_>, - _opt: &Options, - _txn: &Transaction, - _stm: &Statement<'_>, - ) -> Result<(), Error> { - if let Some(v) = self.start { - self.results = mem::take(&mut self.results).into_iter().skip(v).collect(); - } - Ok(()) - } - - #[inline] - async fn output_limit( - &mut self, - _ctx: &Context<'_>, - _opt: &Options, - _txn: &Transaction, - _stm: &Statement<'_>, - ) -> Result<(), Error> { - if let Some(v) = self.limit { - self.results = mem::take(&mut self.results).into_iter().take(v).collect(); - } - Ok(()) - } - #[inline] async fn output_fetch( &mut self, @@ -672,7 +563,7 @@ impl Iterator { let aproc = async { // Process all processed values while let Ok(r) = vals.recv().await { - self.result(r, stm); + self.result(ctx, opt, txn, stm, r).await; } // Shutdown the executor let _ = end.send(()).await; @@ -701,11 +592,18 @@ impl Iterator { // Process the document let res = Document::process(ctx, opt, txn, stm, pro).await; // Process the result - self.result(res, stm); + self.result(ctx, opt, txn, stm, res).await; } /// Accept a processed record result - fn result(&mut self, res: Result, stm: &Statement<'_>) { + async fn result( + &mut self, + ctx: &Context<'_>, + opt: &Options, + txn: &Transaction, + stm: &Statement<'_>, + res: Result, + ) { // Process the result match res { Err(Error::Ignore) => { @@ -716,7 +614,13 @@ impl Iterator { self.run.cancel(); return; } - Ok(v) => self.results.push(v), + Ok(v) => { + if let Err(e) = self.results.push(ctx, opt, txn, stm, v).await { + self.error = Some(e); + self.run.cancel(); + return; + } + } } // Check if we can exit if stm.group().is_none() && stm.order().is_none() { diff --git a/core/src/dbs/mod.rs b/core/src/dbs/mod.rs index 8a2134d0..afab0a9e 100644 --- a/core/src/dbs/mod.rs +++ b/core/src/dbs/mod.rs @@ -4,10 +4,10 @@ //! and executors to process the operations. This module also gives a `context` to the transaction. mod distinct; mod executor; -mod explanation; mod iterator; mod notification; mod options; +mod plan; mod response; mod session; mod statement; @@ -29,6 +29,9 @@ pub mod capabilities; pub use self::capabilities::Capabilities; pub mod node; +mod group; mod processor; +mod result; +mod store; #[cfg(test)] pub(crate) mod test; diff --git a/core/src/dbs/explanation.rs b/core/src/dbs/plan.rs similarity index 75% rename from core/src/dbs/explanation.rs rename to core/src/dbs/plan.rs index 6dbb65da..99a3f70a 100644 --- a/core/src/dbs/explanation.rs +++ b/core/src/dbs/plan.rs @@ -1,21 +1,25 @@ use crate::ctx::Context; -use crate::dbs::Iterable; -use crate::sql::{Explain, Object, Value}; +use crate::dbs::result::Results; +use crate::dbs::{Iterable, Statement}; +use crate::sql::{Object, Value}; use std::collections::HashMap; -#[derive(Default)] -pub(super) struct Explanation(Vec); +pub(super) struct Plan { + pub(super) do_iterate: bool, + pub(super) explanation: Option, +} -impl Explanation { +impl Plan { pub(super) fn new( ctx: &Context<'_>, - e: Option<&Explain>, + stm: &Statement<'_>, iterables: &Vec, - ) -> (bool, Option) { - match e { + results: &Results, + ) -> Self { + let (do_iterate, explanation) = match stm.explain() { None => (true, None), Some(e) => { - let mut exp = Self::default(); + let mut exp = Explanation::default(); for i in iterables { exp.add_iter(ctx, i); } @@ -24,11 +28,21 @@ impl Explanation { exp.add_fallback(reason.to_string()); } } + results.explain(&mut exp); (e.0, Some(exp)) } + }; + Self { + do_iterate, + explanation, } } +} +#[derive(Default)] +pub(super) struct Explanation(Vec); + +impl Explanation { fn add_iter(&mut self, ctx: &Context<'_>, iter: &Iterable) { self.0.push(ExplainItem::new_iter(ctx, iter)); } @@ -37,14 +51,19 @@ impl Explanation { self.0.push(ExplainItem::new_fetch(count)); } + pub(super) fn add_collector( + &mut self, + collector_type: &str, + details: Vec<(&'static str, Value)>, + ) { + self.0.push(ExplainItem::new_collector(collector_type, details)); + } fn add_fallback(&mut self, reason: String) { self.0.push(ExplainItem::new_fallback(reason)); } - pub(super) fn output(self, results: &mut Vec) { - for e in self.0 { - results.push(e.into()); - } + pub(super) fn output(self) -> Vec { + self.0.into_iter().map(|e| e.into()).collect() } } @@ -83,7 +102,7 @@ impl ExplainItem { details: vec![("thing", Value::Thing(t.to_owned()))], }, Iterable::Defer(t) => Self { - name: "Iterate Thing".into(), + name: "Iterate Defer".into(), details: vec![("thing", Value::Thing(t.to_owned()))], }, Iterable::Range(r) => Self { @@ -120,6 +139,17 @@ impl ExplainItem { } } } + + pub(super) fn new_collector( + collector_type: &str, + mut details: Vec<(&'static str, Value)>, + ) -> ExplainItem { + details.insert(0, ("type", collector_type.into())); + Self { + name: "Collector".into(), + details, + } + } } impl From for Value { diff --git a/core/src/dbs/result.rs b/core/src/dbs/result.rs new file mode 100644 index 00000000..632cd91e --- /dev/null +++ b/core/src/dbs/result.rs @@ -0,0 +1,125 @@ +use crate::ctx::Context; +use crate::dbs::group::GroupsCollector; +use crate::dbs::plan::Explanation; +use crate::dbs::store::StoreCollector; +use crate::dbs::{Options, Statement, Transaction}; +use crate::err::Error; +use crate::sql::Value; +use std::cmp::Ordering; +use std::slice::IterMut; + +pub(super) enum Results { + None, + Store(StoreCollector), + Groups(GroupsCollector), +} + +impl Default for Results { + fn default() -> Self { + Self::None + } +} + +impl Results { + pub(super) fn prepare(&mut self, stm: &Statement<'_>) -> Self { + if stm.expr().is_some() && stm.group().is_some() { + Self::Groups(GroupsCollector::new(stm)) + } else { + Self::Store(StoreCollector::default()) + } + } + pub(super) async fn push( + &mut self, + ctx: &Context<'_>, + opt: &Options, + txn: &Transaction, + stm: &Statement<'_>, + val: Value, + ) -> Result<(), Error> { + match self { + Results::None => {} + Results::Store(s) => { + s.push(val); + } + Results::Groups(g) => { + g.push(ctx, opt, txn, stm, val).await?; + } + } + Ok(()) + } + + pub(super) fn sort_by(&mut self, compare: F) + where + F: FnMut(&Value, &Value) -> Ordering, + { + if let Results::Store(s) = self { + s.sort_by(compare) + } + } + + pub(super) fn start_limit(&mut self, start: Option<&usize>, limit: Option<&usize>) { + if let Results::Store(s) = self { + if let Some(&start) = start { + s.start(start); + } + if let Some(&limit) = limit { + s.limit(limit); + } + } + } + + pub(super) fn len(&self) -> usize { + match self { + Results::None => 0, + Results::Store(s) => s.len(), + Results::Groups(g) => g.len(), + } + } + + pub(super) async fn group( + &mut self, + ctx: &Context<'_>, + opt: &Options, + txn: &Transaction, + stm: &Statement<'_>, + ) -> Result { + Ok(match self { + Self::None => Self::None, + Self::Store(s) => Self::Store(s.take_store()), + Self::Groups(g) => Self::Store(g.output(ctx, opt, txn, stm).await?), + }) + } + + pub(super) fn take(&mut self) -> Vec { + if let Self::Store(s) = self { + s.take_vec() + } else { + vec![] + } + } + + pub(super) fn explain(&self, exp: &mut Explanation) { + match self { + Results::None => exp.add_collector("None", vec![]), + Results::Store(s) => { + s.explain(exp); + } + Results::Groups(g) => { + g.explain(exp); + } + } + } +} + +impl<'a> IntoIterator for &'a mut Results { + type Item = &'a mut Value; + type IntoIter = IterMut<'a, Value>; + + fn into_iter(self) -> Self::IntoIter { + if let Results::Store(s) = self { + s.into_iter() + } else { + [].iter_mut() + } + } +} diff --git a/core/src/dbs/store.rs b/core/src/dbs/store.rs new file mode 100644 index 00000000..8a0495cc --- /dev/null +++ b/core/src/dbs/store.rs @@ -0,0 +1,53 @@ +use crate::dbs::plan::Explanation; +use crate::sql::value::Value; +use std::cmp::Ordering; +use std::mem; + +#[derive(Default)] +// TODO Use surreal-kv once the number of record reach a given threshold +pub(super) struct StoreCollector(Vec); + +impl StoreCollector { + pub(super) fn push(&mut self, val: Value) { + self.0.push(val); + } + + // When surreal-kv will be used, the key will be used to sort the records in surreal-kv + pub(super) fn sort_by(&mut self, compare: F) + where + F: FnMut(&Value, &Value) -> Ordering, + { + self.0.sort_by(compare); + } + + pub(super) fn len(&self) -> usize { + self.0.len() + } + + pub(super) fn start(&mut self, start: usize) { + self.0 = mem::take(&mut self.0).into_iter().skip(start).collect(); + } + pub(super) fn limit(&mut self, limit: usize) { + self.0 = mem::take(&mut self.0).into_iter().take(limit).collect(); + } + + pub(super) fn take_vec(&mut self) -> Vec { + mem::take(&mut self.0) + } + pub(super) fn take_store(&mut self) -> Self { + Self(self.take_vec()) + } + + pub(super) fn explain(&self, exp: &mut Explanation) { + exp.add_collector("Store", vec![]); + } +} + +impl<'a> IntoIterator for &'a mut StoreCollector { + type Item = &'a mut Value; + type IntoIter = std::slice::IterMut<'a, Value>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut() + } +} diff --git a/core/src/sql/v1/function.rs b/core/src/sql/v1/function.rs index e2380ee5..4f2a0803 100644 --- a/core/src/sql/v1/function.rs +++ b/core/src/sql/v1/function.rs @@ -31,6 +31,17 @@ pub enum Function { // Add new variants here } +pub(crate) enum OptimisedAggregate { + None, + Count, + MathMax, + MathMin, + MathSum, + MathMean, + TimeMax, + TimeMin, +} + impl PartialOrd for Function { #[inline] fn partial_cmp(&self, _: &Self) -> Option { @@ -142,6 +153,18 @@ impl Function { _ => false, } } + pub(crate) fn get_optimised_aggregate(&self) -> OptimisedAggregate { + match self { + Self::Normal(f, _) if f == "count" => OptimisedAggregate::Count, + Self::Normal(f, _) if f == "math::max" => OptimisedAggregate::MathMax, + Self::Normal(f, _) if f == "math::mean" => OptimisedAggregate::MathMean, + Self::Normal(f, _) if f == "math::min" => OptimisedAggregate::MathMin, + Self::Normal(f, _) if f == "math::sum" => OptimisedAggregate::MathSum, + Self::Normal(f, _) if f == "time::max" => OptimisedAggregate::TimeMax, + Self::Normal(f, _) if f == "time::min" => OptimisedAggregate::TimeMin, + _ => OptimisedAggregate::None, + } + } } impl Function { diff --git a/core/src/sql/v2/function.rs b/core/src/sql/v2/function.rs index e2380ee5..4f2a0803 100644 --- a/core/src/sql/v2/function.rs +++ b/core/src/sql/v2/function.rs @@ -31,6 +31,17 @@ pub enum Function { // Add new variants here } +pub(crate) enum OptimisedAggregate { + None, + Count, + MathMax, + MathMin, + MathSum, + MathMean, + TimeMax, + TimeMin, +} + impl PartialOrd for Function { #[inline] fn partial_cmp(&self, _: &Self) -> Option { @@ -142,6 +153,18 @@ impl Function { _ => false, } } + pub(crate) fn get_optimised_aggregate(&self) -> OptimisedAggregate { + match self { + Self::Normal(f, _) if f == "count" => OptimisedAggregate::Count, + Self::Normal(f, _) if f == "math::max" => OptimisedAggregate::MathMax, + Self::Normal(f, _) if f == "math::mean" => OptimisedAggregate::MathMean, + Self::Normal(f, _) if f == "math::min" => OptimisedAggregate::MathMin, + Self::Normal(f, _) if f == "math::sum" => OptimisedAggregate::MathSum, + Self::Normal(f, _) if f == "time::max" => OptimisedAggregate::TimeMax, + Self::Normal(f, _) if f == "time::min" => OptimisedAggregate::TimeMin, + _ => OptimisedAggregate::None, + } + } } impl Function { diff --git a/lib/tests/group.rs b/lib/tests/group.rs index df6ee8c4..5d1fb3a9 100644 --- a/lib/tests/group.rs +++ b/lib/tests/group.rs @@ -7,7 +7,7 @@ use surrealdb::err::Error; use surrealdb::sql::Value; #[tokio::test] -async fn select_limit_fetch() -> Result<(), Error> { +async fn select_aggregate() -> Result<(), Error> { let sql = " CREATE temperature:1 SET country = 'GBP', time = d'2020-01-01T08:00:00Z'; CREATE temperature:2 SET country = 'GBP', time = d'2020-02-01T08:00:00Z'; @@ -19,12 +19,13 @@ async fn select_limit_fetch() -> Result<(), Error> { CREATE temperature:8 SET country = 'AUD', time = d'2021-01-01T08:00:00Z'; CREATE temperature:9 SET country = 'CHF', time = d'2023-01-01T08:00:00Z'; SELECT *, time::year(time) AS year FROM temperature; - SELECT count(), time::year(time) AS year, country FROM temperature GROUP BY country, year; + SELECT count(), time::min(time) as min, time::max(time) as max, time::year(time) AS year, country FROM temperature GROUP BY country, year; + SELECT count(), time::min(time) as min, time::max(time) as max, time::year(time) AS year, country FROM temperature GROUP BY country, year EXPLAIN; "; let dbs = new_ds().await?; let ses = Session::owner().with_ns("test").with_db("test"); let res = &mut dbs.execute(sql, &ses, None).await?; - assert_eq!(res.len(), 11); + assert_eq!(res.len(), 12); // let tmp = res.remove(0).result?; let val = Value::parse( @@ -198,40 +199,88 @@ async fn select_limit_fetch() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "[ - { - count: 1, - country: 'AUD', - year: 2021 - }, - { - count: 1, - country: 'CHF', - year: 2023 - }, - { - count: 1, - country: 'EUR', - year: 2021 - }, - { - count: 3, - country: 'GBP', - year: 2020 - }, - { - count: 2, - country: 'GBP', - year: 2021 - }, - { - count: 1, - country: 'USD', - year: 2021 - } - ]", + { + count: 1, + country: 'AUD', + max: d'2021-01-01T08:00:00Z', + min: d'2021-01-01T08:00:00Z', + year: 2021 + }, + { + count: 1, + country: 'CHF', + max: d'2023-01-01T08:00:00Z', + min: d'2023-01-01T08:00:00Z', + year: 2023 + }, + { + count: 1, + country: 'EUR', + max: d'2021-01-01T08:00:00Z', + min: d'2021-01-01T08:00:00Z', + year: 2021 + }, + { + count: 3, + country: 'GBP', + max: d'2020-03-01T08:00:00Z', + min: d'2020-01-01T08:00:00Z', + year: 2020 + }, + { + count: 2, + country: 'GBP', + max: d'2021-01-01T08:00:00Z', + min: d'2021-01-01T08:00:00Z', + year: 2021 + }, + { + count: 1, + country: 'USD', + max: d'2021-01-01T08:00:00Z', + min: d'2021-01-01T08:00:00Z', + year: 2021 + } + ]", ); assert_eq!(tmp, val); // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + detail: { + table: 'temperature' + }, + operation: 'Iterate Table' + }, + { + detail: { + idioms: { + count: [ + 'count' + ], + country: [ + 'first' + ], + max: [ + 'time::max' + ], + min: [ + 'time::min' + ], + year: [ + 'array' + ] + }, + type: 'Group' + }, + operation: 'Collector' + } + ]", + ); + assert_eq!(format!("{tmp:#}"), format!("{val:#}")); + // Ok(()) } @@ -242,13 +291,14 @@ async fn select_multi_aggregate() -> Result<(), Error> { CREATE test:2 SET group = 1, one = 4.7, two = 3.9; CREATE test:3 SET group = 2, one = 3.2, two = 9.7; CREATE test:4 SET group = 2, one = 4.4, two = 3.0; - SELECT group, math::sum(one) AS one, math::sum(two) AS two FROM test GROUP BY group; - SELECT group, math::sum(two) AS two, math::sum(one) AS one FROM test GROUP BY group; + SELECT group, math::sum(one) AS one, math::sum(two) AS two, math::min(one) as min FROM test GROUP BY group; + SELECT group, math::sum(two) AS two, math::sum(one) AS one, math::max(two) as max, math::mean(one) as mean FROM test GROUP BY group; + SELECT group, math::sum(two) AS two, math::sum(one) AS one, math::max(two) as max, math::mean(one) as mean FROM test GROUP BY group EXPLAIN; "; let dbs = new_ds().await?; let ses = Session::owner().with_ns("test").with_db("test"); let res = &mut dbs.execute(sql, &ses, None).await?; - assert_eq!(res.len(), 6); + assert_eq!(res.len(), 7); // let tmp = res.remove(0).result?; let val = Value::parse( @@ -305,37 +355,78 @@ async fn select_multi_aggregate() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "[ - { - group: 1, - one: 6.4, - two: 6.3, - }, - { - group: 2, - one: 7.6000000000000005, - two: 12.7, - } - ]", + { + group: 1, + min: 1.7, + one: 6.4, + two: 6.3 + }, + { + group: 2, + min: 3.2f, + one: 7.6000000000000005, + two: 12.7 + } + ]", ); assert_eq!(tmp, val); // let tmp = res.remove(0).result?; let val = Value::parse( "[ - { - group: 1, - one: 6.4, - two: 6.3, - }, - { - group: 2, - one: 7.6000000000000005, - two: 12.7, - } - ]", + { + group: 1, + max: 3.9, + mean: 3.2, + one: 6.4, + two: 6.3 + }, + { + group: 2, + max: 9.7, + mean: 3.8000000000000003, + one: 7.6000000000000005, + two: 12.7 + } + ]", ); assert_eq!(tmp, val); // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + detail: { + table: 'test' + }, + operation: 'Iterate Table' + }, + { + detail: { + idioms: { + group: [ + 'first' + ], + max: [ + 'math::max' + ], + mean: [ + 'math::mean' + ], + one: [ + 'math::sun' + ], + two: [ + 'math::sun' + ] + }, + type: 'Group' + }, + operation: 'Collector' + } + ]", + ); + assert_eq!(format!("{tmp:#}"), format!("{val:#}")); Ok(()) } @@ -349,11 +440,12 @@ async fn select_multi_aggregate_composed() -> Result<(), Error> { SELECT group, math::sum(math::floor(one)) AS one, math::sum(math::floor(two)) AS two FROM test GROUP BY group; SELECT group, math::sum(math::round(one)) AS one, math::sum(math::round(two)) AS two FROM test GROUP BY group; SELECT group, math::sum(math::ceil(one)) AS one, math::sum(math::ceil(two)) AS two FROM test GROUP BY group; + SELECT group, math::sum(math::ceil(one)) AS one, math::sum(math::ceil(two)) AS two FROM test GROUP BY group EXPLAIN; "; let dbs = new_ds().await?; let ses = Session::owner().with_ns("test").with_db("test"); let res = &mut dbs.execute(sql, &ses, None).await?; - assert_eq!(res.len(), 7); + assert_eq!(res.len(), 8); // let tmp = res.remove(0).result?; let val = Value::parse( @@ -458,5 +550,35 @@ async fn select_multi_aggregate_composed() -> Result<(), Error> { ); assert_eq!(tmp, val); // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + detail: { + table: 'test' + }, + operation: 'Iterate Table' + }, + { + detail: { + idioms: { + group: [ + 'first' + ], + one: [ + 'math::sun' + ], + two: [ + 'math::sun' + ] + }, + type: 'Group' + }, + operation: 'Collector' + } + ]", + ); + assert_eq!(format!("{tmp:#}"), format!("{val:#}")); + // Ok(()) } diff --git a/lib/tests/matches.rs b/lib/tests/matches.rs index f1d4b5b7..5f461c02 100644 --- a/lib/tests/matches.rs +++ b/lib/tests/matches.rs @@ -36,6 +36,12 @@ async fn select_where_matches_using_index() -> Result<(), Error> { table: 'blog', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]", ); @@ -81,6 +87,12 @@ async fn select_where_matches_without_using_index_iterator() -> Result<(), Error }, operation: 'Iterate Table' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 1, @@ -140,6 +152,12 @@ async fn select_where_matches_using_index_and_arrays(parallel: bool) -> Result<( table: 'blog', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]", ); @@ -209,6 +227,12 @@ async fn select_where_matches_using_index_and_objects(parallel: bool) -> Result< table: 'blog', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]", ); @@ -438,6 +462,12 @@ async fn select_where_matches_without_complex_query() -> Result<(), Error> { table: 'page' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]", ); diff --git a/lib/tests/planner.rs b/lib/tests/planner.rs index 20c44742..36207853 100644 --- a/lib/tests/planner.rs +++ b/lib/tests/planner.rs @@ -208,6 +208,12 @@ fn table_explain(fetch_count: usize) -> String { }}, operation: 'Iterate Table' }}, + {{ + detail: {{ + type: 'Store' + }}, + operation: 'Collector' + }}, {{ detail: {{ count: {fetch_count} @@ -233,6 +239,12 @@ fn table_explain_no_index(fetch_count: usize) -> String { }}, operation: 'Fallback' }}, + {{ + detail: {{ + type: 'Store' + }}, + operation: 'Collector' + }}, {{ detail: {{ count: {fetch_count} @@ -250,6 +262,12 @@ const THREE_TABLE_EXPLAIN: &str = "[ }, operation: 'Iterate Table' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 3 @@ -292,6 +310,12 @@ const THREE_MULTI_INDEX_EXPLAIN: &str = "[ }, operation: 'Iterate Index' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 3 @@ -312,6 +336,12 @@ const SINGLE_INDEX_FT_EXPLAIN: &str = "[ }, operation: 'Iterate Index' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 1 @@ -332,6 +362,12 @@ const SINGLE_INDEX_UNIQ_EXPLAIN: &str = "[ }, operation: 'Iterate Index' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 1 @@ -352,6 +388,12 @@ const SINGLE_INDEX_IDX_EXPLAIN: &str = "[ }, operation: 'Iterate Index' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 1 @@ -383,6 +425,12 @@ const TWO_MULTI_INDEX_EXPLAIN: &str = "[ }, operation: 'Iterate Index' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 2 @@ -413,6 +461,12 @@ async fn select_with_no_index_unary_operator() -> Result<(), Error> { reason: 'WITH NOINDEX' }, operation: 'Fallback' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"#, ); @@ -441,6 +495,12 @@ async fn select_unsupported_unary_operator() -> Result<(), Error> { reason: 'unary expressions not supported' }, operation: 'Fallback' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"#, ); @@ -525,6 +585,12 @@ const EXPLAIN_FROM_TO: &str = r"[ table: 'test' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; @@ -566,6 +632,12 @@ const EXPLAIN_FROM_INCL_TO: &str = r"[ table: 'test' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; @@ -611,6 +683,12 @@ const EXPLAIN_FROM_TO_INCL: &str = r"[ table: 'test' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; @@ -656,6 +734,12 @@ const EXPLAIN_FROM_INCL_TO_INCL: &str = r"[ table: 'test' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; @@ -744,6 +828,12 @@ const EXPLAIN_LESS: &str = r"[ table: 'test' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; @@ -779,6 +869,12 @@ const EXPLAIN_LESS_OR_EQUAL: &str = r"[ table: 'test' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; @@ -818,6 +914,12 @@ const EXPLAIN_MORE: &str = r"[ table: 'test' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; @@ -853,6 +955,12 @@ const EXPLAIN_MORE_OR_EQUAL: &str = r"[ table: 'test' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; @@ -903,6 +1011,12 @@ async fn select_with_idiom_param_value() -> Result<(), Error> { table: 'person' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"#, ); @@ -945,6 +1059,12 @@ const CONTAINS_TABLE_EXPLAIN: &str = r"[ reason: 'NO INDEX FOUND' }, operation: 'Fallback' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; @@ -996,9 +1116,6 @@ async fn select_contains() -> Result<(), Error> { const INDEX_EXPLAIN: &str = r"[ { - detail: { - table: 'student' - }, detail: { plan: { index: 'subject_idx', @@ -1008,6 +1125,12 @@ async fn select_contains() -> Result<(), Error> { table: 'student', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; const RESULT: &str = r"[ @@ -1036,9 +1159,6 @@ async fn select_contains_all() -> Result<(), Error> { "#; const INDEX_EXPLAIN: &str = r"[ { - detail: { - table: 'student' - }, detail: { plan: { index: 'subject_idx', @@ -1048,6 +1168,12 @@ async fn select_contains_all() -> Result<(), Error> { table: 'student', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; const RESULT: &str = r"[ @@ -1076,9 +1202,6 @@ async fn select_contains_any() -> Result<(), Error> { "#; const INDEX_EXPLAIN: &str = r"[ { - detail: { - table: 'student' - }, detail: { plan: { index: 'subject_idx', @@ -1088,6 +1211,12 @@ async fn select_contains_any() -> Result<(), Error> { table: 'student', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; const RESULT: &str = r"[ @@ -1123,9 +1252,6 @@ async fn select_unique_contains() -> Result<(), Error> { const INDEX_EXPLAIN: &str = r"[ { - detail: { - table: 'student' - }, detail: { plan: { index: 'subject_idx', @@ -1135,6 +1261,12 @@ async fn select_unique_contains() -> Result<(), Error> { table: 'student', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"; const RESULT: &str = r"[ @@ -1182,6 +1314,12 @@ async fn select_with_datetime_value() -> Result<(), Error> { table: 'test_user' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"#, ); @@ -1239,6 +1377,12 @@ async fn select_with_uuid_value() -> Result<(), Error> { table: 'sessions' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"#, ); @@ -1294,6 +1438,12 @@ async fn select_with_in_operator() -> Result<(), Error> { table: 'user' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"#, ); @@ -1366,6 +1516,12 @@ async fn select_with_in_operator_uniq_index() -> Result<(), Error> { table: 'apprenants' }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]"#, ); diff --git a/lib/tests/select.rs b/lib/tests/select.rs index 6683c305..622dd8da 100644 --- a/lib/tests/select.rs +++ b/lib/tests/select.rs @@ -218,6 +218,12 @@ async fn select_expression_value() -> Result<(), Error> { }, operation: 'Iterate Table' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 2, @@ -521,6 +527,12 @@ async fn select_where_field_is_thing_and_with_index() -> Result<(), Error> { table: 'post', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]", ); @@ -540,6 +552,12 @@ async fn select_where_field_is_thing_and_with_index() -> Result<(), Error> { }, operation: 'Iterate Index' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 2, @@ -597,6 +615,12 @@ async fn select_where_and_with_index() -> Result<(), Error> { table: 'person', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]", ); @@ -644,6 +668,12 @@ async fn select_where_and_with_unique_index() -> Result<(), Error> { table: 'person', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]", ); @@ -693,6 +723,12 @@ async fn select_where_and_with_fulltext_index() -> Result<(), Error> { table: 'person', }, operation: 'Iterate Index' + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' } ]", ); @@ -741,7 +777,13 @@ async fn select_where_explain() -> Result<(), Error> { table: 'software', }, operation: 'Iterate Table' - } + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, ]", ); assert_eq!(tmp, val); @@ -761,6 +803,12 @@ async fn select_where_explain() -> Result<(), Error> { }, operation: 'Iterate Table' }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, { detail: { count: 3, diff --git a/lib/tests/vector.rs b/lib/tests/vector.rs index 18299189..058cbb1a 100644 --- a/lib/tests/vector.rs +++ b/lib/tests/vector.rs @@ -52,7 +52,13 @@ async fn select_where_mtree_knn() -> Result<(), Error> { table: 'pts', }, operation: 'Iterate Index' - } + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, ]", ); assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); @@ -199,7 +205,13 @@ async fn select_where_brut_force_knn() -> Result<(), Error> { reason: 'NO INDEX FOUND' }, operation: 'Fallback' - } + }, + { + detail: { + type: 'Store' + }, + operation: 'Collector' + }, ]", ); assert_eq!(format!("{:#}", tmp), format!("{:#}", val));