use std::sync::Arc;
use fathomdb_embedder_api::{Embedder, EmbedderError, EmbedderIdentity, Vector};
use fathomdb_engine::{
ComparisonOp, Engine, EngineError, Filter, FilterTerm, Predicate, PreparedWrite, ReadView,
ScalarValue, SearchFilter,
};
use fathomdb_schema::SQLITE_SUFFIX;
use tempfile::TempDir;
#[derive(Clone, Debug)]
struct FixedEmbedder;
impl Embedder for FixedEmbedder {
fn identity(&self) -> EmbedderIdentity {
EmbedderIdentity::new("deterministic", "rev-a", 8)
}
fn embed(&self, _text: &str) -> Result<Vector, EmbedderError> {
let mut v = vec![0.0_f32; 8];
v[0] = 1.0;
Ok(v)
}
}
fn fixture(name: &str) -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let path = dir.path().join(format!("{name}{SQLITE_SUFFIX}"));
(dir, path)
}
#[test]
fn filterterm_enum_is_exactly_five_variants() {
let terms = [
FilterTerm::SourceType("todo".to_string()),
FilterTerm::Kind("todo".to_string()),
FilterTerm::CreatedAfter(0),
FilterTerm::Status("open".to_string()),
FilterTerm::Json(
Predicate::json_path_eq("$.status", ScalarValue::Text("open".to_string()))
.expect("valid"),
),
];
for t in &terms {
match t {
FilterTerm::SourceType(_)
| FilterTerm::Kind(_)
| FilterTerm::CreatedAfter(_)
| FilterTerm::Status(_)
| FilterTerm::Json(_) => {}
}
let debug = format!("{t:?}").to_lowercase();
assert!(!debug.contains("fused"), "Fused leaked into FilterTerm: {debug}");
assert!(!debug.contains("unchecked"), "_unchecked leaked into FilterTerm: {debug}");
}
}
#[test]
fn searchfilter_sugar_lowers_and_round_trips() {
let mut sf = SearchFilter::default();
sf.source_type = Some("todo".to_string());
sf.kind = Some("todo".to_string());
sf.created_after = Some(1000);
sf.status = Some("open".to_string());
let unified = Filter::from(&sf);
assert_eq!(
unified.terms,
vec![
FilterTerm::SourceType("todo".to_string()),
FilterTerm::Kind("todo".to_string()),
FilterTerm::CreatedAfter(1000),
FilterTerm::Status("open".to_string()),
],
"canonical order: source_type, kind, created_after, status"
);
let back = unified.to_search_filter().expect("metadata-only never rejects");
assert_eq!(back, sf, "round-trip must be identity");
let empty = Filter::from(&SearchFilter::default());
assert!(empty.terms.is_empty(), "all-None SearchFilter -> empty terms");
assert_eq!(empty.to_search_filter().unwrap(), SearchFilter::default());
}
#[test]
fn search_filter_typed_rejects_json_term() {
let json_filter = Filter {
terms: vec![FilterTerm::Json(
Predicate::json_path_compare("$.priority", ComparisonOp::Gt, ScalarValue::Integer(3))
.expect("valid"),
)],
};
match json_filter.to_search_filter() {
Err(EngineError::InvalidFilter { reason }) => {
assert!(
reason.contains("post-KNN") || reason.contains("json-path"),
"rejection reason names the no-demotion cause: {reason}"
);
}
other => panic!("expected InvalidFilter, got {other:?}"),
}
let (_dir, path) = fixture("s40_reject_json");
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(FixedEmbedder)).expect("open");
opened.engine.configure_vector_kind_for_test("todo").expect("vector kind");
opened
.engine
.write(&[PreparedWrite::Node {
kind: "todo".to_string(),
body: r#"{"status":"open","priority":5}"#.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some("T1".to_string()),
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
}])
.expect("write");
opened.engine.drain(10_000).expect("drain");
let result = opened.engine.search_filter("semantic", &json_filter);
assert!(
matches!(result, Err(EngineError::InvalidFilter { .. })),
"search_filter must typed-reject a Json term; got {result:?}"
);
let meta = Filter { terms: vec![FilterTerm::Kind("todo".to_string())] };
assert!(opened.engine.search_filter("semantic", &meta).is_ok(), "metadata subset accepted");
opened.engine.close().unwrap();
}
#[test]
fn read_list_filter_accepts_full_set() {
let dir = TempDir::new().unwrap();
let engine = Engine::open(dir.path().join(format!("rl{SQLITE_SUFFIX}"))).expect("open").engine;
let seed = |lid: &str, body: &str| {
engine
.write(&[PreparedWrite::Node {
kind: "todo".to_string(),
body: body.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some(lid.to_string()),
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
}])
.expect("write");
};
seed("A", r#"{"status":"open","created_at":100,"priority":5}"#);
seed("B", r#"{"status":"done","created_at":200,"priority":1}"#);
seed("C", r#"{"status":"open","created_at":300,"priority":9}"#);
let filter = Filter {
terms: vec![
FilterTerm::Status("open".to_string()),
FilterTerm::CreatedAfter(150),
FilterTerm::Json(
Predicate::json_path_compare(
"$.priority",
ComparisonOp::Gt,
ScalarValue::Integer(3),
)
.expect("valid"),
),
],
};
let rows = engine
.read_list_filter("todo", &filter, 100, &ReadView::default())
.expect("read_list_filter");
let ids: Vec<&str> = rows.iter().map(|r| r.logical_id.as_str()).collect();
assert_eq!(ids, vec!["C"], "open AND created_at>=150 AND priority>3 => only C; got {ids:?}");
}
#[test]
fn read_list_filter_kind_and_source_type_constant_fold() {
let dir = TempDir::new().unwrap();
let engine = Engine::open(dir.path().join(format!("cf{SQLITE_SUFFIX}"))).expect("open").engine;
engine
.write(&[
PreparedWrite::Node {
kind: "todo".to_string(),
body: r#"{"status":"open"}"#.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some("T1".to_string()),
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
},
PreparedWrite::Node {
kind: "todo".to_string(),
body: r#"{"status":"open"}"#.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some("T2".to_string()),
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
},
])
.expect("write");
let kind_match = Filter { terms: vec![FilterTerm::Kind("todo".to_string())] };
assert_eq!(
engine.read_list_filter("todo", &kind_match, 100, &ReadView::default()).unwrap().len(),
2,
"Kind(todo) on partition todo is a no-op => all rows"
);
let kind_mismatch = Filter { terms: vec![FilterTerm::Kind("note".to_string())] };
assert!(
engine
.read_list_filter("todo", &kind_mismatch, 100, &ReadView::default())
.unwrap()
.is_empty(),
"Kind(note) on partition todo constant-folds to empty"
);
let st_match = Filter { terms: vec![FilterTerm::SourceType("todo".to_string())] };
assert_eq!(
engine.read_list_filter("todo", &st_match, 100, &ReadView::default()).unwrap().len(),
2,
"SourceType(todo) folds pass-all on partition todo (resolve_source_type)"
);
let st_mismatch = Filter { terms: vec![FilterTerm::SourceType("email".to_string())] };
assert!(
engine
.read_list_filter("todo", &st_mismatch, 100, &ReadView::default())
.unwrap()
.is_empty(),
"SourceType(email) constant-folds to empty on partition todo"
);
}
#[test]
fn parity_kind_predicate_both_backends() {
let (_dir, path) = fixture("s40_parity");
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(FixedEmbedder)).expect("open");
opened.engine.configure_vector_kind_for_test("todo").expect("vector kind todo");
opened.engine.configure_vector_kind_for_test("note").expect("vector kind note");
opened
.engine
.write(&[
PreparedWrite::Node {
kind: "todo".to_string(),
body: r#"{"status":"open"}"#.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some("TODO1".to_string()),
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
},
PreparedWrite::Node {
kind: "note".to_string(),
body: r#"{"status":"open"}"#.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some("NOTE1".to_string()),
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
},
])
.expect("write");
opened.engine.drain(10_000).expect("drain");
let filter = Filter { terms: vec![FilterTerm::Kind("todo".to_string())] };
let hits = opened.engine.search_filter("semantic", &filter).expect("search_filter");
assert!(!hits.results.is_empty(), "todo hits present on vec0 backend");
assert!(
hits.results.iter().all(|h| h.kind == "todo"),
"search_filter prunes non-todo: {:?}",
hits.results.iter().map(|h| h.kind.clone()).collect::<Vec<_>>()
);
let rows = opened
.engine
.read_list_filter("todo", &filter, 100, &ReadView::default())
.expect("read_list_filter");
let ids: Vec<&str> = rows.iter().map(|r| r.logical_id.as_str()).collect();
assert_eq!(ids, vec!["TODO1"], "read_list_filter returns the todo node; got {ids:?}");
assert!(
opened
.engine
.read_list_filter("note", &filter, 100, &ReadView::default())
.unwrap()
.is_empty(),
"Kind(todo) folds empty on the note partition"
);
opened.engine.close().unwrap();
}