use std::borrow::Cow;
use std::collections::HashMap;
use std::ops::Bound;
use std::sync::Arc;
use anyhow::Result as AnyResult;
use surrealdb_datastore::values::graph::GraphFoldScope;
use super::common::evaluate_bound_key;
use crate::catalog::providers::TableProvider;
use crate::catalog::{DatabaseId, NamespaceId};
use crate::exec::{ControlFlowExt, ExecutionContext, PhysicalExpr};
use crate::expr::{ControlFlow, Dir};
use crate::idx::adjacency::{
AdjacencyScope, MergeStats, MergedAdjacencyCursor, MergedAdjacencyEdge, VertexAdjacency,
vertex_adjacency,
};
pub(crate) use crate::key::schema::DecodedGraphPrefix;
use crate::key::schema::{GraphDirPrefix, GraphForeignTablePrefix};
use crate::key::{KVSubspace, RawRange};
use crate::kvs::Transaction;
use crate::val::{RecordId, RecordIdKey, TableName};
#[derive(Debug, Clone)]
pub struct EdgeTableSpec {
pub table: TableName,
pub range_start: Bound<Arc<dyn PhysicalExpr>>,
pub range_end: Bound<Arc<dyn PhysicalExpr>>,
}
pub(crate) struct GraphRange {
pub(crate) edge_table: Option<TableName>,
pub(crate) fk_lower: Option<RecordIdKey>,
pub(crate) range: RawRange,
pub(crate) decoder: DecodedGraphPrefix,
}
impl GraphRange {
pub(crate) fn as_adjacency_scope<'a>(
&'a self,
ns: NamespaceId,
db: DatabaseId,
vertex: &'a RecordId,
dir: Dir,
) -> AdjacencyScope<'a> {
AdjacencyScope {
ns,
db,
vertex,
dir: Some(dir),
edge_table: self.edge_table.as_ref(),
fk_lower: self.fk_lower.as_ref(),
delta_range: self.range.clone(),
}
}
}
pub(crate) async fn compute_graph_ranges(
ns_id: NamespaceId,
db_id: DatabaseId,
rid: &RecordId,
dir: Dir,
edge_tables: &[EdgeTableSpec],
ctx: &ExecutionContext,
) -> Result<Vec<GraphRange>, ControlFlow> {
if edge_tables.is_empty() {
let prefix = GraphDirPrefix {
ns: ns_id,
db: db_id,
tb: Cow::Borrowed(&rid.table),
id: Cow::Borrowed(&rid.key),
dir,
};
let bound = prefix.encode_bound()?;
let range = prefix.raw(bound.clone().prefix_expect());
Ok(vec![GraphRange {
edge_table: None,
fk_lower: None,
range,
decoder: DecodedGraphPrefix::from_dir_bound(bound),
}])
} else {
let mut ranges = Vec::with_capacity(edge_tables.len());
for spec in edge_tables {
let start = eval_fk_bound(&spec.range_start, ctx).await?;
let end = eval_fk_bound(&spec.range_end, ctx).await?;
let fk_lower = match &start {
Bound::Included(key) | Bound::Excluded(key) => Some(key.clone()),
Bound::Unbounded => None,
};
let prefix = GraphForeignTablePrefix {
ns: ns_id,
db: db_id,
tb: Cow::Borrowed(&rid.table),
id: Cow::Borrowed(&rid.key),
dir,
foreign_table: Cow::Borrowed(&spec.table),
};
let decoder = DecodedGraphPrefix::from_foreign_table_bound(
prefix.encode_bound()?,
spec.table.clone(),
);
let range = prefix.range_where((as_cow_bound(&start), as_cow_bound(&end)))?;
ranges.push(GraphRange {
edge_table: Some(spec.table.clone()),
fk_lower,
range,
decoder,
});
}
Ok(ranges)
}
}
fn as_cow_bound<'a>(bound: &'a Bound<RecordIdKey>) -> Bound<Cow<'a, RecordIdKey>> {
match bound {
Bound::Included(key) => Bound::Included(Cow::Borrowed(key)),
Bound::Excluded(key) => Bound::Excluded(Cow::Borrowed(key)),
Bound::Unbounded => Bound::Unbounded,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn collect_adjacency(
txn: &Transaction,
ns_id: NamespaceId,
db_id: DatabaseId,
rid: &RecordId,
dir: Dir,
edge_tables: &[EdgeTableSpec],
ctx: &ExecutionContext,
version: Option<u64>,
adjacency_memo: &mut HashMap<TableName, VertexAdjacency>,
mut visit: impl FnMut(MergedAdjacencyEdge) -> Result<(), ControlFlow>,
) -> Result<MergeStats, ControlFlow> {
let ranges = compute_graph_ranges(ns_id, db_id, rid, dir, edge_tables, ctx).await?;
let adjacency = vertex_adjacency_memo(txn, ns_id, db_id, &rid.table, adjacency_memo)
.await
.map_err(ControlFlow::Err)?;
if adjacency == VertexAdjacency::Lightweight
&& !txn
.record_exists(ns_id, db_id, &rid.table, &rid.key, version)
.await
.map_err(ControlFlow::Err)?
{
return Ok(MergeStats::default());
}
let fold_threshold = ctx.root().ctx.config.idx.graph_fold_threshold;
let mut stats = MergeStats::default();
for range in &ranges {
let mut cursor = match adjacency {
VertexAdjacency::Lightweight => MergedAdjacencyCursor::open_lightweight(
txn,
&range.as_adjacency_scope(ns_id, db_id, rid, dir),
)
.context("Failed to open graph cursor")?,
VertexAdjacency::Normal {
folded,
} => MergedAdjacencyCursor::open(
txn,
range.as_adjacency_scope(ns_id, db_id, rid, dir),
folded,
version,
Some(ctx.root().ctx.get_index_stores().adjacency_resolve()),
)
.await
.context("Failed to open graph cursor")?,
};
loop {
crate::exec::operators::check_cancelled(ctx)?;
let batch = cursor
.next_batch_scan(crate::kvs::NORMAL_BATCH_SIZE)
.await
.context("Failed to scan graph edge")?;
if batch.is_empty() {
break;
}
for item in batch {
visit(item)?;
}
}
let range_stats = cursor.stats();
stats.block_hits += range_stats.block_hits;
stats.delta_hits += range_stats.delta_hits;
}
observe_fold_candidate(txn, ns_id, db_id, rid, dir, stats.delta_hits, fold_threshold);
Ok(stats)
}
pub(crate) async fn vertex_adjacency_memo(
txn: &Transaction,
ns: NamespaceId,
db: DatabaseId,
table: &TableName,
memo: &mut HashMap<TableName, VertexAdjacency>,
) -> AnyResult<VertexAdjacency> {
if let Some(adjacency) = memo.get(table) {
return Ok(*adjacency);
}
let adjacency = vertex_adjacency(txn, ns, db, table).await?;
memo.insert(table.clone(), adjacency);
Ok(adjacency)
}
pub(crate) fn observe_fold_candidate(
txn: &Transaction,
ns: NamespaceId,
db: DatabaseId,
rid: &RecordId,
dir: Dir,
delta_hits: u64,
threshold: usize,
) {
if threshold == 0 || (delta_hits as usize) < threshold {
return;
}
txn.observe_graph_fold(GraphFoldScope {
ns,
db,
tb: rid.table.clone(),
id: rid.key.clone(),
dir,
});
}
async fn eval_fk_bound(
bound: &Bound<Arc<dyn PhysicalExpr>>,
ctx: &ExecutionContext,
) -> Result<Bound<RecordIdKey>, ControlFlow> {
Ok(match bound {
Bound::Included(expr) => Bound::Included(evaluate_bound_key(expr, ctx).await?),
Bound::Excluded(expr) => Bound::Excluded(evaluate_bound_key(expr, ctx).await?),
Bound::Unbounded => Bound::Unbounded,
})
}