icydb_core/db/runtime_hooks/
mod.rs1use 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
25pub 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 #[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 #[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 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 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#[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
139pub(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
163pub(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
187pub(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
200pub(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
213pub(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 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 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}