Refactor db schema and use Diesel

This commit is contained in:
Elias Projahn 2024-03-23 18:06:46 +01:00
parent 2f6676ba3b
commit 220821a0e0
14 changed files with 1310 additions and 599 deletions

87
src/db/mod.rs Normal file
View file

@ -0,0 +1,87 @@
pub mod models;
pub mod schema;
pub mod tables;
use std::collections::HashMap;
use anyhow::{anyhow, Result};
use diesel::{
backend::Backend,
deserialize::{self, FromSql, FromSqlRow},
expression::AsExpression,
prelude::*,
serialize::{self, IsNull, Output, ToSql},
sql_types::Text,
sqlite::Sqlite,
};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness};
use serde::{Deserialize, Serialize};
// This makes the SQL migration scripts accessible from the code.
const MIGRATIONS: EmbeddedMigrations = diesel_migrations::embed_migrations!();
/// Connect to a Musicus database and apply any pending migrations.
pub fn connect(file_name: &str) -> Result<SqliteConnection> {
log::info!("Opening database file '{}'", file_name);
let mut connection = SqliteConnection::establish(file_name)?;
log::info!("Running migrations if necessary");
connection
.run_pending_migrations(MIGRATIONS)
.map_err(|e| anyhow!(e))?;
// Enable after running migrations to simplify changes in schema.
diesel::sql_query("PRAGMA foreign_keys = ON").execute(&mut connection)?;
Ok(connection)
}
/// A single translated string value.
#[derive(Serialize, Deserialize, AsExpression, FromSqlRow, Clone, Debug)]
#[diesel(sql_type = Text)]
pub struct TranslatedString(HashMap<String, String>);
impl TranslatedString {
/// Get the best translation for the user's current locale.
///
/// This will fall back to the generic variant if no translation exists. If no
/// generic translation exists (which is a bug in the data), an empty string is
/// returned and a warning is logged.
pub fn get(&self) -> &str {
// TODO: Get language from locale.
let lang = "generic";
match self.0.get(lang) {
Some(s) => s,
None => match self.0.get("generic") {
Some(s) => s,
None => {
log::warn!("No generic variant for TranslatedString: {:?}", self);
""
}
},
}
}
}
impl<DB: Backend> FromSql<Text, DB> for TranslatedString
where
String: FromSql<Text, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
let text = String::from_sql(bytes)?;
let translated_string = serde_json::from_str(&text)?;
Ok(translated_string)
}
}
impl ToSql<Text, Sqlite> for TranslatedString
where
String: ToSql<Text, Sqlite>,
{
fn to_sql(&self, out: &mut Output<Sqlite>) -> serialize::Result {
let text = serde_json::to_string(self)?;
out.set_value(text);
Ok(IsNull::No)
}
}

297
src/db/models.rs Normal file
View file

