From 829fb0baf9a1a63b8e612ce2ed7829a54859bb20 Mon Sep 17 00:00:00 2001 From: Emmanuel Keller Date: Sat, 30 Mar 2024 23:11:54 +0000 Subject: [PATCH] Bug fix: Improve support of range queries with complex queries (#3786) --- core/src/dbs/iterator.rs | 9 +- core/src/dbs/processor.rs | 18 +- core/src/idx/planner/executor.rs | 35 +-- core/src/idx/planner/mod.rs | 36 ++- core/src/idx/planner/plan.rs | 166 +++++++++---- core/src/idx/planner/tree.rs | 50 ++-- lib/tests/planner.rs | 393 +++++++++++++++++++++++++++++++ 7 files changed, 613 insertions(+), 94 deletions(-) diff --git a/core/src/dbs/iterator.rs b/core/src/dbs/iterator.rs index f4862574..ca1d3896 100644 --- a/core/src/dbs/iterator.rs +++ b/core/src/dbs/iterator.rs @@ -19,18 +19,19 @@ use crate::sql::thing::Thing; use crate::sql::value::Value; use async_recursion::async_recursion; use std::mem; +use std::sync::Arc; #[derive(Clone)] pub(crate) enum Iterable { Value(Value), - Table(Table), + Table(Arc), Thing(Thing), Range(Range), Edges(Edges), Defer(Thing), Mergeable(Thing, Value), Relatable(Thing, Thing, Thing), - Index(Table, IteratorRef), + Index(Arc
, IteratorRef), } pub(crate) struct Processed { @@ -117,7 +118,7 @@ impl Iterator { } _ => { // Ingest the table for scanning - self.ingest(Iterable::Table(v)) + self.ingest(Iterable::Table(Arc::new(v))) } }, // There is no data clause so create a record id @@ -128,7 +129,7 @@ impl Iterator { } _ => { // Ingest the table for scanning - self.ingest(Iterable::Table(v)) + self.ingest(Iterable::Table(Arc::new(v))) } }, }, diff --git a/core/src/dbs/processor.rs b/core/src/dbs/processor.rs index 7292e205..6d8dea7a 100644 --- a/core/src/dbs/processor.rs +++ b/core/src/dbs/processor.rs @@ -127,10 +127,10 @@ impl<'a> Processor<'a> { // Avoiding search in the hashmap of the query planner for each doc let mut ctx = Context::new(ctx); ctx.set_query_executor(exe.clone()); - return self.process_table(&ctx, opt, txn, stm, v).await; + return self.process_table(&ctx, opt, txn, stm, v.as_ref()).await; } } - self.process_table(ctx, opt, txn, stm, v).await? + self.process_table(ctx, opt, txn, stm, v.as_ref()).await? } Iterable::Range(v) => self.process_range(ctx, opt, txn, stm, v).await?, Iterable::Edges(e) => self.process_edge(ctx, opt, txn, stm, e).await?, @@ -141,10 +141,10 @@ impl<'a> Processor<'a> { // Avoiding search in the hashmap of the query planner for each doc let mut ctx = Context::new(ctx); ctx.set_query_executor(exe.clone()); - return self.process_index(&ctx, opt, txn, stm, t, ir).await; + return self.process_index(&ctx, opt, txn, stm, t.as_ref(), ir).await; } } - self.process_index(ctx, opt, txn, stm, t, ir).await? + self.process_index(ctx, opt, txn, stm, t.as_ref(), ir).await? } Iterable::Mergeable(v, o) => { self.process_mergeable(ctx, opt, txn, stm, v, o).await? @@ -302,13 +302,13 @@ impl<'a> Processor<'a> { opt: &Options, txn: &Transaction, stm: &Statement<'_>, - v: Table, + v: &Table, ) -> Result<(), Error> { // Check that the table exists - txn.lock().await.check_ns_db_tb(opt.ns(), opt.db(), &v, opt.strict).await?; + txn.lock().await.check_ns_db_tb(opt.ns(), opt.db(), v, opt.strict).await?; // Prepare the start and end keys - let beg = thing::prefix(opt.ns(), opt.db(), &v); - let end = thing::suffix(opt.ns(), opt.db(), &v); + let beg = thing::prefix(opt.ns(), opt.db(), v); + let end = thing::suffix(opt.ns(), opt.db(), v); // Loop until no more keys let mut next_page = Some(ScanPage::from(beg..end)); while let Some(page) = next_page { @@ -556,7 +556,7 @@ impl<'a> Processor<'a> { opt: &Options, txn: &Transaction, stm: &Statement<'_>, - table: Table, + table: &Table, ir: IteratorRef, ) -> Result<(), Error> { // Check that the table exists diff --git a/core/src/idx/planner/executor.rs b/core/src/idx/planner/executor.rs index 1bc980c0..4de9ec1b 100644 --- a/core/src/idx/planner/executor.rs +++ b/core/src/idx/planner/executor.rs @@ -266,20 +266,7 @@ impl QueryExecutor { ) -> Result, Error> { if let Some(it_entry) = self.0.it_entries.get(it_ref as usize) { match it_entry { - IteratorEntry::Single(_, io) => { - if let Some(ix) = self.0.index_definitions.get(io.ix_ref() as usize) { - match ix.index { - Index::Idx => Ok(Self::new_index_iterator(opt, ix, io.clone())), - Index::Uniq => Ok(Self::new_unique_index_iterator(opt, ix, io.clone())), - Index::Search { - .. - } => self.new_search_index_iterator(it_ref, io.clone()).await, - Index::MTree(_) => Ok(self.new_mtree_index_knn_iterator(it_ref)), - } - } else { - Ok(None) - } - } + IteratorEntry::Single(_, io) => self.new_single_iterator(opt, it_ref, io).await, IteratorEntry::Range(_, ir, from, to) => { Ok(self.new_range_iterator(opt, *ir, from, to)) } @@ -289,6 +276,26 @@ impl QueryExecutor { } } + async fn new_single_iterator( + &self, + opt: &Options, + it_ref: IteratorRef, + io: &IndexOption, + ) -> Result, Error> { + if let Some(ix) = self.0.index_definitions.get(io.ix_ref() as usize) { + match ix.index { + Index::Idx => Ok(Self::new_index_iterator(opt, ix, io.clone())), + Index::Uniq => Ok(Self::new_unique_index_iterator(opt, ix, io.clone())), + Index::Search { + .. + } => self.new_search_index_iterator(it_ref, io.clone()).await, + Index::MTree(_) => Ok(self.new_mtree_index_knn_iterator(it_ref)), + } + } else { + Ok(None) + } + } + fn new_index_iterator( opt: &Options, ix: &DefineIndexStatement, diff --git a/core/src/idx/planner/mod.rs b/core/src/idx/planner/mod.rs index d7800555..adbb1ae9 100644 --- a/core/src/idx/planner/mod.rs +++ b/core/src/idx/planner/mod.rs @@ -53,12 +53,20 @@ impl<'a> QueryPlanner<'a> { ) -> Result<(), Error> { let mut is_table_iterator = false; let mut is_knn = false; + let t = Arc::new(t); match Tree::build(ctx, self.opt, txn, &t, self.cond, self.with).await? { - Some((node, im, with_indexes, knn_expressions)) => { - is_knn = is_knn || !knn_expressions.is_empty(); - let mut exe = - InnerQueryExecutor::new(ctx, self.opt, txn, &t, im, knn_expressions).await?; - match PlanBuilder::build(node, self.with, with_indexes)? { + Some(tree) => { + is_knn = is_knn || !tree.knn_expressions.is_empty(); + let mut exe = InnerQueryExecutor::new( + ctx, + self.opt, + txn, + &t, + tree.index_map, + tree.knn_expressions, + ) + .await?; + match PlanBuilder::build(tree.root, self.with, tree.with_indexes)? { Plan::SingleIndex(exp, io) => { if io.require_distinct() { self.requires_distinct = true; @@ -66,15 +74,21 @@ impl<'a> QueryPlanner<'a> { let ir = exe.add_iterator(IteratorEntry::Single(exp, io)); self.add(t.clone(), Some(ir), exe, it); } - Plan::MultiIndex(v) => { - for (exp, io) in v { - let ir = exe.add_iterator(IteratorEntry::Single(exp, io)); + Plan::MultiIndex(non_range_indexes, ranges_indexes) => { + for (exp, io) in non_range_indexes { + let ie = IteratorEntry::Single(exp, io); + let ir = exe.add_iterator(ie); it.ingest(Iterable::Index(t.clone(), ir)); - self.requires_distinct = true; } + for (ixn, rq) in ranges_indexes { + let ie = IteratorEntry::Range(rq.exps, ixn, rq.from, rq.to); + let ir = exe.add_iterator(ie); + it.ingest(Iterable::Index(t.clone(), ir)); + } + self.requires_distinct = true; self.add(t.clone(), None, exe, it); } - Plan::SingleIndexMultiExpression(ixn, rq) => { + Plan::SingleIndexRange(ixn, rq) => { let ir = exe.add_iterator(IteratorEntry::Range(rq.exps, ixn, rq.from, rq.to)); self.add(t.clone(), Some(ir), exe, it); @@ -103,7 +117,7 @@ impl<'a> QueryPlanner<'a> { fn add( &mut self, - tb: Table, + tb: Arc
, irf: Option, exe: InnerQueryExecutor, it: &mut Iterator, diff --git a/core/src/idx/planner/plan.rs b/core/src/idx/planner/plan.rs index 98556076..c7ed4c8f 100644 --- a/core/src/idx/planner/plan.rs +++ b/core/src/idx/planner/plan.rs @@ -1,19 +1,29 @@ use crate::err::Error; use crate::idx::ft::MatchRef; -use crate::idx::planner::tree::{IndexRef, Node}; +use crate::idx::planner::tree::{GroupRef, IndexRef, Node}; use crate::sql::with::With; use crate::sql::{Array, Idiom, Object}; use crate::sql::{Expression, Operator, Value}; use std::collections::hash_map::Entry; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::hash::Hash; use std::sync::Arc; +/// The `PlanBuilder` struct represents a builder for constructing query plans. pub(super) struct PlanBuilder { - indexes: Vec<(Arc, IndexOption)>, - range_queries: HashMap, + /// Do we have at least one index? + has_indexes: bool, + /// List of expressions that are not ranges, backed by an index + non_range_indexes: Vec<(Arc, IndexOption)>, + /// List of indexes involved in this plan with_indexes: Vec, + /// Group each possible optimisations local to a SubQuery + groups: BTreeMap, // The order matters because we want the plan to be consistent across repeated queries. + /// Does a group contains only AND relations? + all_and_groups: HashMap, + /// Does the whole query contains only AND relations? all_and: bool, + /// Is every expression backed by an index? all_exp_with_index: bool, } @@ -27,9 +37,11 @@ impl PlanBuilder { return Ok(Plan::TableIterator(Some("WITH NOINDEX".to_string()))); } let mut b = PlanBuilder { - indexes: Default::default(), - range_queries: Default::default(), + has_indexes: false, + non_range_indexes: Default::default(), + groups: Default::default(), with_indexes, + all_and_groups: Default::default(), all_and: true, all_exp_with_index: true, }; @@ -37,8 +49,8 @@ impl PlanBuilder { if let Err(e) = b.eval_node(&root) { return Ok(Plan::TableIterator(Some(e.to_string()))); } - // If we didn't found any index, we're done with no index plan - if b.indexes.is_empty() { + // If we didn't find any index, we're done with no index plan + if !b.has_indexes { return Ok(Plan::TableIterator(Some("NO INDEX FOUND".to_string()))); } @@ -46,17 +58,27 @@ impl PlanBuilder { if b.all_and { // TODO: This is currently pretty arbitrary // We take the "first" range query if one is available - if let Some((ir, rq)) = b.range_queries.drain().take(1).next() { - return Ok(Plan::SingleIndexMultiExpression(ir, rq)); + if let Some((_, group)) = b.groups.into_iter().next() { + if let Some((ir, rq)) = group.take_first_range() { + return Ok(Plan::SingleIndexRange(ir, rq)); + } } // Otherwise we take the first single index option - if let Some((e, i)) = b.indexes.pop() { + if let Some((e, i)) = b.non_range_indexes.pop() { return Ok(Plan::SingleIndex(e, i)); } } // If every expression is backed by an index with can use the MultiIndex plan - if b.all_exp_with_index { - return Ok(Plan::MultiIndex(b.indexes)); + else if b.all_exp_with_index { + let mut ranges = Vec::with_capacity(b.groups.len()); + for (depth, group) in b.groups { + if b.all_and_groups.get(&depth) == Some(&true) { + group.take_union_ranges(&mut ranges); + } else { + group.take_intersect_ranges(&mut ranges); + } + } + return Ok(Plan::MultiIndex(b.non_range_indexes, ranges)); } Ok(Plan::TableIterator(None)) } @@ -74,17 +96,15 @@ impl PlanBuilder { fn eval_node(&mut self, node: &Node) -> Result<(), String> { match node { Node::Expression { + group, io, left, right, exp, } => { - if self.all_and && Operator::Or.eq(exp.operator()) { - self.all_and = false; - } - let is_bool = self.check_boolean_operator(exp.operator()); + let is_bool = self.check_boolean_operator(*group, exp.operator()); if let Some(io) = self.filter_index_option(io.as_ref()) { - self.add_index_option(exp.clone(), io); + self.add_index_option(*group, exp.clone(), io); } else if self.all_exp_with_index && !is_bool { self.all_exp_with_index = false; } @@ -97,41 +117,49 @@ impl PlanBuilder { } } - fn check_boolean_operator(&mut self, op: &Operator) -> bool { + fn check_boolean_operator(&mut self, gr: GroupRef, op: &Operator) -> bool { match op { Operator::Neg | Operator::Or => { if self.all_and { self.all_and = false; } + self.all_and_groups.entry(gr).and_modify(|b| *b = false).or_insert(false); true } - Operator::And => true, - _ => false, + Operator::And => { + self.all_and_groups.entry(gr).or_insert(true); + true + } + _ => { + self.all_and_groups.entry(gr).or_insert(true); + false + } } } - fn add_index_option(&mut self, exp: Arc, io: IndexOption) { - if let IndexOperator::RangePart(o, v) = io.op() { - match self.range_queries.entry(io.ix_ref()) { + fn add_index_option(&mut self, group_ref: GroupRef, exp: Arc, io: IndexOption) { + if let IndexOperator::RangePart(_, _) = io.op() { + let level = self.groups.entry(group_ref).or_default(); + match level.ranges.entry(io.ix_ref()) { Entry::Occupied(mut e) => { - e.get_mut().add(exp.clone(), o, v); + e.get_mut().push((exp, io)); } Entry::Vacant(e) => { - let mut b = RangeQueryBuilder::default(); - b.add(exp.clone(), o, v); - e.insert(b); + e.insert(vec![(exp, io)]); } } + } else { + self.non_range_indexes.push((exp, io)); } - self.indexes.push((exp, io)); + self.has_indexes = true; } } pub(super) enum Plan { TableIterator(Option), SingleIndex(Arc, IndexOption), - MultiIndex(Vec<(Arc, IndexOption)>), - SingleIndexMultiExpression(IndexRef, RangeQueryBuilder), + MultiIndex(Vec<(Arc, IndexOption)>, Vec<(IndexRef, UnionRangeQueryBuilder)>), + SingleIndexRange(IndexRef, UnionRangeQueryBuilder), } #[derive(Debug, Clone, Eq, PartialEq, Hash)] @@ -284,23 +312,79 @@ impl From<&RangeValue> for Value { } } +#[derive(Default)] +pub(super) struct Group { + ranges: HashMap, IndexOption)>>, +} + +impl Group { + fn take_first_range(self) -> Option<(IndexRef, UnionRangeQueryBuilder)> { + if let Some((ir, ri)) = self.ranges.into_iter().take(1).next() { + UnionRangeQueryBuilder::new_aggregate(ri).map(|rb| (ir, rb)) + } else { + None + } + } + + fn take_union_ranges(self, r: &mut Vec<(IndexRef, UnionRangeQueryBuilder)>) { + for (ir, ri) in self.ranges { + if let Some(rb) = UnionRangeQueryBuilder::new_aggregate(ri) { + r.push((ir, rb)); + } + } + } + + fn take_intersect_ranges(self, r: &mut Vec<(IndexRef, UnionRangeQueryBuilder)>) { + for (ir, ri) in self.ranges { + for (exp, io) in ri { + if let Some(rb) = UnionRangeQueryBuilder::new(exp, io) { + r.push((ir, rb)); + } + } + } + } +} + #[derive(Default, Debug)] -pub(super) struct RangeQueryBuilder { +pub(super) struct UnionRangeQueryBuilder { pub(super) exps: HashSet>, pub(super) from: RangeValue, pub(super) to: RangeValue, } -impl RangeQueryBuilder { - fn add(&mut self, exp: Arc, op: &Operator, v: &Value) { - match op { - Operator::LessThan => self.to.set_to(v), - Operator::LessThanOrEqual => self.to.set_to_inclusive(v), - Operator::MoreThan => self.from.set_from(v), - Operator::MoreThanOrEqual => self.from.set_from_inclusive(v), - _ => return, +impl UnionRangeQueryBuilder { + fn new_aggregate(exp_ios: Vec<(Arc, IndexOption)>) -> Option { + if exp_ios.is_empty() { + return None; } - self.exps.insert(exp); + let mut b = Self::default(); + for (exp, io) in exp_ios { + b.add(exp, io); + } + Some(b) + } + + fn new(exp: Arc, io: IndexOption) -> Option { + let mut b = Self::default(); + if b.add(exp, io) { + Some(b) + } else { + None + } + } + + fn add(&mut self, exp: Arc, io: IndexOption) -> bool { + if let IndexOperator::RangePart(op, val) = io.op() { + match op { + Operator::LessThan => self.to.set_to(val), + Operator::LessThanOrEqual => self.to.set_to_inclusive(val), + Operator::MoreThan => self.from.set_from(val), + Operator::MoreThanOrEqual => self.from.set_from_inclusive(val), + _ => return false, + } + self.exps.insert(exp); + } + true } } diff --git a/core/src/idx/planner/tree.rs b/core/src/idx/planner/tree.rs index 07956cef..79a58e37 100644 --- a/core/src/idx/planner/tree.rs +++ b/core/src/idx/planner/tree.rs @@ -12,7 +12,12 @@ use async_recursion::async_recursion; use std::collections::HashMap; use std::sync::Arc; -pub(super) struct Tree {} +pub(super) struct Tree { + pub(super) root: Node, + pub(super) index_map: IndexesMap, + pub(super) with_indexes: Vec, + pub(super) knn_expressions: KnnExpressions, +} impl Tree { /// Traverse all the conditions and extract every expression @@ -24,11 +29,16 @@ impl Tree { table: &'a Table, cond: &'a Option, with: &'a Option, - ) -> Result, KnnExpressions)>, Error> { + ) -> Result, Error> { let mut b = TreeBuilder::new(ctx, opt, txn, table, with); if let Some(cond) = cond { - let node = b.eval_value(&cond.0).await?; - Ok(Some((node, b.index_map, b.with_indexes, b.knn_expressions))) + let root = b.eval_value(0, &cond.0).await?; + Ok(Some(Self { + root, + index_map: b.index_map, + with_indexes: b.with_indexes, + knn_expressions: b.knn_expressions, + })) } else { Ok(None) } @@ -48,6 +58,7 @@ struct TreeBuilder<'a> { index_map: IndexesMap, with_indexes: Vec, knn_expressions: KnnExpressions, + group_sequence: GroupRef, } impl<'a> TreeBuilder<'a> { @@ -75,8 +86,10 @@ impl<'a> TreeBuilder<'a> { index_map: Default::default(), with_indexes, knn_expressions: Default::default(), + group_sequence: 0, } } + async fn lazy_cache_indexes(&mut self) -> Result<(), Error> { if self.indexes.is_none() { let indexes = self @@ -93,10 +106,10 @@ impl<'a> TreeBuilder<'a> { #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] #[cfg_attr(target_arch = "wasm32", async_recursion(?Send))] - async fn eval_value(&mut self, v: &Value) -> Result { + async fn eval_value(&mut self, group: GroupRef, v: &Value) -> Result { match v { - Value::Expression(e) => self.eval_expression(e).await, - Value::Idiom(i) => self.eval_idiom(i).await, + Value::Expression(e) => self.eval_expression(group, e).await, + Value::Idiom(i) => self.eval_idiom(group, i).await, Value::Strand(_) | Value::Number(_) | Value::Bool(_) @@ -110,7 +123,7 @@ impl<'a> TreeBuilder<'a> { Value::Subquery(s) => self.eval_subquery(s).await, Value::Param(p) => { let v = p.compute(self.ctx, self.opt, self.txn, None).await?; - self.eval_value(&v).await + self.eval_value(group, &v).await } _ => Ok(Node::Unsupported(format!("Unsupported value: {}", v))), } @@ -124,7 +137,7 @@ impl<'a> TreeBuilder<'a> { Ok(Node::Computed(Arc::new(Value::Array(Array::from(values))))) } - async fn eval_idiom(&mut self, i: &Idiom) -> Result { + async fn eval_idiom(&mut self, group: GroupRef, i: &Idiom) -> Result { // Check if the idiom has already been resolved if let Some(i) = self.resolved_idioms.get(i) { if let Some(Some(irs)) = self.idioms_indexes.get(i).cloned() { @@ -137,7 +150,7 @@ impl<'a> TreeBuilder<'a> { if let Some(Part::Start(x)) = i.0.first() { if x.is_param() { let v = i.compute(self.ctx, self.opt, self.txn, None).await?; - return self.eval_value(&v).await; + return self.eval_value(group, &v).await; } } @@ -179,7 +192,7 @@ impl<'a> TreeBuilder<'a> { res } - async fn eval_expression(&mut self, e: &Expression) -> Result { + async fn eval_expression(&mut self, group: GroupRef, e: &Expression) -> Result { match e { Expression::Unary { .. @@ -194,8 +207,8 @@ impl<'a> TreeBuilder<'a> { return Ok(re.into()); } let exp = Arc::new(e.clone()); - let left = Arc::new(self.eval_value(l).await?); - let right = Arc::new(self.eval_value(r).await?); + let left = Arc::new(self.eval_value(group, l).await?); + let right = Arc::new(self.eval_value(group, r).await?); let mut io = None; if let Some((id, irs)) = left.is_indexed_field() { io = self.lookup_index_option( @@ -221,6 +234,7 @@ impl<'a> TreeBuilder<'a> { self.eval_knn(id, &left, &exp)?; } let re = ResolvedExpression { + group, exp: exp.clone(), io: io.clone(), left: left.clone(), @@ -337,8 +351,9 @@ impl<'a> TreeBuilder<'a> { } async fn eval_subquery(&mut self, s: &Subquery) -> Result { + self.group_sequence += 1; match s { - Subquery::Value(v) => self.eval_value(v).await, + Subquery::Value(v) => self.eval_value(self.group_sequence, v).await, _ => Ok(Node::Unsupported(format!("Unsupported subquery: {}", s))), } } @@ -352,9 +367,12 @@ pub(super) struct IndexesMap { pub(super) definitions: Vec, } +pub(super) type GroupRef = u16; + #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub(super) enum Node { Expression { + group: GroupRef, io: Option, left: Arc, right: Arc, @@ -398,7 +416,7 @@ enum IdiomPosition { Right, } impl IdiomPosition { - // Reverses the operator for non commutative operators + // Reverses the operator for non-commutative operators fn transform(&self, op: &Operator) -> Operator { match self { IdiomPosition::Left => op.clone(), @@ -415,6 +433,7 @@ impl IdiomPosition { #[derive(Clone)] struct ResolvedExpression { + group: GroupRef, exp: Arc, io: Option, left: Arc, @@ -423,6 +442,7 @@ struct ResolvedExpression { impl From for Node { fn from(re: ResolvedExpression) -> Self { Node::Expression { + group: re.group, io: re.io, left: re.left, right: re.right, diff --git a/lib/tests/planner.rs b/lib/tests/planner.rs index f5a2b543..d47405eb 100644 --- a/lib/tests/planner.rs +++ b/lib/tests/planner.rs @@ -1528,3 +1528,396 @@ async fn select_with_in_operator_uniq_index() -> Result<(), Error> { assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); Ok(()) } + +#[tokio::test] +async fn select_with_in_operator_multiple_indexes() -> Result<(), Error> { + let dbs = new_ds().await?; + let ses = Session::owner().with_ns("test").with_db("test"); + + let sql = r#" + DEFINE INDEX index_note_id ON TABLE notes COLUMNS id; + DEFINE INDEX index_note_kind ON TABLE notes COLUMNS kind; + DEFINE INDEX index_note_pubkey ON TABLE notes COLUMNS pubkey; + DEFINE INDEX index_note_published ON TABLE notes COLUMNS published; + CREATE notes:1 SET kind = 1, pubkey = 123, published=2021; + CREATE notes:2 SET kind = 2, pubkey = 123, published=2022; + CREATE notes:3 SET kind = 1, pubkey = 123, published=2023; + CREATE notes:4 SET kind = 2, pubkey = 123, published=2024; + CREATE notes:5 SET kind = 1, pubkey = 123, published=2025; + SELECT * FROM notes WHERE (kind IN [1,2] OR pubkey IN [123]) AND published > 2022 EXPLAIN; + SELECT * FROM notes WHERE (kind IN [1,2] OR pubkey IN [123]) AND published > 2022; + SELECT * FROM notes WHERE published < 2024 AND (kind IN [1,2] OR pubkey IN [123]) AND published > 2022 EXPLAIN; + SELECT * FROM notes WHERE published < 2024 AND (kind IN [1,2] OR pubkey IN [123]) AND published > 2022; + SELECT * FROM notes WHERE published < 2022 OR (kind IN [1,2] OR pubkey IN [123]) AND published > 2022 EXPLAIN; + SELECT * FROM notes WHERE published < 2022 OR (kind IN [1,2] OR pubkey IN [123]) AND published > 2022; + SELECT * FROM notes WHERE (kind IN [1,2] AND published < 2022) OR (pubkey IN [123] AND published > 2022) EXPLAIN; + SELECT * FROM notes WHERE (kind IN [1,2] AND published < 2022) OR (pubkey IN [123] AND published > 2022); + "#; + let mut res = dbs.execute(sql, &ses, None).await?; + // + assert_eq!(res.len(), 17); + skip_ok(&mut res, 9)?; + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + "detail": { + "plan": { + "index": "index_note_kind", + "operator": "union", + "value": [ + 1, + 2 + ] + }, + "table": "notes" + }, + "operation": "Iterate Index" + }, + { + "detail": { + "plan": { + "index": "index_note_pubkey", + "operator": "union", + "value": [ + 123 + ] + }, + "table": "notes" + }, + "operation": "Iterate Index" + }, + { + "detail": { + plan: { + from: { + inclusive: false, + value: 2022 + }, + index: 'index_note_published', + to: { + inclusive: false, + value: None + } + }, + "table": "notes" + }, + "operation": "Iterate Index" + }, + { + detail: { + type: 'Memory' + }, + operation: 'Collector' + } + ]"#, + ); + assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + id: notes:3, + kind: 1, + pubkey: 123, + published: 2023 + }, + { + id: notes:5, + kind: 1, + pubkey: 123, + published: 2025 + }, + { + id: notes:4, + kind: 2, + pubkey: 123, + published: 2024 + } + ]"#, + ); + assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + "detail": { + "plan": { + "index": "index_note_kind", + "operator": "union", + "value": [ + 1, + 2 + ] + }, + "table": "notes" + }, + "operation": "Iterate Index" + }, + { + "detail": { + "plan": { + "index": "index_note_pubkey", + "operator": "union", + "value": [ + 123 + ] + }, + "table": "notes" + }, + "operation": "Iterate Index" + }, + { + detail: { + plan: { + from: { + inclusive: false, + value: 2022 + }, + index: 'index_note_published', + to: { + inclusive: false, + value: 2024 + } + }, + table: 'notes' + }, + operation: 'Iterate Index' + }, + { + detail: { + type: 'Memory' + }, + operation: 'Collector' + } + ]"#, + ); + assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + id: notes:3, + kind: 1, + pubkey: 123, + published: 2023 + } + ]"#, + ); + assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + "detail": { + "plan": { + "index": "index_note_kind", + "operator": "union", + "value": [ + 1, + 2 + ] + }, + "table": "notes" + }, + "operation": "Iterate Index" + }, + { + "detail": { + "plan": { + "index": "index_note_pubkey", + "operator": "union", + "value": [ + 123 + ] + }, + "table": "notes" + }, + "operation": "Iterate Index" + }, + { + detail: { + plan: { + from: { + inclusive: false, + value: None + }, + index: 'index_note_published', + to: { + inclusive: false, + value: 2022 + } + }, + table: 'notes' + }, + operation: 'Iterate Index' + }, + { + detail: { + plan: { + from: { + inclusive: false, + value: 2022 + }, + index: 'index_note_published', + to: { + inclusive: false, + value: None + } + }, + table: 'notes' + }, + operation: 'Iterate Index' + }, + { + detail: { + type: 'Memory' + }, + operation: 'Collector' + } + ]"#, + ); + assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + id: notes:1, + kind: 1, + pubkey: 123, + published: 2021 + }, + { + id: notes:3, + kind: 1, + pubkey: 123, + published: 2023 + }, + { + id: notes:5, + kind: 1, + pubkey: 123, + published: 2025 + }, + { + id: notes:4, + kind: 2, + pubkey: 123, + published: 2024 + } + ]"#, + ); + assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + "detail": { + "plan": { + "index": "index_note_kind", + "operator": "union", + "value": [ + 1, + 2 + ] + }, + "table": "notes" + }, + "operation": "Iterate Index" + }, + { + "detail": { + "plan": { + "index": "index_note_pubkey", + "operator": "union", + "value": [ + 123 + ] + }, + "table": "notes" + }, + "operation": "Iterate Index" + }, + { + detail: { + plan: { + from: { + inclusive: false, + value: None + }, + index: 'index_note_published', + to: { + inclusive: false, + value: 2022 + } + }, + table: 'notes' + }, + operation: 'Iterate Index' + }, + { + detail: { + plan: { + from: { + inclusive: false, + value: 2022 + }, + index: 'index_note_published', + to: { + inclusive: false, + value: None + } + }, + table: 'notes' + }, + operation: 'Iterate Index' + }, + { + detail: { + type: 'Memory' + }, + operation: 'Collector' + } + ]"#, + ); + assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + r#"[ + { + id: notes:1, + kind: 1, + pubkey: 123, + published: 2021 + }, + { + id: notes:3, + kind: 1, + pubkey: 123, + published: 2023 + }, + { + id: notes:5, + kind: 1, + pubkey: 123, + published: 2025 + }, + { + id: notes:4, + kind: 2, + pubkey: 123, + published: 2024 + } + ]"#, + ); + assert_eq!(format!("{:#}", tmp), format!("{:#}", val)); + Ok(()) +}