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. //! Bundle all functions together.
use std::path::Path; use std::path::{Path, PathBuf};
use r2d2::Pool; use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
use tempfile::NamedTempFile;
use crate::{ use crate::{
data::{ data::{
author::Author, book::Book, error::DataStoreError, pagination::SortOrder, series::Series, 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 /// 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)] #[derive(Debug, Clone)]
pub struct Calibre { pub struct Calibre {
pool: Pool<SqliteConnectionManager>, pool: Pool<SqliteConnectionManager>,
search_db_path: PathBuf,
} }
impl Calibre { impl Calibre {
@ -27,16 +29,20 @@ impl Calibre {
let manager = SqliteConnectionManager::file(path); let manager = SqliteConnectionManager::file(path);
let pool = r2d2::Pool::new(manager)?; 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. /// Full text search with a query.
/// ///
/// See https://www.sqlite.org/fts5.html#full_text_query_syntax for syntax. /// See https://www.sqlite.org/fts5.html#full_text_query_syntax for syntax.
pub fn search(&self, query: &str) -> Result<Vec<Book>, DataStoreError> { 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 /// 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 //! virtual table leveraging fts5 (https://www.sqlite.org/fts5.html). Full-text search is run on
//! that virtual table. //! that virtual table.
use std::path::Path;
use r2d2::{Pool, PooledConnection}; use r2d2::{Pool, PooledConnection};
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::named_params; use rusqlite::named_params;
use tempfile::NamedTempFile;
use crate::data::{book::Book, error::DataStoreError}; 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 LEFT JOIN main.series AS s ON b2s.series = s.id
GROUP BY b.id"; GROUP BY b.id";
/// Attach the fts temporary database to the read-only calibre database. /// Ensure the search database is attached to the connection and
pub(crate) fn attach(pool: &Pool<SqliteConnectionManager>) -> Result<(), DataStoreError> { /// initializes the data if needed.
let conn = pool.get()?; fn ensure_search_db(
let tmpfile = NamedTempFile::new()?; conn: &PooledConnection<SqliteConnectionManager>,
let (_, path) = tmpfile.keep()?; 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( 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(()) Ok(())
} }
/// Initialise the fts virtual table. /// Initialise the fts virtual table.
fn init(conn: &PooledConnection<SqliteConnectionManager>) -> Result<(), DataStoreError> { 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( conn.execute(
"CREATE VIRTUAL TABLE search.fts USING fts5(book_id, data)", "CREATE VIRTUAL TABLE search.fts USING fts5(book_id, data)",
[], [],
)?; )?;
conn.execute(SEARCH_INIT_QUERY, [])?; conn.execute(SEARCH_INIT_QUERY, [])?;
}
Ok(()) Ok(())
} }
@ -62,8 +88,10 @@ fn init(conn: &PooledConnection<SqliteConnectionManager>) -> Result<(), DataStor
pub(crate) fn search( pub(crate) fn search(
query: &str, query: &str,
pool: &Pool<SqliteConnectionManager>, pool: &Pool<SqliteConnectionManager>,
search_db_path: &Path,
) -> Result<Vec<Book>, DataStoreError> { ) -> Result<Vec<Book>, DataStoreError> {
let conn = pool.get()?; let conn = pool.get()?;
ensure_search_db(&conn, search_db_path)?;
let mut stmt = let mut stmt =
conn.prepare("SELECT book_id FROM search.fts WHERE data MATCH (:query) ORDER BY rank")?; conn.prepare("SELECT book_id FROM search.fts WHERE data MATCH (:query) ORDER BY rank")?;