1mod 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};
28use candid::CandidType;
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#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
79pub enum IntegrityCheckRequest {
80 Quick {
82 entity: IntegrityEntityIdentity,
84 },
85 DeepStart {
87 entity: IntegrityEntityIdentity,
89 submission_key: IntegritySubmissionKey,
91 },
92 DeepContinue {
94 job_id: IntegrityJobId,
96 acknowledged_sequence: u64,
98 },
99 DeepAbort {
101 job_id: IntegrityJobId,
103 },
104}
105
106impl IntegrityCheckRequest {
107 #[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 #[must_use]
118 pub const fn deep_abort(job_id: IntegrityJobId) -> Self {
119 Self::DeepAbort { job_id }
120 }
121}
122
123#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
126pub enum IntegrityCheckResult {
127 Quick(QuickIntegrityResult),
129 Deep(IntegrityJobReceipt),
131}
132
133fn 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 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#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
325pub struct DatabaseIncarnationId([u8; 16]);
326
327impl DatabaseIncarnationId {
328 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 #[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.88."
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 hasher.update(ic_cdk::api::canister_self().as_slice());
371 hasher.update(ic_cdk::api::time().to_be_bytes());
372 hasher.update(sequence.to_be_bytes());
373 let digest = hasher.finalize();
374 let mut bytes = [0_u8; 16];
375 bytes.copy_from_slice(&digest[..16]);
376 bytes
377 };
378
379 let _ = sequence;
380 Self::try_from_bytes(bytes)
381 }
382
383 #[cfg(test)]
384 pub(crate) const fn for_tests(fill: u8) -> Self {
385 let mut bytes = [fill; 16];
386 if fill == 0 {
387 bytes[15] = 1;
388 }
389 Self(bytes)
390 }
391}
392
393#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
395pub struct IntegrityEntityIdentity {
396 entity_tag: u64,
397 entity_path: String,
398 store_path: String,
399}
400
401impl IntegrityEntityIdentity {
402 fn from_plan(plan: &AcceptedInspectionPlan) -> Self {
403 Self::from_accepted_identity(plan.identity_ref())
404 }
405
406 pub(in crate::db) fn from_accepted_identity(
407 identity: &crate::db::schema::AcceptedCatalogIdentity,
408 ) -> Self {
409 Self {
410 entity_tag: identity.entity_tag().value(),
411 entity_path: identity.entity_path().to_string(),
412 store_path: identity.store_path().to_string(),
413 }
414 }
415
416 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
417 if self.entity_tag == 0
418 || self.entity_path.is_empty()
419 || self.entity_path.len() > MAX_INTEGRITY_PATH_BYTES
420 || self.store_path.is_empty()
421 || self.store_path.len() > MAX_INTEGRITY_PATH_BYTES
422 {
423 return Err(IntegrityJobError::InvalidEntityIdentity);
424 }
425 Ok(())
426 }
427
428 #[must_use]
430 pub const fn entity_tag(&self) -> u64 {
431 self.entity_tag
432 }
433
434 #[must_use]
436 pub const fn entity_path(&self) -> &str {
437 self.entity_path.as_str()
438 }
439
440 #[must_use]
442 pub const fn store_path(&self) -> &str {
443 self.store_path.as_str()
444 }
445}
446
447#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
449pub enum IntegrityAuthorityClass {
450 Corruption,
452 IncompatiblePersistedFormat,
454 InvariantViolation,
456 Unsupported,
458 Internal,
460}
461
462#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
464pub enum IntegrityFindingClass {
465 Corruption,
467 IncompatiblePersistedFormat,
469 ResourceLimited,
471}
472
473#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
476pub enum IntegrityFindingKind {
477 MalformedDataKey,
479
480 MalformedRow,
482
483 OversizedRow,
485
486 InvalidFieldValue,
488
489 PrimaryKeyMismatch,
491
492 InvalidIdentityValue,
494
495 IdentityHighWaterExceeded,
497
498 ConstraintViolation,
500
501 MissingIndexEntry,
503
504 DivergentIndexEntry,
506
507 MalformedIndexEntry,
509
510 OrphanIndexEntry,
512
513 DuplicateUniqueIndexKey,
515
516 MissingRelationTarget,
518
519 MissingReverseRelationEntry,
521
522 DivergentReverseRelationEntry,
524
525 MalformedReverseRelationEntry,
527
528 OrphanReverseRelationEntry,
530
531 MalformedJournalBatch,
533
534 JournalSequenceGap,
536
537 DuplicateJournalBatchIdentity,
539
540 JournalControlMismatch,
542}
543
544#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
547pub enum IntegrityPhase {
548 QuickMetadata,
550
551 Rows,
553
554 IndexEntries,
556
557 ReverseRelations,
559
560 JournalTails,
562
563 FinalProofVectorCheck,
565}
566
567#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
570pub enum IntegrityVerifierFamily {
571 DataKey,
573
574 RowEnvelope,
576
577 FieldValue,
579
580 PrimaryKey,
582
583 IdentityState,
585
586 ValidatedConstraints,
588
589 ForwardIndex,
591
592 IndexEntry,
594
595 UniqueIndex,
597
598 Relation,
600
601 ReverseRelationEntry,
603
604 JournalEnvelope,
606
607 JournalBatchIdentity,
609}
610
611#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
613pub enum IntegritySeverity {
614 Error,
616 Advisory,
618}
619
620#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
624pub struct IntegrityFinding {
625 diagnostic_code: u16,
626 class: IntegrityFindingClass,
627 severity: IntegritySeverity,
628 kind: IntegrityFindingKind,
629 entity: IntegrityEntityIdentity,
630 store_path: String,
631 phase: IntegrityPhase,
632 verifier_family: IntegrityVerifierFamily,
633 physical_key: Vec<u8>,
634 primary_key: Option<Vec<u8>>,
635 field_paths: Vec<String>,
636 value_path: Option<Box<ConstraintValuePath>>,
637 constraint_id: Option<u32>,
638 constraint_name: Option<String>,
639 schema_index_id: Option<u32>,
640 relation_id: Option<u32>,
641 expected: Option<String>,
642 observed: Option<String>,
643}
644
645impl IntegrityFinding {
646 #[must_use]
648 pub const fn diagnostic_code(&self) -> u16 {
649 self.diagnostic_code
650 }
651
652 #[must_use]
654 pub const fn class(&self) -> IntegrityFindingClass {
655 self.class
656 }
657
658 #[must_use]
660 pub const fn severity(&self) -> IntegritySeverity {
661 self.severity
662 }
663
664 #[must_use]
666 pub const fn kind(&self) -> IntegrityFindingKind {
667 self.kind
668 }
669
670 #[must_use]
672 pub const fn entity(&self) -> &IntegrityEntityIdentity {
673 &self.entity
674 }
675
676 #[must_use]
678 pub const fn store_path(&self) -> &str {
679 self.store_path.as_str()
680 }
681
682 #[must_use]
684 pub const fn phase(&self) -> IntegrityPhase {
685 self.phase
686 }
687
688 #[must_use]
690 pub const fn verifier_family(&self) -> IntegrityVerifierFamily {
691 self.verifier_family
692 }
693
694 #[must_use]
696 pub const fn physical_key(&self) -> &[u8] {
697 self.physical_key.as_slice()
698 }
699
700 #[must_use]
702 pub fn primary_key(&self) -> Option<&[u8]> {
703 self.primary_key.as_deref()
704 }
705
706 #[must_use]
708 pub const fn field_paths(&self) -> &[String] {
709 self.field_paths.as_slice()
710 }
711
712 #[must_use]
714 pub fn value_path(&self) -> Option<&ConstraintValuePath> {
715 self.value_path.as_deref()
716 }
717
718 #[must_use]
720 pub const fn constraint_id(&self) -> Option<u32> {
721 self.constraint_id
722 }
723
724 #[must_use]
726 pub fn constraint_name(&self) -> Option<&str> {
727 self.constraint_name.as_deref()
728 }
729
730 #[must_use]
732 pub const fn schema_index_id(&self) -> Option<u32> {
733 self.schema_index_id
734 }
735
736 #[must_use]
738 pub const fn relation_id(&self) -> Option<u32> {
739 self.relation_id
740 }
741
742 #[must_use]
744 pub fn expected(&self) -> Option<&str> {
745 self.expected.as_deref()
746 }
747
748 #[must_use]
750 pub fn observed(&self) -> Option<&str> {
751 self.observed.as_deref()
752 }
753}
754
755#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
757pub struct IntegrityAuthorityDiagnostic {
758 diagnostic_code: u16,
759 class: IntegrityAuthorityClass,
760}
761
762impl IntegrityAuthorityDiagnostic {
763 pub(in crate::db) fn from_internal(error: &InternalError) -> Self {
764 let class = match error.class {
765 ErrorClass::Corruption => IntegrityAuthorityClass::Corruption,
766 ErrorClass::IncompatiblePersistedFormat => {
767 IntegrityAuthorityClass::IncompatiblePersistedFormat
768 }
769 ErrorClass::InvariantViolation => IntegrityAuthorityClass::InvariantViolation,
770 ErrorClass::Unsupported | ErrorClass::NotFound | ErrorClass::Conflict => {
771 IntegrityAuthorityClass::Unsupported
772 }
773 ErrorClass::Internal => IntegrityAuthorityClass::Internal,
774 };
775 Self {
776 diagnostic_code: error.diagnostic_code().error_code().raw(),
777 class,
778 }
779 }
780
781 #[must_use]
783 pub const fn diagnostic_code(&self) -> u16 {
784 self.diagnostic_code
785 }
786
787 #[must_use]
789 pub const fn class(&self) -> IntegrityAuthorityClass {
790 self.class
791 }
792}
793
794#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
796pub struct IntegrityResourceDiagnostic {
797 diagnostic_code: u16,
798}
799
800impl IntegrityResourceDiagnostic {
801 #[must_use]
803 pub const fn diagnostic_code(&self) -> u16 {
804 self.diagnostic_code
805 }
806}
807
808#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
810pub enum QuickIntegrityStatus {
811 CompleteClean,
813 CompleteWithFindings,
815 Uninspectable(IntegrityAuthorityDiagnostic),
817 ResourceLimited(IntegrityResourceDiagnostic),
819}
820
821#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
823pub struct QuickIntegrityResult {
824 entity: IntegrityEntityIdentity,
825 database_incarnation_id: DatabaseIncarnationId,
826 accepted_schema_version: u32,
827 accepted_schema_fingerprint: [u8; 16],
828 status: QuickIntegrityStatus,
829 total_findings: u64,
830 omitted_findings: u64,
831 findings: Vec<IntegrityFinding>,
832}
833
834impl QuickIntegrityResult {
835 #[must_use]
837 pub const fn entity(&self) -> &IntegrityEntityIdentity {
838 &self.entity
839 }
840
841 #[must_use]
843 pub const fn database_incarnation_id(&self) -> DatabaseIncarnationId {
844 self.database_incarnation_id
845 }
846
847 #[must_use]
849 pub const fn accepted_schema_version(&self) -> u32 {
850 self.accepted_schema_version
851 }
852
853 #[must_use]
855 pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
856 self.accepted_schema_fingerprint
857 }
858
859 #[must_use]
861 pub const fn status(&self) -> &QuickIntegrityStatus {
862 &self.status
863 }
864
865 #[must_use]
867 pub const fn total_findings(&self) -> u64 {
868 self.total_findings
869 }
870
871 #[must_use]
873 pub const fn omitted_findings(&self) -> u64 {
874 self.omitted_findings
875 }
876
877 #[must_use]
879 pub const fn findings(&self) -> &[IntegrityFinding] {
880 self.findings.as_slice()
881 }
882}
883
884struct QuickIntegrityAccumulator {
885 total_findings: u64,
886 findings: Vec<IntegrityFinding>,
887}
888
889impl QuickIntegrityAccumulator {
890 const fn new() -> Self {
891 Self {
892 total_findings: 0,
893 findings: Vec::new(),
894 }
895 }
896
897 fn record(&mut self, finding: IntegrityFinding) -> Result<(), IntegrityResourceDiagnostic> {
898 self.total_findings =
899 self.total_findings
900 .checked_add(1)
901 .ok_or(IntegrityResourceDiagnostic {
902 diagnostic_code: icydb_diagnostic_code::ErrorCode::RUNTIME_INTERNAL.raw(),
903 })?;
904 if self.findings.len() < MAX_QUICK_RETURNED_FINDINGS {
905 self.findings.push(finding);
906 }
907 Ok(())
908 }
909
910 fn complete(
911 self,
912 plan: &AcceptedInspectionPlan,
913 incarnation: DatabaseIncarnationId,
914 ) -> Result<QuickIntegrityResult, InternalError> {
915 let status = if self.total_findings == 0 {
916 QuickIntegrityStatus::CompleteClean
917 } else {
918 QuickIntegrityStatus::CompleteWithFindings
919 };
920 let omitted_findings = self.omitted_findings()?;
921 let identity = plan.identity();
922
923 Ok(QuickIntegrityResult {
924 entity: IntegrityEntityIdentity::from_plan(plan),
925 database_incarnation_id: incarnation,
926 accepted_schema_version: identity.accepted_schema_version().get(),
927 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
928 status,
929 total_findings: self.total_findings,
930 omitted_findings,
931 findings: self.findings,
932 })
933 }
934
935 fn resource_limited(
936 self,
937 plan: &AcceptedInspectionPlan,
938 incarnation: DatabaseIncarnationId,
939 diagnostic: IntegrityResourceDiagnostic,
940 ) -> Result<QuickIntegrityResult, InternalError> {
941 let omitted_findings = self.omitted_findings()?;
942 let identity = plan.identity();
943
944 Ok(QuickIntegrityResult {
945 entity: IntegrityEntityIdentity::from_plan(plan),
946 database_incarnation_id: incarnation,
947 accepted_schema_version: identity.accepted_schema_version().get(),
948 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
949 status: QuickIntegrityStatus::ResourceLimited(diagnostic),
950 total_findings: self.total_findings,
951 omitted_findings,
952 findings: self.findings,
953 })
954 }
955
956 fn omitted_findings(&self) -> Result<u64, InternalError> {
957 let returned = u64::try_from(self.findings.len()).map_err(|_| {
958 InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
959 })?;
960 self.total_findings.checked_sub(returned).ok_or_else(|| {
961 InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
962 })
963 }
964}
965
966pub(in crate::db) fn uninspectable_quick_integrity(
967 identity: crate::db::schema::AcceptedCatalogIdentity,
968 incarnation: DatabaseIncarnationId,
969 diagnostic: IntegrityAuthorityDiagnostic,
970) -> QuickIntegrityResult {
971 QuickIntegrityResult {
972 entity: IntegrityEntityIdentity::from_accepted_identity(&identity),
973 database_incarnation_id: incarnation,
974 accepted_schema_version: identity.accepted_schema_version().get(),
975 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
976 status: QuickIntegrityStatus::Uninspectable(diagnostic),
977 total_findings: 0,
978 omitted_findings: 0,
979 findings: Vec::new(),
980 }
981}
982
983pub(in crate::db) fn execute_quick_integrity<C: CanisterKind>(
984 db: &crate::db::Db<C>,
985 plan: &AcceptedInspectionPlan,
986 incarnation: DatabaseIncarnationId,
987) -> Result<QuickIntegrityResult, InternalError> {
988 ensure_recovery_admitted(db)?;
989 let findings = match validate_quick_integrity_control(db, plan, incarnation) {
990 Ok(findings) => findings,
991 Err(error) => {
992 let IntegrityTerminalOutcome::Uninspectable(diagnostic) =
993 IntegrityTerminalOutcome::from_internal(&error)
994 else {
995 return Err(error);
996 };
997 return Ok(uninspectable_quick_integrity(
998 plan.identity(),
999 incarnation,
1000 diagnostic,
1001 ));
1002 }
1003 };
1004 let mut accumulator = QuickIntegrityAccumulator::new();
1005 for finding in findings {
1006 if let Err(diagnostic) = accumulator.record(finding) {
1007 return accumulator.resource_limited(plan, incarnation, diagnostic);
1008 }
1009 }
1010
1011 accumulator.complete(plan, incarnation)
1012}
1013
1014pub(crate) fn generate_database_incarnation_id() -> Result<DatabaseIncarnationId, InternalError> {
1015 DatabaseIncarnationId::generate()
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021 use crate::{
1022 db::schema::{FieldStorageDecode, LeafCodec, ScalarCodec},
1023 db::{
1024 commit::CommitSchemaFingerprint,
1025 schema::{
1026 AcceptedCatalogIdentity, AcceptedCompositeCatalog, AcceptedFieldKind,
1027 AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
1028 FieldId, IdentityState, IdentityStateOwner, PersistedFieldSnapshot,
1029 PersistedSchemaSnapshot, SchemaFieldSlot, SchemaInsertDefault, SchemaRowLayout,
1030 SchemaVersion, empty_accepted_enum_catalog_for_tests,
1031 },
1032 },
1033 types::EntityTag,
1034 };
1035
1036 fn plan() -> AcceptedInspectionPlan {
1037 let revision = AcceptedSchemaRevision::INITIAL;
1038 let identity = AcceptedCatalogIdentity::new(
1039 EntityTag::new(23),
1040 "tests::QuickEntity",
1041 "tests::QuickStore",
1042 revision,
1043 SchemaVersion::initial(),
1044 CommitSchemaFingerprint::from([0x44; 16]),
1045 );
1046 let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
1047 SchemaVersion::initial(),
1048 "tests::QuickEntity".to_string(),
1049 "QuickEntity".to_string(),
1050 FieldId::new(1),
1051 SchemaRowLayout::initial(vec![(FieldId::new(1), SchemaFieldSlot::new(0))]),
1052 vec![PersistedFieldSnapshot::new_initial(
1053 FieldId::new(1),
1054 "id".to_string(),
1055 SchemaFieldSlot::new(0),
1056 AcceptedFieldKind::Nat64,
1057 Vec::new(),
1058 false,
1059 SchemaInsertDefault::None,
1060 FieldStorageDecode::ByKind,
1061 LeafCodec::Scalar(ScalarCodec::Nat64),
1062 )],
1063 ));
1064 let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
1065 empty_accepted_enum_catalog_for_tests(),
1066 AcceptedCompositeCatalog::empty(),
1067 revision,
1068 );
1069
1070 AcceptedInspectionPlan::compile_relation_free_for_tests(
1071 identity,
1072 snapshot.into(),
1073 value_catalog,
1074 )
1075 .expect("accepted Quick plan should compile")
1076 }
1077
1078 fn finding(plan: &AcceptedInspectionPlan) -> IntegrityFinding {
1079 IntegrityFinding {
1080 diagnostic_code: icydb_diagnostic_code::ErrorCode::STORE_CORRUPTION.raw(),
1081 class: IntegrityFindingClass::Corruption,
1082 severity: IntegritySeverity::Error,
1083 kind: IntegrityFindingKind::MalformedRow,
1084 entity: IntegrityEntityIdentity::from_plan(plan),
1085 store_path: plan.identity().store_path().to_string(),
1086 phase: IntegrityPhase::Rows,
1087 verifier_family: IntegrityVerifierFamily::RowEnvelope,
1088 physical_key: vec![1],
1089 primary_key: None,
1090 field_paths: Vec::new(),
1091 value_path: None,
1092 constraint_id: None,
1093 constraint_name: None,
1094 schema_index_id: None,
1095 relation_id: None,
1096 expected: None,
1097 observed: None,
1098 }
1099 }
1100
1101 #[test]
1102 fn database_incarnation_rejects_zero_and_round_trips_current_bytes() {
1103 assert!(DatabaseIncarnationId::try_from_bytes([0; 16]).is_err());
1104
1105 let identity = DatabaseIncarnationId::for_tests(7);
1106 assert_eq!(
1107 DatabaseIncarnationId::try_from_bytes(identity.to_bytes())
1108 .expect("nonzero incarnation should decode"),
1109 identity,
1110 );
1111 }
1112
1113 #[test]
1114 fn integrity_finding_candid_preserves_targeted_constraint_path() {
1115 let plan = plan();
1116 let mut finding = finding(&plan);
1117 let path = ConstraintValuePath::new(vec![
1118 crate::error::ConstraintValuePathComponent::RootField { field_id: 1 },
1119 crate::error::ConstraintValuePathComponent::ListElement { index: 2 },
1120 ]);
1121 finding.kind = IntegrityFindingKind::ConstraintViolation;
1122 finding.value_path = Some(Box::new(path.clone()));
1123 finding.constraint_id = Some(7);
1124 finding.constraint_name = Some("nested_limit".to_string());
1125
1126 let bytes = candid::encode_one(&finding).expect("integrity finding should encode");
1127 let decoded: IntegrityFinding =
1128 candid::decode_one(&bytes).expect("integrity finding should decode");
1129 assert_eq!(decoded.value_path(), Some(&path));
1130 assert_eq!(decoded.constraint_id(), Some(7));
1131 assert_eq!(decoded.constraint_name(), Some("nested_limit"));
1132 }
1133
1134 #[test]
1135 fn quick_clean_result_binds_incarnation_and_accepted_plan_identity() {
1136 let plan = plan();
1137 let incarnation = DatabaseIncarnationId::for_tests(8);
1138 let result = QuickIntegrityAccumulator::new()
1139 .complete(&plan, incarnation)
1140 .expect("clean Quick accounting should remain valid");
1141
1142 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteClean);
1143 assert_eq!(result.database_incarnation_id(), incarnation);
1144 assert_eq!(result.accepted_schema_version(), 1);
1145 assert_eq!(result.accepted_schema_fingerprint(), [0x44; 16]);
1146 assert_eq!(result.total_findings(), 0);
1147 assert_eq!(result.omitted_findings(), 0);
1148 }
1149
1150 #[test]
1151 fn quick_findings_keep_a_bounded_prefix_and_exact_omitted_count() {
1152 let plan = plan();
1153 let mut accumulator = QuickIntegrityAccumulator::new();
1154 for _ in 0..=MAX_QUICK_RETURNED_FINDINGS {
1155 accumulator
1156 .record(finding(&plan))
1157 .expect("bounded test finding count should fit");
1158 }
1159 let result = accumulator
1160 .complete(&plan, DatabaseIncarnationId::for_tests(9))
1161 .expect("one-over-cap Quick accounting should remain valid");
1162
1163 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteWithFindings,);
1164 assert_eq!(result.total_findings(), 65);
1165 assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1166 assert_eq!(result.omitted_findings(), 1);
1167 assert_eq!(
1168 result.total_findings(),
1169 result.findings().len() as u64 + result.omitted_findings(),
1170 );
1171 }
1172
1173 #[test]
1174 fn quick_findings_at_the_exact_returned_cap_have_no_omissions() {
1175 let plan = plan();
1176 let mut accumulator = QuickIntegrityAccumulator::new();
1177 for _ in 0..MAX_QUICK_RETURNED_FINDINGS {
1178 accumulator
1179 .record(finding(&plan))
1180 .expect("exact-cap finding count should fit");
1181 }
1182 let result = accumulator
1183 .complete(&plan, DatabaseIncarnationId::for_tests(10))
1184 .expect("exact-cap Quick accounting should remain valid");
1185
1186 assert_eq!(result.total_findings(), 64);
1187 assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1188 assert_eq!(result.omitted_findings(), 0);
1189 }
1190
1191 #[test]
1192 fn quick_selected_authority_failure_is_not_a_clean_completion() {
1193 let plan = plan();
1194 let error = InternalError::accepted_row_constraint_program_corrupt();
1195 let result = uninspectable_quick_integrity(
1196 plan.identity(),
1197 DatabaseIncarnationId::for_tests(11),
1198 IntegrityAuthorityDiagnostic::from_internal(&error),
1199 );
1200
1201 assert!(matches!(
1202 result.status(),
1203 QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic {
1204 class: IntegrityAuthorityClass::Corruption,
1205 ..
1206 }),
1207 ));
1208 assert_eq!(result.total_findings(), 0);
1209 assert_eq!(result.omitted_findings(), 0);
1210 }
1211
1212 #[test]
1213 fn quick_identity_inventory_rejects_active_retired_owner_collision_first() {
1214 let incarnation = DatabaseIncarnationId::for_tests(12);
1215 let owner = IdentityStateOwner::try_new(incarnation, EntityTag::new(31), FieldId::new(1))
1216 .expect("identity owner should admit");
1217 let active = IdentityState::new_active(owner, AcceptedFieldKind::Nat64)
1218 .expect("active identity state should admit");
1219 let retired = active.retire().expect("active state should retire");
1220 let mut owners = BTreeMap::new();
1221
1222 record_quick_identity_owner(&mut owners, "tests::first", &active)
1223 .expect("the first owner should admit");
1224 let error = record_quick_identity_owner(&mut owners, "tests::second", &retired)
1225 .expect_err("an active/retired owner collision must reject");
1226
1227 assert_eq!(error.class(), ErrorClass::Corruption);
1228 assert_eq!(error.origin(), ErrorOrigin::Identity);
1229 }
1230}