use crate::entities::*;
use crate::snapshot::{StoreSnapshot, StoreSnapshotTrait};
use crate::types::EntityId;
use im::HashMap;
use std::sync::RwLock;
#[derive(Debug)]
pub struct HashMapStore {
pub roots: RwLock<HashMap<EntityId, Root>>,
pub workspaces: RwLock<HashMap<EntityId, Workspace>>,
pub systems: RwLock<HashMap<EntityId, System>>,
pub entities: RwLock<HashMap<EntityId, Entity>>,
pub fields: RwLock<HashMap<EntityId, Field>>,
pub features: RwLock<HashMap<EntityId, Feature>>,
pub files: RwLock<HashMap<EntityId, File>>,
pub use_cases: RwLock<HashMap<EntityId, UseCase>>,
pub dtos: RwLock<HashMap<EntityId, Dto>>,
pub dto_fields: RwLock<HashMap<EntityId, DtoField>>,
pub globals: RwLock<HashMap<EntityId, Global>>,
pub relationships: RwLock<HashMap<EntityId, Relationship>>,
pub user_interfaces: RwLock<HashMap<EntityId, UserInterface>>,
pub jn_system_from_root_system: RwLock<HashMapJunction>,
pub jn_workspace_from_root_workspace: RwLock<HashMapJunction>,
pub jn_entity_from_workspace_entities: RwLock<HashMapJunction>,
pub jn_feature_from_workspace_features: RwLock<HashMapJunction>,
pub jn_global_from_workspace_global: RwLock<HashMapJunction>,
pub jn_user_interface_from_workspace_user_interface: RwLock<HashMapJunction>,
pub jn_file_from_system_files: RwLock<HashMapJunction>,
pub jn_field_from_entity_fields: RwLock<HashMapJunction>,
pub jn_entity_from_entity_inherits_from: RwLock<HashMapJunction>,
pub jn_relationship_from_entity_relationships: RwLock<HashMapJunction>,
pub jn_entity_from_field_entity: RwLock<HashMapJunction>,
pub jn_use_case_from_feature_use_cases: RwLock<HashMapJunction>,
pub jn_entity_from_file_entity: RwLock<HashMapJunction>,
pub jn_feature_from_file_feature: RwLock<HashMapJunction>,
pub jn_field_from_file_field: RwLock<HashMapJunction>,
pub jn_use_case_from_file_use_case: RwLock<HashMapJunction>,
pub jn_dto_from_use_case_dto_in: RwLock<HashMapJunction>,
pub jn_dto_from_use_case_dto_out: RwLock<HashMapJunction>,
pub jn_entity_from_use_case_entities: RwLock<HashMapJunction>,
pub jn_dto_field_from_dto_fields: RwLock<HashMapJunction>,
pub jn_entity_from_relationship_left_entity: RwLock<HashMapJunction>,
pub jn_entity_from_relationship_right_entity: RwLock<HashMapJunction>,
pub counters: RwLock<std::collections::HashMap<String, EntityId>>,
savepoints: RwLock<std::collections::HashMap<u64, HashMapStoreSnapshot>>,
next_savepoint_id: RwLock<u64>,
}
impl Default for HashMapStore {
fn default() -> Self {
Self {
roots: RwLock::default(),
workspaces: RwLock::default(),
systems: RwLock::default(),
entities: RwLock::default(),
fields: RwLock::default(),
features: RwLock::default(),
files: RwLock::default(),
use_cases: RwLock::default(),
dtos: RwLock::default(),
dto_fields: RwLock::default(),
globals: RwLock::default(),
relationships: RwLock::default(),
user_interfaces: RwLock::default(),
jn_system_from_root_system: RwLock::new(HashMapJunction::exclusive()),
jn_workspace_from_root_workspace: RwLock::new(HashMapJunction::exclusive()),
jn_entity_from_workspace_entities: RwLock::new(HashMapJunction::exclusive()),
jn_feature_from_workspace_features: RwLock::new(HashMapJunction::exclusive()),
jn_global_from_workspace_global: RwLock::new(HashMapJunction::exclusive()),
jn_user_interface_from_workspace_user_interface: RwLock::new(
HashMapJunction::exclusive(),
),
jn_file_from_system_files: RwLock::new(HashMapJunction::exclusive()),
jn_field_from_entity_fields: RwLock::new(HashMapJunction::exclusive()),
jn_entity_from_entity_inherits_from: RwLock::new(HashMapJunction::shared()),
jn_relationship_from_entity_relationships: RwLock::new(HashMapJunction::exclusive()),
jn_entity_from_field_entity: RwLock::new(HashMapJunction::shared()),
jn_use_case_from_feature_use_cases: RwLock::new(HashMapJunction::exclusive()),
jn_entity_from_file_entity: RwLock::new(HashMapJunction::shared()),
jn_feature_from_file_feature: RwLock::new(HashMapJunction::shared()),
jn_field_from_file_field: RwLock::new(HashMapJunction::shared()),
jn_use_case_from_file_use_case: RwLock::new(HashMapJunction::shared()),
jn_dto_from_use_case_dto_in: RwLock::new(HashMapJunction::exclusive()),
jn_dto_from_use_case_dto_out: RwLock::new(HashMapJunction::exclusive()),
jn_entity_from_use_case_entities: RwLock::new(HashMapJunction::shared()),
jn_dto_field_from_dto_fields: RwLock::new(HashMapJunction::exclusive()),
jn_entity_from_relationship_left_entity: RwLock::new(HashMapJunction::shared()),
jn_entity_from_relationship_right_entity: RwLock::new(HashMapJunction::shared()),
counters: RwLock::default(),
savepoints: RwLock::default(),
next_savepoint_id: RwLock::default(),
}
}
}
impl HashMapStore {
pub fn new() -> Self {
Self::default()
}
pub fn snapshot(&self) -> HashMapStoreSnapshot {
HashMapStoreSnapshot {
roots: read_or_recover(&self.roots).clone(),
workspaces: read_or_recover(&self.workspaces).clone(),
systems: read_or_recover(&self.systems).clone(),
entities: read_or_recover(&self.entities).clone(),
fields: read_or_recover(&self.fields).clone(),
features: read_or_recover(&self.features).clone(),
files: read_or_recover(&self.files).clone(),
use_cases: read_or_recover(&self.use_cases).clone(),
dtos: read_or_recover(&self.dtos).clone(),
dto_fields: read_or_recover(&self.dto_fields).clone(),
globals: read_or_recover(&self.globals).clone(),
relationships: read_or_recover(&self.relationships).clone(),
user_interfaces: read_or_recover(&self.user_interfaces).clone(),
jn_system_from_root_system: read_or_recover(&self.jn_system_from_root_system).clone(),
jn_workspace_from_root_workspace: read_or_recover(
&self.jn_workspace_from_root_workspace,
)
.clone(),
jn_entity_from_workspace_entities: read_or_recover(
&self.jn_entity_from_workspace_entities,
)
.clone(),
jn_feature_from_workspace_features: read_or_recover(
&self.jn_feature_from_workspace_features,
)
.clone(),
jn_global_from_workspace_global: read_or_recover(&self.jn_global_from_workspace_global)
.clone(),
jn_user_interface_from_workspace_user_interface: read_or_recover(
&self.jn_user_interface_from_workspace_user_interface,
)
.clone(),
jn_file_from_system_files: read_or_recover(&self.jn_file_from_system_files).clone(),
jn_field_from_entity_fields: read_or_recover(&self.jn_field_from_entity_fields).clone(),
jn_entity_from_entity_inherits_from: read_or_recover(
&self.jn_entity_from_entity_inherits_from,
)
.clone(),
jn_relationship_from_entity_relationships: read_or_recover(
&self.jn_relationship_from_entity_relationships,
)
.clone(),
jn_entity_from_field_entity: read_or_recover(&self.jn_entity_from_field_entity).clone(),
jn_use_case_from_feature_use_cases: read_or_recover(
&self.jn_use_case_from_feature_use_cases,
)
.clone(),
jn_entity_from_file_entity: read_or_recover(&self.jn_entity_from_file_entity).clone(),
jn_feature_from_file_feature: read_or_recover(&self.jn_feature_from_file_feature)
.clone(),
jn_field_from_file_field: read_or_recover(&self.jn_field_from_file_field).clone(),
jn_use_case_from_file_use_case: read_or_recover(&self.jn_use_case_from_file_use_case)
.clone(),
jn_dto_from_use_case_dto_in: read_or_recover(&self.jn_dto_from_use_case_dto_in).clone(),
jn_dto_from_use_case_dto_out: read_or_recover(&self.jn_dto_from_use_case_dto_out)
.clone(),
jn_entity_from_use_case_entities: read_or_recover(
&self.jn_entity_from_use_case_entities,
)
.clone(),
jn_dto_field_from_dto_fields: read_or_recover(&self.jn_dto_field_from_dto_fields)
.clone(),
jn_entity_from_relationship_left_entity: read_or_recover(
&self.jn_entity_from_relationship_left_entity,
)
.clone(),
jn_entity_from_relationship_right_entity: read_or_recover(
&self.jn_entity_from_relationship_right_entity,
)
.clone(),
counters: read_or_recover(&self.counters).clone(),
}
}
pub fn freeze(&self) -> HashMapStore {
let g_roots = read_or_recover(&self.roots);
let g_workspaces = read_or_recover(&self.workspaces);
let g_systems = read_or_recover(&self.systems);
let g_entities = read_or_recover(&self.entities);
let g_fields = read_or_recover(&self.fields);
let g_features = read_or_recover(&self.features);
let g_files = read_or_recover(&self.files);
let g_use_cases = read_or_recover(&self.use_cases);
let g_dtos = read_or_recover(&self.dtos);
let g_dto_fields = read_or_recover(&self.dto_fields);
let g_globals = read_or_recover(&self.globals);
let g_relationships = read_or_recover(&self.relationships);
let g_user_interfaces = read_or_recover(&self.user_interfaces);
let g_jn_system_from_root_system = read_or_recover(&self.jn_system_from_root_system);
let g_jn_workspace_from_root_workspace =
read_or_recover(&self.jn_workspace_from_root_workspace);
let g_jn_entity_from_workspace_entities =
read_or_recover(&self.jn_entity_from_workspace_entities);
let g_jn_feature_from_workspace_features =
read_or_recover(&self.jn_feature_from_workspace_features);
let g_jn_global_from_workspace_global =
read_or_recover(&self.jn_global_from_workspace_global);
let g_jn_user_interface_from_workspace_user_interface =
read_or_recover(&self.jn_user_interface_from_workspace_user_interface);
let g_jn_file_from_system_files = read_or_recover(&self.jn_file_from_system_files);
let g_jn_field_from_entity_fields = read_or_recover(&self.jn_field_from_entity_fields);
let g_jn_entity_from_entity_inherits_from =
read_or_recover(&self.jn_entity_from_entity_inherits_from);
let g_jn_relationship_from_entity_relationships =
read_or_recover(&self.jn_relationship_from_entity_relationships);
let g_jn_entity_from_field_entity = read_or_recover(&self.jn_entity_from_field_entity);
let g_jn_use_case_from_feature_use_cases =
read_or_recover(&self.jn_use_case_from_feature_use_cases);
let g_jn_entity_from_file_entity = read_or_recover(&self.jn_entity_from_file_entity);
let g_jn_feature_from_file_feature = read_or_recover(&self.jn_feature_from_file_feature);
let g_jn_field_from_file_field = read_or_recover(&self.jn_field_from_file_field);
let g_jn_use_case_from_file_use_case =
read_or_recover(&self.jn_use_case_from_file_use_case);
let g_jn_dto_from_use_case_dto_in = read_or_recover(&self.jn_dto_from_use_case_dto_in);
let g_jn_dto_from_use_case_dto_out = read_or_recover(&self.jn_dto_from_use_case_dto_out);
let g_jn_entity_from_use_case_entities =
read_or_recover(&self.jn_entity_from_use_case_entities);
let g_jn_dto_field_from_dto_fields = read_or_recover(&self.jn_dto_field_from_dto_fields);
let g_jn_entity_from_relationship_left_entity =
read_or_recover(&self.jn_entity_from_relationship_left_entity);
let g_jn_entity_from_relationship_right_entity =
read_or_recover(&self.jn_entity_from_relationship_right_entity);
let g_counters = read_or_recover(&self.counters);
HashMapStore {
roots: RwLock::new(g_roots.clone()),
workspaces: RwLock::new(g_workspaces.clone()),
systems: RwLock::new(g_systems.clone()),
entities: RwLock::new(g_entities.clone()),
fields: RwLock::new(g_fields.clone()),
features: RwLock::new(g_features.clone()),
files: RwLock::new(g_files.clone()),
use_cases: RwLock::new(g_use_cases.clone()),
dtos: RwLock::new(g_dtos.clone()),
dto_fields: RwLock::new(g_dto_fields.clone()),
globals: RwLock::new(g_globals.clone()),
relationships: RwLock::new(g_relationships.clone()),
user_interfaces: RwLock::new(g_user_interfaces.clone()),
jn_system_from_root_system: RwLock::new(g_jn_system_from_root_system.clone()),
jn_workspace_from_root_workspace: RwLock::new(
g_jn_workspace_from_root_workspace.clone(),
),
jn_entity_from_workspace_entities: RwLock::new(
g_jn_entity_from_workspace_entities.clone(),
),
jn_feature_from_workspace_features: RwLock::new(
g_jn_feature_from_workspace_features.clone(),
),
jn_global_from_workspace_global: RwLock::new(g_jn_global_from_workspace_global.clone()),
jn_user_interface_from_workspace_user_interface: RwLock::new(
g_jn_user_interface_from_workspace_user_interface.clone(),
),
jn_file_from_system_files: RwLock::new(g_jn_file_from_system_files.clone()),
jn_field_from_entity_fields: RwLock::new(g_jn_field_from_entity_fields.clone()),
jn_entity_from_entity_inherits_from: RwLock::new(
g_jn_entity_from_entity_inherits_from.clone(),
),
jn_relationship_from_entity_relationships: RwLock::new(
g_jn_relationship_from_entity_relationships.clone(),
),
jn_entity_from_field_entity: RwLock::new(g_jn_entity_from_field_entity.clone()),
jn_use_case_from_feature_use_cases: RwLock::new(
g_jn_use_case_from_feature_use_cases.clone(),
),
jn_entity_from_file_entity: RwLock::new(g_jn_entity_from_file_entity.clone()),
jn_feature_from_file_feature: RwLock::new(g_jn_feature_from_file_feature.clone()),
jn_field_from_file_field: RwLock::new(g_jn_field_from_file_field.clone()),
jn_use_case_from_file_use_case: RwLock::new(g_jn_use_case_from_file_use_case.clone()),
jn_dto_from_use_case_dto_in: RwLock::new(g_jn_dto_from_use_case_dto_in.clone()),
jn_dto_from_use_case_dto_out: RwLock::new(g_jn_dto_from_use_case_dto_out.clone()),
jn_entity_from_use_case_entities: RwLock::new(
g_jn_entity_from_use_case_entities.clone(),
),
jn_dto_field_from_dto_fields: RwLock::new(g_jn_dto_field_from_dto_fields.clone()),
jn_entity_from_relationship_left_entity: RwLock::new(
g_jn_entity_from_relationship_left_entity.clone(),
),
jn_entity_from_relationship_right_entity: RwLock::new(
g_jn_entity_from_relationship_right_entity.clone(),
),
counters: RwLock::new(g_counters.clone()),
savepoints: RwLock::new(std::collections::HashMap::new()),
next_savepoint_id: RwLock::new(0),
}
}
pub fn restore(&self, snap: &HashMapStoreSnapshot) {
*write_or_recover(&self.roots) = snap.roots.clone();
*write_or_recover(&self.workspaces) = snap.workspaces.clone();
*write_or_recover(&self.systems) = snap.systems.clone();
*write_or_recover(&self.entities) = snap.entities.clone();
*write_or_recover(&self.fields) = snap.fields.clone();
*write_or_recover(&self.features) = snap.features.clone();
*write_or_recover(&self.files) = snap.files.clone();
*write_or_recover(&self.use_cases) = snap.use_cases.clone();
*write_or_recover(&self.dtos) = snap.dtos.clone();
*write_or_recover(&self.dto_fields) = snap.dto_fields.clone();
*write_or_recover(&self.globals) = snap.globals.clone();
*write_or_recover(&self.relationships) = snap.relationships.clone();
*write_or_recover(&self.user_interfaces) = snap.user_interfaces.clone();
*write_or_recover(&self.jn_system_from_root_system) =
snap.jn_system_from_root_system.clone();
*write_or_recover(&self.jn_workspace_from_root_workspace) =
snap.jn_workspace_from_root_workspace.clone();
*write_or_recover(&self.jn_entity_from_workspace_entities) =
snap.jn_entity_from_workspace_entities.clone();
*write_or_recover(&self.jn_feature_from_workspace_features) =
snap.jn_feature_from_workspace_features.clone();
*write_or_recover(&self.jn_global_from_workspace_global) =
snap.jn_global_from_workspace_global.clone();
*write_or_recover(&self.jn_user_interface_from_workspace_user_interface) =
snap.jn_user_interface_from_workspace_user_interface.clone();
*write_or_recover(&self.jn_file_from_system_files) = snap.jn_file_from_system_files.clone();
*write_or_recover(&self.jn_field_from_entity_fields) =
snap.jn_field_from_entity_fields.clone();
*write_or_recover(&self.jn_entity_from_entity_inherits_from) =
snap.jn_entity_from_entity_inherits_from.clone();
*write_or_recover(&self.jn_relationship_from_entity_relationships) =
snap.jn_relationship_from_entity_relationships.clone();
*write_or_recover(&self.jn_entity_from_field_entity) =
snap.jn_entity_from_field_entity.clone();
*write_or_recover(&self.jn_use_case_from_feature_use_cases) =
snap.jn_use_case_from_feature_use_cases.clone();
*write_or_recover(&self.jn_entity_from_file_entity) =
snap.jn_entity_from_file_entity.clone();
*write_or_recover(&self.jn_feature_from_file_feature) =
snap.jn_feature_from_file_feature.clone();
*write_or_recover(&self.jn_field_from_file_field) = snap.jn_field_from_file_field.clone();
*write_or_recover(&self.jn_use_case_from_file_use_case) =
snap.jn_use_case_from_file_use_case.clone();
*write_or_recover(&self.jn_dto_from_use_case_dto_in) =
snap.jn_dto_from_use_case_dto_in.clone();
*write_or_recover(&self.jn_dto_from_use_case_dto_out) =
snap.jn_dto_from_use_case_dto_out.clone();
*write_or_recover(&self.jn_entity_from_use_case_entities) =
snap.jn_entity_from_use_case_entities.clone();
*write_or_recover(&self.jn_dto_field_from_dto_fields) =
snap.jn_dto_field_from_dto_fields.clone();
*write_or_recover(&self.jn_entity_from_relationship_left_entity) =
snap.jn_entity_from_relationship_left_entity.clone();
*write_or_recover(&self.jn_entity_from_relationship_right_entity) =
snap.jn_entity_from_relationship_right_entity.clone();
*write_or_recover(&self.counters) = snap.counters.clone();
}
pub fn create_savepoint(&self) -> u64 {
let snap = self.snapshot();
let mut id_counter = write_or_recover(&self.next_savepoint_id);
let id = *id_counter;
*id_counter += 1;
write_or_recover(&self.savepoints).insert(id, snap);
id
}
pub fn restore_savepoint(&self, savepoint_id: u64) {
let Some(snap) = read_or_recover(&self.savepoints)
.get(&savepoint_id)
.cloned()
else {
debug_assert!(
false,
"restore_savepoint: savepoint {savepoint_id} does not exist — it was already \
restored, discarded, or never created"
);
return;
};
self.restore(&snap);
}
pub fn discard_savepoint(&self, savepoint_id: u64) {
write_or_recover(&self.savepoints).remove(&savepoint_id);
}
pub(crate) fn next_id(&self, entity_name: &str) -> EntityId {
let mut counters = write_or_recover(&self.counters);
let counter = counters.entry(entity_name.to_string()).or_insert(1);
let id = *counter;
*counter += 1;
id
}
pub fn restore_without_counters(&self, snap: &HashMapStoreSnapshot) {
*write_or_recover(&self.roots) = snap.roots.clone();
*write_or_recover(&self.workspaces) = snap.workspaces.clone();
*write_or_recover(&self.systems) = snap.systems.clone();
*write_or_recover(&self.entities) = snap.entities.clone();
*write_or_recover(&self.fields) = snap.fields.clone();
*write_or_recover(&self.features) = snap.features.clone();
*write_or_recover(&self.files) = snap.files.clone();
*write_or_recover(&self.use_cases) = snap.use_cases.clone();
*write_or_recover(&self.dtos) = snap.dtos.clone();
*write_or_recover(&self.dto_fields) = snap.dto_fields.clone();
*write_or_recover(&self.globals) = snap.globals.clone();
*write_or_recover(&self.relationships) = snap.relationships.clone();
*write_or_recover(&self.user_interfaces) = snap.user_interfaces.clone();
*write_or_recover(&self.jn_system_from_root_system) =
snap.jn_system_from_root_system.clone();
*write_or_recover(&self.jn_workspace_from_root_workspace) =
snap.jn_workspace_from_root_workspace.clone();
*write_or_recover(&self.jn_entity_from_workspace_entities) =
snap.jn_entity_from_workspace_entities.clone();
*write_or_recover(&self.jn_feature_from_workspace_features) =
snap.jn_feature_from_workspace_features.clone();
*write_or_recover(&self.jn_global_from_workspace_global) =
snap.jn_global_from_workspace_global.clone();
*write_or_recover(&self.jn_user_interface_from_workspace_user_interface) =
snap.jn_user_interface_from_workspace_user_interface.clone();
*write_or_recover(&self.jn_file_from_system_files) = snap.jn_file_from_system_files.clone();
*write_or_recover(&self.jn_field_from_entity_fields) =
snap.jn_field_from_entity_fields.clone();
*write_or_recover(&self.jn_entity_from_entity_inherits_from) =
snap.jn_entity_from_entity_inherits_from.clone();
*write_or_recover(&self.jn_relationship_from_entity_relationships) =
snap.jn_relationship_from_entity_relationships.clone();
*write_or_recover(&self.jn_entity_from_field_entity) =
snap.jn_entity_from_field_entity.clone();
*write_or_recover(&self.jn_use_case_from_feature_use_cases) =
snap.jn_use_case_from_feature_use_cases.clone();
*write_or_recover(&self.jn_entity_from_file_entity) =
snap.jn_entity_from_file_entity.clone();
*write_or_recover(&self.jn_feature_from_file_feature) =
snap.jn_feature_from_file_feature.clone();
*write_or_recover(&self.jn_field_from_file_field) = snap.jn_field_from_file_field.clone();
*write_or_recover(&self.jn_use_case_from_file_use_case) =
snap.jn_use_case_from_file_use_case.clone();
*write_or_recover(&self.jn_dto_from_use_case_dto_in) =
snap.jn_dto_from_use_case_dto_in.clone();
*write_or_recover(&self.jn_dto_from_use_case_dto_out) =
snap.jn_dto_from_use_case_dto_out.clone();
*write_or_recover(&self.jn_entity_from_use_case_entities) =
snap.jn_entity_from_use_case_entities.clone();
*write_or_recover(&self.jn_dto_field_from_dto_fields) =
snap.jn_dto_field_from_dto_fields.clone();
*write_or_recover(&self.jn_entity_from_relationship_left_entity) =
snap.jn_entity_from_relationship_left_entity.clone();
*write_or_recover(&self.jn_entity_from_relationship_right_entity) =
snap.jn_entity_from_relationship_right_entity.clone();
}
pub fn store_snapshot(&self) -> StoreSnapshot {
StoreSnapshot::new(self.snapshot())
}
pub fn restore_store_snapshot(&self, snap: &StoreSnapshot) {
let s = snap
.downcast_ref::<HashMapStoreSnapshot>()
.expect("StoreSnapshot must contain HashMapStoreSnapshot");
self.restore_without_counters(s);
}
pub fn check_junction_indexes(&self) -> Result<(), String> {
{
let jn = read_or_recover(&self.jn_system_from_root_system);
if !jn.is_exclusive() {
return Err(
"jn_system_from_root_system: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_system_from_root_system: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_workspace_from_root_workspace);
if !jn.is_exclusive() {
return Err(
"jn_workspace_from_root_workspace: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_workspace_from_root_workspace: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_entity_from_workspace_entities);
if !jn.is_exclusive() {
return Err("jn_entity_from_workspace_entities: exclusivity does not match the relationship".into());
}
jn.check_index()
.map_err(|e| format!("jn_entity_from_workspace_entities: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_feature_from_workspace_features);
if !jn.is_exclusive() {
return Err("jn_feature_from_workspace_features: exclusivity does not match the relationship".into());
}
jn.check_index()
.map_err(|e| format!("jn_feature_from_workspace_features: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_global_from_workspace_global);
if !jn.is_exclusive() {
return Err(
"jn_global_from_workspace_global: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_global_from_workspace_global: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_user_interface_from_workspace_user_interface);
if !jn.is_exclusive() {
return Err("jn_user_interface_from_workspace_user_interface: exclusivity does not match the relationship".into());
}
jn.check_index()
.map_err(|e| format!("jn_user_interface_from_workspace_user_interface: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_file_from_system_files);
if !jn.is_exclusive() {
return Err(
"jn_file_from_system_files: exclusivity does not match the relationship".into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_file_from_system_files: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_field_from_entity_fields);
if !jn.is_exclusive() {
return Err(
"jn_field_from_entity_fields: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_field_from_entity_fields: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_entity_from_entity_inherits_from);
if jn.is_exclusive() {
return Err("jn_entity_from_entity_inherits_from: exclusivity does not match the relationship".into());
}
jn.check_index()
.map_err(|e| format!("jn_entity_from_entity_inherits_from: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_relationship_from_entity_relationships);
if !jn.is_exclusive() {
return Err("jn_relationship_from_entity_relationships: exclusivity does not match the relationship".into());
}
jn.check_index()
.map_err(|e| format!("jn_relationship_from_entity_relationships: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_entity_from_field_entity);
if jn.is_exclusive() {
return Err(
"jn_entity_from_field_entity: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_entity_from_field_entity: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_use_case_from_feature_use_cases);
if !jn.is_exclusive() {
return Err("jn_use_case_from_feature_use_cases: exclusivity does not match the relationship".into());
}
jn.check_index()
.map_err(|e| format!("jn_use_case_from_feature_use_cases: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_entity_from_file_entity);
if jn.is_exclusive() {
return Err(
"jn_entity_from_file_entity: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_entity_from_file_entity: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_feature_from_file_feature);
if jn.is_exclusive() {
return Err(
"jn_feature_from_file_feature: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_feature_from_file_feature: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_field_from_file_field);
if jn.is_exclusive() {
return Err(
"jn_field_from_file_field: exclusivity does not match the relationship".into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_field_from_file_field: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_use_case_from_file_use_case);
if jn.is_exclusive() {
return Err(
"jn_use_case_from_file_use_case: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_use_case_from_file_use_case: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_dto_from_use_case_dto_in);
if !jn.is_exclusive() {
return Err(
"jn_dto_from_use_case_dto_in: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_dto_from_use_case_dto_in: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_dto_from_use_case_dto_out);
if !jn.is_exclusive() {
return Err(
"jn_dto_from_use_case_dto_out: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_dto_from_use_case_dto_out: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_entity_from_use_case_entities);
if jn.is_exclusive() {
return Err(
"jn_entity_from_use_case_entities: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_entity_from_use_case_entities: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_dto_field_from_dto_fields);
if !jn.is_exclusive() {
return Err(
"jn_dto_field_from_dto_fields: exclusivity does not match the relationship"
.into(),
);
}
jn.check_index()
.map_err(|e| format!("jn_dto_field_from_dto_fields: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_entity_from_relationship_left_entity);
if jn.is_exclusive() {
return Err("jn_entity_from_relationship_left_entity: exclusivity does not match the relationship".into());
}
jn.check_index()
.map_err(|e| format!("jn_entity_from_relationship_left_entity: {e}"))?;
}
{
let jn = read_or_recover(&self.jn_entity_from_relationship_right_entity);
if jn.is_exclusive() {
return Err("jn_entity_from_relationship_right_entity: exclusivity does not match the relationship".into());
}
jn.check_index()
.map_err(|e| format!("jn_entity_from_relationship_right_entity: {e}"))?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct HashMapStoreSnapshot {
pub(crate) roots: HashMap<EntityId, Root>,
pub(crate) workspaces: HashMap<EntityId, Workspace>,
pub(crate) systems: HashMap<EntityId, System>,
pub(crate) entities: HashMap<EntityId, Entity>,
pub(crate) fields: HashMap<EntityId, Field>,
pub(crate) features: HashMap<EntityId, Feature>,
pub(crate) files: HashMap<EntityId, File>,
pub(crate) use_cases: HashMap<EntityId, UseCase>,
pub(crate) dtos: HashMap<EntityId, Dto>,
pub(crate) dto_fields: HashMap<EntityId, DtoField>,
pub(crate) globals: HashMap<EntityId, Global>,
pub(crate) relationships: HashMap<EntityId, Relationship>,
pub(crate) user_interfaces: HashMap<EntityId, UserInterface>,
pub(crate) jn_system_from_root_system: HashMapJunction,
pub(crate) jn_workspace_from_root_workspace: HashMapJunction,
pub(crate) jn_entity_from_workspace_entities: HashMapJunction,
pub(crate) jn_feature_from_workspace_features: HashMapJunction,
pub(crate) jn_global_from_workspace_global: HashMapJunction,
pub(crate) jn_user_interface_from_workspace_user_interface: HashMapJunction,
pub(crate) jn_file_from_system_files: HashMapJunction,
pub(crate) jn_field_from_entity_fields: HashMapJunction,
pub(crate) jn_entity_from_entity_inherits_from: HashMapJunction,
pub(crate) jn_relationship_from_entity_relationships: HashMapJunction,
pub(crate) jn_entity_from_field_entity: HashMapJunction,
pub(crate) jn_use_case_from_feature_use_cases: HashMapJunction,
pub(crate) jn_entity_from_file_entity: HashMapJunction,
pub(crate) jn_feature_from_file_feature: HashMapJunction,
pub(crate) jn_field_from_file_field: HashMapJunction,
pub(crate) jn_use_case_from_file_use_case: HashMapJunction,
pub(crate) jn_dto_from_use_case_dto_in: HashMapJunction,
pub(crate) jn_dto_from_use_case_dto_out: HashMapJunction,
pub(crate) jn_entity_from_use_case_entities: HashMapJunction,
pub(crate) jn_dto_field_from_dto_fields: HashMapJunction,
pub(crate) jn_entity_from_relationship_left_entity: HashMapJunction,
pub(crate) jn_entity_from_relationship_right_entity: HashMapJunction,
counters: std::collections::HashMap<String, EntityId>,
}
impl StoreSnapshotTrait for HashMapStoreSnapshot {
fn clone_box(&self) -> Box<dyn StoreSnapshotTrait> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct HashMapJunction {
rows: HashMap<EntityId, Vec<EntityId>>,
owners: Option<HashMap<EntityId, Vec<EntityId>>>,
}
impl HashMapJunction {
pub fn exclusive() -> Self {
Self {
rows: HashMap::new(),
owners: Some(HashMap::new()),
}
}
pub fn shared() -> Self {
Self::default()
}
pub fn is_exclusive(&self) -> bool {
self.owners.is_some()
}
pub fn get(&self, source: &EntityId) -> Option<&Vec<EntityId>> {
self.rows.get(source)
}
pub fn contains_key(&self, source: &EntityId) -> bool {
self.rows.contains_key(source)
}
pub fn iter(&self) -> impl Iterator<Item = (&EntityId, &Vec<EntityId>)> {
self.rows.iter()
}
pub fn keys(&self) -> impl Iterator<Item = &EntityId> {
self.rows.keys()
}
pub fn values(&self) -> impl Iterator<Item = &Vec<EntityId>> {
self.rows.values()
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn sources_of(&self, target: &EntityId) -> Vec<EntityId> {
match &self.owners {
Some(owners) => owners.get(target).cloned().unwrap_or_default(),
None => self
.rows
.iter()
.flat_map(|(source, targets)| {
targets
.iter()
.filter(|t| *t == target)
.map(move |_| *source)
})
.collect(),
}
}
pub fn insert(&mut self, source: EntityId, targets: Vec<EntityId>) -> Option<Vec<EntityId>> {
if let Some(owners) = self.owners.as_mut() {
let previous = self
.rows
.get(&source)
.map(Vec::as_slice)
.unwrap_or_default();
reindex(owners, source, previous, &targets);
}
self.rows.insert(source, targets)
}
pub fn remove(&mut self, source: &EntityId) -> Option<Vec<EntityId>> {
let removed = self.rows.remove(source)?;
if let Some(owners) = self.owners.as_mut() {
reindex(owners, *source, &removed, &[]);
}
Some(removed)
}
pub fn remove_target(&mut self, target: &EntityId) {
let sources: Vec<EntityId> = match self.owners.as_mut() {
Some(owners) => {
let mut sources = owners.remove(target).unwrap_or_default();
sources.sort_unstable();
sources.dedup();
sources
}
None => self
.rows
.iter()
.filter(|(_, targets)| targets.contains(target))
.map(|(source, _)| *source)
.collect(),
};
for source in sources {
if let Some(targets) = self.rows.get_mut(&source) {
targets.retain(|t| t != target);
}
}
}
pub fn check_index(&self) -> Result<(), String> {
let Some(owners) = &self.owners else {
return Ok(());
};
let mut expected: std::collections::HashMap<EntityId, Vec<EntityId>> =
std::collections::HashMap::new();
for (source, targets) in self.rows.iter() {
for target in targets {
expected.entry(*target).or_default().push(*source);
}
}
if expected.len() != owners.len() {
return Err(format!(
"the reverse index lists {} targets but the rows hold {}",
owners.len(),
expected.len()
));
}
for (target, mut holders) in expected {
let mut indexed = owners.get(&target).cloned().unwrap_or_default();
holders.sort_unstable();
indexed.sort_unstable();
if holders != indexed {
return Err(format!(
"target {target} is held by {holders:?} but indexed under {indexed:?}"
));
}
}
Ok(())
}
}
fn reindex(
owners: &mut HashMap<EntityId, Vec<EntityId>>,
source: EntityId,
previous: &[EntityId],
next: &[EntityId],
) {
if previous.is_empty() {
for target in next {
link(owners, source, *target, 1);
}
return;
}
if next.is_empty() {
for target in previous {
unlink(owners, source, *target, 1);
}
return;
}
if previous == next {
return;
}
let mut delta: std::collections::HashMap<EntityId, isize> =
std::collections::HashMap::with_capacity(previous.len() + next.len());
for target in previous {
*delta.entry(*target).or_default() -= 1;
}
for target in next {
*delta.entry(*target).or_default() += 1;
}
for (target, change) in delta {
if change > 0 {
link(owners, source, target, change.unsigned_abs());
} else if change < 0 {
unlink(owners, source, target, change.unsigned_abs());
}
}
}
fn link(
owners: &mut HashMap<EntityId, Vec<EntityId>>,
source: EntityId,
target: EntityId,
count: usize,
) {
owners
.entry(target)
.or_default()
.extend(std::iter::repeat_n(source, count));
}
fn unlink(
owners: &mut HashMap<EntityId, Vec<EntityId>>,
source: EntityId,
target: EntityId,
count: usize,
) {
let emptied = match owners.get_mut(&target) {
Some(holders) => {
for _ in 0..count {
if let Some(at) = holders.iter().position(|h| *h == source) {
holders.swap_remove(at);
}
}
holders.is_empty()
}
None => false,
};
if emptied {
owners.remove(&target);
}
}
pub(crate) fn read_or_recover<T>(lock: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
match lock.read() {
Ok(guard) => guard,
Err(poisoned) => {
let guard = poisoned.into_inner();
lock.clear_poison();
guard
}
}
}
pub(crate) fn write_or_recover<T>(lock: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
match lock.write() {
Ok(guard) => guard,
Err(poisoned) => {
let guard = poisoned.into_inner();
lock.clear_poison();
guard
}
}
}
pub(crate) fn delete_from_backward_junction(junction: &RwLock<HashMapJunction>, id: &EntityId) {
write_or_recover(junction).remove_target(id);
}
pub(crate) fn junction_get(junction: &RwLock<HashMapJunction>, id: &EntityId) -> Vec<EntityId> {
read_or_recover(junction)
.get(id)
.cloned()
.unwrap_or_default()
}
pub(crate) fn junction_set(junction: &RwLock<HashMapJunction>, id: EntityId, ids: Vec<EntityId>) {
write_or_recover(junction).insert(id, ids);
}
pub(crate) fn validate_junction_updates(
junction: &RwLock<HashMapJunction>,
replacements: &[(EntityId, Vec<EntityId>)],
scalar: bool,
) -> Result<(), crate::error::RepositoryError> {
use crate::error::RepositoryError::ConstraintViolation;
use std::collections::HashSet;
let mut owners = HashSet::new();
for (owner, targets) in replacements {
if !owners.insert(*owner) {
return Err(ConstraintViolation(
"duplicate source in relationship batch".into(),
));
}
if scalar && targets.len() > 1 {
return Err(ConstraintViolation(
"to-one relationship accepts at most one target".into(),
));
}
let mut unique = HashSet::new();
if targets.iter().any(|id| !unique.insert(*id)) {
return Err(ConstraintViolation("duplicate relationship target".into()));
}
}
if replacements.iter().all(|(_, targets)| targets.is_empty()) {
return Ok(());
}
let current = read_or_recover(junction);
let Some(index) = current.owners.as_ref() else {
return Ok(());
};
let mut claimed = HashSet::new();
for (_, targets) in replacements {
for id in targets {
let held_outside = index
.get(id)
.is_some_and(|holders| holders.iter().any(|holder| !owners.contains(holder)));
if held_outside || !claimed.insert(*id) {
return Err(ConstraintViolation(
"relationship target already belongs to another source".into(),
));
}
}
}
Ok(())
}
pub(crate) fn junction_remove(junction: &RwLock<HashMapJunction>, id: &EntityId) {
write_or_recover(junction).remove(id);
}
pub(crate) fn junction_get_relationships_from_right_ids(
junction: &RwLock<HashMapJunction>,
right_ids: &[EntityId],
) -> Vec<(EntityId, Vec<EntityId>)> {
let jn = read_or_recover(junction);
jn.iter()
.filter(|(_, rids)| right_ids.iter().any(|eid| rids.contains(eid)))
.map(|(left_id, rids)| (*left_id, rids.clone()))
.collect()
}
pub(crate) fn junction_move_ids(
junction: &RwLock<HashMapJunction>,
id: &EntityId,
ids_to_move: &[EntityId],
new_index: i32,
) -> Vec<EntityId> {
let current = junction_get(junction, id);
if ids_to_move.is_empty() {
return current;
}
let move_set: std::collections::HashSet<EntityId> = ids_to_move.iter().copied().collect();
let mut remaining: Vec<EntityId> = current
.into_iter()
.filter(|eid| !move_set.contains(eid))
.collect();
let insert_pos = if new_index < 0 || (new_index as usize) > remaining.len() {
remaining.len()
} else {
new_index as usize
};
for (i, &eid) in ids_to_move.iter().enumerate() {
remaining.insert(insert_pos + i, eid);
}
junction_set(junction, *id, remaining.clone());
remaining
}
pub(crate) fn reconcile_backref_list(
live_list: &[EntityId],
snap_list: &[EntityId],
scope: &std::collections::HashSet<EntityId>,
) -> Vec<EntityId> {
let snap_members: Vec<EntityId> = snap_list
.iter()
.copied()
.filter(|x| scope.contains(x))
.collect();
let snap_member_set: std::collections::HashSet<EntityId> =
snap_members.iter().copied().collect();
let mut work: Vec<EntityId> = live_list
.iter()
.copied()
.filter(|x| !scope.contains(x) || snap_member_set.contains(x))
.collect();
for id in &snap_members {
if work.contains(id) {
continue;
}
let snap_idx = snap_list.iter().position(|x| x == id).unwrap_or(0);
let pos = snap_list[..snap_idx]
.iter()
.filter(|x| work.contains(x))
.count()
.min(work.len());
work.insert(pos, *id);
}
work
}
#[macro_export]
macro_rules! impl_relationship_methods {
($table_type:ty, $field_enum:ty) => {
fn get_relationship(
&self,
id: &$crate::types::EntityId,
field: &$field_enum,
) -> Result<Vec<$crate::types::EntityId>, $crate::error::RepositoryError> {
Ok($crate::database::hashmap_store::junction_get(
self.resolve_junction(field),
id,
))
}
fn get_relationship_many(
&self,
ids: &[$crate::types::EntityId],
field: &$field_enum,
) -> Result<
std::collections::HashMap<$crate::types::EntityId, Vec<$crate::types::EntityId>>,
$crate::error::RepositoryError,
> {
let jn = self.resolve_junction(field);
let mut map = std::collections::HashMap::new();
for id in ids {
map.insert(*id, $crate::database::hashmap_store::junction_get(jn, id));
}
Ok(map)
}
fn get_relationship_count(
&self,
id: &$crate::types::EntityId,
field: &$field_enum,
) -> Result<usize, $crate::error::RepositoryError> {
Ok(
$crate::database::hashmap_store::junction_get(self.resolve_junction(field), id)
.len(),
)
}
fn get_relationship_in_range(
&self,
id: &$crate::types::EntityId,
field: &$field_enum,
offset: usize,
limit: usize,
) -> Result<Vec<$crate::types::EntityId>, $crate::error::RepositoryError> {
let all =
$crate::database::hashmap_store::junction_get(self.resolve_junction(field), id);
Ok(all.into_iter().skip(offset).take(limit).collect())
}
fn get_relationships_from_right_ids(
&self,
field: &$field_enum,
right_ids: &[$crate::types::EntityId],
) -> Result<
Vec<($crate::types::EntityId, Vec<$crate::types::EntityId>)>,
$crate::error::RepositoryError,
> {
Ok(
$crate::database::hashmap_store::junction_get_relationships_from_right_ids(
self.resolve_junction(field),
right_ids,
),
)
}
};
}
#[macro_export]
macro_rules! impl_write_relationship_methods {
($table_type:ty, $field_enum:ty) => {
$crate::impl_relationship_methods!($table_type, $field_enum);
fn set_relationship_multi(
&mut self,
field: &$field_enum,
relationships: Vec<($crate::types::EntityId, Vec<$crate::types::EntityId>)>,
) -> Result<(), $crate::error::RepositoryError> {
for (id, _) in &relationships {
if !self.relationship_source_exists(id) {
return Err($crate::error::RepositoryError::ConstraintViolation(
"relationship source does not exist".into(),
));
}
}
self.validate_relationship_updates(field, &relationships, &[])?;
let jn = self.resolve_junction(field);
for (left_id, entities) in relationships {
$crate::database::hashmap_store::junction_set(jn, left_id, entities);
}
Ok(())
}
fn set_relationship(
&mut self,
id: &$crate::types::EntityId,
field: &$field_enum,
right_ids: &[$crate::types::EntityId],
) -> Result<(), $crate::error::RepositoryError> {
self.set_relationship_multi(field, vec![(*id, right_ids.to_vec())])
}
fn move_relationship_ids(
&mut self,
id: &$crate::types::EntityId,
field: &$field_enum,
ids_to_move: &[$crate::types::EntityId],
new_index: i32,
) -> Result<Vec<$crate::types::EntityId>, $crate::error::RepositoryError> {
if !self.relationship_is_ordered(field) {
return Err($crate::error::RepositoryError::ConstraintViolation(
"only ordered relationships can be moved".into(),
));
}
let current =
$crate::database::hashmap_store::junction_get(self.resolve_junction(field), id);
let mut seen = std::collections::HashSet::new();
if ids_to_move
.iter()
.any(|target| !seen.insert(*target) || !current.contains(target))
{
return Err($crate::error::RepositoryError::ConstraintViolation(
"move requires distinct existing relationship members".into(),
));
}
self.validate_relationship_updates(field, &[(*id, current)], &[])?;
if !self.relationship_source_exists(id) {
return Err($crate::error::RepositoryError::ConstraintViolation(
"relationship source does not exist".into(),
));
}
Ok($crate::database::hashmap_store::junction_move_ids(
self.resolve_junction(field),
id,
ids_to_move,
new_index,
))
}
};
}
#[macro_export]
macro_rules! impl_leaf_entity_table {
(
entity: $Entity:ident,
entity_name: $entity_name:expr,
store_field: $store_field:ident,
table_trait: $TableTrait:ident,
table_ro_trait: $TableROTrait:ident,
table_struct: $TableStruct:ident,
table_ro_struct: $TableROStruct:ident,
backward_junctions: [ $( ($bj_field:ident) ),* $(,)? ],
) => {
pub struct $TableStruct<'a> {
store: &'a $crate::database::hashmap_store::HashMapStore,
}
impl<'a> $TableStruct<'a> {
pub fn new(store: &'a $crate::database::hashmap_store::HashMapStore) -> Self {
Self { store }
}
}
impl<'a> $TableTrait for $TableStruct<'a> {
fn create(&mut self, entity: &$Entity) -> Result<$Entity, $crate::error::RepositoryError> {
self.create_multi(std::slice::from_ref(entity))
.and_then(|v| {
v.into_iter().next().ok_or_else(|| {
$crate::error::RepositoryError::Other(::anyhow::anyhow!(
"create: the store returned no row for the entity it was given"
))
})
})
}
fn create_multi(&mut self, entities: &[$Entity]) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
let mut created = Vec::with_capacity(entities.len());
let mut map = $crate::database::hashmap_store::write_or_recover(&self.store.$store_field);
for entity in entities {
let new_entity = if entity.id == $crate::types::EntityId::default() {
let id = self.store.next_id($entity_name);
$Entity {
id,
..entity.clone()
}
} else {
if map.contains_key(&entity.id) {
return Err($crate::error::RepositoryError::DuplicateId {
entity: stringify!($Entity),
id: entity.id,
});
}
entity.clone()
};
map.insert(new_entity.id, new_entity.clone());
created.push(new_entity);
}
Ok(created)
}
fn get(&self, id: &$crate::types::EntityId) -> Result<Option<$Entity>, $crate::error::RepositoryError> {
Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).get(id).cloned())
}
fn get_multi(&self, ids: &[$crate::types::EntityId]) -> Result<Vec<Option<$Entity>>, $crate::error::RepositoryError> {
let map = $crate::database::hashmap_store::read_or_recover(&self.store.$store_field);
Ok(ids.iter().map(|id| map.get(id).cloned()).collect())
}
fn get_all(&self) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).values().cloned().collect())
}
fn update(&mut self, entity: &$Entity) -> Result<$Entity, $crate::error::RepositoryError> {
self.update_multi(std::slice::from_ref(entity))
.and_then(|v| {
v.into_iter().next().ok_or_else(|| {
$crate::error::RepositoryError::Other(::anyhow::anyhow!(
"update: the store returned no row for the entity it was given"
))
})
})
}
fn update_multi(&mut self, entities: &[$Entity]) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
let mut map = $crate::database::hashmap_store::write_or_recover(&self.store.$store_field);
let mut result = Vec::with_capacity(entities.len());
for entity in entities {
map.insert(entity.id, entity.clone());
result.push(entity.clone());
}
Ok(result)
}
fn update_with_relationships(&mut self, entity: &$Entity) -> Result<$Entity, $crate::error::RepositoryError> {
self.update(entity)
}
fn update_with_relationships_multi(&mut self, entities: &[$Entity]) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
self.update_multi(entities)
}
fn remove(&mut self, id: &$crate::types::EntityId) -> Result<(), $crate::error::RepositoryError> {
self.remove_multi(std::slice::from_ref(id))
}
fn remove_multi(&mut self, ids: &[$crate::types::EntityId]) -> Result<(), $crate::error::RepositoryError> {
let mut map = $crate::database::hashmap_store::write_or_recover(&self.store.$store_field);
for id in ids {
map.remove(id);
$(
$crate::database::hashmap_store::delete_from_backward_junction(&self.store.$bj_field, id);
)*
}
Ok(())
}
}
pub struct $TableROStruct<'a> {
store: &'a $crate::database::hashmap_store::HashMapStore,
}
impl<'a> $TableROStruct<'a> {
pub fn new(store: &'a $crate::database::hashmap_store::HashMapStore) -> Self {
Self { store }
}
}
impl<'a> $TableROTrait for $TableROStruct<'a> {
fn get(&self, id: &$crate::types::EntityId) -> Result<Option<$Entity>, $crate::error::RepositoryError> {
Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).get(id).cloned())
}
fn get_multi(&self, ids: &[$crate::types::EntityId]) -> Result<Vec<Option<$Entity>>, $crate::error::RepositoryError> {
let map = $crate::database::hashmap_store::read_or_recover(&self.store.$store_field);
Ok(ids.iter().map(|id| map.get(id).cloned()).collect())
}
fn get_all(&self) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).values().cloned().collect())
}
}
};
}
#[cfg(test)]
mod junction_tests {
use super::*;
use crate::error::RepositoryError;
fn validate_by_walking(
junction: &HashMapJunction,
replacements: &[(EntityId, Vec<EntityId>)],
scalar: bool,
) -> Result<(), RepositoryError> {
use RepositoryError::ConstraintViolation;
use std::collections::HashSet;
let mut owners = HashSet::new();
for (owner, targets) in replacements {
if !owners.insert(*owner) {
return Err(ConstraintViolation(
"duplicate source in relationship batch".into(),
));
}
if scalar && targets.len() > 1 {
return Err(ConstraintViolation(
"to-one relationship accepts at most one target".into(),
));
}
let mut unique = HashSet::new();
if targets.iter().any(|id| !unique.insert(*id)) {
return Err(ConstraintViolation("duplicate relationship target".into()));
}
}
if junction.is_exclusive() {
let mut used = HashSet::new();
for (_, targets) in junction.iter().filter(|(id, _)| !owners.contains(id)) {
used.extend(targets.iter().copied());
}
for (_, targets) in replacements {
if targets.iter().any(|id| !used.insert(*id)) {
return Err(ConstraintViolation(
"relationship target already belongs to another source".into(),
));
}
}
}
Ok(())
}
struct Rng(u64);
impl Rng {
fn below(&mut self, bound: u64) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0 % bound
}
fn ids(&mut self, max_len: u64, bound: u64) -> Vec<EntityId> {
(0..self.below(max_len + 1))
.map(|_| self.below(bound))
.collect()
}
}
#[test]
fn the_index_stays_exact_and_the_check_matches_the_walk() {
for exclusive in [true, false] {
let mut rng = Rng(0x9E37_79B9_7F4A_7C15 + u64::from(exclusive));
let junction = RwLock::new(if exclusive {
HashMapJunction::exclusive()
} else {
HashMapJunction::shared()
});
for step in 0..3000 {
{
let mut jn = write_or_recover(&junction);
match rng.below(6) {
0..=2 => {
let (source, targets) = (rng.below(12), rng.ids(4, 16));
jn.insert(source, targets);
}
3 => {
jn.remove(&rng.below(12));
}
4 => jn.remove_target(&rng.below(16)),
_ => {
let copy = jn.clone();
*jn = copy;
}
}
if let Err(problem) = jn.check_index() {
panic!("exclusive={exclusive}, step {step}: {problem}");
}
}
let batch: Vec<(EntityId, Vec<EntityId>)> = (0..=rng.below(3))
.map(|_| (rng.below(12), rng.ids(2, 16)))
.collect();
for scalar in [false, true] {
let walked = validate_by_walking(&read_or_recover(&junction), &batch, scalar);
let indexed = validate_junction_updates(&junction, &batch, scalar);
assert_eq!(
format!("{indexed:?}"),
format!("{walked:?}"),
"exclusive={exclusive}, step {step}, scalar={scalar}, batch {batch:?}"
);
}
}
}
}
#[test]
fn a_transfer_within_one_batch_is_valid_but_not_from_outside_it() {
let junction = RwLock::new(HashMapJunction::exclusive());
junction_set(&junction, 1, vec![10, 11]);
junction_set(&junction, 2, vec![20]);
let check = |batch: &[(EntityId, Vec<EntityId>)]| {
validate_junction_updates(&junction, batch, false)
};
assert!(check(&[(1, vec![11]), (2, vec![20, 10])]).is_ok());
assert!(check(&[(2, vec![20, 10])]).is_err());
assert!(check(&[(3, vec![10])]).is_err());
assert!(check(&[(1, vec![]), (2, vec![10]), (3, vec![10])]).is_err());
assert!(check(&[(3, vec![]), (4, vec![])]).is_ok());
junction_remove(&junction, &1);
assert!(check(&[(3, vec![10])]).is_ok());
delete_from_backward_junction(&junction, &20);
assert!(check(&[(3, vec![20])]).is_ok());
assert_eq!(junction_get(&junction, &2), Vec::<EntityId>::new());
assert!(read_or_recover(&junction).check_index().is_ok());
}
}