Attach shared and local DBs to temporary main DB

We now have one connection to the temporary main DB instead of one for
each. The two actual databases are attached to it during initialization.
This commit is contained in:
Reinout Meliesie 2026-01-22 12:02:51 +01:00
commit 8a8ea5fb94
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
4 changed files with 92 additions and 62 deletions

View file

@ -10,4 +10,24 @@ macro_rules! concat_os_str {
pub trait ResultExt<T, E> {
async fn and_then_async<U, F>(self, op: F) -> Result<U, E>
where
F: AsyncFnOnce(T) -> Result<U, E>;
}
impl<T, E> ResultExt<T, E> for Result<T, E> {
async fn and_then_async<U, F>(self, op: F) -> Result<U, E>
where
F: AsyncFnOnce(T) -> Result<U, E>,
{
match self {
Ok(value) => op(value).await,
Err(error) => Err(error),
}
}
}
pub(crate) use concat_os_str; pub(crate) use concat_os_str;

View file

@ -74,9 +74,7 @@ impl DataManager {
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DataManagerError { pub enum DataManagerError {
NoHomeDir, NoHomeDir,
CannotOpenSharedDB, CannotOpenDB,
UnknownSharedDBError, UnknownDBError,
CannotOpenLocalDB,
UnknownLocalDBError,
UnknownTextureError, UnknownTextureError,
} }

View file

@ -5,38 +5,29 @@ use std::fmt::{Debug, Formatter};
use async_sqlite::rusqlite::fallible_iterator::FallibleIterator; use async_sqlite::rusqlite::fallible_iterator::FallibleIterator;
use async_sqlite::rusqlite::{OpenFlags, Row}; use async_sqlite::rusqlite::{OpenFlags, Row};
use async_sqlite::{Client, ClientBuilder, rusqlite}; use async_sqlite::{Client, ClientBuilder, rusqlite};
use tokio::try_join;
use crate::persist::common::concat_os_str; use crate::persist::common::{ResultExt, concat_os_str};
use crate::persist::data_manager::DataManagerError; use crate::persist::data_manager::DataManagerError;
use crate::views::overview::{FilmOverview, SeriesOverview}; use crate::views::overview::{FilmOverview, SeriesOverview};
pub struct SqliteManager { pub struct SqliteManager {
client_shared: Client, client: Client,
client_local: Client,
} }
impl SqliteManager { impl SqliteManager {
pub async fn new(data_dir: &OsStr) -> Result<SqliteManager, DataManagerError> { pub async fn new(data_dir: &OsStr) -> Result<SqliteManager, DataManagerError> {
let (client_shared, client_local) = try_join!( let client = create_client(data_dir).await?;
create_client(data_dir, DBType::Shared), Ok(SqliteManager { client })
create_client(data_dir, DBType::Local),
)?;
Ok(SqliteManager {
client_shared,
client_local,
})
} }
// The order of the items is undefined. // The order of the items is undefined.
pub async fn films_overview(&self) -> Result<Vec<FilmOverview>, DataManagerError> { pub async fn films_overview(&self) -> Result<Vec<FilmOverview>, DataManagerError> {
let overview = self let overview = self
.client_shared .client
.conn(|connection| { .conn(|connection| {
let overview = connection connection
.prepare( .prepare(
" "
select uuid, name, original_name, release_date, runtime_minutes select uuid, name, original_name, release_date, runtime_minutes
@ -47,15 +38,14 @@ impl SqliteManager {
.query(()) .query(())
.expect("parameters in films overview query should match those in its statement") .expect("parameters in films overview query should match those in its statement")
.map(row_to_film_overview) .map(row_to_film_overview)
.collect()?; .collect()
Ok(overview)
}) })
.await; .await;
overview.map_err(|async_sqlite_error| match async_sqlite_error { overview.map_err(|async_sqlite_error| match async_sqlite_error {
async_sqlite::Error::Closed => panic!( async_sqlite::Error::Closed => {
"shared database connection should remain open as long as the application is running" panic!("database connection should remain open as long as the application is running")
), }
async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error { async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error {
rusqlite::Error::InvalidColumnIndex(_) => { rusqlite::Error::InvalidColumnIndex(_) => {
panic!("column indices obtained from films overview query should exist") panic!("column indices obtained from films overview query should exist")
@ -66,18 +56,18 @@ impl SqliteManager {
rusqlite::Error::InvalidColumnType(..) => panic!( rusqlite::Error::InvalidColumnType(..) => panic!(
"values obtained from films overview query should have a type matching their column" "values obtained from films overview query should have a type matching their column"
), ),
_ => DataManagerError::UnknownSharedDBError, _ => DataManagerError::UnknownDBError,
}, },
_ => DataManagerError::UnknownSharedDBError, _ => DataManagerError::UnknownDBError,
}) })
} }
// The order of the items is undefined. // The order of the items is undefined.
pub async fn series_overview(&self) -> Result<Vec<SeriesOverview>, DataManagerError> { pub async fn series_overview(&self) -> Result<Vec<SeriesOverview>, DataManagerError> {
let overview = self let overview = self
.client_shared .client
.conn(|connection| { .conn(|connection| {
let overview = connection connection
.prepare( .prepare(
" "
select series.uuid, series.name, series.original_name, select series.uuid, series.name, series.original_name,
@ -91,15 +81,14 @@ impl SqliteManager {
.query(()) .query(())
.expect("parameters in series overview query should match those in its statement") .expect("parameters in series overview query should match those in its statement")
.map(row_to_series_overview) .map(row_to_series_overview)
.collect()?; .collect()
Ok(overview)
}) })
.await; .await;
overview.map_err(|async_sqlite_error| match async_sqlite_error { overview.map_err(|async_sqlite_error| match async_sqlite_error {
async_sqlite::Error::Closed => panic!( async_sqlite::Error::Closed => {
"shared database connection should remain open as long as the application is running" panic!("database connection should remain open as long as the application is running")
), }
async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error { async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error {
rusqlite::Error::InvalidColumnIndex(_) => { rusqlite::Error::InvalidColumnIndex(_) => {
panic!("column indices obtained from series overview query should exist") panic!("column indices obtained from series overview query should exist")
@ -110,9 +99,9 @@ impl SqliteManager {
rusqlite::Error::InvalidColumnType(..) => panic!( rusqlite::Error::InvalidColumnType(..) => panic!(
"values obtained from series overview query should have a type matching their column" "values obtained from series overview query should have a type matching their column"
), ),
_ => DataManagerError::UnknownSharedDBError, _ => DataManagerError::UnknownDBError,
}, },
_ => DataManagerError::UnknownSharedDBError, _ => DataManagerError::UnknownDBError,
}) })
} }
} }
@ -123,39 +112,57 @@ impl Debug for SqliteManager {
} }
} }
async fn create_client(data_dir: &OsStr, db_type: DBType) -> Result<Client, DataManagerError> { async fn create_client(data_dir: &OsStr) -> Result<Client, DataManagerError> {
let open_mode = match db_type {
DBType::Shared => OpenFlags::SQLITE_OPEN_READ_ONLY,
DBType::Local => OpenFlags::SQLITE_OPEN_READ_WRITE,
};
let filename = match db_type {
DBType::Shared => "/shared.sqlite",
DBType::Local => "/local.sqlite",
};
let cannot_open_err = match db_type {
DBType::Shared => DataManagerError::CannotOpenSharedDB,
DBType::Local => DataManagerError::CannotOpenLocalDB,
};
let unknown_err = match db_type {
DBType::Shared => DataManagerError::UnknownSharedDBError,
DBType::Local => DataManagerError::UnknownLocalDBError,
};
let client = ClientBuilder::new() let client = ClientBuilder::new()
.path(concat_os_str!(data_dir, filename)) .path("")
.flags(open_mode | OpenFlags::SQLITE_OPEN_NO_MUTEX) .flags(OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX)
.open() .open()
.await; .await;
let data_dir = data_dir.to_os_string();
let client = client
.and_then_async(async move |client| {
client
.conn(move |connection| {
let shared_path_os_str = concat_os_str!(&data_dir, "/shared.sqlite");
let shared_path = shared_path_os_str
.to_str()
.expect("shared database path should be valid Unicode");
connection
.execute("attach database :path as shared", &[(":path", shared_path)])
.expect(
"shared database attaching statement should be valid SQL with matching parameters",
);
let local_path_os_str = concat_os_str!(&data_dir, "/local.sqlite");
let local_path = local_path_os_str
.to_str()
.expect("local database path should be valid Unicode");
connection
.execute("attach database :path as local", &[(":path", local_path)])
.expect(
"local database attaching statement should be valid SQL with matching parameters",
);
Ok(())
})
.await
.map(|_| client)
})
.await;
client.map_err(|async_sqlite_error| match async_sqlite_error { client.map_err(|async_sqlite_error| match async_sqlite_error {
async_sqlite::Error::Closed => {
panic!("database connection should remain open as long as the application is running")
}
async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error { async_sqlite::Error::Rusqlite(rusqlite_error) => match rusqlite_error {
rusqlite::Error::SqliteFailure(sqlite_error, _) => match sqlite_error.code { rusqlite::Error::SqliteFailure(sqlite_error, _) => match sqlite_error.code {
rusqlite::ffi::ErrorCode::CannotOpen => cannot_open_err, rusqlite::ffi::ErrorCode::CannotOpen => DataManagerError::CannotOpenDB,
_ => unknown_err, _ => DataManagerError::UnknownDBError,
}, },
_ => unknown_err, _ => DataManagerError::UnknownDBError,
}, },
_ => unknown_err, _ => DataManagerError::UnknownDBError,
}) })
} }

View file

@ -1,5 +1,5 @@
use gtk4::prelude::{BoxExt, OrientableExt, WidgetExt}; use gtk4::prelude::{BoxExt, ButtonExt, OrientableExt, WidgetExt};
use gtk4::{Label, Orientation}; use gtk4::{Button, Label, Orientation};
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent, component}; use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent, component};
use crate::views::overview::FilmOverview; use crate::views::overview::FilmOverview;
@ -25,7 +25,12 @@ impl SimpleComponent for FilmDetails {
Label { Label {
set_css_classes: &["title-1"], set_css_classes: &["title-1"],
set_label: model.film_overview.name.as_str(), set_label: model.film_overview.name.as_str(),
} },
Button {
set_css_classes: &["suggested-action", "circular"],
set_icon_name: "media-playback-start-symbolic",
},
} }
} }