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/// Validate that each runtime hook owns one unique entity tag.
93///
94/// This runs only in debug builds at hook table construction time so duplicate
95/// registrations fail before runtime dispatch begins.
96///
97/// # Panics
98///
99/// Panics when two runtime hooks declare the same entity tag.
100#[must_use]
101#[cfg(debug_assertions)]
102pub(in crate::db) const fn debug_assert_unique_runtime_hook_tags<C: CanisterKind>(
103    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
104) -> bool {
105    let mut i = 0usize;
106    while i < entity_runtime_hooks.len() {
107        let mut j = i + 1;
108        while j < entity_runtime_hooks.len() {
109            if entity_runtime_hooks[i].entity_tag.value()
110                == entity_runtime_hooks[j].entity_tag.value()
111            {
112                panic!("runtime hook invariant");
113            }
114            j += 1;
115        }
116        i += 1;
117    }
118
119    true
120}
121
122/// Resolve exactly one runtime hook for a persisted `EntityTag`.
123/// Duplicate matches are treated as store invariants.
124pub(in crate::db) fn resolve_runtime_hook_by_tag<C: CanisterKind>(
125    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
126    entity_tag: EntityTag,
127) -> Result<&EntityRuntimeHooks<C>, InternalError> {
128    let mut matched = None;
129    for hooks in entity_runtime_hooks {
130        if hooks.entity_tag != entity_tag {
131            continue;
132        }
133
134        if matched.is_some() {
135            return Err(InternalError::duplicate_runtime_hooks_for_entity_tag(
136                entity_tag,
137            ));
138        }
139
140        matched = Some(hooks);
141    }
142
143    matched.ok_or_else(|| InternalError::unsupported_entity_tag_in_data_store(entity_tag))
144}
145
146/// Resolve exactly one runtime hook for a persisted entity path.
147/// Duplicate matches are treated as store invariants.
148pub(in crate::db) fn resolve_runtime_hook_by_path<'a, C: CanisterKind>(
149    entity_runtime_hooks: &'a [EntityRuntimeHooks<C>],
150    entity_path: &str,
151) -> Result<&'a EntityRuntimeHooks<C>, InternalError> {
152    let mut matched = None;
153    for hooks in entity_runtime_hooks {
154        if hooks.entity_path != entity_path {
155            continue;
156        }
157
158        if matched.is_some() {
159            return Err(InternalError::duplicate_runtime_hooks_for_entity_path(
160                entity_path,
161            ));
162        }
163
164        matched = Some(hooks);
165    }
166
167    matched.ok_or_else(|| InternalError::unsupported_entity_path(entity_path))
168}
169
170/// Prepare one row commit op through the runtime hook registry.
171pub(in crate::db) fn prepare_row_commit_with_hook<C: CanisterKind>(
172    db: &Db<C>,
173    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
174    op: &CommitRowOp,
175) -> Result<PreparedRowCommitOp, InternalError> {
176    let hooks = resolve_runtime_hook_by_path(entity_runtime_hooks, op.entity_path.as_ref())?;
177    let store = db.store_handle(hooks.store_path)?;
178    let context = hooks.prepare_commit_context(db, op.schema_fingerprint)?;
179
180    prepare_row_commit_with_context(db, op, &context, &store, &store)
181}
182
183/// Validate delete-side strong relation constraints through runtime hooks.
184pub(in crate::db) fn validate_delete_strong_relations_with_hooks<C: CanisterKind>(
185    db: &Db<C>,
186    entity_runtime_hooks: &[EntityRuntimeHooks<C>],
187    target_path: &str,
188    deleted_target_keys: &BTreeSet<RawDataStoreKey>,
189) -> Result<(), InternalError> {
190    // Skip hook traversal when no target keys were deleted.
191    if deleted_target_keys.is_empty() {
192        return Ok(());
193    }
194
195    // Delegate delete-side relation validation to each entity runtime hook.
196    // Each hook resolves its accepted source contract before deciding whether
197    // the source owns strong relations to the deleted target.
198    for hooks in entity_runtime_hooks {
199        (hooks.validate_delete_strong_relations)(db, target_path, deleted_target_keys)?;
200    }
201
202    Ok(())
203}