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