Skip to main content

icydb_core/db/
integrity.rs

1//! Module: db::integrity
2//! Responsibility: bounded integrity-inspection result vocabulary and lifecycle identity.
3//! Does not own: accepted schema meaning, physical traversal, or inspection progress persistence.
4//! Boundary: database control + accepted inspection plan -> typed Quick inspection result.
5
6mod deep;
7mod derived;
8mod job;
9mod progress_store;
10mod proof;
11mod row;
12
13use crate::{
14    db::{
15        commit::{database_control_proof_identity, database_incarnation_id, ensure_recovered},
16        registry::{
17            StoreAllocationIdentities, StoreHandle, StoreRuntimeStorageCapabilities,
18            StoreRuntimeStorageMode,
19        },
20        schema::{
21            AcceptedInspectionPlan, IdentityStateLifecycle, MAX_IDENTITY_STATE_RECORDS_PER_DATABASE,
22        },
23    },
24    error::{ConstraintValuePath, ErrorClass, ErrorOrigin, InternalError},
25    traits::CanisterKind,
26};
27use candid::CandidType;
28use serde::Deserialize;
29use std::{
30    collections::BTreeMap,
31    sync::atomic::{AtomicU64, Ordering},
32};
33
34pub(in crate::db) use deep::{
35    abort_deep_integrity_job, continue_deep_integrity_job, run_next_integrity_retention_page,
36    start_deep_integrity_job,
37};
38pub(in crate::db) use derived::{
39    DerivedInspectionLimits, execute_index_integrity_page, execute_reverse_integrity_page,
40};
41pub use job::{
42    DeepIntegrityPage, DeepIntegrityPageStatus, IntegrityAbortReceipt, IntegrityAbortStatus,
43    IntegrityDeepError, IntegrityJobError, IntegrityJobId, IntegrityJobOwner, IntegrityJobReceipt,
44    IntegrityPendingTerminal, IntegritySubmissionKey, IntegrityTerminalOutcome,
45};
46pub(in crate::db) use job::{
47    IntegrityCheckpoint, IntegrityJob, IntegrityJobState, IntegrityReceiptEnvelope,
48    IntegrityReceiptReplayKey, MAX_INTEGRITY_IN_PROGRESS_PAGES,
49};
50pub(in crate::db) use proof::{IntegrityProofVector, capture_integrity_proof_vector};
51pub(in crate::db) use row::{
52    PhysicalUnitCheckpoint, RowInspectionLimits, execute_row_integrity_page,
53};
54
55pub(in crate::db) const MAX_INTEGRITY_PATH_BYTES: usize = 4 * 1024;
56
57/// One authorization-bound typed integrity operation.
58///
59/// Entity-bearing variants pin the generated selector identity that the
60/// session must match against current accepted authority. Continuation and
61/// abort carry only the opaque job identity; private checkpoints never cross
62/// this boundary.
63
64#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
65pub enum IntegrityCheckRequest {
66    /// Execute one bounded metadata/control inspection.
67    Quick {
68        /// Accepted entity selector to resolve and verify.
69        entity: IntegrityEntityIdentity,
70    },
71    /// Create or replay one idempotent Deep job.
72    DeepStart {
73        /// Accepted entity selector to resolve and verify.
74        entity: IntegrityEntityIdentity,
75        /// Owner-scoped idempotency key.
76        submission_key: IntegritySubmissionKey,
77    },
78    /// Advance or replay one retained Deep job.
79    DeepContinue {
80        /// Opaque engine-issued job identity.
81        job_id: IntegrityJobId,
82        /// Sequence of the outstanding receipt being acknowledged.
83        acknowledged_sequence: u64,
84    },
85    /// Freeze one retained Deep job for replayable abort.
86    DeepAbort {
87        /// Opaque engine-issued job identity.
88        job_id: IntegrityJobId,
89    },
90}
91
92impl IntegrityCheckRequest {
93    /// Build one Deep continuation or exact replay request.
94    #[must_use]
95    pub const fn deep_continue(job_id: IntegrityJobId, acknowledged_sequence: u64) -> Self {
96        Self::DeepContinue {
97            job_id,
98            acknowledged_sequence,
99        }
100    }
101
102    /// Build one replayable Deep-abort request.
103    #[must_use]
104    pub const fn deep_abort(job_id: IntegrityJobId) -> Self {
105        Self::DeepAbort { job_id }
106    }
107}
108
109/// Typed result shared by trusted Rust and SQL integrity frontends.
110
111#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
112pub enum IntegrityCheckResult {
113    /// Bounded one-call Quick result.
114    Quick(QuickIntegrityResult),
115    /// Start, continuation, terminal, or abort Deep receipt.
116    Deep(IntegrityJobReceipt),
117}
118
119fn validate_quick_integrity_control<C: CanisterKind>(
120    db: &crate::db::Db<C>,
121    plan: &AcceptedInspectionPlan,
122    incarnation: DatabaseIncarnationId,
123) -> Result<Vec<IntegrityFinding>, InternalError> {
124    let identity = plan.identity();
125    let source_store = db.store_handle(identity.store_path())?;
126    let relations = plan.relation_inspection();
127    let mut participating_stores =
128        BTreeMap::from([(identity.store_path().to_string(), source_store)]);
129    for relation in relations {
130        participating_stores
131            .entry(relation.target_store_path().to_string())
132            .or_insert_with(|| relation.target_store());
133    }
134
135    let _database_control = database_control_proof_identity()?;
136    proof::validate_integrity_allocation_registry()?;
137    validate_quick_identity_control(db, incarnation)?;
138    let mut findings = Vec::new();
139    for (store_path, store) in &participating_stores {
140        if let Some(finding) = validate_quick_store_control(plan, store_path, *store)? {
141            findings.push(finding);
142        }
143    }
144    for ordinal in 0..plan.index_inspection().len() {
145        let _domain = plan
146            .index_inspection()
147            .domain(ordinal, identity.entity_tag())?;
148    }
149
150    Ok(findings)
151}
152
153fn validate_quick_identity_control<C: CanisterKind>(
154    db: &crate::db::Db<C>,
155    incarnation: DatabaseIncarnationId,
156) -> Result<(), InternalError> {
157    let mut stores = db.with_store_registry(|registry| registry.iter().collect::<Vec<_>>());
158    stores.sort_by_key(|(store_path, _)| *store_path);
159
160    let mut owners = BTreeMap::new();
161    let mut state_count = 0usize;
162    for (store_path, store) in stores {
163        let states = store.with_schema(|schema_store| {
164            schema_store.identity_state_inventory_for_integrity(incarnation)
165        })?;
166        state_count = state_count
167            .checked_add(states.len())
168            .ok_or_else(InternalError::identity_state_corruption)?;
169        if state_count > MAX_IDENTITY_STATE_RECORDS_PER_DATABASE {
170            return Err(InternalError::identity_state_corruption());
171        }
172
173        for state in states {
174            let owner = state.owner();
175            record_quick_identity_owner(&mut owners, store_path, &state)?;
176            if state.lifecycle() == IdentityStateLifecycle::Active {
177                let runtime_entity = db
178                    .accepted_runtime_entity_for_tag(owner.entity_tag())
179                    .map_err(|_| InternalError::identity_state_corruption())?;
180                if runtime_entity.store_path() != store_path {
181                    return Err(InternalError::identity_state_corruption());
182                }
183            }
184        }
185    }
186
187    Ok(())
188}
189
190fn record_quick_identity_owner<'a>(
191    owners: &mut BTreeMap<(crate::types::EntityTag, crate::db::schema::FieldId), &'a str>,
192    store_path: &'a str,
193    state: &crate::db::schema::IdentityState,
194) -> Result<(), InternalError> {
195    let owner = state.owner();
196    let key = (owner.entity_tag(), owner.field_id());
197    if owners.insert(key, store_path).is_some() {
198        return Err(InternalError::identity_state_corruption());
199    }
200    Ok(())
201}
202
203fn validate_quick_store_control(
204    plan: &AcceptedInspectionPlan,
205    store_path: &str,
206    store: StoreHandle,
207) -> Result<Option<IntegrityFinding>, InternalError> {
208    let capabilities = store.storage_capabilities();
209    let allocations = store.allocation_identities();
210    match capabilities.storage_mode() {
211        StoreRuntimeStorageMode::Heap => {
212            if capabilities != StoreRuntimeStorageCapabilities::heap()
213                || allocations != StoreAllocationIdentities::absent()
214                || store.journal_tail_store().is_some()
215            {
216                return Err(InternalError::store_invariant());
217            }
218            Ok(None)
219        }
220        StoreRuntimeStorageMode::Journaled => {
221            if capabilities != StoreRuntimeStorageCapabilities::journaled()
222                || !allocations.matches_storage_capabilities(capabilities)
223            {
224                return Err(InternalError::store_invariant());
225            }
226            let journal = store
227                .journal_tail_store()
228                .ok_or_else(InternalError::store_invariant)?
229                .with_borrow(crate::db::journal::JournalTailStore::proof_identity)?;
230            if !journal.is_well_formed() {
231                return Ok(Some(quick_journal_control_finding(plan, store_path)));
232            }
233            Ok(None)
234        }
235    }
236}
237
238fn quick_journal_control_finding(
239    plan: &AcceptedInspectionPlan,
240    store_path: &str,
241) -> IntegrityFinding {
242    let error = InternalError::store_corruption();
243    IntegrityFinding {
244        diagnostic_code: error.diagnostic_code().error_code().raw(),
245        class: IntegrityFindingClass::Corruption,
246        severity: IntegritySeverity::Error,
247        kind: IntegrityFindingKind::JournalControlMismatch,
248        entity: IntegrityEntityIdentity::from_plan(plan),
249        store_path: store_path.to_string(),
250        phase: IntegrityPhase::QuickMetadata,
251        verifier_family: IntegrityVerifierFamily::JournalEnvelope,
252        physical_key: Vec::new(),
253        primary_key: None,
254        field_paths: Vec::new(),
255        value_path: None,
256        constraint_id: None,
257        constraint_name: None,
258        schema_index_id: None,
259        relation_id: None,
260        expected: Some("well-formed-journal-control".to_string()),
261        observed: Some("inconsistent-journal-control".to_string()),
262    }
263}
264
265fn relation_field_paths(plan: &AcceptedInspectionPlan, relation_id: u32) -> Vec<String> {
266    let snapshot = plan.snapshot().persisted_snapshot();
267    let Some(relation) = snapshot
268        .relations()
269        .iter()
270        .find(|relation| relation.id().get() == relation_id)
271    else {
272        return Vec::new();
273    };
274
275    relation
276        .local_field_ids()
277        .iter()
278        .filter_map(|field_id| {
279            snapshot
280                .fields()
281                .iter()
282                .find(|field| field.id() == *field_id)
283                .map(|field| field.name().to_string())
284        })
285        .collect()
286}
287
288const MAX_QUICK_RETURNED_FINDINGS: usize = 64;
289#[cfg(target_arch = "wasm32")]
290const DATABASE_INCARNATION_DOMAIN: &[u8] = b"icydb.database-incarnation.v1";
291static DATABASE_INCARNATION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
292
293/// Durable identity of one database lifecycle.
294///
295/// The identity is independent of accepted schema, row, index, relation, and
296/// journal revisions. Ordinary reopen preserves it. Any future restore,
297/// replacement, or import lane that can reuse those revisions must mint and
298/// publish a fresh identity before the restored database becomes available.
299#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
300pub struct DatabaseIncarnationId([u8; 16]);
301
302impl DatabaseIncarnationId {
303    /// Decode one current-form nonzero incarnation identity.
304    pub(crate) fn try_from_bytes(bytes: [u8; 16]) -> Result<Self, InternalError> {
305        if bytes == [0; 16] {
306            return Err(InternalError::database_incarnation_invalid());
307        }
308
309        Ok(Self(bytes))
310    }
311
312    /// Return the canonical persisted identity bytes.
313    #[must_use]
314    pub const fn to_bytes(self) -> [u8; 16] {
315        self.0
316    }
317
318    fn generate() -> Result<Self, InternalError> {
319        let sequence = DATABASE_INCARNATION_SEQUENCE
320            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
321                current.checked_add(1)
322            })
323            .map_err(|_| InternalError::database_incarnation_generation_failed())?
324            .checked_add(1)
325            .ok_or_else(InternalError::database_incarnation_generation_failed)?;
326
327        #[cfg(not(target_arch = "wasm32"))]
328        let bytes = {
329            let mut bytes = [0_u8; 16];
330            getrandom::fill(&mut bytes)
331                .map_err(|_| InternalError::database_incarnation_generation_failed())?;
332            bytes
333        };
334
335        #[cfg(target_arch = "wasm32")]
336        let bytes = {
337            use sha2::{Digest, Sha256};
338
339            let mut hasher = Sha256::new();
340            hasher.update(DATABASE_INCARNATION_DOMAIN);
341            hasher.update(ic_cdk::api::canister_self().as_slice());
342            hasher.update(ic_cdk::api::time().to_be_bytes());
343            hasher.update(sequence.to_be_bytes());
344            let digest = hasher.finalize();
345            let mut bytes = [0_u8; 16];
346            bytes.copy_from_slice(&digest[..16]);
347            bytes
348        };
349
350        let _ = sequence;
351        Self::try_from_bytes(bytes)
352    }
353
354    #[cfg(test)]
355    pub(crate) const fn for_tests(fill: u8) -> Self {
356        let mut bytes = [fill; 16];
357        if fill == 0 {
358            bytes[15] = 1;
359        }
360        Self(bytes)
361    }
362}
363
364/// Stable entity identity projected into integrity responses.
365#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
366pub struct IntegrityEntityIdentity {
367    entity_tag: u64,
368    entity_path: String,
369    store_path: String,
370}
371
372impl IntegrityEntityIdentity {
373    fn from_plan(plan: &AcceptedInspectionPlan) -> Self {
374        Self::from_accepted_identity(plan.identity_ref())
375    }
376
377    pub(in crate::db) fn from_accepted_identity(
378        identity: &crate::db::schema::AcceptedCatalogIdentity,
379    ) -> Self {
380        Self {
381            entity_tag: identity.entity_tag().value(),
382            entity_path: identity.entity_path().to_string(),
383            store_path: identity.store_path().to_string(),
384        }
385    }
386
387    pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
388        if self.entity_tag == 0
389            || self.entity_path.is_empty()
390            || self.entity_path.len() > MAX_INTEGRITY_PATH_BYTES
391            || self.store_path.is_empty()
392            || self.store_path.len() > MAX_INTEGRITY_PATH_BYTES
393        {
394            return Err(IntegrityJobError::InvalidEntityIdentity);
395        }
396        Ok(())
397    }
398
399    /// Return the stable accepted entity tag.
400    #[must_use]
401    pub const fn entity_tag(&self) -> u64 {
402        self.entity_tag
403    }
404
405    /// Borrow the accepted entity path.
406    #[must_use]
407    pub const fn entity_path(&self) -> &str {
408        self.entity_path.as_str()
409    }
410
411    /// Borrow the accepted store path.
412    #[must_use]
413    pub const fn store_path(&self) -> &str {
414        self.store_path.as_str()
415    }
416}
417
418/// Broad machine-readable accepted-authority failure class.
419#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
420pub enum IntegrityAuthorityClass {
421    /// Accepted authority bytes or closure are corrupt.
422    Corruption,
423    /// Accepted authority uses an unsupported persisted form.
424    IncompatiblePersistedFormat,
425    /// Accepted authority violates an internal invariant.
426    InvariantViolation,
427    /// The selected entity or storage contract is unsupported.
428    Unsupported,
429    /// The engine could not complete accepted-authority inspection.
430    Internal,
431}
432
433/// Broad machine-readable integrity finding class.
434#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
435pub enum IntegrityFindingClass {
436    /// Accepted or physical bytes are corrupt.
437    Corruption,
438    /// Current-form persisted bytes cannot be decoded by this build.
439    IncompatiblePersistedFormat,
440    /// A required bounded proof could not be completed.
441    ResourceLimited,
442}
443
444/// Stable semantic family of one integrity finding.
445
446#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
447pub enum IntegrityFindingKind {
448    /// The physical data key is not a valid current key for its entity interval.
449    MalformedDataKey,
450
451    /// The maintained row envelope or slot table is malformed.
452    MalformedRow,
453
454    /// The row exceeds the maintained current raw-byte bound.
455    OversizedRow,
456
457    /// One active accepted field payload violates its exact field contract.
458    InvalidFieldValue,
459
460    /// The physical key and decoded primary-key field values disagree.
461    PrimaryKeyMismatch,
462
463    /// An Identity primary key is zero or has the wrong exact unsigned shape.
464    InvalidIdentityValue,
465
466    /// A live Identity primary key is above committed high-water.
467    IdentityHighWaterExceeded,
468
469    /// One validated accepted row-local constraint is violated.
470    ConstraintViolation,
471
472    /// One row-derived active forward-index witness is absent.
473    MissingIndexEntry,
474
475    /// One row-derived active forward-index witness has invalid value bytes.
476    DivergentIndexEntry,
477
478    /// One active forward-index entry has malformed key, identity, or value framing.
479    MalformedIndexEntry,
480
481    /// One active forward-index entry points at no authoritative source row.
482    OrphanIndexEntry,
483
484    /// One unique logical key has more than one physical row witness.
485    DuplicateUniqueIndexKey,
486
487    /// One accepted relation points to an absent target row.
488    MissingRelationTarget,
489
490    /// One expected active reverse-relation witness is absent.
491    MissingReverseRelationEntry,
492
493    /// One expected active reverse-relation witness has invalid value bytes.
494    DivergentReverseRelationEntry,
495
496    /// One active reverse-relation entry has malformed key, identity, or value framing.
497    MalformedReverseRelationEntry,
498
499    /// One active reverse-relation entry points at no authoritative source row.
500    OrphanReverseRelationEntry,
501
502    /// One durable journal batch is not a valid current-form envelope.
503    MalformedJournalBatch,
504
505    /// The durable journal tail omits one or more expected sequence values.
506    JournalSequenceGap,
507
508    /// Two durable journal batches carry the same logical batch identity.
509    DuplicateJournalBatchIdentity,
510
511    /// Bounded journal control records disagree without requiring tail traversal.
512    JournalControlMismatch,
513}
514
515/// Canonical Deep inspection phase.
516
517#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
518pub enum IntegrityPhase {
519    /// Bounded accepted metadata and control closure.
520    QuickMetadata,
521
522    /// Canonical physical row storage.
523    Rows,
524
525    /// Active forward-index storage.
526    IndexEntries,
527
528    /// Active source-owned reverse-relation storage.
529    ReverseRelations,
530
531    /// Durable journal tails.
532    JournalTails,
533
534    /// Final unchanged-proof-vector comparison.
535    FinalProofVectorCheck,
536}
537
538/// Deterministic verifier family within one physical inspection unit.
539
540#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
541pub enum IntegrityVerifierFamily {
542    /// Current physical data-key framing and identity.
543    DataKey,
544
545    /// Current row envelope, layout stamp, slot count, and table framing.
546    RowEnvelope,
547
548    /// One accepted field payload or frozen historical fill.
549    FieldValue,
550
551    /// Physical key versus accepted row primary-key fields.
552    PrimaryKey,
553
554    /// Accepted Identity owner and committed high-water.
555    IdentityState,
556
557    /// Validated accepted row-local constraints.
558    ValidatedConstraints,
559
560    /// One expected active forward-index witness.
561    ForwardIndex,
562
563    /// One physical active forward-index entry.
564    IndexEntry,
565
566    /// One unique-key multiplicity proof.
567    UniqueIndex,
568
569    /// One accepted relation's target and reverse witness projection.
570    Relation,
571
572    /// One physical active source-owned reverse-relation entry.
573    ReverseRelationEntry,
574
575    /// Current durable journal batch framing and sequence continuity.
576    JournalEnvelope,
577
578    /// Durable journal batch identity uniqueness.
579    JournalBatchIdentity,
580}
581
582/// Severity of one definite integrity finding.
583#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
584pub enum IntegritySeverity {
585    /// The finding identifies invalid maintained state.
586    Error,
587    /// The finding is an operator advisory and does not invalidate a clean proof.
588    Advisory,
589}
590
591/// One bounded machine-readable integrity finding.
592///
593/// Raw row payloads and unbounded application values are deliberately absent.
594#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
595pub struct IntegrityFinding {
596    diagnostic_code: u16,
597    class: IntegrityFindingClass,
598    severity: IntegritySeverity,
599    kind: IntegrityFindingKind,
600    entity: IntegrityEntityIdentity,
601    store_path: String,
602    phase: IntegrityPhase,
603    verifier_family: IntegrityVerifierFamily,
604    physical_key: Vec<u8>,
605    primary_key: Option<Vec<u8>>,
606    field_paths: Vec<String>,
607    value_path: Option<Box<ConstraintValuePath>>,
608    constraint_id: Option<u32>,
609    constraint_name: Option<String>,
610    schema_index_id: Option<u32>,
611    relation_id: Option<u32>,
612    expected: Option<String>,
613    observed: Option<String>,
614}
615
616impl IntegrityFinding {
617    /// Return the stable compact diagnostic code.
618    #[must_use]
619    pub const fn diagnostic_code(&self) -> u16 {
620        self.diagnostic_code
621    }
622
623    /// Return the broad finding class.
624    #[must_use]
625    pub const fn class(&self) -> IntegrityFindingClass {
626        self.class
627    }
628
629    /// Return the finding severity.
630    #[must_use]
631    pub const fn severity(&self) -> IntegritySeverity {
632        self.severity
633    }
634
635    /// Return the stable semantic finding family.
636    #[must_use]
637    pub const fn kind(&self) -> IntegrityFindingKind {
638        self.kind
639    }
640
641    /// Borrow the accepted entity identity.
642    #[must_use]
643    pub const fn entity(&self) -> &IntegrityEntityIdentity {
644        &self.entity
645    }
646
647    /// Borrow the affected store path.
648    #[must_use]
649    pub const fn store_path(&self) -> &str {
650        self.store_path.as_str()
651    }
652
653    /// Return the Deep phase that observed this finding.
654    #[must_use]
655    pub const fn phase(&self) -> IntegrityPhase {
656        self.phase
657    }
658
659    /// Return the deterministic verifier family that observed this finding.
660    #[must_use]
661    pub const fn verifier_family(&self) -> IntegrityVerifierFamily {
662        self.verifier_family
663    }
664
665    /// Borrow the bounded exact physical key.
666    #[must_use]
667    pub const fn physical_key(&self) -> &[u8] {
668        self.physical_key.as_slice()
669    }
670
671    /// Borrow the canonical primary-key suffix after successful key decoding.
672    #[must_use]
673    pub fn primary_key(&self) -> Option<&[u8]> {
674        self.primary_key.as_deref()
675    }
676
677    /// Borrow bounded accepted field paths relevant to the finding.
678    #[must_use]
679    pub const fn field_paths(&self) -> &[String] {
680        self.field_paths.as_slice()
681    }
682
683    /// Borrow the concrete accepted value path for targeted-rule findings.
684    #[must_use]
685    pub fn value_path(&self) -> Option<&ConstraintValuePath> {
686        self.value_path.as_deref()
687    }
688
689    /// Return the accepted constraint identity when applicable.
690    #[must_use]
691    pub const fn constraint_id(&self) -> Option<u32> {
692        self.constraint_id
693    }
694
695    /// Borrow the accepted constraint name when applicable.
696    #[must_use]
697    pub fn constraint_name(&self) -> Option<&str> {
698        self.constraint_name.as_deref()
699    }
700
701    /// Return the accepted logical index identity when applicable.
702    #[must_use]
703    pub const fn schema_index_id(&self) -> Option<u32> {
704        self.schema_index_id
705    }
706
707    /// Return the accepted relation identity when applicable.
708    #[must_use]
709    pub const fn relation_id(&self) -> Option<u32> {
710        self.relation_id
711    }
712
713    /// Borrow the bounded expected-state label, when applicable.
714    #[must_use]
715    pub fn expected(&self) -> Option<&str> {
716        self.expected.as_deref()
717    }
718
719    /// Borrow the bounded observed-state label, when applicable.
720    #[must_use]
721    pub fn observed(&self) -> Option<&str> {
722        self.observed.as_deref()
723    }
724}
725
726/// Typed reason that accepted authority could not be inspected.
727#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
728pub struct IntegrityAuthorityDiagnostic {
729    diagnostic_code: u16,
730    class: IntegrityAuthorityClass,
731}
732
733impl IntegrityAuthorityDiagnostic {
734    pub(in crate::db) fn from_internal(error: &InternalError) -> Self {
735        let class = match error.class {
736            ErrorClass::Corruption => IntegrityAuthorityClass::Corruption,
737            ErrorClass::IncompatiblePersistedFormat => {
738                IntegrityAuthorityClass::IncompatiblePersistedFormat
739            }
740            ErrorClass::InvariantViolation => IntegrityAuthorityClass::InvariantViolation,
741            ErrorClass::Unsupported | ErrorClass::NotFound | ErrorClass::Conflict => {
742                IntegrityAuthorityClass::Unsupported
743            }
744            ErrorClass::Internal => IntegrityAuthorityClass::Internal,
745        };
746        Self {
747            diagnostic_code: error.diagnostic_code().error_code().raw(),
748            class,
749        }
750    }
751
752    /// Return the stable compact diagnostic code.
753    #[must_use]
754    pub const fn diagnostic_code(&self) -> u16 {
755        self.diagnostic_code
756    }
757
758    /// Return the broad failure class.
759    #[must_use]
760    pub const fn class(&self) -> IntegrityAuthorityClass {
761        self.class
762    }
763}
764
765/// Typed bounded-resource failure.
766#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
767pub struct IntegrityResourceDiagnostic {
768    diagnostic_code: u16,
769}
770
771impl IntegrityResourceDiagnostic {
772    /// Return the stable compact diagnostic code.
773    #[must_use]
774    pub const fn diagnostic_code(&self) -> u16 {
775        self.diagnostic_code
776    }
777}
778
779/// Outcome of one bounded Quick integrity inspection.
780#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
781pub enum QuickIntegrityStatus {
782    /// Every bounded Quick family was inspected without findings.
783    CompleteClean,
784    /// Every bounded Quick family was inspected and definite findings exist.
785    CompleteWithFindings,
786    /// Load-bearing accepted authority could not be inspected.
787    Uninspectable(IntegrityAuthorityDiagnostic),
788    /// The minimum bounded inspection atom could not be completed.
789    ResourceLimited(IntegrityResourceDiagnostic),
790}
791
792/// Complete result of one bounded accepted-native Quick inspection.
793#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
794pub struct QuickIntegrityResult {
795    entity: IntegrityEntityIdentity,
796    database_incarnation_id: DatabaseIncarnationId,
797    accepted_schema_version: u32,
798    accepted_schema_fingerprint: [u8; 16],
799    status: QuickIntegrityStatus,
800    total_findings: u64,
801    omitted_findings: u64,
802    findings: Vec<IntegrityFinding>,
803}
804
805impl QuickIntegrityResult {
806    /// Borrow the accepted entity identity.
807    #[must_use]
808    pub const fn entity(&self) -> &IntegrityEntityIdentity {
809        &self.entity
810    }
811
812    /// Return the durable database incarnation inspected by this call.
813    #[must_use]
814    pub const fn database_incarnation_id(&self) -> DatabaseIncarnationId {
815        self.database_incarnation_id
816    }
817
818    /// Return the accepted entity schema version.
819    #[must_use]
820    pub const fn accepted_schema_version(&self) -> u32 {
821        self.accepted_schema_version
822    }
823
824    /// Return the accepted entity schema fingerprint.
825    #[must_use]
826    pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
827        self.accepted_schema_fingerprint
828    }
829
830    /// Borrow the Quick completion status.
831    #[must_use]
832    pub const fn status(&self) -> &QuickIntegrityStatus {
833        &self.status
834    }
835
836    /// Return the exact number of findings observed.
837    #[must_use]
838    pub const fn total_findings(&self) -> u64 {
839        self.total_findings
840    }
841
842    /// Return the number of findings omitted from the bounded response prefix.
843    #[must_use]
844    pub const fn omitted_findings(&self) -> u64 {
845        self.omitted_findings
846    }
847
848    /// Borrow the bounded canonical finding prefix.
849    #[must_use]
850    pub const fn findings(&self) -> &[IntegrityFinding] {
851        self.findings.as_slice()
852    }
853}
854
855struct QuickIntegrityAccumulator {
856    total_findings: u64,
857    findings: Vec<IntegrityFinding>,
858}
859
860impl QuickIntegrityAccumulator {
861    const fn new() -> Self {
862        Self {
863            total_findings: 0,
864            findings: Vec::new(),
865        }
866    }
867
868    fn record(&mut self, finding: IntegrityFinding) -> Result<(), IntegrityResourceDiagnostic> {
869        self.total_findings =
870            self.total_findings
871                .checked_add(1)
872                .ok_or(IntegrityResourceDiagnostic {
873                    diagnostic_code: icydb_diagnostic_code::ErrorCode::RUNTIME_INTERNAL.raw(),
874                })?;
875        if self.findings.len() < MAX_QUICK_RETURNED_FINDINGS {
876            self.findings.push(finding);
877        }
878        Ok(())
879    }
880
881    fn complete(
882        self,
883        plan: &AcceptedInspectionPlan,
884        incarnation: DatabaseIncarnationId,
885    ) -> Result<QuickIntegrityResult, InternalError> {
886        let status = if self.total_findings == 0 {
887            QuickIntegrityStatus::CompleteClean
888        } else {
889            QuickIntegrityStatus::CompleteWithFindings
890        };
891        let omitted_findings = self.omitted_findings()?;
892        let identity = plan.identity();
893
894        Ok(QuickIntegrityResult {
895            entity: IntegrityEntityIdentity::from_plan(plan),
896            database_incarnation_id: incarnation,
897            accepted_schema_version: identity.accepted_schema_version().get(),
898            accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
899            status,
900            total_findings: self.total_findings,
901            omitted_findings,
902            findings: self.findings,
903        })
904    }
905
906    fn resource_limited(
907        self,
908        plan: &AcceptedInspectionPlan,
909        incarnation: DatabaseIncarnationId,
910        diagnostic: IntegrityResourceDiagnostic,
911    ) -> Result<QuickIntegrityResult, InternalError> {
912        let omitted_findings = self.omitted_findings()?;
913        let identity = plan.identity();
914
915        Ok(QuickIntegrityResult {
916            entity: IntegrityEntityIdentity::from_plan(plan),
917            database_incarnation_id: incarnation,
918            accepted_schema_version: identity.accepted_schema_version().get(),
919            accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
920            status: QuickIntegrityStatus::ResourceLimited(diagnostic),
921            total_findings: self.total_findings,
922            omitted_findings,
923            findings: self.findings,
924        })
925    }
926
927    fn omitted_findings(&self) -> Result<u64, InternalError> {
928        let returned = u64::try_from(self.findings.len()).map_err(|_| {
929            InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
930        })?;
931        self.total_findings.checked_sub(returned).ok_or_else(|| {
932            InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
933        })
934    }
935}
936
937pub(in crate::db) fn uninspectable_quick_integrity(
938    identity: crate::db::schema::AcceptedCatalogIdentity,
939    incarnation: DatabaseIncarnationId,
940    error: &InternalError,
941) -> QuickIntegrityResult {
942    QuickIntegrityResult {
943        entity: IntegrityEntityIdentity::from_accepted_identity(&identity),
944        database_incarnation_id: incarnation,
945        accepted_schema_version: identity.accepted_schema_version().get(),
946        accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
947        status: QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(
948            error,
949        )),
950        total_findings: 0,
951        omitted_findings: 0,
952        findings: Vec::new(),
953    }
954}
955
956pub(in crate::db) fn execute_quick_integrity<C: CanisterKind>(
957    db: &crate::db::Db<C>,
958    plan: &AcceptedInspectionPlan,
959) -> Result<QuickIntegrityResult, InternalError> {
960    ensure_recovered(db)?;
961    let incarnation = database_incarnation_id()?;
962    let findings = match validate_quick_integrity_control(db, plan, incarnation) {
963        Ok(findings) => findings,
964        Err(error) => {
965            return Ok(uninspectable_quick_integrity(
966                plan.identity(),
967                incarnation,
968                &error,
969            ));
970        }
971    };
972    let mut accumulator = QuickIntegrityAccumulator::new();
973    for finding in findings {
974        if let Err(diagnostic) = accumulator.record(finding) {
975            return accumulator.resource_limited(plan, incarnation, diagnostic);
976        }
977    }
978
979    accumulator.complete(plan, incarnation)
980}
981
982pub(crate) fn generate_database_incarnation_id() -> Result<DatabaseIncarnationId, InternalError> {
983    DatabaseIncarnationId::generate()
984}
985
986#[cfg(test)]
987mod tests {
988    use super::*;
989    use crate::{
990        db::schema::{FieldStorageDecode, LeafCodec, ScalarCodec},
991        db::{
992            commit::CommitSchemaFingerprint,
993            schema::{
994                AcceptedCatalogIdentity, AcceptedCompositeCatalog, AcceptedFieldKind,
995                AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
996                FieldId, IdentityState, IdentityStateOwner, PersistedFieldSnapshot,
997                PersistedSchemaSnapshot, SchemaFieldSlot, SchemaInsertDefault, SchemaRowLayout,
998                SchemaVersion, empty_accepted_enum_catalog_for_tests,
999            },
1000        },
1001        types::EntityTag,
1002    };
1003
1004    fn plan() -> AcceptedInspectionPlan {
1005        let revision = AcceptedSchemaRevision::INITIAL;
1006        let identity = AcceptedCatalogIdentity::new(
1007            EntityTag::new(23),
1008            "tests::QuickEntity",
1009            "tests::QuickStore",
1010            revision,
1011            SchemaVersion::initial(),
1012            CommitSchemaFingerprint::from([0x44; 16]),
1013        );
1014        let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
1015            SchemaVersion::initial(),
1016            "tests::QuickEntity".to_string(),
1017            "QuickEntity".to_string(),
1018            FieldId::new(1),
1019            SchemaRowLayout::initial(vec![(FieldId::new(1), SchemaFieldSlot::new(0))]),
1020            vec![PersistedFieldSnapshot::new_initial(
1021                FieldId::new(1),
1022                "id".to_string(),
1023                SchemaFieldSlot::new(0),
1024                AcceptedFieldKind::Nat64,
1025                Vec::new(),
1026                false,
1027                SchemaInsertDefault::None,
1028                FieldStorageDecode::ByKind,
1029                LeafCodec::Scalar(ScalarCodec::Nat64),
1030            )],
1031        ));
1032        let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
1033            empty_accepted_enum_catalog_for_tests(),
1034            AcceptedCompositeCatalog::empty(),
1035            revision,
1036        );
1037
1038        AcceptedInspectionPlan::compile_relation_free_for_tests(identity, snapshot, value_catalog)
1039            .expect("accepted Quick plan should compile")
1040    }
1041
1042    fn finding(plan: &AcceptedInspectionPlan) -> IntegrityFinding {
1043        IntegrityFinding {
1044            diagnostic_code: icydb_diagnostic_code::ErrorCode::STORE_CORRUPTION.raw(),
1045            class: IntegrityFindingClass::Corruption,
1046            severity: IntegritySeverity::Error,
1047            kind: IntegrityFindingKind::MalformedRow,
1048            entity: IntegrityEntityIdentity::from_plan(plan),
1049            store_path: plan.identity().store_path().to_string(),
1050            phase: IntegrityPhase::Rows,
1051            verifier_family: IntegrityVerifierFamily::RowEnvelope,
1052            physical_key: vec![1],
1053            primary_key: None,
1054            field_paths: Vec::new(),
1055            value_path: None,
1056            constraint_id: None,
1057            constraint_name: None,
1058            schema_index_id: None,
1059            relation_id: None,
1060            expected: None,
1061            observed: None,
1062        }
1063    }
1064
1065    #[test]
1066    fn database_incarnation_rejects_zero_and_round_trips_current_bytes() {
1067        assert!(DatabaseIncarnationId::try_from_bytes([0; 16]).is_err());
1068
1069        let identity = DatabaseIncarnationId::for_tests(7);
1070        assert_eq!(
1071            DatabaseIncarnationId::try_from_bytes(identity.to_bytes())
1072                .expect("nonzero incarnation should decode"),
1073            identity,
1074        );
1075    }
1076
1077    #[test]
1078    fn integrity_finding_candid_preserves_targeted_constraint_path() {
1079        let plan = plan();
1080        let mut finding = finding(&plan);
1081        let path = ConstraintValuePath::new(vec![
1082            crate::error::ConstraintValuePathComponent::RootField { field_id: 1 },
1083            crate::error::ConstraintValuePathComponent::ListElement { index: 2 },
1084        ]);
1085        finding.kind = IntegrityFindingKind::ConstraintViolation;
1086        finding.value_path = Some(Box::new(path.clone()));
1087        finding.constraint_id = Some(7);
1088        finding.constraint_name = Some("nested_limit".to_string());
1089
1090        let bytes = candid::encode_one(&finding).expect("integrity finding should encode");
1091        let decoded: IntegrityFinding =
1092            candid::decode_one(&bytes).expect("integrity finding should decode");
1093        assert_eq!(decoded.value_path(), Some(&path));
1094        assert_eq!(decoded.constraint_id(), Some(7));
1095        assert_eq!(decoded.constraint_name(), Some("nested_limit"));
1096    }
1097
1098    #[test]
1099    fn quick_clean_result_binds_incarnation_and_accepted_plan_identity() {
1100        let plan = plan();
1101        let incarnation = DatabaseIncarnationId::for_tests(8);
1102        let result = QuickIntegrityAccumulator::new()
1103            .complete(&plan, incarnation)
1104            .expect("clean Quick accounting should remain valid");
1105
1106        assert_eq!(result.status(), &QuickIntegrityStatus::CompleteClean);
1107        assert_eq!(result.database_incarnation_id(), incarnation);
1108        assert_eq!(result.accepted_schema_version(), 1);
1109        assert_eq!(result.accepted_schema_fingerprint(), [0x44; 16]);
1110        assert_eq!(result.total_findings(), 0);
1111        assert_eq!(result.omitted_findings(), 0);
1112    }
1113
1114    #[test]
1115    fn quick_findings_keep_a_bounded_prefix_and_exact_omitted_count() {
1116        let plan = plan();
1117        let mut accumulator = QuickIntegrityAccumulator::new();
1118        for _ in 0..=MAX_QUICK_RETURNED_FINDINGS {
1119            accumulator
1120                .record(finding(&plan))
1121                .expect("bounded test finding count should fit");
1122        }
1123        let result = accumulator
1124            .complete(&plan, DatabaseIncarnationId::for_tests(9))
1125            .expect("one-over-cap Quick accounting should remain valid");
1126
1127        assert_eq!(result.status(), &QuickIntegrityStatus::CompleteWithFindings,);
1128        assert_eq!(result.total_findings(), 65);
1129        assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1130        assert_eq!(result.omitted_findings(), 1);
1131        assert_eq!(
1132            result.total_findings(),
1133            result.findings().len() as u64 + result.omitted_findings(),
1134        );
1135    }
1136
1137    #[test]
1138    fn quick_findings_at_the_exact_returned_cap_have_no_omissions() {
1139        let plan = plan();
1140        let mut accumulator = QuickIntegrityAccumulator::new();
1141        for _ in 0..MAX_QUICK_RETURNED_FINDINGS {
1142            accumulator
1143                .record(finding(&plan))
1144                .expect("exact-cap finding count should fit");
1145        }
1146        let result = accumulator
1147            .complete(&plan, DatabaseIncarnationId::for_tests(10))
1148            .expect("exact-cap Quick accounting should remain valid");
1149
1150        assert_eq!(result.total_findings(), 64);
1151        assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1152        assert_eq!(result.omitted_findings(), 0);
1153    }
1154
1155    #[test]
1156    fn quick_selected_authority_failure_is_not_a_clean_completion() {
1157        let plan = plan();
1158        let error = InternalError::accepted_row_constraint_program_corrupt();
1159        let result = uninspectable_quick_integrity(
1160            plan.identity(),
1161            DatabaseIncarnationId::for_tests(11),
1162            &error,
1163        );
1164
1165        assert!(matches!(
1166            result.status(),
1167            QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic {
1168                class: IntegrityAuthorityClass::Corruption,
1169                ..
1170            }),
1171        ));
1172        assert_eq!(result.total_findings(), 0);
1173        assert_eq!(result.omitted_findings(), 0);
1174    }
1175
1176    #[test]
1177    fn quick_identity_inventory_rejects_active_retired_owner_collision_first() {
1178        let incarnation = DatabaseIncarnationId::for_tests(12);
1179        let owner = IdentityStateOwner::try_new(incarnation, EntityTag::new(31), FieldId::new(1))
1180            .expect("identity owner should admit");
1181        let active = IdentityState::new_active(owner, AcceptedFieldKind::Nat64)
1182            .expect("active identity state should admit");
1183        let retired = active.retire().expect("active state should retire");
1184        let mut owners = BTreeMap::new();
1185
1186        record_quick_identity_owner(&mut owners, "tests::first", &active)
1187            .expect("the first owner should admit");
1188        let error = record_quick_identity_owner(&mut owners, "tests::second", &retired)
1189            .expect_err("an active/retired owner collision must reject");
1190
1191        assert_eq!(error.class(), ErrorClass::Corruption);
1192        assert_eq!(error.origin(), ErrorOrigin::Identity);
1193    }
1194}