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::{
8        Db,
9        commit::{
10            CommitRowOp, CommitSchemaFingerprint, PreparedRowCommitOp,
11            prepare_row_commit_for_entity,
12        },
13        relation::StrongRelationDeleteValidateFn,
14        schema::commit_schema_fingerprint_for_entity,
15    },
16    error::InternalError,
17    traits::{CanisterKind, EntityIdentity, EntityKind, EntityValue},
18    types::EntityTag,
19};
20
21///
22/// EntityRuntimeHooks
23///
24/// Per-entity runtime callbacks used for commit preparation and delete-side
25/// strong relation validation.
26///
27
28pub struct EntityRuntimeHooks<C: CanisterKind> {
29    pub(crate) entity_tag: EntityTag,
30    pub(crate) entity_name: &'static str,
31    pub(crate) entity_path: &'static str,
32    pub(in crate::db) commit_schema_fingerprint: fn() -> CommitSchemaFingerprint,
33    pub(in crate::db) prepare_row_commit:
34        fn(&Db<C>, &CommitRowOp) -> Result<PreparedRowCommitOp, InternalError>,
35    pub(crate) validate_delete_strong_relations: StrongRelationDeleteValidateFn<C>,
36}
37
38impl<C: CanisterKind> EntityRuntimeHooks<C> {
39    /// Build one runtime hook contract for a concrete runtime entity.
40    #[must_use]
41    pub(in crate::db) const fn new(
42        entity_tag: EntityTag,
43        entity_name: &'static str,
44        entity_path: &'static str,
45        commit_schema_fingerprint: fn() -> CommitSchemaFingerprint,
46        prepare_row_commit: fn(&Db<C>, &CommitRowOp) -> Result<PreparedRowCommitOp, InternalError>,
47        validate_delete_strong_relations: StrongRelationDeleteValidateFn<C>,
48    ) -> Self {
49        Self {
50            entity_tag,
51            entity_name,
52            entity_path,
53            commit_schema_fingerprint,
54            prepare_row_commit,
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 as EntityIdentity>::ENTITY_NAME,
68            E::PATH,
69            commit_schema_fingerprint_for_runtime_entity::<E>,
70            prepare_row_commit_for_entity::<E>,
71            crate::db::relation::validate_delete_strong_relations_for_source::<E>,
72        )
73    }
74}
75
76fn commit_schema_fingerprint_for_runtime_entity<E>() -> CommitSchemaFingerprint
77where
78    E: EntityKind,
79{
80    commit_schema_fingerprint_for_entity::<E>()
81}
82
83/// Return whether this db has any registered runtime hook callbacks.
84#[must_use]
85pub(in crate::db) const fn has_runtime_hooks<C: CanisterKind>(
86    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
87) -> bool {
88    !entity_runtime_hooks.is_empty()
89}
90
91/// Validate that each runtime hook owns one unique entity tag.
92///
93/// This runs only in debug builds at hook table construction time so duplicate
94/// registrations fail before runtime dispatch begins.
95#[must_use]
96#[cfg(debug_assertions)]
97pub(in crate::db) const fn debug_assert_unique_runtime_hook_tags<C: CanisterKind>(
98    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
99) -> bool {
100    let mut i = 0usize;
101    while i < entity_runtime_hooks.len() {
102        let mut j = i + 1;
103        while j < entity_runtime_hooks.len() {
104            if entity_runtime_hooks[i].entity_tag.value()
105                == entity_runtime_hooks[j].entity_tag.value()
106            {
107                panic!("duplicate EntityTag detected in runtime hooks");
108            }
109            j += 1;
110        }
111        i += 1;
112    }
113
114    true
115}
116
117/// Resolve exactly one runtime hook for a persisted `EntityTag`.
118/// Duplicate matches are treated as store invariants.
119pub(in crate::db) fn resolve_runtime_hook_by_tag<C: CanisterKind>(
120    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
121    entity_tag: EntityTag,
122) -> Result<&EntityRuntimeHooks<C>, InternalError> {
123    let mut matched = None;
124    for hooks in entity_runtime_hooks {
125        if hooks.entity_tag != entity_tag {
126            continue;
127        }
128
129        if matched.is_some() {
130            return Err(InternalError::store_invariant(format!(
131                "duplicate runtime hooks for entity tag '{}'",
132                entity_tag.value()
133            )));
134        }
135
136        matched = Some(hooks);
137    }
138
139    matched.ok_or_else(|| {
140        InternalError::store_unsupported(format!(
141            "unsupported entity tag in data store: '{}'",
142            entity_tag.value()
143        ))
144    })
145}
146
147/// Resolve exactly one runtime hook for a persisted entity path.
148/// Duplicate matches are treated as store invariants.
149pub(in crate::db) fn resolve_runtime_hook_by_path<'a, C: CanisterKind>(
150    entity_runtime_hooks: &'a [EntityRuntimeHooks<C>],
151    entity_path: &str,
152) -> Result<&'a EntityRuntimeHooks<C>, InternalError> {
153    let mut matched = None;
154    for hooks in entity_runtime_hooks {
155        if hooks.entity_path != entity_path {
156            continue;
157        }
158
159        if matched.is_some() {
160            return Err(InternalError::store_invariant(format!(
161                "duplicate runtime hooks for entity path '{entity_path}'"
162            )));
163        }
164
165        matched = Some(hooks);
166    }
167
168    matched.ok_or_else(|| InternalError::unsupported_entity_path(entity_path))
169}