use anyhow::Result;
use rusqlite::Transaction;
pub fn migrate_v9_wiki_fts(tx: &Transaction<'_>) -> Result<()> {
migrate_wiki_fts_with_columns(tx, &["title", "body"])
}
pub fn migrate_v10_wiki_fts_path(tx: &Transaction<'_>) -> Result<()> {
migrate_wiki_fts_with_columns(tx, &["title", "body", "path"])
}
pub fn ensure_wiki_fts_healthy(path: &std::path::Path) -> Result<()> {
fn open_wal(path: &std::path::Path) -> Result<rusqlite::Connection> {
let conn = rusqlite::Connection::open(path)?;
conn.busy_timeout(std::time::Duration::from_millis(5000))?;
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
Ok(conn)
}
let healthy = {
let conn = open_wal(path)?;
let exists: bool = conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE name='wiki_pages_fts')",
[],
|r| r.get(0),
)
.unwrap_or(false);
if !exists {
return Ok(());
}
conn.query_row("SELECT COUNT(*) FROM wiki_pages_fts LIMIT 1", [], |r| {
r.get::<_, i64>(0)
})
.is_ok()
};
if healthy {
return Ok(());
}
{
let conn = open_wal(path)?;
conn.execute_batch(
"PRAGMA writable_schema=ON;
DELETE FROM sqlite_master WHERE name='wiki_pages_fts';
DELETE FROM sqlite_master WHERE name LIKE 'wiki_pages_fts\\_%' ESCAPE '\\';
PRAGMA writable_schema=OFF;",
)?;
}
{
let conn = open_wal(path)?;
let tx = conn.unchecked_transaction()?;
migrate_wiki_fts_with_columns(&tx, &["title", "body", "path"])?;
tx.commit()?;
}
{
let conn = open_wal(path)?;
let tx = conn.unchecked_transaction()?;
tx.execute(
"INSERT INTO wiki_pages_fts(wiki_pages_fts) VALUES('delete-all')",
[],
)?;
tx.execute(
"INSERT INTO wiki_pages_fts (rowid, title, body, path)
SELECT id, title, body, path FROM wiki_pages",
[],
)?;
tx.execute(
"INSERT INTO wiki_pages_fts(wiki_pages_fts) VALUES('rebuild')",
[],
)?;
tx.commit()?;
let _ = conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()));
}
Ok(())
}
fn migrate_wiki_fts_with_columns(tx: &Transaction<'_>, columns: &[&str]) -> Result<()> {
tx.execute_batch(
"DROP TABLE IF EXISTS wiki_pages_fts;
DROP TABLE IF EXISTS wiki_pages_fts_data;
DROP TABLE IF EXISTS wiki_pages_fts_idx;
DROP TABLE IF EXISTS wiki_pages_fts_config;
DROP TABLE IF EXISTS wiki_pages_fts_docsize;
DROP TABLE IF EXISTS wiki_pages_fts_content;
DROP TRIGGER IF EXISTS wiki_pages_fts_insert;
DROP TRIGGER IF EXISTS wiki_pages_fts_delete;
DROP TRIGGER IF EXISTS wiki_pages_fts_update;",
)?;
let col_list = columns.join(", ");
let create_sql = format!(
"CREATE VIRTUAL TABLE wiki_pages_fts USING fts5(
{col_list},
content='wiki_pages',
content_rowid='id'
);"
);
tx.execute(&create_sql, [])?;
let new_vals = columns
.iter()
.map(|c| format!("new.{}", c))
.collect::<Vec<_>>()
.join(", ");
let old_vals = columns
.iter()
.map(|c| format!("old.{}", c))
.collect::<Vec<_>>()
.join(", ");
let insert_cols = columns.join(", ");
let trigger_insert = format!(
"CREATE TRIGGER IF NOT EXISTS wiki_pages_fts_insert AFTER INSERT ON wiki_pages BEGIN
INSERT INTO wiki_pages_fts (rowid, {insert_cols})
VALUES (new.id, {new_vals});
END;"
);
let trigger_delete = format!(
"CREATE TRIGGER IF NOT EXISTS wiki_pages_fts_delete AFTER DELETE ON wiki_pages BEGIN
INSERT INTO wiki_pages_fts (wiki_pages_fts, rowid, {insert_cols})
VALUES ('delete', old.id, {old_vals});
END;"
);
let trigger_update = format!(
"CREATE TRIGGER IF NOT EXISTS wiki_pages_fts_update AFTER UPDATE ON wiki_pages BEGIN
INSERT INTO wiki_pages_fts (wiki_pages_fts, rowid, {insert_cols})
VALUES ('delete', old.id, {old_vals});
INSERT INTO wiki_pages_fts (rowid, {insert_cols})
VALUES (new.id, {new_vals});
END;"
);
tx.execute_batch(&format!(
"{}\n{}\n{}\nCREATE INDEX IF NOT EXISTS wiki_pages_path_idx ON wiki_pages(path);",
trigger_insert, trigger_delete, trigger_update
))?;
backfill_wiki_fts(tx, columns)?;
Ok(())
}
fn backfill_wiki_fts(tx: &Transaction<'_>, columns: &[&str]) -> Result<()> {
let existing: i64 = tx.query_row("SELECT COUNT(*) FROM wiki_pages_fts LIMIT 1", [], |r| {
r.get(0)
})?;
if existing > 0 {
return Ok(());
}
let select_cols = columns.join(", ");
let insert_cols = columns.join(", ");
let placeholders: Vec<String> = columns
.iter()
.enumerate()
.map(|(i, _)| format!("?{}", i + 2))
.collect();
let insert_vals = placeholders.join(", ");
let mut stmt = tx.prepare(&format!(
"SELECT id, {select_cols} FROM wiki_pages ORDER BY id"
))?;
let rows: Vec<(i64, Vec<Option<String>>)> = stmt
.query_map([], |r| {
let id: i64 = r.get(0)?;
let mut vals = Vec::with_capacity(columns.len());
for i in 0..columns.len() {
vals.push(r.get::<_, Option<String>>(i + 1)?);
}
Ok((id, vals))
})?
.collect::<Result<Vec<_>, _>>()?;
drop(stmt);
let sql =
format!("INSERT INTO wiki_pages_fts (rowid, {insert_cols}) VALUES (?1, {insert_vals})");
for (id, vals) in rows {
let mut params: Vec<&dyn rusqlite::ToSql> = Vec::with_capacity(vals.len() + 1);
params.push(&id);
let owned: Vec<Option<String>> = vals;
for v in &owned {
params.push(v as &dyn rusqlite::ToSql);
}
tx.execute(&sql, params.as_slice())?;
}
tx.execute(
"INSERT INTO wiki_pages_fts(wiki_pages_fts) VALUES('rebuild')",
[],
)?;
Ok(())
}