use grounddb::Store;
use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;
const SCHEMA: &str = r#"collections:
messages:
path: "{recipient}/{id}.md"
fields:
recipient: { type: string, required: true }
subject: { type: string, required: true }
content: true
"#;
type IndexRow = (
String, String, String, String, Option<String>, Option<String>, Option<String>, );
fn snapshot(dir: &Path) -> Vec<IndexRow> {
let conn = rusqlite::Connection::open(dir.join("_system.db")).unwrap();
let mut stmt = conn
.prepare(
"SELECT id, collection, path, data_json, created_at, modified_at, content_text
FROM documents ORDER BY collection, id",
)
.unwrap();
let rows = stmt
.query_map([], |r| {
Ok((
r.get(0)?,
r.get(1)?,
r.get(2)?,
r.get(3)?,
r.get(4)?,
r.get(5)?,
r.get(6)?,
))
})
.unwrap();
rows.map(|r| r.unwrap()).collect()
}
fn paths(rows: &[IndexRow]) -> Vec<String> {
rows.iter().map(|r| r.2.clone()).collect()
}
fn write_message(dir: &Path, recipient: &str, id: &str, subject: &str, body: &str) {
let bx = dir.join(recipient);
std::fs::create_dir_all(&bx).unwrap();
std::fs::write(
bx.join(format!("{id}.md")),
format!("---\nrecipient: {recipient}\nsubject: {subject}\n---\n\n{body}\n"),
)
.unwrap();
}
fn warm_store(n: usize) -> TempDir {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("schema.yaml"), SCHEMA).unwrap();
for i in 0..n {
write_message(
tmp.path(),
if i % 2 == 0 { "alice" } else { "bob" },
&format!("msg{i:03}"),
&format!("subject {i}"),
&format!("body {i}"),
);
}
drop(Store::open(tmp.path().to_str().unwrap()).unwrap());
tmp
}
fn let_the_clock_tick() {
std::thread::sleep(Duration::from_millis(50));
}
fn assert_incremental_matches_full_rescan(dir: &Path) -> Vec<IndexRow> {
let store = Store::open(dir.to_str().unwrap()).unwrap();
drop(store);
let incremental = snapshot(dir);
let store = Store::open(dir.to_str().unwrap()).unwrap();
store.rebuild(None).unwrap();
drop(store);
let full = snapshot(dir);
assert_eq!(
incremental, full,
"incremental scan and full rescan disagree about the index"
);
incremental
}
#[test]
fn added_file_is_indexed_and_matches_full_rescan() {
let tmp = warm_store(4);
let dir = tmp.path();
write_message(dir, "carol", "msg999", "late arrival", "hello");
let rows = assert_incremental_matches_full_rescan(dir);
assert_eq!(rows.len(), 5);
assert!(paths(&rows).contains(&"carol/msg999.md".to_string()));
}
#[test]
fn modified_file_is_reread_and_matches_full_rescan() {
let tmp = warm_store(4);
let dir = tmp.path();
let_the_clock_tick();
write_message(dir, "alice", "msg000", "edited subject", "edited body");
let rows = assert_incremental_matches_full_rescan(dir);
assert_eq!(rows.len(), 4);
let edited = rows.iter().find(|r| r.0 == "msg000").unwrap();
assert!(
edited.3.contains("edited subject"),
"index kept the stale front matter: {}",
edited.3
);
assert!(
edited.6.as_deref().unwrap_or_default().contains("edited body"),
"index kept the stale body: {:?}",
edited.6
);
}
#[test]
fn deleted_file_is_dropped_and_matches_full_rescan() {
let tmp = warm_store(4);
let dir = tmp.path();
std::fs::remove_file(dir.join("alice").join("msg000.md")).unwrap();
let rows = assert_incremental_matches_full_rescan(dir);
assert_eq!(rows.len(), 3);
assert!(!rows.iter().any(|r| r.0 == "msg000"));
}
#[test]
fn moved_file_keeps_one_row_at_the_new_path() {
let tmp = warm_store(4);
let dir = tmp.path();
std::fs::create_dir_all(dir.join("carol")).unwrap();
std::fs::rename(
dir.join("alice").join("msg000.md"),
dir.join("carol").join("msg000.md"),
)
.unwrap();
let rows = assert_incremental_matches_full_rescan(dir);
assert_eq!(rows.len(), 4, "a move must not duplicate or drop the row");
let moved = rows.iter().find(|r| r.0 == "msg000").unwrap();
assert_eq!(moved.2, "carol/msg000.md");
}
#[test]
fn move_that_changes_nothing_but_the_directory_is_still_noticed() {
let tmp = warm_store(2);
let dir = tmp.path();
std::fs::create_dir_all(dir.join("dave")).unwrap();
std::fs::rename(
dir.join("bob").join("msg001.md"),
dir.join("dave").join("msg001.md"),
)
.unwrap();
let rows = assert_incremental_matches_full_rescan(dir);
let moved = rows.iter().find(|r| r.0 == "msg001").unwrap();
assert_eq!(moved.2, "dave/msg001.md");
}
#[test]
fn everything_at_once_matches_full_rescan() {
let tmp = warm_store(6);
let dir = tmp.path();
let_the_clock_tick();
write_message(dir, "alice", "msg000", "edited", "edited body"); write_message(dir, "carol", "msg900", "new", "new body"); std::fs::remove_file(dir.join("bob").join("msg001.md")).unwrap(); std::fs::create_dir_all(dir.join("dave")).unwrap();
std::fs::rename(
dir.join("alice").join("msg002.md"),
dir.join("dave").join("msg002.md"),
)
.unwrap();
let rows = assert_incremental_matches_full_rescan(dir);
assert_eq!(rows.len(), 6);
let by_path = paths(&rows);
assert!(by_path.contains(&"carol/msg900.md".to_string()));
assert!(by_path.contains(&"dave/msg002.md".to_string()));
assert!(!by_path.contains(&"bob/msg001.md".to_string()));
assert!(!by_path.contains(&"alice/msg002.md".to_string()));
}
#[test]
fn unchanged_files_are_not_reread() {
let tmp = warm_store(4);
let dir = tmp.path();
{
let conn = rusqlite::Connection::open(dir.join("_system.db")).unwrap();
conn.execute(
"UPDATE documents SET content_text = 'SENTINEL' WHERE id = 'msg002'",
[],
)
.unwrap();
}
write_message(dir, "carol", "msg900", "new", "new body");
drop(Store::open(dir.to_str().unwrap()).unwrap());
let rows = snapshot(dir);
let untouched = rows.iter().find(|r| r.0 == "msg002").unwrap();
assert_eq!(
untouched.6.as_deref(),
Some("SENTINEL"),
"an unchanged document was re-read; the scan is not incremental"
);
assert!(rows.iter().any(|r| r.0 == "msg900"), "the new file was missed");
let store = Store::open(dir.to_str().unwrap()).unwrap();
store.rebuild(None).unwrap();
drop(store);
let rows = snapshot(dir);
let rebuilt = rows.iter().find(|r| r.0 == "msg002").unwrap();
assert_eq!(
rebuilt.6.as_deref().map(|s| s.contains("body 2")),
Some(true),
"rebuild() did not re-read from disk: {:?}",
rebuilt.6
);
}
#[test]
fn an_unchanged_tree_is_not_scanned() {
let tmp = warm_store(4);
let dir = tmp.path();
{
let conn = rusqlite::Connection::open(dir.join("_system.db")).unwrap();
conn.execute("UPDATE documents SET content_text = 'SENTINEL'", [])
.unwrap();
}
drop(Store::open(dir.to_str().unwrap()).unwrap());
for row in snapshot(dir) {
assert_eq!(row.6.as_deref(), Some("SENTINEL"), "row {} was re-read", row.0);
}
}
#[test]
fn contested_id_resolves_the_way_a_full_rescan_would() {
let tmp = warm_store(2);
let dir = tmp.path();
write_message(dir, "bob", "msg000", "bob's copy", "bob body");
let rows = assert_incremental_matches_full_rescan(dir);
let contested = rows.iter().find(|r| r.0 == "msg000").unwrap();
assert!(
contested.2 == "alice/msg000.md" || contested.2 == "bob/msg000.md",
"unexpected path {}",
contested.2
);
}