use std::collections::BTreeMap;
use std::sync::Arc;
use rushdb_lmpg::plan::FieldChange;
use rushdb_lmpg::Lmpg;
use rushdb_query::{aggregate_with_cancel_in_view, AggregateRequest, GroupBucket};
use rushdb_storage::read_accounting;
use rushdb_storage::Store;
use rushdb_types::ids::TenantId;
use serde_json::{json, Value};
fn store() -> (Arc<Store>, Lmpg) {
let store = Arc::new(Store::new_in_memory());
let lmpg = Lmpg::new(store.clone(), TenantId::single());
(store, lmpg)
}
fn put(lmpg: &Lmpg, label: &str, fields: Vec<(&str, Value)>) {
lmpg.create_record(
label,
None,
fields
.into_iter()
.map(|(name, value)| (name.to_owned(), FieldChange::Set(value)))
.collect(),
)
.unwrap();
}
fn grouped_count(property: &str) -> AggregateRequest {
AggregateRequest {
labels: vec!["Trip".into()],
group_by: vec![property.into()],
counts: vec![("n".into(), "*".into())],
..Default::default()
}
}
fn grouped_count_by_records(property: &str) -> AggregateRequest {
AggregateRequest {
counts: vec![("n".into(), "*".into()), ("m".into(), "zzz".into())],
..grouped_count(property)
}
}
fn by_key(buckets: &[GroupBucket], key: &str) -> BTreeMap<String, u64> {
buckets
.iter()
.map(|b| (b.keys[key].to_string(), b.count_roots))
.collect()
}
fn run(store: &Arc<Store>, lmpg: &Lmpg, request: &AggregateRequest) -> (Vec<GroupBucket>, u64) {
let view = Arc::new(store.read_view().unwrap());
aggregate_with_cancel_in_view(lmpg, request, &|| false, view).unwrap()
}
#[test]
fn grouped_counts_from_the_dictionary_equal_the_record_loop() {
let (store, lmpg) = store();
for i in 0..60u32 {
let mut fields: Vec<(&str, Value)> = vec![
("vendor", json!(if i % 3 == 0 { "A" } else { "B" })),
("hour", json!(i % 5)),
("fare", json!(f64::from(i % 4) * 2.5)),
("paid", json!(i % 2 == 0)),
("at", json!(format!("2026-01-{:02}T00:00:00Z", i % 3 + 1))),
];
if i % 7 == 0 {
fields.push(("tip", Value::Null));
} else if i % 7 < 4 {
fields.push(("tip", json!(i % 2)));
}
put(&lmpg, "Trip", fields);
}
for i in 0..9 {
put(
&lmpg,
"Other",
vec![("vendor", json!("C")), ("hour", json!(i))],
);
}
for property in ["vendor", "hour", "fare", "paid", "at", "tip", "nothing"] {
let (fast, fast_total) = run(&store, &lmpg, &grouped_count(property));
let (slow, slow_total) = run(&store, &lmpg, &grouped_count_by_records(property));
assert_eq!(fast_total, 60, "{property}");
assert_eq!(slow_total, 60, "{property}");
assert_eq!(
by_key(&fast, property),
by_key(&slow, property),
"{property}: dictionary {fast:?} vs records {slow:?}"
);
}
let (tip, _) = run(&store, &lmpg, &grouped_count("tip"));
let tip = by_key(&tip, "tip");
assert_eq!(tip["null"], 9);
assert_eq!(tip["\"__missing__\""], 24);
assert_eq!(tip["0"] + tip["1"], 27);
}
#[test]
fn a_grouped_count_reads_no_record() {
let (store, lmpg) = store();
for i in 0..200u32 {
put(
&lmpg,
"Trip",
vec![
("vendor", json!(if i % 2 == 0 { "A" } else { "B" })),
("payload", json!("x".repeat(512))),
],
);
}
let view = Arc::new(store.read_view().unwrap());
read_accounting::enable(true);
let _ = read_accounting::take_local();
let (buckets, _) =
aggregate_with_cancel_in_view(&lmpg, &grouped_count("vendor"), &|| false, view).unwrap();
let reads: u64 = read_accounting::take_local().values().sum();
read_accounting::enable(false);
assert!(reads < 50, "{reads} backend reads for 200 records");
assert_eq!(by_key(&buckets, "vendor")["\"A\""], 100);
}
#[test]
fn arrays_long_values_and_type_drift_fall_back_to_the_record_loop() {
let (store, lmpg) = store();
put(
&lmpg,
"Trip",
vec![("tags", json!(["x", "y"])), ("mixed", json!(1))],
);
put(
&lmpg,
"Trip",
vec![("tags", json!(["x"])), ("mixed", json!("one"))],
);
put(&lmpg, "Trip", vec![("long", json!("l".repeat(4096)))]);
put(&lmpg, "Trip", vec![("long", json!("short"))]);
for property in ["tags", "mixed", "long"] {
let (fast, _) = run(&store, &lmpg, &grouped_count(property));
let (slow, _) = run(&store, &lmpg, &grouped_count_by_records(property));
assert_eq!(
by_key(&fast, property),
by_key(&slow, property),
"{property}"
);
}
let (tags, _) = run(&store, &lmpg, &grouped_count("tags"));
assert_eq!(by_key(&tags, "tags")["[\"x\",\"y\"]"], 1);
}
#[test]
fn a_grouped_count_orders_and_pages_its_rows() {
let (store, lmpg) = store();
for i in 0..30u32 {
put(&lmpg, "Trip", vec![("hour", json!(i % 3))]);
}
let request = AggregateRequest {
order_by: vec![("hour".into(), true)],
skip: 1,
limit: Some(1),
..grouped_count("hour")
};
let (rows, total) = run(&store, &lmpg, &request);
assert_eq!(total, 30);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].keys["hour"], json!(1));
assert_eq!(rows[0].count_roots, 10);
}