use anyhow::Result;
use surrealdb_datastore::Transaction;
use surrealdb_kvs::Direction;
use surrealdb_kvs::consts::NORMAL_BATCH_SIZE;
use crate::catalog::providers::TableProvider;
use crate::catalog::{DatabaseId, NamespaceId, Relation, TableType};
use crate::expr::Dir;
use crate::idx::adjacency::{AdjacencyResolveCache, MergedAdjacencyCursor};
use crate::val::{RecordId, TableName};
const FILL_FLOOR: u32 = 64;
pub(crate) struct LightweightEdgeScanner<'a> {
txn: &'a Transaction,
ns: NamespaceId,
db: DatabaseId,
edge_table: TableName,
tables: Vec<TableName>,
at: usize,
cursor: Option<MergedAdjacencyCursor<'a>>,
direction: Direction,
version: Option<u64>,
resolve: Option<&'a AdjacencyResolveCache>,
}
impl<'a> LightweightEdgeScanner<'a> {
#[expect(clippy::too_many_arguments)]
pub(crate) fn new(
txn: &'a Transaction,
ns: NamespaceId,
db: DatabaseId,
edge_table: &TableName,
rel: &Relation,
direction: Direction,
version: Option<u64>,
resolve: Option<&'a AdjacencyResolveCache>,
) -> Self {
let mut tables = rel.from.clone();
tables.sort();
tables.dedup();
if direction == Direction::Backward {
tables.reverse();
}
Self {
txn,
ns,
db,
edge_table: edge_table.clone(),
tables,
at: 0,
cursor: None,
direction,
version,
resolve,
}
}
pub(crate) async fn next_batch(&mut self, limit: u32) -> Result<Vec<RecordId>> {
let limit = limit.max(1) as usize;
let mut out = Vec::new();
while out.len() < limit {
if self.cursor.is_none() {
let Some(table) = self.tables.get(self.at) else {
break;
};
let folded = self
.txn
.get_tb(self.ns, self.db, table, self.version)
.await?
.is_some_and(|tb| tb.graph_folded);
self.cursor = Some(
MergedAdjacencyCursor::open_table_wide(
self.txn,
self.ns,
self.db,
table,
folded,
self.direction,
Some((Dir::Out, self.edge_table.clone())),
self.version,
self.resolve,
)
.await?,
);
}
let cursor = self.cursor.as_mut().expect("a cursor was just installed");
let fill = ((limit - out.len()) as u32).clamp(FILL_FLOOR, NORMAL_BATCH_SIZE);
let batch = cursor.next_batch(fill).await?;
if batch.is_empty() {
self.cursor = None;
self.at += 1;
continue;
}
out.extend(batch.into_iter().map(|edge| edge.edge));
}
Ok(out)
}
}
pub(crate) async fn lightweight_relation_is_empty(
txn: &Transaction,
ns: NamespaceId,
db: DatabaseId,
edge_table: &TableName,
rel: &Relation,
) -> Result<bool> {
let mut scanner =
LightweightEdgeScanner::new(txn, ns, db, edge_table, rel, Direction::Forward, None, None);
Ok(scanner.next_batch(1).await?.is_empty())
}
pub(crate) fn lightweight_relation(table_type: &TableType) -> Option<&Relation> {
match table_type {
TableType::Relation(rel) if rel.lightweight => Some(rel),
_ => None,
}
}
pub(crate) use surrealdb_datastore::tx::lightweight_edge_parts as edge_parts;