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 {
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()))?)
},
)?)
}
}