Skip to main content

icydb_core/db/registry/
handle.rs

1//! Module: db::registry::handle
2//! Responsibility: stable store handles and runtime storage capability descriptors.
3//! Does not own: registry path lookup or store mutation semantics.
4//! Boundary: exposes registered storage roles without exposing registry internals.
5
6use crate::db::{
7    commit::database_incarnation_id,
8    data::DataStore,
9    index::{IndexId, IndexKeyKind, IndexState, IndexStore, UserIndexPrefixCardinalityKey},
10    integrity::DatabaseIncarnationId,
11    journal::{FoldWatermark, JournalTailStore},
12    memory_runtime, open_memory,
13    schema::{
14        SchemaStore,
15        cardinality_build::CardinalityBuildAuthority,
16        cardinality_generation::{
17            CardinalityAcceptedRootIdentity, CardinalityCountDigest, CardinalityGenerationState,
18            CardinalityStoreAllocationIdentity,
19        },
20    },
21};
22use crate::{error::InternalError, types::EntityTag};
23
24use serde::Deserialize;
25use sha2::{Digest, Sha256};
26use std::{cell::RefCell, thread::LocalKey};
27
28///
29/// StoreHandle
30///
31/// StoreHandle binds the row, index, and schema stores for one generated schema
32/// `Store` path.
33/// It is the stable access token passed across commit, recovery, executor, and
34/// diagnostics boundaries instead of exposing registry internals directly.
35///
36
37#[derive(Clone, Copy, Debug)]
38pub struct StoreHandle {
39    data: &'static LocalKey<RefCell<DataStore>>,
40    index: &'static LocalKey<RefCell<IndexStore>>,
41    schema: &'static LocalKey<RefCell<SchemaStore>>,
42    journal: Option<&'static LocalKey<RefCell<JournalTailStore>>>,
43    allocations: StoreAllocationIdentities,
44    cardinality_allocation: Option<CardinalityStoreAllocationIdentity>,
45    capabilities: StoreRuntimeStorageCapabilities,
46}
47
48enum ReadyCardinalityCountTargets<'a> {
49    Digests(&'a [CardinalityCountDigest]),
50    UserIndexPrefixes(&'a [UserIndexPrefixCardinalityKey]),
51}
52
53enum ReadyCardinalitySource {
54    Current {
55        database_incarnation: DatabaseIncarnationId,
56    },
57    Admitted {
58        database_incarnation: DatabaseIncarnationId,
59        accepted_root: CardinalityAcceptedRootIdentity,
60        fold_watermark: FoldWatermark,
61    },
62}
63
64/// Opaque comparable lifecycle identity for optional exact-prefix evidence.
65///
66/// The fields remain private to the evidence owner. Consumers may only retain
67/// and compare this value; they cannot reconstruct generation policy from it.
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub(in crate::db) struct ExactPrefixCardinalityLifecycleStamp(
70    ExactPrefixCardinalityLifecycleIdentity,
71);
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74enum ExactPrefixCardinalityLifecycleIdentity {
75    Volatile,
76    MissingDurableAuthority,
77    Corrupt,
78    Journaled {
79        header_digest: Option<[u8; 32]>,
80        cursor_present: bool,
81        delta_watermark: Option<FoldWatermark>,
82    },
83}
84
85/// One complete exact-prefix evidence attempt through current store authority.
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub(in crate::db) enum ExactUserIndexPrefixEvidence {
88    Exact(Vec<u64>),
89    Unavailable(ExactPrefixCardinalityLifecycleStamp),
90}
91
92impl ReadyCardinalityCountTargets<'_> {
93    const fn len(&self) -> usize {
94        match self {
95            Self::Digests(digests) => digests.len(),
96            Self::UserIndexPrefixes(keys) => keys.len(),
97        }
98    }
99}
100
101/// Diagnostic storage mode carried by a runtime storage capability descriptor.
102///
103/// Policy code should branch on capability axes instead of this display value.
104#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
105pub enum StoreRuntimeStorageMode {
106    /// Volatile in-process heap storage.
107    #[default]
108    Heap,
109    /// Journaled cached-stable durable storage.
110    Journaled,
111}
112
113impl StoreRuntimeStorageMode {
114    /// Return the user-facing storage mode label.
115    #[must_use]
116    pub const fn as_str(self) -> &'static str {
117        match self {
118            Self::Heap => "heap",
119            Self::Journaled => "journaled",
120        }
121    }
122}
123
124/// Whether a store owns durable allocation identity.
125#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, serde::Serialize)]
126#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
127pub enum StoreAllocationIdentityCapability {
128    /// Stable allocation identity is present.
129    #[default]
130    Present,
131    /// Stable allocation identity is absent.
132    Absent,
133}
134
135impl StoreAllocationIdentityCapability {
136    /// Return the user-facing capability label.
137    #[must_use]
138    pub const fn as_str(self) -> &'static str {
139        match self {
140            Self::Present => "present",
141            Self::Absent => "absent",
142        }
143    }
144}
145
146/// Store durability class.
147#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, serde::Serialize)]
148#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
149pub enum StoreDurability {
150    /// Store contents participate in durable storage semantics.
151    #[default]
152    Durable,
153    /// Store contents are live-only and volatile.
154    Volatile,
155}
156
157impl StoreDurability {
158    /// Return the user-facing durability label.
159    #[must_use]
160    pub const fn as_str(self) -> &'static str {
161        match self {
162            Self::Durable => "durable",
163            Self::Volatile => "volatile",
164        }
165    }
166}
167
168/// Store recovery capability.
169#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, serde::Serialize)]
170#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
171pub enum StoreRecoveryCapability {
172    /// Store contents can be recovered from canonical stable BTrees plus a
173    /// committed journal tail.
174    #[default]
175    StableBasePlusJournalReplay,
176    /// Store contents are not recovered.
177    None,
178}
179
180impl StoreRecoveryCapability {
181    /// Return the user-facing recovery label.
182    #[must_use]
183    pub const fn as_str(self) -> &'static str {
184        match self {
185            Self::StableBasePlusJournalReplay => "stable-base-plus-journal-replay",
186            Self::None => "none",
187        }
188    }
189}
190
191/// Store commit participation class.
192#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, serde::Serialize)]
193#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
194pub enum StoreCommitParticipation {
195    /// Store mutations participate in the durable commit path.
196    #[default]
197    Durable,
198    /// Store mutations are live-only side effects.
199    LiveOnly,
200}
201
202impl StoreCommitParticipation {
203    /// Return the user-facing commit-participation label.
204    #[must_use]
205    pub const fn as_str(self) -> &'static str {
206        match self {
207            Self::Durable => "durable",
208            Self::LiveOnly => "live-only",
209        }
210    }
211}
212
213/// Store schema metadata persistence class.
214#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, serde::Serialize)]
215#[cfg_attr(not(target_family = "wasm"), derive(ic_wire::Wire))]
216pub enum StoreSchemaMetadataCapability {
217    /// The store-local projection is rebuilt from a durable accepted checkpoint
218    /// and does not retain its own schema history.
219    LiveRebuiltMetadata,
220    /// Schema metadata is canonical stable history plus committed journal tail.
221    #[default]
222    CanonicalStableHistoryPlusJournalTail,
223}
224
225impl StoreSchemaMetadataCapability {
226    /// Return the user-facing schema-metadata capability label.
227    #[must_use]
228    pub const fn as_str(self) -> &'static str {
229        match self {
230            Self::LiveRebuiltMetadata => "live-rebuilt-metadata",
231            Self::CanonicalStableHistoryPlusJournalTail => {
232                "canonical-stable-history-plus-journal-tail"
233            }
234        }
235    }
236}
237
238/// Relation source capability for a store.
239#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
240pub enum StoreRelationSourceCapability {
241    /// Source rows can own durable relation integrity.
242    #[default]
243    DurableSource,
244    /// Source rows can participate in live relation validation.
245    LiveSource,
246}
247
248/// Relation target capability for a store.
249#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
250pub enum StoreRelationTargetCapability {
251    /// Target rows can be referenced by durable source rows.
252    #[default]
253    DurableTarget,
254    /// Target rows are volatile and cannot satisfy durable source integrity.
255    VolatileTarget,
256}
257
258/// Runtime storage capability descriptor carried by one registered store.
259///
260/// Capabilities describe storage policy. They are not allocation identity.
261#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
262pub struct StoreRuntimeStorageCapabilities {
263    storage_mode: StoreRuntimeStorageMode,
264    allocation_identity: StoreAllocationIdentityCapability,
265    durability: StoreDurability,
266    recovery: StoreRecoveryCapability,
267    commit_participation: StoreCommitParticipation,
268    schema_metadata: StoreSchemaMetadataCapability,
269    relation_source: StoreRelationSourceCapability,
270    relation_target: StoreRelationTargetCapability,
271}
272
273impl StoreRuntimeStorageCapabilities {
274    /// Capability descriptor for heap stores.
275    #[must_use]
276    pub const fn heap() -> Self {
277        Self {
278            storage_mode: StoreRuntimeStorageMode::Heap,
279            allocation_identity: StoreAllocationIdentityCapability::Absent,
280            durability: StoreDurability::Volatile,
281            recovery: StoreRecoveryCapability::None,
282            commit_participation: StoreCommitParticipation::LiveOnly,
283            schema_metadata: StoreSchemaMetadataCapability::LiveRebuiltMetadata,
284            relation_source: StoreRelationSourceCapability::LiveSource,
285            relation_target: StoreRelationTargetCapability::VolatileTarget,
286        }
287    }
288
289    /// Capability descriptor for journaled cached-stable stores.
290    #[must_use]
291    pub const fn journaled() -> Self {
292        Self {
293            storage_mode: StoreRuntimeStorageMode::Journaled,
294            allocation_identity: StoreAllocationIdentityCapability::Present,
295            durability: StoreDurability::Durable,
296            recovery: StoreRecoveryCapability::StableBasePlusJournalReplay,
297            commit_participation: StoreCommitParticipation::Durable,
298            schema_metadata: StoreSchemaMetadataCapability::CanonicalStableHistoryPlusJournalTail,
299            relation_source: StoreRelationSourceCapability::DurableSource,
300            relation_target: StoreRelationTargetCapability::DurableTarget,
301        }
302    }
303
304    /// Diagnostic storage mode. Policy code should use the capability axes.
305    #[must_use]
306    pub const fn storage_mode(self) -> StoreRuntimeStorageMode {
307        self.storage_mode
308    }
309
310    /// Allocation identity capability.
311    #[must_use]
312    pub const fn allocation_identity(self) -> StoreAllocationIdentityCapability {
313        self.allocation_identity
314    }
315
316    /// Durability capability.
317    #[must_use]
318    pub const fn durability(self) -> StoreDurability {
319        self.durability
320    }
321
322    /// Recovery capability.
323    #[must_use]
324    pub const fn recovery(self) -> StoreRecoveryCapability {
325        self.recovery
326    }
327
328    /// Commit participation capability.
329    #[must_use]
330    pub const fn commit_participation(self) -> StoreCommitParticipation {
331        self.commit_participation
332    }
333
334    /// Schema metadata persistence capability.
335    #[must_use]
336    pub const fn schema_metadata(self) -> StoreSchemaMetadataCapability {
337        self.schema_metadata
338    }
339
340    /// Relation source capability.
341    #[must_use]
342    pub const fn relation_source(self) -> StoreRelationSourceCapability {
343        self.relation_source
344    }
345
346    /// Relation target capability.
347    #[must_use]
348    pub const fn relation_target(self) -> StoreRelationTargetCapability {
349        self.relation_target
350    }
351}
352
353///
354/// StoreAllocationIdentity
355///
356/// Durable allocation identity for one physical stable-memory role.
357///
358
359#[derive(Clone, Copy, Debug, Eq, PartialEq)]
360pub struct StoreAllocationIdentity {
361    memory_id: u8,
362    stable_key: &'static str,
363}
364
365impl StoreAllocationIdentity {
366    /// Resolve one logical allocation after host bootstrap has committed it.
367    pub fn from_committed_key(
368        stable_key: &'static str,
369    ) -> Result<Self, ic_memory::RuntimeOpenError> {
370        Ok(Self::new(
371            memory_runtime()
372                .ok_or(ic_memory::RuntimeOpenError::NotBootstrapped)?
373                .memory_id(stable_key)?,
374            stable_key,
375        ))
376    }
377
378    /// Build one stable allocation identity descriptor.
379    #[must_use]
380    pub const fn new(memory_id: u8, stable_key: &'static str) -> Self {
381        Self {
382            memory_id,
383            stable_key,
384        }
385    }
386
387    /// Stable-memory manager ID.
388    #[must_use]
389    pub const fn memory_id(self) -> u8 {
390        self.memory_id
391    }
392
393    /// Durable stable-memory key.
394    #[must_use]
395    pub const fn stable_key(self) -> &'static str {
396        self.stable_key
397    }
398}
399
400/// Preserve the database's persisted key/ID agreement before a key-only open.
401/// The committed TLS capability is immutable between these observations; this
402/// checks retained application metadata without choosing or allocating an ID.
403pub(in crate::db) fn open_committed_memory(
404    stable_key: &str,
405    memory_id: u8,
406) -> Result<ic_memory::RuntimeMemory<ic_memory::DefaultMemoryImpl>, InternalError> {
407    let committed_id = memory_runtime()
408        .ok_or_else(InternalError::store_internal)?
409        .memory_id(stable_key)
410        .map_err(|_| InternalError::store_internal())?;
411    if committed_id != memory_id {
412        return Err(InternalError::store_internal());
413    }
414    open_memory(stable_key).map_err(|_| InternalError::store_internal())
415}
416
417///
418/// StoreAllocationIdentities
419///
420/// Durable allocation identities for one logical store's data, index, and
421/// schema memories.
422///
423
424#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
425pub struct StoreAllocationIdentities {
426    data: Option<StoreAllocationIdentity>,
427    index: Option<StoreAllocationIdentity>,
428    schema: Option<StoreAllocationIdentity>,
429    journal: Option<StoreAllocationIdentity>,
430}
431
432impl StoreAllocationIdentities {
433    /// Build an absent allocation identity bundle.
434    #[must_use]
435    pub const fn absent() -> Self {
436        Self {
437            data: None,
438            index: None,
439            schema: None,
440            journal: None,
441        }
442    }
443
444    /// Build one journaled cached-stable allocation identity bundle.
445    #[must_use]
446    pub const fn new_journaled(
447        data: StoreAllocationIdentity,
448        index: StoreAllocationIdentity,
449        schema: StoreAllocationIdentity,
450        journal: StoreAllocationIdentity,
451    ) -> Self {
452        Self {
453            data: Some(data),
454            index: Some(index),
455            schema: Some(schema),
456            journal: Some(journal),
457        }
458    }
459
460    /// Return data-memory allocation identity.
461    #[must_use]
462    pub const fn data(self) -> Option<StoreAllocationIdentity> {
463        self.data
464    }
465
466    /// Return index-memory allocation identity.
467    #[must_use]
468    pub const fn index(self) -> Option<StoreAllocationIdentity> {
469        self.index
470    }
471
472    /// Return schema-memory allocation identity.
473    #[must_use]
474    pub const fn schema(self) -> Option<StoreAllocationIdentity> {
475        self.schema
476    }
477
478    /// Return journal-tail allocation identity.
479    #[must_use]
480    pub const fn journal(self) -> Option<StoreAllocationIdentity> {
481        self.journal
482    }
483
484    /// Return the allocation capability represented by this triplet, or
485    /// `None` if the triplet is partially populated and therefore invalid.
486    #[must_use]
487    pub const fn allocation_identity_capability(self) -> Option<StoreAllocationIdentityCapability> {
488        match (self.data, self.index, self.schema) {
489            (Some(_), Some(_), Some(_)) => Some(StoreAllocationIdentityCapability::Present),
490            (None, None, None) if self.journal.is_none() => {
491                Some(StoreAllocationIdentityCapability::Absent)
492            }
493            _ => None,
494        }
495    }
496
497    /// Return whether this allocation shape matches the concrete storage
498    /// capability descriptor.
499    #[must_use]
500    pub const fn matches_storage_capabilities(
501        self,
502        capabilities: StoreRuntimeStorageCapabilities,
503    ) -> bool {
504        match capabilities.storage_mode() {
505            StoreRuntimeStorageMode::Heap => {
506                self.data.is_none()
507                    && self.index.is_none()
508                    && self.schema.is_none()
509                    && self.journal.is_none()
510            }
511            StoreRuntimeStorageMode::Journaled => {
512                self.data.is_some()
513                    && self.index.is_some()
514                    && self.schema.is_some()
515                    && self.journal.is_some()
516            }
517        }
518    }
519}
520
521impl StoreHandle {
522    /// Build a store handle with an explicit allocation identity decision.
523    #[must_use]
524    pub const fn new(
525        data: &'static LocalKey<RefCell<DataStore>>,
526        index: &'static LocalKey<RefCell<IndexStore>>,
527        schema: &'static LocalKey<RefCell<SchemaStore>>,
528        allocations: StoreAllocationIdentities,
529        capabilities: StoreRuntimeStorageCapabilities,
530    ) -> Self {
531        Self {
532            data,
533            index,
534            schema,
535            journal: None,
536            allocations,
537            cardinality_allocation: None,
538            capabilities,
539        }
540    }
541
542    /// Build a journaled store handle with an explicit journal-tail store.
543    #[must_use]
544    pub fn new_journaled(
545        data: &'static LocalKey<RefCell<DataStore>>,
546        index: &'static LocalKey<RefCell<IndexStore>>,
547        schema: &'static LocalKey<RefCell<SchemaStore>>,
548        journal: &'static LocalKey<RefCell<JournalTailStore>>,
549        allocations: StoreAllocationIdentities,
550        capabilities: StoreRuntimeStorageCapabilities,
551    ) -> Self {
552        let cardinality_allocation = CardinalityStoreAllocationIdentity::derive(allocations).ok();
553        Self {
554            data,
555            index,
556            schema,
557            journal: Some(journal),
558            allocations,
559            cardinality_allocation,
560            capabilities,
561        }
562    }
563
564    /// Borrow the row store immutably.
565    pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
566        self.data.with_borrow(f)
567    }
568
569    /// Borrow the row store mutably.
570    pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
571        self.data.with_borrow_mut(f)
572    }
573
574    /// Borrow the index store immutably.
575    pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
576        self.index.with_borrow(f)
577    }
578
579    /// Borrow the index store mutably.
580    pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
581        self.index.with_borrow_mut(f)
582    }
583
584    /// Borrow the schema store immutably.
585    pub fn with_schema<R>(&self, f: impl FnOnce(&SchemaStore) -> R) -> R {
586        self.schema.with_borrow(f)
587    }
588
589    /// Borrow the schema store mutably.
590    pub fn with_schema_mut<R>(&self, f: impl FnOnce(&mut SchemaStore) -> R) -> R {
591        self.schema.with_borrow_mut(f)
592    }
593
594    /// Return exact visible entity cardinality through the store's canonical proof boundary.
595    #[must_use]
596    pub(in crate::db) fn exact_entity_count(&self, entity: EntityTag) -> Option<u64> {
597        if self.journal.is_none() {
598            return self.with_data(|store| store.exact_entity_count(entity));
599        }
600        let delta = self.with_data(|store| store.exact_entity_cardinality_delta(entity))?;
601        let digest = CardinalityCountDigest::for_entity(entity);
602        let base = self
603            .ready_cardinality_counts(&[digest], |authority| authority.accepts_entity(entity))
604            .ok()
605            .flatten()?
606            .into_iter()
607            .next()?;
608        apply_visible_cardinality_delta(base, delta)
609    }
610
611    /// Return exact visible cardinality for one accepted user-index prefix.
612    #[must_use]
613    pub(in crate::db) fn exact_user_index_prefix_count(
614        &self,
615        data_generation: u64,
616        key_kind: IndexKeyKind,
617        index_id: IndexId,
618        components: &[Vec<u8>],
619    ) -> Option<u64> {
620        self.exact_user_index_prefix_counts(data_generation, key_kind, index_id, [components])?
621            .into_iter()
622            .next()
623    }
624
625    /// Return exact visible counts for prefixes on one accepted user index.
626    #[must_use]
627    pub(in crate::db) fn exact_user_index_prefix_counts<'a>(
628        &self,
629        data_generation: u64,
630        key_kind: IndexKeyKind,
631        index_id: IndexId,
632        component_prefixes: impl IntoIterator<Item = &'a [Vec<u8>]>,
633    ) -> Option<Vec<u64>> {
634        let component_prefixes = component_prefixes.into_iter().collect::<Vec<_>>();
635        if key_kind != IndexKeyKind::User {
636            return None;
637        }
638        if self.journal.is_none() {
639            return self.with_index(|store| {
640                component_prefixes
641                    .iter()
642                    .map(|components| {
643                        store.exact_prefix_cardinality(
644                            data_generation,
645                            key_kind,
646                            index_id,
647                            components,
648                        )
649                    })
650                    .collect()
651            });
652        }
653        let deltas = self.with_index(|store| {
654            component_prefixes
655                .iter()
656                .map(|components| {
657                    store.exact_prefix_cardinality_delta(key_kind, index_id, components)
658                })
659                .collect::<Option<Vec<_>>>()
660        })?;
661        let digests = component_prefixes
662            .iter()
663            .map(|components| {
664                CardinalityCountDigest::for_user_index_prefix(index_id, components).ok()
665            })
666            .collect::<Option<Vec<_>>>()?;
667        let bases = self
668            .ready_cardinality_counts(&digests, |authority| {
669                component_prefixes.iter().all(|components| {
670                    authority.accepts_user_index_prefix(index_id, components.len())
671                })
672            })
673            .ok()
674            .flatten()?;
675        bases
676            .into_iter()
677            .zip(deltas)
678            .map(|(base, delta)| apply_visible_cardinality_delta(base, delta))
679            .collect()
680    }
681
682    /// Return exact visible counts for accepted prefix keys across user indexes.
683    #[must_use]
684    pub(in crate::db) fn exact_user_index_prefix_key_counts(
685        &self,
686        data_generation: u64,
687        keys: &[UserIndexPrefixCardinalityKey],
688    ) -> Option<Vec<u64>> {
689        self.exact_user_index_prefix_key_counts_with_authority(data_generation, keys, None)
690    }
691
692    /// Return exact prefix counts through an accepted root admitted in this request.
693    #[must_use]
694    pub(in crate::db) fn exact_user_index_prefix_key_counts_for_admitted_root(
695        &self,
696        database_incarnation: DatabaseIncarnationId,
697        accepted_root: CardinalityAcceptedRootIdentity,
698        data_generation: u64,
699        keys: &[UserIndexPrefixCardinalityKey],
700    ) -> Option<Vec<u64>> {
701        self.exact_user_index_prefix_key_counts_with_authority(
702            data_generation,
703            keys,
704            Some((database_incarnation, accepted_root)),
705        )
706    }
707
708    /// Return complete exact counts or one opaque current availability stamp.
709    ///
710    /// This boundary knows nothing about plans, caches, bindings, cursors, or
711    /// selected winners. It only proves current accepted-prefix evidence.
712    #[must_use]
713    pub(in crate::db) fn exact_user_index_prefix_evidence_for_admitted_root(
714        &self,
715        database_incarnation: DatabaseIncarnationId,
716        accepted_root: CardinalityAcceptedRootIdentity,
717        keys: &[UserIndexPrefixCardinalityKey],
718    ) -> ExactUserIndexPrefixEvidence {
719        let data_generation = self.with_data(DataStore::generation);
720        if let Some(counts) = self.exact_user_index_prefix_key_counts_for_admitted_root(
721            database_incarnation,
722            accepted_root,
723            data_generation,
724            keys,
725        ) {
726            return ExactUserIndexPrefixEvidence::Exact(counts);
727        }
728
729        ExactUserIndexPrefixEvidence::Unavailable(
730            self.exact_user_index_prefix_evidence_lifecycle_stamp(),
731        )
732    }
733
734    /// Return the cheap availability identity without reading any prefix count.
735    #[must_use]
736    pub(in crate::db) fn exact_user_index_prefix_evidence_lifecycle_stamp(
737        &self,
738    ) -> ExactPrefixCardinalityLifecycleStamp {
739        if self.journal.is_none() {
740            return ExactPrefixCardinalityLifecycleStamp(
741                ExactPrefixCardinalityLifecycleIdentity::Volatile,
742            );
743        }
744        if self.cardinality_allocation.is_none() {
745            return ExactPrefixCardinalityLifecycleStamp(
746                ExactPrefixCardinalityLifecycleIdentity::MissingDurableAuthority,
747            );
748        }
749        let delta_watermark = self.with_index(IndexStore::exact_prefix_cardinality_delta_watermark);
750        match self.with_schema(SchemaStore::cardinality_generation_lifecycle_control) {
751            Ok((header, cursor_present)) => ExactPrefixCardinalityLifecycleStamp(
752                ExactPrefixCardinalityLifecycleIdentity::Journaled {
753                    header_digest: header.map(|header| {
754                        let mut hasher = Sha256::new();
755                        hasher.update(b"icydb.cardinality-lifecycle-stamp.v1");
756                        hasher.update(header.encode());
757                        hasher.finalize().into()
758                    }),
759                    cursor_present,
760                    delta_watermark,
761                },
762            ),
763            Err(_) => ExactPrefixCardinalityLifecycleStamp(
764                ExactPrefixCardinalityLifecycleIdentity::Corrupt,
765            ),
766        }
767    }
768
769    fn exact_user_index_prefix_key_counts_with_authority(
770        &self,
771        data_generation: u64,
772        keys: &[UserIndexPrefixCardinalityKey],
773        admitted: Option<(DatabaseIncarnationId, CardinalityAcceptedRootIdentity)>,
774    ) -> Option<Vec<u64>> {
775        if keys.is_empty() {
776            return None;
777        }
778        if self.journal.is_none() {
779            return self.with_index(|store| {
780                keys.iter()
781                    .map(|key| {
782                        store.exact_prefix_cardinality(
783                            data_generation,
784                            IndexKeyKind::User,
785                            key.index_id(),
786                            key.prefix_components(),
787                        )
788                    })
789                    .collect()
790            });
791        }
792        let (delta_watermark, deltas) = self.with_index(|store| {
793            let watermark = store.exact_prefix_cardinality_delta_watermark()?;
794            keys.iter()
795                .map(|key| {
796                    store.exact_prefix_cardinality_delta(
797                        IndexKeyKind::User,
798                        key.index_id(),
799                        key.prefix_components(),
800                    )
801                })
802                .collect::<Option<Vec<_>>>()
803                .map(|deltas| (watermark, deltas))
804        })?;
805        let accepts = |authority: &CardinalityBuildAuthority| {
806            keys.iter().all(|key| {
807                authority.accepts_user_index_prefix(key.index_id(), key.prefix_components().len())
808            })
809        };
810        let bases = match admitted {
811            Some((database_incarnation, accepted_root)) => self
812                .ready_cardinality_counts_for_source(
813                    ReadyCardinalitySource::Admitted {
814                        database_incarnation,
815                        accepted_root,
816                        fold_watermark: delta_watermark,
817                    },
818                    ReadyCardinalityCountTargets::UserIndexPrefixes(keys),
819                    accepts,
820                ),
821            None => self.ready_cardinality_counts_for_targets(
822                ReadyCardinalityCountTargets::UserIndexPrefixes(keys),
823                accepts,
824            ),
825        }
826        .ok()
827        .flatten()?;
828        bases
829            .into_iter()
830            .zip(deltas)
831            .map(|(base, delta)| apply_visible_cardinality_delta(base, delta))
832            .collect()
833    }
834
835    /// Prove that one accepted user-index prefix family has a synchronized Ready generation.
836    ///
837    /// This does not expose or infer count values. Callers may retain every
838    /// branch conservatively, but must use the exact count methods above before
839    /// pruning any branch as empty.
840    #[must_use]
841    pub(in crate::db) fn user_index_prefix_family_has_ready_generation<'a, I>(
842        &self,
843        database_incarnation: DatabaseIncarnationId,
844        accepted_root: CardinalityAcceptedRootIdentity,
845        data_generation: u64,
846        key_kind: IndexKeyKind,
847        index_id: IndexId,
848        component_prefixes: I,
849    ) -> bool
850    where
851        I: Clone + IntoIterator<Item = &'a [Vec<u8>]>,
852    {
853        if key_kind != IndexKeyKind::User || component_prefixes.clone().into_iter().next().is_none()
854        {
855            return false;
856        }
857        if self.journal.is_none() {
858            return self.with_index(|store| {
859                component_prefixes.clone().into_iter().all(|components| {
860                    store
861                        .exact_prefix_cardinality(data_generation, key_kind, index_id, components)
862                        .is_some()
863                })
864            });
865        }
866        let delta_watermark = self.with_index(|store| {
867            let watermark = store.exact_prefix_cardinality_delta_watermark()?;
868            component_prefixes
869                .clone()
870                .into_iter()
871                .all(|components| {
872                    store
873                        .exact_prefix_cardinality_delta(key_kind, index_id, components)
874                        .is_some()
875                })
876                .then_some(watermark)
877        });
878        delta_watermark.is_some_and(|watermark| {
879            self.ready_cardinality_counts_for_source(
880                ReadyCardinalitySource::Admitted {
881                    database_incarnation,
882                    accepted_root,
883                    fold_watermark: watermark,
884                },
885                ReadyCardinalityCountTargets::Digests(&[]),
886                |authority| {
887                    component_prefixes.into_iter().all(|components| {
888                        authority.accepts_user_index_prefix(index_id, components.len())
889                    })
890                },
891            )
892            .is_ok_and(|counts| counts.is_some())
893        })
894    }
895
896    /// Sum exact visible counts for prefixes on one accepted user index.
897    #[must_use]
898    pub(in crate::db) fn exact_user_index_prefix_count_sum<'a>(
899        &self,
900        data_generation: u64,
901        key_kind: IndexKeyKind,
902        index_id: IndexId,
903        component_prefixes: impl IntoIterator<Item = &'a [Vec<u8>]>,
904        stop_after: Option<u64>,
905    ) -> Option<u64> {
906        let component_prefixes = component_prefixes.into_iter().collect::<Vec<_>>();
907        if self.journal.is_none() {
908            return self.with_index(|store| {
909                store.exact_prefix_cardinality_sum(
910                    data_generation,
911                    key_kind,
912                    index_id,
913                    component_prefixes.iter().copied(),
914                    stop_after,
915                )
916            });
917        }
918        let counts = self.exact_user_index_prefix_counts(
919            data_generation,
920            key_kind,
921            index_id,
922            component_prefixes.iter().copied(),
923        )?;
924        let mut total = 0_u64;
925        for count in counts {
926            total = total.checked_add(count)?;
927            if stop_after.is_some_and(|required| total >= required) {
928                break;
929            }
930        }
931        Some(total)
932    }
933
934    /// Enumerate one bounded child-prefix family proven complete by exact parent/child totals.
935    pub(in crate::db) fn exact_user_index_child_prefixes_for_parent_set<'a>(
936        &self,
937        data_generation: u64,
938        index_id: IndexId,
939        parent_prefixes: impl IntoIterator<Item = &'a [Vec<u8>]>,
940        total_cap: usize,
941    ) -> Result<Option<Vec<Vec<Vec<u8>>>>, InternalError> {
942        let mut parent_prefixes = parent_prefixes
943            .into_iter()
944            .map(<[Vec<u8>]>::to_vec)
945            .collect::<Vec<_>>();
946        if parent_prefixes.iter().any(Vec::is_empty) {
947            return Ok(None);
948        }
949        icydb_schema::compact_sort_unstable_by(&mut parent_prefixes, Ord::cmp);
950        parent_prefixes.dedup();
951        let Some(child_prefixes) = self.with_index(|store| {
952            store.exact_child_prefixes_for_parent_set(
953                data_generation,
954                IndexKeyKind::User,
955                index_id,
956                parent_prefixes.iter().map(Vec::as_slice),
957                total_cap,
958            )
959        })?
960        else {
961            return Ok(None);
962        };
963        if self.journal.is_none() {
964            return Ok(Some(child_prefixes));
965        }
966        let parent_count = parent_prefixes.len();
967        let Some(counts) = self.exact_user_index_prefix_counts(
968            data_generation,
969            IndexKeyKind::User,
970            index_id,
971            parent_prefixes
972                .iter()
973                .chain(&child_prefixes)
974                .map(Vec::as_slice),
975        ) else {
976            return Ok(None);
977        };
978        let (parent_counts, child_counts) = counts.split_at(parent_count);
979        let Some(parent_total) = checked_cardinality_sum(parent_counts) else {
980            return Ok(None);
981        };
982        let Some(child_total) = checked_cardinality_sum(child_counts) else {
983            return Ok(None);
984        };
985        Ok((parent_total == child_total).then_some(child_prefixes))
986    }
987
988    fn ready_cardinality_counts(
989        &self,
990        digests: &[CardinalityCountDigest],
991        accepts: impl FnOnce(&CardinalityBuildAuthority) -> bool,
992    ) -> Result<Option<Vec<u64>>, InternalError> {
993        self.ready_cardinality_counts_for_targets(
994            ReadyCardinalityCountTargets::Digests(digests),
995            accepts,
996        )
997    }
998
999    fn ready_cardinality_counts_for_targets(
1000        &self,
1001        targets: ReadyCardinalityCountTargets<'_>,
1002        accepts: impl FnOnce(&CardinalityBuildAuthority) -> bool,
1003    ) -> Result<Option<Vec<u64>>, InternalError> {
1004        let incarnation = database_incarnation_id()?;
1005        self.ready_cardinality_counts_for_source(
1006            ReadyCardinalitySource::Current {
1007                database_incarnation: incarnation,
1008            },
1009            targets,
1010            accepts,
1011        )
1012    }
1013
1014    fn ready_cardinality_counts_for_source(
1015        &self,
1016        source: ReadyCardinalitySource,
1017        targets: ReadyCardinalityCountTargets<'_>,
1018        accepts: impl FnOnce(&CardinalityBuildAuthority) -> bool,
1019    ) -> Result<Option<Vec<u64>>, InternalError> {
1020        let Some(journal) = self.journal else {
1021            return Ok(None);
1022        };
1023        let Some(allocation) = self.cardinality_allocation else {
1024            return Ok(None);
1025        };
1026        let (incarnation, accepted_root, watermark) = match source {
1027            ReadyCardinalitySource::Current {
1028                database_incarnation,
1029            } => (
1030                database_incarnation,
1031                None,
1032                journal.with_borrow(JournalTailStore::fold_watermark)?,
1033            ),
1034            ReadyCardinalitySource::Admitted {
1035                database_incarnation,
1036                accepted_root,
1037                fold_watermark,
1038            } => (database_incarnation, Some(accepted_root), fold_watermark),
1039        };
1040        self.with_schema(|schema| {
1041            let (header, cursor) = schema.cardinality_generation_control()?;
1042            let Some(header) = header else {
1043                return Ok(None);
1044            };
1045            if header.state() != CardinalityGenerationState::Ready || cursor.is_some() {
1046                return Ok(None);
1047            }
1048            let authority = match accepted_root {
1049                Some(root) => CardinalityBuildAuthority::derive_for_admitted_consumer_root(
1050                    schema,
1051                    incarnation,
1052                    allocation,
1053                    root,
1054                    watermark,
1055                )?,
1056                None => CardinalityBuildAuthority::derive_for_current_consumer(
1057                    schema,
1058                    incarnation,
1059                    allocation,
1060                    watermark,
1061                )?,
1062            };
1063            let Some(authority) = authority else {
1064                return Ok(None);
1065            };
1066            if !accepts(&authority) {
1067                return Ok(None);
1068            }
1069            if header.validate_source(authority.source()).is_err() {
1070                return Ok(None);
1071            }
1072            if targets.len() != 0 && schema.cardinality_count_slot_is_empty(header.slot())? {
1073                return Ok(Some(vec![0; targets.len()]));
1074            }
1075            let counts = match targets {
1076                ReadyCardinalityCountTargets::Digests(digests) => digests
1077                    .iter()
1078                    .map(|digest| {
1079                        schema
1080                            .cardinality_count(header.slot(), header.generation(), *digest)
1081                            .map(|count| count.unwrap_or(0))
1082                    })
1083                    .collect::<Result<Vec<_>, _>>()?,
1084                ReadyCardinalityCountTargets::UserIndexPrefixes(keys) => keys
1085                    .iter()
1086                    .map(|key| {
1087                        let digest = CardinalityCountDigest::for_user_index_prefix(
1088                            key.index_id(),
1089                            key.prefix_components(),
1090                        )?;
1091                        schema
1092                            .cardinality_count(header.slot(), header.generation(), digest)
1093                            .map(|count| count.unwrap_or(0))
1094                    })
1095                    .collect::<Result<Vec<_>, _>>()?,
1096            };
1097            Ok(Some(counts))
1098        })
1099    }
1100
1101    /// Return the explicit lifecycle state of the bound index store.
1102    #[must_use]
1103    pub(in crate::db) fn index_state(&self) -> IndexState {
1104        self.with_index(IndexStore::state)
1105    }
1106
1107    /// Return the monotonic physical access-readiness revision.
1108    pub(in crate::db) fn access_state_revision(&self) -> Result<u64, crate::error::InternalError> {
1109        self.journal.map_or_else(
1110            || Ok(self.with_index(IndexStore::access_state_revision)),
1111            |journal| journal.with_borrow(JournalTailStore::access_state_revision),
1112        )
1113    }
1114
1115    /// Mark the bound index store as Building.
1116    pub(in crate::db) fn mark_index_building(&self) -> Result<(), crate::error::InternalError> {
1117        self.set_index_state(IndexState::Building)
1118    }
1119
1120    /// Mark the bound index store as Ready.
1121    pub(in crate::db) fn mark_index_ready(&self) -> Result<(), crate::error::InternalError> {
1122        self.set_index_state(IndexState::Ready)
1123    }
1124
1125    fn set_index_state(&self, state: IndexState) -> Result<(), crate::error::InternalError> {
1126        if self.index_state() == state {
1127            return Ok(());
1128        }
1129        let revision = self.journal.map_or_else(
1130            || {
1131                self.with_index(IndexStore::access_state_revision)
1132                    .checked_add(1)
1133                    .ok_or_else(crate::error::InternalError::store_invariant)
1134            },
1135            |journal| journal.with_borrow_mut(JournalTailStore::advance_access_state_revision),
1136        )?;
1137        self.with_index_mut(|index| index.set_access_state(state, revision));
1138        Ok(())
1139    }
1140
1141    /// Return the raw row-store accessor.
1142    #[must_use]
1143    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
1144        self.data
1145    }
1146
1147    /// Return the raw index-store accessor.
1148    #[must_use]
1149    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
1150        self.index
1151    }
1152
1153    /// Return the raw schema-store accessor.
1154    #[must_use]
1155    pub const fn schema_store(&self) -> &'static LocalKey<RefCell<SchemaStore>> {
1156        self.schema
1157    }
1158
1159    /// Return the raw journal-tail store accessor when this store is journaled.
1160    #[must_use]
1161    pub const fn journal_tail_store(&self) -> Option<&'static LocalKey<RefCell<JournalTailStore>>> {
1162        self.journal
1163    }
1164
1165    /// Return the data-memory allocation identity when generated wiring
1166    /// supplied it.
1167    #[must_use]
1168    pub const fn data_allocation(&self) -> Option<StoreAllocationIdentity> {
1169        self.allocations.data()
1170    }
1171
1172    /// Return the index-memory allocation identity when generated wiring
1173    /// supplied it.
1174    #[must_use]
1175    pub const fn index_allocation(&self) -> Option<StoreAllocationIdentity> {
1176        self.allocations.index()
1177    }
1178
1179    /// Return the schema-memory allocation identity when generated wiring
1180    /// supplied it.
1181    #[must_use]
1182    pub const fn schema_allocation(&self) -> Option<StoreAllocationIdentity> {
1183        self.allocations.schema()
1184    }
1185
1186    /// Return the journal-tail allocation identity when generated wiring
1187    /// supplied it.
1188    #[must_use]
1189    pub const fn journal_allocation(&self) -> Option<StoreAllocationIdentity> {
1190        self.allocations.journal()
1191    }
1192
1193    /// Return this store's complete allocation identity bundle.
1194    #[must_use]
1195    pub(in crate::db) const fn allocation_identities(&self) -> StoreAllocationIdentities {
1196        self.allocations
1197    }
1198
1199    /// Return this store's explicit runtime storage capabilities.
1200    #[must_use]
1201    pub const fn storage_capabilities(&self) -> StoreRuntimeStorageCapabilities {
1202        self.capabilities
1203    }
1204}
1205
1206fn apply_visible_cardinality_delta(base: u64, delta: i64) -> Option<u64> {
1207    if delta >= 0 {
1208        base.checked_add(u64::try_from(delta).ok()?)
1209    } else {
1210        base.checked_sub(delta.unsigned_abs())
1211    }
1212}
1213
1214fn checked_cardinality_sum(counts: &[u64]) -> Option<u64> {
1215    counts
1216        .iter()
1217        .try_fold(0_u64, |total, count| total.checked_add(*count))
1218}
1219
1220// Exhaustive cache-retention coverage; new owned fields require accounting.
1221crate::retained::retained_copy!(ExactPrefixCardinalityLifecycleStamp);