use std::sync::Arc;
use fathomdb_embedder_api::{Embedder, EmbedderError, EmbedderIdentity, Vector};
use fathomdb_engine::{
vector_phase1_sql_for_test, Engine, InitialState, LifecycleState, PreparedWrite,
ProjectionRole, ProjectionSpec, ProjectionVector, SearchFilter, SourceId,
};
use fathomdb_schema::SQLITE_SUFFIX;
use rusqlite::Connection;
use std::collections::BTreeSet;
use tempfile::TempDir;
const DIM: usize = 8;
#[derive(Clone, Debug)]
struct HashEmbedder;
impl Embedder for HashEmbedder {
fn identity(&self) -> EmbedderIdentity {
EmbedderIdentity::new("hash8", "rev-a", DIM as u32)
}
fn embed(&self, text: &str) -> Result<Vector, EmbedderError> {
let mut v = vec![0.0_f32; DIM];
for (i, b) in text.bytes().enumerate() {
v[i % DIM] += f32::from(b) / 255.0;
}
if v.iter().all(|x| *x == 0.0) {
v[0] = 1.0;
}
Ok(v)
}
}
fn attr_col(name: &str) -> String {
let mut s = String::from("attr_");
for b in name.as_bytes() {
s.push_str(&format!("{b:02x}"));
}
s
}
fn fixture(name: &str) -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let path = dir.path().join(format!("{name}{SQLITE_SUFFIX}"));
(dir, path)
}
fn open(path: &std::path::Path) -> fathomdb_engine::OpenedEngine {
let opened = Engine::open_with_embedder_for_test(path, Arc::new(HashEmbedder)).expect("open");
opened.engine.configure_vector_kind_for_test("doc").expect("vector kind doc");
opened
}
fn node(logical: &str, body_json: &str) -> PreparedWrite {
PreparedWrite::Node {
kind: "doc".to_string(),
body: body_json.to_string(),
source_id: SourceId::new("test:15e").expect("source id"),
logical_id: Some(logical.to_string()),
state: InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
}
}
fn edge(logical: &str, from: &str, to: &str, body_json: &str) -> PreparedWrite {
PreparedWrite::Edge {
kind: "link".to_string(),
from: from.to_string(),
to: to.to_string(),
source_id: SourceId::new("test:15e").expect("source id"),
logical_id: Some(logical.to_string()),
body: Some(body_json.to_string()),
t_valid: None,
t_invalid: None,
confidence: None,
extractor_model_id: None,
temporal_fallback: None,
}
}
fn filterable_spec(name: &str) -> ProjectionSpec {
let mut roles = BTreeSet::new();
roles.insert(ProjectionRole::Filterable);
ProjectionSpec { name: name.to_string(), roles, fts: None, vector: None, source: None }
}
fn nested_filterable_vector_spec(name: &str, source: &[&str]) -> ProjectionSpec {
let mut roles = BTreeSet::new();
roles.insert(ProjectionRole::Filterable);
roles.insert(ProjectionRole::Searchable);
ProjectionSpec {
name: name.to_string(),
roles,
fts: None,
vector: Some(ProjectionVector::default()),
source: Some(source.iter().map(|segment| (*segment).to_string()).collect()),
}
}
fn table_sql(path: &std::path::Path) -> String {
let conn = Connection::open(path).expect("raw reopen");
conn.query_row(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='vector_default'",
[],
|row| row.get::<_, String>(0),
)
.expect("read vector_default sql")
}
#[test]
fn filterable_predicate_compiles_into_prekn_match_clause() {
let mut filter = SearchFilter::default();
filter.attributes = vec![("priority".to_string(), "high".to_string())];
let sql = vector_phase1_sql_for_test(Some(&filter));
let col = attr_col("priority"); assert_eq!(col, "attr_7072696f72697479", "byte-safe hex encoding of the name");
assert!(
sql.contains(&format!("AND {col}=?3")),
"filterable attribute lowers to the indexed pre-KNN column at ?3:\n{sql}"
);
assert!(!sql.contains("json_extract"), "must not demote to a post-KNN json_extract:\n{sql}");
let candidates_block = sql.split("ORDER BY distance").next().unwrap();
assert!(
candidates_block.contains(&format!("AND {col}=?3")),
"attr predicate must be pre-KNN (inside the MATCH candidates block):\n{sql}"
);
let mut combined = SearchFilter::default();
combined.kind = Some("doc".to_string());
combined.attributes = vec![("priority".to_string(), "high".to_string())];
let csql = vector_phase1_sql_for_test(Some(&combined));
assert!(csql.contains("AND kind=?3"), "metadata field keeps ?3:\n{csql}");
assert!(csql.contains(&format!("AND {col}=?4")), "attribute appended at ?4:\n{csql}");
let mut spaced = SearchFilter::default();
spaced.attributes = vec![("due date".to_string(), "2020".to_string())];
let ssql = vector_phase1_sql_for_test(Some(&spaced));
let scol = attr_col("due date"); assert_eq!(scol, "attr_6475652064617465");
assert!(
ssql.contains(&format!("AND {scol}=?3")),
"space-containing name is byte-safe:\n{ssql}"
);
assert!(!ssql.contains('"'), "no quoted identifier (vec0 rejects them):\n{ssql}");
}
#[test]
fn source_change_refreshes_existing_vec0_attribute_metadata() {
let (_dir, path) = fixture("s60_source_refresh");
let opened = open(&path);
let engine = &opened.engine;
let old = nested_filterable_vector_spec("priority", &["old"]);
engine.configure_projections(std::slice::from_ref(&old), &[]).expect("configure old source");
engine.write(&[node("N1", r#"{"old":"stale","new":"fresh"}"#)]).expect("write");
engine.drain(10_000).expect("drain");
let new = nested_filterable_vector_spec("priority", &["new"]);
let extra = filterable_spec("extra");
engine
.configure_projections(&[new, extra], &["priority".to_string()])
.expect("drop and redeclare changed source");
let column = attr_col("priority");
let conn = Connection::open(&path).expect("open raw");
let value: String = conn
.query_row(&format!("SELECT {column} FROM vector_default LIMIT 1"), [], |row| row.get(0))
.expect("read refreshed vec0 metadata");
assert_eq!(value, "\u{1}fresh", "vec0 metadata must follow the replacement source path");
}
#[test]
fn reactivation_refreshes_vec0_metadata_after_a_source_change() {
let (_dir, path) = fixture("s60_reactivate_refresh");
let opened = open(&path);
let engine = &opened.engine;
let old = nested_filterable_vector_spec("priority", &["old"]);
engine.configure_projections(std::slice::from_ref(&old), &[]).expect("configure old source");
engine.write(&[node("N1", r#"{"old":"stale","new":"fresh"}"#)]).expect("write");
engine.drain(10_000).expect("drain");
engine.transition("N1", LifecycleState::Deleted, None).expect("delete");
let new = nested_filterable_vector_spec("priority", &["new"]);
engine
.configure_projections(&[new], &["priority".to_string()])
.expect("replace source while deleted");
engine.transition("N1", LifecycleState::Active, None).expect("reactivate");
let column = attr_col("priority");
let conn = Connection::open(&path).expect("open raw");
let value: String = conn
.query_row(&format!("SELECT {column} FROM vector_default LIMIT 1"), [], |row| row.get(0))
.expect("read refreshed vec0 metadata");
assert_eq!(value, "\u{1}fresh", "reactivation must refresh vec0 metadata from the new source");
}
#[test]
fn reshape_is_nondestructive_preserves_bits_rowids_and_sentinel() {
let (_dir, path) = fixture("s15e_reshape");
let opened = open(&path);
let engine = &opened.engine;
engine
.write(&[
node("OLD1", r#"{"title":"alpha document about vectors"}"#),
node("OLD2", r#"{"title":"beta note concerning storage"}"#),
])
.expect("write old");
engine.drain(10_000).expect("drain old");
let old_rowids = engine
.query_i64_col_for_test("SELECT rowid FROM vector_default ORDER BY rowid")
.expect("old rowids");
assert_eq!(old_rowids.len(), 2, "two old vector rows exist pre-reshape");
let old_bits: Vec<(i64, Vec<u8>)> =
old_rowids.iter().map(|&r| (r, engine.read_vector_bin_for_test(r).expect("bin"))).collect();
let old_hamming: Vec<i64> = engine
.query_i64_col_for_test(
"SELECT CAST(vec_distance_hamming(embedding_bin, \
vec_quantize_binary(vec_f32('[1,0,0,0,0,0,0,0]'))) AS INTEGER) \
FROM vector_default ORDER BY rowid",
)
.expect("old hamming");
let delta =
engine.configure_projections(&[filterable_spec("priority")], &[]).expect("configure");
assert!(delta.built.contains(&"priority".to_string()), "projection built");
let col = attr_col("priority");
let sql = table_sql(&path);
assert!(sql.contains(&format!("{col} TEXT")), "plain TEXT attr column present:\n{sql}");
assert!(!sql.contains(&format!("+{col}")), "attr column must NOT be aux (`+`):\n{sql}");
let after_rowids = engine
.query_i64_col_for_test("SELECT rowid FROM vector_default ORDER BY rowid")
.expect("after rowids");
assert_eq!(after_rowids, old_rowids, "rowids preserved verbatim across reshape");
for (r, before) in &old_bits {
let after = engine.read_vector_bin_for_test(*r).expect("bin after");
assert_eq!(
&after, before,
"embedding_bin copied verbatim (not re-quantized) for rowid {r}"
);
}
let sentinels = engine
.query_text_col_for_test(&format!("SELECT {col} FROM vector_default ORDER BY rowid"))
.expect("sentinel read");
assert!(
sentinels.iter().all(|s| s.is_empty()),
"old rows back-fill the '' sentinel: {sentinels:?}"
);
engine
.write(&[node("NEW1", r#"{"title":"gamma record on ranking","priority":"high"}"#)])
.expect("write new");
engine.drain(10_000).expect("drain new");
let new_rowids: Vec<i64> = engine
.query_i64_col_for_test("SELECT rowid FROM vector_default ORDER BY rowid")
.expect("new rowids")
.into_iter()
.filter(|r| !old_rowids.contains(r))
.collect();
assert_eq!(new_rowids.len(), 1, "one new vector row");
let new_id = new_rowids[0];
let new_val = engine
.query_text_col_for_test(&format!("SELECT {col} FROM vector_default WHERE rowid={new_id}"))
.expect("new attr value");
assert_eq!(
new_val,
vec!["\u{1}high".to_string()],
"new row's attr column populated from body (fix-3 marker-encoded)"
);
let all_hamming = engine
.query_i64_col_for_test(
"SELECT CAST(vec_distance_hamming(embedding_bin, \
vec_quantize_binary(vec_f32('[1,0,0,0,0,0,0,0]'))) AS INTEGER) \
FROM vector_default ORDER BY rowid",
)
.expect("all hamming");
assert!(
all_hamming.iter().all(|d| (0..=DIM as i64).contains(d)),
"all Hamming distances comparable in [0,{DIM}]: {all_hamming:?}"
);
for (i, &r) in old_rowids.iter().enumerate() {
let idx = after_rowids.iter().position(|x| *x == r).unwrap();
assert_eq!(all_hamming[idx], old_hamming[i], "old row {r} Hamming distance unchanged");
}
let matched = engine
.query_i64_col_for_test(&format!(
"SELECT rowid FROM vector_default \
WHERE embedding_bin MATCH vec_quantize_binary(vec_f32('[1,0,0,0,0,0,0,0]')) \
AND {col}=char(1)||'high' ORDER BY distance LIMIT 10"
))
.expect("filtered KNN must not error (plain column, not aux)");
assert_eq!(matched, vec![new_id], "filter matches only the populated new row");
for r in &old_rowids {
assert!(!matched.contains(r), "sentinel old row {r} is skipped by the filter");
}
engine.close().unwrap();
}
#[test]
fn idempotent_refilterable_registration_is_vec0_noop() {
let (_dir, path) = fixture("s15e_idempotent");
let opened = open(&path);
let engine = &opened.engine;
engine
.write(&[node("A", r#"{"title":"one"}"#), node("B", r#"{"title":"two"}"#)])
.expect("write");
engine.drain(10_000).expect("drain");
let first =
engine.configure_projections(&[filterable_spec("priority")], &[]).expect("first configure");
assert!(first.built.contains(&"priority".to_string()), "first apply builds + reshapes");
let sql_after_first = table_sql(&path);
let rowids = engine
.query_i64_col_for_test("SELECT rowid FROM vector_default ORDER BY rowid")
.expect("rowids");
let bits: Vec<Vec<u8>> =
rowids.iter().map(|&r| engine.read_vector_bin_for_test(r).expect("bin")).collect();
let second = engine
.configure_projections(&[filterable_spec("priority")], &[])
.expect("second configure");
assert!(second.unchanged, "identical re-registration diffs to a no-op");
assert_eq!(table_sql(&path), sql_after_first, "vec0 shape byte-unchanged on no-op re-register");
let rowids2 = engine
.query_i64_col_for_test("SELECT rowid FROM vector_default ORDER BY rowid")
.expect("rowids2");
assert_eq!(rowids2, rowids, "rowids byte-unchanged (no re-insert)");
for (i, &r) in rowids.iter().enumerate() {
assert_eq!(
engine.read_vector_bin_for_test(r).expect("bin2"),
bits[i],
"embeddings unchanged"
);
}
engine.close().unwrap();
}
#[test]
fn filtered_knn_over_attr_column_does_not_hard_error() {
let (_dir, path) = fixture("s15e_aux_trap");
let opened = open(&path);
let engine = &opened.engine;
engine.write(&[node("A", r#"{"priority":"low"}"#)]).expect("write");
engine.drain(10_000).expect("drain");
engine.configure_projections(&[filterable_spec("priority")], &[]).expect("configure");
let col = attr_col("priority");
let res = engine.query_i64_col_for_test(&format!(
"SELECT rowid FROM vector_default \
WHERE embedding_bin MATCH vec_quantize_binary(vec_f32('[1,0,0,0,0,0,0,0]')) \
AND {col}='low' ORDER BY distance LIMIT 5"
));
assert!(res.is_ok(), "filtered KNN over a plain metadata attr column must not error: {res:?}");
engine.close().unwrap();
}
#[test]
fn hybrid_fts_arm_applies_attribute_filter() {
let (_dir, path) = fixture("s15e_hybrid_totality");
let opened = open(&path);
let engine = &opened.engine;
engine.configure_projections(&[filterable_spec("priority")], &[]).expect("configure");
engine
.write(&[
node("MATCH", r#"{"title":"alpha vectors document","priority":"high"}"#),
node("MISS", r#"{"title":"beta vectors document","priority":"low"}"#),
])
.expect("write");
engine.drain(10_000).expect("drain");
let col = attr_col("priority");
let miss_vec_val = engine
.query_text_col_for_test(&format!(
"SELECT v.{col} FROM vector_default v \
JOIN canonical_nodes n ON n.write_cursor = v.rowid \
WHERE n.logical_id = 'MISS'"
))
.expect("read MISS vec0 attr");
assert_eq!(
miss_vec_val,
vec!["\u{1}low".to_string()],
"MISS vec0 column populated from body (fix-3 marker-encoded)"
);
let mut filter = SearchFilter::default();
filter.attributes = vec![("priority".to_string(), "high".to_string())];
let res = engine.search_filtered("vectors", Some(filter)).expect("search");
let bodies: Vec<String> = res.results.iter().map(|h| h.body.clone()).collect();
assert!(
bodies.iter().any(|b| b.contains("alpha vectors")),
"the priority=high doc must be present: {bodies:?}"
);
assert!(
!bodies.iter().any(|b| b.contains("beta vectors")),
"priority=low doc must NOT leak through the FTS arm: {bodies:?}"
);
engine.close().unwrap();
}
#[test]
fn existing_row_backfilled_from_canonical_attributes_not_blanket_sentinel() {
let (_dir, path) = fixture("s15e_existing_backfill");
let opened = open(&path);
let engine = &opened.engine;
engine
.write(&[
node("PRESENT", r#"{"title":"vectors alpha record","priority":"high"}"#),
node("ABSENT", r#"{"title":"vectors beta record"}"#),
])
.expect("write");
engine.drain(10_000).expect("drain");
engine.configure_projections(&[filterable_spec("priority")], &[]).expect("configure");
let col = attr_col("priority");
let present_val = engine
.query_text_col_for_test(&format!(
"SELECT v.{col} FROM vector_default v \
JOIN canonical_nodes n ON n.write_cursor = v.rowid \
WHERE n.logical_id = 'PRESENT'"
))
.expect("read PRESENT vec0 attr");
assert_eq!(
present_val,
vec!["\u{1}high".to_string()],
"pre-existing row whose body HAS the attribute must back-fill its real value \
(fix-3 marker-encoded), not the '' absent sentinel"
);
let absent_val = engine
.query_text_col_for_test(&format!(
"SELECT v.{col} FROM vector_default v \
JOIN canonical_nodes n ON n.write_cursor = v.rowid \
WHERE n.logical_id = 'ABSENT'"
))
.expect("read ABSENT vec0 attr");
assert_eq!(absent_val, vec![String::new()], "genuinely-absent row keeps the '' sentinel");
let mut filter = SearchFilter::default();
filter.attributes = vec![("priority".to_string(), "high".to_string())];
let res = engine.search_filtered("vectors", Some(filter)).expect("search");
let bodies: Vec<String> = res.results.iter().map(|h| h.body.clone()).collect();
assert!(
bodies.iter().any(|b| b.contains("alpha record")),
"pre-existing PRESENT row must be filterable immediately: {bodies:?}"
);
assert!(
!bodies.iter().any(|b| b.contains("beta record")),
"genuinely-absent row must fail-to-match: {bodies:?}"
);
engine.close().unwrap();
}
#[test]
fn attribute_filter_excludes_edge_hits_on_both_arms() {
let (_dir, path) = fixture("s15e_edge_excluded_both_arms");
let opened = open(&path);
let engine = &opened.engine;
engine.configure_projections(&[filterable_spec("priority")], &[]).expect("configure");
engine
.write(&[
node("ENTFROM", r#"{"title":"origin entity"}"#),
node("ENTTO", r#"{"title":"target entity"}"#),
node("NODEMATCH", r#"{"title":"sharedtoken alpha node","priority":"high"}"#),
edge(
"EDGEFACT",
"ENTFROM",
"ENTTO",
r#"{"fact":"sharedtoken edgefact beta","priority":"high"}"#,
),
])
.expect("write");
engine.drain(10_000).expect("drain");
let edge_cursor = engine
.query_i64_col_for_test(
"SELECT write_cursor FROM canonical_edges WHERE logical_id='EDGEFACT'",
)
.expect("edge cursor");
assert_eq!(edge_cursor.len(), 1, "exactly one edge row");
let ec = edge_cursor[0];
let col = attr_col("priority");
let edge_vec_rowid = engine
.query_i64_col_for_test(&format!("SELECT rowid FROM vector_default WHERE rowid={ec}"))
.expect("edge vector candidate");
assert_eq!(edge_vec_rowid, vec![ec], "edge IS a vector_default candidate (rowid=write_cursor)");
let edge_attr = engine
.query_text_col_for_test(&format!("SELECT {col} FROM vector_default WHERE rowid={ec}"))
.expect("edge attr col");
assert_eq!(
edge_attr,
vec![String::new()],
"edge's vec0 attr column is the '' sentinel (edges are not attribute-projected), \
so the pre-KNN attr_<hex>='high' predicate prunes it"
);
let edge_fts_rows = engine
.query_i64_col_for_test(&format!(
"SELECT COUNT(*) FROM search_index_edges WHERE write_cursor={ec}"
))
.expect("edge fts candidate");
assert_eq!(edge_fts_rows, vec![1], "edge IS a search_index_edges (FTS) candidate");
let edge_eav_rows = engine
.query_i64_col_for_test(&format!(
"SELECT COUNT(*) FROM canonical_attributes WHERE write_cursor={ec}"
))
.expect("edge eav count");
assert_eq!(
edge_eav_rows,
vec![0],
"edge has NO canonical_attributes row (attribute projection is Node-gated), \
so hit_attributes_pass_filter reads '' and the FTS arm excludes it"
);
let unfiltered = engine.search_filtered("sharedtoken", None).expect("unfiltered search");
let ub: Vec<String> = unfiltered.results.iter().map(|h| h.body.clone()).collect();
assert!(ub.iter().any(|b| b.contains("edgefact")), "edge is retrievable unfiltered: {ub:?}");
assert!(ub.iter().any(|b| b.contains("alpha node")), "node is retrievable unfiltered: {ub:?}");
let mut filter = SearchFilter::default();
filter.attributes = vec![("priority".to_string(), "high".to_string())];
let filtered = engine.search_filtered("sharedtoken", Some(filter)).expect("filtered search");
let fb: Vec<String> = filtered.results.iter().map(|h| h.body.clone()).collect();
assert!(
fb.iter().any(|b| b.contains("alpha node")),
"the matching NODE with the attribute must still appear (non-vacuous control): {fb:?}"
);
assert!(
!fb.iter().any(|b| b.contains("edgefact")),
"(A) edges are EXCLUDED: an attribute filter is node-scoped, so the edge hit must NOT \
appear via the edge-FTS arm NOR the edge-vector arm: {fb:?}"
);
let mut explained_filter = SearchFilter::default();
explained_filter.attributes = vec![("priority".to_string(), "high".to_string())];
let explained = engine
.search_explained("sharedtoken", Some(explained_filter), 0, false, 0.3, 0)
.expect("explained filtered search");
assert_eq!(
explained.explanation.expect("explanation sidecar").trace.dropped_edge_hits,
1,
"the excluded edge-FTS candidate is observable to the caller"
);
engine.close().unwrap();
}
#[test]
fn empty_string_attribute_value_vs_absent_vector_arm() {
let (_dir, path) = fixture("s15e_empty_vs_absent_vec");
let opened = open(&path);
let engine = &opened.engine;
engine.configure_projections(&[filterable_spec("status")], &[]).expect("configure");
engine
.write(&[
node("PRESENTEMPTY", r#"{"title":"sharedtoken alpha","status":""}"#),
node("ABSENT", r#"{"title":"sharedtoken beta"}"#),
node("PRESENTOPEN", r#"{"title":"sharedtoken gamma","status":"open"}"#),
])
.expect("write");
engine.drain(10_000).expect("drain");
let col = attr_col("status");
let marker = "\u{1}";
let read_col = |logical: &str| {
engine
.query_text_col_for_test(&format!(
"SELECT v.{col} FROM vector_default v \
JOIN canonical_nodes n ON n.write_cursor = v.rowid \
WHERE n.logical_id = '{logical}'"
))
.expect("read vec0 attr")
};
assert_eq!(
read_col("PRESENTEMPTY"),
vec![marker.to_string()],
"present-empty encodes to the marker (enc(\"\")), NOT the '' absent sentinel"
);
assert_eq!(
read_col("ABSENT"),
vec![String::new()],
"absent stays the '' sentinel (disjoint from every present value)"
);
assert_eq!(
read_col("PRESENTOPEN"),
vec![format!("{marker}open")],
"present 'open' encodes to marker||'open'"
);
let eav_value = |logical: &str| {
engine
.query_text_col_for_test(&format!(
"SELECT ca.attr_value FROM canonical_attributes ca \
JOIN canonical_nodes n ON n.write_cursor = ca.write_cursor \
WHERE n.logical_id = '{logical}' AND ca.attr_name='status'"
))
.expect("eav value read")
};
let eav_count = |logical: &str| {
engine
.query_i64_col_for_test(&format!(
"SELECT COUNT(*) FROM canonical_attributes ca \
JOIN canonical_nodes n ON n.write_cursor = ca.write_cursor \
WHERE n.logical_id = '{logical}' AND ca.attr_name='status'"
))
.expect("eav count read")
};
assert_eq!(eav_count("PRESENTEMPTY"), vec![1], "present-empty HAS a canonical_attributes row");
assert_eq!(
eav_value("PRESENTEMPTY"),
vec![String::new()],
"present-empty canonical_attributes.attr_value is RAW '' (NOT encoded)"
);
assert_eq!(eav_count("ABSENT"), vec![0], "absent has NO canonical_attributes row");
let cursor = |logical: &str| {
engine
.query_i64_col_for_test(&format!(
"SELECT write_cursor FROM canonical_nodes WHERE logical_id='{logical}'"
))
.expect("cursor")[0]
};
let (pe, ab, po) = (cursor("PRESENTEMPTY"), cursor("ABSENT"), cursor("PRESENTOPEN"));
let match_empty = engine
.query_i64_col_for_test(&format!(
"SELECT rowid FROM vector_default \
WHERE embedding_bin MATCH vec_quantize_binary(vec_f32('[1,0,0,0,0,0,0,0]')) \
AND {col}=char(1)||'' ORDER BY distance LIMIT 10"
))
.expect("vector arm empty filter");
assert!(match_empty.contains(&pe), "status='' matches present-empty on the vector arm");
assert!(!match_empty.contains(&ab), "status='' must NOT match ABSENT on the vector arm");
assert!(
!match_empty.contains(&po),
"status='' must NOT match present-'open' on the vector arm"
);
let match_open = engine
.query_i64_col_for_test(&format!(
"SELECT rowid FROM vector_default \
WHERE embedding_bin MATCH vec_quantize_binary(vec_f32('[1,0,0,0,0,0,0,0]')) \
AND {col}=char(1)||'open' ORDER BY distance LIMIT 10"
))
.expect("vector arm open filter");
assert!(match_open.contains(&po), "status='open' matches present-'open'");
assert!(!match_open.contains(&pe), "status='open' must NOT match present-empty");
assert!(!match_open.contains(&ab), "status='open' must NOT match ABSENT");
engine.close().unwrap();
}
#[test]
fn empty_string_attribute_value_vs_absent_both_arms_fused() {
let (_dir, path) = fixture("s15e_empty_vs_absent_fused");
let opened = open(&path);
let engine = &opened.engine;
engine.configure_projections(&[filterable_spec("status")], &[]).expect("configure");
engine
.write(&[
node("PRESENTEMPTY", r#"{"title":"sharedtoken alpha","status":""}"#),
node("ABSENT", r#"{"title":"sharedtoken beta"}"#),
node("PRESENTOPEN", r#"{"title":"sharedtoken gamma","status":"open"}"#),
])
.expect("write");
engine.drain(10_000).expect("drain");
let search = |value: &str| -> Vec<String> {
let mut filter = SearchFilter::default();
filter.attributes = vec![("status".to_string(), value.to_string())];
engine
.search_filtered("sharedtoken", Some(filter))
.expect("search")
.results
.iter()
.map(|h| h.body.clone())
.collect()
};
let empty = search("");
assert!(
empty.iter().any(|b| b.contains("alpha")),
"present-empty must match status='' (non-vacuous control): {empty:?}"
);
assert!(
!empty.iter().any(|b| b.contains("beta")),
"ABSENT must NOT false-match status='' on EITHER arm: {empty:?}"
);
assert!(
!empty.iter().any(|b| b.contains("gamma")),
"present-'open' must NOT match status='': {empty:?}"
);
let open = search("open");
assert!(
open.iter().any(|b| b.contains("gamma")),
"present-'open' must match status='open': {open:?}"
);
assert!(
!open.iter().any(|b| b.contains("alpha")),
"present-empty must NOT match status='open': {open:?}"
);
assert!(
!open.iter().any(|b| b.contains("beta")),
"ABSENT must NOT match status='open': {open:?}"
);
engine.close().unwrap();
}
#[test]
fn undeclared_filter_attribute_is_typed_rejection_on_both_arms() {
use fathomdb_engine::EngineError;
let (_dir, path) = fixture("s15e_undeclared_attr");
let opened = open(&path);
let engine = &opened.engine;
engine.configure_projections(&[filterable_spec("priority")], &[]).expect("configure");
engine
.write(&[
node("A", r#"{"title":"sharedtoken alpha","priority":"high"}"#),
node("B", r#"{"title":"sharedtoken beta","priority":"low"}"#),
])
.expect("write");
engine.drain(10_000).expect("drain");
let mut bad = SearchFilter::default();
bad.attributes = vec![("nonexistent".to_string(), "x".to_string())];
match engine.search_filtered("sharedtoken", Some(bad)) {
Err(EngineError::InvalidFilter { reason }) => assert!(
reason.contains("nonexistent"),
"the typed rejection must NAME the undeclared attribute, got: {reason}"
),
Err(other) => panic!(
"an undeclared filter attribute must be a typed InvalidFilter rejection, not \
an opaque error: got {other:?}"
),
Ok(r) => panic!(
"an undeclared filter attribute must be a typed InvalidFilter rejection, not a \
silent Ok with {} result(s)",
r.results.len()
),
}
let mut declared_absent = SearchFilter::default();
declared_absent.attributes = vec![("priority".to_string(), "nonesuch".to_string())];
let res = engine
.search_filtered("sharedtoken", Some(declared_absent))
.expect("a DECLARED attribute with an absent value must NO-MATCH, never reject");
let absent_bodies: Vec<String> = res.results.iter().map(|h| h.body.clone()).collect();
assert!(
!absent_bodies.iter().any(|b| b.contains("alpha") || b.contains("beta")),
"declared-but-absent value excludes the non-matching rows (fix-3 no-match path): \
{absent_bodies:?}"
);
let mut declared_present = SearchFilter::default();
declared_present.attributes = vec![("priority".to_string(), "high".to_string())];
let ok = engine
.search_filtered("sharedtoken", Some(declared_present))
.expect("a declared present value must search normally");
let present_bodies: Vec<String> = ok.results.iter().map(|h| h.body.clone()).collect();
assert!(
present_bodies.iter().any(|b| b.contains("alpha")),
"the declared present value still matches its row: {present_bodies:?}"
);
engine.close().unwrap();
}
#[test]
fn undeclared_after_concurrent_drop_is_typed_invalidfilter_not_storage_race() {
use fathomdb_engine::{
arm_reader_search_hook_for_test, clear_reader_search_hook_for_test, EngineError,
};
use std::sync::mpsc;
let (_dir, path) = fixture("s15e_fix3_toctou");
let opened = open(&path);
let engine = &opened.engine;
engine.configure_projections(&[filterable_spec("priority")], &[]).expect("configure");
engine
.write(&[
node("A", r#"{"title":"sharedtoken alpha","priority":"high"}"#),
node("B", r#"{"title":"sharedtoken beta","priority":"low"}"#),
])
.expect("write");
engine.drain(10_000).expect("drain");
let (reached_tx, reached_rx) = mpsc::channel::<()>();
let (go_tx, go_rx) = mpsc::channel::<()>();
arm_reader_search_hook_for_test(Box::new(move || {
reached_tx.send(()).ok();
go_rx.recv().ok();
}));
let result = std::thread::scope(|s| {
let search = s.spawn(|| {
let mut f = SearchFilter::default();
f.attributes = vec![("priority".to_string(), "high".to_string())];
engine.search_filtered("sharedtoken", Some(f))
});
reached_rx.recv().expect("reader must reach the pre-snapshot hook");
engine
.configure_projections(&[], &["priority".to_string()])
.expect("concurrent DROP of the filterable projection");
go_tx.send(()).expect("release the parked reader");
search.join().expect("search thread joined")
});
clear_reader_search_hook_for_test();
match result {
Err(EngineError::InvalidFilter { reason }) => assert!(
reason.contains("priority"),
"the reader-snapshot rejection must NAME the now-undeclared attribute, got: {reason}"
),
Err(EngineError::Storage) => panic!(
"TOCTOU: a configure_projections DROP racing the search made the vec0 attr_<hex> \
column vanish AFTER the pre-dispatch check passed on the writer connection, so the \
reader crashed with an opaque `no such column` Storage error. fix-3 must validate on \
the reader's OWN transaction snapshot so this is a typed InvalidFilter."
),
other => panic!(
"expected a typed InvalidFilter after the concurrent drop (never a raw error / silent \
Ok), got {other:?}"
),
}
engine.close().unwrap();
}