1mod deep;
7mod derived;
8mod job;
9mod progress_store;
10mod proof;
11mod row;
12
13use crate::{
14 db::{
15 commit::{database_control_proof_identity, database_incarnation_id, ensure_recovered},
16 registry::{
17 StoreAllocationIdentities, StoreHandle, StoreRuntimeStorageCapabilities,
18 StoreRuntimeStorageMode,
19 },
20 relation::{RelationConstraintProjection, ReverseRelationSourceInfo},
21 schema::AcceptedInspectionPlan,
22 },
23 entity::EntityKind,
24 error::{ErrorClass, InternalError},
25 traits::{CanisterKind, Path},
26};
27use candid::CandidType;
28use serde::Deserialize;
29use std::{
30 collections::BTreeMap,
31 sync::atomic::{AtomicU64, Ordering},
32};
33
34#[cfg(all(test, feature = "sql"))]
35pub(in crate::db) use deep::run_integrity_retention_page_for_tests;
36pub(in crate::db) use deep::{
37 abort_deep_integrity_job, continue_deep_integrity_job, run_next_integrity_retention_page,
38 start_deep_integrity_job,
39};
40#[cfg(all(test, feature = "sql"))]
41pub(in crate::db) use deep::{
42 reset_integrity_retention_cursor_for_tests, run_next_integrity_retention_page_for_tests,
43};
44#[cfg(test)]
45pub(in crate::db) use derived::DerivedIntegrityPage;
46pub(in crate::db) use derived::{
47 DerivedInspectionLimits, execute_index_integrity_page, execute_reverse_integrity_page,
48};
49pub use job::{
50 DeepIntegrityPage, DeepIntegrityPageStatus, IntegrityAbortReceipt, IntegrityAbortStatus,
51 IntegrityDeepError, IntegrityJobError, IntegrityJobId, IntegrityJobOwner, IntegrityJobReceipt,
52 IntegrityPendingTerminal, IntegritySubmissionKey, IntegrityTerminalOutcome,
53};
54pub(in crate::db) use job::{
55 IntegrityCheckpoint, IntegrityJob, IntegrityJobState, IntegrityReceiptEnvelope,
56 IntegrityReceiptReplayKey, MAX_INTEGRITY_IN_PROGRESS_PAGES,
57};
58#[cfg(all(test, feature = "sql"))]
59pub(in crate::db) use progress_store::{
60 clear_progress_store_for_tests, corrupt_progress_job_for_tests,
61 progress_job_encoded_len_for_tests, set_progress_job_lease_deadline_for_tests,
62};
63pub(in crate::db) use proof::{IntegrityProofVector, capture_integrity_proof_vector};
64#[cfg(test)]
65pub(in crate::db) use row::RowIntegrityPage;
66pub(in crate::db) use row::{
67 PhysicalUnitCheckpoint, RowInspectionLimits, execute_row_integrity_page,
68};
69
70pub(in crate::db) const MAX_INTEGRITY_PATH_BYTES: usize = 4 * 1024;
71
72#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
80pub enum IntegrityCheckRequest {
81 Quick {
83 entity: IntegrityEntityIdentity,
85 },
86 DeepStart {
88 entity: IntegrityEntityIdentity,
90 submission_key: IntegritySubmissionKey,
92 },
93 DeepContinue {
95 job_id: IntegrityJobId,
97 acknowledged_sequence: u64,
99 },
100 DeepAbort {
102 job_id: IntegrityJobId,
104 },
105}
106
107impl IntegrityCheckRequest {
108 #[must_use]
110 pub fn quick<E: EntityKind>() -> Self {
111 Self::Quick {
112 entity: IntegrityEntityIdentity::for_entity::<E>(),
113 }
114 }
115
116 #[must_use]
118 pub fn deep_start<E: EntityKind>(submission_key: IntegritySubmissionKey) -> Self {
119 Self::DeepStart {
120 entity: IntegrityEntityIdentity::for_entity::<E>(),
121 submission_key,
122 }
123 }
124
125 #[must_use]
127 pub const fn deep_continue(job_id: IntegrityJobId, acknowledged_sequence: u64) -> Self {
128 Self::DeepContinue {
129 job_id,
130 acknowledged_sequence,
131 }
132 }
133
134 #[must_use]
136 pub const fn deep_abort(job_id: IntegrityJobId) -> Self {
137 Self::DeepAbort { job_id }
138 }
139}
140
141#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
144pub enum IntegrityCheckResult {
145 Quick(QuickIntegrityResult),
147 Deep(IntegrityJobReceipt),
149}
150
151fn accepted_relation_projections<C: CanisterKind>(
152 db: &crate::db::Db<C>,
153 plan: &AcceptedInspectionPlan,
154) -> Result<Vec<RelationConstraintProjection>, InternalError> {
155 let identity = plan.identity();
156 let source = ReverseRelationSourceInfo::new(identity.entity_path(), identity.entity_tag());
157
158 plan.snapshot()
159 .persisted_snapshot()
160 .relations()
161 .iter()
162 .map(|edge| {
163 RelationConstraintProjection::new_active(
164 db,
165 source,
166 plan.snapshot().persisted_snapshot(),
167 plan.row_contract(),
168 edge,
169 )
170 })
171 .collect()
172}
173
174fn validate_quick_integrity_control<C: CanisterKind>(
175 db: &crate::db::Db<C>,
176 plan: &AcceptedInspectionPlan,
177) -> Result<Vec<IntegrityFinding>, InternalError> {
178 let identity = plan.identity();
179 let source_store = db.store_handle(identity.store_path())?;
180 let relations = accepted_relation_projections(db, plan)?;
181 let mut participating_stores =
182 BTreeMap::from([(identity.store_path().to_string(), source_store)]);
183 for relation in &relations {
184 participating_stores
185 .entry(relation.target_store_path().to_string())
186 .or_insert_with(|| relation.target_store());
187 }
188
189 let _database_control = database_control_proof_identity()?;
190 proof::validate_integrity_allocation_registry()?;
191 let mut findings = Vec::new();
192 for (store_path, store) in &participating_stores {
193 if let Some(finding) = validate_quick_store_control(plan, store_path, *store)? {
194 findings.push(finding);
195 }
196 }
197 for ordinal in 0..plan.index_inspection().len() {
198 let _domain = plan
199 .index_inspection()
200 .domain(ordinal, identity.entity_tag())?;
201 }
202
203 Ok(findings)
204}
205
206fn validate_quick_store_control(
207 plan: &AcceptedInspectionPlan,
208 store_path: &str,
209 store: StoreHandle,
210) -> Result<Option<IntegrityFinding>, InternalError> {
211 let capabilities = store.storage_capabilities();
212 let allocations = store.allocation_identities();
213 match capabilities.storage_mode() {
214 StoreRuntimeStorageMode::Heap => {
215 if capabilities != StoreRuntimeStorageCapabilities::heap()
216 || allocations != StoreAllocationIdentities::absent()
217 || store.journal_tail_store().is_some()
218 {
219 return Err(InternalError::store_invariant());
220 }
221 Ok(None)
222 }
223 StoreRuntimeStorageMode::Journaled => {
224 if capabilities != StoreRuntimeStorageCapabilities::journaled()
225 || !allocations.matches_storage_capabilities(capabilities)
226 {
227 return Err(InternalError::store_invariant());
228 }
229 let journal = store
230 .journal_tail_store()
231 .ok_or_else(InternalError::store_invariant)?
232 .with_borrow(crate::db::journal::JournalTailStore::proof_identity)?;
233 if !journal.is_well_formed() {
234 return Ok(Some(quick_journal_control_finding(plan, store_path)));
235 }
236 Ok(None)
237 }
238 }
239}
240
241fn quick_journal_control_finding(
242 plan: &AcceptedInspectionPlan,
243 store_path: &str,
244) -> IntegrityFinding {
245 let error = InternalError::store_corruption();
246 IntegrityFinding {
247 diagnostic_code: error.diagnostic_code().error_code().raw(),
248 class: IntegrityFindingClass::Corruption,
249 severity: IntegritySeverity::Error,
250 kind: IntegrityFindingKind::JournalControlMismatch,
251 entity: IntegrityEntityIdentity::from_plan(plan),
252 store_path: store_path.to_string(),
253 phase: IntegrityPhase::QuickMetadata,
254 verifier_family: IntegrityVerifierFamily::JournalEnvelope,
255 physical_key: Vec::new(),
256 primary_key: None,
257 field_paths: Vec::new(),
258 constraint_id: None,
259 constraint_name: None,
260 schema_index_id: None,
261 relation_id: None,
262 expected: Some("well-formed-journal-control".to_string()),
263 observed: Some("inconsistent-journal-control".to_string()),
264 }
265}
266
267fn relation_field_paths(plan: &AcceptedInspectionPlan, relation_id: u32) -> Vec<String> {
268 let snapshot = plan.snapshot().persisted_snapshot();
269 let Some(relation) = snapshot
270 .relations()
271 .iter()
272 .find(|relation| relation.id().get() == relation_id)
273 else {
274 return Vec::new();
275 };
276
277 relation
278 .local_field_ids()
279 .iter()
280 .filter_map(|field_id| {
281 snapshot
282 .fields()
283 .iter()
284 .find(|field| field.id() == *field_id)
285 .map(|field| field.name().to_string())
286 })
287 .collect()
288}
289
290const MAX_QUICK_RETURNED_FINDINGS: usize = 64;
291#[cfg(target_arch = "wasm32")]
292const DATABASE_INCARNATION_DOMAIN: &[u8] = b"icydb.database-incarnation.v1";
293static DATABASE_INCARNATION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
294
295#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
302pub struct DatabaseIncarnationId([u8; 16]);
303
304impl DatabaseIncarnationId {
305 pub(crate) fn try_from_bytes(bytes: [u8; 16]) -> Result<Self, InternalError> {
307 if bytes == [0; 16] {
308 return Err(InternalError::database_incarnation_invalid());
309 }
310
311 Ok(Self(bytes))
312 }
313
314 #[must_use]
316 pub const fn to_bytes(self) -> [u8; 16] {
317 self.0
318 }
319
320 fn generate() -> Result<Self, InternalError> {
321 let sequence = DATABASE_INCARNATION_SEQUENCE
322 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
323 current.checked_add(1)
324 })
325 .map_err(|_| InternalError::database_incarnation_generation_failed())?
326 .checked_add(1)
327 .ok_or_else(InternalError::database_incarnation_generation_failed)?;
328
329 #[cfg(not(target_arch = "wasm32"))]
330 let bytes = {
331 let mut bytes = [0_u8; 16];
332 getrandom::fill(&mut bytes)
333 .map_err(|_| InternalError::database_incarnation_generation_failed())?;
334 bytes
335 };
336
337 #[cfg(target_arch = "wasm32")]
338 let bytes = {
339 use sha2::{Digest, Sha256};
340
341 let mut hasher = Sha256::new();
342 hasher.update(DATABASE_INCARNATION_DOMAIN);
343 hasher.update(ic_cdk::api::canister_self().as_slice());
344 hasher.update(ic_cdk::api::time().to_be_bytes());
345 hasher.update(sequence.to_be_bytes());
346 let digest = hasher.finalize();
347 let mut bytes = [0_u8; 16];
348 bytes.copy_from_slice(&digest[..16]);
349 bytes
350 };
351
352 let _ = sequence;
353 Self::try_from_bytes(bytes)
354 }
355
356 #[cfg(test)]
357 pub(crate) const fn for_tests(fill: u8) -> Self {
358 let mut bytes = [fill; 16];
359 if fill == 0 {
360 bytes[15] = 1;
361 }
362 Self(bytes)
363 }
364}
365
366#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
368pub struct IntegrityEntityIdentity {
369 entity_tag: u64,
370 entity_path: String,
371 store_path: String,
372}
373
374impl IntegrityEntityIdentity {
375 fn from_plan(plan: &AcceptedInspectionPlan) -> Self {
376 Self::from_accepted_identity(plan.identity())
377 }
378
379 pub(in crate::db) fn from_accepted_identity(
380 identity: crate::db::schema::AcceptedCatalogIdentity,
381 ) -> Self {
382 Self {
383 entity_tag: identity.entity_tag().value(),
384 entity_path: identity.entity_path().to_string(),
385 store_path: identity.store_path().to_string(),
386 }
387 }
388
389 #[cfg(feature = "sql")]
394 pub(in crate::db) fn from_runtime_selector(
395 entity_tag: u64,
396 entity_path: &str,
397 store_path: &str,
398 ) -> Self {
399 Self {
400 entity_tag,
401 entity_path: entity_path.to_string(),
402 store_path: store_path.to_string(),
403 }
404 }
405
406 #[must_use]
412 pub fn for_entity<E: EntityKind>() -> Self {
413 Self {
414 entity_tag: E::ENTITY_TAG.value(),
415 entity_path: <E as Path>::PATH.to_string(),
416 store_path: <E::Store as Path>::PATH.to_string(),
417 }
418 }
419
420 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
421 if self.entity_tag == 0
422 || self.entity_path.is_empty()
423 || self.entity_path.len() > MAX_INTEGRITY_PATH_BYTES
424 || self.store_path.is_empty()
425 || self.store_path.len() > MAX_INTEGRITY_PATH_BYTES
426 {
427 return Err(IntegrityJobError::InvalidEntityIdentity);
428 }
429 Ok(())
430 }
431
432 #[must_use]
434 pub const fn entity_tag(&self) -> u64 {
435 self.entity_tag
436 }
437
438 #[must_use]
440 pub const fn entity_path(&self) -> &str {
441 self.entity_path.as_str()
442 }
443
444 #[must_use]
446 pub const fn store_path(&self) -> &str {
447 self.store_path.as_str()
448 }
449}
450
451#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
453pub enum IntegrityAuthorityClass {
454 Corruption,
456 IncompatiblePersistedFormat,
458 InvariantViolation,
460 Unsupported,
462 Internal,
464}
465
466#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
468pub enum IntegrityFindingClass {
469 Corruption,
471 IncompatiblePersistedFormat,
473 ResourceLimited,
475}
476
477#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
480pub enum IntegrityFindingKind {
481 MalformedDataKey,
483
484 MalformedRow,
486
487 OversizedRow,
489
490 InvalidFieldValue,
492
493 PrimaryKeyMismatch,
495
496 ConstraintViolation,
498
499 MissingIndexEntry,
501
502 DivergentIndexEntry,
504
505 MalformedIndexEntry,
507
508 OrphanIndexEntry,
510
511 DuplicateUniqueIndexKey,
513
514 MissingRelationTarget,
516
517 MissingReverseRelationEntry,
519
520 DivergentReverseRelationEntry,
522
523 MalformedReverseRelationEntry,
525
526 OrphanReverseRelationEntry,
528
529 MalformedJournalBatch,
531
532 JournalSequenceGap,
534
535 DuplicateJournalBatchIdentity,
537
538 JournalControlMismatch,
540}
541
542#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
545pub enum IntegrityPhase {
546 QuickMetadata,
548
549 Rows,
551
552 IndexEntries,
554
555 ReverseRelations,
557
558 JournalTails,
560
561 FinalProofVectorCheck,
563}
564
565#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
568pub enum IntegrityVerifierFamily {
569 DataKey,
571
572 RowEnvelope,
574
575 FieldValue,
577
578 PrimaryKey,
580
581 ValidatedConstraints,
583
584 ForwardIndex,
586
587 IndexEntry,
589
590 UniqueIndex,
592
593 Relation,
595
596 ReverseRelationEntry,
598
599 JournalEnvelope,
601
602 JournalBatchIdentity,
604}
605
606#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
608pub enum IntegritySeverity {
609 Error,
611 Advisory,
613}
614
615#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
619pub struct IntegrityFinding {
620 diagnostic_code: u16,
621 class: IntegrityFindingClass,
622 severity: IntegritySeverity,
623 kind: IntegrityFindingKind,
624 entity: IntegrityEntityIdentity,
625 store_path: String,
626 phase: IntegrityPhase,
627 verifier_family: IntegrityVerifierFamily,
628 physical_key: Vec<u8>,
629 primary_key: Option<Vec<u8>>,
630 field_paths: Vec<String>,
631 constraint_id: Option<u32>,
632 constraint_name: Option<String>,
633 schema_index_id: Option<u32>,
634 relation_id: Option<u32>,
635 expected: Option<String>,
636 observed: Option<String>,
637}
638
639impl IntegrityFinding {
640 #[must_use]
642 pub const fn diagnostic_code(&self) -> u16 {
643 self.diagnostic_code
644 }
645
646 #[must_use]
648 pub const fn class(&self) -> IntegrityFindingClass {
649 self.class
650 }
651
652 #[must_use]
654 pub const fn severity(&self) -> IntegritySeverity {
655 self.severity
656 }
657
658 #[must_use]
660 pub const fn kind(&self) -> IntegrityFindingKind {
661 self.kind
662 }
663
664 #[must_use]
666 pub const fn entity(&self) -> &IntegrityEntityIdentity {
667 &self.entity
668 }
669
670 #[must_use]
672 pub const fn store_path(&self) -> &str {
673 self.store_path.as_str()
674 }
675
676 #[must_use]
678 pub const fn phase(&self) -> IntegrityPhase {
679 self.phase
680 }
681
682 #[must_use]
684 pub const fn verifier_family(&self) -> IntegrityVerifierFamily {
685 self.verifier_family
686 }
687
688 #[must_use]
690 pub const fn physical_key(&self) -> &[u8] {
691 self.physical_key.as_slice()
692 }
693
694 #[must_use]
696 pub fn primary_key(&self) -> Option<&[u8]> {
697 self.primary_key.as_deref()
698 }
699
700 #[must_use]
702 pub const fn field_paths(&self) -> &[String] {
703 self.field_paths.as_slice()
704 }
705
706 #[must_use]
708 pub const fn constraint_id(&self) -> Option<u32> {
709 self.constraint_id
710 }
711
712 #[must_use]
714 pub fn constraint_name(&self) -> Option<&str> {
715 self.constraint_name.as_deref()
716 }
717
718 #[must_use]
720 pub const fn schema_index_id(&self) -> Option<u32> {
721 self.schema_index_id
722 }
723
724 #[must_use]
726 pub const fn relation_id(&self) -> Option<u32> {
727 self.relation_id
728 }
729
730 #[must_use]
732 pub fn expected(&self) -> Option<&str> {
733 self.expected.as_deref()
734 }
735
736 #[must_use]
738 pub fn observed(&self) -> Option<&str> {
739 self.observed.as_deref()
740 }
741}
742
743#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
745pub struct IntegrityAuthorityDiagnostic {
746 diagnostic_code: u16,
747 class: IntegrityAuthorityClass,
748}
749
750impl IntegrityAuthorityDiagnostic {
751 fn from_internal(error: &InternalError) -> Self {
752 let class = match error.class {
753 ErrorClass::Corruption => IntegrityAuthorityClass::Corruption,
754 ErrorClass::IncompatiblePersistedFormat => {
755 IntegrityAuthorityClass::IncompatiblePersistedFormat
756 }
757 ErrorClass::InvariantViolation => IntegrityAuthorityClass::InvariantViolation,
758 ErrorClass::Unsupported | ErrorClass::NotFound | ErrorClass::Conflict => {
759 IntegrityAuthorityClass::Unsupported
760 }
761 ErrorClass::Internal => IntegrityAuthorityClass::Internal,
762 };
763 Self {
764 diagnostic_code: error.diagnostic_code().error_code().raw(),
765 class,
766 }
767 }
768
769 #[must_use]
771 pub const fn diagnostic_code(&self) -> u16 {
772 self.diagnostic_code
773 }
774
775 #[must_use]
777 pub const fn class(&self) -> IntegrityAuthorityClass {
778 self.class
779 }
780}
781
782#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
784pub struct IntegrityResourceDiagnostic {
785 diagnostic_code: u16,
786}
787
788impl IntegrityResourceDiagnostic {
789 #[must_use]
791 pub const fn diagnostic_code(&self) -> u16 {
792 self.diagnostic_code
793 }
794}
795
796#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
798pub enum IntegrityPhysicalContainer {
799 Rows,
801 IndexEntries,
803 ReverseRelations,
805 JournalTails,
807}
808
809#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
811pub struct StorageTraversalCorruption {
812 diagnostic_code: u16,
813 store_path: String,
814 container: IntegrityPhysicalContainer,
815 phase: IntegrityPhase,
816 last_verified_physical_key: Option<Vec<u8>>,
817}
818
819impl StorageTraversalCorruption {
820 #[must_use]
822 pub const fn diagnostic_code(&self) -> u16 {
823 self.diagnostic_code
824 }
825
826 #[must_use]
828 pub const fn store_path(&self) -> &str {
829 self.store_path.as_str()
830 }
831
832 #[must_use]
834 pub const fn container(&self) -> IntegrityPhysicalContainer {
835 self.container
836 }
837
838 #[must_use]
840 pub const fn phase(&self) -> IntegrityPhase {
841 self.phase
842 }
843
844 #[must_use]
846 pub fn last_verified_physical_key(&self) -> Option<&[u8]> {
847 self.last_verified_physical_key.as_deref()
848 }
849}
850
851#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
853pub enum QuickIntegrityStatus {
854 CompleteClean,
856 CompleteWithFindings,
858 Uninspectable(IntegrityAuthorityDiagnostic),
860 UninspectableStorage(StorageTraversalCorruption),
862 ResourceLimited(IntegrityResourceDiagnostic),
864}
865
866#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
868pub struct QuickIntegrityResult {
869 entity: IntegrityEntityIdentity,
870 database_incarnation_id: DatabaseIncarnationId,
871 accepted_schema_version: u32,
872 accepted_schema_fingerprint: [u8; 16],
873 status: QuickIntegrityStatus,
874 total_findings: u64,
875 omitted_findings: u64,
876 findings: Vec<IntegrityFinding>,
877}
878
879impl QuickIntegrityResult {
880 #[must_use]
882 pub const fn entity(&self) -> &IntegrityEntityIdentity {
883 &self.entity
884 }
885
886 #[must_use]
888 pub const fn database_incarnation_id(&self) -> DatabaseIncarnationId {
889 self.database_incarnation_id
890 }
891
892 #[must_use]
894 pub const fn accepted_schema_version(&self) -> u32 {
895 self.accepted_schema_version
896 }
897
898 #[must_use]
900 pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
901 self.accepted_schema_fingerprint
902 }
903
904 #[must_use]
906 pub const fn status(&self) -> &QuickIntegrityStatus {
907 &self.status
908 }
909
910 #[must_use]
912 pub const fn total_findings(&self) -> u64 {
913 self.total_findings
914 }
915
916 #[must_use]
918 pub const fn omitted_findings(&self) -> u64 {
919 self.omitted_findings
920 }
921
922 #[must_use]
924 pub const fn findings(&self) -> &[IntegrityFinding] {
925 self.findings.as_slice()
926 }
927}
928
929struct QuickIntegrityAccumulator {
930 total_findings: u64,
931 findings: Vec<IntegrityFinding>,
932}
933
934impl QuickIntegrityAccumulator {
935 const fn new() -> Self {
936 Self {
937 total_findings: 0,
938 findings: Vec::new(),
939 }
940 }
941
942 fn record(&mut self, finding: IntegrityFinding) -> Result<(), IntegrityResourceDiagnostic> {
943 self.total_findings =
944 self.total_findings
945 .checked_add(1)
946 .ok_or(IntegrityResourceDiagnostic {
947 diagnostic_code: icydb_diagnostic_code::ErrorCode::RUNTIME_INTERNAL.raw(),
948 })?;
949 if self.findings.len() < MAX_QUICK_RETURNED_FINDINGS {
950 self.findings.push(finding);
951 }
952 Ok(())
953 }
954
955 fn complete(
956 self,
957 plan: &AcceptedInspectionPlan,
958 incarnation: DatabaseIncarnationId,
959 ) -> QuickIntegrityResult {
960 let status = if self.total_findings == 0 {
961 QuickIntegrityStatus::CompleteClean
962 } else {
963 QuickIntegrityStatus::CompleteWithFindings
964 };
965 let omitted_findings = self
966 .total_findings
967 .saturating_sub(self.findings.len() as u64);
968 let identity = plan.identity();
969
970 QuickIntegrityResult {
971 entity: IntegrityEntityIdentity::from_plan(plan),
972 database_incarnation_id: incarnation,
973 accepted_schema_version: identity.accepted_schema_version().get(),
974 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
975 status,
976 total_findings: self.total_findings,
977 omitted_findings,
978 findings: self.findings,
979 }
980 }
981
982 fn resource_limited(
983 self,
984 plan: &AcceptedInspectionPlan,
985 incarnation: DatabaseIncarnationId,
986 diagnostic: IntegrityResourceDiagnostic,
987 ) -> QuickIntegrityResult {
988 let omitted_findings = self
989 .total_findings
990 .saturating_sub(self.findings.len() as u64);
991 let identity = plan.identity();
992
993 QuickIntegrityResult {
994 entity: IntegrityEntityIdentity::from_plan(plan),
995 database_incarnation_id: incarnation,
996 accepted_schema_version: identity.accepted_schema_version().get(),
997 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
998 status: QuickIntegrityStatus::ResourceLimited(diagnostic),
999 total_findings: self.total_findings,
1000 omitted_findings,
1001 findings: self.findings,
1002 }
1003 }
1004}
1005
1006pub(in crate::db) fn uninspectable_quick_integrity(
1007 identity: crate::db::schema::AcceptedCatalogIdentity,
1008 incarnation: DatabaseIncarnationId,
1009 error: &InternalError,
1010) -> QuickIntegrityResult {
1011 QuickIntegrityResult {
1012 entity: IntegrityEntityIdentity::from_accepted_identity(identity),
1013 database_incarnation_id: incarnation,
1014 accepted_schema_version: identity.accepted_schema_version().get(),
1015 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
1016 status: QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(
1017 error,
1018 )),
1019 total_findings: 0,
1020 omitted_findings: 0,
1021 findings: Vec::new(),
1022 }
1023}
1024
1025pub(in crate::db) fn execute_quick_integrity<C: CanisterKind>(
1026 db: &crate::db::Db<C>,
1027 plan: &AcceptedInspectionPlan,
1028) -> Result<QuickIntegrityResult, InternalError> {
1029 ensure_recovered(db)?;
1030 let incarnation = database_incarnation_id()?;
1031 let findings = match validate_quick_integrity_control(db, plan) {
1032 Ok(findings) => findings,
1033 Err(error) => {
1034 return Ok(uninspectable_quick_integrity(
1035 plan.identity(),
1036 incarnation,
1037 &error,
1038 ));
1039 }
1040 };
1041 let mut accumulator = QuickIntegrityAccumulator::new();
1042 for finding in findings {
1043 if let Err(diagnostic) = accumulator.record(finding) {
1044 return Ok(accumulator.resource_limited(plan, incarnation, diagnostic));
1045 }
1046 }
1047
1048 Ok(accumulator.complete(plan, incarnation))
1049}
1050
1051pub(crate) fn generate_database_incarnation_id() -> Result<DatabaseIncarnationId, InternalError> {
1052 DatabaseIncarnationId::generate()
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057 use super::*;
1058 use crate::{
1059 db::{
1060 commit::CommitSchemaFingerprint,
1061 schema::{
1062 AcceptedCatalogIdentity, AcceptedCompositeCatalog, AcceptedFieldKind,
1063 AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
1064 FieldId, PersistedFieldSnapshot, PersistedSchemaSnapshot, SchemaFieldSlot,
1065 SchemaInsertDefault, SchemaRowLayout, SchemaVersion,
1066 enum_catalog::build_initial_accepted_enum_catalog,
1067 },
1068 },
1069 model::field::{FieldStorageDecode, LeafCodec, ScalarCodec},
1070 types::EntityTag,
1071 };
1072
1073 fn plan() -> AcceptedInspectionPlan {
1074 let revision = AcceptedSchemaRevision::INITIAL;
1075 let identity = AcceptedCatalogIdentity::new(
1076 EntityTag::new(23),
1077 "tests::QuickEntity",
1078 "tests::QuickStore",
1079 revision,
1080 SchemaVersion::initial(),
1081 CommitSchemaFingerprint::from([0x44; 16]),
1082 );
1083 let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
1084 SchemaVersion::initial(),
1085 "tests::QuickEntity".to_string(),
1086 "QuickEntity".to_string(),
1087 FieldId::new(1),
1088 SchemaRowLayout::initial(vec![(FieldId::new(1), SchemaFieldSlot::new(0))]),
1089 vec![PersistedFieldSnapshot::new_initial(
1090 FieldId::new(1),
1091 "id".to_string(),
1092 SchemaFieldSlot::new(0),
1093 AcceptedFieldKind::Nat64,
1094 Vec::new(),
1095 false,
1096 SchemaInsertDefault::None,
1097 FieldStorageDecode::ByKind,
1098 LeafCodec::Scalar(ScalarCodec::Nat64),
1099 )],
1100 ));
1101 let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
1102 build_initial_accepted_enum_catalog(&[])
1103 .expect("empty accepted enum catalog should build"),
1104 AcceptedCompositeCatalog::empty(),
1105 revision,
1106 );
1107
1108 AcceptedInspectionPlan::compile(identity, snapshot, value_catalog)
1109 .expect("accepted Quick plan should compile")
1110 }
1111
1112 fn finding(plan: &AcceptedInspectionPlan) -> IntegrityFinding {
1113 IntegrityFinding {
1114 diagnostic_code: icydb_diagnostic_code::ErrorCode::STORE_CORRUPTION.raw(),
1115 class: IntegrityFindingClass::Corruption,
1116 severity: IntegritySeverity::Error,
1117 kind: IntegrityFindingKind::MalformedRow,
1118 entity: IntegrityEntityIdentity::from_plan(plan),
1119 store_path: plan.identity().store_path().to_string(),
1120 phase: IntegrityPhase::Rows,
1121 verifier_family: IntegrityVerifierFamily::RowEnvelope,
1122 physical_key: vec![1],
1123 primary_key: None,
1124 field_paths: Vec::new(),
1125 constraint_id: None,
1126 constraint_name: None,
1127 schema_index_id: None,
1128 relation_id: None,
1129 expected: None,
1130 observed: None,
1131 }
1132 }
1133
1134 #[test]
1135 fn database_incarnation_rejects_zero_and_round_trips_current_bytes() {
1136 assert!(DatabaseIncarnationId::try_from_bytes([0; 16]).is_err());
1137
1138 let identity = DatabaseIncarnationId::for_tests(7);
1139 assert_eq!(
1140 DatabaseIncarnationId::try_from_bytes(identity.to_bytes())
1141 .expect("nonzero incarnation should decode"),
1142 identity,
1143 );
1144 }
1145
1146 #[test]
1147 fn quick_clean_result_binds_incarnation_and_accepted_plan_identity() {
1148 let plan = plan();
1149 let incarnation = DatabaseIncarnationId::for_tests(8);
1150 let result = QuickIntegrityAccumulator::new().complete(&plan, incarnation);
1151
1152 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteClean);
1153 assert_eq!(result.database_incarnation_id(), incarnation);
1154 assert_eq!(result.accepted_schema_version(), 1);
1155 assert_eq!(result.accepted_schema_fingerprint(), [0x44; 16]);
1156 assert_eq!(result.total_findings(), 0);
1157 assert_eq!(result.omitted_findings(), 0);
1158 }
1159
1160 #[test]
1161 fn quick_findings_keep_a_bounded_prefix_and_exact_omitted_count() {
1162 let plan = plan();
1163 let mut accumulator = QuickIntegrityAccumulator::new();
1164 for _ in 0..=MAX_QUICK_RETURNED_FINDINGS {
1165 accumulator
1166 .record(finding(&plan))
1167 .expect("bounded test finding count should fit");
1168 }
1169 let result = accumulator.complete(&plan, DatabaseIncarnationId::for_tests(9));
1170
1171 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteWithFindings,);
1172 assert_eq!(result.total_findings(), 65);
1173 assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1174 assert_eq!(result.omitted_findings(), 1);
1175 assert_eq!(
1176 result.total_findings(),
1177 result.findings().len() as u64 + result.omitted_findings(),
1178 );
1179 }
1180
1181 #[test]
1182 fn quick_selected_authority_failure_is_not_a_clean_completion() {
1183 let plan = plan();
1184 let error = InternalError::accepted_row_constraint_program_corrupt();
1185 let result = uninspectable_quick_integrity(
1186 plan.identity(),
1187 DatabaseIncarnationId::for_tests(10),
1188 &error,
1189 );
1190
1191 assert!(matches!(
1192 result.status(),
1193 QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic {
1194 class: IntegrityAuthorityClass::Corruption,
1195 ..
1196 }),
1197 ));
1198 assert_eq!(result.total_findings(), 0);
1199 assert_eq!(result.omitted_findings(), 0);
1200 }
1201}