use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
pub const MAX_UNBOUNDED_ROWS: usize = 10_000_000;
#[derive(Clone, Debug, Default)]
pub struct ExecutionBudget {
inner: Arc<BudgetInner>,
}
#[derive(Debug, Default)]
struct BudgetInner {
max_rows: Option<usize>,
collection_items: AtomicUsize,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Charge {
Materialized,
Work,
}
#[derive(Clone, Copy, Debug)]
pub struct MatchCeiling {
max: usize,
operator: &'static str,
}
impl MatchCeiling {
#[inline]
pub fn new(max: usize, operator: &'static str) -> Self {
Self { max, operator }
}
#[inline]
pub fn check(&self, held: usize) -> Result<(), String> {
if held <= self.max {
return Ok(());
}
Err(self.exceeded(held))
}
#[cold]
fn exceeded(&self, held: usize) -> String {
ExecutionBudget::backstop_message(held, "rows", self.operator, self.max)
}
}
impl ExecutionBudget {
#[inline]
pub fn new(max_rows: Option<usize>) -> Self {
Self {
inner: Arc::new(BudgetInner {
max_rows,
..BudgetInner::default()
}),
}
}
#[inline]
pub fn max_rows(&self) -> Option<usize> {
self.inner.max_rows
}
#[inline]
pub fn match_ceiling(&self, operator: &'static str) -> Option<MatchCeiling> {
self.inner
.max_rows
.is_none()
.then(|| MatchCeiling::new(MAX_UNBOUNDED_ROWS, operator))
}
#[inline]
pub fn check_rows(&self, rows: usize, operator: &str) -> Result<(), String> {
self.check(rows, "rows", operator, Charge::Materialized)
}
#[inline]
pub fn check_work(&self, units: usize, operator: &str) -> Result<(), String> {
self.check(units, "work units", operator, Charge::Work)
}
#[inline]
pub fn consume_collection(&self, items: usize, operator: &str) -> Result<(), String> {
self.consume(
&self.inner.collection_items,
items,
"collection items",
operator,
)
}
#[inline]
pub fn reserve_rows(
&self,
current: usize,
additional: usize,
operator: &str,
) -> Result<(), String> {
let total = current
.checked_add(additional)
.ok_or_else(|| format!("Query row count overflow while executing {operator}"))?;
self.check_rows(total, operator)
}
#[inline]
fn check(
&self,
actual: usize,
unit: &str,
operator: &str,
charge: Charge,
) -> Result<(), String> {
let Some(max) = self.inner.max_rows else {
return Self::check_backstop(actual, unit, operator, charge);
};
if actual > max {
return Err(format!(
"Query produced {actual} {unit} while executing {operator}, exceeding \
max_rows limit of {max}. Add a LIMIT clause or increase max_rows."
));
}
Ok(())
}
#[inline]
fn check_backstop(
actual: usize,
unit: &str,
operator: &str,
charge: Charge,
) -> Result<(), String> {
if charge == Charge::Work || actual <= MAX_UNBOUNDED_ROWS {
return Ok(());
}
Err(Self::backstop_message(
actual,
unit,
operator,
MAX_UNBOUNDED_ROWS,
))
}
fn backstop_message(actual: usize, unit: &str, operator: &str, ceiling: usize) -> String {
format!(
"Query materialized {actual} {unit} while executing {operator}, exceeding the \
safety ceiling of {ceiling} {unit} that applies when no max_rows \
is set. Add a LIMIT clause, or set an explicit max_rows (per query: \
max_rows=…; per graph or session: set_default_max_rows(…)) to choose your \
own ceiling."
)
}
fn consume(
&self,
counter: &AtomicUsize,
additional: usize,
unit: &str,
operator: &str,
) -> Result<(), String> {
let previous = counter
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
current.checked_add(additional)
})
.map_err(|_| format!("Query {unit} overflow while executing {operator}"))?;
let total = previous
.checked_add(additional)
.ok_or_else(|| format!("Query {unit} overflow while executing {operator}"))?;
let Some(max) = self.inner.max_rows else {
return Self::check_backstop(total, unit, operator, Charge::Materialized);
};
if total > max {
return Err(format!(
"Query consumed {total} {unit} while executing {operator}, exceeding \
max_rows limit of {max}. Add a LIMIT clause or increase max_rows."
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn limit_is_inclusive_and_overflow_is_rejected() {
let budget = ExecutionBudget::new(Some(2));
assert!(budget.check_rows(2, "test").is_ok());
assert!(budget.check_rows(3, "test").is_err());
assert!(budget.reserve_rows(usize::MAX, 1, "test").is_err());
assert!(budget.check_work(2, "test").is_ok());
assert!(budget.check_work(3, "test").is_err());
}
#[test]
fn unbounded_budget_backstops_rows_at_the_absolute_ceiling() {
let budget = ExecutionBudget::new(None);
assert!(budget.check_rows(MAX_UNBOUNDED_ROWS, "test").is_ok());
assert!(budget
.reserve_rows(MAX_UNBOUNDED_ROWS - 1, 1, "test")
.is_ok());
let err = budget
.check_rows(MAX_UNBOUNDED_ROWS + 1, "UNWIND")
.expect_err("row backstop must fire without max_rows");
assert!(err.contains("UNWIND"), "{err}");
assert!(err.contains(&MAX_UNBOUNDED_ROWS.to_string()), "{err}");
assert!(err.contains("max_rows"), "{err}");
assert!(budget
.reserve_rows(MAX_UNBOUNDED_ROWS, 1, "UNWIND")
.is_err());
assert!(budget.reserve_rows(usize::MAX, 1, "UNWIND").is_err());
}
#[test]
fn unbounded_budget_backstops_accumulated_collection_items() {
let budget = ExecutionBudget::new(None);
let chunk = MAX_UNBOUNDED_ROWS / 2;
assert!(budget.consume_collection(chunk, "range()").is_ok());
assert!(budget.consume_collection(chunk, "range()").is_ok());
let err = budget
.consume_collection(1, "range()")
.expect_err("collection backstop must fire without max_rows");
assert!(err.contains("collection items"), "{err}");
assert!(err.contains(&MAX_UNBOUNDED_ROWS.to_string()), "{err}");
assert!(budget.consume_collection(usize::MAX, "range()").is_err());
}
#[test]
fn unbounded_budget_exempts_scan_work_from_the_backstop() {
let budget = ExecutionBudget::new(None);
assert!(budget
.check_work(MAX_UNBOUNDED_ROWS * 100, "fused node count")
.is_ok());
assert!(budget.check_work(usize::MAX, "fused node count").is_ok());
}
}