use fathomdb_engine::{Engine, PreparedWrite};
use fathomdb_schema::{migrate, migrate_with_steps, SCHEMA_VERSION, 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 node(kind: &str, body: &str, logical_id: Option<&str>) -> PreparedWrite {
PreparedWrite::Node {
kind: kind.to_string(),
body: body.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: logical_id.map(str::to_string),
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
}
}
fn edge(kind: &str, from: &str, to: &str, logical_id: Option<&str>) -> PreparedWrite {
PreparedWrite::Edge {
kind: kind.to_string(),
from: from.to_string(),
to: to.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: logical_id.map(str::to_string),
body: None,
t_valid: None,
t_invalid: None,
confidence: None,
extractor_model_id: None,
temporal_fallback: None,
}
}
#[test]
fn s15_supersession_is_idempotent_one_active_version() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "supersede");
{
let opened = Engine::open(&path).expect("open");
for body in ["v1", "v2", "v3"] {
opened.engine.write(&[node("doc", body, Some("L1"))]).expect("write");
}
opened.engine.close().unwrap();
}
let conn = Connection::open(&path).expect("open sqlite");
let total: u64 = conn
.query_row("SELECT COUNT(*) FROM canonical_nodes WHERE logical_id = 'L1'", [], |r| r.get(0))
.unwrap();
assert_eq!(total, 3, "invalidate-not-delete: all three versions retained");
let active: u64 = conn
.query_row(
"SELECT COUNT(*) FROM canonical_nodes WHERE logical_id = 'L1' AND superseded_at IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(active, 1, "exactly one active version after supersession");
let active_body: String = conn
.query_row(
"SELECT body FROM canonical_nodes WHERE logical_id = 'L1' AND superseded_at IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(active_body, "v3");
let superseded_null_tombstone: u64 = conn
.query_row(
"SELECT COUNT(*) FROM canonical_nodes
WHERE logical_id = 'L1' AND body IN ('v1','v2') AND superseded_at IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(superseded_null_tombstone, 0, "superseded rows must carry a tombstone cursor");
}
#[test]
fn s15_edge_supersession_is_idempotent_one_active_version() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "edge_supersede");
{
let opened = Engine::open(&path).expect("open");
opened.engine.write(&[edge("rel", "a", "b", Some("E1"))]).expect("write v1");
opened.engine.write(&[edge("rel", "a", "c", Some("E1"))]).expect("supersede to v2");
opened.engine.close().unwrap();
}
let conn = Connection::open(&path).expect("open sqlite");
let total: u64 = conn
.query_row("SELECT COUNT(*) FROM canonical_edges WHERE logical_id = 'E1'", [], |r| r.get(0))
.unwrap();
assert_eq!(total, 2, "both edge versions retained (invalidate-not-delete)");
let active_to: String = conn
.query_row(
"SELECT to_id FROM canonical_edges WHERE logical_id = 'E1' AND superseded_at IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(active_to, "c", "exactly one active edge, the most-recent version");
}
#[test]
fn s15_null_logical_id_rows_never_collide() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "nullsafe");
{
let opened = Engine::open(&path).expect("open");
let batch: Vec<PreparedWrite> =
(0..8).map(|i| node("doc", &format!("legacy-{i}"), None)).collect();
opened.engine.write(&batch).expect("write batch of NULL-logical_id rows");
opened.engine.close().unwrap();
}
let conn = Connection::open(&path).expect("open sqlite");
let active_null: u64 = conn
.query_row(
"SELECT COUNT(*) FROM canonical_nodes WHERE logical_id IS NULL AND superseded_at IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(active_null, 8, "all NULL-logical_id rows must coexist active (NULL-safety)");
}
#[test]
fn s15_partial_unique_active_index_rejects_two_active_versions() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "uniqueidx");
let conn = Connection::open(&path).expect("open sqlite");
migrate(&conn).expect("migrate to head");
conn.execute(
"INSERT INTO canonical_nodes(write_cursor, kind, body, logical_id) VALUES(1, 'doc', 'a', 'DUP')",
[],
)
.expect("first active row");
let err = conn.execute(
"INSERT INTO canonical_nodes(write_cursor, kind, body, logical_id) VALUES(2, 'doc', 'b', 'DUP')",
[],
);
assert!(err.is_err(), "two active rows for one logical_id must be rejected");
conn.execute(
"INSERT INTO canonical_nodes(write_cursor, kind, body, logical_id, superseded_at)
VALUES(3, 'doc', 'c', 'DUP', 2)",
[],
)
.expect("a superseded row for the same logical_id must be allowed alongside the active one");
let err_diff_kind = conn.execute(
"INSERT INTO canonical_nodes(write_cursor, kind, body, logical_id) VALUES(4, 'note', 'd', 'DUP')",
[],
);
assert!(
err_diff_kind.is_err(),
"a different kind for the same active logical_id must now be rejected (logical_id-alone)"
);
}
#[test]
fn s31_node_kind_change_reingest_supersedes() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "node_kind_change");
{
let opened = Engine::open(&path).expect("open");
opened.engine.write(&[node("fact", "v1", Some("L1"))]).expect("write kind=fact");
opened.engine.write(&[node("note", "v2", Some("L1"))]).expect("re-ingest kind=note");
opened.engine.close().unwrap();
}
let conn = Connection::open(&path).expect("open sqlite");
let total: u64 = conn
.query_row("SELECT COUNT(*) FROM canonical_nodes WHERE logical_id = 'L1'", [], |r| r.get(0))
.unwrap();
assert_eq!(total, 2, "both kind versions retained (invalidate-not-delete)");
let active: u64 = conn
.query_row(
"SELECT COUNT(*) FROM canonical_nodes WHERE logical_id = 'L1' AND superseded_at IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(active, 1, "a kind-change re-ingest must SUPERSEDE (one active row), not fork");
let (active_kind, active_body): (String, String) = conn
.query_row(
"SELECT kind, body FROM canonical_nodes WHERE logical_id = 'L1' AND superseded_at IS NULL",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap();
assert_eq!(active_kind, "note", "the active row is the most-recent (kind-changed) version");
assert_eq!(active_body, "v2");
let first_tombstoned: u64 = conn
.query_row(
"SELECT COUNT(*) FROM canonical_nodes
WHERE logical_id = 'L1' AND kind = 'fact' AND superseded_at IS NOT NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(first_tombstoned, 1, "the prior (kind=fact) version must be tombstoned, not active");
}
#[test]
fn s31_edge_kind_change_reingest_supersedes() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "edge_kind_change");
{
let opened = Engine::open(&path).expect("open");
opened.engine.write(&[edge("rel", "a", "b", Some("E1"))]).expect("write kind=rel");
opened
.engine
.write(&[edge("mentions", "a", "b", Some("E1"))])
.expect("re-ingest kind=mentions");
opened.engine.close().unwrap();
}
let conn = Connection::open(&path).expect("open sqlite");
let total: u64 = conn
.query_row("SELECT COUNT(*) FROM canonical_edges WHERE logical_id = 'E1'", [], |r| r.get(0))
.unwrap();
assert_eq!(total, 2, "both edge kind versions retained (invalidate-not-delete)");
let active: u64 = conn
.query_row(
"SELECT COUNT(*) FROM canonical_edges WHERE logical_id = 'E1' AND superseded_at IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
active, 1,
"an edge kind-change re-ingest must SUPERSEDE (one active row), not fork"
);
let active_kind: String = conn
.query_row(
"SELECT kind FROM canonical_edges WHERE logical_id = 'E1' AND superseded_at IS NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
active_kind, "mentions",
"the active edge is the most-recent (kind-changed) version"
);
let first_tombstoned: u64 = conn
.query_row(
"SELECT COUNT(*) FROM canonical_edges
WHERE logical_id = 'E1' AND kind = 'rel' AND superseded_at IS NOT NULL",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(first_tombstoned, 1, "the prior (kind=rel) edge must be tombstoned, not active");
}
#[test]
fn s15_row_cursors_are_one_to_one_with_the_batch() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "rowcursors");
let opened = Engine::open(&path).expect("open");
let batch =
vec![node("doc", "r0", None), node("doc", "r1", Some("L")), node("doc", "r2", None)];
let receipt = opened.engine.write(&batch).expect("write");
assert_eq!(receipt.row_cursors.len(), batch.len(), "one row_cursor per input row");
for window in receipt.row_cursors.windows(2) {
assert_eq!(window[1], window[0] + 1, "row cursors are contiguous in input order");
}
assert_eq!(
*receipt.row_cursors.last().unwrap(),
receipt.cursor,
"the final row_cursor is the batch high-water cursor"
);
opened.engine.close().unwrap();
}
#[test]
fn s15_legacy_pre_step12_db_upgrades_in_place_with_null_backfill() {
let dir = TempDir::new().unwrap();
let path = db_path(&dir, "legacy_upgrade");
let conn = Connection::open(&path).expect("open sqlite");
let pre_step12: Vec<_> =
fathomdb_schema::MIGRATIONS.iter().filter(|m| m.step_id <= 11).copied().collect();
migrate_with_steps(&conn, &pre_step12).expect("migrate to v11");
assert_eq!(conn.query_row("PRAGMA user_version", [], |r| r.get::<_, u32>(0)).unwrap(), 11);
conn.execute(
"INSERT INTO canonical_nodes(write_cursor, kind, body) VALUES(7, 'doc', 'legacy-body')",
[],
)
.expect("seed legacy row");
let report = migrate(&conn).expect("upgrade to head");
assert_eq!(report.schema_version_after, SCHEMA_VERSION);
assert_eq!(
conn.query_row("PRAGMA user_version", [], |r| r.get::<_, u32>(0)).unwrap(),
SCHEMA_VERSION
);
let logical_id: Option<String> = conn
.query_row("SELECT logical_id FROM canonical_nodes WHERE body = 'legacy-body'", [], |r| {
r.get(0)
})
.expect("legacy row still queryable");
assert!(logical_id.is_none(), "legacy row must back-fill NULL logical_id, got {logical_id:?}");
let again = migrate(&conn).expect("re-migrate is a no-op");
assert_eq!(again.schema_version_after, SCHEMA_VERSION);
assert!(again.migration_steps.is_empty(), "no steps re-run on an up-to-date DB");
}