use fathomdb_engine::{Engine, PreparedWrite};
use fathomdb_schema::{migrate, SQLITE_SUFFIX};
use rusqlite::Connection;
use tempfile::TempDir;
fn db_path(dir: &TempDir, name: &str) -> std::path::PathBuf {
dir.path().join(format!("{name}{SQLITE_SUFFIX}"))
}
fn register_log(name: &str) -> PreparedWrite {
PreparedWrite::AdminSchema {
name: name.to_string(),
kind: "append_only_log".to_string(),
schema_json: "{\"type\":\"object\"}".to_string(),
retention_json: "{}".to_string(),
}
}
fn append(collection: &str, record_key: &str, body: &str) -> PreparedWrite {
PreparedWrite::OpStore {
collection: collection.to_string(),
record_key: record_key.to_string(),
schema_id: None,
body: body.to_string(),
}
}
fn seed(engine: &Engine, collection: &str, count: usize) {
engine.write(&[register_log(collection)]).expect("register collection");
for i in 0..count {
engine
.write(&[append(collection, &format!("k{i}"), &format!("{{\"n\":{i}}}"))])
.expect("append op-store row");
}
}
#[test]
fn read_collection_returns_rows_ordered_by_id() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "collection_order")).expect("open");
seed(&opened.engine, "events", 5);
let rows = opened.engine.read_collection("events", None, 100).expect("read_collection");
assert_eq!(rows.len(), 5);
for window in rows.windows(2) {
assert!(window[1].id > window[0].id, "rows must be ORDER BY id ascending");
}
assert_eq!(rows[0].record_key, "k0");
assert_eq!(rows[4].record_key, "k4");
assert_eq!(rows[0].collection, "events");
assert_eq!(rows[0].op_kind, "append");
opened.engine.close().unwrap();
}
#[test]
fn read_collection_only_returns_the_named_collection() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "collection_scoped")).expect("open");
seed(&opened.engine, "alpha", 3);
seed(&opened.engine, "beta", 4);
let alpha = opened.engine.read_collection("alpha", None, 100).expect("read alpha");
assert_eq!(alpha.len(), 3);
assert!(alpha.iter().all(|r| r.collection == "alpha"));
opened.engine.close().unwrap();
}
#[test]
fn read_collection_honors_the_mandatory_limit() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "collection_limit")).expect("open");
seed(&opened.engine, "events", 10);
let page = opened.engine.read_collection("events", None, 4).expect("limited read");
assert_eq!(page.len(), 4, "the limit caps the page size");
opened.engine.close().unwrap();
}
#[test]
fn read_collection_after_id_cursor_paginates_across_the_boundary() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "collection_cursor")).expect("open");
seed(&opened.engine, "events", 10);
let page1 = opened.engine.read_collection("events", None, 4).expect("page1");
assert_eq!(page1.len(), 4);
let cursor = page1.last().unwrap().id;
let page2 = opened.engine.read_collection("events", Some(cursor), 4).expect("page2");
assert_eq!(page2.len(), 4);
assert!(page2.iter().all(|r| r.id > cursor), "after-id cursor excludes the boundary id");
assert_eq!(page2[0].record_key, "k4", "page2 resumes exactly after page1's last row");
let cursor2 = page2.last().unwrap().id;
let page3 = opened.engine.read_collection("events", Some(cursor2), 4).expect("page3");
assert_eq!(page3.len(), 2, "10 rows over pages of 4 → 4 + 4 + 2");
opened.engine.close().unwrap();
}
#[test]
fn read_mutations_aliases_the_same_read_back() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "mutations_alias")).expect("open");
seed(&opened.engine, "events", 3);
let via_collection = opened.engine.read_collection("events", None, 100).expect("collection");
let via_mutations = opened.engine.read_mutations("events", None, 100).expect("mutations");
assert_eq!(via_collection.len(), via_mutations.len());
assert_eq!(
via_collection.iter().map(|r| r.id).collect::<Vec<_>>(),
via_mutations.iter().map(|r| r.id).collect::<Vec<_>>(),
"read.mutations and read.collection return the same op-store rows"
);
opened.engine.close().unwrap();
}
#[test]
fn read_collection_clamps_limit_to_the_one_million_cap() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "collection_cap")).expect("open");
seed(&opened.engine, "events", 3);
let rows = opened
.engine
.read_collection("events", None, 5_000_000)
.expect("a huge limit clamps, never an unbounded scan");
assert_eq!(rows.len(), 3);
opened.engine.close().unwrap();
}
#[test]
fn read_collection_plan_is_index_driven_no_scan_no_temp_btree() {
let dir = TempDir::new().unwrap();
let conn = Connection::open(db_path(&dir, "plan")).expect("open sqlite");
migrate(&conn).expect("migrate to head");
for i in 0..2000i64 {
let coll = if i % 100 == 0 { "small" } else { "bulk" };
conn.execute(
"INSERT INTO operational_mutations(collection_name, record_key, op_kind, payload_json, write_cursor)
VALUES (?1, ?2, 'append', '{}', ?3)",
rusqlite::params![coll, format!("k{i}"), i],
)
.unwrap();
}
let plan: Vec<String> = {
let mut stmt = conn
.prepare(
"EXPLAIN QUERY PLAN
SELECT id, collection_name, record_key, op_kind, payload_json, schema_id, write_cursor
FROM operational_mutations
WHERE collection_name = ?1 AND id > ?2
ORDER BY id
LIMIT ?3",
)
.expect("prepare EXPLAIN");
stmt.query_map(rusqlite::params!["small", 0i64, 50i64], |row| row.get::<_, String>(3))
.expect("query plan")
.collect::<Result<Vec<_>, _>>()
.expect("collect plan")
};
let detail = plan.join(" | ");
assert!(
detail.contains("operational_mutations_collection_id_idx"),
"read_collection must use the (collection_name, id) index; plan: {detail}"
);
assert!(
!detail.contains("SCAN"),
"read_collection must not full-scan operational_mutations; plan: {detail}"
);
assert!(
!detail.contains("USE TEMP B-TREE FOR ORDER BY"),
"the composite index must satisfy ORDER BY id without a temp B-tree; plan: {detail}"
);
assert!(
!detail.contains("USING INTEGER PRIMARY KEY"),
"read_collection must not ride the id PK walk (the pre-step-13 pathology); plan: {detail}"
);
}
#[test]
fn read_collection_paginates_small_collection_inside_large_log() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "large_log")).expect("open");
opened.engine.write(&[register_log("small"), register_log("bulk")]).expect("register");
let mut small_keys = Vec::new();
for i in 0..300usize {
if i % 10 == 0 {
let key = format!("s{i}");
opened
.engine
.write(&[append("small", &key, &format!("{{\"i\":{i}}}"))])
.expect("small");
small_keys.push(key);
} else {
opened.engine.write(&[append("bulk", &format!("b{i}"), "{}")]).expect("bulk");
}
}
assert_eq!(small_keys.len(), 30);
let mut collected: Vec<String> = Vec::new();
let mut cursor: Option<i64> = None;
let mut last_id = i64::MIN;
loop {
let page = opened.engine.read_collection("small", cursor, 7).expect("page");
if page.is_empty() {
break;
}
for row in &page {
assert_eq!(row.collection, "small", "pagination must stay scoped to the collection");
assert!(row.id > last_id, "ids strictly increasing across pages (no overlap)");
last_id = row.id;
collected.push(row.record_key.clone());
}
cursor = Some(page.last().unwrap().id);
}
assert_eq!(
collected, small_keys,
"the union of pages is exactly the small collection, in order"
);
opened.engine.close().unwrap();
}
#[test]
fn read_collection_clamp_and_cursor_edge_cases() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "edges")).expect("open");
seed(&opened.engine, "events", 6);
let zero = opened.engine.read_collection("events", None, 0).expect("limit 0");
assert!(zero.is_empty(), "limit == 0 returns an empty page");
let over =
opened.engine.read_collection("events", None, usize::MAX).expect("over-MAX limit clamps");
assert_eq!(over.len(), 6, "all rows returned; the clamp is internal");
let all = opened.engine.read_collection("events", None, 100).expect("all");
let max_id = all.last().unwrap().id;
let past = opened.engine.read_collection("events", Some(max_id + 1000), 100).expect("past end");
assert!(past.is_empty(), "after_id past the last id is an empty page");
let neg = opened.engine.read_collection("events", Some(-42), 100).expect("negative cursor");
assert_eq!(
neg.iter().map(|r| r.id).collect::<Vec<_>>(),
all.iter().map(|r| r.id).collect::<Vec<_>>(),
"a negative after_id reads from the start of the log (same as None)"
);
let unknown = opened.engine.read_collection("does_not_exist", None, 100).expect("unknown");
assert!(unknown.is_empty(), "an unknown / unregistered collection is empty");
opened.engine.close().unwrap();
}