ensure the search database is attached to all connections

This commit is contained in:
Sebastian Hugentobler 2024-06-26 17:41:04 +02:00
parent aa47ec7c43
commit a0c5122735
Signed by: shu
GPG Key ID: BB32CF3CA052C2F0
2 changed files with 52 additions and 18 deletions

View File

@ -1,15 +1,16 @@
//! Bundle all functions together.
use std::path::Path;
use std::path::{Path, PathBuf};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use tempfile::NamedTempFile;
use crate::{
data::{
author::Author, book::Book, error::DataStoreError, pagination::SortOrder, series::Series,
},
search::{self, search},
search::search,
};
/// Top level calibre functions, bundling all sub functions in one place and providing secure access to
@ -17,6 +18,7 @@ use crate::{
#[derive(Debug, Clone)]
pub struct Calibre {
pool: Pool<SqliteConnectionManager>,
search_db_path: PathBuf,
}
impl Calibre {
@ -27,16 +29,20 @@ impl Calibre {
let manager = SqliteConnectionManager::file(path);
let pool = r2d2::Pool::new(manager)?;
search::attach(&pool)?;
let tmpfile = NamedTempFile::new()?;
let (_, search_db_path) = tmpfile.keep()?;
Ok(Self { pool })
Ok(Self {
pool,
search_db_path,
})
}
/// Full text search with a query.
///
/// See https://www.sqlite.org/fts5.html#full_text_query_syntax for syntax.
pub fn search(&self, query: &str) -> Result<Vec<Book>, DataStoreError> {
search(query, &self.pool)
search(query, &self.pool, &self.search_db_path)
}
/// Fetch book data from calibre, starting at `cursor`, fetching up to an amount of `limit` and

View File

@ -5,10 +5,11 @@
//! virtual table leveraging fts5 (https://www.sqlite.org/fts5.html). Full-text search is run on
//! that virtual table.
use std::path::Path;
use r2d2::{Pool, PooledConnection};
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::named_params;
use tempfile::NamedTempFile;
use crate::data::{book::Book, error::DataStoreError};
@ -32,28 +33,53 @@ 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";
/// Attach the fts temporary database to the read-only calibre database.
pub(crate) fn attach(pool: &Pool<SqliteConnectionManager>) -> Result<(), DataStoreError> {
let conn = pool.get()?;
let tmpfile = NamedTempFile::new()?;
let (_, path) = tmpfile.keep()?;
/// 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))?;
let need_attachment = count == 0;
if need_attachment {
attach(conn, db_path)?;
init(conn)?;
}
Ok(())
}
/// Attach the fts temporary database to the read-only calibre database.
fn attach(
conn: &PooledConnection<SqliteConnectionManager>,
db_path: &Path,
) -> Result<(), DataStoreError> {
conn.execute(
&format!("ATTACH DATABASE '{}' AS search", path.to_string_lossy()),
&format!("ATTACH DATABASE '{}' AS search", db_path.to_string_lossy()),
[],
)?;
init(&conn)?;
init(conn)?;
Ok(())
}
/// Initialise the fts virtual table.
fn init(conn: &PooledConnection<SqliteConnectionManager>) -> Result<(), DataStoreError> {
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))?;
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, [])?;
}
Ok(())
}
@ -62,8 +88,10 @@ fn init(conn: &PooledConnection<SqliteConnectionManager>) -> Result<(), DataStor
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)?;
let mut stmt =
conn.prepare("SELECT book_id FROM search.fts WHERE data MATCH (:query) ORDER BY rank")?;