use citadel::{Argon2Profile, DatabaseBuilder};
use citadel_sql::{Connection, ExecutionResult, SqlError, Value};
fn create_db(dir: &std::path::Path) -> citadel::Database {
let db_path = dir.join("test.db");
DatabaseBuilder::new(db_path)
.passphrase(b"savepoint-test")
.argon2_profile(Argon2Profile::Iot)
.create()
.unwrap()
}
fn open_db(dir: &std::path::Path) -> citadel::Database {
let db_path = dir.join("test.db");
DatabaseBuilder::new(db_path)
.passphrase(b"savepoint-test")
.argon2_profile(Argon2Profile::Iot)
.open()
.unwrap()
}
fn assert_ok(r: ExecutionResult) {
match r {
ExecutionResult::Ok => {}
other => panic!("expected Ok, got {other:?}"),
}
}
fn assert_rows(r: ExecutionResult, expected: u64) {
match r {
ExecutionResult::RowsAffected(n) => assert_eq!(n, expected),
other => panic!("expected RowsAffected({expected}), got {other:?}"),
}
}
fn count(conn: &Connection<'_>, sql: &str) -> i64 {
let qr = conn.query(sql).unwrap();
match &qr.rows[0][0] {
Value::Integer(n) => *n,
v => panic!("expected integer count, got {v:?}"),
}
}
fn setup(conn: &Connection<'_>) {
conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
}
#[test]
fn savepoint_outside_txn_errors() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
let err = conn.execute("SAVEPOINT sp1").unwrap_err();
assert!(matches!(err, SqlError::NoActiveTransaction));
}
#[test]
fn release_outside_txn_errors() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
let err = conn.execute("RELEASE SAVEPOINT sp1").unwrap_err();
assert!(matches!(err, SqlError::NoActiveTransaction));
}
#[test]
fn rollback_to_outside_txn_errors() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
let err = conn.execute("ROLLBACK TO sp1").unwrap_err();
assert!(matches!(err, SqlError::NoActiveTransaction));
}
#[test]
fn commit_clears_savepoint_stack() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
assert_ok(conn.execute("BEGIN").unwrap());
assert_ok(conn.execute("SAVEPOINT sp1").unwrap());
assert_ok(conn.execute("COMMIT").unwrap());
assert_ok(conn.execute("BEGIN").unwrap());
let err = conn.execute("ROLLBACK TO sp1").unwrap_err();
assert!(matches!(err, SqlError::SavepointNotFound(_)));
}
#[test]
fn rollback_clears_savepoint_stack() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
assert_ok(conn.execute("BEGIN").unwrap());
assert_ok(conn.execute("SAVEPOINT sp1").unwrap());
assert_ok(conn.execute("ROLLBACK").unwrap());
assert_ok(conn.execute("BEGIN").unwrap());
let err = conn.execute("RELEASE sp1").unwrap_err();
assert!(matches!(err, SqlError::SavepointNotFound(_)));
}
#[test]
fn release_nonexistent_errors() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
assert_ok(conn.execute("BEGIN").unwrap());
let err = conn.execute("RELEASE SAVEPOINT ghost").unwrap_err();
assert!(matches!(err, SqlError::SavepointNotFound(_)));
}
#[test]
fn rollback_to_nonexistent_errors() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
assert_ok(conn.execute("BEGIN").unwrap());
let err = conn.execute("ROLLBACK TO ghost").unwrap_err();
assert!(matches!(err, SqlError::SavepointNotFound(_)));
}
#[test]
fn release_keeps_work() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp1").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a')")
.unwrap();
conn.execute("RELEASE SAVEPOINT sp1").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 1);
}
#[test]
fn rollback_to_discards_post_savepoint() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'keep')")
.unwrap();
conn.execute("SAVEPOINT sp1").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'drop')")
.unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (3, 'drop')")
.unwrap();
conn.execute("ROLLBACK TO sp1").unwrap();
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT id, val FROM t ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0][0], Value::Integer(1));
assert_eq!(qr.rows[0][1], Value::Text("keep".into()));
}
#[test]
fn rollback_to_preserves_savepoint() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp1").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a')")
.unwrap();
conn.execute("ROLLBACK TO sp1").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'b')")
.unwrap();
conn.execute("ROLLBACK TO sp1").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 0);
}
#[test]
fn pre_savepoint_work_persists() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'pre')")
.unwrap();
conn.execute("SAVEPOINT sp1").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'post')")
.unwrap();
conn.execute("ROLLBACK TO sp1").unwrap();
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT val FROM t ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0][0], Value::Text("pre".into()));
}
#[test]
fn release_inner_keeps_outer() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT a").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a')")
.unwrap();
conn.execute("SAVEPOINT b").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'b')")
.unwrap();
conn.execute("RELEASE b").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (3, 'c')")
.unwrap();
conn.execute("ROLLBACK TO a").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 0);
}
#[test]
fn rollback_to_inner_keeps_work_up_to_inner() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'outer')")
.unwrap();
conn.execute("SAVEPOINT a").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'mid')")
.unwrap();
conn.execute("SAVEPOINT b").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (3, 'inner')")
.unwrap();
conn.execute("ROLLBACK TO b").unwrap();
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT id FROM t ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][0], Value::Integer(1));
assert_eq!(qr.rows[1][0], Value::Integer(2));
}
#[test]
fn rollback_to_outer_discards_middle_and_inner() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT a").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a')")
.unwrap();
conn.execute("SAVEPOINT b").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'b')")
.unwrap();
conn.execute("SAVEPOINT c").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (3, 'c')")
.unwrap();
conn.execute("ROLLBACK TO a").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 0);
assert_ok(conn.execute("BEGIN").unwrap());
let err = conn.execute("RELEASE b").unwrap_err();
assert!(matches!(err, SqlError::SavepointNotFound(_)));
}
#[test]
fn shadowed_duplicate_name() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'outer')")
.unwrap();
conn.execute("SAVEPOINT sp").unwrap(); conn.execute("INSERT INTO t (id, val) VALUES (2, 'inner')")
.unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
let qr = conn.query("SELECT val FROM t ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0][0], Value::Text("outer".into()));
conn.execute("COMMIT").unwrap();
}
#[test]
fn shadowed_release_peels_inner() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'outer')")
.unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'inner')")
.unwrap();
conn.execute("RELEASE sp").unwrap(); conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 0);
}
#[test]
fn create_table_rolled_back() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("CREATE TABLE t2 (id INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("INSERT INTO t2 (id) VALUES (1)").unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("COMMIT").unwrap();
let err = conn.query("SELECT * FROM t2").unwrap_err();
assert!(matches!(err, SqlError::TableNotFound(_)));
}
#[test]
fn drop_table_rolled_back() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a')")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("DROP TABLE t").unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 1);
}
#[test]
fn create_index_rolled_back() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("CREATE INDEX idx_val ON t (val)").unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
let err = conn.execute("DROP INDEX idx_val").unwrap_err();
assert!(matches!(err, SqlError::IndexNotFound(_)));
conn.execute("COMMIT").unwrap();
}
#[test]
fn alter_table_rolled_back() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a')")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("ALTER TABLE t ADD COLUMN extra INTEGER")
.unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("COMMIT").unwrap();
let err = conn.query("SELECT extra FROM t").unwrap_err();
assert!(matches!(err, SqlError::ColumnNotFound(_)));
}
#[test]
fn ddl_and_dml_rolled_back_together() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("CREATE TABLE nested (id INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("INSERT INTO nested (id) VALUES (1), (2), (3)")
.unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (10, 'x')")
.unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("COMMIT").unwrap();
let err = conn.query("SELECT * FROM nested").unwrap_err();
assert!(matches!(err, SqlError::TableNotFound(_)));
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 0);
}
#[test]
fn index_insert_rolled_back() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("CREATE INDEX idx_val ON t (val)").unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'x')")
.unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'x')")
.unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t WHERE val = 'x'"), 0);
}
#[test]
fn unique_index_respected_across_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("CREATE UNIQUE INDEX uq_val ON t (val)")
.unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'existing')")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("DELETE FROM t WHERE id = 1").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'existing')")
.unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
let err = conn
.execute("INSERT INTO t (id, val) VALUES (3, 'existing')")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
conn.execute("COMMIT").unwrap();
}
#[test]
fn recover_from_unique_violation() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("INSERT INTO t (id, val) VALUES (1, 'seed')")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
let err = conn
.execute("INSERT INTO t (id, val) VALUES (1, 'dup')")
.unwrap_err();
assert!(matches!(err, SqlError::DuplicateKey));
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'ok')")
.unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 2);
}
#[test]
fn recover_from_check_violation() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("CREATE TABLE tc (id INTEGER PRIMARY KEY, n INTEGER CHECK(n > 0))")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
let err = conn
.execute("INSERT INTO tc (id, n) VALUES (1, -5)")
.unwrap_err();
assert!(matches!(err, SqlError::CheckViolation(_)));
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("INSERT INTO tc (id, n) VALUES (2, 10)")
.unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM tc"), 1);
}
#[test]
fn recover_from_fk_violation() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("CREATE TABLE parent (id INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("CREATE TABLE child (id INTEGER PRIMARY KEY, pid INTEGER REFERENCES parent(id))")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
let err = conn
.execute("INSERT INTO child (id, pid) VALUES (1, 99)")
.unwrap_err();
assert!(matches!(err, SqlError::ForeignKeyViolation(_)));
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("INSERT INTO parent (id) VALUES (5)").unwrap();
conn.execute("INSERT INTO child (id, pid) VALUES (1, 5)")
.unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM child"), 1);
}
#[test]
fn partial_batch_insert_recovered() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("INSERT INTO t (id, val) VALUES (5, 'blocker')")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
let err = conn
.execute(
"INSERT INTO t (id, val) VALUES (1, 'a'), (2, 'b'), (5, 'dup'), (3, 'c'), (4, 'd')",
)
.unwrap_err();
assert!(matches!(err, SqlError::DuplicateKey));
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')")
.unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 5);
}
#[test]
fn case_insensitive_savepoint_name() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT My_SP").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'x')")
.unwrap();
conn.execute("ROLLBACK TO my_sp").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 0);
}
#[test]
fn release_without_savepoint_keyword() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp1").unwrap();
conn.execute("RELEASE sp1").unwrap(); conn.execute("COMMIT").unwrap();
}
#[test]
fn rollback_to_without_savepoint_keyword() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp1").unwrap();
conn.execute("ROLLBACK TO sp1").unwrap(); conn.execute("COMMIT").unwrap();
}
#[test]
fn stmt_cache_invalidated_after_rollback_to() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("INSERT INTO t (id, val) VALUES (1, 'pre')")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("UPDATE t SET val = 'mid' WHERE id = 1")
.unwrap();
conn.execute("ALTER TABLE t ADD COLUMN extra INTEGER")
.unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("UPDATE t SET val = 'post' WHERE id = 1")
.unwrap();
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT val FROM t").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("post".into()));
}
#[test]
fn batch_insert_after_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
assert_rows(
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a'), (2, 'b')")
.unwrap(),
2,
);
conn.execute("ROLLBACK TO sp").unwrap();
assert_rows(
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a'), (2, 'b')")
.unwrap(),
2,
);
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 2);
}
#[test]
fn nested_savepoints_with_release_persist() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'base')")
.unwrap();
conn.execute("SAVEPOINT a").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'a_row')")
.unwrap();
conn.execute("SAVEPOINT b").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (3, 'b_row')")
.unwrap();
conn.execute("ROLLBACK TO b").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (4, 'after_b_rollback')")
.unwrap();
conn.execute("RELEASE a").unwrap();
conn.execute("COMMIT").unwrap();
}
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
let qr = conn.query("SELECT id, val FROM t ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 3);
assert_eq!(qr.rows[0][1], Value::Text("base".into()));
assert_eq!(qr.rows[1][1], Value::Text("a_row".into()));
assert_eq!(qr.rows[2][1], Value::Text("after_b_rollback".into()));
}
#[test]
fn savepoint_then_outer_rollback_persists_nothing() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a')")
.unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'b')")
.unwrap();
conn.execute("RELEASE sp").unwrap();
conn.execute("ROLLBACK").unwrap();
}
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 0);
}
#[test]
fn smoke_simple_flow() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a')")
.unwrap();
conn.execute("SAVEPOINT sp1").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'b')")
.unwrap();
conn.execute("ROLLBACK TO sp1").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 1);
}
#[test]
fn smoke_update_delete_mix() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
for i in 1..=5 {
conn.execute(&format!("INSERT INTO t (id, val) VALUES ({i}, 'v{i}')"))
.unwrap();
}
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("UPDATE t SET val = 'updated' WHERE id > 2")
.unwrap();
conn.execute("DELETE FROM t WHERE id = 1").unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT id, val FROM t ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 5);
for (i, row) in qr.rows.iter().enumerate() {
let id = i + 1;
assert_eq!(row[0], Value::Integer(id as i64));
assert_eq!(row[1], Value::Text(format!("v{id}").into()));
}
}
#[test]
fn smoke_many_nested_savepoints() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
for i in 1..=10 {
conn.execute(&format!("SAVEPOINT s{i}")).unwrap();
conn.execute(&format!("INSERT INTO t (id, val) VALUES ({i}, 'row{i}')"))
.unwrap();
}
conn.execute("ROLLBACK TO s5").unwrap();
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT id FROM t ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 4);
for (i, row) in qr.rows.iter().enumerate() {
assert_eq!(row[0], Value::Integer((i + 1) as i64));
}
}
#[test]
fn commit_and_chain_rejected() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
let err = conn.execute("COMMIT AND CHAIN").unwrap_err();
assert!(matches!(err, SqlError::Unsupported(_)));
conn.execute("COMMIT").unwrap();
}
#[test]
fn rollback_and_chain_rejected() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
let err = conn.execute("ROLLBACK AND CHAIN").unwrap_err();
assert!(matches!(err, SqlError::Unsupported(_)));
conn.execute("ROLLBACK").unwrap();
}
#[test]
fn real_bank_transfer_with_fallback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute(
"CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, balance INTEGER NOT NULL CHECK(balance >= 0), frozen INTEGER NOT NULL)",
)
.unwrap();
conn.execute("INSERT INTO accounts (id, name, balance, frozen) VALUES (1, 'Alice', 1000, 0)")
.unwrap();
conn.execute("INSERT INTO accounts (id, name, balance, frozen) VALUES (2, 'Bob', 500, 1)")
.unwrap();
conn.execute("INSERT INTO accounts (id, name, balance, frozen) VALUES (3, 'Carol', 200, 0)")
.unwrap();
conn.execute("BEGIN").unwrap();
assert_rows(
conn.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
.unwrap(),
1,
);
conn.execute("SAVEPOINT try_bob").unwrap();
let frozen = conn
.query("SELECT frozen FROM accounts WHERE id = 2")
.unwrap();
if matches!(frozen.rows[0][0], Value::Integer(1)) {
conn.execute("ROLLBACK TO try_bob").unwrap();
conn.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 3")
.unwrap();
} else {
conn.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
.unwrap();
conn.execute("RELEASE try_bob").unwrap();
}
conn.execute("COMMIT").unwrap();
let qr = conn
.query("SELECT id, balance FROM accounts ORDER BY id")
.unwrap();
assert_eq!(qr.rows[0][1], Value::Integer(900));
assert_eq!(qr.rows[1][1], Value::Integer(500));
assert_eq!(qr.rows[2][1], Value::Integer(300));
let total = count(&conn, "SELECT SUM(balance) FROM accounts");
assert_eq!(total, 1700);
}
#[test]
fn real_bulk_import_with_batch_recovery() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER CHECK(qty > 0))")
.unwrap();
let batches: &[&[(i64, i64)]] = &[
&[(1, 10), (2, 20), (3, 30)],
&[(4, 40), (5, -5), (6, 60)], &[(7, 70), (8, 80)],
&[(9, 90), (10, 100)],
];
conn.execute("BEGIN").unwrap();
let mut imported = 0;
let mut skipped_batches = 0;
for batch in batches {
conn.execute("SAVEPOINT batch_sp").unwrap();
let mut ok = true;
for &(id, qty) in *batch {
if let Err(e) =
conn.execute(&format!("INSERT INTO items (id, qty) VALUES ({id}, {qty})"))
{
ok = false;
assert!(matches!(e, SqlError::CheckViolation(_)));
break;
}
}
if ok {
conn.execute("RELEASE batch_sp").unwrap();
imported += batch.len();
} else {
conn.execute("ROLLBACK TO batch_sp").unwrap();
conn.execute("RELEASE batch_sp").unwrap();
skipped_batches += 1;
}
}
conn.execute("COMMIT").unwrap();
assert_eq!(skipped_batches, 1);
assert_eq!(imported, 7);
assert_eq!(count(&conn, "SELECT COUNT(*) FROM items"), 7);
let ids = conn.query("SELECT id FROM items ORDER BY id").unwrap();
let got: Vec<i64> = ids
.rows
.iter()
.map(|r| match r[0] {
Value::Integer(n) => n,
_ => panic!(),
})
.collect();
assert_eq!(got, vec![1, 2, 3, 7, 8, 9, 10]);
}
#[test]
fn real_fk_cascade_partial_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT NOT NULL)")
.unwrap();
conn.execute(
"CREATE TABLE order_items (id INTEGER PRIMARY KEY, order_id INTEGER NOT NULL REFERENCES orders(id), sku TEXT NOT NULL, qty INTEGER NOT NULL)",
)
.unwrap();
conn.execute("CREATE UNIQUE INDEX uq_order_sku ON order_items (order_id, sku)")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO orders (id, customer) VALUES (1, 'Alice')")
.unwrap();
conn.execute("INSERT INTO order_items (id, order_id, sku, qty) VALUES (1, 1, 'A', 1)")
.unwrap();
conn.execute("SAVEPOINT add_more").unwrap();
conn.execute("INSERT INTO order_items (id, order_id, sku, qty) VALUES (2, 1, 'B', 2)")
.unwrap();
let err = conn
.execute("INSERT INTO order_items (id, order_id, sku, qty) VALUES (3, 1, 'B', 99)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
conn.execute("ROLLBACK TO add_more").unwrap();
conn.execute("INSERT INTO order_items (id, order_id, sku, qty) VALUES (2, 1, 'B', 2)")
.unwrap();
conn.execute("INSERT INTO order_items (id, order_id, sku, qty) VALUES (3, 1, 'C', 3)")
.unwrap();
conn.execute("COMMIT").unwrap();
let qr = conn
.query("SELECT sku, qty FROM order_items ORDER BY sku")
.unwrap();
assert_eq!(qr.rows.len(), 3);
assert_eq!(qr.rows[0][0], Value::Text("A".into()));
assert_eq!(qr.rows[1][0], Value::Text("B".into()));
assert_eq!(qr.rows[2][0], Value::Text("C".into()));
}
#[test]
fn real_join_query_after_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("CREATE TABLE u (id INTEGER PRIMARY KEY, name TEXT)")
.unwrap();
conn.execute("CREATE TABLE p (id INTEGER PRIMARY KEY, uid INTEGER, tag TEXT)")
.unwrap();
conn.execute("CREATE INDEX idx_p_uid ON p (uid)").unwrap();
conn.execute("INSERT INTO u (id, name) VALUES (1, 'alice'), (2, 'bob')")
.unwrap();
conn.execute("INSERT INTO p (id, uid, tag) VALUES (10, 1, 'x'), (11, 1, 'y'), (12, 2, 'z')")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("INSERT INTO p (id, uid, tag) VALUES (13, 1, 'DROPME'), (14, 2, 'DROPME')")
.unwrap();
conn.execute("UPDATE u SET name = 'ALICE-MOD' WHERE id = 1")
.unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
let qr = conn
.query(
"SELECT u.name, COUNT(p.id) FROM u LEFT JOIN p ON p.uid = u.id GROUP BY u.name ORDER BY u.name",
)
.unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][0], Value::Text("alice".into()));
assert_eq!(qr.rows[0][1], Value::Integer(2));
assert_eq!(qr.rows[1][0], Value::Text("bob".into()));
assert_eq!(qr.rows[1][1], Value::Integer(1));
}
#[test]
fn real_view_rolled_back() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("INSERT INTO t (id, val) VALUES (1, 'a'), (2, 'b')")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("CREATE VIEW v AS SELECT * FROM t").unwrap();
let qr = conn.query("SELECT COUNT(*) FROM v").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(2));
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("COMMIT").unwrap();
let err = conn.query("SELECT * FROM v").unwrap_err();
assert!(matches!(err, SqlError::TableNotFound(_)));
}
#[test]
fn real_deep_nesting_30_levels() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
for i in 1..=30 {
conn.execute(&format!("SAVEPOINT sp{i}")).unwrap();
conn.execute(&format!("INSERT INTO t (id, val) VALUES ({i}, 'lvl{i}')"))
.unwrap();
}
conn.execute("ROLLBACK TO sp5").unwrap();
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT id FROM t ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 4);
for (i, row) in qr.rows.iter().enumerate() {
assert_eq!(row[0], Value::Integer((i + 1) as i64));
}
}
#[test]
fn real_large_rollback_to() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
for i in 1..=50 {
conn.execute(&format!("INSERT INTO t (id, val) VALUES ({i}, 'base{i}')"))
.unwrap();
}
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
for i in 100..=2100 {
conn.execute(&format!("INSERT INTO t (id, val) VALUES ({i}, 'post{i}')"))
.unwrap();
}
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1000000, 'final')")
.unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 51);
let ids: Vec<i64> = conn
.query("SELECT id FROM t WHERE id > 100 ORDER BY id")
.unwrap()
.rows
.iter()
.map(|r| match r[0] {
Value::Integer(n) => n,
_ => panic!(),
})
.collect();
assert_eq!(ids, vec![1_000_000]);
}
#[test]
fn real_triple_shadow_peel() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'l1')")
.unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (2, 'l2')")
.unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (3, 'l3')")
.unwrap();
conn.execute("RELEASE sp").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 3);
conn.execute("ROLLBACK TO sp").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 1);
conn.execute("RELEASE sp").unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 0);
conn.execute("COMMIT").unwrap();
}
#[test]
fn real_repeated_rollback_to_same() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO t (id, val) VALUES (1, 'base')")
.unwrap();
conn.execute("SAVEPOINT sp").unwrap();
for attempt in 1..=5 {
conn.execute(&format!(
"INSERT INTO t (id, val) VALUES ({attempt}0, 'attempt')"
))
.unwrap();
conn.execute(&format!(
"INSERT INTO t (id, val) VALUES ({attempt}1, 'more')"
))
.unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 3);
conn.execute("ROLLBACK TO sp").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 1);
}
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 1);
let qr = conn.query("SELECT val FROM t").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("base".into()));
}
#[test]
fn real_reader_isolation_during_savepoint() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let writer = Connection::open(&db).unwrap();
setup(&writer);
writer
.execute("INSERT INTO t (id, val) VALUES (1, 'committed')")
.unwrap();
let reader = Connection::open(&db).unwrap();
let before = reader.query("SELECT val FROM t").unwrap();
assert_eq!(before.rows[0][0], Value::Text("committed".into()));
writer.execute("BEGIN").unwrap();
writer.execute("SAVEPOINT sp").unwrap();
writer
.execute("INSERT INTO t (id, val) VALUES (2, 'uncommitted')")
.unwrap();
let during = reader.query("SELECT COUNT(*) FROM t").unwrap();
assert_eq!(during.rows[0][0], Value::Integer(1));
writer.execute("ROLLBACK TO sp").unwrap();
writer.execute("COMMIT").unwrap();
let after = reader.query("SELECT COUNT(*) FROM t").unwrap();
assert_eq!(after.rows[0][0], Value::Integer(1));
}
#[test]
fn real_multicol_unique_index_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("CREATE TABLE m (id INTEGER PRIMARY KEY, a INTEGER NOT NULL, b INTEGER NOT NULL)")
.unwrap();
conn.execute("CREATE UNIQUE INDEX uq_ab ON m (a, b)")
.unwrap();
conn.execute("INSERT INTO m (id, a, b) VALUES (1, 1, 1), (2, 1, 2), (3, 2, 1)")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("DELETE FROM m WHERE id = 1").unwrap();
conn.execute("INSERT INTO m (id, a, b) VALUES (4, 1, 1)")
.unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
conn.execute("SAVEPOINT try").unwrap();
let err = conn
.execute("INSERT INTO m (id, a, b) VALUES (5, 1, 1)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
conn.execute("ROLLBACK TO try").unwrap();
conn.execute("INSERT INTO m (id, a, b) VALUES (5, 3, 3)")
.unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM m"), 4);
}
#[test]
fn real_aggregate_after_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("CREATE TABLE s (id INTEGER PRIMARY KEY, category TEXT, amount INTEGER)")
.unwrap();
for i in 1..=20 {
let cat = if i % 2 == 0 { "even" } else { "odd" };
conn.execute(&format!(
"INSERT INTO s (id, category, amount) VALUES ({i}, '{cat}', {})",
i * 10
))
.unwrap();
}
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT sp").unwrap();
conn.execute("UPDATE s SET amount = amount * 100").unwrap();
conn.execute("DELETE FROM s WHERE id > 10").unwrap();
conn.execute("ROLLBACK TO sp").unwrap();
let qr = conn
.query("SELECT category, SUM(amount), COUNT(*) FROM s GROUP BY category ORDER BY category")
.unwrap();
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][0], Value::Text("even".into()));
assert_eq!(qr.rows[0][1], Value::Integer(1100));
assert_eq!(qr.rows[0][2], Value::Integer(10));
assert_eq!(qr.rows[1][0], Value::Text("odd".into()));
assert_eq!(qr.rows[1][1], Value::Integer(1000));
assert_eq!(qr.rows[1][2], Value::Integer(10));
conn.execute("COMMIT").unwrap();
}
#[test]
fn real_prepared_insert_across_savepoints() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup(&conn);
conn.execute("BEGIN").unwrap();
for i in 1..=5 {
conn.execute_params(
"INSERT INTO t (id, val) VALUES ($1, $2)",
&[Value::Integer(i), Value::Text(format!("v{i}").into())],
)
.unwrap();
}
conn.execute("SAVEPOINT sp").unwrap();
for i in 6..=10 {
conn.execute_params(
"INSERT INTO t (id, val) VALUES ($1, $2)",
&[Value::Integer(i), Value::Text(format!("v{i}").into())],
)
.unwrap();
}
conn.execute("ROLLBACK TO sp").unwrap();
for i in 6..=8 {
conn.execute_params(
"INSERT INTO t (id, val) VALUES ($1, $2)",
&[Value::Integer(i), Value::Text(format!("new{i}").into())],
)
.unwrap();
}
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT id, val FROM t ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 8);
assert_eq!(qr.rows[5][1], Value::Text("new6".into()));
assert_eq!(qr.rows[6][1], Value::Text("new7".into()));
assert_eq!(qr.rows[7][1], Value::Text("new8".into()));
}
#[test]
fn real_mixed_workload() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute(
"CREATE TABLE events (id INTEGER PRIMARY KEY, type TEXT NOT NULL, amount INTEGER NOT NULL)",
)
.unwrap();
conn.execute("CREATE INDEX idx_type ON events (type)")
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO events (id, type, amount) VALUES (1, 'seed', 10)")
.unwrap();
conn.execute("SAVEPOINT phase1").unwrap();
conn.execute("INSERT INTO events (id, type, amount) VALUES (2, 'a', 20)")
.unwrap();
conn.execute("INSERT INTO events (id, type, amount) VALUES (3, 'a', 30)")
.unwrap();
conn.execute("SAVEPOINT phase2").unwrap();
conn.execute("ALTER TABLE events ADD COLUMN tag TEXT")
.unwrap();
conn.execute("UPDATE events SET tag = 'auto' WHERE id > 1")
.unwrap();
let qr = conn
.query("SELECT COUNT(*) FROM events WHERE tag = 'auto'")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(2));
conn.execute("ROLLBACK TO phase2").unwrap();
let err = conn.query("SELECT tag FROM events").unwrap_err();
assert!(matches!(err, SqlError::ColumnNotFound(_)));
conn.execute("INSERT INTO events (id, type, amount) VALUES (4, 'b', 40)")
.unwrap();
conn.execute("RELEASE phase1").unwrap();
conn.execute("COMMIT").unwrap();
let qr = conn
.query("SELECT COUNT(*), SUM(amount) FROM events")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(4));
assert_eq!(qr.rows[0][1], Value::Integer(100));
}
#[test]
fn real_savepoint_name_matches_table() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)")
.unwrap();
conn.execute("INSERT INTO t (id) VALUES (1)").unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("SAVEPOINT t").unwrap(); conn.execute("INSERT INTO t (id) VALUES (2)").unwrap();
conn.execute("ROLLBACK TO t").unwrap();
conn.execute("COMMIT").unwrap();
assert_eq!(count(&conn, "SELECT COUNT(*) FROM t"), 1);
}
#[test]
fn real_complex_error_recovery_flow() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute(
"CREATE TABLE inv (sku TEXT PRIMARY KEY, qty INTEGER NOT NULL CHECK(qty >= 0))",
)
.unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("INSERT INTO inv (sku, qty) VALUES ('A', 100)")
.unwrap();
conn.execute("INSERT INTO inv (sku, qty) VALUES ('B', 50)")
.unwrap();
conn.execute("SAVEPOINT alloc").unwrap();
let err = conn
.execute("UPDATE inv SET qty = qty - 150 WHERE sku = 'A'")
.unwrap_err();
assert!(matches!(err, SqlError::CheckViolation(_)));
conn.execute("ROLLBACK TO alloc").unwrap();
conn.execute("UPDATE inv SET qty = qty - 100 WHERE sku = 'A'")
.unwrap();
conn.execute("SAVEPOINT ship_b").unwrap();
conn.execute("UPDATE inv SET qty = 0 WHERE sku = 'B'")
.unwrap();
conn.execute("ROLLBACK TO ship_b").unwrap();
conn.execute("UPDATE inv SET qty = 10 WHERE sku = 'B'")
.unwrap();
conn.execute("RELEASE alloc").unwrap();
conn.execute("COMMIT").unwrap();
}
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
let qr = conn.query("SELECT sku, qty FROM inv ORDER BY sku").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("A".into()));
assert_eq!(qr.rows[0][1], Value::Integer(0));
assert_eq!(qr.rows[1][0], Value::Text("B".into()));
assert_eq!(qr.rows[1][1], Value::Integer(10));
}