mod pipeline;
mod projection;
use std::sync::Arc;
pub(crate) use pipeline::{
FilterAction, PlannedSource, SelectPipelineConfig, TopKPushdownRequest, WhereClauseState,
compute_topk_pushdown_request, filter_action_for_predicate,
};
use super::Planner;
use super::util::{
SELECT_ITERATION_PARAMS, all_value_sources, derive_field_name, extract_bruteforce_knn,
extract_count_field_names, extract_matches_context, extract_record_id_point_lookup,
extract_version, fold_condition_expressions, has_knn_k_operator, has_knn_ktree_operator,
has_knn_operator, has_top_level_or, idiom_to_field_name, index_covers_ordering,
is_bounded_topk_downstream, is_count_all_eligible, is_indexed_count_eligible,
order_is_scan_compatible, resolve_condition_params, resolve_param_value,
resolve_projection_field_idioms, strip_fts_condition, strip_index_conditions,
strip_knn_from_condition, strip_union_index_conditions,
};
use crate::catalog::Index;
use crate::catalog::providers::{DatabaseProvider, NamespaceProvider, TableProvider};
use crate::err::Error;
use crate::exec::index::access_path::{AccessPath, BTreeAccess, IndexRef, select_access_path};
use crate::exec::index::analysis::IndexAnalyzer;
use crate::exec::operators::scan::determine_scan_direction;
use crate::exec::operators::scan::resolved::{ResolvedTableContext, resolve_table_context};
use crate::exec::operators::{
AnalyzePlan, DynamicScan, ExplainPlan, Fetch, FetchStep, Filter, KnnTopK, Limit, RecordIdScan,
SortDirection, SourceExpr, TableScan, Timeout, Union, UnionIndexScan, UnwrapExactlyOne,
VersionScope,
};
use crate::exec::pre_decode_filter::pre_decode_filter_status_at_plan_time;
use crate::exec::{ExecOperator, OperatorMetrics};
use crate::expr::field::{Field, Fields};
use crate::expr::order::Ordering as OrderClause;
use crate::expr::with::With;
use crate::expr::{Cond, Expr, Idiom, Literal};
use crate::idx::planner::ScanDirection;
use crate::kvs::Transaction;
use crate::kvs::index::filter_online_indexes;
use crate::val::TableName;
impl<'ctx> Planner<'ctx> {
async fn resolve_param(&self, name: &str) -> Option<crate::val::Value> {
let ns_db = self.ns_db_ids().await;
resolve_param_value(name, self.ctx, ns_db, SELECT_ITERATION_PARAMS).await
}
async fn ns_db_ids(&self) -> Option<(crate::catalog::NamespaceId, crate::catalog::DatabaseId)> {
self.ns_db_ids_cache
.get_or_init(|| async {
let (txn, ns, db) = match (&self.txn, &self.ns, &self.db) {
(Some(txn), Some(ns), Some(db)) => (txn, ns, db),
_ => return None,
};
match txn.get_db_by_name(ns, db, None).await {
Ok(Some(db_def)) => Some((db_def.namespace_id, db_def.database_id)),
Ok(None) => {
tracing::debug!(
ns = %ns,
db = %db,
"plan-time db lookup returned None; falling back to runtime resolution",
);
None
}
Err(e) => {
tracing::warn!(
ns = %ns,
db = %db,
error = %e,
"plan-time db lookup failed; falling back to runtime resolution \
(plan-time optimisations may be unavailable)",
);
None
}
}
})
.await
.as_ref()
.copied()
}
async fn try_resolve_expr_value(&self, expr: &Expr) -> Option<crate::val::Value> {
use crate::expr::function::Function;
use crate::val::Value;
match expr {
Expr::Param(param) => self.resolve_param(param.as_str()).await,
Expr::Literal(lit) => super::util::try_literal_to_value(lit),
Expr::Table(name) => Some(Value::Table(name.clone())),
Expr::FunctionCall(fc) => {
let Function::Normal(ref name) = fc.receiver else {
return None;
};
self.ctx.check_allowed_function(name).ok()?;
let mut args = Vec::with_capacity(fc.arguments.len());
for arg in &fc.arguments {
args.push(Box::pin(self.try_resolve_expr_value(arg)).await?);
}
crate::fnc::synchronous(self.ctx, None, name, args).ok()
}
_ => None,
}
}
async fn resolve_source_exprs(&self, what: &mut [Expr]) {
for expr in what.iter_mut() {
match expr {
Expr::Table(_) | Expr::Literal(_) | Expr::Select(_) => continue,
_ => {}
}
if let Some(value) = self.try_resolve_expr_value(expr).await {
match value {
crate::val::Value::Table(t) => *expr = Expr::Table(t),
crate::val::Value::RecordId(rid) => {
*expr = crate::val::Value::RecordId(rid).into_literal();
}
_ => *expr = value.into_literal(),
}
}
}
}
pub(crate) async fn plan_fetch(
&self,
fetch: Option<crate::expr::fetch::Fetchs>,
input: Arc<dyn ExecOperator>,
) -> Result<Arc<dyn ExecOperator>, Error> {
let Some(fetchs) = fetch else {
return Ok(input);
};
let mut fields = Vec::with_capacity(fetchs.len());
for fetch_item in fetchs {
let mut idioms = self.resolve_field_idioms(fetch_item.0).await?;
fields.append(&mut idioms);
}
let (fields_sql, physical_fields) = self.convert_fetch_idioms(fields).await?;
Ok(Arc::new(Fetch {
input,
fields_sql,
physical_fields,
metrics: Arc::new(OperatorMetrics::new()),
}) as Arc<dyn ExecOperator>)
}
pub(crate) async fn convert_fetch_idioms(
&self,
idioms: Vec<Idiom>,
) -> Result<(String, Vec<Vec<FetchStep>>), Error> {
use surrealdb_types::ToSql;
let fields_sql = idioms.iter().map(|f| f.to_sql()).collect::<Vec<_>>().join(", ");
let mut physical_fields = Vec::with_capacity(idioms.len());
for idiom in idioms {
let mut steps = Vec::with_capacity(idiom.0.len());
for part in idiom.0 {
let step = match part {
crate::expr::part::Part::Value(expr) => {
FetchStep::Index(self.physical_expr(expr).await?)
}
other => FetchStep::Static(other),
};
steps.push(step);
}
physical_fields.push(steps);
}
Ok((fields_sql, physical_fields))
}
pub(crate) async fn resolve_field_idioms(
&self,
expr: Expr,
) -> Result<Vec<crate::expr::idiom::Idiom>, Error> {
use crate::expr::Function;
match expr {
Expr::Idiom(idiom) => Ok(vec![idiom]),
Expr::Param(ref param) => {
let value =
self.resolve_param(param.as_str()).await.unwrap_or(crate::val::Value::None);
let s = value.clone().coerce_to::<String>().map_err(|_| Error::InvalidFetch {
value: value.into_literal(),
})?;
let idiom: Idiom = crate::syn::idiom(&s)
.map_err(|_| Error::InvalidFetch {
value: expr,
})?
.into();
Ok(vec![idiom])
}
Expr::FunctionCall(ref call) => match &call.receiver {
Function::Normal(name) if self.function_registry().is_projection(name) => {
let mut idioms = Vec::new();
for arg in &call.arguments {
match self.resolve_expr_to_string(arg).await {
Ok(s) => {
let idiom: Idiom = crate::syn::idiom(&s)
.map_err(|e| Error::Query {
message: format!(
"Failed to parse field path '{}': {}",
s, e
),
})?
.into();
idioms.push(idiom);
}
Err(_) => {
let strings = self
.resolve_expr_to_string_array(arg)
.await
.map_err(|_| Error::Query {
message: format!(
"Projection function '{}' argument could not \
be resolved to a field path",
name
),
})?;
for s in strings {
let idiom: Idiom = crate::syn::idiom(&s)
.map_err(|e| Error::Query {
message: format!(
"Failed to parse field path '{}': {}",
s, e
),
})?
.into();
idioms.push(idiom);
}
}
}
}
if idioms.is_empty() {
return Err(Error::Query {
message: format!(
"Projection function '{}' requires at least one argument",
name
),
});
}
Ok(idioms)
}
_ => Err(Error::InvalidFetch {
value: expr,
}),
},
other => Err(Error::InvalidFetch {
value: other,
}),
}
}
async fn resolve_expr_to_string(&self, expr: &Expr) -> Result<String, Error> {
match expr {
Expr::Literal(Literal::String(s)) => Ok(s.as_str().to_owned()),
Expr::Param(param) => {
let value =
self.resolve_param(param.as_str()).await.unwrap_or(crate::val::Value::None);
value.coerce_to::<String>().map_err(|_| Error::Query {
message: "OMIT/FETCH parameter did not resolve to a string".to_string(),
})
}
_ => Err(Error::Query {
message: "OMIT/FETCH with computed expressions not yet supported".to_string(),
}),
}
}
async fn resolve_expr_to_string_array(&self, expr: &Expr) -> Result<Vec<String>, Error> {
match expr {
Expr::Literal(Literal::Array(items)) => {
let mut result = Vec::with_capacity(items.len());
for item in items {
result.push(self.resolve_expr_to_string(item).await?);
}
Ok(result)
}
Expr::Param(param) => {
let value =
self.resolve_param(param.as_str()).await.unwrap_or(crate::val::Value::None);
value.coerce_to::<Vec<String>>().map_err(|_| Error::Query {
message: "OMIT/FETCH parameter did not resolve to an array of strings"
.to_string(),
})
}
_ => Err(Error::Query {
message: "OMIT/FETCH with computed expressions not yet supported".to_string(),
}),
}
}
pub(crate) fn extract_needed_fields(
fields: &Fields,
omit: &[Expr],
cond: Option<&Cond>,
order: Option<&crate::expr::order::Ordering>,
group: Option<&crate::expr::group::Groups>,
split: Option<&crate::expr::split::Splits>,
) -> Option<std::collections::HashSet<String>> {
use crate::expr::visit::{Visit, Visitor};
use crate::expr::{Expr, Part};
match fields {
Fields::Select(field_list) => {
if field_list.iter().any(|f| matches!(f, Field::All)) {
return None;
}
}
Fields::Value(_) => {
}
}
struct NeededFieldExtractor {
fields: std::collections::HashSet<String>,
has_opaque: bool,
filter_depth: u32,
}
impl Visitor for NeededFieldExtractor {
type Error = std::convert::Infallible;
fn visit_idiom(&mut self, idiom: &crate::expr::Idiom) -> Result<(), Self::Error> {
use super::row_scope::{IdiomRoot, classify_idiom_root};
let root = classify_idiom_root(idiom);
match (self.filter_depth, root) {
(0, IdiomRoot::OuterRow) => {
for p in idiom.0.iter().skip(1) {
self.visit_part(p)?;
}
}
(0, IdiomRoot::ThisRow) => {
let mut leading_field_taken = false;
for part in idiom.0.iter() {
match part {
Part::Start(_) => continue,
Part::Field(name) if !leading_field_taken => {
self.fields.insert(name.as_str().to_owned());
leading_field_taken = true;
}
_ => {}
}
self.visit_part(part)?;
}
}
(0, IdiomRoot::Opaque) => {
self.has_opaque = true;
}
(1, IdiomRoot::OuterRow) => {
let mut leading_field_taken = false;
for part in idiom.0.iter().skip(1) {
match part {
Part::Field(name) if !leading_field_taken => {
self.fields.insert(name.as_str().to_owned());
leading_field_taken = true;
}
_ => {}
}
self.visit_part(part)?;
}
}
(1, IdiomRoot::ThisRow) => {
for part in idiom.0.iter() {
if matches!(part, Part::Start(_)) {
continue;
}
self.visit_part(part)?;
}
}
(1, IdiomRoot::Opaque) | (2.., _) => {
self.has_opaque = true;
}
}
Ok(())
}
fn visit_part(&mut self, part: &Part) -> Result<(), Self::Error> {
if matches!(part, Part::Where(_)) {
self.filter_depth = self.filter_depth.saturating_add(1);
let r = part.visit(self);
self.filter_depth = self.filter_depth.saturating_sub(1);
r
} else {
part.visit(self)
}
}
fn visit_expr(&mut self, expr: &Expr) -> Result<(), Self::Error> {
match expr {
Expr::Param(_) => {
self.has_opaque = true;
}
_ => {
expr.visit(self)?;
}
}
Ok(())
}
}
let mut extractor = NeededFieldExtractor {
fields: std::collections::HashSet::new(),
has_opaque: false,
filter_depth: 0,
};
match fields {
Fields::Value(selector) => {
let _ = extractor.visit_expr(&selector.expr);
}
Fields::Select(field_list) => {
for field in field_list {
if let Field::Single(selector) = field {
let _ = extractor.visit_expr(&selector.expr);
if let Some(alias) = &selector.alias {
let _ = extractor.visit_idiom(alias);
}
}
}
}
}
for expr in omit {
let _ = extractor.visit_expr(expr);
}
if let Some(cond) = cond {
let _ = extractor.visit_expr(&cond.0);
}
if let Some(ordering) = order {
match ordering {
crate::expr::order::Ordering::Random => {}
crate::expr::order::Ordering::Order(order_list) => {
for order in order_list.iter() {
let _ = extractor.visit_idiom(&order.value);
}
}
}
}
if let Some(groups) = group {
for group in groups.0.iter() {
let _ = extractor.visit_idiom(&group.0);
}
}
if let Some(splits) = split {
for split in splits.iter() {
let _ = extractor.visit_idiom(&split.0);
}
}
if extractor.has_opaque {
None
} else {
Some(extractor.fields)
}
}
async fn wrap_select_tail(
&self,
op: Arc<dyn ExecOperator>,
timeout: Expr,
version: Option<Arc<dyn crate::exec::PhysicalExpr>>,
only: bool,
only_none_on_empty: bool,
) -> Result<Arc<dyn ExecOperator>, Error> {
let timed = match timeout {
Expr::Literal(Literal::None) => op,
te => {
let tp = self.physical_expr(te).await?;
Arc::new(Timeout::new(op, Some(tp))) as Arc<dyn ExecOperator>
}
};
let versioned: Arc<dyn ExecOperator> = match version {
Some(v) => Arc::new(VersionScope::new(timed, v)),
None => timed,
};
if only {
Ok(Arc::new(UnwrapExactlyOne::new(versioned, only_none_on_empty)))
} else {
Ok(versioned)
}
}
pub(crate) async fn plan_select_statement(
&self,
mut select: crate::expr::statements::SelectStatement,
) -> Result<Arc<dyn ExecOperator>, Error> {
let explain = select.explain.take();
let plan = Box::pin(self.plan_select_core(select)).await?;
match explain {
Some(crate::expr::explain::Explain(full)) => {
if full {
Ok(Arc::new(AnalyzePlan {
plan,
format: crate::expr::ExplainFormat::Json,
redact_volatile_explain_attrs: self.ctx.redact_volatile_explain_attrs(),
}))
} else {
Ok(Arc::new(ExplainPlan {
plan,
format: crate::expr::ExplainFormat::Json,
}))
}
}
None => Ok(plan),
}
}
async fn plan_select_core(
&self,
select: crate::expr::statements::SelectStatement,
) -> Result<Arc<dyn ExecOperator>, Error> {
let crate::expr::statements::SelectStatement {
fields,
omit,
only,
mut what,
with,
cond,
split,
group,
order,
limit,
start,
fetch,
version,
timeout,
explain: _,
tempfiles,
} = select;
let version = extract_version(version, self).await?;
if is_count_all_eligible(&fields, &group, &cond, &split, &order, &fetch, &omit, &what) {
use crate::exec::operators::CountScan;
let table_expr = self
.physical_expr(what.into_iter().next().expect("what verified non-empty"))
.await?;
let field_names = extract_count_field_names(&fields);
let count_scan: Arc<dyn ExecOperator> =
Arc::new(CountScan::new(table_expr, version.clone(), field_names));
return self.wrap_select_tail(count_scan, timeout, version, only, true).await;
}
if is_indexed_count_eligible(&fields, &group, &cond, &split, &order, &fetch, &omit, &what)
&& !matches!(with, Some(crate::expr::with::With::NoIndex))
&& !self.cond_touches_restricted_select_field(&what, &cond).await
{
let has_count_idx = self.has_matching_count_index(&what, &cond).await;
let btree_access = if !has_count_idx {
self.resolve_count_btree_access(&what, &cond, with.as_ref()).await
} else {
None
};
if has_count_idx || btree_access.is_some() {
use crate::exec::operators::scan::index_count::IndexCountScan;
let table_first = what.first().cloned().ok_or_else(|| {
Error::Internal(
"indexed COUNT fast path: `is_indexed_count_eligible` returned true but `what` is empty".into(),
)
})?;
let table_expr = self.physical_expr(table_first).await?;
let condition = cond.clone().ok_or_else(|| {
Error::Internal(
"indexed COUNT fast path: `is_indexed_count_eligible` returned true but `cond` is None".into(),
)
})?;
let predicate = self.physical_expr(condition.0.clone()).await?;
let field_names = extract_count_field_names(&fields);
let index_count_scan: Arc<dyn ExecOperator> = Arc::new(
IndexCountScan::new(
table_expr,
predicate,
condition,
version.clone(),
field_names,
)
.with_btree_access(btree_access),
);
return self.wrap_select_tail(index_count_scan, timeout, version, only, true).await;
}
}
if what.len() == 1
&& matches!(&what[0], Expr::Literal(Literal::RecordId(_)))
&& cond.is_none()
&& order.is_none()
&& group.is_none()
&& split.is_none()
&& fetch.is_none()
&& with.is_none()
&& version.is_none()
{
let needed_fields = Self::extract_needed_fields(
&fields,
&omit,
cond.as_ref(),
order.as_ref(),
group.as_ref(),
split.as_ref(),
);
let rid_lit = match what.into_iter().next() {
Some(Expr::Literal(Literal::RecordId(rid_lit))) => rid_lit,
_ => {
return Err(Error::Internal(
"literal RecordId fast path entered without a literal RecordId source"
.into(),
));
}
};
let table_name_for_resolve = Some(rid_lit.table.clone());
let rid_expr = self.physical_expr(Expr::Literal(Literal::RecordId(rid_lit))).await?;
let resolved_table_ctx: Option<ResolvedTableContext> =
if let Some(ref tb) = table_name_for_resolve {
self.try_resolve_table_ctx(tb).await
} else {
None
};
let pdf = self.pre_decode_filter_status_for(
resolved_table_ctx.as_ref(),
None,
needed_fields.as_ref(),
);
let mut scan = RecordIdScan::new(rid_expr, version.clone(), needed_fields, None);
if let Some(tc) = resolved_table_ctx {
scan = scan.with_resolved(tc);
}
scan = scan.with_pre_decode_filter(pdf);
let scan: Arc<dyn ExecOperator> = Arc::new(scan);
let limited = if limit.is_some() || start.is_some() {
let limit_expr = match limit {
Some(l) => Some(self.physical_expr(l.0).await?),
None => None,
};
let start_expr = match start {
Some(s) => Some(self.physical_expr(s.0).await?),
None => None,
};
Arc::new(Limit::new(scan, limit_expr, start_expr)) as Arc<dyn ExecOperator>
} else {
scan
};
let projected = self.plan_projections(fields, omit, limited).await?;
return self.wrap_select_tail(projected, timeout, version, only, true).await;
}
let literal_primary_table = what.iter().find_map(|e| match e {
Expr::Table(t) => Some(t.clone()),
_ => None,
});
self.resolve_source_exprs(&mut what).await;
let is_value_source = all_value_sources(&what);
let primary_table = literal_primary_table.or_else(|| {
what.iter().find_map(|e| match e {
Expr::Table(t) => Some(t.clone()),
_ => None,
})
});
let has_knn_early = cond.as_ref().is_some_and(|c| has_knn_operator(&c.0));
let planning_ctx: std::borrow::Cow<'_, crate::ctx::FrozenContext> =
if let Some(ref c) = cond {
let mc = extract_matches_context(c, Some(self.ctx));
let hm = !mc.is_empty();
if hm || has_knn_early {
let mut child = crate::ctx::Context::new_child(self.ctx);
if hm {
let mut mc = mc;
if let Some(ref t) = primary_table {
mc.set_table(t.clone());
}
child.set_matches_context(mc);
}
if has_knn_early {
child.set_knn_context(std::sync::Arc::new(
crate::exec::function::KnnContext::new(),
));
}
std::borrow::Cow::Owned(child.freeze())
} else {
std::borrow::Cow::Borrowed(self.ctx)
}
} else {
std::borrow::Cow::Borrowed(self.ctx)
};
let mut pp = if let Some(ref txn) = self.txn {
Planner::with_txn(&planning_ctx, Arc::clone(txn), self.ns.clone(), self.db.clone())
} else {
Planner::new(&planning_ctx)
}
.with_version(version.clone())
.with_cycle_guard(self.cycle_guard());
if let Some(ref auth) = self.auth {
pp = pp.with_auth(Arc::clone(auth));
}
let needed_fields = Self::extract_needed_fields(
&fields,
&omit,
cond.as_ref(),
order.as_ref(),
group.as_ref(),
split.as_ref(),
);
let source_is_single_scan = what.len() == 1
&& matches!(what[0], Expr::Table(_) | Expr::FunctionCall(_) | Expr::Postfix { .. });
let ns_db = self.ns_db_ids().await;
let cond = match cond.as_ref() {
Some(c) => {
Some(resolve_condition_params(c, self.ctx, ns_db, SELECT_ITERATION_PARAMS).await)
}
None => None,
};
let cond = match cond {
Some(mut c) => {
fold_condition_expressions(&mut c, self.function_registry());
Some(c)
}
None => None,
};
let has_knn = cond.as_ref().is_some_and(|c| has_knn_operator(&c.0));
let brute_force_knn = if has_knn {
cond.as_ref().and_then(extract_bruteforce_knn)
} else {
None
};
let (cond_for_index, cond_for_filter) = if has_knn {
let stripped = cond.as_ref().and_then(strip_knn_from_condition);
if let Some(c) = stripped.as_ref()
&& has_knn_operator(&c.0)
{
let message = if has_knn_ktree_operator(&c.0) {
"The `<|k|>` KNN operator (KTree / M-Tree) is no longer supported. \
Use `<|k, EF|>` against an HNSW index (e.g. \
`DEFINE INDEX … HNSW DIMENSION N`), or `<|k, DISTANCE|>` for a \
brute-force KNN with an explicit distance metric."
.to_string()
} else {
"KNN operators must appear at the top level of the WHERE clause \
(joined with AND); nesting `<|k, …|>` inside OR or NOT is not \
supported."
.to_string()
};
return Err(Error::Query {
message,
});
}
if brute_force_knn.is_some() {
(stripped.clone(), stripped)
} else if cond.as_ref().is_some_and(|c| has_knn_k_operator(&c.0)) {
return Err(Error::PlannerUnimplemented(
"Brute-force KNN with parameter-based vectors is not supported \
in the streaming executor"
.to_string(),
));
} else {
(cond, stripped)
}
} else {
let c = cond;
(c.clone(), c)
};
let scan_predicate = if source_is_single_scan {
match cond_for_filter.as_ref() {
Some(c) => Some(pp.physical_expr(c.0.clone()).await?),
None => None,
}
} else {
None
};
let can_push_limit = source_is_single_scan
&& brute_force_knn.is_none()
&& !has_top_level_or(cond_for_filter.as_ref())
&& limit.is_some()
&& split.is_none()
&& group.is_none();
let can_soft_push_limit = !can_push_limit
&& source_is_single_scan
&& brute_force_knn.is_none()
&& limit.is_some()
&& split.is_some()
&& group.is_none();
let (scan_limit, scan_start) = if can_push_limit {
(
match limit.as_ref() {
Some(l) => Some(pp.physical_expr(l.0.clone()).await?),
None => None,
},
match start.as_ref() {
Some(s) => Some(pp.physical_expr(s.0.clone()).await?),
None => None,
},
)
} else if can_soft_push_limit {
(
match limit.as_ref() {
Some(l) => Some(pp.physical_expr(l.0.clone()).await?),
None => None,
},
None,
)
} else {
(None, None)
};
let scan_predicate_for_reuse = scan_predicate.clone();
let downstream_topk = is_bounded_topk_downstream(
order.as_ref(),
&start,
&limit,
tempfiles,
self.ctx.config.max_order_limit_priority_queue_size as usize,
);
let topk_request = if self.ctx.config.topk_threshold_pushdown_enabled {
compute_topk_pushdown_request(
order.as_ref(),
&start,
&limit,
&fields,
tempfiles,
split.is_some() || group.is_some() || brute_force_knn.is_some(),
self.ctx.config.max_order_limit_priority_queue_size as usize,
)
} else {
TopKPushdownRequest::NotApplicable
};
let mut planned = pp
.plan_sources(
what,
version.clone(),
cond_for_index.as_ref(),
order.as_ref(),
with.as_ref(),
needed_fields,
scan_predicate,
scan_limit,
scan_start,
downstream_topk,
&topk_request,
)
.await?;
if can_soft_push_limit {
planned.limit_pushed = false;
}
let topk_handle = planned.topk_pushdown.take();
let where_clause = match planned.filter_action {
FilterAction::FullyConsumed => WhereClauseState::None,
FilterAction::Residual(residual) => WhereClauseState::Original(residual),
FilterAction::UseOriginal => {
if let Some(pred) = scan_predicate_for_reuse {
WhereClauseState::Precompiled(pred)
} else if let Some(c) = cond_for_filter {
WhereClauseState::Original(c)
} else {
WhereClauseState::None
}
}
};
let (source, where_clause) = if let Some(kp) = brute_force_knn {
let input = match &where_clause {
WhereClauseState::Original(c) => {
let pred = pp.physical_expr(c.0.clone()).await?;
Arc::new(Filter::new(planned.operator, pred)) as Arc<dyn ExecOperator>
}
WhereClauseState::Precompiled(predicate) => {
Arc::new(Filter::new(planned.operator, Arc::clone(predicate)))
as Arc<dyn ExecOperator>
}
WhereClauseState::None => planned.operator,
};
let knn_ctx = planning_ctx.get_knn_context().cloned();
let wrapped = Arc::new(
KnnTopK::new(input, kp.field, kp.vector, kp.k as usize, kp.distance)
.with_knn_context(knn_ctx),
) as Arc<dyn ExecOperator>;
(wrapped, WhereClauseState::None)
} else {
(planned.operator, where_clause)
};
let config = SelectPipelineConfig {
where_clause,
split,
group,
order,
limit: if planned.limit_pushed {
None
} else {
limit
},
start: if planned.limit_pushed {
None
} else {
start
},
omit,
tempfiles,
topk_pushdown: topk_handle,
};
let projected = pp.plan_pipeline(source, Some(fields), config).await?;
let fetched = pp.plan_fetch(fetch, projected).await?;
pp.wrap_select_tail(fetched, timeout, version, only, !is_value_source).await
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn plan_sources(
&self,
what: Vec<Expr>,
version: Option<Arc<dyn crate::exec::PhysicalExpr>>,
cond: Option<&Cond>,
order: Option<&crate::expr::order::Ordering>,
with: Option<&crate::expr::with::With>,
needed_fields: Option<std::collections::HashSet<String>>,
scan_predicate: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_limit: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_start: Option<Arc<dyn crate::exec::PhysicalExpr>>,
downstream_topk: bool,
topk_request: &TopKPushdownRequest,
) -> Result<PlannedSource, Error> {
if what.is_empty() {
return Err(Error::Query {
message: "SELECT requires at least one source".to_string(),
});
}
let topk_request = if what.len() > 1 {
&TopKPushdownRequest::NotApplicable
} else {
topk_request
};
let mut plans = Vec::with_capacity(what.len());
for expr in what {
let p = self
.plan_source(
expr,
version.clone(),
cond,
order,
with,
needed_fields.clone(),
scan_predicate.clone(),
scan_limit.clone(),
scan_start.clone(),
downstream_topk,
topk_request,
)
.await?;
plans.push(p);
}
if plans.len() == 1 {
Ok(plans.pop().expect("verified non-empty"))
} else {
let operators = plans.into_iter().map(|p| p.operator).collect();
Ok(PlannedSource {
operator: Arc::new(Union::new(operators)),
filter_action: FilterAction::UseOriginal,
limit_pushed: false,
topk_pushdown: None,
})
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn plan_source(
&self,
expr: Expr,
version: Option<Arc<dyn crate::exec::PhysicalExpr>>,
cond: Option<&Cond>,
order: Option<&crate::expr::order::Ordering>,
with: Option<&crate::expr::with::With>,
needed_fields: Option<std::collections::HashSet<String>>,
scan_predicate: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_limit: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_start: Option<Arc<dyn crate::exec::PhysicalExpr>>,
downstream_topk: bool,
topk_request: &TopKPushdownRequest,
) -> Result<PlannedSource, Error> {
if let Expr::Table(ref table_name) = expr
&& !cond.is_some_and(|c| has_knn_operator(&c.0))
&& let Some(rid_expr) = cond.and_then(|c| {
let mut normalized = c.clone();
resolve_projection_field_idioms(&mut normalized, self.function_registry());
extract_record_id_point_lookup(&normalized, table_name)
}) {
let filter_action = filter_action_for_predicate(&scan_predicate);
let record_id_expr = self.physical_expr(rid_expr).await?;
let resolved_table_ctx: Option<ResolvedTableContext> =
self.try_resolve_table_ctx(table_name).await;
let pdf = self.pre_decode_filter_status_for(
resolved_table_ctx.as_ref(),
scan_predicate.as_ref(),
needed_fields.as_ref(),
);
let mut scan =
RecordIdScan::new(record_id_expr, version, needed_fields, scan_predicate.clone());
if let Some(tc) = resolved_table_ctx {
scan = scan.with_resolved(tc);
}
scan = scan.with_pre_decode_filter(pdf);
return Ok(PlannedSource {
operator: Arc::new(scan) as Arc<dyn ExecOperator>,
filter_action,
limit_pushed: false,
topk_pushdown: None,
});
}
if let Expr::Table(ref table_name) = expr
&& let (Some(txn), Some(ns), Some(db)) = (&self.txn, &self.ns, &self.db)
{
let table_ctx: Option<ResolvedTableContext> =
self.try_resolve_table_ctx(table_name).await;
let restricted_select = self.resolve_restricted_select_prefixes(table_name).await;
let order = match order {
Some(o) if restricted_select.order_touches(o) => None,
other => other,
};
let resolved =
self.resolve_access_path(txn, ns, db, table_name, cond, order, with).await;
if let Ok(Some((access_path, direction))) = resolved {
let table = table_name.clone();
let knn_ctx = self.ctx.get_knn_context().cloned();
match access_path {
AccessPath::BTreeScan {
index_ref,
access,
direction,
} => {
return self
.plan_btree_scan_source(
table,
index_ref,
access,
direction,
cond,
order,
scan_predicate,
scan_limit,
scan_start,
needed_fields,
version,
table_ctx,
)
.await;
}
AccessPath::FullTextSearch {
index_ref,
query,
operator,
} => {
return self
.plan_fulltext_search_source(
table,
index_ref,
query,
operator,
cond,
needed_fields,
version,
table_ctx,
)
.await;
}
AccessPath::KnnSearch {
index_ref,
vector,
k,
ef,
} => {
return self
.plan_knn_search_source(
table,
index_ref,
vector,
k,
ef,
cond,
needed_fields,
version,
table_ctx,
knn_ctx,
)
.await;
}
AccessPath::TableScan => {
return self
.plan_table_scan_source(
table,
direction,
order,
scan_predicate,
scan_limit,
scan_start,
needed_fields,
version,
table_ctx,
topk_request,
)
.await;
}
AccessPath::Union {
paths,
dedupe,
} => {
return self
.plan_union_index_source(
table,
paths,
dedupe,
order,
scan_limit,
cond,
needed_fields,
version,
table_ctx,
knn_ctx,
downstream_topk,
&restricted_select,
)
.await;
}
AccessPath::EmptyScan => {
return Ok(Self::plan_empty_source());
}
}
}
}
let knn_ctx = self.ctx.get_knn_context().cloned();
match expr {
Expr::Literal(crate::expr::literal::Literal::RecordId(rid)) => {
let record_id_expr = self
.physical_expr(Expr::Literal(crate::expr::literal::Literal::RecordId(rid)))
.await?;
Ok(PlannedSource {
operator: Arc::new(RecordIdScan::new(
record_id_expr,
version,
needed_fields,
None,
)) as Arc<dyn ExecOperator>,
filter_action: FilterAction::UseOriginal,
limit_pushed: false,
topk_pushdown: None,
})
}
Expr::Select(inner_select) => {
if version.is_some() {
return Err(Error::Query {
message: "VERSION clause cannot be used with a subquery source. \
Place the VERSION clause inside the subquery instead."
.to_string(),
});
}
Ok(PlannedSource {
operator: self.plan_select_statement(*inner_select).await?,
filter_action: FilterAction::UseOriginal,
limit_pushed: false,
topk_pushdown: None,
})
}
Expr::Param(_) => {
let phys_expr = self.physical_expr(expr).await?;
Ok(PlannedSource {
operator: Arc::new(SourceExpr::new(phys_expr)) as Arc<dyn ExecOperator>,
filter_action: FilterAction::UseOriginal,
limit_pushed: false,
topk_pushdown: None,
})
}
Expr::Table(_)
| Expr::FunctionCall(_)
| Expr::Postfix {
..
} => {
self.plan_dynamic_scan(
expr,
version,
cond,
order,
with,
needed_fields,
scan_predicate,
scan_limit,
scan_start,
knn_ctx,
)
.await
}
other => {
let phys_expr = self.physical_expr(other).await?;
Ok(PlannedSource {
operator: Arc::new(SourceExpr::new(phys_expr)) as Arc<dyn ExecOperator>,
filter_action: FilterAction::UseOriginal,
limit_pushed: false,
topk_pushdown: None,
})
}
}
}
#[allow(clippy::too_many_arguments)]
async fn plan_btree_scan_source(
&self,
table: crate::val::TableName,
index_ref: crate::exec::index::access_path::IndexRef,
access: BTreeAccess,
direction: crate::idx::planner::ScanDirection,
cond: Option<&Cond>,
order: Option<&crate::expr::order::Ordering>,
scan_predicate: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_limit: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_start: Option<Arc<dyn crate::exec::PhysicalExpr>>,
needed_fields: Option<std::collections::HashSet<String>>,
version: Option<Arc<dyn crate::exec::PhysicalExpr>>,
table_ctx: Option<ResolvedTableContext>,
) -> Result<PlannedSource, Error> {
use crate::exec::operators::IndexScan;
let filter_action = if let Some(c) = cond {
match strip_index_conditions(c, &access, &index_ref.cols) {
None => FilterAction::FullyConsumed,
Some(residual) => FilterAction::Residual(residual),
}
} else {
FilterAction::FullyConsumed
};
let order_covered =
|| order.is_none_or(|ord| index_covers_ordering(&index_ref, &access, direction, ord));
let push = scan_limit.is_some()
&& matches!(filter_action, FilterAction::FullyConsumed)
&& order_covered();
let (idx_limit, idx_start, limit_pushed, batch_ceiling) = if push {
(scan_limit, scan_start, true, None)
} else if let Some(sl) = scan_limit
&& matches!(filter_action, FilterAction::Residual(_))
&& order_covered()
{
(None, None, false, Some(sl))
} else {
(None, None, false, None)
};
let index_where_predicate = match &filter_action {
FilterAction::FullyConsumed => scan_predicate,
FilterAction::Residual(_) | FilterAction::UseOriginal => None,
};
let mut scan = IndexScan::new(
index_ref,
access,
direction,
table,
idx_limit,
idx_start,
version,
Some(needed_fields),
index_where_predicate,
)
.with_batch_ceiling(batch_ceiling);
if let Some(tc) = table_ctx {
scan = scan.with_resolved(tc);
}
Ok(PlannedSource {
operator: Arc::new(scan) as Arc<dyn ExecOperator>,
filter_action,
limit_pushed,
topk_pushdown: None,
})
}
#[allow(clippy::too_many_arguments)]
async fn plan_fulltext_search_source(
&self,
table: crate::val::TableName,
index_ref: crate::exec::index::access_path::IndexRef,
query: String,
operator: crate::expr::operator::MatchesOperator,
cond: Option<&Cond>,
needed_fields: Option<std::collections::HashSet<String>>,
version: Option<Arc<dyn crate::exec::PhysicalExpr>>,
table_ctx: Option<ResolvedTableContext>,
) -> Result<PlannedSource, Error> {
use crate::exec::operators::FullTextScan;
let mut scan =
FullTextScan::new(index_ref, query, operator, table, version, Some(needed_fields));
if let Some(tc) = table_ctx {
scan = scan.with_resolved(tc);
}
let filter_action = if let Some(c) = cond {
match strip_fts_condition(c) {
None => FilterAction::FullyConsumed,
Some(residual) => FilterAction::Residual(residual),
}
} else {
FilterAction::FullyConsumed
};
Ok(PlannedSource {
operator: Arc::new(scan) as Arc<dyn ExecOperator>,
filter_action,
limit_pushed: false,
topk_pushdown: None,
})
}
#[allow(clippy::too_many_arguments)]
async fn plan_knn_search_source(
&self,
table: crate::val::TableName,
index_ref: crate::exec::index::access_path::IndexRef,
vector: Vec<crate::val::Number>,
k: u32,
ef: u32,
cond: Option<&Cond>,
needed_fields: Option<std::collections::HashSet<String>>,
version: Option<Arc<dyn crate::exec::PhysicalExpr>>,
table_ctx: Option<ResolvedTableContext>,
knn_ctx: Option<Arc<crate::exec::function::KnnContext>>,
) -> Result<PlannedSource, Error> {
use crate::exec::operators::KnnScan;
let residual_cond = cond.and_then(strip_knn_from_condition);
let mut scan = KnnScan::new(
index_ref,
vector,
k,
ef,
table,
version,
knn_ctx,
residual_cond,
Some(needed_fields),
);
if let Some(tc) = table_ctx {
scan = scan.with_resolved(tc);
}
Ok(PlannedSource {
operator: Arc::new(scan) as Arc<dyn ExecOperator>,
filter_action: FilterAction::UseOriginal,
limit_pushed: false,
topk_pushdown: None,
})
}
fn plan_empty_source() -> PlannedSource {
use crate::exec::operators::EmptyScan;
PlannedSource {
operator: Arc::new(EmptyScan::new()) as Arc<dyn ExecOperator>,
filter_action: FilterAction::FullyConsumed,
limit_pushed: true,
topk_pushdown: None,
}
}
#[allow(clippy::too_many_arguments)]
async fn plan_table_scan_source(
&self,
table: crate::val::TableName,
direction: crate::idx::planner::ScanDirection,
order: Option<&crate::expr::order::Ordering>,
scan_predicate: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_limit: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_start: Option<Arc<dyn crate::exec::PhysicalExpr>>,
needed_fields: Option<std::collections::HashSet<String>>,
version: Option<Arc<dyn crate::exec::PhysicalExpr>>,
table_ctx: Option<ResolvedTableContext>,
topk_request: &TopKPushdownRequest,
) -> Result<PlannedSource, Error> {
use crate::exec::topk_pushdown::{
TopKPushdownHandle, TopKPushdownReason, TopKPushdownStatus, TopKThresholdCell,
TopKThresholdProbe, field_path_wire_segments, topk_pushdown_status_at_plan_time,
};
let filter_action = filter_action_for_predicate(&scan_predicate);
let push = scan_limit.is_some() && order_is_scan_compatible(order);
let (tbl_limit, tbl_start, limit_pushed) = if push {
(scan_limit, scan_start, true)
} else {
(None, None, false)
};
let pdf = self.pre_decode_filter_status_for(
table_ctx.as_ref(),
scan_predicate.as_ref(),
needed_fields.as_ref(),
);
let (topk_status, topk_handle) = match topk_request {
TopKPushdownRequest::NotApplicable => (TopKPushdownStatus::NotApplicable, None),
TopKPushdownRequest::Ineligible(reason) => {
(TopKPushdownStatus::Ineligible(*reason), None)
}
TopKPushdownRequest::Eligible(spec) => {
match field_path_wire_segments(&spec.first_key.path) {
None => {
(TopKPushdownStatus::Ineligible(TopKPushdownReason::UnsupportedOrder), None)
}
Some(segments) => {
let cell = Arc::new(TopKThresholdCell::default());
let probe = Arc::new(TopKThresholdProbe::new(
segments,
spec.first_key.direction,
spec.key_count == 1,
Arc::clone(&cell),
self.ctx.config.idiom_recursion_limit,
));
let status = topk_pushdown_status_at_plan_time(
probe,
table_ctx.as_ref().map(|tc| tc.field_state.as_ref()),
);
let handle = matches!(
status,
TopKPushdownStatus::Active(_) | TopKPushdownStatus::Deferred(_)
)
.then(|| TopKPushdownHandle {
cell,
expected_first_key: spec.first_key.clone(),
expected_key_count: spec.key_count,
});
(status, handle)
}
}
}
};
let mut scan = TableScan::new(
table,
direction,
version,
scan_predicate,
tbl_limit,
tbl_start,
needed_fields,
);
if let Some(tc) = table_ctx {
scan = scan.with_resolved(tc);
}
scan = scan.with_pre_decode_filter(pdf);
scan = scan.with_topk_pushdown(topk_status);
Ok(PlannedSource {
operator: Arc::new(scan) as Arc<dyn ExecOperator>,
filter_action,
limit_pushed,
topk_pushdown: topk_handle,
})
}
#[allow(clippy::too_many_arguments)]
async fn plan_union_index_source(
&self,
table: crate::val::TableName,
paths: Vec<AccessPath>,
dedupe: bool,
order: Option<&crate::expr::order::Ordering>,
scan_limit: Option<Arc<dyn crate::exec::PhysicalExpr>>,
cond: Option<&Cond>,
needed_fields: Option<std::collections::HashSet<String>>,
version: Option<Arc<dyn crate::exec::PhysicalExpr>>,
table_ctx: Option<ResolvedTableContext>,
knn_ctx: Option<Arc<crate::exec::function::KnnContext>>,
downstream_topk: bool,
restricted_select: &RestrictedPrefixes,
) -> Result<PlannedSource, Error> {
let merge_dir = detect_order_by_id_only(order).filter(|_| {
paths.iter().all(|p| {
matches!(
p,
AccessPath::BTreeScan {
access: BTreeAccess::Equality(_),
..
}
)
})
});
let merge_by_index_key = if merge_dir.is_none() {
detect_order_for_composite_union(order, &paths)
} else {
None
};
let paths = if let Some((_, dir)) = &merge_by_index_key {
let target = match dir {
SortDirection::Asc => crate::idx::planner::ScanDirection::Forward,
SortDirection::Desc => crate::idx::planner::ScanDirection::Backward,
};
paths
.into_iter()
.map(|p| match p {
AccessPath::BTreeScan {
index_ref,
access,
..
} => AccessPath::BTreeScan {
index_ref,
access,
direction: target,
},
other => other,
})
.collect::<Vec<_>>()
} else {
paths
};
let merge_active = merge_dir.is_some() || merge_by_index_key.is_some();
let merge_batch_ceiling = if merge_active {
scan_limit
} else {
None
};
let filter_action = if let Some(c) = cond {
let strip_safe = !restricted_select.cond_touches(c);
let stripped = if strip_safe {
strip_union_index_conditions(c, &paths)
} else {
Some(c.clone())
};
match stripped {
None => FilterAction::FullyConsumed,
Some(residual) => FilterAction::Residual(residual),
}
} else {
FilterAction::FullyConsumed
};
let mut sub_operators: Vec<Arc<dyn ExecOperator>> = Vec::with_capacity(paths.len());
for path in paths {
sub_operators.push(self.build_union_sub_operator(
path,
&table,
cond,
version.as_ref(),
table_ctx.as_ref(),
knn_ctx.as_ref(),
merge_batch_ceiling.as_ref(),
)?);
}
let mut union_scan = UnionIndexScan::new(table, sub_operators, needed_fields);
if let Some(dir) = merge_dir {
union_scan = union_scan.with_merge_by_id(dir);
} else if let Some((path, dir)) = merge_by_index_key {
if dedupe {
union_scan = union_scan.with_merge_by_index_key_dedup(path, dir);
} else {
union_scan = union_scan.with_merge_by_index_key(path, dir);
}
} else if downstream_topk {
union_scan = union_scan.with_downstream_topk();
}
if let Some(tc) = table_ctx {
union_scan = union_scan.with_resolved(tc);
}
Ok(PlannedSource {
operator: Arc::new(union_scan) as Arc<dyn ExecOperator>,
filter_action,
limit_pushed: false,
topk_pushdown: None,
})
}
#[allow(clippy::too_many_arguments)]
fn build_union_sub_operator(
&self,
path: AccessPath,
table: &crate::val::TableName,
cond: Option<&Cond>,
version: Option<&Arc<dyn crate::exec::PhysicalExpr>>,
table_ctx: Option<&ResolvedTableContext>,
knn_ctx: Option<&Arc<crate::exec::function::KnnContext>>,
merge_batch_ceiling: Option<&Arc<dyn crate::exec::PhysicalExpr>>,
) -> Result<Arc<dyn ExecOperator>, Error> {
use crate::exec::operators::{FullTextScan, IndexScan, KnnScan};
match path {
AccessPath::BTreeScan {
index_ref,
access,
direction,
} => {
let mut scan = IndexScan::new(
index_ref,
access,
direction,
table.clone(),
None,
None,
version.cloned(),
None,
None,
);
if let Some(ceiling) = merge_batch_ceiling {
scan = scan.with_batch_ceiling(Some(Arc::clone(ceiling)));
}
if let Some(tc) = table_ctx {
scan = scan.with_resolved(tc.clone());
}
Ok(Arc::new(scan))
}
AccessPath::FullTextSearch {
index_ref,
query,
operator,
} => {
let mut scan = FullTextScan::new(
index_ref,
query,
operator,
table.clone(),
version.cloned(),
None,
);
if let Some(tc) = table_ctx {
scan = scan.with_resolved(tc.clone());
}
Ok(Arc::new(scan))
}
AccessPath::KnnSearch {
index_ref,
vector,
k,
ef,
} => {
let residual_cond = cond.and_then(strip_knn_from_condition);
let mut scan = KnnScan::new(
index_ref,
vector,
k,
ef,
table.clone(),
version.cloned(),
knn_ctx.cloned(),
residual_cond,
None,
);
if let Some(tc) = table_ctx {
scan = scan.with_resolved(tc.clone());
}
Ok(Arc::new(scan))
}
other => {
tracing::error!(
path = ?other,
"UnionIndexScan sub-path produced an unexpected access path"
);
Err(Error::Internal(
"UnionIndexScan sub-path produced an unexpected access path; \
only BTreeScan / FullTextSearch / KnnSearch are valid here"
.into(),
))
}
}
}
#[allow(clippy::too_many_arguments)]
async fn plan_dynamic_scan(
&self,
expr: Expr,
version: Option<Arc<dyn crate::exec::PhysicalExpr>>,
cond: Option<&Cond>,
order: Option<&crate::expr::order::Ordering>,
with: Option<&crate::expr::with::With>,
needed_fields: Option<std::collections::HashSet<String>>,
scan_predicate: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_limit: Option<Arc<dyn crate::exec::PhysicalExpr>>,
scan_start: Option<Arc<dyn crate::exec::PhysicalExpr>>,
knn_ctx: Option<Arc<crate::exec::function::KnnContext>>,
) -> Result<PlannedSource, Error> {
let filter_action = filter_action_for_predicate(&scan_predicate);
let push = scan_limit.is_some() && order_is_scan_compatible(order);
let (dyn_limit, dyn_start, limit_pushed) = if push {
(scan_limit, scan_start, true)
} else {
(None, None, false)
};
let resolved_table_ctx: Option<ResolvedTableContext> =
if let Expr::Table(table_name) = &expr {
self.try_resolve_table_ctx(table_name).await
} else {
None
};
let source_expr = self.physical_expr(expr).await?;
let pdf = self.pre_decode_filter_status_for(
resolved_table_ctx.as_ref(),
scan_predicate.as_ref(),
needed_fields.as_ref(),
);
Ok(PlannedSource {
operator: Arc::new(
DynamicScan::new(
source_expr,
version,
cond.cloned(),
order.cloned(),
with.cloned(),
needed_fields,
scan_predicate,
dyn_limit,
dyn_start,
)
.with_knn_context(knn_ctx)
.with_pre_decode_filter(pdf),
) as Arc<dyn ExecOperator>,
filter_action,
limit_pushed,
topk_pushdown: None,
})
}
fn pre_decode_filter_status_for(
&self,
table_ctx: Option<&ResolvedTableContext>,
predicate: Option<&Arc<dyn crate::exec::PhysicalExpr>>,
needed_fields: Option<&std::collections::HashSet<String>>,
) -> crate::exec::pre_decode_filter::PreDecodeFilterStatus {
let projected = table_ctx.map(|tc| tc.field_state_for_projection(needed_fields));
pre_decode_filter_status_at_plan_time(
predicate,
projected.as_ref(),
self.ctx.config.idiom_recursion_limit,
)
}
async fn try_resolve_table_ctx(&self, table_name: &TableName) -> Option<ResolvedTableContext> {
let ns = self.ns()?;
let db = self.db()?;
let (ns_id, db_id) = self.ns_db_ids().await?;
let _entry = match self.cycle_guard().try_enter(ns_id, db_id, table_name.clone()) {
Some(e) => e,
None => {
tracing::debug!(
ns = %ns,
db = %db,
table = %table_name,
"plan-time cycle detected; falling back to runtime resolution",
);
return None;
}
};
match resolve_table_context(self, ns, db, ns_id, db_id, table_name).await {
Ok(opt) => opt,
Err(e) => {
tracing::warn!(
ns = %ns,
db = %db,
table = %table_name,
error = %e,
"plan-time table-context resolution failed; falling back to runtime",
);
None
}
}
}
async fn has_matching_count_index(&self, what: &[Expr], cond: &Option<Cond>) -> bool {
let table_name = match what.first() {
Some(Expr::Table(t)) => t,
_ => return false,
};
let cond = match cond {
Some(c) => c,
None => return false,
};
let Some(txn) = self.txn.as_ref() else {
return false;
};
let Some((ns_id, db_id)) = self.ns_db_ids().await else {
return false;
};
let indexes = match txn.all_tb_indexes(ns_id, db_id, table_name, None).await {
Ok(idx) => idx,
Err(e) => {
tracing::warn!(
table = %table_name,
error = %e,
"plan-time index list failed in has_matching_count_index; assuming no count index",
);
return false;
}
};
let Ok(indexes) = filter_online_indexes(txn, ns_id, db_id, indexes).await else {
return false;
};
indexes.iter().any(|ix| {
if let Index::Count(ref idx_cond) = ix.index {
idx_cond.as_ref() == Some(cond)
} else {
false
}
})
}
async fn cond_touches_restricted_select_field(
&self,
what: &[Expr],
cond: &Option<Cond>,
) -> bool {
let Some(cond) = cond else {
return false;
};
let table_name = match what.first() {
Some(Expr::Table(t)) => t,
_ => return false,
};
self.cond_touches_restricted_select_field_for_table(table_name, cond).await
}
async fn cond_touches_restricted_select_field_for_table(
&self,
table_name: &TableName,
cond: &Cond,
) -> bool {
self.resolve_restricted_select_prefixes(table_name).await.cond_touches(cond)
}
async fn resolve_restricted_select_prefixes(
&self,
table_name: &TableName,
) -> RestrictedPrefixes {
let (Some(ns_name), Some(db_name)) = (self.ns.as_deref(), self.db.as_deref()) else {
return RestrictedPrefixes::AssumeRestricted;
};
if !self.should_check_perms_for_view(ns_name, db_name) {
return RestrictedPrefixes::None;
}
let Some(txn) = self.txn.as_ref() else {
return RestrictedPrefixes::AssumeRestricted;
};
let Some((ns_id, db_id)) = self.ns_db_ids().await else {
return RestrictedPrefixes::AssumeRestricted;
};
let fields = match txn.all_tb_fields(ns_id, db_id, table_name, None).await {
Ok(fs) => fs,
Err(e) => {
tracing::warn!(
table = %table_name,
error = %e,
"plan-time field list failed in \
resolve_restricted_select_prefixes; \
conservatively disabling index fast paths",
);
return RestrictedPrefixes::AssumeRestricted;
}
};
let restricted_prefixes: Vec<crate::expr::Idiom> = fields
.iter()
.filter(|f| !matches!(f.select_permission, crate::catalog::Permission::Full))
.map(|f| f.name.clone())
.collect();
if restricted_prefixes.is_empty() {
RestrictedPrefixes::None
} else {
RestrictedPrefixes::Some(restricted_prefixes)
}
}
async fn resolve_count_btree_access(
&self,
what: &[Expr],
cond: &Option<Cond>,
with: Option<&With>,
) -> Option<(IndexRef, BTreeAccess)> {
let txn = self.txn.as_ref()?;
let table_name = match what.first() {
Some(Expr::Table(t)) => t,
_ => return None,
};
let cond = cond.as_ref()?;
let (ns_id, db_id) = self.ns_db_ids().await?;
let indexes = match txn.all_tb_indexes(ns_id, db_id, table_name, None).await {
Ok(idx) => idx,
Err(e) => {
tracing::warn!(
table = %table_name,
error = %e,
"plan-time index list failed in resolve_count_btree_access; \
falling back to runtime",
);
return None;
}
};
let indexes = filter_online_indexes(txn, ns_id, db_id, indexes).await.ok()?;
if indexes.is_empty() {
return None;
}
let analyzer = IndexAnalyzer::new(indexes, with);
let candidates = analyzer.analyze(Some(cond), None);
for candidate in &candidates {
if strip_index_conditions(cond, &candidate.access, &candidate.index_ref.cols).is_none()
{
return Some((candidate.index_ref.clone(), candidate.access.clone()));
}
}
None
}
#[allow(clippy::too_many_arguments)]
async fn resolve_access_path(
&self,
txn: &Transaction,
ns_name: &str,
db_name: &str,
table_name: &TableName,
cond: Option<&Cond>,
order: Option<&OrderClause>,
with: Option<&With>,
) -> Result<Option<(AccessPath, ScanDirection)>, Error> {
let direction = determine_scan_direction(order);
if let Some(c) = cond
&& matches!(&c.0, Expr::Literal(crate::expr::literal::Literal::Bool(false)))
{
return Ok(Some((AccessPath::EmptyScan, direction)));
}
if matches!(with, Some(With::NoIndex)) {
return Ok(Some((AccessPath::TableScan, direction)));
}
let ns_def = match txn.get_ns_by_name(ns_name, None).await {
Ok(Some(ns)) => ns,
_ => return Ok(None),
};
let db_def = match txn.get_db_by_name(ns_name, db_name, None).await {
Ok(Some(db)) => db,
_ => return Ok(None),
};
let indexes = match txn
.all_tb_indexes(ns_def.namespace_id, db_def.database_id, table_name, None)
.await
{
Ok(idx) => {
match filter_online_indexes(txn, ns_def.namespace_id, db_def.database_id, idx).await
{
Ok(idx) => idx,
Err(_) => return Ok(None),
}
}
Err(_) => return Ok(None),
};
if indexes.is_empty() {
return Ok(Some((AccessPath::TableScan, direction)));
}
let rewritten_cond = cond.map(|c| {
let mut c = c.clone();
resolve_projection_field_idioms(&mut c, self.function_registry());
c
});
let analysis_cond = rewritten_cond.as_ref();
let analyzer = IndexAnalyzer::new(indexes, with);
let candidates = analyzer.analyze(analysis_cond, order);
if candidates.is_empty() {
if let Some(path) = analyzer.try_or_union(analysis_cond, direction) {
return Ok(Some((path, direction)));
}
if let Some((path, _)) = analyzer.try_and_nested_or_union(analysis_cond, direction) {
return Ok(Some((path, direction)));
}
if let Some(path) = analyzer.try_in_expansion(analysis_cond, direction) {
return Ok(Some((path, direction)));
}
if let Some(path) = analyzer.try_containment_expansion(analysis_cond, direction) {
return Ok(Some((path, direction)));
}
return Ok(Some((AccessPath::TableScan, direction)));
}
let best_single_score = candidates.iter().map(|c| c.score()).max().unwrap_or(0);
let path = select_access_path(candidates, with, direction);
let (path, direction) = adjust_direction_for_order(path, order, direction);
if path.is_full_range_scan()
&& let Some(union_path) = analyzer.try_or_union(analysis_cond, direction)
{
return Ok(Some((union_path, direction)));
}
if path.is_full_range_scan()
&& let Some(union_path) = analyzer.try_containment_expansion(analysis_cond, direction)
{
return Ok(Some((union_path, direction)));
}
if with.is_none()
&& let Some((union_path, union_score)) =
analyzer.try_and_nested_or_union(analysis_cond, direction)
&& union_score > best_single_score
{
return Ok(Some((union_path, direction)));
}
Ok(Some((path, direction)))
}
}
enum RestrictedPrefixes {
AssumeRestricted,
None,
Some(Vec<Idiom>),
}
impl RestrictedPrefixes {
fn cond_touches(&self, cond: &Cond) -> bool {
let prefixes = match self {
RestrictedPrefixes::AssumeRestricted => return true,
RestrictedPrefixes::None => return false,
RestrictedPrefixes::Some(p) => p,
};
let mut checker = RestrictedIdiomChecker {
restricted_prefixes: prefixes,
found: false,
};
use crate::expr::visit::Visitor;
let _ = checker.visit_expr(&cond.0);
checker.found
}
fn order_touches(&self, order: &OrderClause) -> bool {
let OrderClause::Order(order_list) = order else {
return false;
};
let prefixes = match self {
RestrictedPrefixes::AssumeRestricted => return true,
RestrictedPrefixes::None => return false,
RestrictedPrefixes::Some(p) => p,
};
let mut checker = RestrictedIdiomChecker {
restricted_prefixes: prefixes,
found: false,
};
use crate::expr::visit::Visitor;
for order in order_list.iter() {
let _ = checker.visit_idiom(&order.value);
if checker.found {
return true;
}
}
false
}
}
struct RestrictedIdiomChecker<'a> {
restricted_prefixes: &'a [Idiom],
found: bool,
}
impl crate::expr::visit::Visitor for RestrictedIdiomChecker<'_> {
type Error = std::convert::Infallible;
fn visit_idiom(&mut self, idiom: &Idiom) -> Result<(), Self::Error> {
if self.found {
return Ok(());
}
for prefix in self.restricted_prefixes {
if idiom.starts_with(prefix.0.as_slice()) {
self.found = true;
return Ok(());
}
}
Ok(())
}
fn visit_select(&mut self, _: &crate::expr::SelectStatement) -> Result<(), Self::Error> {
Ok(())
}
}
fn adjust_direction_for_order(
path: AccessPath,
order: Option<&crate::expr::order::Ordering>,
default_direction: crate::idx::planner::ScanDirection,
) -> (AccessPath, crate::idx::planner::ScanDirection) {
use crate::exec::field_path::FieldPath;
use crate::exec::index::access_path::BTreeAccess;
use crate::expr::order::Ordering;
use crate::idx::planner::ScanDirection;
let AccessPath::BTreeScan {
ref index_ref,
ref access,
..
} = path
else {
return (path, default_direction);
};
let Some(Ordering::Order(order_list)) = order else {
return (path, default_direction);
};
let ix_def = index_ref.definition();
let equality_col_paths: Vec<FieldPath> = match access {
BTreeAccess::Compound {
prefix,
..
} => ix_def
.cols
.iter()
.take(prefix.len())
.filter_map(|idiom| FieldPath::try_from(idiom).ok())
.collect(),
BTreeAccess::Equality(_) => {
ix_def.cols.iter().filter_map(|idiom| FieldPath::try_from(idiom).ok()).collect()
}
_ => vec![],
};
let mut order_idx = 0;
for field in order_list.0.iter() {
if let Ok(fp) = FieldPath::try_from(&field.value)
&& equality_col_paths.contains(&fp)
{
order_idx += 1;
continue;
}
break;
}
let Some(first_order) = order_list.0.get(order_idx) else {
return (path, default_direction);
};
let Ok(order_path) = FieldPath::try_from(&first_order.value) else {
return (path, default_direction);
};
let target_col_index = match access {
BTreeAccess::Compound {
prefix,
..
} => prefix.len(),
BTreeAccess::Equality(_) => ix_def.cols.len(),
_ => 0,
};
if target_col_index >= ix_def.cols.len() {
if order_path == FieldPath::field("id") {
let new_direction = if first_order.direction {
ScanDirection::Forward } else {
ScanDirection::Backward };
let new_path = AccessPath::BTreeScan {
index_ref: index_ref.clone(),
access: access.clone(),
direction: new_direction,
};
return (new_path, new_direction);
}
return (path, default_direction);
}
let Some(target_col) = ix_def.cols.get(target_col_index) else {
return (path, default_direction);
};
let Ok(col_path) = FieldPath::try_from(target_col) else {
return (path, default_direction);
};
if order_path == col_path {
let new_direction = if first_order.direction {
ScanDirection::Forward } else {
ScanDirection::Backward };
let new_path = AccessPath::BTreeScan {
index_ref: index_ref.clone(),
access: access.clone(),
direction: new_direction,
};
(new_path, new_direction)
} else {
(path, default_direction)
}
}
pub(super) fn collect_field_names(fields: &Fields) -> Vec<String> {
match fields {
Fields::Value(_) => vec![], Fields::Select(field_list) => {
let mut names = Vec::with_capacity(field_list.len());
for field in field_list {
if let Field::Single(selector) = field {
let name = if let Some(alias) = &selector.alias {
idiom_to_field_name(alias)
} else {
derive_field_name(&selector.expr)
};
names.push(name);
}
}
names
}
}
}
pub(super) fn collect_simple_source_field_names(fields: &Fields) -> Vec<String> {
match fields {
Fields::Value(selector) => match &selector.expr {
Expr::Idiom(idiom) => simple_field_name(idiom).into_iter().collect(),
_ => vec![],
},
Fields::Select(field_list) => field_list
.iter()
.filter_map(|field| match field {
Field::Single(selector) => match &selector.expr {
Expr::Idiom(idiom) => simple_field_name(idiom),
_ => None,
},
Field::All => None,
})
.collect(),
}
}
fn simple_field_name(idiom: &Idiom) -> Option<String> {
use crate::expr::part::Part;
if idiom.len() == 1
&& let Some(Part::Field(name)) = idiom.first()
{
return Some(name.as_str().to_owned());
}
None
}
fn detect_order_by_id_only(order: Option<&crate::expr::order::Ordering>) -> Option<SortDirection> {
use crate::expr::order::Ordering;
if let Some(Ordering::Order(order_list)) = order
&& order_list.len() == 1
&& let Some(first) = order_list.0.first()
&& first.value.is_id()
&& !first.collate
&& !first.numeric
{
Some(if first.direction {
SortDirection::Asc
} else {
SortDirection::Desc
})
} else {
None
}
}
fn detect_order_for_composite_union(
order: Option<&crate::expr::order::Ordering>,
paths: &[AccessPath],
) -> Option<(crate::exec::field_path::FieldPath, SortDirection)> {
use crate::exec::field_path::FieldPath;
use crate::exec::index::access_path::BTreeAccess;
use crate::expr::order::Ordering;
let Some(Ordering::Order(order_list)) = order else {
return None;
};
if order_list.len() != 1 {
return None;
}
let order_field = order_list.0.first()?;
if order_field.collate || order_field.numeric {
return None;
}
let order_path = FieldPath::try_from(&order_field.value).ok()?;
let direction = if order_field.direction {
SortDirection::Asc
} else {
SortDirection::Desc
};
let mut branches = paths.iter();
let first = branches.next()?;
let (first_index_ref, first_prefix_len) = match first {
AccessPath::BTreeScan {
index_ref,
access: BTreeAccess::Compound {
prefix,
range: None,
},
..
} => (index_ref.clone(), prefix.len()),
_ => return None,
};
let ix_def = first_index_ref.definition();
if ix_def.cols.len() <= first_prefix_len {
return None;
}
let sort_col_idiom = ix_def.cols.get(first_prefix_len)?;
let sort_col_path = FieldPath::try_from(sort_col_idiom).ok()?;
if sort_col_path != order_path {
return None;
}
let first_prefix = match first {
AccessPath::BTreeScan {
access: BTreeAccess::Compound {
prefix,
..
},
..
} => prefix.as_slice(),
_ => return None,
};
let mut seen_prefixes: Vec<&[crate::val::Value]> = Vec::with_capacity(paths.len());
seen_prefixes.push(first_prefix);
for path in branches {
let (idx, access) = match path {
AccessPath::BTreeScan {
index_ref,
access: access @ BTreeAccess::Compound {
range: None,
..
},
..
} => (index_ref, access),
_ => return None,
};
if idx != &first_index_ref {
return None;
}
let BTreeAccess::Compound {
prefix,
..
} = access
else {
return None;
};
if prefix.len() != first_prefix_len {
return None;
}
if seen_prefixes.iter().any(|p| p == &prefix.as_slice()) {
return None;
}
seen_prefixes.push(prefix.as_slice());
}
Some((order_path, direction))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syn;
fn parse_select(src: &str) -> crate::expr::statements::SelectStatement {
let ast = syn::parse(src).expect("parse");
let mut exprs = ast.expressions;
assert_eq!(exprs.len(), 1, "expected one statement: got {} from {src:?}", exprs.len());
let top: crate::expr::TopLevelExpr = exprs.remove(0).into();
match top {
crate::expr::TopLevelExpr::Expr(crate::expr::Expr::Select(s)) => *s,
other => panic!("expected SELECT, got {other:?}"),
}
}
#[test]
fn extract_needed_fields_excludes_outer_row_paths() {
let stmt = parse_select(
"SELECT cat FROM users \
WHERE $parent.refs CONTAINS $this.cat",
);
let needed = Planner::extract_needed_fields(
&stmt.fields,
&stmt.omit,
stmt.cond.as_ref(),
stmt.order.as_ref(),
stmt.group.as_ref(),
stmt.split.as_ref(),
)
.expect("non-wildcard projection should produce a Some(set)");
assert!(
needed.contains("cat"),
"current row's `cat` should be in needed-fields: got {needed:?}",
);
assert!(
!needed.contains("refs"),
"$parent.refs is the OUTER row's `refs`, must NOT be in this row's \
needed-fields: got {needed:?}",
);
}
#[test]
fn extract_needed_fields_excludes_parent_paths_in_method_args() {
let stmt = parse_select(
"SELECT cat FROM users \
WHERE array::find($parent.refs, $this.cat) != NONE",
);
let needed = Planner::extract_needed_fields(
&stmt.fields,
&stmt.omit,
stmt.cond.as_ref(),
stmt.order.as_ref(),
stmt.group.as_ref(),
stmt.split.as_ref(),
)
.expect("non-wildcard projection should produce a Some(set)");
assert!(needed.contains("cat"), "got {needed:?}");
assert!(
!needed.contains("refs"),
"`$parent.refs` inside a function-call argument must not leak \
into this row's needed-fields: got {needed:?}",
);
}
#[test]
fn extract_needed_fields_keeps_this_row_paths() {
let stmt = parse_select("SELECT name FROM users WHERE $this.age > 18");
let needed = Planner::extract_needed_fields(
&stmt.fields,
&stmt.omit,
stmt.cond.as_ref(),
stmt.order.as_ref(),
stmt.group.as_ref(),
stmt.split.as_ref(),
)
.expect("non-wildcard projection should produce a Some(set)");
assert!(needed.contains("name"), "got {needed:?}");
assert!(needed.contains("age"), "got {needed:?}");
}
#[test]
fn extract_needed_fields_filter_predicate_scope_excluded() {
let stmt = parse_select(
"SELECT name FROM users \
WHERE refs[WHERE kind = 'tag'] != []",
);
let needed = Planner::extract_needed_fields(
&stmt.fields,
&stmt.omit,
stmt.cond.as_ref(),
stmt.order.as_ref(),
stmt.group.as_ref(),
stmt.split.as_ref(),
)
.expect("non-wildcard projection should produce a Some(set)");
assert!(needed.contains("name"), "got {needed:?}");
assert!(needed.contains("refs"), "got {needed:?}");
assert!(
!needed.contains("kind"),
"`kind` inside `[WHERE …]` references the iteration element, \
not this row: got {needed:?}",
);
}
#[test]
fn extract_needed_fields_filter_predicate_parent_promotes() {
let stmt = parse_select(
"SELECT name FROM users \
WHERE refs[WHERE $parent.cat = kind] != []",
);
let needed = Planner::extract_needed_fields(
&stmt.fields,
&stmt.omit,
stmt.cond.as_ref(),
stmt.order.as_ref(),
stmt.group.as_ref(),
stmt.split.as_ref(),
)
.expect("non-wildcard projection should produce a Some(set)");
assert!(needed.contains("name"), "got {needed:?}");
assert!(needed.contains("refs"), "got {needed:?}");
assert!(
needed.contains("cat"),
"`$parent.cat` inside `[WHERE …]` is rebound to this row: \
got {needed:?}",
);
assert!(!needed.contains("kind"), "got {needed:?}");
}
#[test]
fn extract_needed_fields_treats_bare_parent_as_field() {
let stmt = parse_select("SELECT parent.sub FROM table");
let needed = Planner::extract_needed_fields(
&stmt.fields,
&stmt.omit,
stmt.cond.as_ref(),
stmt.order.as_ref(),
stmt.group.as_ref(),
stmt.split.as_ref(),
)
.expect("non-wildcard projection should produce a Some(set)");
assert!(
needed.contains("parent"),
"bare `parent.sub` is a real column path: got {needed:?}",
);
}
}