use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::{DataType, SqlValue};
use crate::database::Database;
use crate::row::Row;
fn make_users_table(db: &mut Database) {
let columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new("name".to_string(), DataType::Varchar { max_length: Some(50) }, true),
];
let schema = TableSchema::with_primary_key(
"users".to_string(),
columns,
vec!["id".to_string()],
);
db.create_table(schema).unwrap();
}
fn user_row(id: i64, name: &str) -> Row {
Row::from_vec(vec![SqlValue::Integer(id), SqlValue::Varchar(arcstr::ArcStr::from(name))])
}
#[test]
fn vacuum_mvcc_on_empty_database_returns_zero() {
let mut db = Database::new();
let reclaimed = db.vacuum_mvcc().unwrap();
assert_eq!(reclaimed, 0);
}
#[test]
fn vacuum_mvcc_with_no_stamped_tombstones_is_a_noop() {
let mut db = Database::new();
make_users_table(&mut db);
db.insert_row("users", user_row(1, "Alice")).unwrap();
db.insert_row("users", user_row(2, "Bob")).unwrap();
db.insert_row("users", user_row(3, "Charlie")).unwrap();
let reclaimed = db.vacuum_mvcc().unwrap();
assert_eq!(reclaimed, 0);
let table = db.get_table("users").unwrap();
assert_eq!(table.row_count(), 3);
}
#[test]
fn vacuum_mvcc_refuses_to_run_inside_transaction() {
let mut db = Database::new();
make_users_table(&mut db);
db.begin_transaction().unwrap();
let err = db.vacuum_mvcc().expect_err("vacuum must refuse mid-transaction");
let msg = format!("{err}");
assert!(
msg.contains("vacuum_mvcc"),
"error message should mention vacuum_mvcc, got: {msg}"
);
db.rollback_transaction().unwrap();
}
#[cfg(feature = "mvcc_enabled")]
#[test]
fn vacuum_mvcc_reclaims_committed_deletions() {
let mut db = Database::new();
make_users_table(&mut db);
db.insert_row("users", user_row(1, "Alice")).unwrap();
db.insert_row("users", user_row(2, "Bob")).unwrap();
db.insert_row("users", user_row(3, "Charlie")).unwrap();
{
let table = db.get_table_mut("users").unwrap();
table.stamp_row_xmax_inplace(1, 1);
}
assert_eq!(db.compute_gc_horizon(), 1);
let reclaimed = db.vacuum_mvcc().unwrap();
assert_eq!(reclaimed, 0, "row at horizon boundary must be retained");
db.begin_transaction().unwrap();
db.commit_transaction().unwrap();
assert_eq!(db.compute_gc_horizon(), 2);
let reclaimed = db.vacuum_mvcc().unwrap();
assert_eq!(reclaimed, 1, "row stamped before horizon must be reclaimed");
let table = db.get_table("users").unwrap();
assert_eq!(table.row_count(), 2);
}
#[cfg(feature = "mvcc_enabled")]
#[test]
fn vacuum_mvcc_preserves_subsequent_query_correctness() {
let mut db = Database::new();
make_users_table(&mut db);
db.insert_row("users", user_row(1, "Alice")).unwrap();
db.insert_row("users", user_row(2, "Bob")).unwrap();
db.insert_row("users", user_row(3, "Charlie")).unwrap();
db.insert_row("users", user_row(4, "Dave")).unwrap();
{
let table = db.get_table_mut("users").unwrap();
table.stamp_row_xmax_inplace(0, 1);
table.stamp_row_xmax_inplace(2, 1);
}
db.begin_transaction().unwrap();
db.commit_transaction().unwrap();
let reclaimed = db.vacuum_mvcc().unwrap();
assert_eq!(reclaimed, 2);
let table = db.get_table("users").unwrap();
assert_eq!(table.row_count(), 2);
let pk_index =
table.primary_key_index().expect("PK index must survive GC compaction");
assert!(pk_index.contains_key(&vec![SqlValue::Integer(2)]));
assert!(pk_index.contains_key(&vec![SqlValue::Integer(4)]));
assert!(
!pk_index.contains_key(&vec![SqlValue::Integer(1)]),
"reclaimed row's PK must be gone from index"
);
assert!(
!pk_index.contains_key(&vec![SqlValue::Integer(3)]),
"reclaimed row's PK must be gone from index"
);
}
#[test]
fn gc_horizon_pin_holds_horizon_and_release_restores_it() {
let mut db = Database::new();
assert_eq!(db.compute_gc_horizon(), 1);
let pin = db.pin_gc_horizon();
for _ in 0..2 {
db.begin_transaction().unwrap();
db.commit_transaction().unwrap();
}
assert_eq!(db.compute_gc_horizon(), 1, "pin must hold the horizon back");
db.release_gc_horizon(pin);
assert_eq!(db.compute_gc_horizon(), 3, "release must let the horizon advance");
db.release_gc_horizon(9999);
assert_eq!(db.compute_gc_horizon(), 3);
}
#[test]
fn gc_horizon_pins_combine_to_the_minimum() {
let mut db = Database::new();
let early_pin = db.pin_gc_horizon();
db.begin_transaction().unwrap();
db.commit_transaction().unwrap();
let late_pin = db.pin_gc_horizon();
db.begin_transaction().unwrap();
db.commit_transaction().unwrap();
assert_eq!(db.compute_gc_horizon(), 1, "lowest pin wins");
db.release_gc_horizon(early_pin);
assert_eq!(db.compute_gc_horizon(), 1);
db.release_gc_horizon(late_pin);
assert_eq!(db.compute_gc_horizon(), 3);
}
#[test]
fn gc_horizon_pin_does_not_block_transactions() {
let mut db = Database::new();
make_users_table(&mut db);
let pin = db.pin_gc_horizon();
db.begin_transaction().unwrap();
db.insert_row("users", user_row(1, "Alice")).unwrap();
db.commit_transaction().unwrap();
db.release_gc_horizon(pin);
assert_eq!(db.get_table("users").unwrap().row_count(), 1);
}
#[cfg(feature = "mvcc_enabled")]
#[test]
fn vacuum_mvcc_held_back_by_gc_horizon_pin() {
let mut db = Database::new();
make_users_table(&mut db);
db.insert_row("users", user_row(1, "Alice")).unwrap();
db.insert_row("users", user_row(2, "Bob")).unwrap();
{
let table = db.get_table_mut("users").unwrap();
table.stamp_row_xmax_inplace(0, 1);
}
let pin = db.pin_gc_horizon();
db.begin_transaction().unwrap();
db.commit_transaction().unwrap();
assert_eq!(db.vacuum_mvcc().unwrap(), 0, "pinned horizon must block reclamation");
assert_eq!(db.get_table("users").unwrap().row_count(), 2);
db.release_gc_horizon(pin);
assert_eq!(db.vacuum_mvcc().unwrap(), 1, "released pin must allow reclamation");
assert_eq!(db.get_table("users").unwrap().row_count(), 1);
}
#[test]
fn set_next_txn_id_controls_allocation_and_rejects_active_txn() {
let mut db = Database::new();
db.set_next_txn_id(42).unwrap();
db.begin_transaction().unwrap();
assert_eq!(db.transaction_id(), Some(42), "next BEGIN must use the overridden id");
let err = db.set_next_txn_id(99).expect_err("must refuse inside a transaction");
assert!(format!("{err}").contains("set_next_txn_id"), "unexpected error: {err}");
db.commit_transaction().unwrap();
db.set_next_txn_id(99).unwrap();
db.begin_transaction().unwrap();
assert_eq!(db.transaction_id(), Some(99));
db.rollback_transaction().unwrap();
}
#[cfg(feature = "mvcc_enabled")]
#[test]
fn vacuum_mvcc_horizon_held_back_by_active_transaction() {
let mut db = Database::new();
make_users_table(&mut db);
db.insert_row("users", user_row(1, "Alice")).unwrap();
{
let table = db.get_table_mut("users").unwrap();
table.stamp_row_xmax_inplace(0, 1);
}
assert_eq!(db.compute_gc_horizon(), 1);
db.begin_transaction().unwrap();
let horizon_during_txn = db.compute_gc_horizon();
let snap = db.current_snapshot().unwrap();
assert_eq!(horizon_during_txn, snap.xmin_active);
db.commit_transaction().unwrap();
}