Skip to main content

icydb_core/db/schema/
store.rs

1//! Module: db::schema::store
2//! Responsibility: stable BTreeMap-backed schema metadata persistence.
3//! Does not own: reconciliation policy, typed snapshot encoding, or generated proposal construction.
4//! Boundary: provides the third per-store stable memory alongside row and index stores.
5
6use crate::{
7    db::{
8        codec::{
9            finalize_hash_sha256, new_hash_sha256, write_hash_len_u32, write_hash_str_u32,
10            write_hash_tag_u8, write_hash_u32, write_hash_u64,
11        },
12        commit::CommitSchemaFingerprint,
13        direction::Direction,
14        ordered_overlay::{OrderedOverlayEntry, OrderedOverlayVisit, visit_ordered_overlay},
15        schema::{
16            AcceptedFieldKind, AcceptedSchemaSnapshot, PersistedIndexKeyItemSnapshot,
17            PersistedIndexKeySnapshot, PersistedSchemaSnapshot, SchemaVersion,
18            accepted_schema_cache_fingerprint,
19            accepted_schema_cache_fingerprint_for_persisted_snapshot,
20            accepted_schema_cache_fingerprint_method_version, decode_persisted_schema_snapshot,
21            encode_persisted_schema_snapshot,
22            enum_catalog::{
23                AcceptedEnumCatalogHandle, AcceptedSchemaAuthority, AcceptedSchemaPublicationError,
24                AcceptedSchemaRevision, AcceptedSchemaRevisionBundle, AcceptedSchemaRootSelection,
25                AcceptedStoreCatalogScope, CandidateSchemaRevision,
26                decode_verified_accepted_schema_revision_bundle,
27                prepare_accepted_schema_root_publication, select_current_accepted_schema_root,
28            },
29            schema_snapshot_integrity_detail,
30        },
31    },
32    error::InternalError,
33    types::EntityTag,
34};
35use ic_stable_structures::{
36    BTreeMap as StableBTreeMap, DefaultMemoryImpl, Storable, memory_manager::VirtualMemory,
37    storable::Bound as StorableBound,
38};
39use sha2::Digest;
40use std::borrow::Cow;
41#[cfg(test)]
42use std::cell::Cell;
43use std::cell::{OnceCell, Ref, RefCell};
44use std::collections::{BTreeMap as StdBTreeMap, BTreeSet};
45use std::convert::Infallible;
46use std::ops::Bound as RangeBound;
47
48const SCHEMA_KEY_BYTES_USIZE: usize = 16;
49const SCHEMA_KEY_BYTES: u32 = 16;
50const SCHEMA_KEY_NAMESPACE_ENTITY_SNAPSHOT: u8 = 0;
51const SCHEMA_KEY_NAMESPACE_ACCEPTED_BUNDLE: u8 = 1;
52const SCHEMA_KEY_NAMESPACE_ACCEPTED_ROOT: u8 = 2;
53pub(in crate::db) const MAX_SCHEMA_SNAPSHOT_BYTES: u32 = 512 * 1024;
54// Every role exposes the sole current method version while its separate domain
55// tag keeps data, index, and full-catalog fingerprint inputs disjoint.
56const SCHEMA_STORE_FINGERPRINT_METHOD_VERSION: u8 = 1;
57const SCHEMA_STORE_CATALOG_FINGERPRINT_DOMAIN: u8 = 1;
58const SCHEMA_STORE_DATA_ALLOCATION_FINGERPRINT_DOMAIN: u8 = 2;
59const SCHEMA_STORE_INDEX_ALLOCATION_FINGERPRINT_DOMAIN: u8 = 3;
60const RAW_SCHEMA_SNAPSHOT_MAGIC: &[u8; 8] = b"ICYDBCAT";
61const RAW_SCHEMA_SNAPSHOT_VALUE_VERSION: u8 = 1;
62const RAW_SCHEMA_SNAPSHOT_HEADER_BYTES: usize = 25;
63
64#[cfg(test)]
65thread_local! {
66    static LATEST_RAW_SNAPSHOTS_BY_ENTITY_CALLS: Cell<u64> = const { Cell::new(0) };
67    static ACCEPTED_SCHEMA_BUNDLE_CACHE_MISSES: Cell<u64> = const { Cell::new(0) };
68}
69
70#[cfg(test)]
71pub(in crate::db) fn reset_latest_raw_snapshots_by_entity_call_count_for_tests() {
72    LATEST_RAW_SNAPSHOTS_BY_ENTITY_CALLS.with(|calls| calls.set(0));
73}
74
75#[cfg(test)]
76pub(in crate::db) fn latest_raw_snapshots_by_entity_call_count_for_tests() -> u64 {
77    LATEST_RAW_SNAPSHOTS_BY_ENTITY_CALLS.with(Cell::get)
78}
79
80#[cfg(test)]
81fn reset_accepted_schema_bundle_cache_miss_count_for_tests() {
82    ACCEPTED_SCHEMA_BUNDLE_CACHE_MISSES.with(|misses| misses.set(0));
83}
84
85#[cfg(test)]
86fn accepted_schema_bundle_cache_miss_count_for_tests() -> u64 {
87    ACCEPTED_SCHEMA_BUNDLE_CACHE_MISSES.with(Cell::get)
88}
89
90///
91/// RawSchemaKey
92///
93/// Stable key for one persisted schema snapshot entry.
94/// It combines the entity tag and schema version so reconciliation can load
95/// concrete versions without depending on generated entity names.
96///
97
98#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
99struct RawSchemaKey([u8; SCHEMA_KEY_BYTES_USIZE]);
100
101impl RawSchemaKey {
102    /// Build the raw persisted key for one entity schema version.
103    #[must_use]
104    fn from_entity_version(entity: EntityTag, version: SchemaVersion) -> Self {
105        let mut out = [0u8; SCHEMA_KEY_BYTES_USIZE];
106        out[0] = SCHEMA_KEY_NAMESPACE_ENTITY_SNAPSHOT;
107        out[4..12].copy_from_slice(&entity.value().to_be_bytes());
108        out[12..].copy_from_slice(&version.get().to_be_bytes());
109
110        Self(out)
111    }
112
113    fn from_accepted_bundle(bundle_key: super::enum_catalog::AcceptedSchemaBundleKey) -> Self {
114        let mut out = [0u8; SCHEMA_KEY_BYTES_USIZE];
115        out[0] = SCHEMA_KEY_NAMESPACE_ACCEPTED_BUNDLE;
116        out[4..12].copy_from_slice(&bundle_key.get().to_be_bytes());
117        Self(out)
118    }
119
120    fn from_accepted_root_slot(slot: usize) -> Result<Self, InternalError> {
121        let slot = u32::try_from(slot).map_err(|_| InternalError::store_invariant())?;
122        if slot > 1 {
123            return Err(InternalError::store_invariant());
124        }
125        let mut out = [0u8; SCHEMA_KEY_BYTES_USIZE];
126        out[0] = SCHEMA_KEY_NAMESPACE_ACCEPTED_ROOT;
127        out[12..].copy_from_slice(&slot.to_be_bytes());
128        Ok(Self(out))
129    }
130
131    /// Return the entity tag encoded in this schema key.
132    #[must_use]
133    fn entity_tag(self) -> EntityTag {
134        let mut bytes = [0u8; size_of::<u64>()];
135        bytes.copy_from_slice(&self.0[4..12]);
136
137        EntityTag::new(u64::from_be_bytes(bytes))
138    }
139
140    /// Return the schema version encoded in this schema key.
141    #[must_use]
142    fn version(self) -> u32 {
143        let mut bytes = [0u8; size_of::<u32>()];
144        bytes.copy_from_slice(&self.0[12..]);
145
146        u32::from_be_bytes(bytes)
147    }
148
149    fn entity_range_bounds(entity: EntityTag) -> (RangeBound<Self>, RangeBound<Self>) {
150        (
151            RangeBound::Included(Self::from_entity_version(entity, SchemaVersion::new(0))),
152            RangeBound::Included(Self::from_entity_version(
153                entity,
154                SchemaVersion::new(u32::MAX),
155            )),
156        )
157    }
158
159    const fn all_entity_range_bounds() -> (RangeBound<Self>, RangeBound<Self>) {
160        let mut end = [u8::MAX; SCHEMA_KEY_BYTES_USIZE];
161        end[0] = SCHEMA_KEY_NAMESPACE_ENTITY_SNAPSHOT;
162        (
163            RangeBound::Included(Self([0; SCHEMA_KEY_BYTES_USIZE])),
164            RangeBound::Included(Self(end)),
165        )
166    }
167
168    #[cfg(test)]
169    const fn is_entity_snapshot(self) -> bool {
170        self.0[0] == SCHEMA_KEY_NAMESPACE_ENTITY_SNAPSHOT
171    }
172
173    const fn is_accepted_root(self) -> bool {
174        self.0[0] == SCHEMA_KEY_NAMESPACE_ACCEPTED_ROOT
175    }
176}
177
178impl Storable for RawSchemaKey {
179    fn to_bytes(&self) -> Cow<'_, [u8]> {
180        Cow::Borrowed(&self.0)
181    }
182
183    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
184        debug_assert_eq!(
185            bytes.len(),
186            SCHEMA_KEY_BYTES_USIZE,
187            "RawSchemaKey::from_bytes received unexpected byte length",
188        );
189
190        if bytes.len() != SCHEMA_KEY_BYTES_USIZE {
191            return Self([0u8; SCHEMA_KEY_BYTES_USIZE]);
192        }
193
194        let mut out = [0u8; SCHEMA_KEY_BYTES_USIZE];
195        out.copy_from_slice(bytes.as_ref());
196        Self(out)
197    }
198
199    fn into_bytes(self) -> Vec<u8> {
200        self.0.to_vec()
201    }
202
203    const BOUND: StorableBound = StorableBound::Bounded {
204        max_size: SCHEMA_KEY_BYTES,
205        is_fixed_size: true,
206    };
207}
208
209///
210/// RawSchemaSnapshot
211///
212/// Raw persisted value in the schema metadata store.
213///
214/// Entity snapshots carry this wrapper's identity header. Accepted catalog
215/// bundles and root slots are already-versioned control records and remain
216/// opaque here. Key-specific readers decide which representation is required.
217///
218
219#[derive(Clone, Debug, Eq, PartialEq)]
220struct RawSchemaSnapshot {
221    payload: Vec<u8>,
222    accepted_schema_fingerprint: Option<CommitSchemaFingerprint>,
223}
224
225impl RawSchemaSnapshot {
226    /// Encode one typed persisted-schema snapshot into a raw store payload.
227    fn from_persisted_snapshot(snapshot: &PersistedSchemaSnapshot) -> Result<Self, InternalError> {
228        validate_typed_schema_snapshot_for_store(snapshot)?;
229
230        let accepted_schema_fingerprint =
231            accepted_schema_cache_fingerprint_for_persisted_snapshot(snapshot)?;
232        let payload = encode_persisted_schema_snapshot(snapshot)?;
233
234        Ok(Self {
235            payload,
236            accepted_schema_fingerprint: Some(accepted_schema_fingerprint),
237        })
238    }
239
240    /// Store one already-versioned accepted-catalog control record.
241    #[must_use]
242    const fn from_encoded_control_record(payload: Vec<u8>) -> Self {
243        Self {
244            payload,
245            accepted_schema_fingerprint: None,
246        }
247    }
248
249    /// Build a framed entity snapshot around deliberately untrusted payload
250    /// bytes so decode-boundary tests can exercise current-format corruption.
251    #[cfg(test)]
252    #[must_use]
253    const fn from_unchecked_persisted_snapshot_payload(payload: Vec<u8>) -> Self {
254        Self {
255            payload,
256            accepted_schema_fingerprint: Some([0; size_of::<CommitSchemaFingerprint>()]),
257        }
258    }
259
260    /// Borrow the encoded schema snapshot payload.
261    #[must_use]
262    const fn as_bytes(&self) -> &[u8] {
263        self.payload.as_slice()
264    }
265
266    /// Consume the snapshot into its encoded payload bytes.
267    #[must_use]
268    fn into_bytes(self) -> Vec<u8> {
269        self.payload
270    }
271
272    /// Return the accepted schema identity fingerprint stored beside the raw
273    /// payload, without decoding the persisted snapshot.
274    fn accepted_schema_fingerprint(&self) -> Result<CommitSchemaFingerprint, InternalError> {
275        self.accepted_schema_fingerprint
276            .ok_or_else(InternalError::store_corruption)
277    }
278
279    /// Decode this raw store payload into a typed persisted-schema snapshot.
280    fn decode_persisted_snapshot(&self) -> Result<PersistedSchemaSnapshot, InternalError> {
281        // The identity header is the outer format gate. Do not pass a
282        // headerless value or a control record into the schema payload codec.
283        let _fingerprint = self.accepted_schema_fingerprint()?;
284        decode_persisted_schema_snapshot(self.as_bytes())
285    }
286}
287
288#[cfg(test)]
289pub(in crate::db::schema) fn validate_raw_schema_snapshot_bytes_for_tests(
290    bytes: Vec<u8>,
291) -> Result<(), InternalError> {
292    let raw = <RawSchemaSnapshot as Storable>::from_bytes(Cow::Owned(bytes));
293    raw.decode_persisted_snapshot().map(drop)
294}
295
296#[derive(Clone, Copy, Debug, Eq, PartialEq)]
297pub(in crate::db) struct AcceptedCatalogIdentity {
298    entity_tag: EntityTag,
299    entity_path: &'static str,
300    store_path: &'static str,
301    accepted_schema_revision: AcceptedSchemaRevision,
302    accepted_schema_version: SchemaVersion,
303    fingerprint_method_version: u8,
304    accepted_schema_fingerprint: CommitSchemaFingerprint,
305}
306
307impl AcceptedCatalogIdentity {
308    #[must_use]
309    pub(in crate::db) const fn new(
310        entity_tag: EntityTag,
311        entity_path: &'static str,
312        store_path: &'static str,
313        accepted_schema_revision: AcceptedSchemaRevision,
314        accepted_schema_version: SchemaVersion,
315        accepted_schema_fingerprint: CommitSchemaFingerprint,
316    ) -> Self {
317        Self {
318            entity_tag,
319            entity_path,
320            store_path,
321            accepted_schema_revision,
322            accepted_schema_version,
323            fingerprint_method_version: accepted_schema_cache_fingerprint_method_version(),
324            accepted_schema_fingerprint,
325        }
326    }
327
328    #[must_use]
329    pub(in crate::db) const fn entity_tag(self) -> EntityTag {
330        self.entity_tag
331    }
332
333    #[must_use]
334    pub(in crate::db) const fn entity_path(self) -> &'static str {
335        self.entity_path
336    }
337
338    #[must_use]
339    pub(in crate::db) const fn store_path(self) -> &'static str {
340        self.store_path
341    }
342
343    #[must_use]
344    pub(in crate::db) const fn accepted_schema_revision(self) -> AcceptedSchemaRevision {
345        self.accepted_schema_revision
346    }
347
348    #[must_use]
349    pub(in crate::db) const fn accepted_schema_version(self) -> SchemaVersion {
350        self.accepted_schema_version
351    }
352
353    #[must_use]
354    pub(in crate::db) const fn fingerprint_method_version(self) -> u8 {
355        self.fingerprint_method_version
356    }
357
358    #[must_use]
359    pub(in crate::db) const fn accepted_schema_fingerprint(self) -> CommitSchemaFingerprint {
360        self.accepted_schema_fingerprint
361    }
362}
363
364#[derive(Clone, Debug, Eq, PartialEq)]
365pub(in crate::db) struct AcceptedCatalogSnapshotSelection {
366    identity: AcceptedCatalogIdentity,
367    enum_catalog: AcceptedEnumCatalogHandle,
368    raw_snapshot: Vec<u8>,
369}
370
371impl AcceptedCatalogSnapshotSelection {
372    #[must_use]
373    const fn new(
374        identity: AcceptedCatalogIdentity,
375        enum_catalog: AcceptedEnumCatalogHandle,
376        raw_snapshot: Vec<u8>,
377    ) -> Self {
378        Self {
379            identity,
380            enum_catalog,
381            raw_snapshot,
382        }
383    }
384
385    #[must_use]
386    pub(in crate::db) const fn identity(&self) -> AcceptedCatalogIdentity {
387        self.identity
388    }
389
390    #[must_use]
391    pub(in crate::db) const fn enum_catalog(&self) -> &AcceptedEnumCatalogHandle {
392        &self.enum_catalog
393    }
394
395    /// Re-encode a cached accepted snapshot under its verified catalog identity.
396    pub(in crate::db) fn from_accepted_snapshot(
397        identity: AcceptedCatalogIdentity,
398        enum_catalog: AcceptedEnumCatalogHandle,
399        snapshot: &AcceptedSchemaSnapshot,
400    ) -> Result<Self, InternalError> {
401        let raw_snapshot =
402            RawSchemaSnapshot::from_persisted_snapshot(snapshot.persisted_snapshot())?;
403        if raw_snapshot.accepted_schema_fingerprint()? != identity.accepted_schema_fingerprint() {
404            return Err(InternalError::store_invariant());
405        }
406
407        Ok(Self::new(identity, enum_catalog, raw_snapshot.into_bytes()))
408    }
409
410    /// Select one entity snapshot and catalog directly from a verified schema
411    /// candidate while recovery is still applying its accepted root.
412    pub(in crate::db) fn from_candidate(
413        candidate: &CandidateSchemaRevision,
414        entity_tag: EntityTag,
415        entity_path: &'static str,
416        store_path: &'static str,
417    ) -> Result<Option<Self>, InternalError> {
418        if candidate.store_path() != store_path {
419            return Err(InternalError::store_corruption());
420        }
421        let Some(snapshot) = candidate.bundle().entity_snapshots().get(&entity_tag) else {
422            return Ok(None);
423        };
424        if snapshot.entity_path() != entity_path {
425            return Err(InternalError::store_corruption());
426        }
427
428        let raw_snapshot = RawSchemaSnapshot::from_persisted_snapshot(snapshot)?;
429        let fingerprint = raw_snapshot.accepted_schema_fingerprint()?;
430        let identity = AcceptedCatalogIdentity::new(
431            entity_tag,
432            entity_path,
433            store_path,
434            candidate.revision(),
435            snapshot.version(),
436            fingerprint,
437        );
438
439        Ok(Some(Self::new(
440            identity,
441            AcceptedEnumCatalogHandle::new(
442                candidate.bundle().enum_catalog().clone(),
443                AcceptedStoreCatalogScope::new(),
444                candidate.revision(),
445                candidate.root().fingerprint(),
446            ),
447            raw_snapshot.into_bytes(),
448        )))
449    }
450
451    pub(in crate::db) fn decode_verified(&self) -> Result<AcceptedSchemaSnapshot, InternalError> {
452        let snapshot = decode_persisted_schema_snapshot(&self.raw_snapshot)?;
453        let accepted = AcceptedSchemaSnapshot::try_new(snapshot)?;
454        let identity = self.identity();
455
456        if accepted.persisted_snapshot().version() != identity.accepted_schema_version() {
457            return Err(InternalError::store_invariant());
458        }
459        if accepted.entity_path() != identity.entity_path() {
460            return Err(InternalError::store_invariant());
461        }
462
463        let decoded_fingerprint = accepted_schema_cache_fingerprint(&accepted)?;
464        if decoded_fingerprint != identity.accepted_schema_fingerprint() {
465            return Err(InternalError::store_invariant());
466        }
467
468        Ok(accepted)
469    }
470}
471
472impl Storable for RawSchemaSnapshot {
473    fn to_bytes(&self) -> Cow<'_, [u8]> {
474        let Some(fingerprint) = self.accepted_schema_fingerprint else {
475            return Cow::Borrowed(self.as_bytes());
476        };
477
478        let mut bytes = Vec::with_capacity(RAW_SCHEMA_SNAPSHOT_HEADER_BYTES + self.payload.len());
479        bytes.extend_from_slice(RAW_SCHEMA_SNAPSHOT_MAGIC);
480        bytes.push(RAW_SCHEMA_SNAPSHOT_VALUE_VERSION);
481        bytes.extend_from_slice(&fingerprint);
482        bytes.extend_from_slice(self.as_bytes());
483
484        Cow::Owned(bytes)
485    }
486
487    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
488        let bytes = bytes.into_owned();
489        if bytes.len() >= RAW_SCHEMA_SNAPSHOT_HEADER_BYTES
490            && &bytes[..RAW_SCHEMA_SNAPSHOT_MAGIC.len()] == RAW_SCHEMA_SNAPSHOT_MAGIC
491            && bytes[RAW_SCHEMA_SNAPSHOT_MAGIC.len()] == RAW_SCHEMA_SNAPSHOT_VALUE_VERSION
492        {
493            let fingerprint_start = RAW_SCHEMA_SNAPSHOT_MAGIC.len() + size_of::<u8>();
494            let fingerprint_end = fingerprint_start + size_of::<CommitSchemaFingerprint>();
495            let mut fingerprint = [0_u8; size_of::<CommitSchemaFingerprint>()];
496            fingerprint.copy_from_slice(&bytes[fingerprint_start..fingerprint_end]);
497
498            return Self {
499                payload: bytes[fingerprint_end..].to_vec(),
500                accepted_schema_fingerprint: Some(fingerprint),
501            };
502        }
503
504        Self {
505            payload: bytes,
506            accepted_schema_fingerprint: None,
507        }
508    }
509
510    fn into_bytes(self) -> Vec<u8> {
511        let Some(fingerprint) = self.accepted_schema_fingerprint else {
512            return self.payload;
513        };
514
515        let mut bytes = Vec::with_capacity(RAW_SCHEMA_SNAPSHOT_HEADER_BYTES + self.payload.len());
516        bytes.extend_from_slice(RAW_SCHEMA_SNAPSHOT_MAGIC);
517        bytes.push(RAW_SCHEMA_SNAPSHOT_VALUE_VERSION);
518        bytes.extend_from_slice(&fingerprint);
519        bytes.extend_from_slice(&self.payload);
520
521        bytes
522    }
523
524    const BOUND: StorableBound = StorableBound::Unbounded;
525}
526
527// Validate typed schema snapshots before they are encoded into the raw schema
528// metadata store. This catches caller-side invariant violations separately from
529// raw persisted-byte corruption handled by the codec decode boundary.
530fn validate_typed_schema_snapshot_for_store(
531    snapshot: &PersistedSchemaSnapshot,
532) -> Result<(), InternalError> {
533    if schema_snapshot_integrity_detail(
534        "schema snapshot",
535        snapshot.version(),
536        snapshot.primary_key_field_ids(),
537        snapshot.row_layout(),
538        snapshot.fields(),
539    )
540    .is_some()
541    {
542        return Err(InternalError::store_invariant());
543    }
544
545    Ok(())
546}
547
548///
549/// SchemaStoreFootprint
550///
551/// Current raw schema metadata footprint for one entity. Reconciliation uses
552/// this value to report stable-memory pressure without decoding schema payloads
553/// or exposing field-level metadata through metrics.
554///
555
556#[derive(Clone, Copy, Debug, Eq, PartialEq)]
557pub(in crate::db) struct SchemaStoreFootprint {
558    snapshots: u64,
559    encoded_bytes: u64,
560    latest_snapshot_bytes: u64,
561}
562
563///
564/// SchemaStoreCatalogMetadata
565///
566/// Accepted schema-store catalog metadata derived from latest persisted
567/// snapshots. This is diagnostic allocation metadata, not allocation identity.
568///
569
570#[derive(Clone, Copy, Debug, Eq, PartialEq)]
571pub(in crate::db) struct SchemaStoreCatalogMetadata {
572    schema_version: SchemaVersion,
573    schema_fingerprint_method_version: u8,
574    schema_fingerprint: CommitSchemaFingerprint,
575    entity_count: u64,
576}
577
578impl SchemaStoreCatalogMetadata {
579    /// Build catalog metadata from already-derived accepted schema facts.
580    #[must_use]
581    const fn new(
582        schema_version: SchemaVersion,
583        schema_fingerprint_method_version: u8,
584        schema_fingerprint: CommitSchemaFingerprint,
585        entity_count: u64,
586    ) -> Self {
587        Self {
588            schema_version,
589            schema_fingerprint_method_version,
590            schema_fingerprint,
591            entity_count,
592        }
593    }
594
595    /// Return the maximum latest schema version represented in the catalog.
596    #[must_use]
597    pub(in crate::db) const fn schema_version(self) -> SchemaVersion {
598        self.schema_version
599    }
600
601    /// Return the fingerprint method version for this diagnostic metadata row.
602    #[must_use]
603    pub(in crate::db) const fn schema_fingerprint_method_version(self) -> u8 {
604        self.schema_fingerprint_method_version
605    }
606
607    /// Return the deterministic catalog fingerprint for latest accepted
608    /// snapshots.
609    #[must_use]
610    pub(in crate::db) const fn schema_fingerprint(self) -> CommitSchemaFingerprint {
611        self.schema_fingerprint
612    }
613
614    /// Return number of entity schemas represented in this catalog metadata.
615    #[must_use]
616    pub(in crate::db) const fn entity_count(self) -> u64 {
617        self.entity_count
618    }
619}
620
621///
622/// SchemaStoreAllocationMetadata
623///
624/// Role-specific allocation metadata derived from latest accepted schema-store
625/// snapshots. These fingerprints describe the accepted contract that owns each
626/// allocation role; they are diagnostics, not allocation identity.
627///
628
629#[derive(Clone, Copy, Debug, Eq, PartialEq)]
630pub(in crate::db) struct SchemaStoreAllocationMetadata {
631    data: SchemaStoreCatalogMetadata,
632    index: SchemaStoreCatalogMetadata,
633    schema: SchemaStoreCatalogMetadata,
634}
635
636impl SchemaStoreAllocationMetadata {
637    /// Build one role-specific metadata set from already-derived accepted
638    /// schema facts.
639    #[must_use]
640    const fn new(
641        data: SchemaStoreCatalogMetadata,
642        index: SchemaStoreCatalogMetadata,
643        schema: SchemaStoreCatalogMetadata,
644    ) -> Self {
645        Self {
646            data,
647            index,
648            schema,
649        }
650    }
651
652    /// Return accepted row-layout allocation metadata for data memory.
653    #[must_use]
654    pub(in crate::db) const fn data(self) -> SchemaStoreCatalogMetadata {
655        self.data
656    }
657
658    /// Return accepted index-catalog allocation metadata for index memory.
659    #[must_use]
660    pub(in crate::db) const fn index(self) -> SchemaStoreCatalogMetadata {
661        self.index
662    }
663
664    /// Return accepted full schema-catalog allocation metadata for schema
665    /// memory.
666    #[must_use]
667    pub(in crate::db) const fn schema(self) -> SchemaStoreCatalogMetadata {
668        self.schema
669    }
670}
671
672impl SchemaStoreFootprint {
673    /// Build one schema-store footprint from already-counted raw payload facts.
674    #[must_use]
675    const fn new(snapshots: u64, encoded_bytes: u64, latest_snapshot_bytes: u64) -> Self {
676        Self {
677            snapshots,
678            encoded_bytes,
679            latest_snapshot_bytes,
680        }
681    }
682
683    /// Return the number of raw schema snapshots stored for the entity.
684    #[must_use]
685    pub(in crate::db) const fn snapshots(self) -> u64 {
686        self.snapshots
687    }
688
689    /// Return the total encoded payload bytes stored for the entity.
690    #[must_use]
691    pub(in crate::db) const fn encoded_bytes(self) -> u64 {
692        self.encoded_bytes
693    }
694
695    /// Return the encoded payload bytes for the highest-version snapshot.
696    #[must_use]
697    pub(in crate::db) const fn latest_snapshot_bytes(self) -> u64 {
698        self.latest_snapshot_bytes
699    }
700}
701
702///
703/// SchemaStore
704///
705/// Thin persistence wrapper over one journaled or heap schema metadata BTreeMap.
706/// Startup reconciliation writes and validates encoded schema snapshots here
707/// before row/index operations proceed.
708///
709
710pub struct SchemaStore {
711    backend: SchemaStoreBackend,
712    accepted_bundle_cache: RefCell<Option<AcceptedSchemaBundleCache>>,
713    accepted_catalog_scope: OnceCell<AcceptedStoreCatalogScope>,
714}
715
716struct AcceptedSchemaBundleCache {
717    selection: AcceptedSchemaRootSelection,
718    bundle: AcceptedSchemaRevisionBundle,
719}
720
721enum SchemaStoreBackend {
722    Heap(StdBTreeMap<RawSchemaKey, RawSchemaSnapshot>),
723    Journaled {
724        canonical:
725            StableBTreeMap<RawSchemaKey, RawSchemaSnapshot, VirtualMemory<DefaultMemoryImpl>>,
726        live: StdBTreeMap<RawSchemaKey, RawSchemaSnapshot>,
727        tombstones: BTreeSet<RawSchemaKey>,
728    },
729}
730
731/// Control-flow result for schema-store traversal visitors.
732#[derive(Clone, Copy, Debug, Eq, PartialEq)]
733enum SchemaStoreVisit {
734    Continue,
735    Stop,
736}
737
738impl SchemaStoreVisit {
739    const fn should_stop(self) -> bool {
740        matches!(self, Self::Stop)
741    }
742}
743
744impl SchemaStore {
745    /// Initialize a volatile heap-backed schema store.
746    #[must_use]
747    pub const fn init_heap() -> Self {
748        Self {
749            backend: SchemaStoreBackend::Heap(StdBTreeMap::new()),
750            accepted_bundle_cache: RefCell::new(None),
751            accepted_catalog_scope: OnceCell::new(),
752        }
753    }
754
755    /// Initialize a journaled cached-stable schema store.
756    ///
757    /// Normal schema publication writes only the live projection. Canonical
758    /// stable schema history is updated by future journal fold/recovery paths.
759    #[must_use]
760    pub fn init_journaled(memory: VirtualMemory<DefaultMemoryImpl>) -> Self {
761        Self {
762            backend: SchemaStoreBackend::Journaled {
763                canonical: StableBTreeMap::init(memory),
764                live: StdBTreeMap::new(),
765                tombstones: BTreeSet::new(),
766            },
767            accepted_bundle_cache: RefCell::new(None),
768            accepted_catalog_scope: OnceCell::new(),
769        }
770    }
771
772    /// Insert or replace one typed persisted schema snapshot.
773    pub(in crate::db) fn insert_persisted_snapshot(
774        &mut self,
775        entity: EntityTag,
776        snapshot: &PersistedSchemaSnapshot,
777    ) -> Result<(), InternalError> {
778        let key = RawSchemaKey::from_entity_version(entity, snapshot.version());
779        let raw_snapshot = RawSchemaSnapshot::from_persisted_snapshot(snapshot)?;
780        let _ = self.insert_raw_snapshot(key, raw_snapshot);
781
782        Ok(())
783    }
784
785    /// Reset the volatile projection for journaled recovery without mutating
786    /// the canonical stable schema base.
787    pub(in crate::db) fn reset_journaled_live_projection(&mut self) -> Result<(), InternalError> {
788        let SchemaStoreBackend::Journaled {
789            live, tombstones, ..
790        } = &mut self.backend
791        else {
792            return Err(InternalError::store_invariant());
793        };
794
795        live.clear();
796        tombstones.clear();
797        self.accepted_bundle_cache.get_mut().take();
798
799        Ok(())
800    }
801
802    /// Apply one folded journal schema snapshot into the canonical stable base.
803    pub(in crate::db) fn fold_persisted_snapshot(
804        &mut self,
805        entity: EntityTag,
806        snapshot: &PersistedSchemaSnapshot,
807    ) -> Result<(), InternalError> {
808        let SchemaStoreBackend::Journaled { canonical, .. } = &mut self.backend else {
809            return Err(InternalError::store_invariant());
810        };
811
812        let key = RawSchemaKey::from_entity_version(entity, snapshot.version());
813        let raw_snapshot = RawSchemaSnapshot::from_persisted_snapshot(snapshot)?;
814        canonical.insert(key, raw_snapshot);
815
816        Ok(())
817    }
818
819    /// Return the current accepted store root selected from its two checksummed slots.
820    pub(in crate::db) fn current_accepted_schema_root(
821        &self,
822    ) -> Result<Option<AcceptedSchemaRootSelection>, InternalError> {
823        let first = self.accepted_root_slot_bytes(0)?;
824        let second = self.accepted_root_slot_bytes(1)?;
825        select_current_accepted_schema_root([first.as_deref(), second.as_deref()])
826    }
827
828    /// Load and verify the immutable bundle referenced by the current root.
829    pub(in crate::db) fn current_accepted_schema_bundle(
830        &self,
831    ) -> Result<Option<AcceptedSchemaRevisionBundle>, InternalError> {
832        let Some(selection) = self.current_accepted_schema_root()? else {
833            return Ok(None);
834        };
835        let key = RawSchemaKey::from_accepted_bundle(selection.root().bundle_key());
836        let raw = self
837            .get_raw_snapshot(&key)
838            .ok_or_else(InternalError::store_corruption)?;
839        decode_verified_accepted_schema_revision_bundle(selection.root(), raw.as_bytes()).map(Some)
840    }
841
842    /// Return the current accepted revision without decoding its bundle.
843    pub(in crate::db) fn current_accepted_schema_revision(
844        &self,
845    ) -> Result<Option<AcceptedSchemaRevision>, InternalError> {
846        Ok(self
847            .current_accepted_schema_root()?
848            .map(|selection| selection.root().revision()))
849    }
850
851    /// Return whether one retained schema authority still names this store's
852    /// current immutable accepted root.
853    pub(in crate::db) fn current_accepted_schema_authority_matches(
854        &self,
855        expected: &AcceptedSchemaAuthority,
856    ) -> Result<bool, InternalError> {
857        let Some(store_scope) = self.accepted_catalog_scope.get() else {
858            return Ok(false);
859        };
860
861        // Root-writing primitives invalidate this cache before publication,
862        // so a retained selection is the current in-memory authority.
863        if let Some(cached) = self
864            .accepted_bundle_cache
865            .try_borrow()
866            .map_err(|_| InternalError::store_invariant())?
867            .as_ref()
868        {
869            let root = cached.selection.root();
870            return Ok(expected.matches_store_root(
871                store_scope,
872                root.revision(),
873                root.fingerprint(),
874            ));
875        }
876
877        let Some(selection) = self.current_accepted_schema_root()? else {
878            return Ok(false);
879        };
880        let root = selection.root();
881
882        Ok(expected.matches_store_root(store_scope, root.revision(), root.fingerprint()))
883    }
884
885    /// Bootstrap an immutable candidate directly into the schema allocation.
886    ///
887    /// Journaled online revisions must use `apply_journaled_accepted_schema_candidate`.
888    pub(in crate::db) fn publish_accepted_schema_candidate(
889        &mut self,
890        expected_revision: AcceptedSchemaRevision,
891        candidate: &CandidateSchemaRevision,
892    ) -> Result<(), InternalError> {
893        if self.current_root_matches_candidate(candidate)? {
894            let selection = self
895                .current_accepted_schema_root()?
896                .ok_or_else(InternalError::store_corruption)?;
897            self.retain_durable_candidate_entries(candidate, selection.slot())?;
898            return Ok(());
899        }
900        let first = self.accepted_root_slot_bytes(0)?;
901        let second = self.accepted_root_slot_bytes(1)?;
902        prepare_accepted_schema_root_publication(
903            [first.as_deref(), second.as_deref()],
904            expected_revision,
905            candidate,
906        )
907        .map_err(map_schema_publication_error)?;
908
909        self.insert_durable_candidate_snapshots(candidate)?;
910        let bundle_key = RawSchemaKey::from_accepted_bundle(candidate.root().bundle_key());
911        self.insert_durable_raw_value(bundle_key, candidate.encoded_bundle().to_vec());
912        let persisted_bundle = self
913            .get_raw_snapshot(&bundle_key)
914            .ok_or_else(InternalError::store_corruption)?;
915        let _verified = decode_verified_accepted_schema_revision_bundle(
916            candidate.root(),
917            persisted_bundle.as_bytes(),
918        )?;
919
920        // Re-read the root immediately before the inactive-slot write. This is
921        // the compare-and-swap check after candidate persistence.
922        let first = self.accepted_root_slot_bytes(0)?;
923        let second = self.accepted_root_slot_bytes(1)?;
924        let publication = prepare_accepted_schema_root_publication(
925            [first.as_deref(), second.as_deref()],
926            expected_revision,
927            candidate,
928        )
929        .map_err(map_schema_publication_error)?;
930        let root_key = RawSchemaKey::from_accepted_root_slot(publication.target_slot())?;
931        self.insert_durable_raw_value(root_key, publication.encoded_root().to_vec());
932
933        let selected = self
934            .current_accepted_schema_root()?
935            .ok_or_else(InternalError::store_corruption)?;
936        if selected.root() != candidate.root() {
937            return Err(InternalError::store_corruption());
938        }
939        self.retain_durable_candidate_entries(candidate, selected.slot())?;
940        Ok(())
941    }
942
943    /// Apply one marker-bound schema candidate to the journaled live projection.
944    pub(in crate::db) fn apply_journaled_accepted_schema_candidate(
945        &mut self,
946        expected_revision: AcceptedSchemaRevision,
947        candidate: &CandidateSchemaRevision,
948    ) -> Result<(), InternalError> {
949        if !matches!(self.backend, SchemaStoreBackend::Journaled { .. }) {
950            return Err(InternalError::store_invariant());
951        }
952        if self.current_root_matches_candidate(candidate)? {
953            let selection = self
954                .current_accepted_schema_root()?
955                .ok_or_else(InternalError::store_corruption)?;
956            self.retain_materialized_candidate_entries(candidate, selection.slot())?;
957            return Ok(());
958        }
959
960        let first = self.accepted_root_slot_bytes(0)?;
961        let second = self.accepted_root_slot_bytes(1)?;
962        prepare_accepted_schema_root_publication(
963            [first.as_deref(), second.as_deref()],
964            expected_revision,
965            candidate,
966        )
967        .map_err(map_schema_publication_error)?;
968
969        for (entity_tag, snapshot) in candidate.bundle().entity_snapshots() {
970            self.insert_persisted_snapshot(*entity_tag, snapshot)?;
971        }
972        let bundle_key = RawSchemaKey::from_accepted_bundle(candidate.root().bundle_key());
973        self.insert_raw_snapshot(
974            bundle_key,
975            RawSchemaSnapshot::from_encoded_control_record(candidate.encoded_bundle().to_vec()),
976        );
977        let persisted_bundle = self
978            .get_raw_snapshot(&bundle_key)
979            .ok_or_else(InternalError::store_corruption)?;
980        let _verified = decode_verified_accepted_schema_revision_bundle(
981            candidate.root(),
982            persisted_bundle.as_bytes(),
983        )?;
984
985        let first = self.accepted_root_slot_bytes(0)?;
986        let second = self.accepted_root_slot_bytes(1)?;
987        let publication = prepare_accepted_schema_root_publication(
988            [first.as_deref(), second.as_deref()],
989            expected_revision,
990            candidate,
991        )
992        .map_err(map_schema_publication_error)?;
993        let root_key = RawSchemaKey::from_accepted_root_slot(publication.target_slot())?;
994        self.insert_raw_snapshot(
995            root_key,
996            RawSchemaSnapshot::from_encoded_control_record(publication.encoded_root().to_vec()),
997        );
998
999        if !self.current_root_matches_candidate(candidate)? {
1000            return Err(InternalError::store_corruption());
1001        }
1002        let selection = self
1003            .current_accepted_schema_root()?
1004            .ok_or_else(InternalError::store_corruption)?;
1005        self.retain_materialized_candidate_entries(candidate, selection.slot())?;
1006        Ok(())
1007    }
1008
1009    /// Fold one committed schema candidate into the canonical schema BTree.
1010    pub(in crate::db) fn fold_journaled_accepted_schema_candidate(
1011        &mut self,
1012        expected_revision: AcceptedSchemaRevision,
1013        candidate: &CandidateSchemaRevision,
1014    ) -> Result<(), InternalError> {
1015        if self.canonical_root_matches_candidate(candidate)? {
1016            let first = self.canonical_root_slot_bytes(0)?;
1017            let second = self.canonical_root_slot_bytes(1)?;
1018            let selection =
1019                select_current_accepted_schema_root([first.as_deref(), second.as_deref()])?
1020                    .ok_or_else(InternalError::store_corruption)?;
1021            self.retain_canonical_candidate_entries(candidate, selection.slot())?;
1022            return Ok(());
1023        }
1024
1025        let first = self.canonical_root_slot_bytes(0)?;
1026        let second = self.canonical_root_slot_bytes(1)?;
1027        prepare_accepted_schema_root_publication(
1028            [first.as_deref(), second.as_deref()],
1029            expected_revision,
1030            candidate,
1031        )
1032        .map_err(map_schema_publication_error)?;
1033
1034        for (entity_tag, snapshot) in candidate.bundle().entity_snapshots() {
1035            self.fold_persisted_snapshot(*entity_tag, snapshot)?;
1036        }
1037        let bundle_key = RawSchemaKey::from_accepted_bundle(candidate.root().bundle_key());
1038        self.insert_canonical_raw_value(bundle_key, candidate.encoded_bundle().to_vec())?;
1039        let persisted_bundle = self
1040            .get_canonical_raw_value(&bundle_key)?
1041            .ok_or_else(InternalError::store_corruption)?;
1042        let _verified = decode_verified_accepted_schema_revision_bundle(
1043            candidate.root(),
1044            persisted_bundle.as_bytes(),
1045        )?;
1046
1047        let first = self.canonical_root_slot_bytes(0)?;
1048        let second = self.canonical_root_slot_bytes(1)?;
1049        let publication = prepare_accepted_schema_root_publication(
1050            [first.as_deref(), second.as_deref()],
1051            expected_revision,
1052            candidate,
1053        )
1054        .map_err(map_schema_publication_error)?;
1055        let root_key = RawSchemaKey::from_accepted_root_slot(publication.target_slot())?;
1056        self.insert_canonical_raw_value(root_key, publication.encoded_root().to_vec())?;
1057
1058        if !self.canonical_root_matches_candidate(candidate)? {
1059            return Err(InternalError::store_corruption());
1060        }
1061        let first = self.canonical_root_slot_bytes(0)?;
1062        let second = self.canonical_root_slot_bytes(1)?;
1063        let selection = select_current_accepted_schema_root([first.as_deref(), second.as_deref()])?
1064            .ok_or_else(InternalError::store_corruption)?;
1065        self.retain_canonical_candidate_entries(candidate, selection.slot())?;
1066        Ok(())
1067    }
1068
1069    /// Load and decode one typed persisted schema snapshot.
1070    #[cfg(test)]
1071    pub(in crate::db) fn get_persisted_snapshot(
1072        &self,
1073        entity: EntityTag,
1074        version: SchemaVersion,
1075    ) -> Result<Option<PersistedSchemaSnapshot>, InternalError> {
1076        let key = RawSchemaKey::from_entity_version(entity, version);
1077        self.get_raw_snapshot(&key)
1078            .map(|snapshot| snapshot.decode_persisted_snapshot())
1079            .transpose()
1080    }
1081
1082    /// Load and decode the highest staged schema snapshot for one entity.
1083    ///
1084    /// Candidate construction and publication gates use this view. Runtime
1085    /// execution must use `current_accepted_persisted_snapshot` instead.
1086    pub(in crate::db) fn latest_staged_persisted_snapshot(
1087        &self,
1088        entity: EntityTag,
1089    ) -> Result<Option<PersistedSchemaSnapshot>, InternalError> {
1090        self.latest_raw_snapshot(entity)
1091            .map(|snapshot| snapshot.decode_persisted_snapshot())
1092            .transpose()
1093    }
1094
1095    /// Load one entity snapshot from the immutable bundle selected by the
1096    /// current accepted root.
1097    pub(in crate::db) fn current_accepted_persisted_snapshot(
1098        &self,
1099        entity: EntityTag,
1100    ) -> Result<Option<PersistedSchemaSnapshot>, InternalError> {
1101        let Some(bundle) = self.current_accepted_schema_bundle_ref()? else {
1102            return Ok(None);
1103        };
1104
1105        Ok(bundle.entity_snapshots().get(&entity).cloned())
1106    }
1107
1108    /// Return one accepted catalog selection from the current immutable root.
1109    pub(in crate::db) fn current_accepted_catalog_selection(
1110        &self,
1111        entity: EntityTag,
1112        entity_path: &'static str,
1113        store_path: &'static str,
1114    ) -> Result<Option<AcceptedCatalogSnapshotSelection>, InternalError> {
1115        let Some(bundle) = self.current_accepted_schema_bundle_ref()? else {
1116            return Ok(None);
1117        };
1118        if bundle.store_path() != store_path {
1119            return Err(InternalError::store_corruption());
1120        }
1121        let Some(snapshot) = bundle.entity_snapshots().get(&entity) else {
1122            return Ok(None);
1123        };
1124        if snapshot.entity_path() != entity_path {
1125            return Err(InternalError::store_corruption());
1126        }
1127
1128        let raw_snapshot = RawSchemaSnapshot::from_persisted_snapshot(snapshot)?;
1129        let fingerprint = raw_snapshot.accepted_schema_fingerprint()?;
1130        let identity = AcceptedCatalogIdentity::new(
1131            entity,
1132            entity_path,
1133            store_path,
1134            bundle.revision(),
1135            snapshot.version(),
1136            fingerprint,
1137        );
1138
1139        Ok(Some(AcceptedCatalogSnapshotSelection::new(
1140            identity,
1141            AcceptedEnumCatalogHandle::new(
1142                bundle.enum_catalog().clone(),
1143                self.accepted_catalog_scope
1144                    .get_or_init(AcceptedStoreCatalogScope::new)
1145                    .clone(),
1146                bundle.revision(),
1147                self.current_accepted_schema_root()?
1148                    .ok_or_else(InternalError::store_corruption)?
1149                    .root()
1150                    .fingerprint(),
1151            ),
1152            raw_snapshot.into_bytes(),
1153        )))
1154    }
1155
1156    /// Return one accepted catalog selection from the canonical journal base.
1157    /// Recovery uses this while folding historical row batches whose schema
1158    /// revision can precede the current live accepted root.
1159    pub(in crate::db) fn current_canonical_accepted_catalog_selection(
1160        &self,
1161        entity: EntityTag,
1162        entity_path: &'static str,
1163        store_path: &'static str,
1164    ) -> Result<Option<AcceptedCatalogSnapshotSelection>, InternalError> {
1165        let first = self.canonical_root_slot_bytes(0)?;
1166        let second = self.canonical_root_slot_bytes(1)?;
1167        let Some(selection) =
1168            select_current_accepted_schema_root([first.as_deref(), second.as_deref()])?
1169        else {
1170            return Ok(None);
1171        };
1172        let bundle_key = RawSchemaKey::from_accepted_bundle(selection.root().bundle_key());
1173        let raw_bundle = self
1174            .get_canonical_raw_value(&bundle_key)?
1175            .ok_or_else(InternalError::store_corruption)?;
1176        let bundle = decode_verified_accepted_schema_revision_bundle(
1177            selection.root(),
1178            raw_bundle.as_bytes(),
1179        )?;
1180        if bundle.store_path() != store_path {
1181            return Err(InternalError::store_corruption());
1182        }
1183        let Some(snapshot) = bundle.entity_snapshots().get(&entity) else {
1184            return Ok(None);
1185        };
1186        if snapshot.entity_path() != entity_path {
1187            return Err(InternalError::store_corruption());
1188        }
1189
1190        let raw_snapshot = RawSchemaSnapshot::from_persisted_snapshot(snapshot)?;
1191        let fingerprint = raw_snapshot.accepted_schema_fingerprint()?;
1192        let identity = AcceptedCatalogIdentity::new(
1193            entity,
1194            entity_path,
1195            store_path,
1196            bundle.revision(),
1197            snapshot.version(),
1198            fingerprint,
1199        );
1200
1201        Ok(Some(AcceptedCatalogSnapshotSelection::new(
1202            identity,
1203            AcceptedEnumCatalogHandle::new(
1204                bundle.enum_catalog().clone(),
1205                self.accepted_catalog_scope
1206                    .get_or_init(AcceptedStoreCatalogScope::new)
1207                    .clone(),
1208                bundle.revision(),
1209                selection.root().fingerprint(),
1210            ),
1211            raw_snapshot.into_bytes(),
1212        )))
1213    }
1214
1215    /// Return raw schema-store footprint facts for one entity.
1216    #[must_use]
1217    pub(in crate::db) fn entity_footprint(&self, entity: EntityTag) -> SchemaStoreFootprint {
1218        let mut snapshots = 0u64;
1219        let mut encoded_bytes = 0u64;
1220        let mut latest = None::<(SchemaVersion, u64)>;
1221
1222        let _: Result<(), std::convert::Infallible> = self.visit_raw_snapshots(|key, snapshot| {
1223            if key.entity_tag() != entity {
1224                return Ok(SchemaStoreVisit::Continue);
1225            }
1226
1227            let snapshot_bytes = u64::try_from(snapshot.as_bytes().len()).unwrap_or(u64::MAX);
1228            snapshots = snapshots.saturating_add(1);
1229            encoded_bytes = encoded_bytes.saturating_add(snapshot_bytes);
1230
1231            let version = SchemaVersion::new(key.version());
1232            if latest
1233                .as_ref()
1234                .is_none_or(|(latest_version, _)| version > *latest_version)
1235            {
1236                latest = Some((version, snapshot_bytes));
1237            }
1238            Ok(SchemaStoreVisit::Continue)
1239        });
1240
1241        SchemaStoreFootprint::new(
1242            snapshots,
1243            encoded_bytes,
1244            latest.map_or(0, |(_, snapshot_bytes)| snapshot_bytes),
1245        )
1246    }
1247
1248    /// Derive accepted catalog metadata from latest persisted schema snapshots.
1249    ///
1250    /// This function intentionally reads only the persisted schema store. It
1251    /// does not reconstruct metadata from generated models when the store has
1252    /// no accepted snapshots.
1253    #[cfg(test)]
1254    pub(in crate::db) fn catalog_metadata(
1255        &self,
1256    ) -> Result<Option<SchemaStoreCatalogMetadata>, InternalError> {
1257        Ok(self
1258            .allocation_metadata()?
1259            .map(SchemaStoreAllocationMetadata::schema))
1260    }
1261
1262    /// Derive role-specific allocation metadata from latest persisted schema
1263    /// snapshots.
1264    ///
1265    /// This function intentionally reads only accepted schema-store payloads.
1266    /// It never reconstructs metadata from generated models when the store has
1267    /// no accepted snapshots.
1268    pub(in crate::db) fn allocation_metadata(
1269        &self,
1270    ) -> Result<Option<SchemaStoreAllocationMetadata>, InternalError> {
1271        let latest_by_entity = self.latest_raw_snapshots_by_entity();
1272        if latest_by_entity.is_empty() {
1273            return Ok(None);
1274        }
1275
1276        Ok(Some(SchemaStoreAllocationMetadata::new(
1277            derive_data_allocation_metadata(&latest_by_entity)?,
1278            derive_index_allocation_metadata(&latest_by_entity)?,
1279            derive_schema_catalog_metadata(&latest_by_entity)?,
1280        )))
1281    }
1282
1283    /// Insert or replace one raw schema snapshot.
1284    fn insert_raw_snapshot(
1285        &mut self,
1286        key: RawSchemaKey,
1287        snapshot: RawSchemaSnapshot,
1288    ) -> Option<RawSchemaSnapshot> {
1289        self.invalidate_accepted_bundle_cache_for_key(key);
1290        let previous_journaled = if matches!(self.backend, SchemaStoreBackend::Journaled { .. }) {
1291            self.get_raw_snapshot_for_backend(&key)
1292        } else {
1293            None
1294        };
1295        match &mut self.backend {
1296            SchemaStoreBackend::Heap(map) => map.insert(key, snapshot),
1297            SchemaStoreBackend::Journaled {
1298                live, tombstones, ..
1299            } => {
1300                tombstones.remove(&key);
1301                live.insert(key, snapshot);
1302                previous_journaled
1303            }
1304        }
1305    }
1306
1307    /// Load one raw schema snapshot by key.
1308    #[must_use]
1309    fn get_raw_snapshot(&self, key: &RawSchemaKey) -> Option<RawSchemaSnapshot> {
1310        match &self.backend {
1311            SchemaStoreBackend::Heap(map) => map.get(key).cloned(),
1312            SchemaStoreBackend::Journaled { .. } => self.get_raw_snapshot_for_backend(key),
1313        }
1314    }
1315
1316    fn accepted_root_slot_bytes(&self, slot: usize) -> Result<Option<Vec<u8>>, InternalError> {
1317        let key = RawSchemaKey::from_accepted_root_slot(slot)?;
1318        Ok(self
1319            .get_raw_snapshot(&key)
1320            .map(RawSchemaSnapshot::into_bytes))
1321    }
1322
1323    fn canonical_root_slot_bytes(&self, slot: usize) -> Result<Option<Vec<u8>>, InternalError> {
1324        let key = RawSchemaKey::from_accepted_root_slot(slot)?;
1325        Ok(self
1326            .get_canonical_raw_value(&key)?
1327            .map(RawSchemaSnapshot::into_bytes))
1328    }
1329
1330    fn current_root_matches_candidate(
1331        &self,
1332        candidate: &CandidateSchemaRevision,
1333    ) -> Result<bool, InternalError> {
1334        let Some(selection) = self.current_accepted_schema_root()? else {
1335            return Ok(false);
1336        };
1337        if selection.root() != candidate.root() {
1338            return Ok(false);
1339        }
1340        let key = RawSchemaKey::from_accepted_bundle(candidate.root().bundle_key());
1341        let bundle = self
1342            .get_raw_snapshot(&key)
1343            .ok_or_else(InternalError::store_corruption)?;
1344        let _verified =
1345            decode_verified_accepted_schema_revision_bundle(candidate.root(), bundle.as_bytes())?;
1346        Ok(true)
1347    }
1348
1349    fn canonical_root_matches_candidate(
1350        &self,
1351        candidate: &CandidateSchemaRevision,
1352    ) -> Result<bool, InternalError> {
1353        let first = self.canonical_root_slot_bytes(0)?;
1354        let second = self.canonical_root_slot_bytes(1)?;
1355        let Some(selection) =
1356            select_current_accepted_schema_root([first.as_deref(), second.as_deref()])?
1357        else {
1358            return Ok(false);
1359        };
1360        if selection.root() != candidate.root() {
1361            return Ok(false);
1362        }
1363        let key = RawSchemaKey::from_accepted_bundle(candidate.root().bundle_key());
1364        let bundle = self
1365            .get_canonical_raw_value(&key)?
1366            .ok_or_else(InternalError::store_corruption)?;
1367        let _verified =
1368            decode_verified_accepted_schema_revision_bundle(candidate.root(), bundle.as_bytes())?;
1369        Ok(true)
1370    }
1371
1372    fn get_canonical_raw_value(
1373        &self,
1374        key: &RawSchemaKey,
1375    ) -> Result<Option<RawSchemaSnapshot>, InternalError> {
1376        match &self.backend {
1377            SchemaStoreBackend::Journaled { canonical, .. } => Ok(canonical.get(key)),
1378            SchemaStoreBackend::Heap(_) => Err(InternalError::store_invariant()),
1379        }
1380    }
1381
1382    fn insert_canonical_raw_value(
1383        &mut self,
1384        key: RawSchemaKey,
1385        bytes: Vec<u8>,
1386    ) -> Result<(), InternalError> {
1387        self.invalidate_accepted_bundle_cache_for_key(key);
1388        let SchemaStoreBackend::Journaled { canonical, .. } = &mut self.backend else {
1389            return Err(InternalError::store_invariant());
1390        };
1391        canonical.insert(key, RawSchemaSnapshot::from_encoded_control_record(bytes));
1392        Ok(())
1393    }
1394
1395    // Initial accepted-catalog bootstrap persists immutable bundle/root values
1396    // directly in the schema allocation. Later online schema mutation will
1397    // carry the same values through the journal before calling this primitive.
1398    fn insert_durable_raw_value(&mut self, key: RawSchemaKey, bytes: Vec<u8>) {
1399        self.invalidate_accepted_bundle_cache_for_key(key);
1400        let value = RawSchemaSnapshot::from_encoded_control_record(bytes);
1401        match &mut self.backend {
1402            SchemaStoreBackend::Heap(map) => {
1403                map.insert(key, value);
1404            }
1405            SchemaStoreBackend::Journaled {
1406                canonical,
1407                live,
1408                tombstones,
1409            } => {
1410                live.remove(&key);
1411                tombstones.remove(&key);
1412                canonical.insert(key, value);
1413            }
1414        }
1415    }
1416
1417    fn invalidate_accepted_bundle_cache_for_key(&mut self, key: RawSchemaKey) {
1418        if key.is_accepted_root() {
1419            self.accepted_bundle_cache.get_mut().take();
1420        }
1421    }
1422
1423    fn insert_durable_candidate_snapshots(
1424        &mut self,
1425        candidate: &CandidateSchemaRevision,
1426    ) -> Result<(), InternalError> {
1427        for (entity_tag, snapshot) in candidate.bundle().entity_snapshots() {
1428            let key = RawSchemaKey::from_entity_version(*entity_tag, snapshot.version());
1429            let value = RawSchemaSnapshot::from_persisted_snapshot(snapshot)?;
1430            match &mut self.backend {
1431                SchemaStoreBackend::Heap(map) => {
1432                    map.insert(key, value);
1433                }
1434                SchemaStoreBackend::Journaled {
1435                    canonical,
1436                    live,
1437                    tombstones,
1438                } => {
1439                    live.remove(&key);
1440                    tombstones.remove(&key);
1441                    canonical.insert(key, value);
1442                }
1443            }
1444        }
1445        Ok(())
1446    }
1447
1448    fn candidate_entry_keys(
1449        candidate: &CandidateSchemaRevision,
1450        root_slot: usize,
1451    ) -> Result<BTreeSet<RawSchemaKey>, InternalError> {
1452        let mut keys = candidate
1453            .bundle()
1454            .entity_snapshots()
1455            .iter()
1456            .map(|(entity_tag, snapshot)| {
1457                RawSchemaKey::from_entity_version(*entity_tag, snapshot.version())
1458            })
1459            .collect::<BTreeSet<_>>();
1460        keys.insert(RawSchemaKey::from_accepted_bundle(
1461            candidate.root().bundle_key(),
1462        ));
1463        keys.insert(RawSchemaKey::from_accepted_root_slot(root_slot)?);
1464        Ok(keys)
1465    }
1466
1467    // Keep only the current entity snapshots, immutable bundle, and selected
1468    // root. The inactive root is needed only during publication and is removed
1469    // after the new root has been verified.
1470    fn retain_durable_candidate_entries(
1471        &mut self,
1472        candidate: &CandidateSchemaRevision,
1473        root_slot: usize,
1474    ) -> Result<(), InternalError> {
1475        let keep = Self::candidate_entry_keys(candidate, root_slot)?;
1476        self.accepted_bundle_cache.get_mut().take();
1477        match &mut self.backend {
1478            SchemaStoreBackend::Heap(map) => map.retain(|key, _| keep.contains(key)),
1479            SchemaStoreBackend::Journaled {
1480                canonical,
1481                live,
1482                tombstones,
1483            } => {
1484                let stale = canonical
1485                    .iter()
1486                    .filter_map(|entry| (!keep.contains(entry.key())).then_some(*entry.key()))
1487                    .collect::<Vec<_>>();
1488                for key in stale {
1489                    canonical.remove(&key);
1490                }
1491                live.retain(|key, _| keep.contains(key));
1492                tombstones.clear();
1493            }
1494        }
1495        Ok(())
1496    }
1497
1498    fn retain_materialized_candidate_entries(
1499        &mut self,
1500        candidate: &CandidateSchemaRevision,
1501        root_slot: usize,
1502    ) -> Result<(), InternalError> {
1503        let keep = Self::candidate_entry_keys(candidate, root_slot)?;
1504        self.accepted_bundle_cache.get_mut().take();
1505        let SchemaStoreBackend::Journaled {
1506            canonical,
1507            live,
1508            tombstones,
1509        } = &mut self.backend
1510        else {
1511            return Err(InternalError::store_invariant());
1512        };
1513        live.retain(|key, _| keep.contains(key));
1514        let canonical_keys = canonical
1515            .iter()
1516            .map(|entry| *entry.key())
1517            .collect::<Vec<_>>();
1518        for key in canonical_keys {
1519            if keep.contains(&key) {
1520                tombstones.remove(&key);
1521            } else {
1522                tombstones.insert(key);
1523            }
1524        }
1525        Ok(())
1526    }
1527
1528    fn retain_canonical_candidate_entries(
1529        &mut self,
1530        candidate: &CandidateSchemaRevision,
1531        root_slot: usize,
1532    ) -> Result<(), InternalError> {
1533        let keep = Self::candidate_entry_keys(candidate, root_slot)?;
1534        self.accepted_bundle_cache.get_mut().take();
1535        let SchemaStoreBackend::Journaled { canonical, .. } = &mut self.backend else {
1536            return Err(InternalError::store_invariant());
1537        };
1538        let stale = canonical
1539            .iter()
1540            .filter_map(|entry| (!keep.contains(entry.key())).then_some(*entry.key()))
1541            .collect::<Vec<_>>();
1542        for key in stale {
1543            canonical.remove(&key);
1544        }
1545        Ok(())
1546    }
1547
1548    /// Return whether one schema snapshot key is present.
1549    #[must_use]
1550    #[cfg(test)]
1551    fn contains_raw_snapshot(&self, key: &RawSchemaKey) -> bool {
1552        match &self.backend {
1553            SchemaStoreBackend::Heap(map) => map.contains_key(key),
1554            SchemaStoreBackend::Journaled { .. } => {
1555                self.get_raw_snapshot_for_backend(key).is_some()
1556            }
1557        }
1558    }
1559
1560    /// Return the number of schema snapshot entries in this store.
1561    #[must_use]
1562    #[cfg(test)]
1563    pub(in crate::db) fn len(&self) -> u64 {
1564        match &self.backend {
1565            SchemaStoreBackend::Heap(map) => u64::try_from(map.len()).unwrap_or(u64::MAX),
1566            SchemaStoreBackend::Journaled { .. } => {
1567                let mut count = 0_u64;
1568                let _: Result<(), Infallible> = self.visit_raw_snapshots(|_key, _snapshot| {
1569                    count = count.saturating_add(1);
1570                    Ok(SchemaStoreVisit::Continue)
1571                });
1572                count
1573            }
1574        }
1575    }
1576
1577    /// Return whether this schema store currently has no persisted snapshots.
1578    #[must_use]
1579    #[cfg(test)]
1580    pub(in crate::db) fn is_empty(&self) -> bool {
1581        match &self.backend {
1582            SchemaStoreBackend::Heap(map) => map.is_empty(),
1583            SchemaStoreBackend::Journaled { .. } => {
1584                let mut empty = true;
1585                let _: Result<(), Infallible> = self.visit_raw_snapshots(|_key, _snapshot| {
1586                    empty = false;
1587                    Ok(SchemaStoreVisit::Stop)
1588                });
1589                empty
1590            }
1591        }
1592    }
1593
1594    /// Clear all schema metadata entries from the store.
1595    #[cfg(test)]
1596    pub(in crate::db) fn clear(&mut self) {
1597        self.accepted_bundle_cache.get_mut().take();
1598        match &mut self.backend {
1599            SchemaStoreBackend::Heap(map) => map.clear(),
1600            SchemaStoreBackend::Journaled {
1601                canonical,
1602                live,
1603                tombstones,
1604            } => {
1605                live.clear();
1606                tombstones.clear();
1607                let keys = canonical
1608                    .iter()
1609                    .map(|entry| *entry.key())
1610                    .collect::<Vec<_>>();
1611                for key in keys {
1612                    if key.is_entity_snapshot() {
1613                        tombstones.insert(key);
1614                    } else {
1615                        canonical.remove(&key);
1616                    }
1617                }
1618            }
1619        }
1620    }
1621
1622    fn current_accepted_schema_bundle_ref(
1623        &self,
1624    ) -> Result<Option<Ref<'_, AcceptedSchemaRevisionBundle>>, InternalError> {
1625        let Some(selection) = self.current_accepted_schema_root()? else {
1626            self.accepted_bundle_cache
1627                .try_borrow_mut()
1628                .map_err(|_| InternalError::store_invariant())?
1629                .take();
1630            return Ok(None);
1631        };
1632
1633        let cache_matches = self
1634            .accepted_bundle_cache
1635            .try_borrow()
1636            .map_err(|_| InternalError::store_invariant())?
1637            .as_ref()
1638            .is_some_and(|cached| cached.selection == selection);
1639        if !cache_matches {
1640            let key = RawSchemaKey::from_accepted_bundle(selection.root().bundle_key());
1641            let raw = self
1642                .get_raw_snapshot(&key)
1643                .ok_or_else(InternalError::store_corruption)?;
1644            let bundle =
1645                decode_verified_accepted_schema_revision_bundle(selection.root(), raw.as_bytes())?;
1646            #[cfg(test)]
1647            ACCEPTED_SCHEMA_BUNDLE_CACHE_MISSES
1648                .with(|misses| misses.set(misses.get().saturating_add(1)));
1649            *self
1650                .accepted_bundle_cache
1651                .try_borrow_mut()
1652                .map_err(|_| InternalError::store_invariant())? =
1653                Some(AcceptedSchemaBundleCache { selection, bundle });
1654        }
1655
1656        let cache = self
1657            .accepted_bundle_cache
1658            .try_borrow()
1659            .map_err(|_| InternalError::store_invariant())?;
1660        Ref::filter_map(cache, |cache| {
1661            cache
1662                .as_ref()
1663                .filter(|cached| cached.selection == selection)
1664                .map(|cached| &cached.bundle)
1665        })
1666        .map(Some)
1667        .map_err(|_| InternalError::store_invariant())
1668    }
1669
1670    fn latest_raw_snapshots_by_entity(
1671        &self,
1672    ) -> StdBTreeMap<EntityTag, (SchemaVersion, RawSchemaSnapshot)> {
1673        #[cfg(test)]
1674        LATEST_RAW_SNAPSHOTS_BY_ENTITY_CALLS.with(|calls| calls.set(calls.get().saturating_add(1)));
1675
1676        let mut latest_by_entity =
1677            StdBTreeMap::<EntityTag, (SchemaVersion, RawSchemaSnapshot)>::new();
1678
1679        let _: Result<(), std::convert::Infallible> = self.visit_raw_snapshots(|key, snapshot| {
1680            let version = SchemaVersion::new(key.version());
1681            match latest_by_entity.get_mut(&key.entity_tag()) {
1682                Some((latest_version, latest_snapshot)) if version > *latest_version => {
1683                    *latest_version = version;
1684                    *latest_snapshot = snapshot.clone();
1685                }
1686                None => {
1687                    latest_by_entity.insert(key.entity_tag(), (version, snapshot.clone()));
1688                }
1689                Some(_) => {}
1690            }
1691            Ok(SchemaStoreVisit::Continue)
1692        });
1693
1694        latest_by_entity
1695    }
1696
1697    /// Visit raw schema snapshots in canonical store order without exposing
1698    /// the backing stable-map iterator.
1699    fn visit_raw_snapshots<E>(
1700        &self,
1701        visitor: impl FnMut(&RawSchemaKey, &RawSchemaSnapshot) -> Result<SchemaStoreVisit, E>,
1702    ) -> Result<(), E> {
1703        let bounds = RawSchemaKey::all_entity_range_bounds();
1704        match &self.backend {
1705            SchemaStoreBackend::Heap(map) => {
1706                let mut visitor = visitor;
1707                for (key, snapshot) in map.range((bounds.0, bounds.1)) {
1708                    if visitor(key, snapshot)?.should_stop() {
1709                        break;
1710                    }
1711                }
1712            }
1713            SchemaStoreBackend::Journaled {
1714                canonical,
1715                live,
1716                tombstones,
1717            } => Self::visit_journaled_raw_snapshot_range(
1718                canonical,
1719                live,
1720                tombstones,
1721                bounds,
1722                Direction::Asc,
1723                visitor,
1724            )?,
1725        }
1726
1727        Ok(())
1728    }
1729
1730    #[cfg(test)]
1731    #[must_use]
1732    pub(in crate::db) fn canonical_len_for_tests(&self) -> u64 {
1733        match &self.backend {
1734            SchemaStoreBackend::Journaled { canonical: map, .. } => map.len(),
1735            SchemaStoreBackend::Heap(_) => 0,
1736        }
1737    }
1738
1739    fn get_raw_snapshot_for_backend(&self, key: &RawSchemaKey) -> Option<RawSchemaSnapshot> {
1740        let SchemaStoreBackend::Journaled {
1741            canonical,
1742            live,
1743            tombstones,
1744        } = &self.backend
1745        else {
1746            return None;
1747        };
1748
1749        if tombstones.contains(key) {
1750            return None;
1751        }
1752        live.get(key).cloned().or_else(|| canonical.get(key))
1753    }
1754
1755    fn latest_raw_snapshot(&self, entity: EntityTag) -> Option<RawSchemaSnapshot> {
1756        self.latest_raw_snapshot_entry(entity)
1757            .map(|(_, snapshot)| snapshot)
1758    }
1759
1760    fn latest_raw_snapshot_entry(
1761        &self,
1762        entity: EntityTag,
1763    ) -> Option<(SchemaVersion, RawSchemaSnapshot)> {
1764        let bounds = RawSchemaKey::entity_range_bounds(entity);
1765        match &self.backend {
1766            SchemaStoreBackend::Heap(map) => map
1767                .range((bounds.0, bounds.1))
1768                .next_back()
1769                .map(|(key, snapshot)| (SchemaVersion::new(key.version()), snapshot.clone())),
1770            SchemaStoreBackend::Journaled {
1771                canonical,
1772                live,
1773                tombstones,
1774            } => {
1775                let mut latest = None;
1776                let _: Result<(), Infallible> = Self::visit_journaled_raw_snapshot_range(
1777                    canonical,
1778                    live,
1779                    tombstones,
1780                    bounds,
1781                    Direction::Desc,
1782                    |key, snapshot| {
1783                        latest = Some((SchemaVersion::new(key.version()), snapshot.clone()));
1784                        Ok(SchemaStoreVisit::Stop)
1785                    },
1786                );
1787                latest
1788            }
1789        }
1790    }
1791
1792    fn visit_journaled_raw_snapshot_range<E>(
1793        canonical: &StableBTreeMap<
1794            RawSchemaKey,
1795            RawSchemaSnapshot,
1796            VirtualMemory<DefaultMemoryImpl>,
1797        >,
1798        live: &StdBTreeMap<RawSchemaKey, RawSchemaSnapshot>,
1799        tombstones: &BTreeSet<RawSchemaKey>,
1800        bounds: (RangeBound<RawSchemaKey>, RangeBound<RawSchemaKey>),
1801        direction: Direction,
1802        mut visitor: impl FnMut(&RawSchemaKey, &RawSchemaSnapshot) -> Result<SchemaStoreVisit, E>,
1803    ) -> Result<(), E> {
1804        match direction {
1805            Direction::Asc => visit_ordered_overlay(
1806                canonical.range((bounds.0, bounds.1)),
1807                live.range((bounds.0, bounds.1)),
1808                Direction::Asc,
1809                |canonical_entry, live_entry| canonical_entry.key().cmp(live_entry.0),
1810                |canonical_entry| !tombstones.contains(canonical_entry.key()),
1811                |live_entry| !tombstones.contains(live_entry.0),
1812                |entry| {
1813                    let visit = match entry {
1814                        OrderedOverlayEntry::Canonical(canonical_entry) => {
1815                            visitor(canonical_entry.key(), &canonical_entry.value())?
1816                        }
1817                        OrderedOverlayEntry::Live((key, snapshot)) => visitor(key, snapshot)?,
1818                    };
1819                    Ok(if visit.should_stop() {
1820                        OrderedOverlayVisit::Stop
1821                    } else {
1822                        OrderedOverlayVisit::Continue
1823                    })
1824                },
1825            ),
1826            Direction::Desc => visit_ordered_overlay(
1827                canonical.range((bounds.0, bounds.1)).rev(),
1828                live.range((bounds.0, bounds.1)).rev(),
1829                Direction::Desc,
1830                |canonical_entry, live_entry| canonical_entry.key().cmp(live_entry.0),
1831                |canonical_entry| !tombstones.contains(canonical_entry.key()),
1832                |live_entry| !tombstones.contains(live_entry.0),
1833                |entry| {
1834                    let visit = match entry {
1835                        OrderedOverlayEntry::Canonical(canonical_entry) => {
1836                            visitor(canonical_entry.key(), &canonical_entry.value())?
1837                        }
1838                        OrderedOverlayEntry::Live((key, snapshot)) => visitor(key, snapshot)?,
1839                    };
1840                    Ok(if visit.should_stop() {
1841                        OrderedOverlayVisit::Stop
1842                    } else {
1843                        OrderedOverlayVisit::Continue
1844                    })
1845                },
1846            ),
1847        }
1848    }
1849}
1850
1851fn map_schema_publication_error(error: AcceptedSchemaPublicationError) -> InternalError {
1852    match error {
1853        AcceptedSchemaPublicationError::StaleSchemaRevision { .. }
1854        | AcceptedSchemaPublicationError::RevisionExhausted => InternalError::store_unsupported(),
1855        AcceptedSchemaPublicationError::InvalidCandidate => InternalError::store_invariant(),
1856        AcceptedSchemaPublicationError::CorruptRootSlots => InternalError::store_corruption(),
1857    }
1858}
1859
1860fn derive_data_allocation_metadata(
1861    latest_by_entity: &StdBTreeMap<EntityTag, (SchemaVersion, RawSchemaSnapshot)>,
1862) -> Result<SchemaStoreCatalogMetadata, InternalError> {
1863    let mut max_version = SchemaVersion::initial();
1864    let mut hasher = new_hash_sha256();
1865    write_hash_tag_u8(&mut hasher, SCHEMA_STORE_DATA_ALLOCATION_FINGERPRINT_DOMAIN);
1866
1867    for (entity, (_, snapshot)) in latest_by_entity {
1868        let persisted = snapshot.decode_persisted_snapshot()?;
1869        if persisted.version() > max_version {
1870            max_version = persisted.version();
1871        }
1872
1873        let data_projection = PersistedSchemaSnapshot::new_with_primary_key_fields_and_indexes(
1874            persisted.version(),
1875            persisted.entity_path().to_string(),
1876            persisted.entity_name().to_string(),
1877            persisted.primary_key_field_ids().to_vec(),
1878            persisted.row_layout().clone(),
1879            persisted.fields().to_vec(),
1880            Vec::new(),
1881        );
1882        let encoded = encode_persisted_schema_snapshot(&data_projection)?;
1883
1884        write_hash_u64(&mut hasher, entity.value());
1885        write_hash_u32(&mut hasher, persisted.version().get());
1886        write_hash_len_u32(&mut hasher, encoded.len());
1887        hasher.update(encoded);
1888    }
1889
1890    Ok(finalize_schema_metadata(
1891        max_version,
1892        SCHEMA_STORE_FINGERPRINT_METHOD_VERSION,
1893        hasher,
1894        latest_by_entity.len(),
1895    ))
1896}
1897
1898fn derive_index_allocation_metadata(
1899    latest_by_entity: &StdBTreeMap<EntityTag, (SchemaVersion, RawSchemaSnapshot)>,
1900) -> Result<SchemaStoreCatalogMetadata, InternalError> {
1901    let mut max_version = SchemaVersion::initial();
1902    let mut hasher = new_hash_sha256();
1903    write_hash_tag_u8(
1904        &mut hasher,
1905        SCHEMA_STORE_INDEX_ALLOCATION_FINGERPRINT_DOMAIN,
1906    );
1907
1908    for (entity, (_, snapshot)) in latest_by_entity {
1909        let persisted = snapshot.decode_persisted_snapshot()?;
1910        if persisted.version() > max_version {
1911            max_version = persisted.version();
1912        }
1913
1914        write_hash_u64(&mut hasher, entity.value());
1915        write_hash_u32(&mut hasher, persisted.version().get());
1916        write_hash_len_u32(&mut hasher, persisted.indexes().len());
1917        for index in persisted.indexes() {
1918            write_hash_u32(&mut hasher, u32::from(index.ordinal()));
1919            write_hash_str_u32(&mut hasher, index.name());
1920            write_hash_str_u32(&mut hasher, index.store());
1921            write_hash_tag_u8(&mut hasher, u8::from(index.unique()));
1922            write_hash_str_u32(&mut hasher, persisted_index_origin_name(index.origin()));
1923            match index.predicate_sql() {
1924                Some(predicate_sql) => {
1925                    write_hash_tag_u8(&mut hasher, 1);
1926                    write_hash_str_u32(&mut hasher, predicate_sql);
1927                }
1928                None => write_hash_tag_u8(&mut hasher, 0),
1929            }
1930            hash_persisted_index_key(&mut hasher, index.key());
1931        }
1932    }
1933
1934    Ok(finalize_schema_metadata(
1935        max_version,
1936        SCHEMA_STORE_FINGERPRINT_METHOD_VERSION,
1937        hasher,
1938        latest_by_entity.len(),
1939    ))
1940}
1941
1942fn derive_schema_catalog_metadata(
1943    latest_by_entity: &StdBTreeMap<EntityTag, (SchemaVersion, RawSchemaSnapshot)>,
1944) -> Result<SchemaStoreCatalogMetadata, InternalError> {
1945    let mut max_version = SchemaVersion::initial();
1946    let mut hasher = new_hash_sha256();
1947    write_hash_tag_u8(&mut hasher, SCHEMA_STORE_CATALOG_FINGERPRINT_DOMAIN);
1948
1949    for (entity, (version, snapshot)) in latest_by_entity {
1950        let persisted = snapshot.decode_persisted_snapshot()?;
1951        if persisted.version() > max_version {
1952            max_version = persisted.version();
1953        }
1954
1955        write_hash_u64(&mut hasher, entity.value());
1956        write_hash_u32(&mut hasher, version.get());
1957        write_hash_len_u32(&mut hasher, snapshot.as_bytes().len());
1958        hasher.update(snapshot.as_bytes());
1959    }
1960
1961    Ok(finalize_schema_metadata(
1962        max_version,
1963        SCHEMA_STORE_FINGERPRINT_METHOD_VERSION,
1964        hasher,
1965        latest_by_entity.len(),
1966    ))
1967}
1968
1969fn finalize_schema_metadata(
1970    schema_version: SchemaVersion,
1971    schema_fingerprint_method_version: u8,
1972    hasher: sha2::Sha256,
1973    entity_count: usize,
1974) -> SchemaStoreCatalogMetadata {
1975    let digest = finalize_hash_sha256(hasher);
1976    let mut schema_fingerprint = [0u8; 16];
1977    schema_fingerprint.copy_from_slice(&digest[..16]);
1978
1979    SchemaStoreCatalogMetadata::new(
1980        schema_version,
1981        schema_fingerprint_method_version,
1982        schema_fingerprint,
1983        u64::try_from(entity_count).unwrap_or(u64::MAX),
1984    )
1985}
1986
1987fn hash_persisted_index_key(hasher: &mut sha2::Sha256, key: &PersistedIndexKeySnapshot) {
1988    match key {
1989        PersistedIndexKeySnapshot::FieldPath(paths) => {
1990            write_hash_tag_u8(hasher, 1);
1991            write_hash_len_u32(hasher, paths.len());
1992            for path in paths {
1993                hash_persisted_index_field_path(hasher, path);
1994            }
1995        }
1996        PersistedIndexKeySnapshot::Items(items) => {
1997            write_hash_tag_u8(hasher, 2);
1998            write_hash_len_u32(hasher, items.len());
1999            for item in items {
2000                match item {
2001                    PersistedIndexKeyItemSnapshot::FieldPath(path) => {
2002                        write_hash_tag_u8(hasher, 1);
2003                        hash_persisted_index_field_path(hasher, path);
2004                    }
2005                    PersistedIndexKeyItemSnapshot::Expression(expression) => {
2006                        write_hash_tag_u8(hasher, 2);
2007                        write_hash_str_u32(hasher, persisted_expression_op_name(expression.op()));
2008                        hash_persisted_index_field_path(hasher, expression.source());
2009                        hash_accepted_field_kind(hasher, expression.input_kind());
2010                        hash_accepted_field_kind(hasher, expression.output_kind());
2011                        write_hash_str_u32(hasher, expression.canonical_text());
2012                    }
2013                }
2014            }
2015        }
2016    }
2017}
2018
2019fn hash_persisted_index_field_path(
2020    hasher: &mut sha2::Sha256,
2021    path: &crate::db::schema::PersistedIndexFieldPathSnapshot,
2022) {
2023    write_hash_u32(hasher, path.field_id().get());
2024    write_hash_u32(hasher, u32::from(path.slot().get()));
2025    write_hash_len_u32(hasher, path.path().len());
2026    for segment in path.path() {
2027        write_hash_str_u32(hasher, segment);
2028    }
2029    hash_accepted_field_kind(hasher, path.kind());
2030    write_hash_tag_u8(hasher, u8::from(path.nullable()));
2031}
2032
2033fn hash_accepted_field_kind(hasher: &mut sha2::Sha256, kind: &AcceptedFieldKind) {
2034    match kind {
2035        AcceptedFieldKind::Account => write_hash_tag_u8(hasher, 1),
2036        AcceptedFieldKind::Blob { max_len } => {
2037            write_hash_tag_u8(hasher, 2);
2038            hash_optional_u32(hasher, *max_len);
2039        }
2040        AcceptedFieldKind::Bool => write_hash_tag_u8(hasher, 3),
2041        AcceptedFieldKind::Date => write_hash_tag_u8(hasher, 4),
2042        AcceptedFieldKind::Decimal { scale } => {
2043            write_hash_tag_u8(hasher, 5);
2044            write_hash_u32(hasher, *scale);
2045        }
2046        AcceptedFieldKind::Duration => write_hash_tag_u8(hasher, 6),
2047        AcceptedFieldKind::Enum { type_id } => {
2048            write_hash_tag_u8(hasher, 7);
2049            write_hash_u32(hasher, type_id.get());
2050        }
2051        AcceptedFieldKind::Float32 => write_hash_tag_u8(hasher, 8),
2052        AcceptedFieldKind::Float64 => write_hash_tag_u8(hasher, 9),
2053        AcceptedFieldKind::Int8 => write_hash_tag_u8(hasher, 10),
2054        AcceptedFieldKind::Int16 => write_hash_tag_u8(hasher, 11),
2055        AcceptedFieldKind::Int32 => write_hash_tag_u8(hasher, 12),
2056        AcceptedFieldKind::Int64 => write_hash_tag_u8(hasher, 13),
2057        AcceptedFieldKind::Int128 => write_hash_tag_u8(hasher, 14),
2058        AcceptedFieldKind::IntBig { max_bytes } => {
2059            write_hash_tag_u8(hasher, 15);
2060            write_hash_u32(hasher, *max_bytes);
2061        }
2062        AcceptedFieldKind::Principal => write_hash_tag_u8(hasher, 16),
2063        AcceptedFieldKind::Subaccount => write_hash_tag_u8(hasher, 17),
2064        AcceptedFieldKind::Text { max_len } => {
2065            write_hash_tag_u8(hasher, 18);
2066            hash_optional_u32(hasher, *max_len);
2067        }
2068        AcceptedFieldKind::Timestamp => write_hash_tag_u8(hasher, 19),
2069        AcceptedFieldKind::Nat8 => write_hash_tag_u8(hasher, 20),
2070        AcceptedFieldKind::Nat16 => write_hash_tag_u8(hasher, 21),
2071        AcceptedFieldKind::Nat32 => write_hash_tag_u8(hasher, 22),
2072        AcceptedFieldKind::Nat64 => write_hash_tag_u8(hasher, 23),
2073        AcceptedFieldKind::Nat128 => write_hash_tag_u8(hasher, 24),
2074        AcceptedFieldKind::NatBig { max_bytes } => {
2075            write_hash_tag_u8(hasher, 25);
2076            write_hash_u32(hasher, *max_bytes);
2077        }
2078        AcceptedFieldKind::Ulid => write_hash_tag_u8(hasher, 26),
2079        AcceptedFieldKind::Unit => write_hash_tag_u8(hasher, 27),
2080        AcceptedFieldKind::Relation {
2081            target_path,
2082            target_entity_name,
2083            target_entity_tag,
2084            target_store_path,
2085            key_kind,
2086        } => {
2087            write_hash_tag_u8(hasher, 28);
2088            write_hash_str_u32(hasher, target_path);
2089            write_hash_str_u32(hasher, target_entity_name);
2090            write_hash_u64(hasher, target_entity_tag.value());
2091            write_hash_str_u32(hasher, target_store_path);
2092            hash_accepted_field_kind(hasher, key_kind);
2093        }
2094        AcceptedFieldKind::List(inner) => {
2095            write_hash_tag_u8(hasher, 29);
2096            hash_accepted_field_kind(hasher, inner);
2097        }
2098        AcceptedFieldKind::Set(inner) => {
2099            write_hash_tag_u8(hasher, 30);
2100            hash_accepted_field_kind(hasher, inner);
2101        }
2102        AcceptedFieldKind::Map { key, value } => {
2103            write_hash_tag_u8(hasher, 31);
2104            hash_accepted_field_kind(hasher, key);
2105            hash_accepted_field_kind(hasher, value);
2106        }
2107        AcceptedFieldKind::Structured { queryable } => {
2108            write_hash_tag_u8(hasher, 32);
2109            write_hash_tag_u8(hasher, u8::from(*queryable));
2110        }
2111    }
2112}
2113
2114fn hash_optional_u32(hasher: &mut sha2::Sha256, value: Option<u32>) {
2115    match value {
2116        Some(value) => {
2117            write_hash_tag_u8(hasher, 1);
2118            write_hash_u32(hasher, value);
2119        }
2120        None => write_hash_tag_u8(hasher, 0),
2121    }
2122}
2123
2124const fn persisted_index_origin_name(
2125    origin: crate::db::schema::PersistedIndexOrigin,
2126) -> &'static str {
2127    match origin {
2128        crate::db::schema::PersistedIndexOrigin::Generated => "generated",
2129        crate::db::schema::PersistedIndexOrigin::SqlDdl => "sql_ddl",
2130    }
2131}
2132
2133const fn persisted_expression_op_name(
2134    op: crate::db::schema::PersistedIndexExpressionOp,
2135) -> &'static str {
2136    match op {
2137        crate::db::schema::PersistedIndexExpressionOp::Lower => "lower",
2138        crate::db::schema::PersistedIndexExpressionOp::Upper => "upper",
2139        crate::db::schema::PersistedIndexExpressionOp::Trim => "trim",
2140        crate::db::schema::PersistedIndexExpressionOp::LowerTrim => "lower_trim",
2141        crate::db::schema::PersistedIndexExpressionOp::Date => "date",
2142        crate::db::schema::PersistedIndexExpressionOp::Year => "year",
2143        crate::db::schema::PersistedIndexExpressionOp::Month => "month",
2144        crate::db::schema::PersistedIndexExpressionOp::Day => "day",
2145    }
2146}
2147
2148///
2149/// TESTS
2150///
2151
2152#[cfg(test)]
2153mod tests;