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