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