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