Skip to main content

icydb_core/db/commit/
hooks.rs

1//! Module: commit::hooks
2//! Responsibility: runtime hook contracts and resolution for commit/recovery orchestration.
3//! Does not own: planner semantics, executor branching, or relation invariants.
4//! Boundary: db root delegates hook discovery and hook contract shape to commit.
5
6use crate::{
7    db::index::{StructuralIndexEntryReader, StructuralPrimaryRowReader},
8    db::{
9        Db,
10        commit::{
11            CommitRowOp, PreparedRowCommitOp, prepare_row_commit_for_entity_with_structural_readers,
12        },
13        relation::StrongRelationDeleteValidateFn,
14    },
15    error::InternalError,
16    model::entity::EntityModel,
17    traits::{CanisterKind, EntityKind, EntityValue, Path},
18    types::EntityTag,
19};
20
21// Runtime hook callback used when commit preparation must read existing primary
22// rows and index entries through structural reader facades.
23type PrepareRowCommitWithReadersFn<C> = fn(
24    &Db<C>,
25    &CommitRowOp,
26    &dyn StructuralPrimaryRowReader,
27    &dyn StructuralIndexEntryReader,
28) -> Result<PreparedRowCommitOp, InternalError>;
29
30///
31/// EntityRuntimeHooks
32///
33/// Per-entity runtime callbacks used for commit preparation and delete-side
34/// strong relation validation.
35/// Keeps entity and store routing metadata alongside callback roots so runtime
36/// recovery and structural preflight can resolve the right store without
37/// reintroducing typed entity parameters.
38///
39
40pub struct EntityRuntimeHooks<C: CanisterKind> {
41    pub(crate) entity_tag: EntityTag,
42    pub(crate) model: &'static EntityModel,
43    pub(crate) entity_path: &'static str,
44    pub(crate) store_path: &'static str,
45    pub(in crate::db) prepare_row_commit_with_readers: PrepareRowCommitWithReadersFn<C>,
46    pub(crate) validate_delete_strong_relations: StrongRelationDeleteValidateFn<C>,
47}
48
49impl<C: CanisterKind> EntityRuntimeHooks<C> {
50    /// Build one runtime hook contract for a concrete runtime entity.
51    #[must_use]
52    pub(in crate::db) const fn new(
53        entity_tag: EntityTag,
54        model: &'static EntityModel,
55        entity_path: &'static str,
56        store_path: &'static str,
57        prepare_row_commit_with_readers: PrepareRowCommitWithReadersFn<C>,
58        validate_delete_strong_relations: StrongRelationDeleteValidateFn<C>,
59    ) -> Self {
60        Self {
61            entity_tag,
62            model,
63            entity_path,
64            store_path,
65            prepare_row_commit_with_readers,
66            validate_delete_strong_relations,
67        }
68    }
69
70    /// Build runtime hooks from one entity type.
71    #[must_use]
72    pub const fn for_entity<E>() -> Self
73    where
74        E: EntityKind<Canister = C> + EntityValue,
75    {
76        Self::new(
77            E::ENTITY_TAG,
78            E::MODEL,
79            E::PATH,
80            E::Store::PATH,
81            prepare_row_commit_for_entity_with_structural_readers::<E>,
82            crate::db::relation::validate_delete_strong_relations_for_source::<E>,
83        )
84    }
85}
86
87/// Return whether this db has any registered runtime hook callbacks.
88#[must_use]
89pub(in crate::db) const fn has_runtime_hooks<C: CanisterKind>(
90    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
91) -> bool {
92    !entity_runtime_hooks.is_empty()
93}
94
95/// Validate that each runtime hook owns one unique entity tag.
96///
97/// This runs only in debug builds at hook table construction time so duplicate
98/// registrations fail before runtime dispatch begins.
99#[must_use]
100#[cfg(debug_assertions)]
101pub(in crate::db) const fn debug_assert_unique_runtime_hook_tags<C: CanisterKind>(
102    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
103) -> bool {
104    let mut i = 0usize;
105    while i < entity_runtime_hooks.len() {
106        let mut j = i + 1;
107        while j < entity_runtime_hooks.len() {
108            if entity_runtime_hooks[i].entity_tag.value()
109                == entity_runtime_hooks[j].entity_tag.value()
110            {
111                panic!("duplicate EntityTag detected in runtime hooks");
112            }
113            j += 1;
114        }
115        i += 1;
116    }
117
118    true
119}
120
121/// Resolve exactly one runtime hook for a persisted `EntityTag`.
122/// Duplicate matches are treated as store invariants.
123pub(in crate::db) fn resolve_runtime_hook_by_tag<C: CanisterKind>(
124    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
125    entity_tag: EntityTag,
126) -> Result<&EntityRuntimeHooks<C>, InternalError> {
127    let mut matched = None;
128    for hooks in entity_runtime_hooks {
129        if hooks.entity_tag != entity_tag {
130            continue;
131        }
132
133        if matched.is_some() {
134            return Err(InternalError::duplicate_runtime_hooks_for_entity_tag(
135                entity_tag,
136            ));
137        }
138
139        matched = Some(hooks);
140    }
141
142    matched.ok_or_else(|| InternalError::unsupported_entity_tag_in_data_store(entity_tag))
143}
144
145/// Resolve exactly one runtime hook for a persisted entity path.
146/// Duplicate matches are treated as store invariants.
147pub(in crate::db) fn resolve_runtime_hook_by_path<'a, C: CanisterKind>(
148    entity_runtime_hooks: &'a [EntityRuntimeHooks<C>],
149    entity_path: &str,
150) -> Result<&'a EntityRuntimeHooks<C>, InternalError> {
151    let mut matched = None;
152    for hooks in entity_runtime_hooks {
153        if hooks.entity_path != entity_path {
154            continue;
155        }
156
157        if matched.is_some() {
158            return Err(InternalError::duplicate_runtime_hooks_for_entity_path(
159                entity_path,
160            ));
161        }
162
163        matched = Some(hooks);
164    }
165
166    matched.ok_or_else(|| InternalError::unsupported_entity_path(entity_path))
167}