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