@ -0,0 +1,297 @@
//! This module contains higher-level models combining information from
//! multiple database tables.
use std::{fmt::Display, path::Path};
use anyhow::Result;
use diesel::prelude::*;
use super::{schema::*, tables, TranslatedString};
// Re-exports for tables that don't need additional information.
pub use tables::{Instrument, Person, Role};
#[derive(Clone, Debug)]
pub struct Work {
pub work_id: String,
pub name: TranslatedString,
pub parts: Vec<WorkPart>,
pub persons: Vec<Person>,
pub instruments: Vec<Instrument>,
}
#[derive(Clone, Debug)]
pub struct WorkPart {
pub work_id: String,
pub level: u8,
pub name: TranslatedString,
}
#[derive(Clone, Debug)]
pub struct Ensemble {
pub ensemble_id: String,
pub name: TranslatedString,
pub persons: Vec<(Person, Instrument)>,
}
#[derive(Clone, Debug)]
pub struct Recording {
pub recording_id: String,
pub work: Work,
pub year: Option<i32>,
pub persons: Vec<Performer>,
pub ensembles: Vec<Ensemble>,
pub tracks: Vec<Track>,
}
#[derive(Clone, Debug)]
pub struct Performer {
pub person: Person,
pub role: Role,
pub instrument: Option<Instrument>,
}
#[derive(Clone, Debug)]
pub struct Track {
pub track_id: String,
pub path: String,
pub works: Vec<Work>,
}
impl Eq for Person {}
impl PartialEq for Person {
fn eq(&self, other: &Self) -> bool {
self.person_id == other.person_id
}
}
impl Work {
pub fn from_table(data: tables::Work, connection: &mut SqliteConnection) -> Result<Self> {
fn visit_children(
work_id: &str,
level: u8,
connection: &mut SqliteConnection,
) -> Result<Vec<WorkPart>> {
let mut parts = Vec::new();
let children: Vec<tables::Work> = works::table
.filter(works::parent_work_id.eq(work_id))
.load(connection)?;
for child in children {
let mut grand_children = visit_children(&child.work_id, level + 1, connection)?;
parts.push(WorkPart {
work_id: child.work_id,
level,
name: child.name,
});
parts.append(&mut grand_children);
}
Ok(parts)
}
let parts = visit_children(&data.work_id, 0, connection)?;
let persons: Vec<Person> = persons::table
.inner_join(work_persons::table)
.order(work_persons::sequence_number)
.filter(work_persons::work_id.eq(&data.work_id))
.select(tables::Person::as_select())
.load(connection)?;
let instruments: Vec<Instrument> = instruments::table
.inner_join(work_instruments::table)
.order(work_instruments::sequence_number)
.filter(work_instruments::work_id.eq(&data.work_id))
.select(tables::Instrument::as_select())
.load(connection)?;
Ok(Self {
work_id: data.work_id,
name: data.name,
parts,
persons,
instruments,
})
}
pub fn composers_string(&self) -> String {
self.persons
.iter()
.map(|p| p.name.get().to_string())
.collect::<Vec<String>>()
.join(", ")
}
}
impl Eq for Work {}
impl PartialEq for Work {
fn eq(&self, other: &Self) -> bool {
self.work_id == other.work_id
}
}
impl Ensemble {
pub fn from_table(data: tables::Ensemble, connection: &mut SqliteConnection) -> Result<Self> {
let persons: Vec<(Person, Instrument)> = persons::table
.inner_join(ensemble_persons::table.inner_join(instruments::table))
.order(ensemble_persons::sequence_number)
.filter(ensemble_persons::ensemble_id.eq(&data.ensemble_id))
.select((tables::Person::as_select(), tables::Instrument::as_select()))
.load(connection)?;
Ok(Self {
ensemble_id: data.ensemble_id,
name: data.name,
persons,
})
}
}
impl Eq for Ensemble {}
impl PartialEq for Ensemble {
fn eq(&self, other: &Self) -> bool {
self.ensemble_id == other.ensemble_id
}
}
impl Recording {
pub fn from_table(
data: tables::Recording,
library_path: &str,
connection: &mut SqliteConnection,
) -> Result<Self> {
let work = Work::from_table(
works::table
.filter(works::work_id.eq(&data.work_id))
.first::<tables::Work>(connection)?,
connection,
)?;
let persons = recording_persons::table
.order(recording_persons::sequence_number)
.filter(recording_persons::recording_id.eq(&data.recording_id))
.load::<tables::RecordingPerson>(connection)?
.into_iter()
.map(|r| Performer::from_table(r, connection))
.collect::<Result<Vec<Performer>>>()?;
let ensembles: Vec<Ensemble> = ensembles::table
.inner_join(recording_ensembles::table)
.order(recording_ensembles::sequence_number)
.filter(recording_ensembles::recording_id.eq(&data.recording_id))
.select(tables::Ensemble::as_select())
.load::<tables::Ensemble>(connection)?
.into_iter()
.map(|e| Ensemble::from_table(e, connection))
.collect::<Result<Vec<Ensemble>>>()?;
let tracks: Vec<Track> = tracks::table
.order(tracks::sequence_number)
.filter(tracks::recording_id.eq(&data.recording_id))
.select(tables::Track::as_select())
.load::<tables::Track>(connection)?
.into_iter()
.map(|t| Track::from_table(t, library_path, connection))
.collect::<Result<Vec<Track>>>()?;
Ok(Self {
recording_id: data.recording_id,
work,
year: data.year,
persons,
ensembles,
tracks,
})
}
pub fn performers_string(&self) -> String {
let mut performers = self
.persons
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>();
performers.append(
&mut self
.ensembles
.iter()
.map(|e| e.name.get().to_string())
.collect::<Vec<String>>(),
);
performers.join(", ")
}
}
impl Performer {
pub fn from_table(
data: tables::RecordingPerson,
connection: &mut SqliteConnection,
) -> Result<Self> {
let person: Person = persons::table
.filter(persons::person_id.eq(&data.person_id))
.first(connection)?;
let role: Role = roles::table
.filter(roles::role_id.eq(&data.role_id))
.first(connection)?;
let instrument = match &data.instrument_id {
Some(instrument_id) => Some(
instruments::table
.filter(instruments::instrument_id.eq(instrument_id))
.first::<Instrument>(connection)?,
),
None => None,
};
Ok(Self {
person,
role,
instrument,
})
}
}
impl Display for Performer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.instrument {
Some(instrument) => {
format!("{} ({})", self.person.name.get(), instrument.name.get()).fmt(f)
}
None => self.person.name.get().fmt(f),
}
}
}
impl Track {
pub fn from_table(
data: tables::Track,
library_path: &str,
connection: &mut SqliteConnection,
) -> Result<Self> {
let works: Vec<Work> = works::table
.inner_join(track_works::table)
.order(track_works::sequence_number)
.filter(track_works::track_id.eq(&data.track_id))
.select(tables::Work::as_select())
.load::<tables::Work>(connection)?
.into_iter()
.map(|w| Work::from_table(w, connection))
.collect::<Result<Vec<Work>>>()?;
Ok(Self {
track_id: data.track_id,
path: Path::new(library_path)
.join(&data.path)
.to_str()
.unwrap()
.to_string(),
works,
})
}
}

181
src/db/schema.rs Normal file
View file

