use super::*;
const COMPACT_AGGREGATE_ROW: &str = "__hawdb_compact_aggregate";
pub(super) fn encode_compact_group_binding(
key: Vec<Value>,
inputs: Vec<AggregateInput>,
) -> Binding {
let inputs = inputs
.into_iter()
.map(encode_aggregate_input)
.collect::<Vec<_>>();
Binding::scalar(
COMPACT_AGGREGATE_ROW,
Value::List(vec![Value::List(key), Value::List(inputs)]),
)
}
pub(super) fn decode_compact_group_binding(
ordinal: u64,
mut binding: Binding,
expected_inputs: usize,
) -> Result<GroupRunRow> {
if !binding.nodes.is_empty() || !binding.relationships.is_empty() || binding.values.len() != 1 {
return Err(invalid_compact_row("has an invalid binding shape"));
}
let Some(Value::List(mut encoded)) = binding.values.remove(COMPACT_AGGREGATE_ROW) else {
return Err(invalid_compact_row("is missing its payload"));
};
if encoded.len() != 2 {
return Err(invalid_compact_row("has an invalid payload width"));
}
let Value::List(inputs) = encoded.pop().expect("compact payload width was checked") else {
return Err(invalid_compact_row("has an invalid input list"));
};
let Value::List(key) = encoded.pop().expect("compact payload width was checked") else {
return Err(invalid_compact_row("has an invalid group key"));
};
if inputs.len() != expected_inputs {
return Err(invalid_compact_row("has an invalid input width"));
}
let inputs = inputs
.into_iter()
.map(decode_aggregate_input)
.collect::<Result<Vec<_>>>()?;
Ok(GroupRunRow {
key,
ordinal,
inputs,
})
}
fn encode_aggregate_input(input: AggregateInput) -> Value {
let fields = match input {
AggregateInput::Missing => vec![Value::Int(0)],
AggregateInput::Present => vec![Value::Int(1)],
AggregateInput::Identity(kind, id) => vec![
Value::Int(2),
Value::Int(i64::from(kind)),
Value::Int((id >> u32::BITS) as i64),
Value::Int((id & u64::from(u32::MAX)) as i64),
],
AggregateInput::Value(value) => vec![Value::Int(3), value],
};
Value::List(fields)
}
fn decode_aggregate_input(value: Value) -> Result<AggregateInput> {
let Value::List(mut fields) = value else {
return Err(invalid_compact_row("contains a non-list input"));
};
let Some(Value::Int(tag)) = fields.first() else {
return Err(invalid_compact_row("contains an untagged input"));
};
match *tag {
0 if fields.len() == 1 => Ok(AggregateInput::Missing),
1 if fields.len() == 1 => Ok(AggregateInput::Present),
2 if fields.len() == 4 => {
let (Value::Int(kind), Value::Int(high), Value::Int(low)) =
(&fields[1], &fields[2], &fields[3])
else {
return Err(invalid_compact_row("contains an invalid identity input"));
};
let kind = u8::try_from(*kind)
.map_err(|_| invalid_compact_row("contains an invalid identity kind"))?;
let high = u32::try_from(*high)
.map_err(|_| invalid_compact_row("contains invalid identity high bits"))?;
let low = u32::try_from(*low)
.map_err(|_| invalid_compact_row("contains invalid identity low bits"))?;
Ok(AggregateInput::Identity(
kind,
(u64::from(high) << u32::BITS) | u64::from(low),
))
}
3 if fields.len() == 2 => Ok(AggregateInput::Value(fields.pop().expect("value exists"))),
_ => Err(invalid_compact_row(
"contains an invalid input tag or width",
)),
}
}
fn invalid_compact_row(reason: &str) -> HawDBError {
HawDBError::Execution(format!("AggregateExec compact spill record {reason}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compact_group_binding_round_trip_preserves_all_input_kinds() {
let row = decode_compact_group_binding(
7,
encode_compact_group_binding(
vec![Value::String("group".to_string())],
vec![
AggregateInput::Missing,
AggregateInput::Present,
AggregateInput::Identity(2, u64::MAX),
AggregateInput::Value(Value::Int(42)),
],
),
4,
)
.unwrap();
assert_eq!(row.ordinal, 7);
assert_eq!(row.key, vec![Value::String("group".to_string())]);
assert!(matches!(row.inputs[0], AggregateInput::Missing));
assert!(matches!(row.inputs[1], AggregateInput::Present));
assert!(matches!(
row.inputs[2],
AggregateInput::Identity(2, u64::MAX)
));
assert!(matches!(
row.inputs[3],
AggregateInput::Value(Value::Int(42))
));
}
}