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, SqlDeleteExecutionBounds, SqlDeleteExposurePolicy,
183    SqlDeleteOrderPolicy, SqlDeletePolicyContext, SqlDeletePolicyRejection, SqlDeletePolicyReport,
184    SqlDeleteReturningBounds, SqlDeleteReturningPolicy, SqlDeleteStatementClassification,
185    SqlDeleteWherePolicy, SqlPublicBoundedDeletePlan, SqlPublicBoundedUpdatePlan,
186    SqlPublicPrimaryKeyDeletePlan, SqlPublicPrimaryKeyUpdatePlan, SqlSessionCurrentDeletePlan,
187    SqlSessionCurrentUpdatePlan, SqlStatementDispatch, SqlStatementResult,
188    SqlStatementShellSurface, SqlStatementSurface, SqlUpdateAssignmentPolicy,
189    SqlUpdateExposurePolicy, SqlUpdateOrderPolicy, SqlUpdatePolicyContext,
190    SqlUpdatePolicyRejection, SqlUpdatePolicyReport, SqlUpdateReturningBounds,
191    SqlUpdateReturningPolicy, SqlUpdateStatementClassification, SqlUpdateWherePolicy,
192    SqlValidatedDeletePlan, SqlValidatedUpdatePlan, classify_sql_delete_policy,
193    classify_sql_update_policy, sql_statement_dispatch, sql_statement_entity_name,
194    sql_statement_shell_surface, sql_statement_surface,
195};
196#[cfg(feature = "diagnostics")]
197pub use session::{
198    DirectDataRowAttribution, FluentTerminalExecutionAttribution, GroupedCountAttribution,
199    GroupedExecutionAttribution, KernelRowAttribution, QueryExecutionAttribution,
200    ScalarAggregateAttribution,
201};
202#[cfg(all(feature = "sql", feature = "diagnostics"))]
203pub use session::{
204    SqlCompileAttribution, SqlExecutionAttribution, SqlHybridCoveringAttribution,
205    SqlOutputBlobAttribution, SqlPureCoveringAttribution, SqlQueryCacheAttribution,
206    SqlQueryExecutionAttribution, SqlScalarAggregateAttribution,
207};
208#[cfg(all(feature = "sql", feature = "diagnostics"))]
209#[doc(hidden)]
210pub use session::{
211    SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics,
212};
213#[cfg(feature = "sql")]
214pub use sql::identifier::{
215    identifier_last_segment, identifiers_tail_match, normalize_identifier_to_scope,
216    split_qualified_identifier,
217};
218#[cfg(feature = "sql")]
219pub use sql::lowering::LoweredSqlCommand;
220
221/// Hidden generated-code alias for borrowed structural map entry payload slices.
222#[doc(hidden)]
223pub type GeneratedStructuralMapPayloadSlices<'a> = Vec<(&'a [u8], &'a [u8])>;
224
225/// Hidden generated-code alias for one decoded enum payload frame.
226#[doc(hidden)]
227pub type GeneratedStructuralEnumPayload<'a> = (String, Option<String>, Option<&'a [u8]>);
228
229/// Hidden generated-code helper for canonical structural text payload framing.
230#[doc(hidden)]
231#[must_use]
232pub(crate) fn encode_generated_structural_text_payload_bytes(value: &str) -> Vec<u8> {
233    data::encode_value_storage_text(value)
234}
235
236/// Hidden generated-code helper for canonical structural list payload framing.
237#[doc(hidden)]
238#[must_use]
239pub(crate) fn encode_generated_structural_list_payload_bytes(items: &[&[u8]]) -> Vec<u8> {
240    data::encode_value_storage_list_item_slices(items)
241}
242
243/// Hidden generated-code helper for canonical structural map payload framing.
244#[doc(hidden)]
245#[must_use]
246pub(crate) fn encode_generated_structural_map_payload_bytes(entries: &[(&[u8], &[u8])]) -> Vec<u8> {
247    data::encode_value_storage_map_entry_slices(entries)
248}
249
250/// Hidden generated-code helper for canonical structural enum payload framing.
251#[doc(hidden)]
252#[must_use]
253pub(crate) fn encode_generated_structural_enum_payload_bytes(
254    variant: &str,
255    path: Option<&str>,
256    payload: Option<&[u8]>,
257) -> Vec<u8> {
258    data::encode_enum(variant, path, payload)
259}
260
261/// Hidden generated-code helper for structural text payload decoding.
262#[doc(hidden)]
263pub(crate) fn decode_generated_structural_text_payload_bytes(
264    raw_bytes: &[u8],
265) -> Result<String, InternalError> {
266    data::decode_value_storage_text(raw_bytes).map_err(InternalError::persisted_row_decode_failed)
267}
268
269/// Hidden generated-code helper for structural list payload decoding.
270#[doc(hidden)]
271pub(crate) fn decode_generated_structural_list_payload_bytes(
272    raw_bytes: &[u8],
273) -> Result<Vec<&[u8]>, InternalError> {
274    data::decode_value_storage_list_item_slices(raw_bytes)
275        .map_err(InternalError::persisted_row_decode_failed)
276}
277
278/// Hidden generated-code helper for structural map payload decoding.
279#[doc(hidden)]
280pub(crate) fn decode_generated_structural_map_payload_bytes(
281    raw_bytes: &[u8],
282) -> Result<GeneratedStructuralMapPayloadSlices<'_>, InternalError> {
283    data::decode_value_storage_map_entry_slices(raw_bytes)
284        .map_err(InternalError::persisted_row_decode_failed)
285}
286
287/// Hidden generated-code helper for structural enum payload decoding.
288#[doc(hidden)]
289pub(crate) fn decode_generated_structural_enum_payload_bytes(
290    raw_bytes: &[u8],
291) -> Result<GeneratedStructuralEnumPayload<'_>, InternalError> {
292    data::decode_enum(raw_bytes).map_err(InternalError::persisted_row_decode_failed)
293}
294
295/// Hidden generated-code helper for persisted structured payload decode errors.
296#[doc(hidden)]
297pub(crate) fn generated_persisted_structured_payload_decode_failed(
298    detail: impl Sized,
299) -> InternalError {
300    InternalError::persisted_row_decode_failed(detail)
301}
302
303///
304/// Db
305/// A handle to the set of stores registered for a specific canister domain.
306///
307
308pub(crate) struct Db<C: CanisterKind> {
309    store: &'static LocalKey<StoreRegistry>,
310    entity_runtime_hooks: &'static [EntityRuntimeHooks<C>],
311    _marker: PhantomData<C>,
312}
313
314impl<C: CanisterKind> Db<C> {
315    /// Construct a db handle without per-entity runtime hooks.
316    #[must_use]
317    #[cfg(test)]
318    pub(crate) const fn new(store: &'static LocalKey<StoreRegistry>) -> Self {
319        Self::new_with_hooks(store, &[])
320    }
321
322    /// Construct a db handle with explicit per-entity runtime hook wiring.
323    #[must_use]
324    pub(crate) const fn new_with_hooks(
325        store: &'static LocalKey<StoreRegistry>,
326        entity_runtime_hooks: &'static [EntityRuntimeHooks<C>],
327    ) -> Self {
328        #[cfg(debug_assertions)]
329        {
330            let _ = crate::db::runtime_hooks::debug_assert_unique_runtime_hook_tags(
331                entity_runtime_hooks,
332            );
333        }
334
335        Self {
336            store,
337            entity_runtime_hooks,
338            _marker: PhantomData,
339        }
340    }
341
342    #[must_use]
343    pub(in crate::db) const fn context<E>(&self) -> Context<'_, E>
344    where
345        E: EntityKind<Canister = C> + EntityValue,
346    {
347        Context::new(self)
348    }
349
350    /// Resolve one named store after enforcing startup recovery.
351    pub(in crate::db) fn recovered_store(&self, path: &str) -> Result<StoreHandle, InternalError> {
352        ensure_recovered(self)?;
353
354        self.store_handle(path)
355    }
356
357    // Resolve one named store without re-entering recovery.
358    //
359    // Internal commit/recovery paths already own recovery authority and must
360    // not bounce back through `ensure_recovered`, or they can recurse through
361    // replay/rebuild preparation.
362    pub(in crate::db) fn store_handle(&self, path: &str) -> Result<StoreHandle, InternalError> {
363        self.with_store_registry(|registry| registry.try_get_store(path))
364    }
365
366    /// Ensure startup/in-progress commit recovery has been applied.
367    pub(crate) fn ensure_recovered_state(&self) -> Result<(), InternalError> {
368        ensure_recovered(self)
369    }
370
371    /// Execute one closure against the registered store set.
372    pub(crate) fn with_store_registry<R>(&self, f: impl FnOnce(&StoreRegistry) -> R) -> R {
373        self.store.with(|reg| f(reg))
374    }
375
376    /// Resolve one stable in-process cache scope identifier for this store registry.
377    ///
378    /// Session-level SQL and structural query caches use this scope to share
379    /// reusable artifacts across fresh `DbSession` values that point at the
380    /// same generated canister store wiring without leaking entries across
381    /// unrelated registries in tests or multi-canister host processes.
382    #[must_use]
383    pub(in crate::db) fn cache_scope_id(&self) -> usize {
384        std::ptr::from_ref::<LocalKey<StoreRegistry>>(self.store) as usize
385    }
386
387    /// Build one named-store resolver for executor/runtime helpers.
388    #[must_use]
389    pub(in crate::db) fn store_resolver(&self) -> executor::StoreResolver<'_> {
390        executor::StoreResolver::new(self)
391    }
392
393    /// Mark every registered index store as fully rebuilt and query-visible.
394    ///
395    /// Recovery restores visibility only after rebuild and post-recovery
396    /// integrity validation complete successfully.
397    pub(in crate::db) fn mark_all_registered_index_stores_ready(&self) {
398        self.with_store_registry(|registry| {
399            for (_, handle) in registry.iter() {
400                handle.mark_index_ready();
401            }
402        });
403    }
404
405    /// Build one storage diagnostics report for registered stores/entities.
406    pub(crate) fn storage_report(
407        &self,
408        name_to_path: &[(&'static str, &'static str)],
409    ) -> Result<StorageReport, InternalError> {
410        diagnostics::storage_report(self, name_to_path)
411    }
412
413    /// Build one storage diagnostics report using default entity-path labels.
414    pub(crate) fn storage_report_default(&self) -> Result<StorageReport, InternalError> {
415        diagnostics::storage_report_default(self)
416    }
417
418    /// Build one integrity scan report for registered stores/entities.
419    pub(crate) fn integrity_report(&self) -> Result<IntegrityReport, InternalError> {
420        diagnostics::integrity_report(self)
421    }
422
423    pub(in crate::db) fn prepare_row_commit_op(
424        &self,
425        op: &CommitRowOp,
426    ) -> Result<PreparedRowCommitOp, InternalError> {
427        runtime_hooks::prepare_row_commit_with_hook(self, self.entity_runtime_hooks, op)
428    }
429
430    // Validate strong relation constraints for delete-selected target keys.
431    pub(crate) fn validate_delete_strong_relations(
432        &self,
433        target_path: &str,
434        deleted_target_keys: &BTreeSet<RawDataStoreKey>,
435    ) -> Result<(), InternalError> {
436        runtime_hooks::validate_delete_strong_relations_with_hooks(
437            self,
438            self.entity_runtime_hooks,
439            target_path,
440            deleted_target_keys,
441        )
442    }
443}
444
445impl<C: CanisterKind> Db<C> {
446    /// Return whether this db has any registered runtime hook callbacks.
447    #[must_use]
448    pub(crate) const fn has_runtime_hooks(&self) -> bool {
449        runtime_hooks::has_runtime_hooks(self.entity_runtime_hooks)
450    }
451
452    /// Return one deterministic list of registered runtime entity catalog rows.
453    pub(crate) fn runtime_entity_catalog(
454        &self,
455    ) -> Result<Vec<EntityCatalogDescription>, InternalError> {
456        let mut entities = Vec::with_capacity(self.entity_runtime_hooks.len());
457
458        for hooks in self.entity_runtime_hooks {
459            let store = self.recovered_store(hooks.store_path)?;
460            let storage = store
461                .storage_capabilities()
462                .storage_mode()
463                .as_str()
464                .to_string();
465            let accepted = store.with_schema_mut(|schema_store| {
466                if let Some(snapshot) = schema_store.latest_persisted_snapshot(hooks.entity_tag)? {
467                    let accepted = AcceptedSchemaSnapshot::try_new(snapshot)?;
468                    if accepted.entity_path() == hooks.entity_path {
469                        return Ok(accepted);
470                    }
471                }
472
473                ensure_accepted_schema_snapshot(
474                    schema_store,
475                    hooks.entity_tag,
476                    hooks.entity_path,
477                    hooks.model,
478                )
479            })?;
480            let snapshot = accepted.persisted_snapshot();
481
482            entities.push(EntityCatalogDescription::new(
483                snapshot.entity_name().to_string(),
484                snapshot.entity_path().to_string(),
485                hooks.store_path.to_string(),
486                storage,
487                EntityCatalogCounts::new(
488                    u32::try_from(snapshot.fields().len()).unwrap_or(u32::MAX),
489                    u32::try_from(snapshot.indexes().len()).unwrap_or(u32::MAX),
490                    u32::try_from(relation_field_count(snapshot.fields())).unwrap_or(u32::MAX),
491                    snapshot.version().get(),
492                ),
493            ));
494        }
495
496        Ok(entities)
497    }
498
499    /// Return one deterministic list of registered runtime stores.
500    #[must_use]
501    pub(crate) fn runtime_store_catalog(&self) -> Vec<StoreCatalogDescription> {
502        let mut stores = self.with_store_registry(|registry| {
503            registry
504                .iter()
505                .map(|(store_path, handle)| {
506                    StoreCatalogDescription::new(
507                        store_path.to_string(),
508                        handle
509                            .storage_capabilities()
510                            .storage_mode()
511                            .as_str()
512                            .to_string(),
513                    )
514                })
515                .collect::<Vec<_>>()
516        });
517        stores.sort_by(|left, right| left.store_path().cmp(right.store_path()));
518        stores
519    }
520
521    /// Return one deterministic list of registered stable-memory allocations.
522    #[must_use]
523    pub(crate) fn runtime_memory_catalog(&self) -> Vec<MemoryCatalogDescription> {
524        let mut memory = self.with_store_registry(|registry| {
525            registry
526                .iter()
527                .flat_map(|(store_path, handle)| {
528                    [
529                        handle.data_allocation(),
530                        handle.index_allocation(),
531                        handle.schema_allocation(),
532                        handle.journal_allocation(),
533                    ]
534                    .into_iter()
535                    .flatten()
536                    .map(move |allocation| {
537                        MemoryCatalogDescription::new(
538                            allocation.stable_key().to_string(),
539                            allocation.memory_id(),
540                            store_path.to_string(),
541                        )
542                    })
543                })
544                .collect::<Vec<_>>()
545        });
546        memory.sort_by(|left, right| {
547            left.memory_id()
548                .cmp(&right.memory_id())
549                .then_with(|| left.tag().cmp(right.tag()))
550                .then_with(|| left.store_path().cmp(right.store_path()))
551        });
552        memory
553    }
554
555    // Resolve exactly one runtime hook for a persisted entity tag.
556    // Duplicate matches are treated as store invariants.
557    pub(crate) fn runtime_hook_for_entity_tag(
558        &self,
559        entity_tag: EntityTag,
560    ) -> Result<&EntityRuntimeHooks<C>, InternalError> {
561        runtime_hooks::resolve_runtime_hook_by_tag(self.entity_runtime_hooks, entity_tag)
562    }
563
564    // Resolve exactly one runtime hook for a persisted entity path.
565    // Duplicate matches are treated as store invariants.
566    pub(crate) fn runtime_hook_for_entity_path(
567        &self,
568        entity_path: &str,
569    ) -> Result<&EntityRuntimeHooks<C>, InternalError> {
570        runtime_hooks::resolve_runtime_hook_by_path(self.entity_runtime_hooks, entity_path)
571    }
572}
573
574fn relation_field_count(fields: &[crate::db::schema::PersistedFieldSnapshot]) -> usize {
575    fields
576        .iter()
577        .filter(|field| persisted_kind_is_relation_field(field.kind()))
578        .count()
579}
580
581fn persisted_kind_is_relation_field(kind: &PersistedFieldKind) -> bool {
582    match kind {
583        PersistedFieldKind::Relation { .. } => true,
584        PersistedFieldKind::List(inner) | PersistedFieldKind::Set(inner) => {
585            matches!(inner.as_ref(), PersistedFieldKind::Relation { .. })
586        }
587        _ => false,
588    }
589}
590
591impl<C: CanisterKind> Copy for Db<C> {}
592
593impl<C: CanisterKind> Clone for Db<C> {
594    fn clone(&self) -> Self {
595        *self
596    }
597}