use mongreldb_core::query::{Condition, Query};
use mongreldb_core::{schema::*, Database, Value};
use tempfile::tempdir;
fn pk_schema() -> Schema {
Schema {
schema_id: 1,
columns: vec![ColumnDef {
id: 1,
name: "v".into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
default_value: None,
}],
indexes: vec![],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
#[test]
fn transaction_larger_than_threshold_spills_and_commits_atomically() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("t", pk_schema()).unwrap();
db.set_spill_threshold(1);
let n: u64 = 100;
db.transaction(|t| {
for i in 0..n {
t.put("t", vec![(1, Value::Int64(i as i64))])?;
}
Ok(())
})
.unwrap();
assert_eq!(db.table("t").unwrap().lock().count(), n);
let tdir = dir
.path()
.join("tables")
.join(db.table_id("t").unwrap().to_string());
let txn_dir = tdir.join("_txn");
assert!(
!txn_dir.exists() || std::fs::read_dir(&txn_dir).unwrap().next().is_none(),
"pending _txn/ dir should be empty after commit"
);
drop(db);
let db = Database::open(dir.path()).unwrap();
assert_eq!(db.table("t").unwrap().lock().count(), n);
}
#[test]
fn stale_txn_dir_is_swept_on_reopen() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("t", pk_schema()).unwrap();
let table_id = db.table_id("t").unwrap();
let tdir = dir.path().join("tables").join(table_id.to_string());
let stale = tdir.join("_txn").join("99999");
std::fs::create_dir_all(&stale).unwrap();
std::fs::write(stale.join("r-1.sr"), b"stale").unwrap();
drop(db);
let db = Database::open(dir.path()).unwrap();
assert!(!stale.exists(), "stale _txn/ dir must be swept on reopen");
assert_eq!(db.table("t").unwrap().lock().count(), 0);
}
#[test]
fn huge_writeset_pre_validation_keeps_sequencer_bounded() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("t", pk_schema()).unwrap();
let n: u64 = 500;
db.transaction(|t| {
for i in 0..n {
t.put("t", vec![(1, Value::Int64(i as i64))])?;
}
Ok(())
})
.unwrap();
assert_eq!(db.table("t").unwrap().lock().count(), n);
db.transaction(|t| {
for i in n..(2 * n) {
t.put("t", vec![(1, Value::Int64(i as i64))])?;
}
Ok(())
})
.unwrap();
assert_eq!(db.table("t").unwrap().lock().count(), 2 * n);
}
#[test]
fn spilled_txn_does_not_materialize_rows_in_memtable() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("t", pk_schema()).unwrap();
db.set_spill_threshold(1);
let n: u64 = 200;
db.transaction(|t| {
for i in 0..n {
t.put("t", vec![(1, Value::Int64(i as i64))])?;
}
Ok(())
})
.unwrap();
let tbl = db.table("t").unwrap();
let mut g = tbl.lock();
assert_eq!(g.count(), n, "all spilled rows must be visible");
assert_eq!(
g.memtable_len(),
0,
"spilled rows must not be materialized in the memtable"
);
let q = Query::new().and(Condition::Range {
column_id: 1,
lo: 0,
hi: (n as i64) - 1,
});
assert_eq!(g.query(&q).unwrap().len(), n as usize);
}
#[test]
fn spilled_run_respects_snapshot_isolation() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("t", pk_schema()).unwrap();
db.set_spill_threshold(1);
let tbl = db.table("t").unwrap();
let snap_before = tbl.lock().snapshot();
db.transaction(|t| {
for i in 0..50u64 {
t.put("t", vec![(1, Value::Int64(i as i64))])?;
}
Ok(())
})
.unwrap();
let g = tbl.lock();
let before = g.visible_rows(snap_before).unwrap();
assert_eq!(
before.len(),
0,
"snapshot pinned before commit must not see spilled rows"
);
assert_eq!(g.count(), 50, "current readers see all committed rows");
}
#[test]
fn spilled_run_relink_is_idempotent_across_reopens() {
let dir = tempdir().unwrap();
{
let db = Database::create(dir.path()).unwrap();
db.create_table("t", pk_schema()).unwrap();
db.set_spill_threshold(1);
db.transaction(|t| {
for i in 0..40i64 {
t.put("t", vec![(1, Value::Int64(i))])?;
}
Ok(())
})
.unwrap();
assert_eq!(db.table("t").unwrap().lock().count(), 40);
}
for _ in 0..3 {
let db = Database::open(dir.path()).unwrap();
let tbl = db.table("t").unwrap();
let mut g = tbl.lock();
assert_eq!(g.count(), 40, "count must stay stable across reopens");
let q = Query::new().and(Condition::Range {
column_id: 1,
lo: 0,
hi: 39,
});
assert_eq!(g.query(&q).unwrap().len(), 40);
drop(g);
assert!(
db.check().is_empty(),
"check reports no integrity issues: {:?}",
db.check()
);
}
}
#[test]
fn spilled_txn_recovers_run_from_wal_after_crash() {
let dir = tempdir().unwrap();
{
let db = Database::create(dir.path()).unwrap();
db.create_table("t", pk_schema()).unwrap();
db.set_spill_threshold(1);
db.transaction(|t| {
for i in 0..50i64 {
t.put("t", vec![(1, Value::Int64(i))])?;
}
Ok(())
})
.unwrap();
assert_eq!(db.table("t").unwrap().lock().count(), 50);
}
let db = Database::open(dir.path()).unwrap();
assert_eq!(
db.table("t").unwrap().lock().count(),
50,
"spilled run data must survive reopen"
);
}