use std::sync::mpsc;
use std::time::Duration;
use super::*;
fn integer_column(result: &SQLResult, name: &str) -> Vec<i64> {
result
.rows
.iter()
.map(|row| match row.get(name) {
Some(crate::Value::Int(value)) => *value,
other => panic!("expected integer column {name}, got {other:?}"),
})
.collect()
}
fn end_backend_transaction_early(engine: &Engine) {
engine
.storage
.backend
.as_ref()
.expect("persistent test engine")
.rollback_transaction()
.expect("end backend transaction early");
}
fn assert_combined_panic_and_rollback_error(error: &str) {
assert!(error.contains("rollback"), "{error}");
assert!(error.contains("original panic: callback panic"), "{error}");
}
#[test]
fn implicit_read_transaction_rolls_back_an_unclassified_storage_write() {
let directory = tempfile::tempdir().unwrap();
let engine = Engine::open(&directory.path().join("read-only-guard.db")).unwrap();
engine.begin_implicit_statement_transaction(true).unwrap();
engine
.storage
.catalog
.as_ref()
.unwrap()
.set_metadata("hidden-write", "x")
.unwrap();
let error = engine.commit().unwrap_err().to_string();
assert!(error.contains("read-only SQL execution"), "{error}");
assert_eq!(engine.transaction_depth(), 0);
assert_eq!(
engine
.storage
.catalog
.as_ref()
.unwrap()
.get_metadata("hidden-write")
.unwrap(),
None
);
}
#[test]
fn rollback_failure_after_callback_panic_is_returned_instead_of_panicking_again() {
let directory = tempfile::tempdir().unwrap();
let transaction_engine = Engine::open(&directory.path().join("transaction.db")).unwrap();
let transaction_result: Result<(), SQLError> = transaction_engine.transaction(|engine| {
end_backend_transaction_early(engine);
panic!("callback panic");
});
let transaction_error = transaction_result.unwrap_err();
assert_combined_panic_and_rollback_error(&transaction_error.to_string());
assert_eq!(transaction_engine.transaction_depth(), 0);
let storage_engine = Engine::open(&directory.path().join("storage.db")).unwrap();
let storage_result: StorageBackendResult<()> = storage_engine
.with_implicit_storage_transaction(|engine| {
end_backend_transaction_early(engine);
panic!("callback panic");
});
let storage_error = storage_result.unwrap_err();
assert_combined_panic_and_rollback_error(&storage_error.to_string());
assert_eq!(storage_engine.transaction_depth(), 0);
let string_engine = Engine::open(&directory.path().join("string.db")).unwrap();
let string_result: Result<(), String> =
string_engine.with_implicit_string_transaction(|engine| {
end_backend_transaction_early(engine);
panic!("callback panic");
});
let string_error = string_result.unwrap_err();
assert_combined_panic_and_rollback_error(&string_error);
assert_eq!(string_engine.transaction_depth(), 0);
}
#[test]
fn waiting_writer_refreshes_when_sqlite_commit_precedes_epoch_publication() {
for create_sql in [
"CREATE TABLE fresh.items (id INTEGER)",
"CREATE TABLE fresh.items AS SELECT 1 AS id",
] {
let directory = tempfile::tempdir().unwrap();
let root = Engine::open(&directory.path().join("catalog-race.db")).unwrap();
let writer = root.new_session().unwrap();
let waiter = root.new_session().unwrap();
writer.begin().unwrap();
assert!(!waiter.has_schema("fresh").unwrap());
writer.sql("CREATE SCHEMA fresh", &[]).unwrap();
let (started_tx, started_rx) = mpsc::channel();
let (done_tx, done_rx) = mpsc::channel();
let waiting_thread = std::thread::spawn(move || {
started_tx.send(()).unwrap();
let result = waiter.sql(create_sql, &[]);
done_tx.send(result).unwrap();
});
started_rx.recv_timeout(Duration::from_secs(2)).unwrap();
match done_rx.recv_timeout(Duration::from_millis(200)) {
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(error) => panic!("waiting writer result channel failed early: {error}"),
Ok(result) => panic!("waiting writer completed before writer release: {result:?}"),
}
writer
.storage
.backend
.as_ref()
.unwrap()
.commit_transaction()
.unwrap();
writer.row_locks.release_session(writer.session_id);
done_rx
.recv_timeout(Duration::from_secs(2))
.unwrap()
.unwrap();
waiting_thread.join().unwrap();
writer.session.transactions.lock().clear();
assert!(root
.new_session()
.unwrap()
.has_table("fresh.items")
.unwrap());
}
}
#[test]
fn unchanged_persistent_statements_keep_their_loaded_catalog_snapshot() {
let directory = tempfile::tempdir().unwrap();
let engine = Engine::open(&directory.path().join("stable-snapshot.db")).unwrap();
engine
.sql("CREATE TABLE items (id INTEGER PRIMARY KEY)", &[])
.unwrap();
engine.sql("SELECT id FROM items", &[]).unwrap();
let before = engine.require_table("items").unwrap();
engine.sql("SELECT id FROM items", &[]).unwrap();
let after = engine.require_table("items").unwrap();
assert!(
std::sync::Arc::ptr_eq(&before, &after),
"an unchanged statement rebuilt the complete persistent catalog"
);
}
#[test]
fn compressed_write_refresh_uses_the_pinned_transaction_connection() {
let directory = tempfile::tempdir().unwrap();
let engine = Engine::open_compressed(
&directory.path().join("compressed-write.db"),
uqa_storage::SQLiteCompressionOptions::default(),
)
.unwrap();
engine
.sql("CREATE TABLE items (id INTEGER PRIMARY KEY)", &[])
.unwrap();
engine
.sql("INSERT INTO items (id) VALUES (1)", &[])
.unwrap();
let result = engine.sql("SELECT id FROM items", &[]).unwrap();
assert_eq!(result.rows.len(), 1);
}
#[test]
fn compressed_fixed_snapshot_releases_reader_locks_and_preserves_repeatable_read() {
let directory = tempfile::tempdir().unwrap();
let root = Engine::open_compressed(
&directory.path().join("compressed-fixed-snapshot.db"),
uqa_storage::SQLiteCompressionOptions::default(),
)
.unwrap();
root.sql(
"CREATE TABLE items (id INTEGER PRIMARY KEY, value INTEGER)",
&[],
)
.unwrap();
root.sql("INSERT INTO items VALUES (1, 10), (2, 20)", &[])
.unwrap();
let reader = root.new_session().unwrap();
let writer = root.new_session().unwrap();
reader
.sql("BEGIN ISOLATION LEVEL REPEATABLE READ", &[])
.unwrap();
assert_eq!(
integer_column(
&reader
.sql("SELECT value FROM items ORDER BY id", &[])
.unwrap(),
"value",
),
[10, 20]
);
writer
.sql("UPDATE items SET value = 11 WHERE id = 1", &[])
.unwrap();
assert_eq!(
integer_column(
&writer
.sql("SELECT value FROM items ORDER BY id", &[])
.unwrap(),
"value",
),
[11, 20]
);
reader
.sql("UPDATE items SET value = 21 WHERE id = 2", &[])
.unwrap();
assert_eq!(
integer_column(
&reader
.sql("SELECT value FROM items ORDER BY id", &[])
.unwrap(),
"value",
),
[10, 21]
);
reader.sql("COMMIT", &[]).unwrap();
let observer = root.new_session().unwrap();
assert_eq!(
integer_column(
&observer
.sql("SELECT value FROM items ORDER BY id", &[])
.unwrap(),
"value",
),
[11, 21]
);
}
#[test]
fn pinned_reader_defers_sibling_catalog_epochs_until_transaction_end() {
let directory = tempfile::tempdir().unwrap();
let root = Engine::open(&directory.path().join("pinned-reader.db")).unwrap();
let reader = root.new_session().unwrap();
let writer = root.new_session().unwrap();
{
let characteristics = reader.default_transaction_characteristics();
let mut stack = reader.session.transactions.lock();
reader
.begin_transaction_frame(&mut stack, true, true, false, characteristics)
.unwrap();
}
assert!(!reader.has_schema("later").unwrap());
writer.sql("CREATE SCHEMA later", &[]).unwrap();
writer.create_graph("later_graph").unwrap();
assert!(!reader.has_schema("later").unwrap());
assert!(!reader.has_graph("later_graph").unwrap());
reader.commit().unwrap();
assert!(reader.has_schema("later").unwrap());
assert!(reader.has_graph("later_graph").unwrap());
}