mod metadata;
mod reverse_index;
mod save_validate;
mod validate;
use crate::{
db::{
Db, EntityRuntimeHooks,
data::RawDataStoreKey,
identity::EntityName,
schema::{PersistedFieldKind, PersistedRelationStrength},
},
error::InternalError,
traits::CanisterKind,
types::EntityTag,
value::Value,
};
use std::{collections::BTreeSet, fmt::Display};
pub(in crate::db) use metadata::{
RelationDescriptor, RelationDescriptorCardinality, relation_descriptors_for_model_iter,
};
pub(crate) use reverse_index::{
ReverseRelationSourceInfo, prepare_reverse_relation_index_mutations_for_source_slot_readers,
};
pub(in crate::db) use save_validate::validate_save_strong_relations_with_accepted_contract;
pub(in crate::db) use validate::validate_delete_strong_relations_for_source;
pub(crate) type StrongRelationDeleteValidateFn<C> =
fn(&Db<C>, &str, &BTreeSet<RawDataStoreKey>) -> Result<(), InternalError>;
#[derive(Clone, Copy, Debug)]
enum RelationTargetDecodeContext {
DeleteValidation,
ReverseIndexPrepare,
}
#[derive(Clone, Copy, Debug)]
enum RelationTargetMismatchPolicy {
Skip,
Reject,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum AcceptedRelationCardinality {
Single,
List,
Set,
}
#[derive(Clone, Copy)]
struct AcceptedRelationTargetDescriptor<'a> {
target_path: &'a str,
target_entity_name: &'a str,
target_entity_tag: EntityTag,
target_store_path: &'a str,
scalar_target_key_kind: &'a PersistedFieldKind,
strength: PersistedRelationStrength,
cardinality: AcceptedRelationCardinality,
}
fn accepted_relation_target_descriptor_from_kind(
kind: &PersistedFieldKind,
) -> Option<AcceptedRelationTargetDescriptor<'_>> {
fn relation_target(
kind: &PersistedFieldKind,
cardinality: AcceptedRelationCardinality,
) -> Option<AcceptedRelationTargetDescriptor<'_>> {
let PersistedFieldKind::Relation {
target_path,
target_entity_name,
target_entity_tag,
target_store_path,
key_kind,
strength,
} = kind
else {
return None;
};
Some(AcceptedRelationTargetDescriptor {
target_path,
target_entity_name,
target_entity_tag: *target_entity_tag,
target_store_path,
scalar_target_key_kind: key_kind.as_ref(),
strength: *strength,
cardinality,
})
}
match kind {
PersistedFieldKind::Relation { .. } => {
relation_target(kind, AcceptedRelationCardinality::Single)
}
PersistedFieldKind::List(inner) | PersistedFieldKind::Set(inner) => {
let cardinality = match kind {
PersistedFieldKind::List(_) => AcceptedRelationCardinality::List,
PersistedFieldKind::Set(_) => AcceptedRelationCardinality::Set,
_ => unreachable!("outer relation collection shape was already matched"),
};
relation_target(inner.as_ref(), cardinality)
}
_ => None,
}
}
fn validate_relation_primary_key_component_kind(
key_kind: &PersistedFieldKind,
) -> Result<(), InternalError> {
match key_kind {
PersistedFieldKind::Account
| PersistedFieldKind::Int8
| PersistedFieldKind::Int16
| PersistedFieldKind::Int32
| PersistedFieldKind::Int64
| PersistedFieldKind::Int128
| PersistedFieldKind::Principal
| PersistedFieldKind::Subaccount
| PersistedFieldKind::Timestamp
| PersistedFieldKind::Nat8
| PersistedFieldKind::Nat16
| PersistedFieldKind::Nat32
| PersistedFieldKind::Nat64
| PersistedFieldKind::Nat128
| PersistedFieldKind::Ulid
| PersistedFieldKind::Unit => Ok(()),
PersistedFieldKind::Relation { key_kind, .. } => {
validate_relation_primary_key_component_kind(key_kind)
}
other => Err(InternalError::relation_source_row_unsupported_key_kind(
other,
)),
}
}
#[derive(Clone, Debug)]
struct AcceptedRelationTargetAuthority {
path: String,
entity_name: EntityName,
entity_tag: EntityTag,
store_path: String,
}
impl AcceptedRelationTargetAuthority {
fn try_new(
source_path: &str,
field_name: &str,
target_path: &str,
target_entity_name: &str,
target_entity_tag: EntityTag,
target_store_path: &str,
) -> Result<Self, InternalError> {
let entity_name = EntityName::try_from_str(target_entity_name).map_err(|err| {
InternalError::strong_relation_target_name_invalid(
source_path,
field_name,
target_path,
target_entity_name,
err,
)
})?;
Ok(Self {
path: target_path.to_string(),
entity_name,
entity_tag: target_entity_tag,
store_path: target_store_path.to_string(),
})
}
#[must_use]
const fn path(&self) -> &str {
self.path.as_str()
}
#[must_use]
const fn entity_name(&self) -> EntityName {
self.entity_name
}
#[must_use]
const fn entity_tag(&self) -> EntityTag {
self.entity_tag
}
#[must_use]
const fn store_path(&self) -> &str {
self.store_path.as_str()
}
fn validate_against_db<'db, C>(
&self,
db: &'db Db<C>,
source_path: &str,
field_name: &str,
) -> Result<Option<&'db EntityRuntimeHooks<C>>, InternalError>
where
C: CanisterKind,
{
if !db.has_runtime_hooks() {
return Ok(None);
}
let hook = db
.runtime_hook_for_entity_tag(self.entity_tag)
.map_err(|err| {
InternalError::strong_relation_target_identity_mismatch(
source_path,
field_name,
self.path.as_str(),
format!(
"target_entity_tag={} is not registered: {err}",
self.entity_tag.value()
),
)
})?;
if hook.entity_path != self.path {
return Err(InternalError::strong_relation_target_identity_mismatch(
source_path,
field_name,
self.path.as_str(),
format!(
"target_entity_tag={} resolves to entity_path={} but relation declares {}",
self.entity_tag.value(),
hook.entity_path,
self.path
),
));
}
if hook.model.name() != self.entity_name.as_str() {
return Err(InternalError::strong_relation_target_identity_mismatch(
source_path,
field_name,
self.path.as_str(),
format!(
"target_entity_tag={} resolves to entity_name={} but relation declares {}",
self.entity_tag.value(),
hook.model.name(),
self.entity_name.as_str(),
),
));
}
if hook.store_path != self.store_path {
return Err(InternalError::strong_relation_target_identity_mismatch(
source_path,
field_name,
self.path.as_str(),
format!(
"target_store_path={} does not match runtime store {} for target_entity_tag={}",
self.store_path,
hook.store_path,
self.entity_tag.value(),
),
));
}
Ok(Some(hook))
}
}
impl InternalError {
pub(in crate::db::relation) fn relation_target_raw_key_error(
source_path: &'static str,
field_name: &str,
target_path: &str,
value: &Value,
message: &'static str,
) -> Self {
Self::executor_unsupported(format!(
"{message}: source={source_path} field={field_name} target={target_path} value={value:?}",
))
}
pub(in crate::db) fn strong_relation_target_name_invalid(
source_path: &str,
field_name: &str,
target_path: &str,
target_entity_name: &str,
err: impl Display,
) -> Self {
Self::executor_internal(format!(
"strong relation target name invalid: source={source_path} field={field_name} target={target_path} name={target_entity_name} ({err})",
))
}
pub(in crate::db) fn strong_relation_target_identity_mismatch(
source_path: &str,
field_name: &str,
target_path: &str,
detail: impl Display,
) -> Self {
Self::executor_internal(format!(
"strong relation target identity mismatch: source={source_path} field={field_name} target={target_path} ({detail})",
))
}
pub(crate) fn strong_relation_target_missing(
source_path: &'static str,
field_name: &str,
target_path: &str,
value: &Value,
) -> Self {
Self::executor_unsupported(format!(
"strong relation missing: source={source_path} field={field_name} target={target_path} key={value:?}",
))
}
pub(crate) fn strong_relation_target_store_missing(
source_path: &'static str,
field_name: &str,
target_path: &str,
target_store_path: &str,
value: &Value,
err: impl Display,
) -> Self {
Self::executor_internal(format!(
"strong relation target store missing: source={source_path} field={field_name} target={target_path} store={target_store_path} key={value:?} ({err})",
))
}
}
pub(super) fn for_each_relation_target_value(
value: &Value,
mut visit: impl FnMut(&Value) -> Result<(), InternalError>,
) -> Result<(), InternalError> {
match value {
Value::List(items) => {
for item in items {
if matches!(item, Value::Null) {
continue;
}
visit(item)?;
}
}
Value::Null => {}
_ => visit(value)?,
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::validate_relation_primary_key_component_kind;
use crate::{
db::schema::{PersistedFieldKind, PersistedRelationStrength},
types::EntityTag,
};
fn relation_key_kind(key_kind: PersistedFieldKind) -> PersistedFieldKind {
PersistedFieldKind::Relation {
target_path: "Target".to_string(),
target_entity_name: "Target".to_string(),
target_entity_tag: EntityTag::new(11),
target_store_path: "TargetStore".to_string(),
key_kind: Box::new(key_kind),
strength: PersistedRelationStrength::Strong,
}
}
#[test]
fn relation_primary_key_component_kind_accepts_admitted_scalar_lanes() {
for kind in [
PersistedFieldKind::Account,
PersistedFieldKind::Int64,
PersistedFieldKind::Int128,
PersistedFieldKind::Nat64,
PersistedFieldKind::Nat128,
PersistedFieldKind::Principal,
PersistedFieldKind::Subaccount,
PersistedFieldKind::Timestamp,
PersistedFieldKind::Ulid,
PersistedFieldKind::Unit,
] {
validate_relation_primary_key_component_kind(&kind)
.expect("admitted relation primary-key component kind should validate");
}
}
#[test]
fn relation_primary_key_component_kind_unwraps_relation_key_kind() {
let kind = relation_key_kind(PersistedFieldKind::Nat128);
validate_relation_primary_key_component_kind(&kind)
.expect("relation field wrapper should validate through its key kind");
}
#[test]
fn relation_primary_key_component_kind_rejects_non_admitted_bigints() {
for kind in [
PersistedFieldKind::IntBig { max_bytes: 32 },
PersistedFieldKind::NatBig { max_bytes: 32 },
relation_key_kind(PersistedFieldKind::IntBig { max_bytes: 32 }),
relation_key_kind(PersistedFieldKind::NatBig { max_bytes: 32 }),
] {
validate_relation_primary_key_component_kind(&kind)
.expect_err("big integer relation primary-key components must reject");
}
}
}