|
|
|
@ -6,7 +6,7 @@ extern crate log; |
|
|
|
|
use std::borrow::Cow; |
|
|
|
|
use std::collections::HashMap; |
|
|
|
|
use std::fs::OpenOptions; |
|
|
|
|
use std::io::{BufReader, BufWriter}; |
|
|
|
|
use std::io::{BufReader, BufWriter, Read, Write}; |
|
|
|
|
use std::path::{Path, PathBuf}; |
|
|
|
|
|
|
|
|
|
use itertools::Itertools; |
|
|
|
@ -23,6 +23,8 @@ use model::ObjectModel; |
|
|
|
|
|
|
|
|
|
use crate::model::{PropertyModel, RelationModel}; |
|
|
|
|
use crate::update::{UpdateObj, UpsertValue}; |
|
|
|
|
use crate::cool::IsNoneOrElse; |
|
|
|
|
use crate::data::{Object, Value}; |
|
|
|
|
|
|
|
|
|
mod cool; |
|
|
|
|
pub mod data; |
|
|
|
@ -38,6 +40,9 @@ mod tests; |
|
|
|
|
|
|
|
|
|
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION"); |
|
|
|
|
|
|
|
|
|
pub const YOPA_MAGIC : &[u8; 4] = b"YOPA"; |
|
|
|
|
pub const BINARY_FORMAT : u8 = 1; |
|
|
|
|
|
|
|
|
|
/// Stupid storage with naive inefficient file persistence
|
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize)] |
|
|
|
|
pub struct Storage { |
|
|
|
@ -90,8 +95,14 @@ pub enum StorageError { |
|
|
|
|
NotExist(Cow<'static, str>), |
|
|
|
|
#[error("{0}")] |
|
|
|
|
ConstraintViolation(Cow<'static, str>), |
|
|
|
|
#[error("{0}")] |
|
|
|
|
Invalid(Cow<'static, str>), |
|
|
|
|
#[error("Persistence not configured!")] |
|
|
|
|
NotPersistent, |
|
|
|
|
#[error("Bad magic! Not a binary Yopa file")] |
|
|
|
|
BadMagic, |
|
|
|
|
#[error("Binary format {0} is not compatible with this version of Yopa")] |
|
|
|
|
NotCompatible(u8), |
|
|
|
|
#[error(transparent)] |
|
|
|
|
IO(#[from] std::io::Error), |
|
|
|
|
#[error(transparent)] |
|
|
|
@ -164,11 +175,25 @@ impl Storage { |
|
|
|
|
|
|
|
|
|
let f = OpenOptions::new().read(true).open(&path)?; |
|
|
|
|
|
|
|
|
|
let reader = BufReader::new(f); |
|
|
|
|
let mut reader = BufReader::new(f); |
|
|
|
|
|
|
|
|
|
let parsed: Self = match self.opts.file_format { |
|
|
|
|
FileEncoding::JSON => serde_json::from_reader(reader)?, |
|
|
|
|
FileEncoding::BINCODE => bincode::deserialize_from(reader)?, |
|
|
|
|
FileEncoding::BINCODE => { |
|
|
|
|
let mut magic : [u8; 5] = [0; 5]; |
|
|
|
|
reader.read_exact(&mut magic)?; |
|
|
|
|
|
|
|
|
|
if &magic[0..4] != YOPA_MAGIC { |
|
|
|
|
return Err(StorageError::BadMagic); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let version = magic[4]; |
|
|
|
|
if version != BINARY_FORMAT { |
|
|
|
|
return Err(StorageError::NotCompatible(version)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
bincode::deserialize_from(reader)? |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let opts = std::mem::replace(&mut self.opts, StoreOpts::default()); |
|
|
|
@ -196,13 +221,17 @@ impl Storage { |
|
|
|
|
.create(true) |
|
|
|
|
.truncate(true) |
|
|
|
|
.open(&path)?; |
|
|
|
|
let writer = BufWriter::new(f); |
|
|
|
|
let mut writer = BufWriter::new(f); |
|
|
|
|
|
|
|
|
|
match self.opts.file_format { |
|
|
|
|
FileEncoding::JSON => { |
|
|
|
|
serde_json::to_writer(writer, self)?; |
|
|
|
|
} |
|
|
|
|
FileEncoding::BINCODE => bincode::serialize_into(writer, self)?, |
|
|
|
|
FileEncoding::BINCODE => { |
|
|
|
|
writer.write_all(YOPA_MAGIC)?; |
|
|
|
|
writer.write_all(&[BINARY_FORMAT])?; |
|
|
|
|
bincode::serialize_into(writer, self)? |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -223,6 +252,25 @@ impl Storage { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Get object name, or the ID as string if no name is configured
|
|
|
|
|
pub fn get_object_name_by_id<'a>(&'a self, id : ID) -> Cow<'a, str> { |
|
|
|
|
if let Some(o) = self.get_object(id) { |
|
|
|
|
return self.get_object_name(o); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return id.to_string().into(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn get_object_name<'b, 'a: 'b>(&'a self, object : &'a Object) -> Cow<'b, str> { |
|
|
|
|
if let Some(ObjectModel{ name_property: Some(name_property), .. }) = self.get_object_model(object.model) { |
|
|
|
|
if let Some(v) = self.values.values().find(|v| v.object == object.id && &v.model == name_property) { |
|
|
|
|
return v.value.to_cow(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return object.id.to_string().into(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Get model name. Accepts ID of object, relation or property models.
|
|
|
|
|
pub fn get_model_name(&self, id: ID) -> &str { |
|
|
|
|
if let Some(x) = self.obj_models.get(&id) { |
|
|
|
@ -243,6 +291,11 @@ impl Storage { |
|
|
|
|
self.obj_models.values() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Iterate object models with given IDs
|
|
|
|
|
pub fn get_object_models_by_ids(&self, ids: Vec<ID>) -> impl Iterator<Item = &ObjectModel> { |
|
|
|
|
self.obj_models.values().filter(move |m| ids.contains(&m.id)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Get an object model by ID
|
|
|
|
|
pub fn get_object_model(&self, model_id: ID) -> Option<&ObjectModel> { |
|
|
|
|
self.obj_models.get(&model_id) |
|
|
|
@ -297,6 +350,16 @@ impl Storage { |
|
|
|
|
.filter(move |model| parents.contains(&model.object)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Find properties for a parent ID
|
|
|
|
|
pub fn get_property_models_for_parent( |
|
|
|
|
&self, |
|
|
|
|
parent: ID, |
|
|
|
|
) -> impl Iterator<Item = &PropertyModel> { |
|
|
|
|
self.prop_models |
|
|
|
|
.values() |
|
|
|
|
.filter(move |model| model.object == parent) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Get all relation models, grouped by their source object model ID
|
|
|
|
|
pub fn get_grouped_relation_models(&self) -> HashMap<ID, Vec<&RelationModel>> { |
|
|
|
|
self.rel_models |
|
|
|
@ -485,6 +548,12 @@ impl Storage { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if prop.unique && prop.multiple { |
|
|
|
|
return Err(StorageError::Invalid( |
|
|
|
|
"Multi-value properties cannot have the \"unique\" constraint".into(), |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if self |
|
|
|
|
.prop_models |
|
|
|
|
.iter() |
|
|
|
@ -688,6 +757,12 @@ impl Storage { |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if prop.unique && prop.multiple { |
|
|
|
|
return Err(StorageError::Invalid( |
|
|
|
|
"Multi-value properties cannot have the \"unique\" constraint".into(), |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Object can't be changed, so we re-fill them from the existing model
|
|
|
|
|
if let Some(existing) = self.prop_models.get(&prop.id) { |
|
|
|
|
prop.object = existing.object; |
|
|
|
@ -745,28 +820,10 @@ impl Storage { |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// validate unique name
|
|
|
|
|
if self |
|
|
|
|
.objects |
|
|
|
|
.iter() |
|
|
|
|
.find(|(_, o)| o.model == obj_model_id && o.name == insobj.name) |
|
|
|
|
.is_some() |
|
|
|
|
{ |
|
|
|
|
return Err(StorageError::ConstraintViolation( |
|
|
|
|
format!( |
|
|
|
|
"{} named \"{}\" already exists", |
|
|
|
|
self.get_model_name(obj_model_id), |
|
|
|
|
insobj.name |
|
|
|
|
) |
|
|
|
|
.into(), |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let object_id = self.next_id(); |
|
|
|
|
let object = data::Object { |
|
|
|
|
id: object_id, |
|
|
|
|
model: obj_model_id, |
|
|
|
|
name: insobj.name, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let find_values_to_insert = |values: Vec<InsertValue>, |
|
|
|
@ -776,12 +833,12 @@ impl Storage { |
|
|
|
|
let mut values_by_id = values.into_iter().into_group_map_by(|iv| iv.model); |
|
|
|
|
let mut values_to_insert = vec![]; |
|
|
|
|
|
|
|
|
|
for (id, prop) in self |
|
|
|
|
for (prop_model_id, prop) in self |
|
|
|
|
.prop_models |
|
|
|
|
.iter() |
|
|
|
|
.filter(|(_id, p)| p.object == parent_model_id) |
|
|
|
|
{ |
|
|
|
|
if let Some(values) = values_by_id.remove(id) { |
|
|
|
|
if let Some(values) = values_by_id.remove(prop_model_id) { |
|
|
|
|
if values.len() > 1 && !prop.multiple { |
|
|
|
|
return Err(StorageError::ConstraintViolation( |
|
|
|
|
format!( |
|
|
|
@ -792,6 +849,35 @@ impl Storage { |
|
|
|
|
.into(), |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if prop.unique { |
|
|
|
|
// we know the length is at least 1. Unique should not be allowed together
|
|
|
|
|
// with "multiple", but if it is set so, only the first value will
|
|
|
|
|
// be checked.
|
|
|
|
|
let first = &values[0]; |
|
|
|
|
|
|
|
|
|
// validate unique name
|
|
|
|
|
if self |
|
|
|
|
.values |
|
|
|
|
.iter() |
|
|
|
|
.find(|(_, o)| { |
|
|
|
|
&o.model == prop_model_id |
|
|
|
|
&& o.object == parent_id |
|
|
|
|
&& o.value == first.value |
|
|
|
|
}) |
|
|
|
|
.is_some() |
|
|
|
|
{ |
|
|
|
|
return Err(StorageError::ConstraintViolation( |
|
|
|
|
format!( |
|
|
|
|
"Value {} already used for unique property \"{}\"", |
|
|
|
|
first.value, |
|
|
|
|
self.get_model_name(*prop_model_id), |
|
|
|
|
) |
|
|
|
|
.into(), |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for val_instance in values { |
|
|
|
|
values_to_insert.push(data::Value { |
|
|
|
|
id: self.next_id(), |
|
|
|
@ -920,27 +1006,6 @@ impl Storage { |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// validate unique name
|
|
|
|
|
if self |
|
|
|
|
.objects |
|
|
|
|
.iter() |
|
|
|
|
.find(|(_, o)| { |
|
|
|
|
o.model == updated_object_model_id |
|
|
|
|
&& o.name == updobj.name |
|
|
|
|
&& o.id != updated_object_id |
|
|
|
|
}) |
|
|
|
|
.is_some() |
|
|
|
|
{ |
|
|
|
|
return Err(StorageError::ConstraintViolation( |
|
|
|
|
format!( |
|
|
|
|
"{} named \"{}\" already exists", |
|
|
|
|
self.get_model_name(updated_object_model_id), |
|
|
|
|
updobj.name |
|
|
|
|
) |
|
|
|
|
.into(), |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Update the object after everything else is checked
|
|
|
|
|
|
|
|
|
|
let find_values_to_change = |values: Vec<UpsertValue>, |
|
|
|
@ -982,6 +1047,35 @@ impl Storage { |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if prop.unique { |
|
|
|
|
// we know the length is at least 1. Unique should not be allowed together
|
|
|
|
|
// with "multiple", but if it is set so, only the first value will
|
|
|
|
|
// be checked.
|
|
|
|
|
let first = &values[0]; |
|
|
|
|
|
|
|
|
|
// validate unique name
|
|
|
|
|
if self |
|
|
|
|
.values |
|
|
|
|
.iter() |
|
|
|
|
.find(|(_, o)| { |
|
|
|
|
&o.model == prop_model_id |
|
|
|
|
&& o.object == parent_id |
|
|
|
|
&& o.value == first.value |
|
|
|
|
&& (first.id.is_none_or_else(|id| &o.id != id)) |
|
|
|
|
}) |
|
|
|
|
.is_some() |
|
|
|
|
{ |
|
|
|
|
return Err(StorageError::ConstraintViolation( |
|
|
|
|
format!( |
|
|
|
|
"Value {} already used for unique property \"{}\"", |
|
|
|
|
first.value, |
|
|
|
|
self.get_model_name(*prop_model_id), |
|
|
|
|
) |
|
|
|
|
.into(), |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let updated_ids = values.iter().filter_map(|v| v.id).collect_vec(); |
|
|
|
|
|
|
|
|
|
ids_to_delete.extend( |
|
|
|
@ -1103,7 +1197,6 @@ impl Storage { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let obj_mut = self.objects.get_mut(&updated_object_id).unwrap(); |
|
|
|
|
obj_mut.name = updobj.name; |
|
|
|
|
|
|
|
|
|
debug!("Add {} new object relations", relations_to_insert.len()); |
|
|
|
|
for rel in relations_to_insert { |
|
|
|
@ -1130,8 +1223,9 @@ impl Storage { |
|
|
|
|
|
|
|
|
|
/// Delete an object and associated data
|
|
|
|
|
pub fn delete_object(&mut self, id: ID) -> Result<data::Object, StorageError> { |
|
|
|
|
let name = self.get_object_name_by_id(id).to_string(); |
|
|
|
|
return if let Some(t) = self.objects.remove(&id) { |
|
|
|
|
debug!("Delete object \"{}\"", t.name); |
|
|
|
|
debug!("Delete object \"{}\"", name); |
|
|
|
|
// Remove relation templates
|
|
|
|
|
let removed_relation_ids = map_drain_filter(&mut self.relations, |_k, v| { |
|
|
|
|
v.object == id || v.related == id |
|
|
|
|