ensure the search database is attached to all connections
This commit is contained in:
parent
aa47ec7c43
commit
a0c5122735
@ -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
|
||||
|
@ -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> {
|
||||
conn.execute(
|
||||
"CREATE VIRTUAL TABLE search.fts USING fts5(book_id, data)",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(SEARCH_INIT_QUERY, [])?;
|
||||
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")?;
|
||||
|
Loading…
Reference in New Issue
Block a user