use super::*;
use crate::external::NoExternalReadOperator;
use hawdb_plan_cypher::GraphAlgorithmKind;
use std::cell::Cell;
mod boundaries;
mod capacity;
mod count_source;
mod dispatch;
mod fixtures;
mod graph_match;
mod handlers;
mod source_memory;
mod store;
fn with_context<T>(
task_context: Option<&RuntimeTaskContext>,
run: impl FnOnce(BatchReadContext<'_>) -> T,
) -> T {
let mut catalog = Catalog::default();
let label = catalog.get_or_create_label("Memory");
let rel_type = catalog.get_or_create_rel_type("MENTIONS");
let store = store::ReadFixture {
nodes: (0..2)
.map(|id| NodeRecord {
id: NodeId(id),
labels: [label].into_iter().collect(),
properties: BTreeMap::from([("id".into(), Value::Int(id as i64 + 1))]),
})
.collect(),
relationships: vec![hawdb_storage::RelRecord {
id: hawdb_storage::RelId(0),
source: NodeId(0),
target: NodeId(1),
rel_type,
properties: BTreeMap::new(),
}],
definition: Some(hawdb_storage::projection::ProjectedGraphDefinition {
node_labels: vec!["Memory".into()],
rel_types: vec!["MENTIONS".into()],
relationship_predicates: BTreeMap::new(),
}),
out_of_core: false,
..store::ReadFixture::default()
};
let memory = ExecutionMemoryConfig::default();
let ledger = QueryMemoryLedger::new(memory.query_memory_bytes);
let parameters = BTreeMap::new();
let mut external = NoExternalReadOperator;
let external = BatchExternalReadAdapter::new(&mut external);
let observer = QueryExecutionObserver::default();
let result = run(BatchReadContext {
catalog: &catalog,
store: &store,
memory: &memory,
memory_ledger: &ledger,
parameters: ¶meters,
external: &external,
observer: &observer,
host_scorer: None,
task_context,
});
assert_eq!(ledger.snapshot().used_bytes, 0);
result
}
#[test]
fn support_probe_does_not_execute_or_capture_runtime_state() {
let called = Cell::new(false);
assert!(BatchSupport.supported(|_, _, _| {
called.set(true);
panic!("a capability probe must not execute its handler")
}));
assert!(!called.get());
for (plan, supported) in fixtures::operators() {
assert_eq!(dispatch_batch_operator(&plan, BatchSupport), supported);
}
}
#[test]
fn unsupported_operator_dispatch_returns_an_error_instead_of_panicking() {
let plan = PhysicalPlan::CreateNode {
label: "Item".to_string(),
properties: BTreeMap::new(),
};
let error = with_context(None, |context| {
execute_binding_batches_inner(
BatchPlanRef(&plan),
context,
ExecutionLimit::unlimited(),
&mut |_| panic!("unsupported output"),
)
})
.unwrap_err();
assert!(error
.to_string()
.contains("physical operator 'CreateNode' does not support batch execution"));
}
#[test]
fn graph_handler_checks_cancellation_inside_its_execution_boundary() {
for algorithm in [GraphAlgorithmKind::PageRank, GraphAlgorithmKind::Louvain] {
let plan = PhysicalPlan::GraphAlgorithm {
algorithm,
graph_name: "MemoryGraph".into(),
options: hawdb_plan_cypher::GraphAlgorithmOptions {
damping: None,
max_iterations: Some(2),
max_levels: Some(1),
..Default::default()
},
score_column: "score".into(),
return_node_identity: false,
node_visibility_predicate: None,
};
let cancellation = hawdb_core::RuntimeCancellationToken::new();
assert!(cancellation.cancel());
let task = RuntimeTaskContext::without_deadline(cancellation);
let result = with_context(Some(&task), |context| {
dispatch_batch_operator(
&plan,
BatchExecution {
context,
execution_limit: ExecutionLimit::unlimited(),
emit: &mut |_| panic!("cancelled graph handler emitted a row"),
},
)
});
assert_eq!(
result.unwrap_err(),
HawDBError::Execution("runtime task stopped: cancelled".to_string())
);
}
}
#[cfg(test)]
mod cancellation_tests {
use super::*;
#[test]
fn optional_relationship_count_checks_cancellation_without_matching_nodes() {
let mut catalog = Catalog::default();
let other = catalog.get_or_create_label("Other");
let store = store::ReadFixture {
nodes: vec![NodeRecord {
id: NodeId(0),
labels: [other].into_iter().collect(),
properties: BTreeMap::new(),
}],
..store::ReadFixture::default()
};
let plan = PhysicalPlan::OptionalRelationshipCountSumExec {
variable: "m".to_string(),
label: "Memory".to_string(),
properties: BTreeMap::new(),
legs: vec![RelationshipCountLeg {
rel_type: "HAS_MEMORY".to_string(),
direction: RelationshipDirection::Outgoing,
distinct: false,
filter: None,
}],
output: "count".to_string(),
};
let memory = ExecutionMemoryConfig {
batch_rows: NonZeroUsize::new(1).unwrap(),
..ExecutionMemoryConfig::default()
};
let memory_ledger = QueryMemoryLedger::new(memory.query_memory_bytes);
let parameters = BTreeMap::new();
let mut external_operator = NoExternalReadOperator;
let external = BatchExternalReadAdapter::new(&mut external_operator);
let observer = QueryExecutionObserver::default();
let cancellation = hawdb_core::RuntimeCancellationToken::new();
let task_context = RuntimeTaskContext::without_deadline(cancellation.clone());
assert!(cancellation.cancel());
let context = BatchReadContext {
catalog: &catalog,
store: &store,
parameters: ¶meters,
external: &external,
memory: &memory,
memory_ledger: &memory_ledger,
task_context: Some(&task_context),
observer: &observer,
host_scorer: None,
};
let error = dispatch_batch_operator(
&plan,
BatchExecution {
context,
execution_limit: ExecutionLimit::unlimited(),
emit: &mut |_| Ok(BatchControl::Continue),
},
)
.unwrap_err();
assert!(error
.to_string()
.contains("runtime task stopped: cancelled"));
}
}
#[cfg(test)]
mod byte_bounded_batch_tests {
use super::*;
#[test]
fn within_budget_batch_keeps_its_allocation() {
let batch = vec![Binding {
values: BTreeMap::from([("value".to_string(), Value::Int(1))]),
nodes: BTreeMap::new().into(),
relationships: BTreeMap::new().into(),
}];
let allocation = batch.as_ptr();
let observer = QueryExecutionObserver::default();
let memory = ExecutionMemoryConfig::default();
let memory_ledger = QueryMemoryLedger::new(memory.query_memory_bytes);
let memory_account = memory_ledger.account(
QueryMemoryClass::PipelineBatch,
"test batch",
memory.query_memory_bytes,
);
let mut emitted_allocation = None;
let control = emit_byte_bounded_batches(
batch,
usize::MAX,
&memory_account,
&observer,
&mut |emitted| {
emitted_allocation = Some(emitted.as_ptr());
Ok(BatchControl::Continue)
},
)
.unwrap();
assert_eq!(control, BatchControl::Continue);
assert_eq!(emitted_allocation, Some(allocation));
assert_eq!(observer.into_reports().pipeline_memory.intermediate_rows, 1);
}
#[test]
fn single_walk_emission_matches_separate_memory_and_payload_walks() {
let row = |index: usize| Binding {
values: BTreeMap::from([
("name".to_string(), Value::String("n".repeat(index * 40))),
(
"nested".to_string(),
Value::List(vec![
Value::Null,
Value::Map(BTreeMap::from([(
"inner".to_string(),
Value::List(vec![Value::Int(index as i64); index]),
)])),
]),
),
]),
nodes: BTreeMap::from([(
"n".to_string(),
NodeRecord {
id: NodeId(index as u64),
labels: Default::default(),
properties: BTreeMap::from([(
"body".to_string(),
Value::String("b".repeat(index)),
)]),
},
)])
.into(),
relationships: BTreeMap::new().into(),
};
let batch = (0..9).map(row).collect::<Vec<_>>();
let total = batch
.iter()
.map(crate::binding::binding_memory_bytes)
.sum::<usize>();
let largest = batch
.iter()
.map(crate::binding::binding_memory_bytes)
.max()
.unwrap();
for max_payload_bytes in [usize::MAX, total, total - 1, largest * 3, largest] {
let memory = ExecutionMemoryConfig::default();
let ledger = QueryMemoryLedger::new(memory.query_memory_bytes);
let account = ledger.account(
QueryMemoryClass::PipelineBatch,
"test batch",
memory.query_memory_bytes,
);
let observer = QueryExecutionObserver::default();
let expected = QueryExecutionObserver::default();
let mut expected_lengths = vec![0usize];
let mut open_bytes = 0usize;
for binding in &batch {
let bytes = crate::binding::binding_memory_bytes(binding);
if total > max_payload_bytes
&& *expected_lengths.last().unwrap() > 0
&& open_bytes + bytes > max_payload_bytes
{
expected_lengths.push(0);
open_bytes = 0;
}
open_bytes += bytes;
*expected_lengths.last_mut().unwrap() += 1;
}
let mut lengths = Vec::new();
let mut emitted = Vec::new();
let control = emit_byte_bounded_batches(
batch.clone(),
max_payload_bytes,
&account,
&observer,
&mut |output| {
let charged = output
.iter()
.map(crate::binding::binding_memory_bytes)
.sum::<usize>();
assert!(charged <= max_payload_bytes);
assert_eq!(ledger.snapshot().used_bytes, charged);
expected.record_pipeline_batch(&output);
lengths.push(output.len());
emitted.extend(output);
Ok(BatchControl::Continue)
},
)
.unwrap();
assert_eq!(control, BatchControl::Continue);
assert_eq!(emitted, batch, "budget {max_payload_bytes}");
assert_eq!(lengths, expected_lengths, "budget {max_payload_bytes}");
assert_eq!(
observer.into_reports().pipeline_memory,
expected.into_reports().pipeline_memory,
"budget {max_payload_bytes}"
);
assert_eq!(ledger.snapshot().used_bytes, 0);
}
}
}