use anyhow::{Context, Result};
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct BackfillTitlesArgs {
pub root: PathBuf,
pub tenant: String,
pub dry_run: bool,
pub limit: Option<usize>,
}
#[derive(Debug, Default, Clone)]
pub struct BackfillTitlesReport {
pub notes_scanned: usize,
pub titles_extracted: usize,
pub titles_updated: usize,
pub titles_no_h1: usize,
}
pub async fn backfill_titles(args: BackfillTitlesArgs) -> Result<BackfillTitlesReport> {
let db_path = args.root.join("vault/.gradatum/index.db");
if !db_path.exists() {
anyhow::bail!(
"index.db introuvable : {} — le worker doit avoir démarré au moins une fois",
db_path.display()
);
}
tokio::task::spawn_blocking(move || {
run_backfill_sync(&db_path, &args.tenant, args.dry_run, args.limit)
})
.await
.context("spawn_blocking backfill_titles")?
}
fn run_backfill_sync(
db_path: &std::path::Path,
tenant: &str,
dry_run: bool,
limit: Option<usize>,
) -> Result<BackfillTitlesReport> {
let conn =
rusqlite::Connection::open(db_path).context("ouverture index.db pour backfill-titles")?;
conn.execute_batch("PRAGMA journal_mode=WAL;")
.context("PRAGMA journal_mode=WAL")?;
let limit_clause = limit.map(|n| format!("LIMIT {n}")).unwrap_or_default();
let query = format!(
"SELECT id, body_text FROM notes \
WHERE (title IS NULL OR title = '') AND vault_id = ?1 \
ORDER BY created ASC \
{limit_clause}"
);
let mut stmt = conn
.prepare(&query)
.context("préparation SELECT notes sans titre")?;
let rows: Vec<(String, String)> = stmt
.query_map(rusqlite::params![tenant], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.context("exécution SELECT notes sans titre")?
.collect::<std::result::Result<_, _>>()
.context("collecte notes sans titre")?;
drop(stmt);
let notes_scanned = rows.len();
let mut report = BackfillTitlesReport {
notes_scanned,
..Default::default()
};
if dry_run {
for (_id, body) in &rows {
match gradatum_curator::extract_h1_title(body) {
Some(title) if !title.is_empty() => {
report.titles_extracted += 1;
}
Some(_) | None => {
report.titles_no_h1 += 1;
}
}
}
return Ok(report);
}
let tx = conn
.unchecked_transaction()
.context("début transaction backfill-titles")?;
for (id, body) in &rows {
match gradatum_curator::extract_h1_title(body) {
Some(title) if !title.is_empty() => {
let title_owned = title.to_string();
report.titles_extracted += 1;
tx.execute(
"UPDATE notes SET title = ?1 WHERE id = ?2",
rusqlite::params![title_owned, id],
)
.context("UPDATE title")?;
report.titles_updated += 1;
}
Some(_) | None => {
report.titles_no_h1 += 1;
}
}
}
tx.commit().context("commit transaction backfill-titles")?;
Ok(report)
}