#![allow(clippy::unwrap_used, clippy::expect_used)]
use chronix::prelude::*;
use chronix::{Chronix, fields, tags};
use tempfile::TempDir;
const TINY_BUDGET: usize = 16 * 1024;
fn db_with_budget(dir: &TempDir, budget: usize) -> Chronix {
let config = ChronixConfig::builder()
.data_dir(dir.path())
.per_query_memory_limit(budget)
.max_series_cardinality(200_000)
.build()
.unwrap();
Chronix::open(config).unwrap()
}
fn seed(db: &Chronix, n: usize) {
let points: Vec<Point> = (0..n)
.map(|i| {
let key = SeriesKey::new("m", tags! { "host" => format!("h{i}").as_str() }).unwrap();
Point::new(key, fields! { "v" => i as f64 }, 1_000 + i as i64).unwrap()
})
.collect();
db.insert_batch(&points).unwrap().into_complete().unwrap();
}
fn grouped_plan(db: &Chronix) -> QueryPlan {
db.query()
.measurement("m")
.range(0, i64::MAX)
.group_by(&["host"])
.aggregate(AggFn::Avg)
.build()
.unwrap()
}
#[test]
fn execute_stream_enforces_the_per_query_memory_limit() {
let tmp = TempDir::new().unwrap();
let db = db_with_budget(&tmp, TINY_BUDGET);
seed(&db, 20_000);
let plan = grouped_plan(&db);
let err = db
.execute_stream(&plan)
.expect_err("a 20 000-group aggregate must not fit in a 16 KiB budget");
let msg = err.to_string();
assert!(
msg.contains("memory") || msg.contains("budget"),
"the error must name the budget, got: {msg}"
);
}
#[test]
fn both_query_paths_agree_that_the_budget_binds() {
let tmp = TempDir::new().unwrap();
let db = db_with_budget(&tmp, TINY_BUDGET);
seed(&db, 20_000);
let plan = grouped_plan(&db);
assert!(
db.execute(&plan).is_err(),
"execute() must refuse the query"
);
assert!(
db.execute_stream(&plan).is_err(),
"execute_stream() must refuse the same query"
);
}
#[test]
fn a_modest_group_by_still_succeeds_under_the_budget() {
let tmp = TempDir::new().unwrap();
let db = db_with_budget(&tmp, TINY_BUDGET);
seed(&db, 8);
let plan = grouped_plan(&db);
let batches = db
.execute_stream(&plan)
.expect("eight groups must fit comfortably");
let rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
assert_eq!(rows, 8, "one row per group");
}
#[test]
fn a_zero_budget_means_unlimited() {
let tmp = TempDir::new().unwrap();
let db = db_with_budget(&tmp, 0);
seed(&db, 5_000);
let plan = grouped_plan(&db);
let batches = db
.execute_stream(&plan)
.expect("a zero budget must not impose a limit");
let rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
assert_eq!(rows, 5_000);
}