#[derive(Debug, Clone, PartialEq)]
pub struct AggregateSpec {
pub op: String,
pub column: Option<String>,
pub alias: String,
pub group_by: Vec<String>,
}
impl AggregateSpec {
pub fn new(op: impl Into<String>, alias: impl Into<String>) -> Self {
Self {
op: op.into(),
column: None,
alias: alias.into(),
group_by: Vec::new(),
}
}
pub fn column(mut self, column: impl Into<String>) -> Self {
self.column = Some(column.into());
self
}
pub fn group_by(mut self, column: impl Into<String>) -> Self {
self.group_by.push(column.into());
self
}
pub fn cache_key(&self, source_key: &str) -> String {
let mut key = format!("{source_key}#{}", self.op);
if let Some(column) = &self.column {
key.push_str(&format!("({column})"));
}
if !self.group_by.is_empty() {
key.push_str(&format!("/by:{}", self.group_by.join(",")));
}
key
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mocks::MockShell;
use crate::vista::Vista;
use ciborium::Value as CborValue;
fn text(s: &str) -> CborValue {
CborValue::Text(s.to_string())
}
fn source_key(conditions: &[(String, CborValue)]) -> String {
let vista = Vista::new("email_events", Box::new(MockShell::new()));
vista.index_key(conditions, None)
}
#[test]
fn a_bare_reduction_keys_on_its_op() {
let key = AggregateSpec::new("count", "total").cache_key(&source_key(&[]));
assert!(key.ends_with("#count"), "got {key}");
}
#[test]
fn the_reduced_column_is_part_of_the_key() {
let key = AggregateSpec::new("sum", "bytes")
.column("Size")
.cache_key(&source_key(&[]));
assert!(key.ends_with("#sum(Size)"), "got {key}");
}
#[test]
fn conditions_on_the_source_separate_two_aggregates() {
let spec = AggregateSpec::new("count", "n");
let opened = spec.cache_key(&source_key(&[("Event".into(), text("opened"))]));
let failed = spec.cache_key(&source_key(&[("Event".into(), text("failed"))]));
assert_ne!(opened, failed);
}
#[test]
fn source_term_order_does_not_change_the_key() {
let spec = AggregateSpec::new("count", "n");
let a = spec.cache_key(&source_key(&[
("Event".into(), text("opened")),
("Ip".into(), text("1.2.3.4")),
]));
let b = spec.cache_key(&source_key(&[
("Ip".into(), text("1.2.3.4")),
("Event".into(), text("opened")),
]));
assert_eq!(a, b);
}
#[test]
fn group_order_does_change_the_key() {
let key = source_key(&[]);
let a = AggregateSpec::new("count", "n")
.group_by("a")
.group_by("b")
.cache_key(&key);
let b = AggregateSpec::new("count", "n")
.group_by("b")
.group_by("a")
.cache_key(&key);
assert_ne!(a, b);
}
#[test]
fn the_alias_does_not_affect_the_key() {
let key = source_key(&[]);
assert_eq!(
AggregateSpec::new("count", "total").cache_key(&key),
AggregateSpec::new("count", "observed").cache_key(&key),
);
}
#[test]
fn a_derived_key_cannot_collide_with_its_source() {
let key = source_key(&[]);
let derived = AggregateSpec::new("count", "total").cache_key(&key);
assert!(derived.starts_with(&key));
assert_ne!(derived, key);
}
}