use crate::{LIVE_BYTES, PEAK_BYTES, perf_lock};
use spg_engine::{Engine, EngineError, QueryResult};
use std::sync::atomic::Ordering;
const ROWS: usize = 400;
const BODY_BYTES: usize = 256 * 1024; const BUDGET: usize = 4 * 1024 * 1024; const PEAK_BUDGET: usize = 32 * 1024 * 1024;
fn seed(eng: &mut Engine) {
eng.execute("CREATE TABLE messages (id BIGINT, mailbox_id BIGINT, body TEXT)")
.unwrap();
eng.execute("CREATE TABLE mailboxes (id BIGINT, user_address TEXT)")
.unwrap();
eng.execute("INSERT INTO mailboxes VALUES (1,'a@x'),(2,'b@x'),(3,'c@x'),(4,'d@x')")
.unwrap();
let mut i = 0usize;
while i < ROWS {
let mut stmt = String::with_capacity(10 * BODY_BYTES + 1024);
stmt.push_str("INSERT INTO messages VALUES ");
for k in 0..10 {
let id = i + k + 1;
if k > 0 {
stmt.push(',');
}
let body = format!("{id:08}{}", "x".repeat(BODY_BYTES - 8));
stmt.push_str(&format!("({id},{},'{body}')", id % 4 + 1));
}
eng.execute(&stmt).unwrap();
i += 10;
}
}
#[test]
fn tiny_budget_rejects_fat_queries_without_dying() {
let _g = perf_lock();
let mut eng = Engine::new().with_max_query_bytes(BUDGET);
seed(&mut eng);
let fat = [
"SELECT * FROM messages",
"SELECT m.body FROM messages m JOIN mailboxes mb ON m.mailbox_id = mb.id",
"SELECT * FROM messages ORDER BY body",
];
let floor = LIVE_BYTES.load(Ordering::Relaxed);
PEAK_BYTES.store(floor, Ordering::Relaxed);
for round in 0..15 {
for q in &fat {
match eng.execute_readonly(q) {
Err(EngineError::QueryBytesExceeded(_)) => {} Ok(QueryResult::Rows { rows, .. }) => panic!(
"round {round}: fat query `{q}` returned {} rows under a {BUDGET}-byte budget \
— the ceiling didn't fire",
rows.len()
),
Ok(other) => panic!("round {round}: `{q}` unexpected ok {other:?}"),
Err(other) => panic!(
"round {round}: fat query `{q}` must reject with QueryBytesExceeded, got {other:?}"
),
}
}
}
let peak = PEAK_BYTES.load(Ordering::Relaxed);
match eng
.execute_readonly("SELECT id FROM messages WHERE id = 1")
.expect("engine must still answer a small query after the barrage")
{
QueryResult::Rows { rows, .. } => assert_eq!(rows.len(), 1),
other => panic!("expected Rows from the survivor query, got {other:?}"),
}
let delta = peak.saturating_sub(floor);
assert!(
delta < PEAK_BUDGET,
"fat-query barrage peaked at {delta} bytes (budget {PEAK_BUDGET}) — a query \
ballooned toward the ~100 MiB table size before the {BUDGET}-byte ceiling \
rejected it; N concurrent copies would OOM the host (never-die violated)"
);
}