use crate::errors::{Error, LOCKED_DB_MESSAGE};
use crate::output::{DoctorFtsProject, DoctorFtsResponse, print_json};
use crate::sqlite::Database;
use crate::sqlite::fts::FtsDesyncReport;
use rusqlite::{Connection, OpenFlags};
use std::collections::HashMap;
use std::path::Path;
use std::process::ExitCode;
#[derive(Default)]
struct MemoryCounts {
by_project: HashMap<String, usize>,
}
#[allow(dead_code)] pub fn handle_doctor_fts(
db_path: &Path,
project_filter: Option<&str>,
repair: bool,
json: bool,
) -> Result<ExitCode, Error> {
if repair {
handle_doctor_fts_repair(db_path, project_filter, json)
} else {
handle_doctor_fts_detect(db_path, project_filter, json)
}
}
fn handle_doctor_fts_detect(
db_path: &Path,
project_filter: Option<&str>,
json: bool,
) -> Result<ExitCode, Error> {
let db = Database::from_conn(super::db_busy::wrap_busy(Connection::open_with_flags(
db_path,
OpenFlags::SQLITE_OPEN_READ_ONLY,
))?);
let report: FtsDesyncReport = super::db_busy::wrap_busy(db.detect_fts_desync())?;
let counts = super::db_busy::wrap_busy(memory_counts(&db))?;
let response = build_response(&report, &counts, project_filter, false, 0);
if json {
print_json(&response);
} else {
print_human_detect(&response);
}
Ok(ExitCode::SUCCESS)
}
fn handle_doctor_fts_repair(
db_path: &Path,
project_filter: Option<&str>,
json: bool,
) -> Result<ExitCode, Error> {
let pre = Database::from_conn(super::db_busy::wrap_busy(Connection::open_with_flags(
db_path,
OpenFlags::SQLITE_OPEN_READ_ONLY,
))?);
let pre_report: FtsDesyncReport = super::db_busy::wrap_busy(pre.detect_fts_desync())?;
let counts = super::db_busy::wrap_busy(memory_counts(&pre))?;
if !pre_report.is_desynced() {
let response = build_response(&pre_report, &counts, project_filter, true, 0);
emit(&response, json);
return Ok(ExitCode::SUCCESS);
}
let db = open_read_write(db_path)?;
if let Err(e) = db.rebuild_fts().map_err(Error::from) {
return Err(recovery_for(e));
}
let post_report: FtsDesyncReport = super::db_busy::wrap_busy(db.detect_fts_desync())?;
let actions = post_report.total_desynced() + pre_report.total_desynced();
let response = build_response(&post_report, &counts, project_filter, true, actions);
emit(&response, json);
Ok(ExitCode::SUCCESS)
}
fn open_read_write(db_path: &Path) -> Result<Database, Error> {
let db = super::db_busy::open_db(db_path)?;
db.set_busy_timeout(std::time::Duration::ZERO)?;
Ok(db)
}
fn emit(response: &DoctorFtsResponse, json: bool) {
if json {
print_json(response);
} else {
print_human_detect(response);
}
}
pub(crate) fn recovery_note() -> String {
format!(
"{LOCKED_DB_MESSAGE} The FTS index may be half-rebuilt; re-run `vipune doctor --fts --repair` to complete it."
)
}
pub(crate) fn recovery_for(e: Error) -> Error {
if e.is_locked() {
Error::Config(recovery_note())
} else {
e
}
}
fn memory_counts(db: &Database) -> Result<MemoryCounts, Error> {
let mut counts = MemoryCounts::default();
for (project_id, rows) in db.row_counts_by_project()? {
*counts.by_project.entry(project_id).or_insert(0) += rows;
}
Ok(counts)
}
fn build_response(
report: &FtsDesyncReport,
counts: &MemoryCounts,
project_filter: Option<&str>,
repaired: bool,
actions: usize,
) -> DoctorFtsResponse {
let in_sync = !report.is_desynced();
let mut underpopulated: Vec<DoctorFtsProject> = Vec::new();
let projects: Vec<String> = match project_filter {
Some(filter) => {
if report
.underpopulated_by_project
.get(filter)
.copied()
.unwrap_or(0)
> 0
{
vec![filter.to_string()]
} else {
Vec::new()
}
}
None => report.underpopulated_by_project.keys().cloned().collect(),
};
for pid in projects {
let missing = report
.underpopulated_by_project
.get(&pid)
.copied()
.unwrap_or(0);
let total = counts.by_project.get(&pid).copied().unwrap_or(0);
underpopulated.push(DoctorFtsProject {
project_id: pid.clone(),
memory_rows: total,
missing_from_fts: missing,
});
}
underpopulated.sort_by(|a, b| a.project_id.cmp(&b.project_id));
DoctorFtsResponse {
in_sync,
underpopulated_by_project: underpopulated,
orphan_rows: report.orphans,
total_desynced: report.total_desynced(),
repaired,
actions,
}
}
fn print_human_detect(response: &DoctorFtsResponse) {
if response.in_sync {
if response.repaired {
println!("FTS index is in sync. No rebuild needed (0 actions).");
} else {
println!("FTS index is in sync with the memories table.");
}
return;
}
for p in &response.underpopulated_by_project {
println!(
"Warning: project '{}' has {} memories row(s) missing from the FTS index ({} total).",
p.project_id, p.missing_from_fts, p.memory_rows
);
}
if response.orphan_rows > 0 {
println!(
"Warning: {} FTS row(s) have no matching memories row (orphan).",
response.orphan_rows
);
}
println!(
"FTS index is desynced: {} total desynced row(s). Run 'vipune doctor --fts --repair' to rebuild.",
response.total_desynced
);
}