This commit is contained in:
Sebastian Hugentobler 2025-07-02 21:09:37 +02:00
parent 1c95f4391f
commit b4a0aadef9
Signed by: shu
SSH key fingerprint: SHA256:ppcx6MlixdNZd5EUM1nkHOKoyQYoJwzuQKXM6J/t66M
73 changed files with 2993 additions and 1632 deletions

View file

@ -10,8 +10,9 @@ use std::path::Path;
use r2d2::{Pool, PooledConnection};
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::named_params;
use snafu::{ResultExt, Snafu};
use crate::data::{book::Book, error::DataStoreError};
use crate::data::book::Book;
/// A lot of joins but only run once at startup.
const SEARCH_INIT_QUERY: &str = "INSERT INTO search.fts(book_id, data)
@ -33,20 +34,61 @@ const SEARCH_INIT_QUERY: &str = "INSERT INTO search.fts(book_id, data)
LEFT JOIN main.series AS s ON b2s.series = s.id
GROUP BY b.id";
#[derive(Debug, Snafu)]
pub enum EnsureSearchDbError {
#[snafu(display("Failed to prepare statement."))]
PrepareEnsureSearch { source: rusqlite::Error },
#[snafu(display("Failed to execute statement."))]
ExecuteEnsureSearch { source: rusqlite::Error },
#[snafu(display("Failed to attach database."))]
Attach { source: AttachError },
#[snafu(display("Failed to initialize database."))]
Init { source: InitError },
}
#[derive(Debug, Snafu)]
pub enum AttachError {
#[snafu(display("Failed to execute statement."))]
ExecuteAttach { source: rusqlite::Error },
}
#[derive(Debug, Snafu)]
pub enum InitError {
#[snafu(display("Failed to prepare statement."))]
PrepareInit { source: rusqlite::Error },
#[snafu(display("Failed to execute statement."))]
ExecuteInit { source: rusqlite::Error },
}
#[derive(Debug, Snafu)]
pub enum SearchError {
#[snafu(display("Failed ensure the search db is initialized."))]
EnsureDb { source: EnsureSearchDbError },
#[snafu(display("Failed to get connection from pool."))]
Connection { source: r2d2::Error },
#[snafu(display("Failed to prepare statement."))]
PrepareSearch { source: rusqlite::Error },
#[snafu(display("Failed to execute statement."))]
ExecuteSearch { source: rusqlite::Error },
}
/// Ensure the search database is attached to the connection and
/// initializes the data if needed.
fn ensure_search_db(
conn: &PooledConnection<SqliteConnectionManager>,
db_path: &Path,
) -> Result<(), DataStoreError> {
let mut stmt =
conn.prepare("SELECT COUNT() FROM pragma_database_list WHERE name = 'search'")?;
let count: u64 = stmt.query_row([], |x| x.get(0))?;
) -> Result<(), EnsureSearchDbError> {
let mut stmt = conn
.prepare("SELECT COUNT() FROM pragma_database_list WHERE name = 'search'")
.context(PrepareEnsureSearchSnafu)?;
let count: u64 = stmt
.query_row([], |x| x.get(0))
.context(ExecuteEnsureSearchSnafu)?;
let need_attachment = count == 0;
if need_attachment {
attach(conn, db_path)?;
init(conn)?;
attach(conn, db_path).context(AttachSnafu)?;
init(conn).context(InitSnafu)?;
}
Ok(())
@ -56,29 +98,32 @@ fn ensure_search_db(
fn attach(
conn: &PooledConnection<SqliteConnectionManager>,
db_path: &Path,
) -> Result<(), DataStoreError> {
) -> Result<(), AttachError> {
conn.execute(
&format!("ATTACH DATABASE '{}' AS search", db_path.to_string_lossy()),
[],
)?;
init(conn)?;
)
.context(ExecuteAttachSnafu)?;
Ok(())
}
/// Initialise the fts virtual table.
fn init(conn: &PooledConnection<SqliteConnectionManager>) -> Result<(), DataStoreError> {
fn init(conn: &PooledConnection<SqliteConnectionManager>) -> Result<(), InitError> {
let mut stmt = conn
.prepare("SELECT COUNT() FROM search.sqlite_master WHERE type='table' AND name = 'fts'")?;
let count: u64 = stmt.query_row([], |x| x.get(0))?;
.prepare("SELECT COUNT() FROM search.sqlite_master WHERE type='table' AND name = 'fts'")
.context(PrepareInitSnafu)?;
let count: u64 = stmt.query_row([], |x| x.get(0)).context(ExecuteInitSnafu)?;
let need_init = count == 0;
if need_init {
conn.execute(
"CREATE VIRTUAL TABLE search.fts USING fts5(book_id, data)",
[],
)?;
conn.execute(SEARCH_INIT_QUERY, [])?;
)
.context(ExecuteInitSnafu)?;
conn.execute(SEARCH_INIT_QUERY, [])
.context(ExecuteInitSnafu)?;
}
Ok(())
@ -89,15 +134,17 @@ pub(crate) fn search(
query: &str,
pool: &Pool<SqliteConnectionManager>,
search_db_path: &Path,
) -> Result<Vec<Book>, DataStoreError> {
let conn = pool.get()?;
ensure_search_db(&conn, search_db_path)?;
) -> Result<Vec<Book>, SearchError> {
let conn = pool.get().context(ConnectionSnafu)?;
ensure_search_db(&conn, search_db_path).context(EnsureDbSnafu)?;
let mut stmt =
conn.prepare("SELECT book_id FROM search.fts WHERE data MATCH (:query) ORDER BY rank")?;
let mut stmt = conn
.prepare("SELECT book_id FROM search.fts WHERE data MATCH (:query) ORDER BY rank")
.context(PrepareSearchSnafu)?;
let params = named_params! { ":query": query };
let books = stmt
.query_map(params, |r| -> Result<u64, rusqlite::Error> { r.get(0) })?
.query_map(params, |r| -> Result<u64, rusqlite::Error> { r.get(0) })
.context(ExecuteSearchSnafu)?
.filter_map(Result::ok)
.filter_map(|id| Book::scalar_book(&conn, id).ok())
.collect();