use powdb_query::ast::{Literal, ParamValue};
use powdb_query::executor::Engine;
use powdb_query::result::{QueryError, QueryResult};
use powdb_storage::types::Value;
use powdb_storage::wal::WalSyncMode;
const MODES: [WalSyncMode; 3] = [WalSyncMode::Full, WalSyncMode::Normal, WalSyncMode::Off];
fn exec(engine: &mut Engine, query: &str) -> QueryResult {
engine
.execute_powql(query)
.unwrap_or_else(|e| panic!("{query}: {e}"))
}
fn seeded(mode: WalSyncMode) -> (tempfile::TempDir, Engine) {
let dir = tempfile::tempdir().unwrap();
let mut engine = Engine::new(dir.path()).unwrap();
engine.set_wal_sync_mode(mode);
exec(
&mut engine,
"type User { required unique id: int, unique email: str, value: int }",
);
exec(
&mut engine,
r#"insert User { id := 1, email := "a", value := 10 }"#,
);
exec(
&mut engine,
r#"insert User { id := 2, email := "b", value := 20 }"#,
);
(dir, engine)
}
fn rows(engine: &mut Engine) -> Vec<Vec<Value>> {
match exec(engine, "User order .id { .id, .email, .value }") {
QueryResult::Rows { rows, .. } => rows,
result => panic!("expected rows, got {result:?}"),
}
}
#[test]
fn failed_unique_update_is_atomic_across_entry_points_and_wal_modes() {
for mode in MODES {
for surface in [
"powql",
"sql",
"params",
"prepared",
"prepared_take",
"plan",
] {
let (dir, mut engine) = seeded(mode);
let expected = rows(&mut engine);
exec(&mut engine, "User filter .id = 1 update { value := 11 }");
let mut expected = expected;
expected[0][2] = Value::Int(11);
let query = r#"User update { email := "duplicate" }"#;
let result = match surface {
"powql" => engine.execute_powql(query),
"sql" => engine.execute_sql("UPDATE User SET email = 'duplicate'"),
"params" => engine.execute_powql_with_params(
"User update { email := $1 }",
&[ParamValue::Str("duplicate".into())],
),
"prepared" | "prepared_take" => {
let prep = engine.prepare(query).unwrap();
let mut literals = [Literal::String("duplicate".into())];
if surface == "prepared" {
engine.execute_prepared(&prep, &literals)
} else {
engine.execute_prepared_take(&prep, &mut literals)
}
}
"plan" => engine.execute_plan(&powdb_query::planner::plan(query).unwrap()),
_ => unreachable!(),
};
let error = result.expect_err("second row must violate uniqueness");
assert!(
error.to_string().contains("unique constraint"),
"{surface}: {error}"
);
assert_eq!(rows(&mut engine), expected, "{surface}, {mode:?}");
exec(
&mut engine,
r#"insert User { id := 3, email := "c", value := 30 }"#,
);
expected.push(vec![Value::Int(3), Value::Str("c".into()), Value::Int(30)]);
assert_eq!(rows(&mut engine), expected);
drop(engine);
let mut reopened = Engine::new(dir.path()).unwrap();
assert_eq!(rows(&mut reopened), expected, "reopen: {surface}, {mode:?}");
let result = exec(&mut reopened, r#"User filter .email = "duplicate" { .id }"#);
assert_eq!(result.row_count(), 0, "index retained a failed prefix");
}
}
}
#[test]
fn explicit_transaction_errors_require_rollback_and_preserve_prior_commits() {
for mode in MODES {
for failure in ["unique", "parse", "bind", "readonly", "readonly_sql"] {
let (_dir, mut engine) = seeded(mode);
let expected = rows(&mut engine);
exec(&mut engine, "begin");
exec(
&mut engine,
r#"insert User { id := 3, email := "c", value := 30 }"#,
);
let result = match failure {
"unique" => engine.execute_powql(r#"User update { email := "same" }"#),
"parse" => engine.execute_powql("insert User {"),
"bind" => engine.execute_powql_with_params("User filter .id = $1", &[]),
"readonly" => engine.execute_powql_readonly("User { .missing }"),
"readonly_sql" => engine.execute_sql_readonly("SELECT missing FROM User"),
_ => unreachable!(),
};
assert!(result.is_err(), "{failure} must fail");
assert_eq!(
engine.execute_powql("User").unwrap_err(),
QueryError::TransactionAborted
);
assert_eq!(
engine.execute_sql("COMMIT").unwrap_err(),
QueryError::TransactionAborted
);
assert_eq!(
engine.execute_powql_readonly("User").unwrap_err(),
QueryError::TransactionAborted
);
exec(&mut engine, "rollback");
assert_eq!(rows(&mut engine), expected, "{failure}, {mode:?}");
exec(&mut engine, "begin");
exec(&mut engine, "User filter .id = 1 update { value := 12 }");
exec(&mut engine, "commit");
assert_eq!(rows(&mut engine)[0][2], Value::Int(12));
}
}
}
#[test]
fn arithmetic_error_after_a_written_prefix_is_rolled_back() {
for mode in MODES {
let (_dir, mut engine) = seeded(mode);
let expected = rows(&mut engine);
assert!(engine
.execute_powql("User update { value := 100 / (2 - .id) }")
.is_err());
assert_eq!(rows(&mut engine), expected, "{mode:?}");
}
}
#[test]
fn prepared_insert_failure_does_not_consume_input_or_abandon_prior_off_writes() {
for mode in MODES {
let (_dir, mut engine) = seeded(mode);
let expected = rows(&mut engine);
let prep = engine
.prepare(r#"insert User { id := 3, email := "a", value := 30 }"#)
.unwrap();
let mut values = [
Literal::Int(3),
Literal::String("a".into()),
Literal::Int(30),
];
assert!(engine.execute_prepared_take(&prep, &mut values).is_err());
assert!(matches!(&values[1], Literal::String(value) if value == "a"));
assert_eq!(rows(&mut engine), expected);
}
}
#[test]
fn ordinary_prepared_and_direct_plan_writes_have_one_durability_boundary() {
let (_dir, mut engine) = seeded(WalSyncMode::Full);
let before = engine.wal_fsync_count();
exec(&mut engine, "User filter .id = 1 update { value := 11 }");
assert_eq!(engine.wal_fsync_count() - before, 1);
let prep = engine
.prepare("User filter .id = 1 update { value := 12 }")
.unwrap();
let before = engine.wal_fsync_count();
engine
.execute_prepared(&prep, &[Literal::Int(1), Literal::Int(12)])
.unwrap();
assert_eq!(engine.wal_fsync_count() - before, 1);
let plan = powdb_query::planner::plan("User filter .id = 1 update { value := 13 }").unwrap();
let before = engine.wal_fsync_count();
engine.execute_plan(&plan).unwrap();
assert_eq!(engine.wal_fsync_count() - before, 1);
}
#[test]
fn a_failed_view_refresh_never_serves_its_partially_rebuilt_cache() {
for mode in MODES {
let (dir, mut engine) = seeded(mode);
exec(&mut engine, "materialize V as User { .id, .value }");
exec(&mut engine, "alter V add unique .value");
exec(&mut engine, "User update { value := 10 }");
for query in ["V", "V filter .value = 10 { .id }"] {
let error = engine.execute_powql(query).unwrap_err();
assert!(error.to_string().contains("unique constraint"), "{error}");
}
drop(engine);
let mut engine = Engine::new(dir.path()).unwrap();
assert!(engine.execute_powql("V").is_err());
exec(&mut engine, "User filter .id = 2 update { value := 20 }");
let result = exec(&mut engine, "V order .id { .id, .value }");
match result {
QueryResult::Rows { rows, .. } => assert_eq!(
rows,
vec![
vec![Value::Int(1), Value::Int(10)],
vec![Value::Int(2), Value::Int(20)],
]
),
result => panic!("unexpected repaired view: {result:?}"),
}
}
}
#[test]
fn pre_cancelled_prepared_writes_preserve_state_and_input() {
use powdb_query::cancel::{install, CancelReason, ExecCancel};
use std::sync::Arc;
for mode in MODES {
let (_dir, mut engine) = seeded(mode);
let expected = rows(&mut engine);
let prep = engine
.prepare(r#"insert User { id := 3, email := "c", value := 30 }"#)
.unwrap();
let mut values = [
Literal::Int(3),
Literal::String("c".into()),
Literal::Int(30),
];
let cancel = Arc::new(ExecCancel::new());
cancel.cancel(CancelReason::Disconnect);
{
let _guard = install(cancel);
assert_eq!(
engine
.execute_prepared_take(&prep, &mut values)
.unwrap_err(),
QueryError::Cancelled
);
assert!(matches!(&values[1], Literal::String(value) if value == "c"));
}
assert_eq!(rows(&mut engine), expected);
}
}
#[cfg(feature = "testing")]
#[test]
fn prepared_take_generic_and_fast_paths_share_commit_and_rollback_semantics() {
for mode in MODES {
let mut outcomes = Vec::new();
for generic in [false, true] {
let (_dir, mut engine) = seeded(mode);
engine.set_force_generic_path(generic);
let prep = engine
.prepare(r#"insert User { id := 3, email := "c", value := 30 }"#)
.unwrap();
let mut literals = [
Literal::Int(3),
Literal::String("c".into()),
Literal::Int(30),
];
engine.execute_prepared_take(&prep, &mut literals).unwrap();
assert!(matches!(&literals[1], Literal::String(value) if value.is_empty()));
let expected = rows(&mut engine);
literals[0] = Literal::Int(4);
literals[1] = Literal::String("a".into());
assert!(engine.execute_prepared_take(&prep, &mut literals).is_err());
assert!(matches!(&literals[1], Literal::String(value) if value == "a"));
assert_eq!(rows(&mut engine), expected);
if generic {
assert!(engine
.forced_generic_sites()
.contains(&"prepared-insert-take"));
}
outcomes.push(expected);
}
assert_eq!(outcomes[0], outcomes[1]);
}
}
#[test]
fn failed_view_registry_reload_after_rollback_poisons_the_handle() {
let (dir, mut engine) = seeded(WalSyncMode::Full);
let expected = rows(&mut engine);
std::fs::write(dir.path().join("views.bin"), b"invalid registry").unwrap();
assert_eq!(
engine
.execute_powql(r#"User update { email := "same" }"#)
.unwrap_err(),
QueryError::EnginePoisoned
);
assert_eq!(
engine.execute_powql("User").unwrap_err(),
QueryError::EnginePoisoned
);
drop(engine);
std::fs::remove_file(dir.path().join("views.bin")).unwrap();
let mut reopened = Engine::new(dir.path()).unwrap();
assert_eq!(rows(&mut reopened), expected);
}