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