use mongreldb_core::{schema::*, Database, Value};
use std::sync::{mpsc, Arc};
use tempfile::tempdir;
fn make_schema() -> Schema {
Schema {
schema_id: 0,
columns: vec![
ColumnDef {
id: 1,
name: "id".into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
default_value: None,
},
ColumnDef {
id: 2,
name: "name".into(),
ty: TypeId::Bytes,
flags: ColumnFlags::empty(),
default_value: None,
},
],
indexes: vec![],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
#[test]
fn checkpoint_produces_stable_directory() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("items", make_schema()).unwrap();
let handle = db.table("items").unwrap();
{
let mut t = handle.lock();
t.put(vec![
(1, Value::Int64(1)),
(2, Value::Bytes(b"alice".to_vec())),
])
.unwrap();
t.put(vec![
(1, Value::Int64(2)),
(2, Value::Bytes(b"bob".to_vec())),
])
.unwrap();
t.put(vec![
(1, Value::Int64(3)),
(2, Value::Bytes(b"carol".to_vec())),
])
.unwrap();
t.commit().unwrap();
}
assert_eq!(handle.lock().count(), 3);
db.checkpoint().unwrap();
let wal_dir = dir.path().join("_wal");
let segments: Vec<_> = std::fs::read_dir(&wal_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "wal"))
.collect();
assert!(
segments.len() <= 1,
"expected at most 1 WAL segment after checkpoint, found {}",
segments.len()
);
if let Some(seg) = segments.first() {
let size = seg.metadata().unwrap().len();
assert!(
size < 1024,
"WAL segment should be header-only after checkpoint, got {} bytes",
size
);
}
let tables_dir = dir.path().join("tables");
for entry in std::fs::read_dir(&tables_dir).unwrap() {
let entry = entry.unwrap();
let runs_dir = entry.path().join("_runs");
if runs_dir.exists() {
let runs: Vec<_> = std::fs::read_dir(&runs_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "sr"))
.collect();
assert_eq!(
runs.len(),
1,
"expected exactly 1 sorted run per table after checkpoint"
);
}
}
}
#[test]
fn checkpoint_preserves_data_after_reopen() {
let dir = tempdir().unwrap();
{
let db = Database::create(dir.path()).unwrap();
db.create_table("data", make_schema()).unwrap();
let handle = db.table("data").unwrap();
{
let mut t = handle.lock();
t.put(vec![
(1, Value::Int64(42)),
(2, Value::Bytes(b"answer".to_vec())),
])
.unwrap();
t.commit().unwrap();
}
db.checkpoint().unwrap();
}
let db = Database::open(dir.path()).unwrap();
let handle = db.table("data").unwrap();
let mut g = handle.lock();
assert_eq!(g.count(), 1, "count should include all rows");
use mongreldb_core::query::{Condition, Query};
let key = Value::Int64(42).encode_key();
let mut q = Query::new();
q = q.and(Condition::Pk(key));
let rows = g.query(&q).unwrap();
assert_eq!(rows.len(), 1, "PK query should find the row");
}
#[test]
fn commit_after_checkpoint_uses_the_new_durable_wal() {
let dir = tempdir().unwrap();
{
let db = Database::create(dir.path()).unwrap();
db.create_table("data", make_schema()).unwrap();
db.transaction(|transaction| {
transaction.put(
"data",
vec![(1, Value::Int64(1)), (2, Value::Bytes(b"before".to_vec()))],
)
})
.unwrap();
db.checkpoint().unwrap();
db.transaction(|transaction| {
transaction.put(
"data",
vec![(1, Value::Int64(2)), (2, Value::Bytes(b"after".to_vec()))],
)
})
.unwrap();
}
let db = Database::open(dir.path()).unwrap();
assert_eq!(db.table("data").unwrap().lock().count(), 2);
}
#[test]
fn checkpoint_blocks_commit_until_wal_reset_finishes() {
let dir = tempdir().unwrap();
let db = Arc::new(Database::create(dir.path()).unwrap());
db.create_table("data", make_schema()).unwrap();
db.transaction(|transaction| {
transaction.put(
"data",
vec![(1, Value::Int64(1)), (2, Value::Bytes(b"before".to_vec()))],
)
})
.unwrap();
let (at_reset_tx, at_reset_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel();
let checkpoint_db = Arc::clone(&db);
let checkpoint = std::thread::spawn(move || {
checkpoint_db.checkpoint_controlled(|| {
at_reset_tx.send(()).unwrap();
release_rx.recv().unwrap();
Ok(())
})
});
at_reset_rx.recv().unwrap();
let (commit_started_tx, commit_started_rx) = mpsc::channel();
let (commit_done_tx, commit_done_rx) = mpsc::channel();
let commit_db = Arc::clone(&db);
let commit = std::thread::spawn(move || {
commit_started_tx.send(()).unwrap();
let result = commit_db.transaction(|transaction| {
transaction.put(
"data",
vec![(1, Value::Int64(2)), (2, Value::Bytes(b"after".to_vec()))],
)
});
commit_done_tx.send(result).unwrap();
});
commit_started_rx.recv().unwrap();
assert!(commit_done_rx
.recv_timeout(std::time::Duration::from_millis(50))
.is_err());
release_tx.send(()).unwrap();
checkpoint.join().unwrap().unwrap();
commit_done_rx.recv().unwrap().unwrap();
commit.join().unwrap();
drop(db);
let reopened = Database::open(dir.path()).unwrap();
assert_eq!(reopened.table("data").unwrap().lock().count(), 2);
}
#[test]
fn failed_strict_flush_keeps_old_wal_for_recovery() {
let dir = tempdir().unwrap();
let table_id;
{
let db = Database::create(dir.path()).unwrap();
table_id = db.create_table("data", make_schema()).unwrap();
db.transaction(|transaction| {
transaction.put(
"data",
vec![(1, Value::Int64(1)), (2, Value::Bytes(b"value".to_vec()))],
)
})
.unwrap();
let manifest = dir
.path()
.join("tables")
.join(table_id.to_string())
.join("_mf");
let saved_manifest = manifest.with_extension("saved");
std::fs::rename(&manifest, &saved_manifest).unwrap();
std::fs::create_dir(&manifest).unwrap();
assert!(db.checkpoint().is_err());
let wal_files = std::fs::read_dir(dir.path().join("_wal"))
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "wal"))
.count();
assert!(wal_files >= 1);
std::fs::remove_dir(&manifest).unwrap();
std::fs::rename(&saved_manifest, &manifest).unwrap();
}
let reopened = Database::open(dir.path()).unwrap();
assert_eq!(reopened.table("data").unwrap().lock().count(), 1);
}
#[test]
fn snapshot_is_idempotent() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("t", make_schema()).unwrap();
let handle = db.table("t").unwrap();
{
let mut t = handle.lock();
t.put(vec![(1, Value::Int64(1)), (2, Value::Bytes(b"x".to_vec()))])
.unwrap();
t.commit().unwrap();
}
db.checkpoint().unwrap();
let listing1: Vec<String> = list_all_files(dir.path());
db.checkpoint().unwrap();
let listing2: Vec<String> = list_all_files(dir.path());
assert_eq!(
listing1.len(),
listing2.len(),
"directory file count should be stable after repeated checkpoints"
);
}
fn list_all_files(root: &std::path::Path) -> Vec<String> {
let mut files = Vec::new();
if root.is_dir() {
for entry in std::fs::read_dir(root).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
files.extend(list_all_files(&path));
} else {
files.push(
path.strip_prefix(root)
.unwrap()
.to_string_lossy()
.into_owned(),
);
}
}
}
files.sort();
files
}