use spg_embedded::{Database, FreezerOptions};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;
struct Scratch {
path: PathBuf,
}
impl Scratch {
fn new(label: &str) -> Self {
let mut p = std::env::temp_dir();
let nanos: u64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
p.push(format!(
"spg-embedded-autocompact-{label}-{nanos}-{}",
std::process::id()
));
std::fs::create_dir_all(&p).unwrap();
Self { path: p }
}
fn db_path(&self) -> PathBuf {
self.path.join("app.db")
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
#[test]
fn freezer_produces_segments_then_compacts_them() {
let scratch = Scratch::new("compact");
let p = scratch.db_path();
let db = Arc::new(Mutex::new(Database::open_path(&p).unwrap()));
{
let mut g = db.lock().unwrap();
g.execute("CREATE TABLE t (id INT NOT NULL, payload TEXT)")
.unwrap();
g.execute("CREATE INDEX t_pk ON t (id)").unwrap();
for i in 0..2_000 {
g.execute(&format!("INSERT INTO t VALUES ({i}, 'x')"))
.unwrap();
}
}
let mut freezer = Database::spawn_background_freezer(
db.clone(),
FreezerOptions {
tick: Duration::from_millis(10),
hot_tier_bytes: 256,
batch_rows: 20, compact_when_segments_exceed: 8,
compact_target_bytes: 1 << 30, },
);
let mut max_seen = 0usize;
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while std::time::Instant::now() < deadline {
let count = {
let g = db.lock().unwrap();
g.cold_segment_count()
};
max_seen = max_seen.max(count);
std::thread::sleep(Duration::from_millis(20));
}
freezer.stop();
for probe in [0, 500, 1000, 1500, 1999] {
let count = {
let mut g = db.lock().unwrap();
match g
.execute(&format!("SELECT id FROM t WHERE id = {probe}"))
.unwrap()
{
spg_embedded::QueryResult::Rows { rows, .. } => rows.len(),
_ => 0,
}
};
assert_eq!(count, 1, "row id={probe} lost across compaction");
}
eprintln!("auto-compact test: max_seen={max_seen}");
assert!(
max_seen <= 16,
"auto-compact should bound segments near threshold; got max_seen={max_seen}"
);
}
#[test]
fn auto_compact_disabled_when_threshold_is_max() {
let scratch = Scratch::new("nocompact");
let p = scratch.db_path();
let db = Arc::new(Mutex::new(Database::open_path(&p).unwrap()));
{
let mut g = db.lock().unwrap();
g.execute("CREATE TABLE t (id INT NOT NULL, payload TEXT)")
.unwrap();
g.execute("CREATE INDEX t_pk ON t (id)").unwrap();
for i in 0..500 {
g.execute(&format!("INSERT INTO t VALUES ({i}, 'x')"))
.unwrap();
}
}
let mut freezer = Database::spawn_background_freezer(
db.clone(),
FreezerOptions {
tick: Duration::from_millis(20),
hot_tier_bytes: 128,
batch_rows: 50,
compact_when_segments_exceed: usize::MAX,
compact_target_bytes: 1 << 30,
},
);
std::thread::sleep(Duration::from_millis(400));
let count = {
let g = db.lock().unwrap();
g.cold_segment_count()
};
freezer.stop();
assert!(
count >= 4,
"expected ≥ 4 segments when compaction disabled, got {count}"
);
}