1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//! Per-query byte budget and heap-size estimators for join/filter
//! materialisation. Lifted out of `lib.rs` (v7.32 engine
//! modularisation). `ByteBudget` is the net-accounting meter the
//! v7.30.3 (mailrs round-26) bounded-join path charges/releases as
//! stages clone and free rows; the `approx_*` helpers estimate the
//! resident heap cost of a `Value` / `Row` / row set.
use alloc::string::String;
use spg_storage::{Row, Value};
use crate::EngineError;
/// v7.30.3 (mailrs round-26) — approximate heap bytes held by one
/// `Value`. Fat payloads (text / json / bytea / vectors / arrays)
/// dominate; fixed-size variants count 0 here because the per-cell
/// enum overhead is charged separately in `approx_row_bytes`. An
/// under-estimate is acceptable — the budget is a host-pressure
/// guard, not an exact meter.
pub(crate) fn approx_value_bytes(v: &Value) -> usize {
match v {
Value::Text(s) | Value::Json(s) => s.len(),
Value::Bytes(b) => b.len(),
Value::Vector(v) => v.len() * 4,
Value::TextArray(a) => a
.iter()
.map(|o| o.as_ref().map_or(0, String::len) + 8)
.sum(),
Value::IntArray(a) => a.len() * 8,
_ => 0,
}
}
/// Approximate heap bytes held by one materialised `Row`: per-cell
/// enum slots plus fat payloads.
pub(crate) fn approx_row_bytes(row: &Row<'static>) -> usize {
approx_values_bytes(&row.values)
}
/// v7.37.42-arena Phase 2 — same accounting as `approx_row_bytes` but
/// takes a borrowed `&[Value]` slice. The SCALARSQ streaming executor
/// keeps a reusable per-query `Vec<Value>` scratch (in a bumpalo
/// arena) and charges the budget per row without ever wrapping it in
/// a `Row` — this lets it skip the per-row Row alloc on the hot path.
pub(crate) fn approx_values_bytes(values: &[Value<'_>]) -> usize {
core::mem::size_of_val(values) + values.iter().map(approx_value_bytes).sum::<usize>()
}
/// v7.30.3 (mailrs round-26) — per-query byte budget for join/filter
/// materialisation. Net accounting: stages charge what they clone and
/// release what they free (`working` is released when the next stage
/// replaces it), so the meter tracks live bytes, not cumulative
/// churn. `limit = usize::MAX` when the budget is disabled keeps the
/// hot path branch-free apart from one saturating add + compare.
pub(crate) struct ByteBudget {
limit: usize,
used: usize,
}
impl ByteBudget {
pub(crate) const fn new(limit: Option<usize>) -> Self {
Self {
limit: match limit {
Some(n) => n,
None => usize::MAX,
},
used: 0,
}
}
pub(crate) fn charge(&mut self, n: usize) -> Result<(), EngineError> {
self.used = self.used.saturating_add(n);
if self.used > self.limit {
return Err(EngineError::QueryBytesExceeded(self.limit));
}
Ok(())
}
pub(crate) fn release(&mut self, n: usize) {
self.used = self.used.saturating_sub(n);
}
}
/// Sum `approx_row_bytes` over a freshly materialised row set.
pub(crate) fn approx_rows_bytes(rows: &[Row<'static>]) -> usize {
rows.iter().map(approx_row_bytes).sum()
}