use std::collections::HashSet;
use std::sync::Arc;
use linkrs_core::types::Timestamp;
use super::core::EdgeStore;
use super::stats::ScanPruneReport;
use crate::cursor::ScanPredicate;
use crate::edge::node_group::ShardCsrIterator;
use crate::edge::{EdgeRecord, Nbr};
pub const DEFAULT_ADJACENCY_BATCH: usize = 256;
pub struct AdjacencyBatchAccessor<'a> {
table: &'a EdgeStore,
outgoing: bool,
ts: Timestamp,
}
impl<'a> AdjacencyBatchAccessor<'a> {
pub fn new(table: &'a EdgeStore, outgoing: bool, ts: Timestamp) -> Self {
Self {
table,
outgoing,
ts,
}
}
fn csr(&self) -> &crate::edge::node_group::CsrShardSet {
if self.outgoing {
&self.table.out_csr
} else {
&self.table.in_csr
}
}
pub fn fill_into(&self, src: u32, out: &mut Vec<Nbr>) {
out.clear();
if !self.table.is_direction_available(self.outgoing) {
return;
}
self.table.fill_visible_into(self.csr(), src, self.ts, out);
}
pub fn fill_limited(&self, src: u32, out: &mut Vec<Nbr>, limit: usize) {
out.clear();
if limit == 0 || !self.table.is_direction_available(self.outgoing) {
return;
}
out.reserve(limit.min(32));
let table = self.table;
let ts = self.ts;
self.csr().visit_physical(src, |nbr| {
if table.is_visible(nbr.edge_id, ts) {
out.push(nbr);
out.len() < limit
} else {
true
}
});
}
pub fn visit_batched<F>(&self, src: u32, scratch: &mut Vec<Nbr>, batch_size: usize, mut f: F)
where
F: FnMut(&[Nbr]) -> bool,
{
let batch_size = if batch_size == 0 {
DEFAULT_ADJACENCY_BATCH
} else {
batch_size
};
scratch.clear();
if !self.table.is_direction_available(self.outgoing) {
return;
}
let table = self.table;
let ts = self.ts;
let mut done = false;
self.csr().visit_physical(src, |nbr| {
if done {
return false;
}
if table.is_visible(nbr.edge_id, ts) {
scratch.push(nbr);
if scratch.len() >= batch_size {
if !f(scratch.as_slice()) {
done = true;
return false;
}
scratch.clear();
}
}
true
});
if !done && !scratch.is_empty() {
f(scratch.as_slice());
scratch.clear();
}
}
pub fn lookup(&self, src: u32, dst: u32, rank: i64) -> Option<Nbr> {
if !self.table.is_direction_available(self.outgoing) {
return None;
}
let csr = self.csr();
let ts = self.ts;
let table = self.table;
let mut found = None;
csr.visit_physical(src, |nbr| {
if nbr.endpoint == dst && nbr.rank == rank && table.is_visible(nbr.edge_id, ts) {
found = Some(nbr);
false
} else {
true
}
});
found
}
pub fn fill_many_into(&self, srcs: &[u32], out: &mut Vec<Nbr>, scratch: &mut Vec<Nbr>) {
out.clear();
if !self.table.is_direction_available(self.outgoing) {
return;
}
let csr = self.csr();
for src in srcs {
self.table.fill_visible_into(csr, *src, self.ts, scratch);
out.extend(scratch.iter().copied());
}
}
pub fn fill_many_with_values_into(
&self,
srcs: &[u32],
out: &mut Vec<(Nbr, Option<u64>)>,
scratch: &mut Vec<(Nbr, Option<u64>)>,
) {
out.clear();
if !self.table.is_direction_available(self.outgoing) {
return;
}
let csr = self.csr();
for src in srcs {
self.table
.fill_visible_with_values_into(csr, *src, self.ts, scratch);
out.extend(scratch.iter().copied());
}
}
}
pub struct EdgeTableScanIterator<'a> {
table: &'a EdgeStore,
inner: ShardCsrIterator<'a>,
outgoing: bool,
ts: Timestamp,
max_records: Option<usize>,
current_count: usize,
projection: Option<Vec<Arc<str>>>,
predicates: Vec<ScanPredicate>,
pruned_groups: HashSet<usize>,
segments_total: usize,
segments_pruned: usize,
rows_scanned: usize,
rows_filtered: usize,
last_row: Option<u32>,
last_row_pruned: bool,
}
impl<'a> EdgeTableScanIterator<'a> {
pub fn new(table: &'a EdgeStore, ts: Timestamp) -> Self {
Self::with_limit(table, ts, None)
}
pub fn with_projection(
table: &'a EdgeStore,
ts: Timestamp,
projection: Option<Vec<Arc<str>>>,
) -> Self {
Self::with_predicates(table, ts, projection, Vec::new())
}
pub fn with_predicates(
table: &'a EdgeStore,
ts: Timestamp,
projection: Option<Vec<Arc<str>>>,
predicates: Vec<ScanPredicate>,
) -> Self {
let outgoing = table.has_out_edges();
let shards = if outgoing {
&table.out_csr
} else {
&table.in_csr
};
let existing = shards.existing_group_ids();
let segments_total = existing.len();
let mut pruned_groups = HashSet::new();
if !predicates.is_empty() {
for gid in &existing {
if !table.segment_may_contain(*gid as u32, &predicates) {
pruned_groups.insert(*gid);
}
}
}
let segments_pruned = pruned_groups.len();
Self {
table,
inner: shards.iter_all(),
outgoing,
ts,
max_records: None,
current_count: 0,
projection,
predicates,
pruned_groups,
segments_total,
segments_pruned,
rows_scanned: 0,
rows_filtered: 0,
last_row: None,
last_row_pruned: false,
}
}
pub fn prune_report(&self) -> ScanPruneReport {
ScanPruneReport {
segments_total: self.segments_total,
segments_pruned: self.segments_pruned,
rows_scanned: self.rows_scanned,
rows_filtered: self.rows_filtered,
}
}
pub fn with_limit(table: &'a EdgeStore, ts: Timestamp, max_records: Option<usize>) -> Self {
let outgoing = table.has_out_edges();
let shards = if outgoing {
&table.out_csr
} else {
&table.in_csr
};
let existing = shards.existing_group_ids();
let segments_total = existing.len();
Self {
table,
inner: shards.iter_all(),
outgoing,
ts,
max_records,
current_count: 0,
projection: None,
predicates: Vec::new(),
pruned_groups: HashSet::new(),
segments_total,
segments_pruned: 0,
rows_scanned: 0,
rows_filtered: 0,
last_row: None,
last_row_pruned: false,
}
}
}
impl<'a> Iterator for EdgeTableScanIterator<'a> {
type Item = EdgeRecord;
fn next(&mut self) -> Option<Self::Item> {
if let Some(max) = self.max_records {
if self.current_count >= max {
return None;
}
}
for (row_vid, nbr) in self.inner.by_ref() {
self.rows_scanned += 1;
let Some(row) = row_vid.as_internal_u32() else {
continue;
};
let pruned = match self.last_row {
Some(cached) if cached == row => self.last_row_pruned,
_ => {
let shards = if self.outgoing {
&self.table.out_csr
} else {
&self.table.in_csr
};
let pruned = shards
.group_of(row)
.is_some_and(|gid| self.pruned_groups.contains(&gid));
self.last_row = Some(row);
self.last_row_pruned = pruned;
pruned
}
};
if pruned {
continue;
}
if !self.table.is_visible(nbr.edge_id, self.ts) {
continue;
}
if !self.predicates.is_empty()
&& !self.table.matches_pushdown_assume_visible(
nbr.edge_id,
self.ts,
&self.predicates,
)
{
self.rows_filtered += 1;
continue;
}
self.current_count += 1;
if self.outgoing {
return Some(self.table.edge_record_from_nbr_projected_assume_visible(
row,
nbr,
self.ts,
self.projection.as_deref(),
));
}
return Some(self.table.edge_record_from_in_nbr_assume_visible(
row,
nbr,
self.ts,
self.projection.as_deref(),
));
}
None
}
}