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    /// Build one stable allocation identity descriptor.
361    #[must_use]
362    pub const fn new(memory_id: u8, stable_key: &'static str) -> Self {
363        Self {
364            memory_id,
365            stable_key,
366        }
367    }
368
369    /// Stable-memory manager ID.
370    #[must_use]
371    pub const fn memory_id(self) -> u8 {
372        self.memory_id
373    }
374
375    /// Durable stable-memory key.
376    #[must_use]
377    pub const fn stable_key(self) -> &'static str {
378        self.stable_key
379    }
380}
381
382///
383/// StoreAllocationIdentities
384///
385/// Durable allocation identities for one logical store's data, index, and
386/// schema memories.
387///
388
389#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
390pub struct StoreAllocationIdentities {
391    data: Option<StoreAllocationIdentity>,
392    index: Option<StoreAllocationIdentity>,
393    schema: Option<StoreAllocationIdentity>,
394    journal: Option<StoreAllocationIdentity>,
395}
396
397impl StoreAllocationIdentities {
398    /// Build an absent allocation identity bundle.
399    #[must_use]
400    pub const fn absent() -> Self {
401        Self {
402            data: None,
403            index: None,
404            schema: None,
405            journal: None,
406        }
407    }
408
409    /// Build one journaled cached-stable allocation identity bundle.
410    #[must_use]
411    pub const fn new_journaled(
412        data: StoreAllocationIdentity,
413        index: StoreAllocationIdentity,
414        schema: StoreAllocationIdentity,
415        journal: StoreAllocationIdentity,
416    ) -> Self {
417        Self {
418            data: Some(data),
419            index: Some(index),
420            schema: Some(schema),
421            journal: Some(journal),
422        }
423    }
424
425    /// Return data-memory allocation identity.
426    #[must_use]
427    pub const fn data(self) -> Option<StoreAllocationIdentity> {
428        self.data
429    }
430
431    /// Return index-memory allocation identity.
432    #[must_use]
433    pub const fn index(self) -> Option<StoreAllocationIdentity> {
434        self.index
435    }
436
437    /// Return schema-memory allocation identity.
438    #[must_use]
439    pub const fn schema(self) -> Option<StoreAllocationIdentity> {
440        self.schema
441    }
442
443    /// Return journal-tail allocation identity.
444    #[must_use]
445    pub const fn journal(self) -> Option<StoreAllocationIdentity> {
446        self.journal
447    }
448
449    /// Return the allocation capability represented by this triplet, or
450    /// `None` if the triplet is partially populated and therefore invalid.
451    #[must_use]
452    pub const fn allocation_identity_capability(self) -> Option<StoreAllocationIdentityCapability> {
453        match (self.data, self.index, self.schema) {
454            (Some(_), Some(_), Some(_)) => Some(StoreAllocationIdentityCapability::Present),
455            (None, None, None) if self.journal.is_none() => {
456                Some(StoreAllocationIdentityCapability::Absent)
457            }
458            _ => None,
459        }
460    }
461
462    /// Return whether this allocation shape matches the concrete storage
463    /// capability descriptor.
464    #[must_use]
465    pub const fn matches_storage_capabilities(
466        self,
467        capabilities: StoreRuntimeStorageCapabilities,
468    ) -> bool {
469        match capabilities.storage_mode() {
470            StoreRuntimeStorageMode::Heap => {
471                self.data.is_none()
472                    && self.index.is_none()
473                    && self.schema.is_none()
474                    && self.journal.is_none()
475            }
476            StoreRuntimeStorageMode::Journaled => {
477                self.data.is_some()
478                    && self.index.is_some()
479                    && self.schema.is_some()
480                    && self.journal.is_some()
481            }
482        }
483    }
484}
485
486impl StoreHandle {
487    /// Build a store handle with an explicit allocation identity decision.
488    #[must_use]
489    pub const fn new(
490        data: &'static LocalKey<RefCell<DataStore>>,
491        index: &'static LocalKey<RefCell<IndexStore>>,
492        schema: &'static LocalKey<RefCell<SchemaStore>>,
493        allocations: StoreAllocationIdentities,
494        capabilities: StoreRuntimeStorageCapabilities,
495    ) -> Self {
496        Self {
497            data,
498            index,
499            schema,
500            journal: None,
501            allocations,
502            cardinality_allocation: None,
503            capabilities,
504        }
505    }
506
507    /// Build a journaled store handle with an explicit journal-tail store.
508    #[must_use]
509    pub fn new_journaled(
510        data: &'static LocalKey<RefCell<DataStore>>,
511        index: &'static LocalKey<RefCell<IndexStore>>,
512        schema: &'static LocalKey<RefCell<SchemaStore>>,
513        journal: &'static LocalKey<RefCell<JournalTailStore>>,
514        allocations: StoreAllocationIdentities,
515        capabilities: StoreRuntimeStorageCapabilities,
516    ) -> Self {
517        let cardinality_allocation = CardinalityStoreAllocationIdentity::derive(allocations).ok();
518        Self {
519            data,
520            index,
521            schema,
522            journal: Some(journal),
523            allocations,
524            cardinality_allocation,
525            capabilities,
526        }
527    }
528
529    /// Borrow the row store immutably.
530    pub fn with_data<R>(&self, f: impl FnOnce(&DataStore) -> R) -> R {
531        self.data.with_borrow(f)
532    }
533
534    /// Borrow the row store mutably.
535    pub fn with_data_mut<R>(&self, f: impl FnOnce(&mut DataStore) -> R) -> R {
536        self.data.with_borrow_mut(f)
537    }
538
539    /// Borrow the index store immutably.
540    pub fn with_index<R>(&self, f: impl FnOnce(&IndexStore) -> R) -> R {
541        self.index.with_borrow(f)
542    }
543
544    /// Borrow the index store mutably.
545    pub fn with_index_mut<R>(&self, f: impl FnOnce(&mut IndexStore) -> R) -> R {
546        self.index.with_borrow_mut(f)
547    }
548
549    /// Borrow the schema store immutably.
550    pub fn with_schema<R>(&self, f: impl FnOnce(&SchemaStore) -> R) -> R {
551        self.schema.with_borrow(f)
552    }
553
554    /// Borrow the schema store mutably.
555    pub fn with_schema_mut<R>(&self, f: impl FnOnce(&mut SchemaStore) -> R) -> R {
556        self.schema.with_borrow_mut(f)
557    }
558
559    /// Return exact visible entity cardinality through the store's canonical proof boundary.
560    #[must_use]
561    pub(in crate::db) fn exact_entity_count(&self, entity: EntityTag) -> Option<u64> {
562        if self.journal.is_none() {
563            return self.with_data(|store| store.exact_entity_count(entity));
564        }
565        let delta = self.with_data(|store| store.exact_entity_cardinality_delta(entity))?;
566        let digest = CardinalityCountDigest::for_entity(entity);
567        let base = self
568            .ready_cardinality_counts(&[digest], |authority| authority.accepts_entity(entity))
569            .ok()
570            .flatten()?
571            .into_iter()
572            .next()?;
573        apply_visible_cardinality_delta(base, delta)
574    }
575
576    /// Return exact visible cardinality for one accepted user-index prefix.
577    #[must_use]
578    pub(in crate::db) fn exact_user_index_prefix_count(
579        &self,
580        data_generation: u64,
581        key_kind: IndexKeyKind,
582        index_id: IndexId,
583        components: &[Vec<u8>],
584    ) -> Option<u64> {
585        self.exact_user_index_prefix_counts(data_generation, key_kind, index_id, [components])?
586            .into_iter()
587            .next()
588    }
589
590    /// Return exact visible counts for prefixes on one accepted user index.
591    #[must_use]
592    pub(in crate::db) fn exact_user_index_prefix_counts<'a>(
593        &self,
594        data_generation: u64,
595        key_kind: IndexKeyKind,
596        index_id: IndexId,
597        component_prefixes: impl IntoIterator<Item = &'a [Vec<u8>]>,
598    ) -> Option<Vec<u64>> {
599        let component_prefixes = component_prefixes.into_iter().collect::<Vec<_>>();
600        if key_kind != IndexKeyKind::User {
601            return None;
602        }
603        if self.journal.is_none() {
604            return self.with_index(|store| {
605                component_prefixes
606                    .iter()
607                    .map(|components| {
608                        store.exact_prefix_cardinality(
609                            data_generation,
610                            key_kind,
611                            index_id,
612                            components,
613                        )
614                    })
615                    .collect()
616            });
617        }
618        let deltas = self.with_index(|store| {
619            component_prefixes
620                .iter()
621                .map(|components| {
622                    store.exact_prefix_cardinality_delta(key_kind, index_id, components)
623                })
624                .collect::<Option<Vec<_>>>()
625        })?;
626        let digests = component_prefixes
627            .iter()
628            .map(|components| {
629                CardinalityCountDigest::for_user_index_prefix(index_id, components).ok()
630            })
631            .collect::<Option<Vec<_>>>()?;
632        let bases = self
633            .ready_cardinality_counts(&digests, |authority| {
634                component_prefixes.iter().all(|components| {
635                    authority.accepts_user_index_prefix(index_id, components.len())
636                })
637            })
638            .ok()
639            .flatten()?;
640        bases
641            .into_iter()
642            .zip(deltas)
643            .map(|(base, delta)| apply_visible_cardinality_delta(base, delta))
644            .collect()
645    }
646
647    /// Return exact visible counts for accepted prefix keys across user indexes.
648    #[must_use]
649    pub(in crate::db) fn exact_user_index_prefix_key_counts(
650        &self,
651        data_generation: u64,
652        keys: &[UserIndexPrefixCardinalityKey],
653    ) -> Option<Vec<u64>> {
654        self.exact_user_index_prefix_key_counts_with_authority(data_generation, keys, None)
655    }
656
657    /// Return exact prefix counts through an accepted root admitted in this request.
658    #[must_use]
659    pub(in crate::db) fn exact_user_index_prefix_key_counts_for_admitted_root(
660        &self,
661        database_incarnation: DatabaseIncarnationId,
662        accepted_root: CardinalityAcceptedRootIdentity,
663        data_generation: u64,
664        keys: &[UserIndexPrefixCardinalityKey],
665    ) -> Option<Vec<u64>> {
666        self.exact_user_index_prefix_key_counts_with_authority(
667            data_generation,
668            keys,
669            Some((database_incarnation, accepted_root)),
670        )
671    }
672
673    /// Return complete exact counts or one opaque current availability stamp.
674    ///
675    /// This boundary knows nothing about plans, caches, bindings, cursors, or
676    /// selected winners. It only proves current accepted-prefix evidence.
677    #[must_use]
678    pub(in crate::db) fn exact_user_index_prefix_evidence_for_admitted_root(
679        &self,
680        database_incarnation: DatabaseIncarnationId,
681        accepted_root: CardinalityAcceptedRootIdentity,
682        keys: &[UserIndexPrefixCardinalityKey],
683    ) -> ExactUserIndexPrefixEvidence {
684        let data_generation = self.with_data(DataStore::generation);
685        if let Some(counts) = self.exact_user_index_prefix_key_counts_for_admitted_root(
686            database_incarnation,
687            accepted_root,
688            data_generation,
689            keys,
690        ) {
691            return ExactUserIndexPrefixEvidence::Exact(counts);
692        }
693
694        ExactUserIndexPrefixEvidence::Unavailable(
695            self.exact_user_index_prefix_evidence_lifecycle_stamp(),
696        )
697    }
698
699    /// Return the cheap availability identity without reading any prefix count.
700    #[must_use]
701    pub(in crate::db) fn exact_user_index_prefix_evidence_lifecycle_stamp(
702        &self,
703    ) -> ExactPrefixCardinalityLifecycleStamp {
704        if self.journal.is_none() {
705            return ExactPrefixCardinalityLifecycleStamp(
706                ExactPrefixCardinalityLifecycleIdentity::Volatile,
707            );
708        }
709        if self.cardinality_allocation.is_none() {
710            return ExactPrefixCardinalityLifecycleStamp(
711                ExactPrefixCardinalityLifecycleIdentity::MissingDurableAuthority,
712            );
713        }
714        let delta_watermark = self.with_index(IndexStore::exact_prefix_cardinality_delta_watermark);
715        match self.with_schema(SchemaStore::cardinality_generation_lifecycle_control) {
716            Ok((header, cursor_present)) => ExactPrefixCardinalityLifecycleStamp(
717                ExactPrefixCardinalityLifecycleIdentity::Journaled {
718                    header_digest: header.map(|header| {
719                        let mut hasher = Sha256::new();
720                        hasher.update(b"icydb.cardinality-lifecycle-stamp.v1");
721                        hasher.update(header.encode());
722                        hasher.finalize().into()
723                    }),
724                    cursor_present,
725                    delta_watermark,
726                },
727            ),
728            Err(_) => ExactPrefixCardinalityLifecycleStamp(
729                ExactPrefixCardinalityLifecycleIdentity::Corrupt,
730            ),
731        }
732    }
733
734    fn exact_user_index_prefix_key_counts_with_authority(
735        &self,
736        data_generation: u64,
737        keys: &[UserIndexPrefixCardinalityKey],
738        admitted: Option<(DatabaseIncarnationId, CardinalityAcceptedRootIdentity)>,
739    ) -> Option<Vec<u64>> {
740        if keys.is_empty() {
741            return None;
742        }
743        if self.journal.is_none() {
744            return self.with_index(|store| {
745                keys.iter()
746                    .map(|key| {
747                        store.exact_prefix_cardinality(
748                            data_generation,
749                            IndexKeyKind::User,
750                            key.index_id(),
751                            key.prefix_components(),
752                        )
753                    })
754                    .collect()
755            });
756        }
757        let (delta_watermark, deltas) = self.with_index(|store| {
758            let watermark = store.exact_prefix_cardinality_delta_watermark()?;
759            keys.iter()
760                .map(|key| {
761                    store.exact_prefix_cardinality_delta(
762                        IndexKeyKind::User,
763                        key.index_id(),
764                        key.prefix_components(),
765                    )
766                })
767                .collect::<Option<Vec<_>>>()
768                .map(|deltas| (watermark, deltas))
769        })?;
770        let accepts = |authority: &CardinalityBuildAuthority| {
771            keys.iter().all(|key| {
772                authority.accepts_user_index_prefix(key.index_id(), key.prefix_components().len())
773            })
774        };
775        let bases = match admitted {
776            Some((database_incarnation, accepted_root)) => self
777                .ready_cardinality_counts_for_source(
778                    ReadyCardinalitySource::Admitted {
779                        database_incarnation,
780                        accepted_root,
781                        fold_watermark: delta_watermark,
782                    },
783                    ReadyCardinalityCountTargets::UserIndexPrefixes(keys),
784                    accepts,
785                ),
786            None => self.ready_cardinality_counts_for_targets(
787                ReadyCardinalityCountTargets::UserIndexPrefixes(keys),
788                accepts,
789            ),
790        }
791        .ok()
792        .flatten()?;
793        bases
794            .into_iter()
795            .zip(deltas)
796            .map(|(base, delta)| apply_visible_cardinality_delta(base, delta))
797            .collect()
798    }
799
800    /// Prove that one accepted user-index prefix family has a synchronized Ready generation.
801    ///
802    /// This does not expose or infer count values. Callers may retain every
803    /// branch conservatively, but must use the exact count methods above before
804    /// pruning any branch as empty.
805    #[must_use]
806    pub(in crate::db) fn user_index_prefix_family_has_ready_generation<'a, I>(
807        &self,
808        database_incarnation: DatabaseIncarnationId,
809        accepted_root: CardinalityAcceptedRootIdentity,
810        data_generation: u64,
811        key_kind: IndexKeyKind,
812        index_id: IndexId,
813        component_prefixes: I,
814    ) -> bool
815    where
816        I: Clone + IntoIterator<Item = &'a [Vec<u8>]>,
817    {
818        if key_kind != IndexKeyKind::User || component_prefixes.clone().into_iter().next().is_none()
819        {
820            return false;
821        }
822        if self.journal.is_none() {
823            return self.with_index(|store| {
824                component_prefixes.clone().into_iter().all(|components| {
825                    store
826                        .exact_prefix_cardinality(data_generation, key_kind, index_id, components)
827                        .is_some()
828                })
829            });
830        }
831        let delta_watermark = self.with_index(|store| {
832            let watermark = store.exact_prefix_cardinality_delta_watermark()?;
833            component_prefixes
834                .clone()
835                .into_iter()
836                .all(|components| {
837                    store
838                        .exact_prefix_cardinality_delta(key_kind, index_id, components)
839                        .is_some()
840                })
841                .then_some(watermark)
842        });
843        delta_watermark.is_some_and(|watermark| {
844            self.ready_cardinality_counts_for_source(
845                ReadyCardinalitySource::Admitted {
846                    database_incarnation,
847                    accepted_root,
848                    fold_watermark: watermark,
849                },
850                ReadyCardinalityCountTargets::Digests(&[]),
851                |authority| {
852                    component_prefixes.into_iter().all(|components| {
853                        authority.accepts_user_index_prefix(index_id, components.len())
854                    })
855                },
856            )
857            .is_ok_and(|counts| counts.is_some())
858        })
859    }
860
861    /// Sum exact visible counts for prefixes on one accepted user index.
862    #[must_use]
863    pub(in crate::db) fn exact_user_index_prefix_count_sum<'a>(
864        &self,
865        data_generation: u64,
866        key_kind: IndexKeyKind,
867        index_id: IndexId,
868        component_prefixes: impl IntoIterator<Item = &'a [Vec<u8>]>,
869        stop_after: Option<u64>,
870    ) -> Option<u64> {
871        let component_prefixes = component_prefixes.into_iter().collect::<Vec<_>>();
872        if self.journal.is_none() {
873            return self.with_index(|store| {
874                store.exact_prefix_cardinality_sum(
875                    data_generation,
876                    key_kind,
877                    index_id,
878                    component_prefixes.iter().copied(),
879                    stop_after,
880                )
881            });
882        }
883        let counts = self.exact_user_index_prefix_counts(
884            data_generation,
885            key_kind,
886            index_id,
887            component_prefixes.iter().copied(),
888        )?;
889        let mut total = 0_u64;
890        for count in counts {
891            total = total.checked_add(count)?;
892            if stop_after.is_some_and(|required| total >= required) {
893                break;
894            }
895        }
896        Some(total)
897    }
898
899    /// Enumerate one bounded child-prefix family proven complete by exact parent/child totals.
900    #[must_use]
901    pub(in crate::db) fn exact_user_index_child_prefixes_for_parent_set<'a>(
902        &self,
903        data_generation: u64,
904        index_id: IndexId,
905        parent_prefixes: impl IntoIterator<Item = &'a [Vec<u8>]>,
906        total_cap: usize,
907    ) -> Option<Vec<Vec<Vec<u8>>>> {
908        let mut parent_prefixes = parent_prefixes
909            .into_iter()
910            .map(<[Vec<u8>]>::to_vec)
911            .collect::<Vec<_>>();
912        if parent_prefixes.iter().any(Vec::is_empty) {
913            return None;
914        }
915        icydb_schema::compact_sort_unstable_by(&mut parent_prefixes, Ord::cmp);
916        parent_prefixes.dedup();
917        let child_prefixes = self.with_index(|store| {
918            store.exact_child_prefixes_for_parent_set(
919                data_generation,
920                IndexKeyKind::User,
921                index_id,
922                parent_prefixes.iter().map(Vec::as_slice),
923                total_cap,
924            )
925        })?;
926        if self.journal.is_none() {
927            return Some(child_prefixes);
928        }
929        let parent_count = parent_prefixes.len();
930        let counts = self.exact_user_index_prefix_counts(
931            data_generation,
932            IndexKeyKind::User,
933            index_id,
934            parent_prefixes
935                .iter()
936                .chain(&child_prefixes)
937                .map(Vec::as_slice),
938        )?;
939        let (parent_counts, child_counts) = counts.split_at(parent_count);
940        let parent_total = checked_cardinality_sum(parent_counts)?;
941        let child_total = checked_cardinality_sum(child_counts)?;
942        (parent_total == child_total).then_some(child_prefixes)
943    }
944
945    fn ready_cardinality_counts(
946        &self,
947        digests: &[CardinalityCountDigest],
948        accepts: impl FnOnce(&CardinalityBuildAuthority) -> bool,
949    ) -> Result<Option<Vec<u64>>, InternalError> {
950        self.ready_cardinality_counts_for_targets(
951            ReadyCardinalityCountTargets::Digests(digests),
952            accepts,
953        )
954    }
955
956    fn ready_cardinality_counts_for_targets(
957        &self,
958        targets: ReadyCardinalityCountTargets<'_>,
959        accepts: impl FnOnce(&CardinalityBuildAuthority) -> bool,
960    ) -> Result<Option<Vec<u64>>, InternalError> {
961        let incarnation = database_incarnation_id()?;
962        self.ready_cardinality_counts_for_source(
963            ReadyCardinalitySource::Current {
964                database_incarnation: incarnation,
965            },
966            targets,
967            accepts,
968        )
969    }
970
971    fn ready_cardinality_counts_for_source(
972        &self,
973        source: ReadyCardinalitySource,
974        targets: ReadyCardinalityCountTargets<'_>,
975        accepts: impl FnOnce(&CardinalityBuildAuthority) -> bool,
976    ) -> Result<Option<Vec<u64>>, InternalError> {
977        let Some(journal) = self.journal else {
978            return Ok(None);
979        };
980        let Some(allocation) = self.cardinality_allocation else {
981            return Ok(None);
982        };
983        let (incarnation, accepted_root, watermark) = match source {
984            ReadyCardinalitySource::Current {
985                database_incarnation,
986            } => (
987                database_incarnation,
988                None,
989                journal.with_borrow(JournalTailStore::fold_watermark)?,
990            ),
991            ReadyCardinalitySource::Admitted {
992                database_incarnation,
993                accepted_root,
994                fold_watermark,
995            } => (database_incarnation, Some(accepted_root), fold_watermark),
996        };
997        self.with_schema(|schema| {
998            let (header, cursor) = schema.cardinality_generation_control()?;
999            let Some(header) = header else {
1000                return Ok(None);
1001            };
1002            if header.state() != CardinalityGenerationState::Ready || cursor.is_some() {
1003                return Ok(None);
1004            }
1005            let authority = match accepted_root {
1006                Some(root) => CardinalityBuildAuthority::derive_for_admitted_consumer_root(
1007                    schema,
1008                    incarnation,
1009                    allocation,
1010                    root,
1011                    watermark,
1012                )?,
1013                None => CardinalityBuildAuthority::derive_for_current_consumer(
1014                    schema,
1015                    incarnation,
1016                    allocation,
1017                    watermark,
1018                )?,
1019            };
1020            let Some(authority) = authority else {
1021                return Ok(None);
1022            };
1023            if !accepts(&authority) {
1024                return Ok(None);
1025            }
1026            if header.validate_source(authority.source()).is_err() {
1027                return Ok(None);
1028            }
1029            if targets.len() != 0 && schema.cardinality_count_slot_is_empty(header.slot())? {
1030                return Ok(Some(vec![0; targets.len()]));
1031            }
1032            let counts = match targets {
1033                ReadyCardinalityCountTargets::Digests(digests) => digests
1034                    .iter()
1035                    .map(|digest| {
1036                        schema
1037                            .cardinality_count(header.slot(), header.generation(), *digest)
1038                            .map(|count| count.unwrap_or(0))
1039                    })
1040                    .collect::<Result<Vec<_>, _>>()?,
1041                ReadyCardinalityCountTargets::UserIndexPrefixes(keys) => keys
1042                    .iter()
1043                    .map(|key| {
1044                        let digest = CardinalityCountDigest::for_user_index_prefix(
1045                            key.index_id(),
1046                            key.prefix_components(),
1047                        )?;
1048                        schema
1049                            .cardinality_count(header.slot(), header.generation(), digest)
1050                            .map(|count| count.unwrap_or(0))
1051                    })
1052                    .collect::<Result<Vec<_>, _>>()?,
1053            };
1054            Ok(Some(counts))
1055        })
1056    }
1057
1058    /// Return the explicit lifecycle state of the bound index store.
1059    #[must_use]
1060    pub(in crate::db) fn index_state(&self) -> IndexState {
1061        self.with_index(IndexStore::state)
1062    }
1063
1064    /// Return the monotonic physical access-readiness revision.
1065    pub(in crate::db) fn access_state_revision(&self) -> Result<u64, crate::error::InternalError> {
1066        self.journal.map_or_else(
1067            || Ok(self.with_index(IndexStore::access_state_revision)),
1068            |journal| journal.with_borrow(JournalTailStore::access_state_revision),
1069        )
1070    }
1071
1072    /// Mark the bound index store as Building.
1073    pub(in crate::db) fn mark_index_building(&self) -> Result<(), crate::error::InternalError> {
1074        self.set_index_state(IndexState::Building)
1075    }
1076
1077    /// Mark the bound index store as Ready.
1078    pub(in crate::db) fn mark_index_ready(&self) -> Result<(), crate::error::InternalError> {
1079        self.set_index_state(IndexState::Ready)
1080    }
1081
1082    fn set_index_state(&self, state: IndexState) -> Result<(), crate::error::InternalError> {
1083        if self.index_state() == state {
1084            return Ok(());
1085        }
1086        let revision = self.journal.map_or_else(
1087            || {
1088                self.with_index(IndexStore::access_state_revision)
1089                    .checked_add(1)
1090                    .ok_or_else(crate::error::InternalError::store_invariant)
1091            },
1092            |journal| journal.with_borrow_mut(JournalTailStore::advance_access_state_revision),
1093        )?;
1094        self.with_index_mut(|index| index.set_access_state(state, revision));
1095        Ok(())
1096    }
1097
1098    /// Return the raw row-store accessor.
1099    #[must_use]
1100    pub const fn data_store(&self) -> &'static LocalKey<RefCell<DataStore>> {
1101        self.data
1102    }
1103
1104    /// Return the raw index-store accessor.
1105    #[must_use]
1106    pub const fn index_store(&self) -> &'static LocalKey<RefCell<IndexStore>> {
1107        self.index
1108    }
1109
1110    /// Return the raw schema-store accessor.
1111    #[must_use]
1112    pub const fn schema_store(&self) -> &'static LocalKey<RefCell<SchemaStore>> {
1113        self.schema
1114    }
1115
1116    /// Return the raw journal-tail store accessor when this store is journaled.
1117    #[must_use]
1118    pub const fn journal_tail_store(&self) -> Option<&'static LocalKey<RefCell<JournalTailStore>>> {
1119        self.journal
1120    }
1121
1122    /// Return the data-memory allocation identity when generated wiring
1123    /// supplied it.
1124    #[must_use]
1125    pub const fn data_allocation(&self) -> Option<StoreAllocationIdentity> {
1126        self.allocations.data()
1127    }
1128
1129    /// Return the index-memory allocation identity when generated wiring
1130    /// supplied it.
1131    #[must_use]
1132    pub const fn index_allocation(&self) -> Option<StoreAllocationIdentity> {
1133        self.allocations.index()
1134    }
1135
1136    /// Return the schema-memory allocation identity when generated wiring
1137    /// supplied it.
1138    #[must_use]
1139    pub const fn schema_allocation(&self) -> Option<StoreAllocationIdentity> {
1140        self.allocations.schema()
1141    }
1142
1143    /// Return the journal-tail allocation identity when generated wiring
1144    /// supplied it.
1145    #[must_use]
1146    pub const fn journal_allocation(&self) -> Option<StoreAllocationIdentity> {
1147        self.allocations.journal()
1148    }
1149
1150    /// Return this store's complete allocation identity bundle.
1151    #[must_use]
1152    pub(in crate::db) const fn allocation_identities(&self) -> StoreAllocationIdentities {
1153        self.allocations
1154    }
1155
1156    /// Return this store's explicit runtime storage capabilities.
1157    #[must_use]
1158    pub const fn storage_capabilities(&self) -> StoreRuntimeStorageCapabilities {
1159        self.capabilities
1160    }
1161}
1162
1163fn apply_visible_cardinality_delta(base: u64, delta: i64) -> Option<u64> {
1164    if delta >= 0 {
1165        base.checked_add(u64::try_from(delta).ok()?)
1166    } else {
1167        base.checked_sub(delta.unsigned_abs())
1168    }
1169}
1170
1171fn checked_cardinality_sum(counts: &[u64]) -> Option<u64> {
1172    counts
1173        .iter()
1174        .try_fold(0_u64, |total, count| total.checked_add(*count))
1175}
1176
1177// Exhaustive cache-retention coverage; new owned fields require accounting.
1178crate::retained::retained_copy!(ExactPrefixCardinalityLifecycleStamp);