use rusqlite::{params, Connection};
use crate::db::entities::{
list_critiques, list_milestones, list_problems, list_solutions,
populate_problem_computed_fields, populate_solution_computed_fields, upsert_critique,
upsert_milestone, upsert_problem, upsert_solution,
};
use crate::db::events::{clear_events, insert_event};
use crate::db::Database;
use crate::error::Result;
use crate::storage::MetadataStore;
pub fn load_from_markdown(db: &Database, store: &MetadataStore) -> Result<()> {
let conn = db.conn();
set_dirty_internal(conn, true)?;
conn.execute_batch("BEGIN")?;
let result = (|| -> Result<()> {
clear_all_tables(conn)?;
let milestones = store.list_milestones()?;
for milestone in &milestones {
upsert_milestone(conn, milestone)?;
}
let problems = store.list_problems()?;
for problem in &problems {
upsert_problem(conn, problem)?;
}
let solutions = store.list_solutions()?;
for solution in &solutions {
upsert_solution(conn, solution)?;
}
let critiques = store.list_critiques()?;
for critique in &critiques {
upsert_critique(conn, critique)?;
}
let events = store.list_events()?;
for event in &events {
insert_event(conn, event)?;
}
Ok(())
})();
match result {
Ok(()) => {
conn.execute_batch("COMMIT")?;
rebuild_fts(db)?;
set_dirty_internal(conn, false)?;
Ok(())
}
Err(e) => {
let _ = conn.execute_batch("ROLLBACK");
Err(e)
}
}
}
pub fn dump_to_markdown(db: &Database, store: &MetadataStore) -> Result<()> {
let conn = db.conn();
let mut problems = list_problems(conn)?;
populate_problem_computed_fields(conn, &mut problems)?;
for problem in &problems {
store.save_problem(problem)?;
}
let mut solutions = list_solutions(conn)?;
populate_solution_computed_fields(conn, &mut solutions)?;
for solution in &solutions {
store.save_solution(solution)?;
}
let critiques = list_critiques(conn)?;
for critique in &critiques {
store.save_critique(critique)?;
}
let milestones = list_milestones(conn)?;
for milestone in &milestones {
store.save_milestone(milestone)?;
}
Ok(())
}
pub fn rebuild_fts(db: &Database) -> Result<()> {
let conn = db.conn();
conn.execute("DELETE FROM fts", [])?;
let problems = list_problems(conn)?;
for problem in &problems {
let body = format!("{}\n{}", problem.description, problem.tags.join(" "));
conn.execute(
"INSERT INTO fts (entity_type, entity_id, title, body) VALUES (?1, ?2, ?3, ?4)",
params!["problem", &problem.id, &problem.title, &body],
)?;
}
let solutions = list_solutions(conn)?;
for solution in &solutions {
let body = format!("{}\n{}", solution.approach, solution.tags.join(" "));
conn.execute(
"INSERT INTO fts (entity_type, entity_id, title, body) VALUES (?1, ?2, ?3, ?4)",
params!["solution", &solution.id, &solution.title, &body],
)?;
}
let critiques = list_critiques(conn)?;
for critique in &critiques {
let body = critique.argument.clone();
conn.execute(
"INSERT INTO fts (entity_type, entity_id, title, body) VALUES (?1, ?2, ?3, ?4)",
params!["critique", &critique.id, &critique.title, &body],
)?;
}
let milestones = list_milestones(conn)?;
for milestone in &milestones {
let body = milestone.description.clone();
conn.execute(
"INSERT INTO fts (entity_type, entity_id, title, body) VALUES (?1, ?2, ?3, ?4)",
params!["milestone", &milestone.id, &milestone.title, &body],
)?;
}
Ok(())
}
pub fn update_fts_entry(
conn: &Connection,
entity_type: &str,
entity_id: &str,
title: &str,
body: &str,
) -> std::result::Result<(), rusqlite::Error> {
conn.execute(
"DELETE FROM fts WHERE entity_type = ?1 AND entity_id = ?2",
params![entity_type, entity_id],
)?;
conn.execute(
"INSERT INTO fts (entity_type, entity_id, title, body) VALUES (?1, ?2, ?3, ?4)",
params![entity_type, entity_id, title, body],
)?;
Ok(())
}
pub fn remove_fts_entry(
conn: &Connection,
entity_type: &str,
entity_id: &str,
) -> std::result::Result<(), rusqlite::Error> {
conn.execute(
"DELETE FROM fts WHERE entity_type = ?1 AND entity_id = ?2",
params![entity_type, entity_id],
)?;
Ok(())
}
pub fn rebuild_embeddings(
db: &Database,
client: &crate::embeddings::EmbeddingClient,
) -> Result<()> {
use crate::db::embeddings::{clear_embeddings, upsert_embedding};
use crate::embeddings::{
prepare_critique_text, prepare_milestone_text, prepare_problem_text, prepare_solution_text,
};
let conn = db.conn();
let model = client.model();
clear_embeddings(conn)?;
let problems = list_problems(conn)?;
for problem in &problems {
let text = prepare_problem_text(&problem.title, &problem.description);
if let Ok(embedding) = client.embed(&text) {
upsert_embedding(conn, "problem", &problem.id, model, &embedding)?;
}
}
let solutions = list_solutions(conn)?;
for solution in &solutions {
let text = prepare_solution_text(&solution.title, &solution.approach);
if let Ok(embedding) = client.embed(&text) {
upsert_embedding(conn, "solution", &solution.id, model, &embedding)?;
}
}
let critiques = list_critiques(conn)?;
for critique in &critiques {
let text = prepare_critique_text(&critique.title, &critique.argument);
if let Ok(embedding) = client.embed(&text) {
upsert_embedding(conn, "critique", &critique.id, model, &embedding)?;
}
}
let milestones = list_milestones(conn)?;
for milestone in &milestones {
let text = prepare_milestone_text(&milestone.title, &milestone.description);
if let Ok(embedding) = client.embed(&text) {
upsert_embedding(conn, "milestone", &milestone.id, model, &embedding)?;
}
}
Ok(())
}
pub fn is_dirty(db: &Database) -> Result<bool> {
is_dirty_internal(db.conn())
}
pub fn set_dirty(db: &Database, dirty: bool) -> Result<()> {
set_dirty_internal(db.conn(), dirty)
}
fn clear_all_tables(conn: &Connection) -> Result<()> {
conn.execute("DELETE FROM embeddings", [])?;
conn.execute("DELETE FROM critiques", [])?;
conn.execute("DELETE FROM solutions", [])?;
conn.execute("DELETE FROM problems", [])?;
conn.execute("DELETE FROM milestones", [])?;
clear_events(conn)?;
conn.execute("DELETE FROM fts", [])?;
Ok(())
}
fn is_dirty_internal(conn: &Connection) -> Result<bool> {
let result: std::result::Result<String, _> =
conn.query_row("SELECT value FROM meta WHERE key = 'dirty'", [], |row| {
row.get(0)
});
match result {
Ok(value) => Ok(value == "true" || value == "1"),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
Err(e) => Err(e.into()),
}
}
fn set_dirty_internal(conn: &Connection, dirty: bool) -> Result<()> {
conn.execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES ('dirty', ?1)",
[if dirty { "true" } else { "false" }],
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dirty_flag() {
let db = Database::open_in_memory().expect("Failed to open database");
assert!(!is_dirty(&db).expect("Failed to check dirty"));
set_dirty(&db, true).expect("Failed to set dirty");
assert!(is_dirty(&db).expect("Failed to check dirty"));
set_dirty(&db, false).expect("Failed to clear dirty");
assert!(!is_dirty(&db).expect("Failed to check dirty"));
}
#[test]
fn test_rebuild_fts_empty() {
let db = Database::open_in_memory().expect("Failed to open database");
rebuild_fts(&db).expect("Failed to rebuild FTS");
let count: i64 = db
.conn()
.query_row("SELECT COUNT(*) FROM fts", [], |row| row.get(0))
.expect("Failed to count FTS rows");
assert_eq!(count, 0);
}
#[test]
fn test_rebuild_fts_with_data() {
use crate::db::entities::upsert_problem;
use crate::models::Problem;
let db = Database::open_in_memory().expect("Failed to open database");
let mut problem = Problem::new("p1".to_string(), "Test Problem".to_string());
problem.description = "This is a test description".to_string();
problem.description = "Some context here".to_string();
upsert_problem(db.conn(), &problem).expect("Failed to insert problem");
rebuild_fts(&db).expect("Failed to rebuild FTS");
let count: i64 = db
.conn()
.query_row("SELECT COUNT(*) FROM fts", [], |row| row.get(0))
.expect("Failed to count FTS rows");
assert_eq!(count, 1);
let match_count: i64 = db
.conn()
.query_row(
"SELECT COUNT(*) FROM fts WHERE fts MATCH 'test'",
[],
|row| row.get(0),
)
.expect("Failed to search FTS");
assert_eq!(match_count, 1);
}
#[test]
fn test_clear_all_tables() {
use crate::db::entities::{upsert_problem, upsert_solution};
use crate::models::{Problem, Solution};
let db = Database::open_in_memory().expect("Failed to open database");
let conn = db.conn();
let problem = Problem::new("p1".to_string(), "Test Problem".to_string());
upsert_problem(conn, &problem).expect("Failed to insert problem");
let solution = Solution::new(
"s1".to_string(),
"Test Solution".to_string(),
"p1".to_string(),
);
upsert_solution(conn, &solution).expect("Failed to insert solution");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM problems", [], |row| row.get(0))
.expect("Failed to count problems");
assert_eq!(count, 1);
clear_all_tables(conn).expect("Failed to clear tables");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM problems", [], |row| row.get(0))
.expect("Failed to count problems");
assert_eq!(count, 0);
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM solutions", [], |row| row.get(0))
.expect("Failed to count solutions");
assert_eq!(count, 0);
}
}