use crate::{
db::{
Db,
data::{DataKey, RawDataKey},
registry::StoreHandle,
relation::{
RelationTargetDecodeContext, RelationTargetMismatchPolicy,
metadata::{StrongRelationInfo, strong_relations_for_model_iter},
model_has_strong_relations_to_target,
reverse_index::{
ReverseRelationSourceInfo, decode_relation_target_data_key, decode_reverse_entry,
relation_target_store, reverse_index_key_for_target_storage_key,
source_row_references_relation_target,
},
},
},
error::InternalError,
metrics::sink::{MetricsEvent, record},
model::entity::EntityModel,
traits::{CanisterKind, EntityKind, EntityValue, Path},
value::StorageKey,
};
use std::collections::BTreeSet;
struct BlockedDeleteProof {
relation: StrongRelationInfo,
source_data_key: DataKey,
target_key: StorageKey,
}
impl BlockedDeleteProof {
fn into_internal_error<S>(self) -> Result<InternalError, InternalError>
where
S: EntityKind + EntityValue,
{
Ok(InternalError::executor_unsupported(
blocked_delete_diagnostic::<S>(
self.relation,
self.source_data_key.try_key::<S>()?,
self.target_key,
),
))
}
}
pub(in crate::db) fn validate_delete_strong_relations_for_source<S>(
db: &Db<S::Canister>,
target_path: &str,
deleted_target_keys: &BTreeSet<RawDataKey>,
) -> Result<(), InternalError>
where
S: EntityKind + EntityValue,
{
let source_info = ReverseRelationSourceInfo::for_type::<S>();
if deleted_target_keys.is_empty() {
return Ok(());
}
if !model_has_strong_relations_to_target(S::MODEL, target_path) {
return Ok(());
}
let source_store = db.with_store_registry(|reg| reg.try_get_store(S::Store::PATH))?;
let Some(blocked) = validate_delete_strong_relations_structural(
db,
source_info,
S::PATH,
S::MODEL,
target_path,
source_store,
deleted_target_keys,
)?
else {
return Ok(());
};
Err(blocked.into_internal_error::<S>()?)
}
fn validate_delete_strong_relations_structural<C>(
db: &Db<C>,
source_info: ReverseRelationSourceInfo,
source_path: &'static str,
source_model: &'static EntityModel,
target_path: &str,
source_store: StoreHandle,
deleted_target_keys: &BTreeSet<RawDataKey>,
) -> Result<Option<BlockedDeleteProof>, InternalError>
where
C: CanisterKind,
{
for relation in strong_relations_for_model_iter(source_model, Some(target_path)) {
let target_index_store = relation_target_store(db, source_info, relation)?;
for target_raw_key in deleted_target_keys {
let Some(target_data_key) = decode_relation_target_data_key(
source_info,
relation,
target_raw_key,
RelationTargetDecodeContext::DeleteValidation,
RelationTargetMismatchPolicy::Skip,
)?
else {
continue;
};
let target_storage_key = target_data_key.storage_key();
let Some(reverse_key) = reverse_index_key_for_target_storage_key(
source_info,
relation,
target_storage_key,
)?
else {
continue;
};
record(MetricsEvent::RelationValidation {
entity_path: source_path,
reverse_lookups: 1,
blocked_deletes: 0,
});
let Some(raw_entry) = target_index_store.with_borrow(|store| store.get(&reverse_key))
else {
continue;
};
let entry = decode_reverse_entry(source_info, relation, &reverse_key, &raw_entry)?;
for source_key in entry.iter_ids() {
let source_data_key = DataKey::new(source_info.entity_tag(), source_key);
let source_raw_key = source_data_key.to_raw()?;
let source_raw_row = source_store.with_data(|store| store.get(&source_raw_key));
let Some(source_raw_row) = source_raw_row else {
return Err(InternalError::reverse_index_entry_corrupted(
source_path,
relation.field_name,
relation.target_path,
&reverse_key,
format!(
"reverse index points at missing source row: source_id={source_key:?} key={:?}",
target_data_key.storage_key(),
),
));
};
let still_references_target = source_row_references_relation_target(
&source_raw_row,
source_model,
source_info,
relation,
target_storage_key,
)?;
if still_references_target {
record(MetricsEvent::RelationValidation {
entity_path: source_path,
reverse_lookups: 0,
blocked_deletes: 1,
});
return Ok(Some(BlockedDeleteProof {
relation,
source_data_key,
target_key: target_storage_key,
}));
}
}
}
}
Ok(None)
}
fn blocked_delete_diagnostic<S>(
relation: StrongRelationInfo,
source_key: S::Key,
target_key: StorageKey,
) -> String
where
S: EntityKind + EntityValue,
{
format!(
"delete blocked by strong relation: source_entity={} source_field={} source_id={source_key:?} target_entity={} target_key={:?}; action=delete source rows or retarget relation before deleting target",
S::PATH,
relation.field_name,
relation.target_path,
target_key.as_value(),
)
}