use crate::api::{RedDBError, RedDBResult};
use crate::RedDB;
use reddb_types::Value;
const RED_CONFIG_COLLECTION: &str = "red_config";
const CONFIG_KEY: &str = "stream.executor.max_materialized_rows";
pub(crate) const DEFAULT_MAX_MATERIALIZED_ROWS: usize = 1_000_000;
pub(crate) fn max_materialized_rows(db: &RedDB) -> RedDBResult<usize> {
let mut newest: Option<(u64, u64)> = None;
if let Some(manager) = db.store().get_collection(RED_CONFIG_COLLECTION) {
manager.try_for_each_entity(usize::MAX, |entity| {
let Some(row) = entity.data.as_row() else {
return true;
};
let Some(Value::Text(key)) = row.get_field("key") else {
return true;
};
if !key.eq_ignore_ascii_case(CONFIG_KEY) {
return true;
}
let parsed: Option<u64> = match row.get_field("value") {
Some(Value::Integer(v)) if *v >= 0 => Some(*v as u64),
Some(Value::UnsignedInteger(v)) => Some(*v),
Some(Value::Float(v)) if *v >= 0.0 => Some(*v as u64),
Some(Value::Text(text)) => text.trim().parse().ok(),
_ => None,
};
if let Some(v) = parsed {
let id = entity.id.raw();
if newest.is_none_or(|(best_id, _)| id >= best_id) {
newest = Some((id, v));
}
}
true
})?;
}
Ok(match newest {
None => DEFAULT_MAX_MATERIALIZED_ROWS,
Some((_, 0)) => usize::MAX, Some((_, v)) => v as usize,
})
}
pub(crate) fn check(executor: &'static str, current: usize, limit: usize) -> RedDBResult<()> {
if current > limit {
return Err(RedDBError::MaterializationLimitExceeded {
executor,
limit,
current,
});
}
Ok(())
}
pub(crate) fn guard(db: &RedDB, executor: &'static str, current: usize) -> RedDBResult<()> {
check(executor, current, max_materialized_rows(db)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{RedDBOptions, RedDBRuntime};
fn mk_runtime() -> RedDBRuntime {
RedDBRuntime::with_options(RedDBOptions::in_memory())
.expect("in-memory runtime should open")
}
#[test]
fn check_passes_at_or_below_limit() {
assert!(check("aggregation", 0, 10).is_ok());
assert!(check("aggregation", 10, 10).is_ok());
}
#[test]
fn check_fails_just_past_limit_and_names_executor() {
let err = check("sort", 11, 10).unwrap_err();
match err {
RedDBError::MaterializationLimitExceeded {
executor,
limit,
current,
} => {
assert_eq!(executor, "sort");
assert_eq!(limit, 10);
assert_eq!(current, 11);
}
other => panic!("expected MaterializationLimitExceeded, got {other:?}"),
}
}
#[test]
fn default_applies_when_key_absent() {
let rt = mk_runtime();
assert_eq!(
max_materialized_rows(&rt.db()).expect("default limit"),
DEFAULT_MAX_MATERIALIZED_ROWS
);
}
#[test]
fn configured_value_is_read_back() {
let rt = mk_runtime();
rt.execute_query("SET CONFIG stream.executor.max_materialized_rows = 42")
.expect("set config ok");
assert_eq!(
max_materialized_rows(&rt.db()).expect("configured limit"),
42
);
}
#[test]
fn zero_means_unbounded() {
let rt = mk_runtime();
rt.execute_query("SET CONFIG stream.executor.max_materialized_rows = 0")
.expect("set config ok");
assert_eq!(
max_materialized_rows(&rt.db()).expect("unbounded config"),
usize::MAX
);
}
fn cold_limit_fixture() -> (tempfile::TempDir, crate::RedDBOptions, RedDBRuntime) {
let directory = tempfile::tempdir().expect("cold limit fixture");
let options = RedDBOptions::persistent(directory.path().join("limits.rdb"))
.with_memory_budget(64 * 1024 * 1024)
.with_storage_profile(
crate::storage::profile::StorageDeployPreset::PrimaryReplicaDev.selection(),
)
.expect("profile");
let runtime = RedDBRuntime::with_options(options.clone()).expect("seed runtime");
for sql in [
"CREATE TABLE limit_groups (cohort INTEGER)",
"INSERT INTO limit_groups (cohort) VALUES (1), (2), (3)",
"SET CONFIG stream.executor.max_materialized_rows = 1",
"SET CONFIG stream.executor.max_materialized_rows = 2",
] {
runtime.execute_query(sql).expect("seed limit and groups");
}
runtime.flush().expect("persist configured limit");
drop(runtime);
let runtime = cold_open(&options);
assert!(runtime
.db()
.store()
.get_collection(RED_CONFIG_COLLECTION)
.expect("config collection")
.has_backing());
(directory, options, runtime)
}
fn cold_open(options: &RedDBOptions) -> RedDBRuntime {
crate::storage::RedDB::with_disk_residency_for_qualification(|| {
RedDBRuntime::with_options(options.clone())
})
.expect("cold runtime")
}
#[test]
fn cold_group_limit_survives_reopen_and_overlay_update() {
let (_directory, options, runtime) = cold_limit_fixture();
let sql = "SELECT cohort, COUNT(*) FROM limit_groups GROUP BY cohort";
let error = runtime
.execute_query(sql)
.expect_err("cold configured ceiling applies");
assert!(
matches!(
error,
RedDBError::MaterializationLimitExceeded {
executor: "aggregation",
limit: 2,
current: 3,
}
),
"{error:?}"
);
runtime
.execute_query("SET CONFIG stream.executor.max_materialized_rows = 4")
.expect("newest overlay limit");
assert_eq!(
runtime
.execute_query(sql)
.expect("raised limit")
.result
.records
.len(),
3
);
runtime.flush().expect("persist new limit");
drop(runtime);
let reopened = cold_open(&options);
assert_eq!(
reopened
.execute_query(sql)
.expect("persisted new limit")
.result
.records
.len(),
3
);
}
#[test]
fn cold_group_limit_page_failure_does_not_relax_admission() {
use crate::storage::engine::{Page, PageType};
let (_directory, _options, runtime) = cold_limit_fixture();
let store = runtime.db().store();
let root = store
.get_collection(RED_CONFIG_COLLECTION)
.expect("config")
.backing_root_page()
.expect("config root");
let pager = store.pager().expect("fixture pager");
let original = pager.read_page(root).expect("config page");
pager
.write_page(root, Page::new(PageType::Overflow, root))
.expect("inject page failure");
let result = guard(&runtime.db(), "aggregation", 1);
pager
.write_page(root, original)
.expect("restore fixture before assertions");
let error = result.expect_err("unreadable config must not select the default ceiling");
assert!(
!matches!(error, RedDBError::MaterializationLimitExceeded { .. }),
"{error:?}"
);
guard(&runtime.db(), "aggregation", 1).expect("restored config read");
}
}