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