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