use std::collections::HashSet;
use std::sync::Arc;
use surrealdb_types::ToSql;
use super::common::{fetch_and_filter_records_batch, resolve_version_stamp};
use super::pipeline::{build_field_state, eval_limit_expr};
use super::resolved::ResolvedTableContext;
use crate::err::Error;
use crate::exec::index::access_path::{BTreeAccess, IndexRef};
use crate::exec::index::iterator::btree::{CompoundEqualIterator, CompoundRangeIterator};
use crate::exec::index::iterator::{
IndexEqualIterator, IndexRangeIterator, UniqueEqualIterator, UniqueRangeIterator,
};
use crate::exec::permission::{
PhysicalPermission, convert_permission_to_physical_runtime, should_check_perms,
validate_record_user_access,
};
use crate::exec::{
AccessMode, ContextLevel, ControlFlowExt, ExecOperator, ExecutionContext, FlowResult,
OperatorMetrics, PhysicalExpr, ValueBatch, ValueBatchStream, monitor_stream,
};
use crate::expr::ControlFlow;
use crate::iam::Action;
use crate::idx::planner::ScanDirection;
use crate::kvs::CachePolicy;
#[derive(Debug)]
pub struct IndexScan {
pub index_ref: IndexRef,
pub access: BTreeAccess,
pub direction: ScanDirection,
pub table_name: crate::val::TableName,
pub(crate) limit: Option<Arc<dyn PhysicalExpr>>,
pub(crate) start: Option<Arc<dyn PhysicalExpr>>,
pub(crate) version: Option<Arc<dyn PhysicalExpr>>,
pub(crate) resolved: Option<ResolvedTableContext>,
pub(crate) needed_fields: Option<Option<HashSet<String>>>,
pub(crate) where_predicate: Option<Arc<dyn PhysicalExpr>>,
pub(crate) batch_ceiling: Option<Arc<dyn PhysicalExpr>>,
pub(crate) metrics: Arc<OperatorMetrics>,
}
impl IndexScan {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
index_ref: IndexRef,
access: BTreeAccess,
direction: ScanDirection,
table_name: crate::val::TableName,
limit: Option<Arc<dyn PhysicalExpr>>,
start: Option<Arc<dyn PhysicalExpr>>,
version: Option<Arc<dyn PhysicalExpr>>,
needed_fields: Option<Option<HashSet<String>>>,
where_predicate: Option<Arc<dyn PhysicalExpr>>,
) -> Self {
Self {
index_ref,
access,
direction,
table_name,
limit,
start,
version,
resolved: None,
needed_fields,
where_predicate,
batch_ceiling: None,
metrics: Arc::new(OperatorMetrics::new()),
}
}
pub(crate) fn with_resolved(mut self, resolved: ResolvedTableContext) -> Self {
self.resolved = Some(resolved);
self
}
pub(crate) fn with_batch_ceiling(mut self, ceiling: Option<Arc<dyn PhysicalExpr>>) -> Self {
self.batch_ceiling = ceiling;
self
}
}
impl ExecOperator for IndexScan {
fn name(&self) -> &'static str {
"IndexScan"
}
fn attrs(&self) -> Vec<(String, String)> {
let access_str = match &self.access {
BTreeAccess::Equality(v) => format!("= {}", v.to_sql()),
BTreeAccess::Range {
from,
to,
} => {
let from_str = match from {
Some(r) => format!(
"{}{}",
if r.inclusive {
">="
} else {
">"
},
r.value.to_sql()
),
None => String::new(),
};
let to_str = match to {
Some(r) => format!(
"{}{}",
if r.inclusive {
"<="
} else {
"<"
},
r.value.to_sql()
),
None => String::new(),
};
format!("{from_str} {to_str}").trim().to_string()
}
BTreeAccess::Compound {
prefix,
range,
} => {
let prefix_str = prefix.iter().map(|v| v.to_sql()).collect::<Vec<_>>().join(", ");
if let Some((op, val)) = range {
let val_sql = val.to_sql();
format!("[{prefix_str}] {op:?} {val_sql}")
} else {
format!("[{prefix_str}]")
}
}
BTreeAccess::FullText {
..
}
| BTreeAccess::Knn {
..
} => {
unreachable!("IndexScan does not support FullText or KNN access")
}
};
let mut attrs = vec![
("index".to_string(), self.index_ref.name.to_string()),
("access".to_string(), access_str),
("direction".to_string(), format!("{:?}", self.direction)),
];
if let Some(ref limit) = self.limit {
attrs.push(("limit".to_string(), limit.to_sql()));
}
if let Some(ref start) = self.start {
attrs.push(("offset".to_string(), start.to_sql()));
}
attrs
}
fn required_context(&self) -> ContextLevel {
ContextLevel::Database
}
fn access_mode(&self) -> AccessMode {
let mut mode = AccessMode::ReadOnly;
if let Some(ref limit) = self.limit {
mode = mode.combine(limit.access_mode());
}
if let Some(ref start) = self.start {
mode = mode.combine(start.access_mode());
}
if let Some(ref pred) = self.where_predicate {
mode = mode.combine(pred.access_mode());
}
mode
}
fn metrics(&self) -> Option<&OperatorMetrics> {
Some(&self.metrics)
}
fn output_ordering(&self) -> crate::exec::OutputOrdering {
use crate::exec::operators::SortDirection;
use crate::exec::ordering::SortProperty;
let dir = match self.direction {
ScanDirection::Forward => SortDirection::Asc,
ScanDirection::Backward => SortDirection::Desc,
};
let skip_cols = match &self.access {
BTreeAccess::Compound {
prefix,
..
} => prefix.len(),
BTreeAccess::Equality(_) => self.index_ref.definition().cols.len(),
_ => 0,
};
let ix_def = self.index_ref.definition();
let mut cols: Vec<SortProperty> = ix_def
.cols
.iter()
.skip(skip_cols)
.filter_map(|idiom| {
crate::exec::field_path::FieldPath::try_from(idiom).ok().map(|path| SortProperty {
path,
direction: dir,
collate: false,
numeric: false,
})
})
.collect();
if !self.index_ref.is_unique() {
if !ix_def.cols.is_empty() {
cols.push(SortProperty {
path: crate::exec::field_path::FieldPath::field("id"),
direction: dir,
collate: false,
numeric: false,
});
}
}
if cols.is_empty() {
crate::exec::OutputOrdering::Unordered
} else {
crate::exec::OutputOrdering::Sorted(cols)
}
}
fn constant_output_fields(&self) -> Vec<crate::exec::field_path::FieldPath> {
use crate::exec::index::access_path::BTreeAccess;
let ix_def = self.index_ref.definition();
match &self.access {
BTreeAccess::Equality(_) => ix_def
.cols
.iter()
.filter_map(|idiom| crate::exec::field_path::FieldPath::try_from(idiom).ok())
.collect(),
BTreeAccess::Compound {
prefix,
..
} => ix_def
.cols
.iter()
.take(prefix.len())
.filter_map(|idiom| crate::exec::field_path::FieldPath::try_from(idiom).ok())
.collect(),
_ => vec![],
}
}
fn execute(&self, ctx: &ExecutionContext) -> FlowResult<ValueBatchStream> {
let db_ctx = ctx.database()?.clone();
validate_record_user_access(&db_ctx)?;
let check_perms = should_check_perms(&db_ctx, Action::View)?;
let index_ref = self.index_ref.clone();
let access = self.access.clone();
let direction = self.direction;
let table_name = self.table_name.clone();
let limit_expr = self.limit.clone();
let start_expr = self.start.clone();
let ceiling_expr = self.batch_ceiling.clone();
let version_expr = self.version.clone();
let resolved = self.resolved.clone();
let needed_fields = self.needed_fields.clone();
let where_predicate = self.where_predicate.clone();
let ctx = ctx.clone();
let stream = async_stream::try_stream! {
let db_ctx = ctx.database()?;
let txn = ctx.txn();
let ns = Arc::clone(&db_ctx.ns_ctx.ns);
let db = Arc::clone(&db_ctx.db);
let ns_id = ns.namespace_id;
let db_id = db.database_id;
let limit_val: Option<usize> = match &limit_expr {
Some(expr) => Some(eval_limit_expr(&**expr, &ctx).await?),
None => None,
};
let start_val: usize = match &start_expr {
Some(expr) => eval_limit_expr(&**expr, &ctx).await?,
None => 0,
};
let batch_max: u32 = match &ceiling_expr {
Some(expr) => {
let c = eval_limit_expr(&**expr, &ctx).await?;
c.saturating_add(start_val).saturating_mul(4).clamp(1, 1000) as u32
}
None => u32::MAX, };
let version: Option<u64> = resolve_version_stamp(&ctx, version_expr.as_ref()).await?;
let select_permission = if let Some(ref res) = resolved {
res.select_permission(check_perms)
} else if check_perms {
let table_def = db_ctx
.get_table_def(&table_name, version)
.await
.context("Failed to get table")?;
if let Some(def) = &table_def {
convert_permission_to_physical_runtime(&def.permissions.select, ctx.ctx())
.await
.context("Failed to convert permission")?
} else {
Err(ControlFlow::Err(anyhow::Error::new(Error::TbNotFound {
name: table_name.clone(),
})))?
}
} else {
PhysicalPermission::Allow
};
if matches!(select_permission, PhysicalPermission::Deny) {
return;
}
if limit_val == Some(0) {
return;
}
let field_state = match &needed_fields {
Some(nf) => {
if let Some(ref res) = resolved {
res.field_state_for_projection(nf.as_ref())
} else {
build_field_state(
&ctx, &table_name, check_perms, nf.as_ref(),
).await?
}
}
None => super::pipeline::FieldState::empty(),
};
let mut pipeline = super::pipeline::ScanPipeline::new(
PhysicalPermission::Allow,
where_predicate,
field_state,
check_perms,
limit_val,
start_val,
);
let is_unique = index_ref.is_unique();
let ix = index_ref.definition();
match (&access, is_unique) {
(BTreeAccess::Equality(value), true) => {
let mut iter = UniqueEqualIterator::new(ns_id, db_id, ix, value)
.context("Failed to create iterator")?;
loop {
if ctx.cancellation().is_cancelled() {
Err(ControlFlow::Err(anyhow::anyhow!(
crate::err::Error::QueryCancelled
)))?;
}
let rids = iter.next_batch(&txn).await
.context("Failed to iterate index")?;
if rids.is_empty() {
break;
}
let mut values = fetch_and_filter_records_batch(
&ctx, &txn, ns_id, db_id, &rids, &select_permission, check_perms, version,
CachePolicy::ReadOnly,
).await?;
let cont = pipeline.process_batch(&mut values, &ctx).await?;
if !values.is_empty() {
yield ValueBatch { values };
}
if !cont {
break;
}
}
}
(BTreeAccess::Equality(value), false) => {
let reverse = matches!(direction, ScanDirection::Backward);
let mut iter = IndexEqualIterator::with_direction(ns_id, db_id, ix, value, reverse)
.context("Failed to create iterator")?;
loop {
if ctx.cancellation().is_cancelled() {
Err(ControlFlow::Err(anyhow::anyhow!(
crate::err::Error::QueryCancelled
)))?;
}
let rids = iter.next_batch(&txn).await
.context("Failed to iterate index")?;
if rids.is_empty() {
break;
}
let mut values = fetch_and_filter_records_batch(
&ctx, &txn, ns_id, db_id, &rids, &select_permission, check_perms, version,
CachePolicy::ReadOnly,
).await?;
let cont = pipeline.process_batch(&mut values, &ctx).await?;
if !values.is_empty() {
yield ValueBatch { values };
}
if !cont {
break;
}
}
}
(BTreeAccess::Range { from, to }, true) => {
let mut iter = UniqueRangeIterator::new(ns_id, db_id, ix, from.as_ref(), to.as_ref(), direction).context("Failed to create iterator")?;
loop {
if ctx.cancellation().is_cancelled() {
Err(ControlFlow::Err(anyhow::anyhow!(
Error::QueryCancelled
)))?;
}
let rids = iter.next_batch(&txn).await
.context("Failed to iterate index")?;
if rids.is_empty() { break; }
let mut values = fetch_and_filter_records_batch(
&ctx, &txn, ns_id, db_id, &rids, &select_permission, check_perms, version,
CachePolicy::ReadOnly,
).await?;
let cont = pipeline.process_batch(&mut values, &ctx).await?;
if !values.is_empty() {
yield ValueBatch { values };
}
if !cont {
break;
}
}
}
(BTreeAccess::Range { from, to }, false) => {
let mut iter = IndexRangeIterator::new(ns_id, db_id, ix, from.as_ref(), to.as_ref(), direction).context("Failed to create iterator")?;
loop {
if ctx.cancellation().is_cancelled() {
Err(ControlFlow::Err(anyhow::anyhow!(
Error::QueryCancelled
)))?
}
let rids = iter.next_batch(&txn).await
.context("Failed to iterate index")?;
if rids.is_empty() { break; }
let mut values = fetch_and_filter_records_batch(
&ctx, &txn, ns_id, db_id, &rids, &select_permission, check_perms, version,
CachePolicy::ReadOnly,
).await?;
let cont = pipeline.process_batch(&mut values, &ctx).await?;
if !values.is_empty() {
yield ValueBatch { values };
}
if !cont {
break;
}
}
}
(BTreeAccess::Compound { prefix, range: None }, _) => {
let mut iter = CompoundEqualIterator::new(ns_id, db_id, ix, prefix, None, direction).context("Failed to create compound iterator")?;
let can_cap = !matches!(select_permission, PhysicalPermission::Conditional(_));
let mut remaining: u32 = match (limit_val, can_cap) {
(Some(l), true) => l.saturating_add(start_val).min(u32::MAX as usize) as u32,
_ => u32::MAX,
};
let mut rids = iter.next_batch(&txn, remaining.min(batch_max)).await
.context("Failed to iterate compound index")?;
while !rids.is_empty() {
if ctx.cancellation().is_cancelled() {
Err(ControlFlow::Err(anyhow::anyhow!(
crate::err::Error::QueryCancelled
)))?;
}
remaining = remaining.saturating_sub(rids.len() as u32);
let (values_result, next_rids_result) = if remaining > 0 {
let fetch_fut = fetch_and_filter_records_batch(
&ctx, &txn, ns_id, db_id, &rids, &select_permission, check_perms, version,
CachePolicy::ReadOnly,
);
let scan_fut = iter.next_batch(&txn, remaining.min(batch_max));
let (v, n) = futures::join!(fetch_fut, scan_fut);
(v, Some(n))
} else {
let v = fetch_and_filter_records_batch(
&ctx, &txn, ns_id, db_id, &rids, &select_permission, check_perms, version,
CachePolicy::ReadOnly,
).await;
(v, None)
};
let mut values = values_result?;
let cont = pipeline.process_batch(&mut values, &ctx).await?;
if !values.is_empty() {
yield ValueBatch { values };
}
if !cont || remaining == 0 {
break;
}
rids = match next_rids_result {
Some(r) => r.context("Failed to iterate compound index")?,
None => break,
};
}
}
(BTreeAccess::Compound { prefix, range: Some(range) }, _) => {
let mut iter = CompoundRangeIterator::new(ns_id, db_id, ix, prefix, range, direction).context("Failed to create compound range iterator")?;
let can_cap = !matches!(select_permission, PhysicalPermission::Conditional(_));
let mut remaining: u32 = match (limit_val, can_cap) {
(Some(l), true) => l.saturating_add(start_val).min(u32::MAX as usize) as u32,
_ => u32::MAX,
};
let mut rids = iter.next_batch(&txn, remaining.min(batch_max)).await
.context("Failed to iterate compound index")?;
while !rids.is_empty() {
if ctx.cancellation().is_cancelled() {
Err(ControlFlow::Err(anyhow::anyhow!(
crate::err::Error::QueryCancelled
)))?;
}
remaining = remaining.saturating_sub(rids.len() as u32);
let (values_result, next_rids_result) = if remaining > 0 {
let fetch_fut = fetch_and_filter_records_batch(
&ctx, &txn, ns_id, db_id, &rids, &select_permission, check_perms, version,
CachePolicy::ReadOnly,
);
let scan_fut = iter.next_batch(&txn, remaining.min(batch_max));
let (v, n) = futures::join!(fetch_fut, scan_fut);
(v, Some(n))
} else {
let v = fetch_and_filter_records_batch(
&ctx, &txn, ns_id, db_id, &rids, &select_permission, check_perms, version,
CachePolicy::ReadOnly,
).await;
(v, None)
};
let mut values = values_result?;
let cont = pipeline.process_batch(&mut values, &ctx).await?;
if !values.is_empty() {
yield ValueBatch { values };
}
if !cont || remaining == 0 {
break;
}
rids = match next_rids_result {
Some(r) => r.context("Failed to iterate compound index")?,
None => break,
};
}
}
(BTreeAccess::FullText { .. }, _) | (BTreeAccess::Knn { .. }, _) => {
Err(ControlFlow::Err(anyhow::anyhow!(
"IndexScan does not support FullText or KNN access - use dedicated operators"
)))?
}
}
};
Ok(monitor_stream(Box::pin(stream), "IndexScan", &self.metrics))
}
}