Feature: Add fallback reason to the explanation (#2647)

This commit is contained in:
Emmanuel Keller 2023-09-08 00:36:39 +01:00 committed by GitHub
parent 5626dccd21
commit fe78ca3c32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 175 additions and 71 deletions

View file

@ -1,3 +1,4 @@
use crate::ctx::Context;
use crate::dbs::Iterable; use crate::dbs::Iterable;
use crate::sql::{Explain, Object, Value}; use crate::sql::{Explain, Object, Value};
use std::collections::HashMap; use std::collections::HashMap;
@ -6,7 +7,11 @@ use std::collections::HashMap;
pub(super) struct Explanation(Vec<ExplainItem>); pub(super) struct Explanation(Vec<ExplainItem>);
impl Explanation { impl Explanation {
pub(super) fn new(e: Option<&Explain>, iterables: &Vec<Iterable>) -> (bool, Option<Self>) { pub(super) fn new(
ctx: &Context<'_>,
e: Option<&Explain>,
iterables: &Vec<Iterable>,
) -> (bool, Option<Self>) {
match e { match e {
None => (true, None), None => (true, None),
Some(e) => { Some(e) => {
@ -14,6 +19,11 @@ impl Explanation {
for i in iterables { for i in iterables {
exp.add_iter(i); exp.add_iter(i);
} }
if let Some(qp) = ctx.get_query_planner() {
for reason in qp.fallbacks() {
exp.add_fallback(reason.to_string());
}
}
(e.0, Some(exp)) (e.0, Some(exp))
} }
} }
@ -27,6 +37,10 @@ impl Explanation {
self.0.push(ExplainItem::new_fetch(count)); self.0.push(ExplainItem::new_fetch(count));
} }
fn add_fallback(&mut self, reason: String) {
self.0.push(ExplainItem::new_fallback(reason));
}
pub(super) fn output(self, results: &mut Vec<Value>) { pub(super) fn output(self, results: &mut Vec<Value>) {
for e in self.0 { for e in self.0 {
results.push(e.into()); results.push(e.into());
@ -47,6 +61,13 @@ impl ExplainItem {
} }
} }
fn new_fallback(reason: String) -> Self {
Self {
name: "Fallback".into(),
details: vec![("reason", reason.into())],
}
}
fn new_iter(iter: &Iterable) -> Self { fn new_iter(iter: &Iterable) -> Self {
match iter { match iter {
Iterable::Value(v) => Self { Iterable::Value(v) => Self {

View file

@ -278,7 +278,7 @@ impl Iterator {
// Process the query START clause // Process the query START clause
self.setup_start(&cancel_ctx, opt, txn, stm).await?; self.setup_start(&cancel_ctx, opt, txn, stm).await?;
// Extract the expected behaviour depending on the presence of EXPLAIN with or without FULL // Extract the expected behaviour depending on the presence of EXPLAIN with or without FULL
let (do_iterate, mut explanation) = Explanation::new(stm.explain(), &self.entries); let (do_iterate, mut explanation) = Explanation::new(ctx, stm.explain(), &self.entries);
if do_iterate { if do_iterate {
// Process prepared values // Process prepared values

View file

@ -57,7 +57,7 @@ impl<'a> Document<'a> {
Index::Search(p) => ic.index_full_text(&mut run, p).await?, Index::Search(p) => ic.index_full_text(&mut run, p).await?,
Index::MTree(_) => { Index::MTree(_) => {
return Err(Error::FeatureNotYetImplemented { return Err(Error::FeatureNotYetImplemented {
feature: "MTree indexing", feature: "MTree indexing".to_string(),
}) })
} }
}; };

View file

@ -606,7 +606,7 @@ pub enum Error {
/// The feature has not yet being implemented /// The feature has not yet being implemented
#[error("Feature not yet implemented: {feature}")] #[error("Feature not yet implemented: {feature}")]
FeatureNotYetImplemented { FeatureNotYetImplemented {
feature: &'static str, feature: String,
}, },
/// Duplicated match references are not allowed /// Duplicated match references are not allowed

View file

@ -140,13 +140,13 @@ pub mod distance {
pub fn hamming((_, _): (String, String)) -> Result<Value, Error> { pub fn hamming((_, _): (String, String)) -> Result<Value, Error> {
Err(Error::FeatureNotYetImplemented { Err(Error::FeatureNotYetImplemented {
feature: "string::distance::hamming() function", feature: "string::distance::hamming() function".to_string(),
}) })
} }
pub fn levenshtein((_, _): (String, String)) -> Result<Value, Error> { pub fn levenshtein((_, _): (String, String)) -> Result<Value, Error> {
Err(Error::FeatureNotYetImplemented { Err(Error::FeatureNotYetImplemented {
feature: "string::distance::levenshtein() function", feature: "string::distance::levenshtein() function".to_string(),
}) })
} }
} }
@ -234,7 +234,7 @@ pub mod similarity {
pub fn jaro((_, _): (String, String)) -> Result<Value, Error> { pub fn jaro((_, _): (String, String)) -> Result<Value, Error> {
Err(Error::FeatureNotYetImplemented { Err(Error::FeatureNotYetImplemented {
feature: "string::similarity::jaro() function", feature: "string::similarity::jaro() function".to_string(),
}) })
} }

View file

@ -66,7 +66,7 @@ pub mod distance {
pub fn mahalanobis((_, _): (Vec<Number>, Vec<Number>)) -> Result<Value, Error> { pub fn mahalanobis((_, _): (Vec<Number>, Vec<Number>)) -> Result<Value, Error> {
Err(Error::FeatureNotYetImplemented { Err(Error::FeatureNotYetImplemented {
feature: "vector::distance::mahalanobis() function", feature: "vector::distance::mahalanobis() function".to_string(),
}) })
} }
@ -99,7 +99,7 @@ pub mod similarity {
pub fn spearman((_, _): (Vec<Number>, Vec<Number>)) -> Result<Value, Error> { pub fn spearman((_, _): (Vec<Number>, Vec<Number>)) -> Result<Value, Error> {
Err(Error::FeatureNotYetImplemented { Err(Error::FeatureNotYetImplemented {
feature: "vector::similarity::spearman() function", feature: "vector::similarity::spearman() function".to_string(),
}) })
} }
} }

View file

@ -122,7 +122,7 @@ impl QueryExecutor {
.. ..
} => self.new_search_index_iterator(ir, io).await, } => self.new_search_index_iterator(ir, io).await,
_ => Err(Error::FeatureNotYetImplemented { _ => Err(Error::FeatureNotYetImplemented {
feature: "VectorSearch iterator", feature: "VectorSearch iterator".to_string(),
}), }),
} }
} }

View file

@ -20,6 +20,7 @@ pub(crate) struct QueryPlanner<'a> {
/// There is one executor per table /// There is one executor per table
executors: HashMap<String, QueryExecutor>, executors: HashMap<String, QueryExecutor>,
requires_distinct: bool, requires_distinct: bool,
fallbacks: Vec<String>,
} }
impl<'a> QueryPlanner<'a> { impl<'a> QueryPlanner<'a> {
@ -30,6 +31,7 @@ impl<'a> QueryPlanner<'a> {
cond, cond,
executors: HashMap::default(), executors: HashMap::default(),
requires_distinct: false, requires_distinct: false,
fallbacks: vec![],
} }
} }
@ -40,14 +42,14 @@ impl<'a> QueryPlanner<'a> {
t: Table, t: Table,
it: &mut Iterator, it: &mut Iterator,
) -> Result<(), Error> { ) -> Result<(), Error> {
let res = Tree::build(ctx, self.opt, txn, &t, self.cond).await?; match Tree::build(ctx, self.opt, txn, &t, self.cond).await? {
if let Some((node, im)) = res { Some((node, im)) => {
let mut exe = QueryExecutor::new(self.opt, txn, &t, im).await?; let mut exe = QueryExecutor::new(self.opt, txn, &t, im).await?;
let ok = match PlanBuilder::build(node, self.with)? { match PlanBuilder::build(node, self.with)? {
Plan::SingleIndex(exp, io) => { Plan::SingleIndex(exp, io) => {
let ir = exe.add_iterator(exp); let ir = exe.add_iterator(exp);
it.ingest(Iterable::Index(t.clone(), ir, io)); it.ingest(Iterable::Index(t.clone(), ir, io));
true self.executors.insert(t.0.clone(), exe);
} }
Plan::MultiIndex(v) => { Plan::MultiIndex(v) => {
for (exp, io) in v { for (exp, io) in v {
@ -55,16 +57,21 @@ impl<'a> QueryPlanner<'a> {
it.ingest(Iterable::Index(t.clone(), ir, io)); it.ingest(Iterable::Index(t.clone(), ir, io));
self.requires_distinct = true; self.requires_distinct = true;
} }
true
}
Plan::TableIterator => false,
};
self.executors.insert(t.0.clone(), exe); self.executors.insert(t.0.clone(), exe);
if ok {
return Ok(());
} }
Plan::TableIterator(fallback) => {
if let Some(fallback) = fallback {
self.fallbacks.push(fallback);
} }
self.executors.insert(t.0.clone(), exe);
it.ingest(Iterable::Table(t)); it.ingest(Iterable::Table(t));
}
}
}
None => {
it.ingest(Iterable::Table(t));
}
}
Ok(()) Ok(())
} }
@ -79,4 +86,8 @@ impl<'a> QueryPlanner<'a> {
pub(crate) fn requires_distinct(&self) -> bool { pub(crate) fn requires_distinct(&self) -> bool {
self.requires_distinct self.requires_distinct
} }
pub(crate) fn fallbacks(&self) -> &Vec<String> {
&self.fallbacks
}
} }

View file

@ -20,7 +20,7 @@ impl<'a> PlanBuilder<'a> {
pub(super) fn build(root: Node, with: &'a Option<With>) -> Result<Plan, Error> { pub(super) fn build(root: Node, with: &'a Option<With>) -> Result<Plan, Error> {
if let Some(with) = with { if let Some(with) = with {
if matches!(with, With::NoIndex) { if matches!(with, With::NoIndex) {
return Ok(Plan::TableIterator); return Ok(Plan::TableIterator(Some("WITH NOINDEX".to_string())));
} }
} }
let mut b = PlanBuilder { let mut b = PlanBuilder {
@ -30,12 +30,12 @@ impl<'a> PlanBuilder<'a> {
all_exp_with_index: true, all_exp_with_index: true,
}; };
// Browse the AST and collect information // Browse the AST and collect information
if !b.eval_node(root)? { if let Err(e) = b.eval_node(root) {
return Ok(Plan::TableIterator); return Ok(Plan::TableIterator(Some(e.to_string())));
} }
// If we didn't found any index, we're done with no index plan // If we didn't found any index, we're done with no index plan
if b.indexes.is_empty() { if b.indexes.is_empty() {
return Ok(Plan::TableIterator); return Ok(Plan::TableIterator(Some("NO INDEX FOUND".to_string())));
} }
// If every boolean operator are AND then we can use the single index plan // If every boolean operator are AND then we can use the single index plan
if b.all_and { if b.all_and {
@ -47,7 +47,7 @@ impl<'a> PlanBuilder<'a> {
if b.all_exp_with_index { if b.all_exp_with_index {
return Ok(Plan::MultiIndex(b.indexes)); return Ok(Plan::MultiIndex(b.indexes));
} }
Ok(Plan::TableIterator) Ok(Plan::TableIterator(None))
} }
// Check if we have an explicit list of index we can use // Check if we have an explicit list of index we can use
@ -62,7 +62,7 @@ impl<'a> PlanBuilder<'a> {
io io
} }
fn eval_node(&mut self, node: Node) -> Result<bool, Error> { fn eval_node(&mut self, node: Node) -> Result<(), String> {
match node { match node {
Node::Expression { Node::Expression {
io, io,
@ -79,10 +79,12 @@ impl<'a> PlanBuilder<'a> {
} else if self.all_exp_with_index && !is_bool { } else if self.all_exp_with_index && !is_bool {
self.all_exp_with_index = false; self.all_exp_with_index = false;
} }
self.eval_expression(*left, *right) self.eval_node(*left)?;
self.eval_node(*right)?;
Ok(())
} }
Node::Unsupported => Ok(false), Node::Unsupported(reason) => Err(reason),
_ => Ok(true), _ => Ok(()),
} }
} }
@ -99,23 +101,13 @@ impl<'a> PlanBuilder<'a> {
} }
} }
fn eval_expression(&mut self, left: Node, right: Node) -> Result<bool, Error> {
if !self.eval_node(left)? {
return Ok(false);
}
if !self.eval_node(right)? {
return Ok(false);
}
Ok(true)
}
fn add_index_option(&mut self, e: Expression, i: IndexOption) { fn add_index_option(&mut self, e: Expression, i: IndexOption) {
self.indexes.push((e, i)); self.indexes.push((e, i));
} }
} }
pub(super) enum Plan { pub(super) enum Plan {
TableIterator, TableIterator(Option<String>),
SingleIndex(Expression, IndexOption), SingleIndex(Expression, IndexOption),
MultiIndex(Vec<(Expression, IndexOption)>), MultiIndex(Vec<(Expression, IndexOption)>),
} }

View file

@ -71,20 +71,20 @@ impl<'a> TreeBuilder<'a> {
#[cfg_attr(not(target_arch = "wasm32"), async_recursion)] #[cfg_attr(not(target_arch = "wasm32"), async_recursion)]
#[cfg_attr(target_arch = "wasm32", async_recursion(?Send))] #[cfg_attr(target_arch = "wasm32", async_recursion(?Send))]
async fn eval_value(&mut self, v: &Value) -> Result<Node, Error> { async fn eval_value(&mut self, v: &Value) -> Result<Node, Error> {
Ok(match v { match v {
Value::Expression(e) => self.eval_expression(e).await?, Value::Expression(e) => self.eval_expression(e).await,
Value::Idiom(i) => self.eval_idiom(i).await?, Value::Idiom(i) => self.eval_idiom(i).await,
Value::Strand(_) => Node::Scalar(v.to_owned()), Value::Strand(_) => Ok(Node::Scalar(v.to_owned())),
Value::Number(_) => Node::Scalar(v.to_owned()), Value::Number(_) => Ok(Node::Scalar(v.to_owned())),
Value::Bool(_) => Node::Scalar(v.to_owned()), Value::Bool(_) => Ok(Node::Scalar(v.to_owned())),
Value::Thing(_) => Node::Scalar(v.to_owned()), Value::Thing(_) => Ok(Node::Scalar(v.to_owned())),
Value::Subquery(s) => self.eval_subquery(s).await?, Value::Subquery(s) => self.eval_subquery(s).await,
Value::Param(p) => { Value::Param(p) => {
let v = p.compute(self.ctx, self.opt, self.txn, None).await?; let v = p.compute(self.ctx, self.opt, self.txn, None).await?;
self.eval_value(&v).await? self.eval_value(&v).await
}
_ => Ok(Node::Unsupported(format!("Unsupported value: {}", v))),
} }
_ => Node::Unsupported,
})
} }
async fn eval_idiom(&mut self, i: &Idiom) -> Result<Node, Error> { async fn eval_idiom(&mut self, i: &Idiom) -> Result<Node, Error> {
@ -99,9 +99,7 @@ impl<'a> TreeBuilder<'a> {
match e { match e {
Expression::Unary { Expression::Unary {
.. ..
} => Err(Error::FeatureNotYetImplemented { } => Ok(Node::Unsupported("unary expressions not supported".to_string())),
feature: "unary expressions in index",
}),
Expression::Binary { Expression::Binary {
l, l,
o, o,
@ -173,10 +171,10 @@ impl<'a> TreeBuilder<'a> {
} }
async fn eval_subquery(&mut self, s: &Subquery) -> Result<Node, Error> { async fn eval_subquery(&mut self, s: &Subquery) -> Result<Node, Error> {
Ok(match s { match s {
Subquery::Value(v) => self.eval_value(v).await?, Subquery::Value(v) => self.eval_value(v).await,
_ => Node::Unsupported, _ => Ok(Node::Unsupported(format!("Unsupported subquery: {}", s))),
}) }
} }
} }
@ -201,7 +199,7 @@ pub(super) enum Node {
IndexedField(Idiom, DefineIndexStatement), IndexedField(Idiom, DefineIndexStatement),
NonIndexedField, NonIndexedField,
Scalar(Value), Scalar(Value),
Unsupported, Unsupported(String),
} }
impl Node { impl Node {

View file

@ -58,7 +58,7 @@ impl AnalyzeStatement {
} }
_ => { _ => {
return Err(Error::FeatureNotYetImplemented { return Err(Error::FeatureNotYetImplemented {
feature: "Statistics on unique and non-unique indexes.", feature: "Statistics on unique and non-unique indexes.".to_string(),
}) })
} }
}; };

View file

@ -110,10 +110,10 @@ async fn select_where_iterate_two_no_index() -> Result<(), Error> {
let mut res = execute_test(&two_multi_index_query("WITH NOINDEX", ""), 9).await?; let mut res = execute_test(&two_multi_index_query("WITH NOINDEX", ""), 9).await?;
// OR results // OR results
check_result(&mut res, "[{ name: 'Jaime' }, { name: 'Tobie' }]")?; check_result(&mut res, "[{ name: 'Jaime' }, { name: 'Tobie' }]")?;
check_result(&mut res, &table_explain(2))?; check_result(&mut res, &table_explain_no_index(2))?;
// AND results // AND results
check_result(&mut res, "[{name: 'Jaime'}]")?; check_result(&mut res, "[{name: 'Jaime'}]")?;
check_result(&mut res, &table_explain(1))?; check_result(&mut res, &table_explain_no_index(1))?;
Ok(()) Ok(())
} }
@ -185,6 +185,31 @@ fn table_explain(fetch_count: usize) -> String {
) )
} }
fn table_explain_no_index(fetch_count: usize) -> String {
format!(
"[
{{
detail: {{
table: 'person'
}},
operation: 'Iterate Table'
}},
{{
detail: {{
reason: 'WITH NOINDEX'
}},
operation: 'Fallback'
}},
{{
detail: {{
count: {fetch_count}
}},
operation: 'Fetch'
}}
]"
)
}
const THREE_TABLE_EXPLAIN: &str = "[ const THREE_TABLE_EXPLAIN: &str = "[
{ {
detail: { detail: {
@ -332,3 +357,60 @@ const TWO_MULTI_INDEX_EXPLAIN: &str = "[
operation: 'Fetch' operation: 'Fetch'
} }
]"; ]";
#[tokio::test]
async fn select_with_no_index_unary_operator() -> Result<(), Error> {
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");
let mut res = dbs
.execute("SELECT * FROM table WITH NOINDEX WHERE !param.subparam EXPLAIN", &ses, None)
.await?;
assert_eq!(res.len(), 1);
let tmp = res.remove(0).result?;
let val = Value::parse(
r#"[
{
detail: {
table: 'table'
},
operation: 'Iterate Table'
},
{
detail: {
reason: 'WITH NOINDEX'
},
operation: 'Fallback'
}
]"#,
);
assert_eq!(format!("{:#}", tmp), format!("{:#}", val));
Ok(())
}
#[tokio::test]
async fn select_unsupported_unary_operator() -> Result<(), Error> {
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");
let mut res =
dbs.execute("SELECT * FROM table WHERE !param.subparam EXPLAIN", &ses, None).await?;
assert_eq!(res.len(), 1);
let tmp = res.remove(0).result?;
let val = Value::parse(
r#"[
{
detail: {
table: 'table'
},
operation: 'Iterate Table'
},
{
detail: {
reason: 'unary expressions not supported'
},
operation: 'Fallback'
}
]"#,
);
assert_eq!(format!("{:#}", tmp), format!("{:#}", val));
Ok(())
}