@ -0,0 +1,181 @@
// @generated automatically by Diesel CLI.
diesel::table! {
ensemble_persons (ensemble_id, person_id, instrument_id) {
ensemble_id -> Text,
person_id -> Text,
instrument_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
ensembles (ensemble_id) {
ensemble_id -> Text,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
instruments (instrument_id) {
instrument_id -> Text,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
persons (person_id) {
person_id -> Text,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
recording_ensembles (recording_id, ensemble_id, role_id) {
recording_id -> Text,
ensemble_id -> Text,
role_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
recording_persons (recording_id, person_id, role_id, instrument_id) {
recording_id -> Text,
person_id -> Text,
role_id -> Text,
instrument_id -> Nullable<Text>,
sequence_number -> Integer,
}
}
diesel::table! {
recordings (recording_id) {
recording_id -> Text,
work_id -> Text,
year -> Nullable<Integer>,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
roles (role_id) {
role_id -> Text,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
}
}
diesel::table! {
track_works (track_id, work_id) {
track_id -> Text,
work_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
tracks (track_id) {
track_id -> Text,
recording_id -> Text,
sequence_number -> Integer,
path -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::table! {
work_instruments (work_id, instrument_id) {
work_id -> Text,
instrument_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
work_persons (work_id, person_id, role_id) {
work_id -> Text,
person_id -> Text,
role_id -> Text,
sequence_number -> Integer,
}
}
diesel::table! {
work_sections (id) {
id -> BigInt,
work -> Text,
title -> Text,
before_index -> BigInt,
}
}
diesel::table! {
works (work_id) {
work_id -> Text,
parent_work_id -> Nullable<Text>,
sequence_number -> Nullable<Integer>,
name -> Text,
created_at -> Timestamp,
edited_at -> Timestamp,
last_used_at -> Timestamp,
last_played_at -> Nullable<Timestamp>,
}
}
diesel::joinable!(ensemble_persons -> ensembles (ensemble_id));
diesel::joinable!(ensemble_persons -> instruments (instrument_id));
diesel::joinable!(ensemble_persons -> persons (person_id));
diesel::joinable!(recording_ensembles -> ensembles (ensemble_id));
diesel::joinable!(recording_ensembles -> recordings (recording_id));
diesel::joinable!(recording_ensembles -> roles (role_id));
diesel::joinable!(recording_persons -> instruments (instrument_id));
diesel::joinable!(recording_persons -> persons (person_id));
diesel::joinable!(recording_persons -> recordings (recording_id));
diesel::joinable!(recording_persons -> roles (role_id));
diesel::joinable!(recordings -> works (work_id));
diesel::joinable!(track_works -> tracks (track_id));
diesel::joinable!(track_works -> works (work_id));
diesel::joinable!(tracks -> recordings (recording_id));
diesel::joinable!(work_instruments -> instruments (instrument_id));
diesel::joinable!(work_instruments -> works (work_id));
diesel::joinable!(work_persons -> persons (person_id));
diesel::joinable!(work_persons -> roles (role_id));
diesel::joinable!(work_persons -> works (work_id));
diesel::allow_tables_to_appear_in_same_query!(
ensemble_persons,
ensembles,
instruments,
persons,
recording_ensembles,
recording_persons,
recordings,
roles,
track_works,
tracks,
work_instruments,
work_persons,
work_sections,
works,
);

142
src/db/tables.rs Normal file
View file

@ -0,0 +1,142 @@
//! This module contains structs that are one-to-one representations of the
//! tables in the database schema.
use chrono::NaiveDateTime;
use diesel::prelude::*;
use diesel::sqlite::Sqlite;
use super::{schema::*, TranslatedString};
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Person {
pub person_id: String,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Role {
pub role_id: String,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Instrument {
pub instrument_id: String,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Work {
pub work_id: String,
pub parent_work_id: Option<String>,
pub sequence_number: Option<i32>,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct WorkPerson {
pub work_id: String,
pub person_id: String,
pub role_id: String,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct WorkInstrument {
pub work_id: String,
pub instrument_id: String,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Ensemble {
pub ensemble_id: String,
pub name: TranslatedString,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct EnsemblePerson {
pub ensemble_id: String,
pub person_id: String,
pub instrument_id: String,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Recording {
pub recording_id: String,
pub work_id: String,
pub year: Option<i32>,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct RecordingPerson {
pub recording_id: String,
pub person_id: String,
pub role_id: String,
pub instrument_id: Option<String>,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct RecordingEnsemble {
pub recording_id: String,
pub ensemble_id: String,
pub role_id: String,
pub sequence_number: i32,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct Track {
pub track_id: String,
pub recording_id: String,
pub sequence_number: i32,
pub path: String,
pub created_at: NaiveDateTime,
pub edited_at: NaiveDateTime,
pub last_used_at: NaiveDateTime,
pub last_played_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Queryable, Selectable, Clone, Debug)]
#[diesel(check_for_backend(Sqlite))]
pub struct TrackWork {
pub track_id: String,
pub work_id: String,
pub sequence_number: i32,
}