use std::sync::Arc;
use surrealdb_strand::TableName;
use super::Planner;
use super::util::{extract_table_from_matches, get_effective_limit_literal, key_lit_to_expr};
use crate::err::Error;
use crate::exec::operators::{
CurrentValueSource, EdgeTableSpec, Filter, GraphEdgeScan, GraphScanOutput, Limit, OrderByField,
ReferenceScan, ReferenceScanOutput, SortDirection,
};
use crate::exec::parts::LookupDirection;
use crate::exec::planner::select::SelectPipelineConfig;
use crate::exec::{Error as ExecError, ExecOperator};
use crate::expr::{Expr, Literal};
pub(crate) struct TargetVertexPlan {
pub direction: LookupDirection,
pub edge_tables: Vec<EdgeTableSpec>,
pub target_tables: Vec<TableName>,
}
impl<'ctx> Planner<'ctx> {
pub(crate) async fn plan_index_function(
&self,
name: &str,
mut ast_args: Vec<Expr>,
) -> Result<Arc<dyn crate::exec::PhysicalExpr>, Error> {
use crate::exec::function::{IndexContext, IndexContextKind};
use crate::exec::physical_expr::function::IndexFunctionExec;
let registry = self.function_registry();
let func = registry.get_index_function(name).ok_or_else(|| ExecError::Query {
message: format!("Index function '{}' not found in registry", name),
})?;
let index_ctx = match func.index_context_kind() {
IndexContextKind::FullText => {
let ref_idx = func.index_ref_arg_index().ok_or_else(|| ExecError::Query {
message: format!(
"Index function '{}': FullText functions must declare an index_ref_arg_index",
name
),
})?;
if ref_idx >= ast_args.len() {
return Err(ExecError::Query {
message: format!(
"Index function '{}' requires at least {} arguments",
name,
ref_idx + 1
),
}
.into());
}
let match_ref_ast = ast_args.remove(ref_idx);
let match_ref = match match_ref_ast {
Expr::Literal(Literal::Integer(n)) if (0..=255).contains(&n) => n as u8,
Expr::Literal(Literal::Float(n))
if (0.0..=255.0).contains(&n) && n.fract() == 0.0 =>
{
n as u8
}
_ => {
return Err(ExecError::Query {
message: format!(
"Index function '{}': index_ref argument must be a literal integer in range 0..255",
name
),
}
.into());
}
};
let matches_ctx =
self.matches_context.as_ref().ok_or_else(|| ExecError::Query {
message: format!(
"Index function '{}': no MATCHES clause found in WHERE condition",
name
),
})?;
let match_ctx = matches_ctx
.resolve(match_ref, extract_table_from_matches(matches_ctx))
.map_err(|e| ExecError::Query {
message: format!("Index function '{}': {}", name, e),
})?;
IndexContext::FullText(match_ctx)
}
IndexContextKind::Knn => {
if let Some(ref_idx) = func.index_ref_arg_index()
&& ref_idx < ast_args.len()
{
ast_args.remove(ref_idx);
}
let knn_ctx = self.knn_context.as_ref().ok_or_else(|| ExecError::Query {
message: format!(
"Index function '{}': no KNN operator found in WHERE condition",
name
),
})?;
IndexContext::Knn(Arc::clone(knn_ctx))
}
};
let mut phys_args = Vec::with_capacity(ast_args.len());
for arg in ast_args {
phys_args.push(self.physical_expr(arg).await?);
}
let func_ctx = func.required_context();
Ok(Arc::new(IndexFunctionExec {
name: name.to_string(),
arguments: phys_args,
index_ctx,
func_required_context: func_ctx,
}))
}
pub(crate) async fn convert_order_list(
&self,
order_list: crate::expr::order::OrderList,
) -> Result<Vec<OrderByField>, Error> {
let mut fields = Vec::with_capacity(order_list.len());
for order_field in order_list {
let expr: Arc<dyn crate::exec::PhysicalExpr> =
self.convert_idiom(order_field.value).await?;
let direction = if order_field.direction {
SortDirection::Asc
} else {
SortDirection::Desc
};
fields.push(OrderByField {
expr,
direction,
collate: order_field.collate,
numeric: order_field.numeric,
});
}
Ok(fields)
}
pub(crate) async fn convert_range_bound(
&self,
bound: &std::ops::Bound<crate::expr::RecordIdKeyLit>,
) -> Result<std::ops::Bound<Arc<dyn crate::exec::PhysicalExpr>>, Error> {
match bound {
std::ops::Bound::Unbounded => Ok(std::ops::Bound::Unbounded),
std::ops::Bound::Included(lit) => {
let expr = key_lit_to_expr(lit)?;
let phys = self.physical_expr(expr).await?;
Ok(std::ops::Bound::Included(phys))
}
std::ops::Bound::Excluded(lit) => {
let expr = key_lit_to_expr(lit)?;
let phys = self.physical_expr(expr).await?;
Ok(std::ops::Bound::Excluded(phys))
}
}
}
pub(crate) async fn plan_lookup(
&self,
lookup: crate::expr::lookup::Lookup,
) -> Result<Arc<dyn ExecOperator>, Error> {
let input: Arc<dyn ExecOperator> = Arc::new(CurrentValueSource::new());
self.plan_lookup_with_input(input, lookup).await
}
pub(crate) async fn plan_lookup_with_input(
&self,
input: Arc<dyn ExecOperator>,
crate::expr::lookup::Lookup {
kind,
expr,
only: _,
what,
cond,
split,
group,
order,
limit,
start,
alias: _,
}: crate::expr::lookup::Lookup,
) -> Result<Arc<dyn ExecOperator>, Error> {
let needs_full_pipeline = expr.is_some() || group.is_some();
let is_graph = matches!(&kind, crate::expr::lookup::LookupKind::Graph(_));
let graph_pushdown: Option<(
Arc<dyn crate::exec::PhysicalExpr>,
bool,
Option<crate::exec::operators::scan::PayloadPredicateSpec>,
)> = if is_graph && !needs_full_pipeline {
match &cond {
Some(c) => {
let pred = self.physical_expr(c.0.clone()).await?;
let key_resident = Self::graph_predicate_is_key_resident(&pred);
let direction = match &kind {
crate::expr::lookup::LookupKind::Graph(dir) => (*dir).into(),
_ => LookupDirection::Both,
};
let payload = if key_resident {
None
} else {
self.graph_predicate_payload_spec(&c.0, &pred, &what, direction).await
};
Some((pred, key_resident, payload))
}
None => None,
}
} else {
None
};
let cond_pushed_pre_fetch = graph_pushdown
.as_ref()
.map(|(_, kr, payload)| *kr || payload.is_some())
.unwrap_or(false);
let needs_full_records =
needs_full_pipeline || (cond.is_some() && !cond_pushed_pre_fetch) || split.is_some();
let output_mode = if needs_full_records {
GraphScanOutput::FullEdge
} else {
GraphScanOutput::TargetId
};
let base_scan: Arc<dyn ExecOperator> = match &kind {
crate::expr::lookup::LookupKind::Graph(dir) => {
let mut edge_tables: Vec<EdgeTableSpec> = Vec::with_capacity(what.len());
for s in what {
let spec = match s {
crate::expr::lookup::LookupSubject::Table {
table,
..
} => EdgeTableSpec {
table,
range_start: std::ops::Bound::Unbounded,
range_end: std::ops::Bound::Unbounded,
},
crate::expr::lookup::LookupSubject::Range {
table,
range,
..
} => {
let range_start = self.convert_range_bound(&range.start).await?;
let range_end = self.convert_range_bound(&range.end).await?;
EdgeTableSpec {
table,
range_start,
range_end,
}
}
};
edge_tables.push(spec);
}
let ordered_scan_cap = !matches!(dir, crate::expr::Dir::Both)
&& edge_tables.len() == 1
&& order.as_ref().is_some_and(order_is_plain_id_asc)
&& order_by_id_is_source_id(expr.as_ref());
let pushed_limit = if (cond.is_none() || graph_pushdown.is_some())
&& split.is_none()
&& (order.is_none() || ordered_scan_cap)
&& group.is_none()
{
match get_effective_limit_literal(&start, &limit) {
Some(effective_limit)
if graph_pushdown.is_some()
|| self.graph_limit_count_is_exact(&edge_tables).await =>
{
Some(effective_limit)
}
_ => None,
}
} else {
None
};
let scan = GraphEdgeScan::new(
input,
LookupDirection::from(dir),
edge_tables,
output_mode,
self.version.clone(),
);
let scan = match pushed_limit {
Some(effective_limit) if ordered_scan_cap => {
scan.with_ordered_limit(effective_limit)
}
Some(effective_limit) => scan.with_limit(effective_limit),
None => scan,
};
let scan = match &graph_pushdown {
Some((pred, _, Some(payload))) => {
scan.with_payload_predicate(Arc::clone(pred), payload.clone())
}
Some((pred, key_resident, None)) => {
scan.with_predicate(Arc::clone(pred), *key_resident)
}
None => scan,
};
Arc::new(scan)
}
crate::expr::lookup::LookupKind::Reference => {
let (referencing_table, referencing_field, range_start, range_end) =
if let Some(subject) = what.first() {
match subject {
crate::expr::lookup::LookupSubject::Table {
table,
referencing_field,
} => (
Some(table.clone()),
referencing_field.clone(),
std::ops::Bound::Unbounded,
std::ops::Bound::Unbounded,
),
crate::expr::lookup::LookupSubject::Range {
table,
referencing_field,
range,
} => {
let rs = self.convert_range_bound(&range.start).await?;
let re = self.convert_range_bound(&range.end).await?;
(Some(table.clone()), referencing_field.clone(), rs, re)
}
}
} else {
(None, None, std::ops::Bound::Unbounded, std::ops::Bound::Unbounded)
};
let ref_output_mode = if needs_full_records {
ReferenceScanOutput::FullRecord
} else {
ReferenceScanOutput::RecordId
};
Arc::new(ReferenceScan::new(
input,
referencing_table,
referencing_field,
ref_output_mode,
range_start,
range_end,
self.version.clone(),
))
}
};
if needs_full_pipeline {
let config = SelectPipelineConfig {
where_clause: match cond {
Some(c) => crate::exec::planner::select::WhereClauseState::Original(c),
None => crate::exec::planner::select::WhereClauseState::None,
},
split,
group,
order,
limit,
start,
omit: vec![],
tempfiles: false,
topk_pushdown: None,
};
self.plan_pipeline(base_scan, expr, config).await
} else {
let filtered: Arc<dyn ExecOperator> = match cond {
Some(_) if graph_pushdown.is_some() => base_scan,
Some(cond) => Arc::new(Filter::new(base_scan, self.physical_expr(cond.0).await?)),
None => base_scan,
};
let split_op: Arc<dyn ExecOperator> = if let Some(splits) = split {
Arc::new(crate::exec::operators::Split::new(
filtered,
splits.into_iter().map(|s| s.0).collect(),
))
} else {
filtered
};
let sorted: Arc<dyn ExecOperator> = match order {
Some(order) => self.plan_sort(split_op, order, &start, &limit, false).await?,
None => split_op,
};
let limited: Arc<dyn ExecOperator> = if limit.is_some() || start.is_some() {
let limit_expr = match limit {
Some(l) => Some(self.physical_expr(l.0).await?),
None => None,
};
let offset_expr = match start {
Some(s) => Some(self.physical_expr(s.0).await?),
None => None,
};
Arc::new(Limit::new(sorted, limit_expr, offset_expr))
} else {
sorted
};
Ok(limited)
}
}
fn graph_predicate_is_key_resident(_pred: &Arc<dyn crate::exec::PhysicalExpr>) -> bool {
false
}
async fn graph_predicate_payload_spec(
&self,
cond: &crate::expr::Expr,
pred: &Arc<dyn crate::exec::PhysicalExpr>,
what: &[crate::expr::lookup::LookupSubject],
direction: LookupDirection,
) -> Option<crate::exec::operators::scan::PayloadPredicateSpec> {
use crate::catalog::providers::TableProvider;
use crate::exec::operators::scan::{PayloadPredicateSpec, PayloadTableSpec};
let (Some(txn), Some(ns), Some(db)) = (&self.txn, &self.ns, &self.db) else {
return None;
};
if self.version.is_some() || what.is_empty() {
return None;
}
if pred.access_mode() != crate::exec::AccessMode::ReadOnly {
return None;
}
let deps = crate::expr::computed_deps::extract_computed_deps(cond);
if !deps.is_complete {
return None;
}
let needs_id = deps.fields.iter().any(|f| f == "id");
let needs_in = deps.fields.iter().any(|f| f == "in");
let needs_out = deps.fields.iter().any(|f| f == "out");
let needed: Vec<&String> =
deps.fields.iter().filter(|f| !matches!(f.as_str(), "id" | "in" | "out")).collect();
if needed.is_empty() {
let matcher = crate::exec::operators::scan::key_matcher::KeyPredicateMatcher::compile(
cond, direction,
)
.map(Arc::new);
return Some(PayloadPredicateSpec {
tables: std::collections::HashMap::new(),
needs_id,
needs_in,
needs_out,
key_only: true,
matcher,
});
}
let perms_skipped = !self.should_check_perms_for_view(ns, db);
let mut tables = std::collections::HashMap::new();
for subject in what {
let table = match subject {
crate::expr::lookup::LookupSubject::Table {
table,
..
}
| crate::expr::lookup::LookupSubject::Range {
table,
..
} => table,
};
let Ok(Some(def)) = txn.get_tb_by_name(ns, db, table, None).await else {
return None;
};
if !perms_skipped && !matches!(def.permissions.select, crate::catalog::Permission::Full)
{
return None;
}
match &def.table_type {
crate::catalog::TableType::Relation(rel) if !rel.lightweight => {}
_ => return None,
}
let Ok(fields) =
txn.all_tb_fields(def.namespace_id, def.database_id, &def.name, None).await
else {
return None;
};
let inline = crate::doc::inline_fields(&fields);
let names: Vec<String> =
inline.iter().map(|f| crate::doc::inline_field_name(f)).collect();
for field in &needed {
names.iter().position(|n| &n == field)?;
}
if !perms_skipped {
for fd in fields.iter() {
let Some(crate::expr::Part::Field(root)) = fd.name.0.first() else {
continue;
};
if needed.iter().any(|n| n.as_str() == root.as_str())
&& !matches!(fd.select_permission, crate::catalog::Permission::Full)
{
return None;
}
}
}
let referenced = names.iter().map(|n| needed.contains(&n)).collect::<Vec<_>>();
tables.insert(
def.name.clone(),
PayloadTableSpec {
generation: def.graph_inline_gen,
fields: names,
referenced,
},
);
}
Some(PayloadPredicateSpec {
tables,
needs_id,
needs_in,
needs_out,
key_only: false,
matcher: None,
})
}
async fn graph_limit_count_is_exact(&self, edge_tables: &[EdgeTableSpec]) -> bool {
use crate::catalog::providers::TableProvider;
let (Some(ns), Some(db)) = (self.ns(), self.db()) else {
return false;
};
if !self.should_check_perms_for_view(ns, db) {
return true;
}
if self.version.is_some() || edge_tables.is_empty() {
return false;
}
let Some(txn) = self.txn() else {
return false;
};
for spec in edge_tables {
match txn.get_tb_by_name(ns, db, &spec.table, None).await {
Ok(Some(td))
if matches!(td.permissions.select, crate::catalog::Permission::Full) => {}
_ => return false,
}
}
true
}
pub(crate) async fn try_fast_path_pair(
&self,
edge: &crate::expr::lookup::Lookup,
vertex: &crate::expr::lookup::Lookup,
) -> Result<Option<TargetVertexPlan>, Error> {
use crate::expr::lookup::{LookupKind, LookupSubject};
let edge_dir = match (&edge.kind, &vertex.kind) {
(LookupKind::Graph(d1), LookupKind::Graph(d2)) if d1 == d2 => d1,
_ => return Ok(None),
};
if self.version.is_some() {
return Ok(None);
}
if matches!(edge_dir, crate::expr::Dir::Both) {
return Ok(None);
}
let lookup_is_plain = |l: &crate::expr::lookup::Lookup| {
l.expr.is_none()
&& l.cond.is_none()
&& l.split.is_none()
&& l.group.is_none()
&& l.order.is_none()
&& l.limit.is_none()
&& l.start.is_none()
&& l.alias.is_none()
&& !l.only
};
if !lookup_is_plain(edge) || !lookup_is_plain(vertex) {
return Ok(None);
}
let mut edge_tables: Vec<EdgeTableSpec> = Vec::with_capacity(edge.what.len());
for s in &edge.what {
let LookupSubject::Table {
table,
..
} = s
else {
return Ok(None);
};
edge_tables.push(EdgeTableSpec {
table: table.clone(),
range_start: std::ops::Bound::Unbounded,
range_end: std::ops::Bound::Unbounded,
});
}
if edge_tables.is_empty() {
return Ok(None);
}
let mut target_tables: Vec<TableName> = Vec::with_capacity(vertex.what.len());
for s in &vertex.what {
let LookupSubject::Table {
table,
..
} = s
else {
return Ok(None);
};
target_tables.push(table.clone());
}
if target_tables.is_empty() {
return Ok(None);
}
let Some(txn) = self.txn() else {
return Ok(None);
};
let (Some(ns), Some(db)) = (self.ns(), self.db()) else {
return Ok(None);
};
if self.should_check_perms_for_view(ns, db) {
let edge_table_names: Vec<TableName> =
edge_tables.iter().map(|spec| spec.table.clone()).collect();
if !crate::exec::permission::tables_have_full_select_permission(
txn,
ns,
db,
&edge_table_names,
)
.await
{
return Ok(None);
}
}
Ok(Some(TargetVertexPlan {
direction: LookupDirection::from(edge_dir),
edge_tables,
target_tables,
}))
}
pub(crate) async fn plan_target_vertex_scan(
&self,
input: Arc<dyn ExecOperator>,
plan: TargetVertexPlan,
) -> Result<Arc<dyn ExecOperator>, Error> {
let scan = GraphEdgeScan::new(
input,
plan.direction,
plan.edge_tables,
GraphScanOutput::TargetVertex,
self.version.clone(),
)
.with_target_tables(plan.target_tables);
Ok(Arc::new(scan))
}
}
fn order_is_plain_id_asc(order: &crate::expr::order::Ordering) -> bool {
let crate::expr::order::Ordering::Order(list) = order else {
return false;
};
let [key] = list.0.as_slice() else {
return false;
};
key.direction
&& !key.collate
&& !key.numeric
&& matches!(key.value.0.as_slice(), [crate::expr::Part::Field(f)] if f.as_str() == "id")
}
fn order_by_id_is_source_id(fields: Option<&crate::expr::Fields>) -> bool {
let Some(fields) = fields else {
return true;
};
let id_idiom = crate::expr::Idiom(crate::expr::paths::ID.to_vec());
match crate::exec::expression_registry::resolve_order_by_alias(&id_idiom, fields) {
None => true,
Some((resolved, _)) => matches!(
&resolved,
crate::expr::Expr::Idiom(idiom)
if matches!(idiom.0.as_slice(), [crate::expr::Part::Field(f)] if f.as_str() == "id")
),
}
}