Skip to main content

icydb_core/db/runtime_hooks/
mod.rs

1//! Module: db::runtime_hooks
2//! Responsibility: runtime entity hook contracts and lookup helpers.
3//! Does not own: commit protocol, relation semantics, or executor branching.
4//! Boundary: db root owns hook registration; commit/delete consume callback lanes.
5
6use crate::{
7    db::{
8        Db,
9        commit::{
10            CommitPrepareContext, CommitRowOp, CommitSchemaFingerprint, PreparedRowCommitOp,
11            prepare_commit_context_for_runtime_entity, prepare_row_commit_with_context,
12        },
13        data::RawDataStoreKey,
14        relation::StrongRelationDeleteValidateFn,
15    },
16    error::InternalError,
17    model::entity::EntityModel,
18    traits::{CanisterKind, EntityKind, EntityValue, Path},
19    types::EntityTag,
20};
21use std::collections::BTreeSet;
22
23///
24/// EntityRuntimeHooks
25///
26/// Per-entity runtime callbacks used by commit preparation and delete-side
27/// strong relation validation. The registry keeps entity and store routing
28/// metadata next to callback roots so runtime recovery and structural preflight
29/// can resolve typed behavior without reintroducing typed entity parameters.
30///
31
32pub struct EntityRuntimeHooks<C: CanisterKind> {
33    pub(in crate::db) entity_tag: EntityTag,
34    pub(in crate::db) model: &'static EntityModel,
35    pub(in crate::db) entity_path: &'static str,
36    pub(in crate::db) store_path: &'static str,
37    pub(in crate::db) validate_delete_strong_relations: StrongRelationDeleteValidateFn<C>,
38}
39
40impl<C: CanisterKind> EntityRuntimeHooks<C> {
41    /// Build one runtime hook contract for a concrete runtime entity.
42    #[must_use]
43    pub(in crate::db) const fn new(
44        entity_tag: EntityTag,
45        model: &'static EntityModel,
46        entity_path: &'static str,
47        store_path: &'static str,
48        validate_delete_strong_relations: StrongRelationDeleteValidateFn<C>,
49    ) -> Self {
50        Self {
51            entity_tag,
52            model,
53            entity_path,
54            store_path,
55            validate_delete_strong_relations,
56        }
57    }
58
59    /// Build runtime hooks from one entity type.
60    #[must_use]
61    pub const fn for_entity<E>() -> Self
62    where
63        E: EntityKind<Canister = C> + EntityValue,
64    {
65        Self::new(
66            E::ENTITY_TAG,
67            E::MODEL,
68            E::PATH,
69            E::Store::PATH,
70            crate::db::relation::validate_delete_strong_relations_for_source::<E>,
71        )
72    }
73
74    /// Resolve accepted commit authority once for a batch targeting this entity.
75    pub(in crate::db) fn prepare_commit_context(
76        &self,
77        db: &Db<C>,
78        schema_fingerprint: CommitSchemaFingerprint,
79    ) -> Result<CommitPrepareContext, InternalError> {
80        prepare_commit_context_for_runtime_entity(
81            db,
82            self.entity_path,
83            self.entity_tag,
84            self.store_path,
85            self.model,
86            schema_fingerprint,
87        )
88    }
89}
90
91/// Return whether this db has any registered runtime hook callbacks.
92#[must_use]
93pub(in crate::db) const fn has_runtime_hooks<C: CanisterKind>(
94    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
95) -> bool {
96    !entity_runtime_hooks.is_empty()
97}
98
99/// Validate that each runtime hook owns one unique entity tag.
100///
101/// This runs only in debug builds at hook table construction time so duplicate
102/// registrations fail before runtime dispatch begins.
103///
104/// # Panics
105///
106/// Panics when two runtime hooks declare the same entity tag.
107#[must_use]
108#[cfg(debug_assertions)]
109pub(in crate::db) const fn debug_assert_unique_runtime_hook_tags<C: CanisterKind>(
110    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
111) -> bool {
112    let mut i = 0usize;
113    while i < entity_runtime_hooks.len() {
114        let mut j = i + 1;
115        while j < entity_runtime_hooks.len() {
116            if entity_runtime_hooks[i].entity_tag.value()
117                == entity_runtime_hooks[j].entity_tag.value()
118            {
119                panic!("runtime hook invariant");
120            }
121            j += 1;
122        }
123        i += 1;
124    }
125
126    true
127}
128
129/// Resolve exactly one runtime hook for a persisted `EntityTag`.
130/// Duplicate matches are treated as store invariants.
131pub(in crate::db) fn resolve_runtime_hook_by_tag<C: CanisterKind>(
132    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
133    entity_tag: EntityTag,
134) -> Result<&EntityRuntimeHooks<C>, InternalError> {
135    let mut matched = None;
136    for hooks in entity_runtime_hooks {
137        if hooks.entity_tag != entity_tag {
138            continue;
139        }
140
141        if matched.is_some() {
142            return Err(InternalError::duplicate_runtime_hooks_for_entity_tag(
143                entity_tag,
144            ));
145        }
146
147        matched = Some(hooks);
148    }
149
150    matched.ok_or_else(|| InternalError::unsupported_entity_tag_in_data_store(entity_tag))
151}
152
153/// Resolve exactly one runtime hook for a persisted entity path.
154/// Duplicate matches are treated as store invariants.
155pub(in crate::db) fn resolve_runtime_hook_by_path<'a, C: CanisterKind>(
156    entity_runtime_hooks: &'a [EntityRuntimeHooks<C>],
157    entity_path: &str,
158) -> Result<&'a EntityRuntimeHooks<C>, InternalError> {
159    let mut matched = None;
160    for hooks in entity_runtime_hooks {
161        if hooks.entity_path != entity_path {
162            continue;
163        }
164
165        if matched.is_some() {
166            return Err(InternalError::duplicate_runtime_hooks_for_entity_path(
167                entity_path,
168            ));
169        }
170
171        matched = Some(hooks);
172    }
173
174    matched.ok_or_else(|| InternalError::unsupported_entity_path(entity_path))
175}
176
177/// Prepare one row commit op through the runtime hook registry.
178pub(in crate::db) fn prepare_row_commit_with_hook<C: CanisterKind>(
179    db: &Db<C>,
180    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
181    op: &CommitRowOp,
182) -> Result<PreparedRowCommitOp, InternalError> {
183    let hooks = resolve_runtime_hook_by_path(entity_runtime_hooks, op.entity_path.as_ref())?;
184    let store = db.store_handle(hooks.store_path)?;
185    let context = hooks.prepare_commit_context(db, op.schema_fingerprint)?;
186
187    prepare_row_commit_with_context(db, op, &context, &store, &store)
188}
189
190/// Validate delete-side strong relation constraints through runtime hooks.
191pub(in crate::db) fn validate_delete_strong_relations_with_hooks<C: CanisterKind>(
192    db: &Db<C>,
193    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
194    target_path: &str,
195    deleted_target_keys: &BTreeSet<RawDataStoreKey>,
196) -> Result<(), InternalError> {
197    // Skip hook traversal when no target keys were deleted.
198    if deleted_target_keys.is_empty() {
199        return Ok(());
200    }
201
202    // Delegate delete-side relation validation to each entity runtime hook.
203    // Each hook resolves its accepted source contract before deciding whether
204    // the source owns strong relations to the deleted target.
205    for hooks in entity_runtime_hooks {
206        (hooks.validate_delete_strong_relations)(db, target_path, deleted_target_keys)?;
207    }
208
209    Ok(())
210}