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