initial commit
This commit is contained in:
163
src/database.rs
Normal file
163
src/database.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use diesel::prelude::*;
|
||||
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::schema::{projects, sessions};
|
||||
|
||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
|
||||
#[derive(Clone, Debug, Identifiable, Queryable, Selectable)]
|
||||
#[diesel(table_name = projects)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Project {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = projects)]
|
||||
struct NewProject<'a> {
|
||||
name: &'a str,
|
||||
path: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)]
|
||||
#[diesel(belongs_to(Project))]
|
||||
#[diesel(table_name = sessions)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct Session {
|
||||
pub id: i32,
|
||||
pub project_id: i32,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = sessions)]
|
||||
struct NewSession<'a> {
|
||||
project_id: i32,
|
||||
title: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProjectWithSessions {
|
||||
pub project: Project,
|
||||
pub sessions: Vec<Session>,
|
||||
}
|
||||
|
||||
pub struct Database {
|
||||
connection: SqliteConnection,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn open(path: &Path) -> Result<Self, String> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| "database path has no parent directory".to_owned())?;
|
||||
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
|
||||
let url = path
|
||||
.to_str()
|
||||
.ok_or_else(|| "database path is not valid UTF-8".to_owned())?;
|
||||
let mut connection = SqliteConnection::establish(url).map_err(|error| error.to_string())?;
|
||||
connection
|
||||
.run_pending_migrations(MIGRATIONS)
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(Self { connection })
|
||||
}
|
||||
|
||||
pub fn load_projects(&mut self) -> Result<Vec<ProjectWithSessions>, String> {
|
||||
let project_rows = projects::table
|
||||
.order(projects::id.asc())
|
||||
.select(Project::as_select())
|
||||
.load(&mut self.connection)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let session_rows = sessions::table
|
||||
.order(sessions::id.asc())
|
||||
.select(Session::as_select())
|
||||
.load::<Session>(&mut self.connection)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut sessions_by_project: HashMap<i32, Vec<Session>> = HashMap::new();
|
||||
for session in session_rows {
|
||||
sessions_by_project
|
||||
.entry(session.project_id)
|
||||
.or_default()
|
||||
.push(session);
|
||||
}
|
||||
|
||||
Ok(project_rows
|
||||
.into_iter()
|
||||
.map(|project| ProjectWithSessions {
|
||||
sessions: sessions_by_project.remove(&project.id).unwrap_or_default(),
|
||||
project,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn create_project(&mut self, name: &str, path: &str) -> Result<Project, String> {
|
||||
diesel::insert_into(projects::table)
|
||||
.values(NewProject { name, path })
|
||||
.returning(Project::as_returning())
|
||||
.get_result(&mut self.connection)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub fn delete_project(&mut self, project_id: i32) -> Result<(), String> {
|
||||
self.connection
|
||||
.transaction(|connection| {
|
||||
diesel::delete(sessions::table.filter(sessions::project_id.eq(project_id)))
|
||||
.execute(connection)?;
|
||||
diesel::delete(projects::table.find(project_id)).execute(connection)?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|error: diesel::result::Error| error.to_string())
|
||||
}
|
||||
|
||||
pub fn create_session(&mut self, project_id: i32, title: &str) -> Result<Session, String> {
|
||||
diesel::insert_into(sessions::table)
|
||||
.values(NewSession { project_id, title })
|
||||
.returning(Session::as_returning())
|
||||
.get_result(&mut self.connection)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub fn delete_session(&mut self, session_id: i32) -> Result<(), String> {
|
||||
diesel::delete(sessions::table.find(session_id))
|
||||
.execute(&mut self.connection)
|
||||
.map(|_| ())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[test]
|
||||
fn migrations_and_crud_work() {
|
||||
let id = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("ds4-server-{id}.sqlite3"));
|
||||
let mut database = Database::open(&path).unwrap();
|
||||
|
||||
let project = database.create_project("DS4", "/tmp/ds4").unwrap();
|
||||
let first = database
|
||||
.create_session(project.id, "First session")
|
||||
.unwrap();
|
||||
database
|
||||
.create_session(project.id, "Second session")
|
||||
.unwrap();
|
||||
database.delete_session(first.id).unwrap();
|
||||
let loaded = database.load_projects().unwrap();
|
||||
assert_eq!(loaded[0].sessions[0].title, "Second session");
|
||||
|
||||
database.delete_project(project.id).unwrap();
|
||||
assert!(database.load_projects().unwrap().is_empty());
|
||||
drop(database);
|
||||
fs::remove_file(path).unwrap();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user