use super::result::QueryRow;
use crate::{Error, Result};
pub(crate) fn estimate_query_row_bytes(row: &QueryRow) -> usize {
let values_bytes: usize = row
.values
.values()
.map(crate::memory::estimate_value_size)
.sum();
values_bytes.saturating_add(row.key.as_bytes().len())
}
pub(crate) fn estimate_result_bytes(rows: &[QueryRow]) -> usize {
rows.iter().fold(0usize, |acc, row| {
acc.saturating_add(estimate_query_row_bytes(row))
})
}
pub(crate) fn enforce_result_budget(
results: &[QueryRow],
result_bytes: usize,
byte_budget: usize,
max_rows: usize,
) -> Result<()> {
if result_bytes > byte_budget {
return Err(Error::ResultTooLarge {
budget_bytes: byte_budget,
estimated_bytes: result_bytes,
rows: results.len(),
});
}
if results.len() > max_rows {
return Err(Error::query_execution(
"Result set too large, consider adding LIMIT".to_string(),
));
}
Ok(())
}
pub(crate) fn enforce_materialized_rows(
rows: &[QueryRow],
byte_budget: usize,
max_rows: usize,
) -> Result<()> {
let result_bytes = estimate_result_bytes(rows);
enforce_result_budget(rows, result_bytes, byte_budget, max_rows)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::estimate_value_size;
use crate::query::result::{QueryRow, RowMetadata};
use crate::types::{RowKey, Value};
use std::collections::HashMap;
use std::sync::Arc;
fn row_with(key_bytes: usize, value: Value) -> QueryRow {
let mut values: HashMap<Arc<str>, Value> = HashMap::new();
values.insert(Arc::from("c"), value);
QueryRow {
values,
key: RowKey::new(vec![b'k'; key_bytes]),
metadata: RowMetadata::default(),
cell_metadata: None,
}
}
#[test]
fn wide_key_small_value_trips_budget() {
let rows = vec![row_with(1000, Value::Integer(1))];
let value_only = rows[0]
.values
.values()
.map(estimate_value_size)
.sum::<usize>();
assert_eq!(value_only, 4, "projected value estimate should be small");
let budget = 500;
assert!(value_only <= budget, "keys-ignored estimate would NOT trip");
let err = enforce_materialized_rows(&rows, budget, usize::MAX)
.expect_err("wide key must push the row over the byte budget");
match err {
Error::ResultTooLarge {
budget_bytes,
estimated_bytes,
rows: n,
} => {
assert_eq!(budget_bytes, budget);
assert!(estimated_bytes > budget, "estimate must include key bytes");
assert!(
estimated_bytes >= 1000,
"estimate must include the 1000-byte key"
);
assert_eq!(n, 1);
}
other => panic!("expected ResultTooLarge, got {other:?}"),
}
}
#[test]
fn small_key_small_value_passes() {
let rows = vec![row_with(8, Value::Integer(1))];
enforce_materialized_rows(&rows, 500, usize::MAX)
.expect("small key + value must stay under budget");
}
}