use crate::{
db::{
Db,
commit::{
CommitRowOp, PreparedRowCommitOp, prepare_row_commit_for_entity_with_structural_readers,
},
data::RawDataKey,
index::{StructuralIndexEntryReader, StructuralPrimaryRowReader},
relation::{StrongRelationDeleteValidateFn, model_has_strong_relations_to_target},
},
error::InternalError,
model::entity::EntityModel,
traits::{CanisterKind, EntityKind, EntityValue, Path},
types::EntityTag,
};
use std::collections::BTreeSet;
pub(in crate::db) type PrepareRowCommitWithReadersFn<C> =
fn(
&Db<C>,
&CommitRowOp,
&dyn StructuralPrimaryRowReader,
&dyn StructuralIndexEntryReader,
) -> Result<PreparedRowCommitOp, InternalError>;
pub struct EntityRuntimeHooks<C: CanisterKind> {
pub(crate) entity_tag: EntityTag,
pub(crate) model: &'static EntityModel,
pub(crate) entity_path: &'static str,
pub(crate) store_path: &'static str,
pub(in crate::db) prepare_row_commit_with_readers: PrepareRowCommitWithReadersFn<C>,
pub(crate) validate_delete_strong_relations: StrongRelationDeleteValidateFn<C>,
}
impl<C: CanisterKind> EntityRuntimeHooks<C> {
#[must_use]
pub(in crate::db) const fn new(
entity_tag: EntityTag,
model: &'static EntityModel,
entity_path: &'static str,
store_path: &'static str,
prepare_row_commit_with_readers: PrepareRowCommitWithReadersFn<C>,
validate_delete_strong_relations: StrongRelationDeleteValidateFn<C>,
) -> Self {
Self {
entity_tag,
model,
entity_path,
store_path,
prepare_row_commit_with_readers,
validate_delete_strong_relations,
}
}
#[must_use]
pub const fn for_entity<E>() -> Self
where
E: EntityKind<Canister = C> + EntityValue,
{
Self::new(
E::ENTITY_TAG,
E::MODEL,
E::PATH,
E::Store::PATH,
prepare_row_commit_for_entity_with_structural_readers::<E>,
crate::db::relation::validate_delete_strong_relations_for_source::<E>,
)
}
}
#[must_use]
pub(in crate::db) const fn has_runtime_hooks<C: CanisterKind>(
entity_runtime_hooks: &[EntityRuntimeHooks<C>],
) -> bool {
!entity_runtime_hooks.is_empty()
}
#[must_use]
#[cfg(debug_assertions)]
pub(in crate::db) const fn debug_assert_unique_runtime_hook_tags<C: CanisterKind>(
entity_runtime_hooks: &[EntityRuntimeHooks<C>],
) -> bool {
let mut i = 0usize;
while i < entity_runtime_hooks.len() {
let mut j = i + 1;
while j < entity_runtime_hooks.len() {
if entity_runtime_hooks[i].entity_tag.value()
== entity_runtime_hooks[j].entity_tag.value()
{
panic!("duplicate EntityTag detected in runtime hooks");
}
j += 1;
}
i += 1;
}
true
}
pub(in crate::db) fn resolve_runtime_hook_by_tag<C: CanisterKind>(
entity_runtime_hooks: &[EntityRuntimeHooks<C>],
entity_tag: EntityTag,
) -> Result<&EntityRuntimeHooks<C>, InternalError> {
let mut matched = None;
for hooks in entity_runtime_hooks {
if hooks.entity_tag != entity_tag {
continue;
}
if matched.is_some() {
return Err(InternalError::duplicate_runtime_hooks_for_entity_tag(
entity_tag,
));
}
matched = Some(hooks);
}
matched.ok_or_else(|| InternalError::unsupported_entity_tag_in_data_store(entity_tag))
}
pub(in crate::db) fn resolve_runtime_hook_by_path<'a, C: CanisterKind>(
entity_runtime_hooks: &'a [EntityRuntimeHooks<C>],
entity_path: &str,
) -> Result<&'a EntityRuntimeHooks<C>, InternalError> {
let mut matched = None;
for hooks in entity_runtime_hooks {
if hooks.entity_path != entity_path {
continue;
}
if matched.is_some() {
return Err(InternalError::duplicate_runtime_hooks_for_entity_path(
entity_path,
));
}
matched = Some(hooks);
}
matched.ok_or_else(|| InternalError::unsupported_entity_path(entity_path))
}
pub(in crate::db) fn prepare_row_commit_with_hook<C: CanisterKind>(
db: &Db<C>,
entity_runtime_hooks: &[EntityRuntimeHooks<C>],
op: &CommitRowOp,
) -> Result<PreparedRowCommitOp, InternalError> {
let hooks = resolve_runtime_hook_by_path(entity_runtime_hooks, op.entity_path.as_ref())?;
let store = db.store_handle(hooks.store_path)?;
(hooks.prepare_row_commit_with_readers)(db, op, &store, &store)
}
pub(in crate::db) fn validate_delete_strong_relations_with_hooks<C: CanisterKind>(
db: &Db<C>,
entity_runtime_hooks: &[EntityRuntimeHooks<C>],
target_path: &str,
deleted_target_keys: &BTreeSet<RawDataKey>,
) -> Result<(), InternalError> {
if deleted_target_keys.is_empty() {
return Ok(());
}
for hooks in entity_runtime_hooks {
if !model_has_strong_relations_to_target(hooks.model, target_path) {
continue;
}
(hooks.validate_delete_strong_relations)(db, target_path, deleted_target_keys)?;
}
Ok(())
}