Skip to main content

icydb_core/db/
mod.rs

1//! Module: db
2//!
3//! Responsibility: root subsystem wiring, façade re-exports, and runtime hook contracts.
4//! Does not own: feature semantics delegated to child modules (`query`, `executor`, etc.).
5//! Boundary: top-level db API and internal orchestration entrypoints.
6
7pub(crate) mod access;
8pub(crate) mod catalog;
9pub(crate) mod cursor;
10pub(crate) mod diagnostics;
11pub(crate) mod identity;
12#[cfg(feature = "diagnostics")]
13pub(in crate::db) mod physical_access;
14pub(crate) mod predicate;
15pub(crate) mod query;
16pub(crate) mod registry;
17pub(crate) mod response;
18pub(crate) mod runtime_hooks;
19pub(crate) mod scalar_expr;
20pub(crate) mod schema;
21pub(crate) mod session;
22#[cfg(feature = "sql")]
23pub(crate) mod sql;
24
25pub(in crate::db) mod codec;
26pub(in crate::db) mod commit;
27pub(in crate::db) mod data;
28pub(in crate::db) mod direction;
29pub(in crate::db) mod executor;
30pub(in crate::db) mod index;
31pub(in crate::db) mod journal;
32pub(in crate::db) mod key_taxonomy;
33pub(in crate::db) mod numeric;
34pub(in crate::db) mod ordered_overlay;
35pub(in crate::db) mod relation;
36pub(in crate::db) mod sql_shared;
37#[cfg(test)]
38pub(in crate::db) mod test_support;
39#[cfg(test)]
40mod tests;
41
42use crate::{
43    db::{
44        commit::{CommitRowOp, PreparedRowCommitOp, ensure_recovered},
45        data::RawDataStoreKey,
46        executor::Context,
47        registry::StoreHandle,
48        schema::{AcceptedSchemaSnapshot, PersistedFieldKind, ensure_accepted_schema_snapshot},
49    },
50    error::InternalError,
51    traits::{CanisterKind, EntityKind, EntityValue},
52    types::EntityTag,
53};
54use std::{collections::BTreeSet, marker::PhantomData, thread::LocalKey};
55
56pub use catalog::{
57    EntityCatalogCounts, EntityCatalogDescription, MemoryCatalogDescription,
58    StoreCatalogDescription,
59};
60#[doc(hidden)]
61pub use codec::hex::encode_hex_lower;
62pub use cursor::{decode_cursor, encode_cursor};
63pub use runtime_hooks::EntityRuntimeHooks;
64// These hidden helper re-exports remain public so the crate-root `__macro`
65// boundary can route generated code through one stable path without widening
66// the normal `db` facade contract.
67pub use data::{DataStore, PersistedRow, SlotReader, SlotWriter, StructuralPatch};
68#[doc(hidden)]
69pub use data::{
70    PersistedScalar, ScalarSlotValueRef, ScalarValueRef,
71    decode_persisted_many_slot_payload_by_meta, decode_persisted_option_scalar_slot_payload,
72    decode_persisted_option_slot_payload_by_kind, decode_persisted_option_slot_payload_by_meta,
73    decode_persisted_scalar_slot_payload, decode_persisted_slot_payload_by_kind,
74    decode_persisted_slot_payload_by_meta, decode_persisted_structured_many_slot_payload,
75    decode_persisted_structured_slot_payload, decode_slot_into_runtime_value,
76    encode_persisted_many_slot_payload_by_meta, encode_persisted_option_scalar_slot_payload,
77    encode_persisted_option_slot_payload_by_meta, encode_persisted_scalar_slot_payload,
78    encode_persisted_slot_payload_by_kind, encode_persisted_slot_payload_by_meta,
79    encode_persisted_structured_many_slot_payload, encode_persisted_structured_slot_payload,
80    encode_runtime_value_into_slot,
81};
82#[cfg(feature = "diagnostics")]
83#[doc(hidden)]
84pub use data::{StructuralReadMetrics, with_structural_read_metrics};
85#[cfg(all(test, not(feature = "diagnostics")))]
86#[expect(unused_imports)]
87pub(crate) use data::{StructuralReadMetrics, with_structural_read_metrics};
88pub use diagnostics::{
89    DataStoreSnapshot, EntitySnapshot, ExecutionAccessPathVariant, ExecutionMetrics,
90    ExecutionOptimization, ExecutionStats, ExecutionTrace, IndexStoreSnapshot, IntegrityReport,
91    IntegrityStoreSnapshot, IntegrityTotals, SchemaStoreSnapshot, StorageReport,
92    StoreSnapshotStorageMode,
93};
94#[doc(hidden)]
95pub use executor::EntityAuthority;
96pub use executor::MutationMode;
97pub use executor::{ExecutionFamily, RouteExecutionMode};
98#[cfg(feature = "diagnostics")]
99#[doc(hidden)]
100pub use executor::{RowCheckMetrics, with_row_check_metrics};
101#[cfg(all(test, not(feature = "diagnostics")))]
102#[expect(unused_imports)]
103pub(crate) use executor::{RowCheckMetrics, with_row_check_metrics};
104#[cfg(feature = "diagnostics")]
105#[doc(hidden)]
106pub use executor::{ScalarMaterializationLaneMetrics, with_scalar_materialization_lane_metrics};
107#[cfg(all(test, not(feature = "diagnostics")))]
108#[expect(unused_imports)]
109pub(crate) use executor::{
110    ScalarMaterializationLaneMetrics, with_scalar_materialization_lane_metrics,
111};
112pub use identity::{EntityName, IndexName};
113pub use index::{IndexState, IndexStore};
114#[doc(hidden)]
115pub use journal::JournalTailStore;
116#[doc(hidden)]
117pub use key_taxonomy::{
118    CompositePrimaryKeyValue, CompositePrimaryKeyValueError, PrimaryKeyComponent, PrimaryKeyValue,
119};
120pub use predicate::{
121    CoercionId, CompareFieldsPredicate, CompareOp, ComparePredicate, MissingRowPolicy, Predicate,
122    UnsupportedQueryFeature,
123};
124#[doc(hidden)]
125pub use predicate::{
126    parse_generated_index_predicate_sql, validate_generated_index_predicate_fields,
127};
128pub use query::builder::numeric_projection::{
129    NumericProjectionExpr, RoundProjectionExpr, add, div, mul, round, round_expr, sub,
130};
131pub use query::plan::validate::PlanError;
132pub use query::{
133    api::ResponseCardinalityExt,
134    builder::{
135        AggregateExpr, FieldRef, TextProjectionExpr, ValueProjectionExpr, avg, contains, count,
136        count_by, ends_with, exists, first, last, left, length, lower, ltrim, max, max_by, min,
137        min_by, position, replace, right, rtrim, starts_with, substring, substring_with_length,
138        sum, trim, upper,
139    },
140    explain::{
141        ExplainAccessCandidateV1, ExplainAccessDecisionKind, ExplainAccessDecisionV1,
142        ExplainAggregateTerminalPlan, ExplainEligibleAlternativeV1, ExplainExecutionDescriptor,
143        ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
144        ExplainExecutionOrderingSource, ExplainPlan, ExplainRejectedIndexV1,
145        ExplainResidualSummaryV1, ExplainSelectedAccessV1,
146    },
147    expr::{FilterExpr, FilterValue, OrderExpr, OrderTerm, asc, desc, field},
148    fluent::{
149        delete::FluentDeleteQuery,
150        load::{FluentLoadQuery, LoadQueryResult, PagedLoadQuery},
151    },
152    intent::{
153        AccessRequirementError, AccessRequirementViolation, CompiledQuery, IntentError,
154        PlannedQuery, Query, QueryError, QueryExecutionError, RequiredAccessPath,
155    },
156    plan::{DeleteSpec, LoadSpec, OrderDirection, QueryMode},
157    trace::{QueryTracePlan, TraceExecutionFamily, TraceReuseArtifactClass, TraceReuseEvent},
158};
159pub use registry::{
160    StoreAllocationIdentities, StoreAllocationIdentity, StoreAllocationIdentityCapability,
161    StoreCommitParticipation, StoreDurability, StoreLiveValidationCapability,
162    StoreRecoveryCapability, StoreRegistry, StoreRelationSourceCapability,
163    StoreRelationTargetCapability, StoreRuntimeStorageCapabilities, StoreRuntimeStorageMode,
164    StoreSchemaMetadataCapability,
165};
166pub use response::{
167    EntityResponse, GroupedRow, PagedGroupedExecution, PagedGroupedExecutionWithTrace,
168    PagedLoadExecution, PagedLoadExecutionWithTrace, ProjectedRow, ProjectionResponse,
169    Response as RowResponse, ResponseError, ResponseRow, Row, WriteBatchResponse,
170};
171pub use schema::{
172    EntityFieldDescription, EntityIndexDescription, EntityRelationCardinality,
173    EntityRelationDescription, EntityRelationStrength, EntitySchemaCheckDescription,
174    EntitySchemaDescription, SchemaLiteralValidationReason, SchemaStore, SchemaValidationOperator,
175    ValidateError,
176};
177#[cfg(not(feature = "sql"))]
178pub use session::DbSession;
179#[cfg(feature = "sql")]
180pub use session::{
181    DbSession, SqlAdminBulkDeletePlan, SqlAdminBulkUpdatePlan, SqlDdlExecutionStatus,
182    SqlDdlMutationKind, SqlDdlPreparationReport, SqlDeleteExposurePolicy, SqlDeletePolicyContext,
183    SqlDeletePolicyRejection, SqlDeletePolicyReport, SqlDeleteStatementClassification,
184    SqlPublicBoundedDeletePlan, SqlPublicBoundedUpdatePlan, SqlPublicPrimaryKeyDeletePlan,
185    SqlPublicPrimaryKeyUpdatePlan, SqlSessionCurrentDeletePlan, SqlSessionCurrentUpdatePlan,
186    SqlStatementDispatch, SqlStatementResult, SqlStatementShellSurface, SqlStatementSurface,
187    SqlUpdateAssignmentPolicy, SqlUpdateExposurePolicy, SqlUpdatePolicyContext,
188    SqlUpdatePolicyRejection, SqlUpdatePolicyReport, SqlUpdateStatementClassification,
189    SqlValidatedDeletePlan, SqlValidatedUpdatePlan, SqlWriteExecutionBounds, SqlWriteOrderProof,
190    SqlWriteReturningBounds, SqlWriteReturningShape, SqlWriteStatementShape, SqlWriteWhereProof,
191    classify_sql_delete_policy, classify_sql_update_policy, sql_statement_dispatch,
192    sql_statement_entity_name, sql_statement_shell_surface, sql_statement_surface,
193};
194#[cfg(feature = "diagnostics")]
195pub use session::{
196    DirectDataRowAttribution, FluentTerminalExecutionAttribution, GroupedCountAttribution,
197    GroupedExecutionAttribution, KernelRowAttribution, QueryExecutionAttribution,
198    ScalarAggregateAttribution,
199};
200#[cfg(all(feature = "sql", feature = "diagnostics"))]
201pub use session::{
202    SqlCompileAttribution, SqlExecutionAttribution, SqlHybridCoveringAttribution,
203    SqlOutputBlobAttribution, SqlPureCoveringAttribution, SqlQueryCacheAttribution,
204    SqlQueryExecutionAttribution, SqlScalarAggregateAttribution,
205};
206#[cfg(all(feature = "sql", feature = "diagnostics"))]
207#[doc(hidden)]
208pub use session::{
209    SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics,
210};
211#[cfg(feature = "sql")]
212pub use sql::identifier::{
213    identifier_last_segment, identifiers_tail_match, normalize_identifier_to_scope,
214    split_qualified_identifier,
215};
216#[cfg(feature = "sql")]
217pub use sql::lowering::LoweredSqlCommand;
218
219/// Hidden generated-code alias for borrowed structural map entry payload slices.
220#[doc(hidden)]
221pub type GeneratedStructuralMapPayloadSlices<'a> = Vec<(&'a [u8], &'a [u8])>;
222
223/// Hidden generated-code alias for one decoded enum payload frame.
224#[doc(hidden)]
225pub type GeneratedStructuralEnumPayload<'a> = (String, Option<String>, Option<&'a [u8]>);
226
227/// Hidden generated-code helper for canonical structural text payload framing.
228#[doc(hidden)]
229#[must_use]
230pub(crate) fn encode_generated_structural_text_payload_bytes(value: &str) -> Vec<u8> {
231    data::encode_value_storage_text(value)
232}
233
234/// Hidden generated-code helper for canonical structural list payload framing.
235#[doc(hidden)]
236#[must_use]
237pub(crate) fn encode_generated_structural_list_payload_bytes(items: &[&[u8]]) -> Vec<u8> {
238    data::encode_value_storage_list_item_slices(items)
239}
240
241/// Hidden generated-code helper for canonical structural map payload framing.
242#[doc(hidden)]
243#[must_use]
244pub(crate) fn encode_generated_structural_map_payload_bytes(entries: &[(&[u8], &[u8])]) -> Vec<u8> {
245    data::encode_value_storage_map_entry_slices(entries)
246}
247
248/// Hidden generated-code helper for canonical structural enum payload framing.
249#[doc(hidden)]
250#[must_use]
251pub(crate) fn encode_generated_structural_enum_payload_bytes(
252    variant: &str,
253    path: Option<&str>,
254    payload: Option<&[u8]>,
255) -> Vec<u8> {
256    data::encode_enum(variant, path, payload)
257}
258
259/// Hidden generated-code helper for structural text payload decoding.
260#[doc(hidden)]
261pub(crate) fn decode_generated_structural_text_payload_bytes(
262    raw_bytes: &[u8],
263) -> Result<String, InternalError> {
264    data::decode_value_storage_text(raw_bytes).map_err(InternalError::persisted_row_decode_failed)
265}
266
267/// Hidden generated-code helper for structural list payload decoding.
268#[doc(hidden)]
269pub(crate) fn decode_generated_structural_list_payload_bytes(
270    raw_bytes: &[u8],
271) -> Result<Vec<&[u8]>, InternalError> {
272    data::decode_value_storage_list_item_slices(raw_bytes)
273        .map_err(InternalError::persisted_row_decode_failed)
274}
275
276/// Hidden generated-code helper for structural map payload decoding.
277#[doc(hidden)]
278pub(crate) fn decode_generated_structural_map_payload_bytes(
279    raw_bytes: &[u8],
280) -> Result<GeneratedStructuralMapPayloadSlices<'_>, InternalError> {
281    data::decode_value_storage_map_entry_slices(raw_bytes)
282        .map_err(InternalError::persisted_row_decode_failed)
283}
284
285/// Hidden generated-code helper for structural enum payload decoding.
286#[doc(hidden)]
287pub(crate) fn decode_generated_structural_enum_payload_bytes(
288    raw_bytes: &[u8],
289) -> Result<GeneratedStructuralEnumPayload<'_>, InternalError> {
290    data::decode_enum(raw_bytes).map_err(InternalError::persisted_row_decode_failed)
291}
292
293/// Hidden generated-code helper for persisted structured payload decode errors.
294#[doc(hidden)]
295pub(crate) fn generated_persisted_structured_payload_decode_failed(
296    detail: impl Sized,
297) -> InternalError {
298    InternalError::persisted_row_decode_failed(detail)
299}
300
301///
302/// Db
303/// A handle to the set of stores registered for a specific canister domain.
304///
305
306pub(crate) struct Db<C: CanisterKind> {
307    store: &'static LocalKey<StoreRegistry>,
308    entity_runtime_hooks: &'static [EntityRuntimeHooks<C>],
309    _marker: PhantomData<C>,
310}
311
312impl<C: CanisterKind> Db<C> {
313    /// Construct a db handle without per-entity runtime hooks.
314    #[must_use]
315    #[cfg(test)]
316    pub(crate) const fn new(store: &'static LocalKey<StoreRegistry>) -> Self {
317        Self::new_with_hooks(store, &[])
318    }
319
320    /// Construct a db handle with explicit per-entity runtime hook wiring.
321    #[must_use]
322    pub(crate) const fn new_with_hooks(
323        store: &'static LocalKey<StoreRegistry>,
324        entity_runtime_hooks: &'static [EntityRuntimeHooks<C>],
325    ) -> Self {
326        #[cfg(debug_assertions)]
327        {
328            let _ = crate::db::runtime_hooks::debug_assert_unique_runtime_hook_tags(
329                entity_runtime_hooks,
330            );
331        }
332
333        Self {
334            store,
335            entity_runtime_hooks,
336            _marker: PhantomData,
337        }
338    }
339
340    #[must_use]
341    pub(in crate::db) const fn context<E>(&self) -> Context<'_, E>
342    where
343        E: EntityKind<Canister = C> + EntityValue,
344    {
345        Context::new(self)
346    }
347
348    /// Resolve one named store after enforcing startup recovery.
349    pub(in crate::db) fn recovered_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
350        ensure_recovered(self)?;
351
352        self.store_handle(path)
353    }
354
355    // Resolve one named store without re-entering recovery.
356    //
357    // Internal commit/recovery paths already own recovery authority and must
358    // not bounce back through `ensure_recovered`, or they can recurse through
359    // replay/rebuild preparation.
360    pub(in crate::db) fn store_handle(&self, path: &str) -> Result<StoreHandle, InternalError> {
361        self.with_store_registry(|registry| registry.try_get_store(path))
362    }
363
364    /// Ensure startup/in-progress commit recovery has been applied.
365    pub(crate) fn ensure_recovered_state(&self) -> Result<(), InternalError> {
366        ensure_recovered(self)
367    }
368
369    /// Execute one closure against the registered store set.
370    pub(crate) fn with_store_registry<R>(&self, f: impl FnOnce(&StoreRegistry) -> R) -> R {
371        self.store.with(|reg| f(reg))
372    }
373
374    /// Resolve one stable in-process cache scope identifier for this store registry.
375    ///
376    /// Session-level SQL and structural query caches use this scope to share
377    /// reusable artifacts across fresh `DbSession` values that point at the
378    /// same generated canister store wiring without leaking entries across
379    /// unrelated registries in tests or multi-canister host processes.
380    #[must_use]
381    pub(in crate::db) fn cache_scope_id(&self) -> usize {
382        std::ptr::from_ref::<LocalKey<StoreRegistry>>(self.store) as usize
383    }
384
385    /// Build one named-store resolver for executor/runtime helpers.
386    #[must_use]
387    pub(in crate::db) fn store_resolver(&self) -> executor::StoreResolver<'_> {
388        executor::StoreResolver::new(self)
389    }
390
391    /// Mark every registered index store as fully rebuilt and query-visible.
392    ///
393    /// Recovery restores visibility only after rebuild and post-recovery
394    /// integrity validation complete successfully.
395    pub(in crate::db) fn mark_all_registered_index_stores_ready(&self) {
396        self.with_store_registry(|registry| {
397            for (_, handle) in registry.iter() {
398                handle.mark_index_ready();
399            }
400        });
401    }
402
403    /// Build one storage diagnostics report for registered stores/entities.
404    pub(crate) fn storage_report(
405        &self,
406        name_to_path: &[(&'static str, &'static str)],
407    ) -> Result<StorageReport, InternalError> {
408        diagnostics::storage_report(self, name_to_path)
409    }
410
411    /// Build one storage diagnostics report using default entity-path labels.
412    pub(crate) fn storage_report_default(&self) -> Result<StorageReport, InternalError> {
413        diagnostics::storage_report_default(self)
414    }
415
416    /// Build one integrity scan report for registered stores/entities.
417    pub(crate) fn integrity_report(&self) -> Result<IntegrityReport, InternalError> {
418        diagnostics::integrity_report(self)
419    }
420
421    pub(in crate::db) fn prepare_row_commit_op(
422        &self,
423        op: &CommitRowOp,
424    ) -> Result<PreparedRowCommitOp, InternalError> {
425        runtime_hooks::prepare_row_commit_with_hook(self, self.entity_runtime_hooks, op)
426    }
427
428    // Validate strong relation constraints for delete-selected target keys.
429    pub(crate) fn validate_delete_strong_relations(
430        &self,
431        target_path: &str,
432        deleted_target_keys: &BTreeSet<RawDataStoreKey>,
433    ) -> Result<(), InternalError> {
434        runtime_hooks::validate_delete_strong_relations_with_hooks(
435            self,
436            self.entity_runtime_hooks,
437            target_path,
438            deleted_target_keys,
439        )
440    }
441}
442
443impl<C: CanisterKind> Db<C> {
444    /// Return whether this db has any registered runtime hook callbacks.
445    #[must_use]
446    pub(crate) const fn has_runtime_hooks(&self) -> bool {
447        runtime_hooks::has_runtime_hooks(self.entity_runtime_hooks)
448    }
449
450    /// Return one deterministic list of registered runtime entity catalog rows.
451    pub(crate) fn runtime_entity_catalog(
452        &self,
453    ) -> Result<Vec<EntityCatalogDescription>, InternalError> {
454        let mut entities = Vec::with_capacity(self.entity_runtime_hooks.len());
455
456        for hooks in self.entity_runtime_hooks {
457            let store = self.recovered_store(hooks.store_path)?;
458            let storage = store
459                .storage_capabilities()
460                .storage_mode()
461                .as_str()
462                .to_string();
463            let accepted = store.with_schema_mut(|schema_store| {
464                if let Some(snapshot) = schema_store.latest_persisted_snapshot(hooks.entity_tag)? {
465                    let accepted = AcceptedSchemaSnapshot::try_new(snapshot)?;
466                    if accepted.entity_path() == hooks.entity_path {
467                        return Ok(accepted);
468                    }
469                }
470
471                ensure_accepted_schema_snapshot(
472                    schema_store,
473                    hooks.entity_tag,
474                    hooks.entity_path,
475                    hooks.model,
476                )
477            })?;
478            let snapshot = accepted.persisted_snapshot();
479
480            entities.push(EntityCatalogDescription::new(
481                snapshot.entity_name().to_string(),
482                snapshot.entity_path().to_string(),
483                hooks.store_path.to_string(),
484                storage,
485                EntityCatalogCounts::new(
486                    u32::try_from(snapshot.fields().len()).unwrap_or(u32::MAX),
487                    u32::try_from(snapshot.indexes().len()).unwrap_or(u32::MAX),
488                    u32::try_from(relation_field_count(snapshot.fields())).unwrap_or(u32::MAX),
489                    snapshot.version().get(),
490                ),
491            ));
492        }
493
494        Ok(entities)
495    }
496
497    /// Return one deterministic list of registered runtime stores.
498    #[must_use]
499    pub(crate) fn runtime_store_catalog(&self) -> Vec<StoreCatalogDescription> {
500        let mut stores = self.with_store_registry(|registry| {
501            registry
502                .iter()
503                .map(|(store_path, handle)| {
504                    StoreCatalogDescription::new(
505                        store_path.to_string(),
506                        handle
507                            .storage_capabilities()
508                            .storage_mode()
509                            .as_str()
510                            .to_string(),
511                    )
512                })
513                .collect::<Vec<_>>()
514        });
515        stores.sort_by(|left, right| left.store_path().cmp(right.store_path()));
516        stores
517    }
518
519    /// Return one deterministic list of registered stable-memory allocations.
520    #[must_use]
521    pub(crate) fn runtime_memory_catalog(&self) -> Vec<MemoryCatalogDescription> {
522        let mut memory = self.with_store_registry(|registry| {
523            registry
524                .iter()
525                .flat_map(|(store_path, handle)| {
526                    [
527                        handle.data_allocation(),
528                        handle.index_allocation(),
529                        handle.schema_allocation(),
530                        handle.journal_allocation(),
531                    ]
532                    .into_iter()
533                    .flatten()
534                    .map(move |allocation| {
535                        MemoryCatalogDescription::new(
536                            allocation.stable_key().to_string(),
537                            allocation.memory_id(),
538                            store_path.to_string(),
539                        )
540                    })
541                })
542                .collect::<Vec<_>>()
543        });
544        memory.sort_by(|left, right| {
545            left.memory_id()
546                .cmp(&right.memory_id())
547                .then_with(|| left.tag().cmp(right.tag()))
548                .then_with(|| left.store_path().cmp(right.store_path()))
549        });
550        memory
551    }
552
553    // Resolve exactly one runtime hook for a persisted entity tag.
554    // Duplicate matches are treated as store invariants.
555    pub(crate) fn runtime_hook_for_entity_tag(
556        &self,
557        entity_tag: EntityTag,
558    ) -> Result<&EntityRuntimeHooks<C>, InternalError> {
559        runtime_hooks::resolve_runtime_hook_by_tag(self.entity_runtime_hooks, entity_tag)
560    }
561
562    // Resolve exactly one runtime hook for a persisted entity path.
563    // Duplicate matches are treated as store invariants.
564    pub(crate) fn runtime_hook_for_entity_path(
565        &self,
566        entity_path: &str,
567    ) -> Result<&EntityRuntimeHooks<C>, InternalError> {
568        runtime_hooks::resolve_runtime_hook_by_path(self.entity_runtime_hooks, entity_path)
569    }
570}
571
572fn relation_field_count(fields: &[crate::db::schema::PersistedFieldSnapshot]) -> usize {
573    fields
574        .iter()
575        .filter(|field| persisted_kind_is_relation_field(field.kind()))
576        .count()
577}
578
579fn persisted_kind_is_relation_field(kind: &PersistedFieldKind) -> bool {
580    match kind {
581        PersistedFieldKind::Relation { .. } => true,
582        PersistedFieldKind::List(inner) | PersistedFieldKind::Set(inner) => {
583            matches!(inner.as_ref(), PersistedFieldKind::Relation { .. })
584        }
585        _ => false,
586    }
587}
588
589impl<C: CanisterKind> Copy for Db<C> {}
590
591impl<C: CanisterKind> Clone for Db<C> {
592    fn clone(&self) -> Self {
593        *self
594    }
595}