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