libikarus 0.1.14

The core functionality of Ikarus wrapped neatly in a rust library
Documentation
use crate::args::validate::{validate_path_parent_exists, PathValidationError};
use crate::objects::FromId;
use itertools::Itertools;
use sqrite::connection::{Connection, DbError, DbLocation};
use sqrite::sql_interface::FromSqlError;
use std::error::Error;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use tracing::*;

use crate::objects::id::TypedId;
use crate::objects::object::{Object, ObjectTypes};
use crate::persistence::migrations;

pub struct Project {
    // no need to add any locking here, as SQLite does _not_ support proper multithreading with a
    // single connection
    // even if you used the "Serialized" MT mode all transactions, even readonly ones would be
    // "serialized", i.e. sequential, which removes the entire point of multithreading.
    // Instead you can use ProjectConnections multiple times from multiple threads and connect to
    // the database that way
    pub(crate) db: Rc<Connection>,
    path: Option<PathBuf>,
}

#[derive(thiserror::Error, Debug)]
pub enum ProjectError {
    #[error("unable to open database: {0}")]
    DbOpeningError(DbError),
    #[error("unable to interface with database: {0}")]
    DbError(#[from] DbError),
    #[error("unable to obtain db handle")]
    DbLockError,
    #[error("invalid path: {0}")]
    PathValidationError(#[from] PathValidationError),
    #[error("unable to access file on filesystem: {0}")]
    FilesystemError(#[from] std::io::Error),
    #[error("unable to fetch read-only handle to project's db")]
    CannotFetchReadOnlyHandle,
    #[error("unable to fetch read-write handle to project's db")]
    CannotFetchReadWriteHandle,
    #[error("unable to migrate project: {0}")]
    MigrationError(Box<dyn Error + Send + Sync>),
    #[error("cannot close project while connection is still in use")]
    StillInUse,
}

#[derive(thiserror::Error, Debug)]
pub enum ProjectDataError {
    #[error("failed to interface with database: {0}")]
    DatabaseError(#[from] DbError),
    #[error("failed to interface with project: {0}")]
    ProjectError(#[from] ProjectError),
}

impl Project {
    #[instrument(level = Level::DEBUG, skip(self))]
    pub fn get_path(&self) -> Option<&PathBuf> {
        self.path.as_ref()
    }
}

impl Project {
    #[instrument(level = Level::INFO, skip(path), fields(path = path.as_ref().display().to_string()))]
    pub fn open(path: impl AsRef<Path>) -> Result<Project, ProjectError> {
        validate_path_parent_exists(&path)?;

        let mut ret = Project {
            db: Rc::new(Connection::open(DbLocation::Path(path.as_ref()))?),
            path: Some(path.as_ref().to_path_buf()),
        };

        ret.migrate()?;

        Ok(ret)
    }

    #[instrument(level = Level::INFO)]
    pub fn open_in_memory() -> Result<Project, ProjectError> {
        let mut ret = Project {
            db: Rc::new(Connection::open(DbLocation::InMemory)?),
            path: None,
        };

        ret.migrate()?;

        Ok(ret)
    }

    #[instrument(level = Level::INFO, skip(self))]
    fn migrate(&mut self) -> Result<(), ProjectError> {
        self.db.migrate(migrations::get_migrations())?;

        Ok(())
    }

    #[instrument(level = Level::INFO, skip(self))]
    pub fn close(self) -> Result<(), ProjectError> {
        Rc::into_inner(self.db)
            .ok_or(ProjectError::StillInUse)?
            .close()?;

        Ok(())
    }

    #[instrument(level = Level::INFO, skip(self))]
    pub fn delete(self) -> Result<(), ProjectError> {
        Rc::into_inner(self.db)
            .ok_or(ProjectError::StillInUse)?
            .close()?;

        if let Some(path) = self.path {
            std::fs::remove_file(path)?;
        }

        Ok(())
    }

    #[instrument(level = Level::DEBUG, skip(self))]
    pub fn get_objects(&self, object_types: ObjectTypes) -> Result<Vec<Object>, ProjectDataError> {
        let object_types = object_types
            .iter()
            .map(|t| (t.bits().trailing_zeros() + 1))
            .collect_vec();
        let question_marks = object_types.iter().map(|_| "?").join(", ");

        Ok(self.db.query_all(
            format!(
                "SELECT `id` FROM `objects` WHERE `object_type` IN ({})",
                question_marks
            ),
            object_types,
            |row| {
                Ok(Object::from_id(self.db.clone(), row.get::<TypedId>(0)?)
                    .map_err(|v| FromSqlError::Custom(v.to_string()))?)
            },
        )?)
    }
}