talon-core 0.4.2

Core retrieval engine for Talon: hybrid search (BM25 + semantic + reranker), indexing, and graph-aware ranking over markdown corpora.
Documentation
use super::*;
use crate::store::open_database;
use fs_err as fs;
use std::env::temp_dir;
use std::sync::atomic::{AtomicU64, Ordering};

fn unique_dir(label: &str) -> std::path::PathBuf {
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
    let pid = std::process::id();
    temp_dir().join(format!("talon-sync-test-{label}-{pid}-{n}"))
}

#[test]
fn run_sync_indexes_then_reconciles() {
    let vault = unique_dir("end-to-end");
    fs::create_dir_all(&vault).unwrap();
    fs::write(vault.join("a.md"), "# A").unwrap();
    fs::write(vault.join("b.md"), "# B").unwrap();
    let db = vault.join("idx.sqlite");
    let lock = vault.join(".talon").join("sync.lock");
    let mut conn = open_database(&db).unwrap();

    let (first, embed) = run_sync(
        &mut conn,
        &vault,
        &lock,
        &IndexerConfig::index_all(),
        None,
        None,
    )
    .unwrap();
    assert_eq!(first.indexed, 2);
    assert_eq!(first.deleted, 0);
    assert!(embed.is_none());

    fs::remove_file(vault.join("b.md")).unwrap();
    let (second, _) = run_sync(
        &mut conn,
        &vault,
        &lock,
        &IndexerConfig::index_all(),
        None,
        None,
    )
    .unwrap();
    assert_eq!(second.indexed, 0);
    assert_eq!(second.deleted, 1);

    let active: i64 = conn
        .query_row("SELECT COUNT(*) FROM notes WHERE active = 1", [], |r| {
            r.get(0)
        })
        .unwrap();
    assert_eq!(active, 1);

    drop(conn);
    let _ = fs::remove_file(&db);
    let _ = fs::remove_file(db.with_extension("sqlite-wal"));
    let _ = fs::remove_file(db.with_extension("sqlite-shm"));
    fs::remove_dir_all(&vault).unwrap();
}

#[test]
fn run_sync_reconciles_renamed_file_without_orphan_chunks() {
    let vault = unique_dir("rename");
    fs::create_dir_all(&vault).unwrap();
    fs::write(
        vault.join("old.md"),
        "# Stable Title\n\nA paragraph with enough text to produce a searchable chunk.",
    )
    .unwrap();
    let db = vault.join("idx.sqlite");
    let lock = vault.join(".talon").join("sync.lock");
    let mut conn = open_database(&db).unwrap();

    let (first, _) = run_sync(
        &mut conn,
        &vault,
        &lock,
        &IndexerConfig::index_all(),
        None,
        None,
    )
    .unwrap();
    assert_eq!(first.indexed, 1);
    assert_eq!(first.deleted, 0);

    fs::rename(vault.join("old.md"), vault.join("new.md")).unwrap();
    let (second, _) = run_sync(
        &mut conn,
        &vault,
        &lock,
        &IndexerConfig::index_all(),
        None,
        None,
    )
    .unwrap();
    assert_eq!(second.indexed, 1);
    assert_eq!(second.deleted, 1);

    let active_paths: Vec<String> = {
        let mut stmt = conn
            .prepare("SELECT vault_path FROM notes WHERE active = 1 ORDER BY vault_path")
            .unwrap();
        stmt.query_map([], |row| row.get::<_, String>(0))
            .unwrap()
            .collect::<rusqlite::Result<_>>()
            .unwrap()
    };
    assert_eq!(active_paths, vec!["new.md"]);

    let old_chunks: i64 = conn
        .query_row(
            "SELECT COUNT(*)
             FROM chunks
             JOIN notes ON notes.id = chunks.note_id
             WHERE notes.vault_path = 'old.md'",
            [],
            |row| row.get(0),
        )
        .unwrap();
    assert_eq!(old_chunks, 0);

    let new_chunks: i64 = conn
        .query_row(
            "SELECT COUNT(*)
             FROM chunks
             JOIN notes ON notes.id = chunks.note_id
             WHERE notes.vault_path = 'new.md'",
            [],
            |row| row.get(0),
        )
        .unwrap();
    assert!(new_chunks > 0);

    drop(conn);
    let _ = fs::remove_file(&db);
    let _ = fs::remove_file(db.with_extension("sqlite-wal"));
    let _ = fs::remove_file(db.with_extension("sqlite-shm"));
    fs::remove_dir_all(&vault).unwrap();
}