use super::*;
use crate::cancel::{CancelReason, ExecCancel};
use crate::result::QueryError;
use std::sync::Arc as CancelArc;
use std::time::{Duration as CancelDuration, Instant as CancelInstant};
fn compound_join_engine(left: usize, right: usize) -> Engine {
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"powdb_cancel_join_{}_{}_{}",
std::process::id(),
id,
nonce
));
let mut engine = Engine::new(&dir).unwrap();
engine.set_wal_sync_mode(super::WalSyncMode::Off);
engine
.execute_powql("type Ver { required id: int }")
.unwrap();
engine
.execute_powql("type Grp { required version_id: int, required field_ns: str }")
.unwrap();
for i in 0..left {
engine
.execute_powql(&format!("insert Ver {{ id := {i} }}"))
.unwrap();
}
for i in 0..right {
engine
.execute_powql(&format!(
r#"insert Grp {{ version_id := {i}, field_ns := "f1" }}"#
))
.unwrap();
}
engine
}
fn item_engine(n: usize) -> Engine {
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("powdb_cancel_item_{}_{}", std::process::id(), id));
let mut engine = Engine::new(&dir).unwrap();
engine.set_wal_sync_mode(super::WalSyncMode::Off);
engine
.execute_powql("type Item { required id: int, required v: int }")
.unwrap();
for i in 0..n {
engine
.execute_powql(&format!("insert Item {{ id := {i}, v := 0 }}"))
.unwrap();
}
engine
}
pub(super) fn cancel_after_checkpoint(
token: CancelArc<ExecCancel>,
target: usize,
) -> std::thread::JoinHandle<()> {
token.block_at_checkpoint(target);
std::thread::spawn(move || {
let deadline = CancelInstant::now() + CancelDuration::from_secs(3);
while token.checkpoint_count() < target && CancelInstant::now() < deadline {
std::thread::yield_now();
}
assert!(
token.checkpoint_count() >= target,
"query completed without reaching cancellation checkpoint {target}"
);
token.cancel(CancelReason::Disconnect);
})
}
#[test]
fn nested_loop_join_honors_deadline() {
let engine = compound_join_engine(1200, 1200);
let cancel = CancelArc::new(ExecCancel::with_deadline(
CancelInstant::now() + CancelDuration::from_millis(100),
100,
));
let start = CancelInstant::now();
let result = engine.execute_powql_readonly_with_cancel(
r#"Ver as ver join Grp as g on ver.id + 0 = g.version_id and g.field_ns = "f1""#,
cancel,
);
let elapsed = start.elapsed();
assert!(
matches!(result, Err(QueryError::Timeout { timeout_ms: 100 })),
"expected Timeout, got {result:?}"
);
assert!(
elapsed < CancelDuration::from_secs(3),
"cancellation should be prompt, took {elapsed:?}"
);
let after = engine.execute_powql_readonly("count(Ver)").unwrap();
assert!(matches!(after, QueryResult::Scalar(Value::Int(1200))));
}
#[test]
fn nested_loop_join_honors_explicit_cancel() {
let engine = compound_join_engine(1200, 1200);
let cancel = CancelArc::new(ExecCancel::new());
let cancel_thread = cancel_after_checkpoint(CancelArc::clone(&cancel), 2);
let start = CancelInstant::now();
let result = engine.execute_powql_readonly_with_cancel(
r#"Ver as ver join Grp as g on ver.id + 0 = g.version_id and g.field_ns = "f1""#,
cancel,
);
cancel_thread.join().unwrap();
assert!(
matches!(result, Err(QueryError::Cancelled)),
"expected Cancelled, got {result:?}"
);
assert!(start.elapsed() < CancelDuration::from_secs(3));
}
#[test]
fn cooperative_stable_sort_matches_std_stable_sort() {
let mut actual: Vec<(u32, usize)> = (0..20_000).map(|i| (((i * 37) % 97) as u32, i)).collect();
let mut expected = actual.clone();
expected.sort_by_key(|&(key, _)| key);
super::mem_budget::reset();
super::plan_exec::cooperative_stable_sort_by(
&mut actual,
usize::MAX,
|&(left, _), &(right, _)| left.cmp(&right),
)
.unwrap();
assert_eq!(
actual, expected,
"ordering and equal-key stability must match"
);
}
#[test]
fn regular_sort_honors_live_cancel_inside_sort_on_both_entry_paths() {
let mut mutable_engine = item_engine(20_000);
let mutable_cancel = CancelArc::new(ExecCancel::new());
let mutable_signal = cancel_after_checkpoint(CancelArc::clone(&mutable_cancel), 12);
let mutable_result =
mutable_engine.execute_powql_with_cancel("Item order .id desc", mutable_cancel);
mutable_signal.join().unwrap();
assert!(matches!(mutable_result, Err(QueryError::Cancelled)));
assert!(matches!(
mutable_engine.execute_powql("count(Item)").unwrap(),
QueryResult::Scalar(Value::Int(20_000))
));
let readonly_engine = item_engine(20_000);
let readonly_cancel = CancelArc::new(ExecCancel::new());
let readonly_signal = cancel_after_checkpoint(CancelArc::clone(&readonly_cancel), 12);
let readonly_result =
readonly_engine.execute_powql_readonly_with_cancel("Item order .id desc", readonly_cancel);
readonly_signal.join().unwrap();
assert!(matches!(readonly_result, Err(QueryError::Cancelled)));
assert!(matches!(
readonly_engine
.execute_powql_readonly("count(Item)")
.unwrap(),
QueryResult::Scalar(Value::Int(20_000))
));
}
#[test]
fn window_sort_honors_live_cancel_inside_sort_and_engine_stays_usable() {
let engine = item_engine(20_000);
let cancel = CancelArc::new(ExecCancel::new());
let signal = cancel_after_checkpoint(CancelArc::clone(&cancel), 8);
let result = engine.execute_powql_readonly_with_cancel(
"Item { .id, rn: row_number() over (order .id desc) }",
cancel,
);
signal.join().unwrap();
assert!(matches!(result, Err(QueryError::Cancelled)));
assert!(matches!(
engine.execute_powql_readonly("count(Item)").unwrap(),
QueryResult::Scalar(Value::Int(20_000))
));
}
#[test]
fn compiled_scan_honors_cancel_and_engine_stays_usable() {
let engine = item_engine(20_000);
let cancel = CancelArc::new(ExecCancel::new());
let signal = cancel_after_checkpoint(CancelArc::clone(&cancel), 2);
let result = engine.execute_powql_readonly_with_cancel("Item filter .id >= 0 { id }", cancel);
signal.join().unwrap();
assert!(
matches!(result, Err(QueryError::Cancelled)),
"expected Cancelled, got {result:?}"
);
let full = engine
.execute_powql_readonly("count(Item filter .id >= 0)")
.unwrap();
assert!(matches!(full, QueryResult::Scalar(Value::Int(20_000))));
}
#[test]
fn mutable_alias_scan_honors_live_cancel_and_engine_stays_usable() {
let mut engine = item_engine(20_000);
let cancel = CancelArc::new(ExecCancel::new());
let signal = cancel_after_checkpoint(CancelArc::clone(&cancel), 2);
let result = engine.execute_powql_with_cancel("Item as i", cancel);
signal.join().unwrap();
assert!(
matches!(result, Err(QueryError::Cancelled)),
"expected Cancelled, got {result:?}"
);
let after = engine.execute_powql("count(Item)").unwrap();
assert!(matches!(after, QueryResult::Scalar(Value::Int(20_000))));
}
#[test]
fn readonly_fast_count_honors_live_cancel_and_engine_stays_usable() {
let engine = item_engine(20_000);
let cancel = CancelArc::new(ExecCancel::new());
let signal = cancel_after_checkpoint(CancelArc::clone(&cancel), 2);
let result = engine.execute_powql_readonly_with_cancel("count(Item)", cancel);
signal.join().unwrap();
assert!(
matches!(result, Err(QueryError::Cancelled)),
"expected Cancelled, got {result:?}"
);
let after = engine.execute_powql_readonly("count(Item)").unwrap();
assert!(matches!(after, QueryResult::Scalar(Value::Int(20_000))));
}
#[test]
fn distinct_materialization_honors_live_cancel_and_engine_stays_usable() {
let engine = item_engine(20_000);
let cancel = CancelArc::new(ExecCancel::new());
let signal = cancel_after_checkpoint(CancelArc::clone(&cancel), 6);
let result = engine.execute_powql_readonly_with_cancel("Item distinct { .id, .v }", cancel);
signal.join().unwrap();
assert!(
matches!(result, Err(QueryError::Cancelled)),
"expected Cancelled, got {result:?}"
);
let after = engine.execute_powql_readonly("count(Item)").unwrap();
assert!(matches!(after, QueryResult::Scalar(Value::Int(20_000))));
}
#[test]
fn cancelled_update_before_write_leaves_all_rows_unchanged() {
let mut engine = item_engine(20_000);
let cancel = CancelArc::new(ExecCancel::with_deadline(
CancelInstant::now() - CancelDuration::from_millis(1),
500,
));
let result =
engine.execute_powql_with_cancel("Item filter .id >= 0 update { v := .v + 1 }", cancel);
assert!(
matches!(result, Err(QueryError::Timeout { .. })),
"expected Timeout, got {result:?}"
);
let unchanged = engine.execute_powql("sum(Item { .v })").unwrap();
assert!(
matches!(unchanged, QueryResult::Scalar(Value::Int(0))),
"a cancelled update must not leave a written prefix: {unchanged:?}"
);
let full = engine
.execute_powql("Item filter .id >= 0 update { v := .v + 1 }")
.unwrap();
assert!(
matches!(full, QueryResult::Modified(20_000)),
"expected all rows updated, got {full:?}"
);
}
#[test]
fn cancelled_mutation_does_not_abort_explicit_transaction() {
let mut engine = item_engine(1);
engine.execute_powql("begin").unwrap();
let cancel = CancelArc::new(ExecCancel::new());
cancel.cancel(CancelReason::Disconnect);
let result =
engine.execute_powql_with_cancel("Item filter .id = 0 update { v := .v + 1 }", cancel);
assert!(matches!(result, Err(QueryError::Cancelled)));
let unchanged = engine.execute_powql("sum(Item { .v })").unwrap();
assert!(matches!(unchanged, QueryResult::Scalar(Value::Int(0))));
let committed = engine.execute_powql("commit").unwrap();
assert!(matches!(committed, QueryResult::Executed { .. }));
}
#[test]
fn cancel_only_affects_its_own_query() {
let engine = compound_join_engine(600, 600);
let cancel = CancelArc::new(ExecCancel::new());
cancel.cancel(CancelReason::Timeout);
let _ = engine.execute_powql_readonly_with_cancel(
r#"Ver as ver join Grp as g on ver.id + 0 = g.version_id and g.field_ns = "f1""#,
cancel,
);
let ok = engine
.execute_powql_readonly(
r#"Ver as ver join Grp as g on ver.id = g.version_id and g.field_ns = "f1""#,
)
.unwrap();
match ok {
QueryResult::Rows { rows, .. } => assert_eq!(rows.len(), 600),
other => panic!("expected rows, got {other:?}"),
}
}
#[test]
fn symmetric_rid_dedup_set_is_charged_to_query_memory_budget() {
super::mem_budget::reset();
let columns = vec!["a.value".to_string()];
let rows = vec![vec![Value::Int(10)]];
let provenance = vec![vec![Some(RowId {
page_id: 1,
slot_index: 0,
})]];
let error = super::plan_exec::compute_group_aggregate(
crate::ast::AggFunc::Sum,
&Expr::QualifiedField {
qualifier: "a".into(),
field: "value".into(),
},
Some(0),
super::plan_exec::GroupAggregateContext {
columns: &columns,
all_rows: &rows,
row_indices: &[0],
source_index: Some(0),
provenance: Some((&provenance, 1)),
},
)
.unwrap_err();
assert!(matches!(error, QueryError::MemoryLimitExceeded { .. }));
}