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