use super::*;
use crate::expression::with_legacy_map_keys;
use crate::observer::NoopExecutionObserver;
use hawdb_core::{LabelId, RelTypeId};
use hawdb_plan_cypher::ProjectionExpression;
use hawdb_storage::{NodeId, NodeRecord, RelId, RelRecord};
use std::sync::Arc;
struct Rows(Vec<BindingBatch>);
impl BindingBatchSource for Rows {
fn execute(
&mut self,
_input: &PhysicalPlan,
_execution_limit: ExecutionLimit,
emit: &mut dyn FnMut(BindingBatch) -> Result<BatchControl>,
) -> Result<BatchControl> {
for batch in std::mem::take(&mut self.0) {
if emit(batch)? == BatchControl::Stop {
return Ok(BatchControl::Stop);
}
}
Ok(BatchControl::Continue)
}
}
fn catalog() -> (Catalog, LabelId) {
let mut catalog = Catalog::default();
let label = catalog.get_or_create_label("Item");
(catalog, label)
}
fn node(id: u64, value: i64) -> NodeRecord {
NodeRecord {
id: NodeId(id),
labels: [catalog().1].into_iter().collect(),
properties: BTreeMap::from([
("v".to_string(), Value::Int(value)),
("text".to_string(), Value::String("t".repeat(64))),
]),
}
}
fn row(node: NodeRecord) -> Binding {
let mut binding = Binding::values(BTreeMap::new());
binding.nodes.insert("n".to_string(), node);
binding
}
fn group_by_n(function: AggregateFunction) -> (Vec<Projection>, Vec<Aggregation>) {
(
vec![Projection {
expression: ProjectionExpression::Variable {
variable: "n".to_string(),
},
name: "n".to_string(),
}],
vec![Aggregation {
function,
target: match function {
AggregateFunction::Collect => AggregateTarget::Property {
variable: "n".to_string(),
property: "v".to_string(),
},
_ => AggregateTarget::All,
},
distinct: false,
name: "out".to_string(),
}],
)
}
struct Run {
rows: Result<Vec<Binding>>,
used_bytes: usize,
peak_bytes: usize,
}
fn run(
rows: Vec<Binding>,
function: AggregateFunction,
graph_keys: GraphGroupKeys,
memory: &ExecutionMemoryConfig,
) -> Run {
let (group_keys, items) = group_by_n(function);
let (catalog, _) = catalog();
let ledger = QueryMemoryLedger::new(memory.query_memory_bytes);
let mut output = Vec::new();
let result = stream_grouped_aggregate_batches(
&PhysicalPlan::EmptyExec,
&group_keys,
&items,
graph_keys,
&mut Rows(rows.chunks(4).map(<[Binding]>::to_vec).collect()),
BlockingExecutionContext {
catalog: &catalog,
memory,
memory_ledger: &ledger,
task_context: None,
observer: &NoopExecutionObserver,
graph: None,
},
ExecutionLimit::unlimited(),
&mut |batch| {
output.extend(batch);
Ok(BatchControl::Continue)
},
);
let snapshot = ledger.snapshot();
Run {
rows: result.map(|_| output),
used_bytes: snapshot.used_bytes,
peak_bytes: snapshot.peak_bytes,
}
}
#[test]
fn shared_records_leave_uncopied_and_charge_each_group_once() {
let memory = ExecutionMemoryConfig::default();
let first = row(node(7, 1));
for function in [AggregateFunction::Count, AggregateFunction::Collect] {
for graph_keys in [GraphGroupKeys::Values, GraphGroupKeys::Slots] {
let context = format!("{function:?} {graph_keys:?}");
let single = run(vec![first.clone()], function, graph_keys, &memory);
let many = run(vec![first.clone(); 64], function, graph_keys, &memory);
assert_eq!((single.used_bytes, many.used_bytes), (0, 0), "{context}");
let rows = many.rows.unwrap();
assert_eq!(rows.len(), 1, "{context}");
if graph_keys == GraphGroupKeys::Slots {
let record = rows[0].nodes.get_shared("n").expect("slot output");
assert!(
Arc::ptr_eq(record, first.nodes.get_shared("n").unwrap()),
"{context}"
);
assert!(!rows[0].values.contains_key("n"), "{context}");
} else {
assert!(rows[0].nodes.is_empty(), "{context}");
assert_eq!(
rows[0].values["n"],
crate::expression::node_value(first.nodes.get("n").unwrap(), &catalog().0),
"{context}"
);
}
if function == AggregateFunction::Count {
assert_eq!(single.peak_bytes, many.peak_bytes, "{context}");
}
let exact = ExecutionMemoryConfig {
query_memory_bytes: NonZeroUsize::new(many.peak_bytes).unwrap(),
..memory.clone()
};
let fitted = run(vec![first.clone(); 64], function, graph_keys, &exact);
assert_eq!(fitted.rows.unwrap(), rows, "{context}");
assert_eq!((fitted.used_bytes, fitted.peak_bytes), (0, many.peak_bytes));
let below = ExecutionMemoryConfig {
query_memory_bytes: NonZeroUsize::new(many.peak_bytes - 1).unwrap(),
..memory.clone()
};
let refused = run(vec![first.clone(); 64], function, graph_keys, &below);
let error = refused.rows.unwrap_err();
assert!(
matches!(error, HawDBError::ResourceBudgetExceeded(_))
|| error.to_string().contains(&format!(
"exceeding query_memory_bytes {}",
many.peak_bytes - 1
)),
"{context}: {error:?}"
);
assert_eq!(refused.used_bytes, 0, "{context}");
}
}
}
#[test]
fn divergent_records_of_one_entity_form_one_group_with_the_first_record() {
let memory = ExecutionMemoryConfig::default();
let rows = vec![
row(node(7, 1)),
row(node(7, 2)),
row(node(8, 1)),
row(node(7, 3)),
];
for graph_keys in [GraphGroupKeys::Values, GraphGroupKeys::Slots] {
let output = run(rows.clone(), AggregateFunction::Count, graph_keys, &memory)
.rows
.unwrap();
let groups = output
.iter()
.map(|binding| {
let record = match graph_keys {
GraphGroupKeys::Values => {
let Value::Map(values) = &binding.values["n"] else {
panic!("expected a node map");
};
(values["_id"].clone(), values["v"].clone())
}
_ => {
let node = binding.nodes.get("n").unwrap();
(Value::Int(node.id.0 as i64), node.properties["v"].clone())
}
};
(record, binding.values["out"].clone())
})
.collect::<Vec<_>>();
assert_eq!(
groups,
vec![
((Value::Int(7), Value::Int(1)), Value::Int(3)),
((Value::Int(8), Value::Int(1)), Value::Int(1)),
],
"{graph_keys:?}"
);
}
let legacy = with_legacy_map_keys(|| {
run(
rows,
AggregateFunction::Count,
GraphGroupKeys::Values,
&memory,
)
});
assert_eq!(legacy.rows.unwrap().len(), 4);
}
#[test]
fn spill_key_codec_round_trips_records_and_refuses_misplaced_slots() {
let node = Arc::new(node(3, 9));
let relationship = Arc::new(RelRecord {
id: RelId(u64::MAX),
source: NodeId(1),
target: NodeId(2),
rel_type: RelTypeId(4),
properties: BTreeMap::from([("w".to_string(), Value::Float(0.5))]),
});
let key = vec![
GroupKeyValue::Value(Value::Null),
GroupKeyValue::Node(Arc::clone(&node)),
GroupKeyValue::Value(Value::List(vec![Value::Null])),
GroupKeyValue::Relationship(Arc::clone(&relationship)),
];
let encoded = compact::encode_compact_group_binding(key.clone(), vec![AggregateInput::Present]);
let encoded = crate::spill::round_trip_binding_for_tests(&encoded).unwrap();
let decoded = compact::decode_compact_group_binding(5, encoded.clone(), 1).unwrap();
assert_eq!(decoded.key, key);
let GroupKeyValue::Node(decoded_node) = &decoded.key[1] else {
panic!("expected a node key");
};
assert_eq!(decoded_node.as_ref(), node.as_ref());
let GroupKeyValue::Relationship(decoded_relationship) = &decoded.key[3] else {
panic!("expected a relationship key");
};
assert_eq!(decoded_relationship.as_ref(), relationship.as_ref());
assert_eq!(decoded.key[0], GroupKeyValue::Value(Value::Null));
for (name, reason) in [
("4", "outside its key"),
("2", "non-null key value"),
("01", "outside"),
] {
let mut invalid = encoded.clone();
invalid
.nodes
.insert(name.to_string(), node.as_ref().clone());
let error = compact::decode_compact_group_binding(5, invalid, 1)
.err()
.expect("misplaced slot");
assert!(error.to_string().contains(reason), "{name}: {error}");
}
let mut collision = encoded;
collision
.nodes
.insert("3".to_string(), node.as_ref().clone());
assert!(compact::decode_compact_group_binding(5, collision, 1).is_err());
}