use super::*;
use crate::store::admit_graph_read;
use crate::QueryMemoryClass;
use hawdb_storage::read_view::{
AdmittedVec, ControlledGraphReadAllocator, GraphReadAdmission, GraphReadAllocation,
};
use std::cell::RefCell;
pub(crate) fn admit_access_copy<'a>(
account: &QueryMemoryAccount,
task_context: Option<&RuntimeTaskContext>,
properties: impl Iterator<Item = &'a str>,
values: impl Iterator<Item = &'a Value>,
) -> Result<Box<dyn GraphReadAllocation>> {
let bytes = properties
.fold(3 * std::mem::size_of::<usize>(), |total, key| {
total
.saturating_add(key.len())
.saturating_add(std::mem::size_of::<String>())
})
.saturating_add(values.fold(0usize, |total, value| {
total.saturating_add(crate::binding::value_memory_bytes(value))
}));
admit_graph_read(account, task_context, bytes)
}
fn binding_overhead(variable: &str) -> usize {
1024usize
.saturating_add(std::mem::size_of::<Binding>().saturating_mul(2))
.saturating_add(variable.len())
}
pub(super) fn allocation_vector(
context: NodeScanContext<'_>,
) -> Result<AdmittedVec<Box<dyn GraphReadAllocation>>> {
let account = context.memory_account.sibling(
QueryMemoryClass::ExternalRead,
"full node scan permit storage",
context.memory_account.budget_bytes(),
);
let mut allocate = |bytes| admit_graph_read(&account, context.task_context, bytes);
AdmittedVec::new(&GraphReadAdmission::new(&mut allocate))
}
struct ScanVisit {
control: ScanControl,
fallback_label: Option<Option<LabelId>>,
}
fn visit_full_nodes(
spec: NodeScanSpec<'_>,
context: NodeScanContext<'_>,
observer: &dyn ExecutionObserver,
admit: &mut ControlledGraphReadAllocator<'_>,
consume: &mut dyn FnMut(NodeRecord, Box<dyn GraphReadAllocation>) -> Result<ScanControl>,
) -> Result<ScanVisit> {
let exact_label = exact_scan_label_id(context.catalog, spec.label);
if !context
.task_context
.is_some_and(RuntimeTaskContext::has_work_limit)
&& !context.store.is_out_of_core()
&& let Some(label_id) = exact_label
&& context
.store
.node_count_for_label(label_id)
.saturating_mul(std::mem::size_of::<&NodeRecord>())
<= context.memory_account.budget_bytes().get()
{
let scan = context.pruned_scan(label_id, spec.property_filter)?;
observer.record_scan_pruning_report(scan.report.clone());
for node in scan.nodes {
runtime_checkpoint(context.task_context)?;
let Some(allocation) = admit.reserve(hawdb_core::ids::node_allocation_bytes(&node))?
else {
return Ok(ScanVisit {
control: ScanControl::Stop,
fallback_label: None,
});
};
if consume(node.into_owned(), allocation)? == ScanControl::Stop {
return Ok(ScanVisit {
control: ScanControl::Stop,
fallback_label: None,
});
}
}
return Ok(ScanVisit {
control: ScanControl::Continue,
fallback_label: None,
});
}
let label_id = exact_label.flatten();
let control = context
.store
.visit_nodes_with_allocation(label_id, admit, &mut |input| {
let (node, allocation) = input.into_parts();
consume(node, allocation)
})?;
Ok(ScanVisit {
control,
fallback_label: Some(label_id),
})
}
fn matches_scan(spec: NodeScanSpec<'_>, context: NodeScanContext<'_>, node: &NodeRecord) -> bool {
let label_ids = label_ids_for_pattern(context.catalog, spec.label);
node_matches_label_pattern(node, label_ids.as_deref())
&& spec
.property_filter
.is_none_or(|filter| node_matches_property_filter(node, filter))
}
fn record_fallback(
visit: &ScanVisit,
context: NodeScanContext<'_>,
observer: &dyn ExecutionObserver,
emitted: usize,
) {
if let Some(label_id) = visit.fallback_label {
let candidate_count = context.store.node_count_for_label(label_id);
observer.record_scan_pruning_report(ScanPruningReport {
target_kind: ScanPruningTargetKind::Node,
label_id,
rel_type_id: None,
strategy: ScanPruningStrategy::FullLabelScan,
pruned: false,
exact_empty: candidate_count == 0,
candidate_count_before_pruning: candidate_count,
pruned_candidate_count: 0,
candidate_count_before_filter: candidate_count,
output_count: emitted,
filtered_out_count: candidate_count.saturating_sub(emitted),
});
}
}
struct OwnedNodeBatch<'plan, 'context, 'consumer> {
spec: NodeScanSpec<'plan>,
context: NodeScanContext<'context>,
predicate: &'consumer mut dyn FnMut(&Binding) -> Result<bool>,
emit: &'consumer mut dyn FnMut(BindingBatch) -> Result<BatchControl>,
batch: AccountedBindingBatch,
allocations: AdmittedVec<Box<dyn GraphReadAllocation>>,
emitted: usize,
stopped: bool,
}
impl OwnedNodeBatch<'_, '_, '_> {
fn flush(&mut self) -> Result<()> {
let result = self.batch.emit(self.emit);
self.allocations.clear();
self.stopped |= result? == BatchControl::Stop;
Ok(())
}
fn admit(&mut self, node_bytes: usize) -> Result<Option<Box<dyn GraphReadAllocation>>> {
runtime_checkpoint(self.context.task_context)?;
if node_bytes == 0 {
return if self.stopped {
Ok(None)
} else {
admit_graph_read(self.context.memory_account, self.context.task_context, 0)
.map(Some)
};
}
let bytes = node_bytes.saturating_add(binding_overhead(self.spec.variable));
if !self.stopped
&& (!self.context.memory_account.can_reserve(bytes)
|| self.batch.would_exceed_payload(bytes))
{
self.flush()?;
}
if self.stopped {
return Ok(None);
}
admit_graph_read(
self.context.memory_account,
self.context.task_context,
bytes,
)
.map(Some)
}
fn push(
&mut self,
node: NodeRecord,
allocation: Box<dyn GraphReadAllocation>,
) -> Result<ScanControl> {
runtime_checkpoint(self.context.task_context)?;
if !matches_scan(self.spec, self.context, &node) {
return Ok(ScanControl::Continue);
}
let binding = node_binding(self.spec.variable, node);
if !(self.predicate)(&binding)? {
return Ok(ScanControl::Continue);
}
self.batch.check_row_size(binding_memory_bytes(&binding))?;
self.allocations.try_push(allocation)?;
if self.batch.push(binding, self.emit)? == BatchControl::Stop {
self.stopped = true;
return Ok(ScanControl::Stop);
}
self.emitted = self.emitted.saturating_add(1);
if self.batch.is_full() {
self.flush()?;
}
self.stopped |= self.context.execution_limit.is_reached(self.emitted);
Ok(if self.stopped {
ScanControl::Stop
} else {
ScanControl::Continue
})
}
}
pub(super) fn stream(
spec: NodeScanSpec<'_>,
context: NodeScanContext<'_>,
predicate: &mut dyn FnMut(&Binding) -> Result<bool>,
observer: &dyn ExecutionObserver,
emit: &mut dyn FnMut(BindingBatch) -> Result<BatchControl>,
) -> Result<BatchControl> {
let source_account = context.source_account();
let context = NodeScanContext {
memory_account: &source_account,
..context
};
let state = RefCell::new(OwnedNodeBatch {
spec,
context,
predicate,
emit,
batch: context.output_batch("NodeScanExec"),
allocations: allocation_vector(context)?,
emitted: 0,
stopped: false,
});
let visit = visit_full_nodes(
spec,
context,
observer,
&mut ControlledGraphReadAllocator::new(
&mut |bytes| state.borrow_mut().admit(bytes),
context.task_context,
),
&mut |node, allocation| state.borrow_mut().push(node, allocation),
)?;
let mut state = state.into_inner();
record_fallback(&visit, context, observer, state.emitted);
state.flush()?;
Ok(if state.stopped || visit.control == ScanControl::Stop {
BatchControl::Stop
} else {
BatchControl::Continue
})
}
pub(super) fn materialize(
spec: NodeScanSpec<'_>,
context: NodeScanContext<'_>,
predicate: &mut dyn FnMut(&Binding) -> Result<bool>,
observer: &dyn ExecutionObserver,
) -> Result<Vec<Binding>> {
let source_account = context.source_account();
let context = NodeScanContext {
memory_account: &source_account,
..context
};
let mut output = Vec::new();
let mut allocations = allocation_vector(context)?;
let mut tracker = OperatorMemoryTracker::new(context.memory_account.budget_bytes());
let visit = visit_full_nodes(
spec,
context,
observer,
&mut ControlledGraphReadAllocator::new(
&mut |bytes| {
admit_graph_read(
context.memory_account,
context.task_context,
bytes.saturating_add(binding_overhead(spec.variable)),
)
.map(Some)
},
context.task_context,
),
&mut |node, allocation| {
runtime_checkpoint(context.task_context)?;
if !matches_scan(spec, context, &node) {
return Ok(ScanControl::Continue);
}
let binding = node_binding(spec.variable, node);
if !predicate(&binding)? {
return Ok(ScanControl::Continue);
}
allocations.try_push(allocation)?;
push_bounded_operator_binding("NodeScanExec", &mut output, binding, &mut tracker)?;
Ok(if context.execution_limit.is_reached(output.len()) {
ScanControl::Stop
} else {
ScanControl::Continue
})
},
)?;
record_fallback(&visit, context, observer, output.len());
Ok(output)
}
pub(super) fn stream_visited(
spec: NodeScanSpec<'_>,
context: NodeScanContext<'_>,
predicate: &mut dyn FnMut(&Binding) -> Result<bool>,
emit: &mut dyn FnMut(BindingBatch) -> Result<BatchControl>,
visit: impl FnOnce(
&mut ControlledGraphReadAllocator<'_>,
&mut dyn FnMut(hawdb_storage::read_view::AdmittedNodeRecord) -> Result<ScanControl>,
) -> Result<ScanControl>,
) -> Result<BatchControl> {
let source_account = context.source_account();
let context = NodeScanContext {
memory_account: &source_account,
..context
};
let state = RefCell::new(OwnedNodeBatch {
spec,
context,
predicate,
emit,
batch: context.output_batch("IndexNodeScanExec"),
allocations: allocation_vector(context)?,
emitted: 0,
stopped: false,
});
let control = visit(
&mut ControlledGraphReadAllocator::new(
&mut |bytes| state.borrow_mut().admit(bytes),
context.task_context,
),
&mut |input| {
let (node, allocation) = input.into_parts();
state.borrow_mut().push(node, allocation)
},
)?;
let mut state = state.into_inner();
state.flush()?;
Ok(if state.stopped || control == ScanControl::Stop {
BatchControl::Stop
} else {
BatchControl::Continue
})
}