use powdb_query::ast::Literal;
use powdb_query::executor::Engine;
use powdb_query::result::QueryResult;
use powdb_storage::types::Value;
use std::sync::{Arc, Barrier, RwLock};
use std::thread;
use std::time::Instant;
fn fresh_engine() -> Arc<RwLock<Engine>> {
let test_id = std::process::id();
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let data_dir = std::env::temp_dir().join(format!("powdb_conc_read_{test_id}_{ts}"));
let _ = std::fs::remove_dir_all(&data_dir);
std::fs::create_dir_all(&data_dir).unwrap();
let engine = Engine::new(&data_dir).unwrap();
Arc::new(RwLock::new(engine))
}
fn seed(engine: &Arc<RwLock<Engine>>, rows: usize) {
let mut eng = engine.write().unwrap();
eng.execute_powql(
"type User { required id: int, required name: str, required age: int, required status: str }"
).unwrap();
for i in 0..rows {
let q = format!(
r#"insert User {{ id := {i}, name := "user_{i}", age := {age}, status := "{status}" }}"#,
i = i,
age = 18 + (i % 70),
status = if i % 3 == 0 { "active" } else { "inactive" },
);
eng.execute_powql(&q).unwrap();
}
}
#[test]
fn concurrent_readers_make_progress_in_parallel() {
let engine = fresh_engine();
seed(&engine, 8_000);
let query = r#"User filter .age > 25 and .status = "active" { .id, .name, .age }"#;
{
let eng = engine.read().unwrap();
let res = eng.execute_powql_readonly(query).unwrap();
match res {
QueryResult::Rows { rows, .. } => {
assert!(rows.len() > 100, "expected many rows, got {}", rows.len());
}
other => panic!("expected rows, got {other:?}"),
}
}
for _ in 0..4 {
let eng = engine.read().unwrap();
let _ = eng.execute_powql_readonly(query).unwrap();
}
let iters_per_thread = 8;
let n_threads = 4;
let seq_start = Instant::now();
for _ in 0..(iters_per_thread * n_threads) {
let eng = engine.read().unwrap();
let _ = eng.execute_powql_readonly(query).unwrap();
}
let seq_elapsed = seq_start.elapsed();
let barrier = Arc::new(Barrier::new(n_threads));
let conc_start = Instant::now();
let handles: Vec<_> = (0..n_threads)
.map(|_| {
let eng = engine.clone();
let bar = barrier.clone();
let q = query.to_string();
thread::spawn(move || {
bar.wait();
for _ in 0..iters_per_thread {
let guard = eng.read().unwrap();
let _ = guard.execute_powql_readonly(&q).unwrap();
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let conc_elapsed = conc_start.elapsed();
let ratio = conc_elapsed.as_secs_f64() / seq_elapsed.as_secs_f64();
eprintln!(
"concurrent_readers: sequential={:?}, concurrent={:?}, ratio={:.3}",
seq_elapsed, conc_elapsed, ratio,
);
assert!(
ratio < 0.85,
"expected parallel reads under RwLock, but concurrent/sequential ratio was {ratio:.3} \
(sequential={seq_elapsed:?}, concurrent={conc_elapsed:?}). \
A ratio near 1.0 suggests reads are serialising on a mutex again.",
);
}
#[test]
fn concurrent_readers_see_uncorrupted_rows() {
let data_dir = {
let test_id = std::process::id();
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("powdb_conc_read_corr_{test_id}_{ts}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
};
let engine = Arc::new(RwLock::new(Engine::new(&data_dir).unwrap()));
const N: usize = 100_000;
{
let mut eng = engine.write().unwrap();
eng.execute_powql("type Row { required id: int, required payload: str }")
.unwrap();
let prep = eng
.prepare(r#"insert Row { id := 0, payload := "x" }"#)
.unwrap();
for i in 0..N {
let literals = [Literal::Int(i as i64), Literal::String(format!("p_{i}"))];
eng.execute_prepared(&prep, &literals).unwrap();
}
let tbl = eng
.catalog_mut()
.get_table_mut("Row")
.expect("Row table exists");
tbl.create_index("id", &data_dir).expect("build id index");
tbl.heap.flush_all_dirty().expect("flush dirty pages");
}
{
let eng = engine.read().unwrap();
let res = eng
.execute_powql_readonly("Row filter .id = 42 { .id, .payload }")
.unwrap();
match res {
QueryResult::Rows { rows, .. } => {
assert_eq!(rows.len(), 1, "expected one row for .id = 42");
let id = match &rows[0][0] {
Value::Int(n) => *n,
other => panic!("expected Int id, got {other:?}"),
};
let payload = match &rows[0][1] {
Value::Str(s) => s.as_str(),
other => panic!("expected Str payload, got {other:?}"),
};
assert_eq!(id, 42);
assert_eq!(payload, "p_42");
}
other => panic!("expected rows, got {other:?}"),
}
}
const LOOKUPS_PER_THREAD: usize = 10_000;
let n_threads = 16;
let barrier = Arc::new(Barrier::new(n_threads));
let handles: Vec<_> = (0..n_threads)
.map(|thread_idx| {
let eng = engine.clone();
let bar = barrier.clone();
thread::spawn(move || {
let stride: usize = 7 * (thread_idx + 1) + 1;
let mut k = thread_idx * 13;
bar.wait();
let guard = eng.read().unwrap();
let tbl = guard.catalog().get_table("Row").expect("Row table");
for _ in 0..LOOKUPS_PER_THREAD {
k = (k + stride) % N;
let key = Value::Int(k as i64);
let (_rid, row) = tbl.index_lookup("id", &key).unwrap_or_else(|| {
panic!(
"index_lookup for id {k} returned None \
— btree/heap disagreement, likely a \
torn read from a racing seek+read"
)
});
assert_eq!(row.len(), 2, "expected 2 columns per row");
let got_id = match &row[0] {
Value::Int(n) => *n,
other => panic!("expected Int id, got {other:?}"),
};
let got_payload = match &row[1] {
Value::Str(s) => s.as_str(),
other => panic!("expected Str payload, got {other:?}"),
};
assert_eq!(
got_id, k as i64,
"lookup for id {k} returned id {got_id} \
— byte corruption from a racing seek+read \
on the shared &File"
);
let expected = format!("p_{k}");
assert_eq!(
got_payload, expected,
"lookup for id {k} returned payload \
{got_payload:?}, expected {expected:?} \
— byte corruption from a racing seek+read \
on the shared &File"
);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[test]
fn write_lock_excludes_readers_correctness() {
let engine = fresh_engine();
{
let mut eng = engine.write().unwrap();
eng.execute_powql("type Item { required id: int, required name: str }")
.unwrap();
eng.execute_powql(r#"insert Item { id := 1, name := "apple" }"#)
.unwrap();
eng.execute_powql(r#"insert Item { id := 2, name := "banana" }"#)
.unwrap();
}
{
let eng = engine.read().unwrap();
let res = eng.execute_powql_readonly("count(Item)").unwrap();
match res {
QueryResult::Scalar(powdb_storage::types::Value::Int(n)) => {
assert_eq!(n, 2);
}
other => panic!("expected scalar count, got {other:?}"),
}
}
{
let mut eng = engine.write().unwrap();
eng.execute_powql(r#"insert Item { id := 3, name := "cherry" }"#)
.unwrap();
}
{
let eng = engine.read().unwrap();
let res = eng.execute_powql_readonly("count(Item)").unwrap();
match res {
QueryResult::Scalar(powdb_storage::types::Value::Int(n)) => {
assert_eq!(n, 3);
}
other => panic!("expected scalar count, got {other:?}"),
}
}
}
#[test]
fn readonly_rejects_mutations_with_escalation_sentinel() {
use powdb_query::executor::READONLY_NEEDS_WRITE;
let engine = fresh_engine();
{
let mut eng = engine.write().unwrap();
eng.execute_powql("type T { required id: int }").unwrap();
}
let eng = engine.read().unwrap();
let err = eng
.execute_powql_readonly(r#"insert T { id := 1 }"#)
.unwrap_err();
assert_eq!(err, READONLY_NEEDS_WRITE);
}