1mod deep;
7mod derived;
8mod job;
9mod progress_codec;
10mod progress_store;
11mod proof;
12mod row;
13
14use crate::{
15 db::{
16 commit::{database_control_proof_identity, database_incarnation_id, ensure_recovered},
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};
51pub(in crate::db) use proof::{IntegrityProofVector, capture_integrity_proof_vector};
52pub(in crate::db) use row::{
53 PhysicalUnitCheckpoint, RowInspectionLimits, execute_row_integrity_page,
54};
55
56pub(in crate::db) const MAX_INTEGRITY_PATH_BYTES: usize = 4 * 1024;
57
58#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
66pub enum IntegrityCheckRequest {
67 Quick {
69 entity: IntegrityEntityIdentity,
71 },
72 DeepStart {
74 entity: IntegrityEntityIdentity,
76 submission_key: IntegritySubmissionKey,
78 },
79 DeepContinue {
81 job_id: IntegrityJobId,
83 acknowledged_sequence: u64,
85 },
86 DeepAbort {
88 job_id: IntegrityJobId,
90 },
91}
92
93impl IntegrityCheckRequest {
94 #[must_use]
96 pub const fn deep_continue(job_id: IntegrityJobId, acknowledged_sequence: u64) -> Self {
97 Self::DeepContinue {
98 job_id,
99 acknowledged_sequence,
100 }
101 }
102
103 #[must_use]
105 pub const fn deep_abort(job_id: IntegrityJobId) -> Self {
106 Self::DeepAbort { job_id }
107 }
108}
109
110#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
113pub enum IntegrityCheckResult {
114 Quick(QuickIntegrityResult),
116 Deep(IntegrityJobReceipt),
118}
119
120fn validate_quick_integrity_control<C: CanisterKind>(
121 db: &crate::db::Db<C>,
122 plan: &AcceptedInspectionPlan,
123 incarnation: DatabaseIncarnationId,
124) -> Result<Vec<IntegrityFinding>, InternalError> {
125 let identity = plan.identity();
126 let source_store = db.store_handle(identity.store_path())?;
127 let relations = plan.relation_inspection();
128 let mut participating_stores =
129 BTreeMap::from([(identity.store_path().to_string(), source_store)]);
130 for relation in relations {
131 participating_stores
132 .entry(relation.target_store_path().to_string())
133 .or_insert_with(|| relation.target_store());
134 }
135
136 let _database_control = database_control_proof_identity()?;
137 proof::validate_integrity_allocation_registry()?;
138 validate_quick_identity_control(db, incarnation)?;
139 let mut findings = Vec::new();
140 for (store_path, store) in &participating_stores {
141 if let Some(finding) = validate_quick_store_control(plan, store_path, *store)? {
142 findings.push(finding);
143 }
144 }
145 for ordinal in 0..plan.index_inspection().len() {
146 let _domain = plan
147 .index_inspection()
148 .domain(ordinal, identity.entity_tag())?;
149 }
150
151 Ok(findings)
152}
153
154fn validate_quick_identity_control<C: CanisterKind>(
155 db: &crate::db::Db<C>,
156 incarnation: DatabaseIncarnationId,
157) -> Result<(), InternalError> {
158 let mut stores = db.with_store_registry(|registry| registry.iter().collect::<Vec<_>>());
159 stores.sort_unstable_by_key(|(store_path, _)| *store_path);
160
161 let mut owners = BTreeMap::new();
162 let mut state_count = 0usize;
163 for (store_path, store) in stores {
164 let states = store.with_schema(|schema_store| {
165 schema_store.identity_state_inventory_for_integrity(incarnation)
166 })?;
167 state_count = state_count
168 .checked_add(states.len())
169 .ok_or_else(InternalError::identity_state_corruption)?;
170 if state_count > MAX_IDENTITY_STATE_RECORDS_PER_DATABASE {
171 return Err(InternalError::identity_state_corruption());
172 }
173
174 for state in states {
175 let owner = state.owner();
176 record_quick_identity_owner(&mut owners, store_path, &state)?;
177 if state.lifecycle() == IdentityStateLifecycle::Active {
178 let runtime_entity = db
179 .accepted_runtime_entity_for_tag(owner.entity_tag())
180 .map_err(|_| InternalError::identity_state_corruption())?;
181 if runtime_entity.store_path() != store_path {
182 return Err(InternalError::identity_state_corruption());
183 }
184 }
185 }
186 }
187
188 Ok(())
189}
190
191fn record_quick_identity_owner<'a>(
192 owners: &mut BTreeMap<(crate::types::EntityTag, crate::db::schema::FieldId), &'a str>,
193 store_path: &'a str,
194 state: &crate::db::schema::IdentityState,
195) -> Result<(), InternalError> {
196 let owner = state.owner();
197 let key = (owner.entity_tag(), owner.field_id());
198 if owners.insert(key, store_path).is_some() {
199 return Err(InternalError::identity_state_corruption());
200 }
201 Ok(())
202}
203
204fn validate_quick_store_control(
205 plan: &AcceptedInspectionPlan,
206 store_path: &str,
207 store: StoreHandle,
208) -> Result<Option<IntegrityFinding>, InternalError> {
209 let capabilities = store.storage_capabilities();
210 let allocations = store.allocation_identities();
211 match capabilities.storage_mode() {
212 StoreRuntimeStorageMode::Heap => {
213 if capabilities != StoreRuntimeStorageCapabilities::heap()
214 || allocations != StoreAllocationIdentities::absent()
215 || store.journal_tail_store().is_some()
216 {
217 return Err(InternalError::store_invariant());
218 }
219 Ok(None)
220 }
221 StoreRuntimeStorageMode::Journaled => {
222 if capabilities != StoreRuntimeStorageCapabilities::journaled()
223 || !allocations.matches_storage_capabilities(capabilities)
224 {
225 return Err(InternalError::store_invariant());
226 }
227 let journal = store
228 .journal_tail_store()
229 .ok_or_else(InternalError::store_invariant)?
230 .with_borrow(crate::db::journal::JournalTailStore::proof_identity)?;
231 if !journal.is_well_formed() {
232 return Ok(Some(quick_journal_control_finding(plan, store_path)));
233 }
234 Ok(None)
235 }
236 }
237}
238
239fn quick_journal_control_finding(
240 plan: &AcceptedInspectionPlan,
241 store_path: &str,
242) -> IntegrityFinding {
243 let error = InternalError::store_corruption();
244 IntegrityFinding {
245 diagnostic_code: error.diagnostic_code().error_code().raw(),
246 class: IntegrityFindingClass::Corruption,
247 severity: IntegritySeverity::Error,
248 kind: IntegrityFindingKind::JournalControlMismatch,
249 entity: IntegrityEntityIdentity::from_plan(plan),
250 store_path: store_path.to_string(),
251 phase: IntegrityPhase::QuickMetadata,
252 verifier_family: IntegrityVerifierFamily::JournalEnvelope,
253 physical_key: Vec::new(),
254 primary_key: None,
255 field_paths: Vec::new(),
256 value_path: None,
257 constraint_id: None,
258 constraint_name: None,
259 schema_index_id: None,
260 relation_id: None,
261 expected: Some("well-formed-journal-control".to_string()),
262 observed: Some("inconsistent-journal-control".to_string()),
263 }
264}
265
266fn relation_field_paths(plan: &AcceptedInspectionPlan, relation_id: u32) -> Vec<String> {
267 let snapshot = plan.snapshot().persisted_snapshot();
268 let Some(relation) = snapshot
269 .relations()
270 .iter()
271 .find(|relation| relation.id().get() == relation_id)
272 else {
273 return Vec::new();
274 };
275
276 relation
277 .local_field_ids()
278 .iter()
279 .filter_map(|field_id| {
280 snapshot
281 .fields()
282 .iter()
283 .find(|field| field.id() == *field_id)
284 .map(|field| field.name().to_string())
285 })
286 .collect()
287}
288
289const MAX_QUICK_RETURNED_FINDINGS: usize = 64;
290#[cfg(target_arch = "wasm32")]
291const DATABASE_INCARNATION_DOMAIN: &[u8] = b"icydb.database-incarnation.v1";
292static DATABASE_INCARNATION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
293
294#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
301pub struct DatabaseIncarnationId([u8; 16]);
302
303impl DatabaseIncarnationId {
304 pub(crate) fn try_from_bytes(bytes: [u8; 16]) -> Result<Self, InternalError> {
306 if bytes == [0; 16] {
307 return Err(InternalError::database_incarnation_invalid());
308 }
309
310 Ok(Self(bytes))
311 }
312
313 #[must_use]
315 pub const fn to_bytes(self) -> [u8; 16] {
316 self.0
317 }
318
319 fn generate() -> Result<Self, InternalError> {
320 let sequence = DATABASE_INCARNATION_SEQUENCE
321 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
322 current.checked_add(1)
323 })
324 .map_err(|_| InternalError::database_incarnation_generation_failed())?
325 .checked_add(1)
326 .ok_or_else(InternalError::database_incarnation_generation_failed)?;
327
328 #[cfg(not(target_arch = "wasm32"))]
329 let bytes = {
330 let mut bytes = [0_u8; 16];
331 getrandom::fill(&mut bytes)
332 .map_err(|_| InternalError::database_incarnation_generation_failed())?;
333 bytes
334 };
335
336 #[cfg(target_arch = "wasm32")]
337 let bytes = {
338 use sha2::{Digest, Sha256};
339
340 let mut hasher = Sha256::new();
341 hasher.update(DATABASE_INCARNATION_DOMAIN);
342 hasher.update(ic_cdk::api::canister_self().as_slice());
343 hasher.update(ic_cdk::api::time().to_be_bytes());
344 hasher.update(sequence.to_be_bytes());
345 let digest = hasher.finalize();
346 let mut bytes = [0_u8; 16];
347 bytes.copy_from_slice(&digest[..16]);
348 bytes
349 };
350
351 let _ = sequence;
352 Self::try_from_bytes(bytes)
353 }
354
355 #[cfg(test)]
356 pub(crate) const fn for_tests(fill: u8) -> Self {
357 let mut bytes = [fill; 16];
358 if fill == 0 {
359 bytes[15] = 1;
360 }
361 Self(bytes)
362 }
363}
364
365#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
367pub struct IntegrityEntityIdentity {
368 entity_tag: u64,
369 entity_path: String,
370 store_path: String,
371}
372
373impl IntegrityEntityIdentity {
374 fn from_plan(plan: &AcceptedInspectionPlan) -> Self {
375 Self::from_accepted_identity(plan.identity_ref())
376 }
377
378 pub(in crate::db) fn from_accepted_identity(
379 identity: &crate::db::schema::AcceptedCatalogIdentity,
380 ) -> Self {
381 Self {
382 entity_tag: identity.entity_tag().value(),
383 entity_path: identity.entity_path().to_string(),
384 store_path: identity.store_path().to_string(),
385 }
386 }
387
388 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
389 if self.entity_tag == 0
390 || self.entity_path.is_empty()
391 || self.entity_path.len() > MAX_INTEGRITY_PATH_BYTES
392 || self.store_path.is_empty()
393 || self.store_path.len() > MAX_INTEGRITY_PATH_BYTES
394 {
395 return Err(IntegrityJobError::InvalidEntityIdentity);
396 }
397 Ok(())
398 }
399
400 #[must_use]
402 pub const fn entity_tag(&self) -> u64 {
403 self.entity_tag
404 }
405
406 #[must_use]
408 pub const fn entity_path(&self) -> &str {
409 self.entity_path.as_str()
410 }
411
412 #[must_use]
414 pub const fn store_path(&self) -> &str {
415 self.store_path.as_str()
416 }
417}
418
419#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
421pub enum IntegrityAuthorityClass {
422 Corruption,
424 IncompatiblePersistedFormat,
426 InvariantViolation,
428 Unsupported,
430 Internal,
432}
433
434#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
436pub enum IntegrityFindingClass {
437 Corruption,
439 IncompatiblePersistedFormat,
441 ResourceLimited,
443}
444
445#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
448pub enum IntegrityFindingKind {
449 MalformedDataKey,
451
452 MalformedRow,
454
455 OversizedRow,
457
458 InvalidFieldValue,
460
461 PrimaryKeyMismatch,
463
464 InvalidIdentityValue,
466
467 IdentityHighWaterExceeded,
469
470 ConstraintViolation,
472
473 MissingIndexEntry,
475
476 DivergentIndexEntry,
478
479 MalformedIndexEntry,
481
482 OrphanIndexEntry,
484
485 DuplicateUniqueIndexKey,
487
488 MissingRelationTarget,
490
491 MissingReverseRelationEntry,
493
494 DivergentReverseRelationEntry,
496
497 MalformedReverseRelationEntry,
499
500 OrphanReverseRelationEntry,
502
503 MalformedJournalBatch,
505
506 JournalSequenceGap,
508
509 DuplicateJournalBatchIdentity,
511
512 JournalControlMismatch,
514}
515
516#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
519pub enum IntegrityPhase {
520 QuickMetadata,
522
523 Rows,
525
526 IndexEntries,
528
529 ReverseRelations,
531
532 JournalTails,
534
535 FinalProofVectorCheck,
537}
538
539#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
542pub enum IntegrityVerifierFamily {
543 DataKey,
545
546 RowEnvelope,
548
549 FieldValue,
551
552 PrimaryKey,
554
555 IdentityState,
557
558 ValidatedConstraints,
560
561 ForwardIndex,
563
564 IndexEntry,
566
567 UniqueIndex,
569
570 Relation,
572
573 ReverseRelationEntry,
575
576 JournalEnvelope,
578
579 JournalBatchIdentity,
581}
582
583#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
585pub enum IntegritySeverity {
586 Error,
588 Advisory,
590}
591
592#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
596pub struct IntegrityFinding {
597 diagnostic_code: u16,
598 class: IntegrityFindingClass,
599 severity: IntegritySeverity,
600 kind: IntegrityFindingKind,
601 entity: IntegrityEntityIdentity,
602 store_path: String,
603 phase: IntegrityPhase,
604 verifier_family: IntegrityVerifierFamily,
605 physical_key: Vec<u8>,
606 primary_key: Option<Vec<u8>>,
607 field_paths: Vec<String>,
608 value_path: Option<Box<ConstraintValuePath>>,
609 constraint_id: Option<u32>,
610 constraint_name: Option<String>,
611 schema_index_id: Option<u32>,
612 relation_id: Option<u32>,
613 expected: Option<String>,
614 observed: Option<String>,
615}
616
617impl IntegrityFinding {
618 #[must_use]
620 pub const fn diagnostic_code(&self) -> u16 {
621 self.diagnostic_code
622 }
623
624 #[must_use]
626 pub const fn class(&self) -> IntegrityFindingClass {
627 self.class
628 }
629
630 #[must_use]
632 pub const fn severity(&self) -> IntegritySeverity {
633 self.severity
634 }
635
636 #[must_use]
638 pub const fn kind(&self) -> IntegrityFindingKind {
639 self.kind
640 }
641
642 #[must_use]
644 pub const fn entity(&self) -> &IntegrityEntityIdentity {
645 &self.entity
646 }
647
648 #[must_use]
650 pub const fn store_path(&self) -> &str {
651 self.store_path.as_str()
652 }
653
654 #[must_use]
656 pub const fn phase(&self) -> IntegrityPhase {
657 self.phase
658 }
659
660 #[must_use]
662 pub const fn verifier_family(&self) -> IntegrityVerifierFamily {
663 self.verifier_family
664 }
665
666 #[must_use]
668 pub const fn physical_key(&self) -> &[u8] {
669 self.physical_key.as_slice()
670 }
671
672 #[must_use]
674 pub fn primary_key(&self) -> Option<&[u8]> {
675 self.primary_key.as_deref()
676 }
677
678 #[must_use]
680 pub const fn field_paths(&self) -> &[String] {
681 self.field_paths.as_slice()
682 }
683
684 #[must_use]
686 pub fn value_path(&self) -> Option<&ConstraintValuePath> {
687 self.value_path.as_deref()
688 }
689
690 #[must_use]
692 pub const fn constraint_id(&self) -> Option<u32> {
693 self.constraint_id
694 }
695
696 #[must_use]
698 pub fn constraint_name(&self) -> Option<&str> {
699 self.constraint_name.as_deref()
700 }
701
702 #[must_use]
704 pub const fn schema_index_id(&self) -> Option<u32> {
705 self.schema_index_id
706 }
707
708 #[must_use]
710 pub const fn relation_id(&self) -> Option<u32> {
711 self.relation_id
712 }
713
714 #[must_use]
716 pub fn expected(&self) -> Option<&str> {
717 self.expected.as_deref()
718 }
719
720 #[must_use]
722 pub fn observed(&self) -> Option<&str> {
723 self.observed.as_deref()
724 }
725}
726
727#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
729pub struct IntegrityAuthorityDiagnostic {
730 diagnostic_code: u16,
731 class: IntegrityAuthorityClass,
732}
733
734impl IntegrityAuthorityDiagnostic {
735 pub(in crate::db) fn from_internal(error: &InternalError) -> Self {
736 let class = match error.class {
737 ErrorClass::Corruption => IntegrityAuthorityClass::Corruption,
738 ErrorClass::IncompatiblePersistedFormat => {
739 IntegrityAuthorityClass::IncompatiblePersistedFormat
740 }
741 ErrorClass::InvariantViolation => IntegrityAuthorityClass::InvariantViolation,
742 ErrorClass::Unsupported | ErrorClass::NotFound | ErrorClass::Conflict => {
743 IntegrityAuthorityClass::Unsupported
744 }
745 ErrorClass::Internal => IntegrityAuthorityClass::Internal,
746 };
747 Self {
748 diagnostic_code: error.diagnostic_code().error_code().raw(),
749 class,
750 }
751 }
752
753 #[must_use]
755 pub const fn diagnostic_code(&self) -> u16 {
756 self.diagnostic_code
757 }
758
759 #[must_use]
761 pub const fn class(&self) -> IntegrityAuthorityClass {
762 self.class
763 }
764}
765
766#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
768pub struct IntegrityResourceDiagnostic {
769 diagnostic_code: u16,
770}
771
772impl IntegrityResourceDiagnostic {
773 #[must_use]
775 pub const fn diagnostic_code(&self) -> u16 {
776 self.diagnostic_code
777 }
778}
779
780#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
782pub enum QuickIntegrityStatus {
783 CompleteClean,
785 CompleteWithFindings,
787 Uninspectable(IntegrityAuthorityDiagnostic),
789 ResourceLimited(IntegrityResourceDiagnostic),
791}
792
793#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
795pub struct QuickIntegrityResult {
796 entity: IntegrityEntityIdentity,
797 database_incarnation_id: DatabaseIncarnationId,
798 accepted_schema_version: u32,
799 accepted_schema_fingerprint: [u8; 16],
800 status: QuickIntegrityStatus,
801 total_findings: u64,
802 omitted_findings: u64,
803 findings: Vec<IntegrityFinding>,
804}
805
806impl QuickIntegrityResult {
807 #[must_use]
809 pub const fn entity(&self) -> &IntegrityEntityIdentity {
810 &self.entity
811 }
812
813 #[must_use]
815 pub const fn database_incarnation_id(&self) -> DatabaseIncarnationId {
816 self.database_incarnation_id
817 }
818
819 #[must_use]
821 pub const fn accepted_schema_version(&self) -> u32 {
822 self.accepted_schema_version
823 }
824
825 #[must_use]
827 pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
828 self.accepted_schema_fingerprint
829 }
830
831 #[must_use]
833 pub const fn status(&self) -> &QuickIntegrityStatus {
834 &self.status
835 }
836
837 #[must_use]
839 pub const fn total_findings(&self) -> u64 {
840 self.total_findings
841 }
842
843 #[must_use]
845 pub const fn omitted_findings(&self) -> u64 {
846 self.omitted_findings
847 }
848
849 #[must_use]
851 pub const fn findings(&self) -> &[IntegrityFinding] {
852 self.findings.as_slice()
853 }
854}
855
856struct QuickIntegrityAccumulator {
857 total_findings: u64,
858 findings: Vec<IntegrityFinding>,
859}
860
861impl QuickIntegrityAccumulator {
862 const fn new() -> Self {
863 Self {
864 total_findings: 0,
865 findings: Vec::new(),
866 }
867 }
868
869 fn record(&mut self, finding: IntegrityFinding) -> Result<(), IntegrityResourceDiagnostic> {
870 self.total_findings =
871 self.total_findings
872 .checked_add(1)
873 .ok_or(IntegrityResourceDiagnostic {
874 diagnostic_code: icydb_diagnostic_code::ErrorCode::RUNTIME_INTERNAL.raw(),
875 })?;
876 if self.findings.len() < MAX_QUICK_RETURNED_FINDINGS {
877 self.findings.push(finding);
878 }
879 Ok(())
880 }
881
882 fn complete(
883 self,
884 plan: &AcceptedInspectionPlan,
885 incarnation: DatabaseIncarnationId,
886 ) -> Result<QuickIntegrityResult, InternalError> {
887 let status = if self.total_findings == 0 {
888 QuickIntegrityStatus::CompleteClean
889 } else {
890 QuickIntegrityStatus::CompleteWithFindings
891 };
892 let omitted_findings = self.omitted_findings()?;
893 let identity = plan.identity();
894
895 Ok(QuickIntegrityResult {
896 entity: IntegrityEntityIdentity::from_plan(plan),
897 database_incarnation_id: incarnation,
898 accepted_schema_version: identity.accepted_schema_version().get(),
899 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
900 status,
901 total_findings: self.total_findings,
902 omitted_findings,
903 findings: self.findings,
904 })
905 }
906
907 fn resource_limited(
908 self,
909 plan: &AcceptedInspectionPlan,
910 incarnation: DatabaseIncarnationId,
911 diagnostic: IntegrityResourceDiagnostic,
912 ) -> Result<QuickIntegrityResult, InternalError> {
913 let omitted_findings = self.omitted_findings()?;
914 let identity = plan.identity();
915
916 Ok(QuickIntegrityResult {
917 entity: IntegrityEntityIdentity::from_plan(plan),
918 database_incarnation_id: incarnation,
919 accepted_schema_version: identity.accepted_schema_version().get(),
920 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
921 status: QuickIntegrityStatus::ResourceLimited(diagnostic),
922 total_findings: self.total_findings,
923 omitted_findings,
924 findings: self.findings,
925 })
926 }
927
928 fn omitted_findings(&self) -> Result<u64, InternalError> {
929 let returned = u64::try_from(self.findings.len()).map_err(|_| {
930 InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
931 })?;
932 self.total_findings.checked_sub(returned).ok_or_else(|| {
933 InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
934 })
935 }
936}
937
938pub(in crate::db) fn uninspectable_quick_integrity(
939 identity: crate::db::schema::AcceptedCatalogIdentity,
940 incarnation: DatabaseIncarnationId,
941 error: &InternalError,
942) -> QuickIntegrityResult {
943 QuickIntegrityResult {
944 entity: IntegrityEntityIdentity::from_accepted_identity(&identity),
945 database_incarnation_id: incarnation,
946 accepted_schema_version: identity.accepted_schema_version().get(),
947 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
948 status: QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(
949 error,
950 )),
951 total_findings: 0,
952 omitted_findings: 0,
953 findings: Vec::new(),
954 }
955}
956
957pub(in crate::db) fn execute_quick_integrity<C: CanisterKind>(
958 db: &crate::db::Db<C>,
959 plan: &AcceptedInspectionPlan,
960) -> Result<QuickIntegrityResult, InternalError> {
961 ensure_recovered(db)?;
962 let incarnation = database_incarnation_id()?;
963 let findings = match validate_quick_integrity_control(db, plan, incarnation) {
964 Ok(findings) => findings,
965 Err(error) => {
966 return Ok(uninspectable_quick_integrity(
967 plan.identity(),
968 incarnation,
969 &error,
970 ));
971 }
972 };
973 let mut accumulator = QuickIntegrityAccumulator::new();
974 for finding in findings {
975 if let Err(diagnostic) = accumulator.record(finding) {
976 return accumulator.resource_limited(plan, incarnation, diagnostic);
977 }
978 }
979
980 accumulator.complete(plan, incarnation)
981}
982
983pub(crate) fn generate_database_incarnation_id() -> Result<DatabaseIncarnationId, InternalError> {
984 DatabaseIncarnationId::generate()
985}
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990 use crate::{
991 db::schema::{FieldStorageDecode, LeafCodec, ScalarCodec},
992 db::{
993 commit::CommitSchemaFingerprint,
994 schema::{
995 AcceptedCatalogIdentity, AcceptedCompositeCatalog, AcceptedFieldKind,
996 AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
997 FieldId, IdentityState, IdentityStateOwner, PersistedFieldSnapshot,
998 PersistedSchemaSnapshot, SchemaFieldSlot, SchemaInsertDefault, SchemaRowLayout,
999 SchemaVersion, empty_accepted_enum_catalog_for_tests,
1000 },
1001 },
1002 types::EntityTag,
1003 };
1004
1005 fn plan() -> AcceptedInspectionPlan {
1006 let revision = AcceptedSchemaRevision::INITIAL;
1007 let identity = AcceptedCatalogIdentity::new(
1008 EntityTag::new(23),
1009 "tests::QuickEntity",
1010 "tests::QuickStore",
1011 revision,
1012 SchemaVersion::initial(),
1013 CommitSchemaFingerprint::from([0x44; 16]),
1014 );
1015 let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
1016 SchemaVersion::initial(),
1017 "tests::QuickEntity".to_string(),
1018 "QuickEntity".to_string(),
1019 FieldId::new(1),
1020 SchemaRowLayout::initial(vec![(FieldId::new(1), SchemaFieldSlot::new(0))]),
1021 vec![PersistedFieldSnapshot::new_initial(
1022 FieldId::new(1),
1023 "id".to_string(),
1024 SchemaFieldSlot::new(0),
1025 AcceptedFieldKind::Nat64,
1026 Vec::new(),
1027 false,
1028 SchemaInsertDefault::None,
1029 FieldStorageDecode::ByKind,
1030 LeafCodec::Scalar(ScalarCodec::Nat64),
1031 )],
1032 ));
1033 let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
1034 empty_accepted_enum_catalog_for_tests(),
1035 AcceptedCompositeCatalog::empty(),
1036 revision,
1037 );
1038
1039 AcceptedInspectionPlan::compile_relation_free_for_tests(identity, snapshot, value_catalog)
1040 .expect("accepted Quick plan should compile")
1041 }
1042
1043 fn finding(plan: &AcceptedInspectionPlan) -> IntegrityFinding {
1044 IntegrityFinding {
1045 diagnostic_code: icydb_diagnostic_code::ErrorCode::STORE_CORRUPTION.raw(),
1046 class: IntegrityFindingClass::Corruption,
1047 severity: IntegritySeverity::Error,
1048 kind: IntegrityFindingKind::MalformedRow,
1049 entity: IntegrityEntityIdentity::from_plan(plan),
1050 store_path: plan.identity().store_path().to_string(),
1051 phase: IntegrityPhase::Rows,
1052 verifier_family: IntegrityVerifierFamily::RowEnvelope,
1053 physical_key: vec![1],
1054 primary_key: None,
1055 field_paths: Vec::new(),
1056 value_path: None,
1057 constraint_id: None,
1058 constraint_name: None,
1059 schema_index_id: None,
1060 relation_id: None,
1061 expected: None,
1062 observed: None,
1063 }
1064 }
1065
1066 #[test]
1067 fn database_incarnation_rejects_zero_and_round_trips_current_bytes() {
1068 assert!(DatabaseIncarnationId::try_from_bytes([0; 16]).is_err());
1069
1070 let identity = DatabaseIncarnationId::for_tests(7);
1071 assert_eq!(
1072 DatabaseIncarnationId::try_from_bytes(identity.to_bytes())
1073 .expect("nonzero incarnation should decode"),
1074 identity,
1075 );
1076 }
1077
1078 #[test]
1079 fn integrity_finding_candid_preserves_targeted_constraint_path() {
1080 let plan = plan();
1081 let mut finding = finding(&plan);
1082 let path = ConstraintValuePath::new(vec![
1083 crate::error::ConstraintValuePathComponent::RootField { field_id: 1 },
1084 crate::error::ConstraintValuePathComponent::ListElement { index: 2 },
1085 ]);
1086 finding.kind = IntegrityFindingKind::ConstraintViolation;
1087 finding.value_path = Some(Box::new(path.clone()));
1088 finding.constraint_id = Some(7);
1089 finding.constraint_name = Some("nested_limit".to_string());
1090
1091 let bytes = candid::encode_one(&finding).expect("integrity finding should encode");
1092 let decoded: IntegrityFinding =
1093 candid::decode_one(&bytes).expect("integrity finding should decode");
1094 assert_eq!(decoded.value_path(), Some(&path));
1095 assert_eq!(decoded.constraint_id(), Some(7));
1096 assert_eq!(decoded.constraint_name(), Some("nested_limit"));
1097 }
1098
1099 #[test]
1100 fn quick_clean_result_binds_incarnation_and_accepted_plan_identity() {
1101 let plan = plan();
1102 let incarnation = DatabaseIncarnationId::for_tests(8);
1103 let result = QuickIntegrityAccumulator::new()
1104 .complete(&plan, incarnation)
1105 .expect("clean Quick accounting should remain valid");
1106
1107 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteClean);
1108 assert_eq!(result.database_incarnation_id(), incarnation);
1109 assert_eq!(result.accepted_schema_version(), 1);
1110 assert_eq!(result.accepted_schema_fingerprint(), [0x44; 16]);
1111 assert_eq!(result.total_findings(), 0);
1112 assert_eq!(result.omitted_findings(), 0);
1113 }
1114
1115 #[test]
1116 fn quick_findings_keep_a_bounded_prefix_and_exact_omitted_count() {
1117 let plan = plan();
1118 let mut accumulator = QuickIntegrityAccumulator::new();
1119 for _ in 0..=MAX_QUICK_RETURNED_FINDINGS {
1120 accumulator
1121 .record(finding(&plan))
1122 .expect("bounded test finding count should fit");
1123 }
1124 let result = accumulator
1125 .complete(&plan, DatabaseIncarnationId::for_tests(9))
1126 .expect("one-over-cap Quick accounting should remain valid");
1127
1128 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteWithFindings,);
1129 assert_eq!(result.total_findings(), 65);
1130 assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1131 assert_eq!(result.omitted_findings(), 1);
1132 assert_eq!(
1133 result.total_findings(),
1134 result.findings().len() as u64 + result.omitted_findings(),
1135 );
1136 }
1137
1138 #[test]
1139 fn quick_findings_at_the_exact_returned_cap_have_no_omissions() {
1140 let plan = plan();
1141 let mut accumulator = QuickIntegrityAccumulator::new();
1142 for _ in 0..MAX_QUICK_RETURNED_FINDINGS {
1143 accumulator
1144 .record(finding(&plan))
1145 .expect("exact-cap finding count should fit");
1146 }
1147 let result = accumulator
1148 .complete(&plan, DatabaseIncarnationId::for_tests(10))
1149 .expect("exact-cap Quick accounting should remain valid");
1150
1151 assert_eq!(result.total_findings(), 64);
1152 assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1153 assert_eq!(result.omitted_findings(), 0);
1154 }
1155
1156 #[test]
1157 fn quick_selected_authority_failure_is_not_a_clean_completion() {
1158 let plan = plan();
1159 let error = InternalError::accepted_row_constraint_program_corrupt();
1160 let result = uninspectable_quick_integrity(
1161 plan.identity(),
1162 DatabaseIncarnationId::for_tests(11),
1163 &error,
1164 );
1165
1166 assert!(matches!(
1167 result.status(),
1168 QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic {
1169 class: IntegrityAuthorityClass::Corruption,
1170 ..
1171 }),
1172 ));
1173 assert_eq!(result.total_findings(), 0);
1174 assert_eq!(result.omitted_findings(), 0);
1175 }
1176
1177 #[test]
1178 fn quick_identity_inventory_rejects_active_retired_owner_collision_first() {
1179 let incarnation = DatabaseIncarnationId::for_tests(12);
1180 let owner = IdentityStateOwner::try_new(incarnation, EntityTag::new(31), FieldId::new(1))
1181 .expect("identity owner should admit");
1182 let active = IdentityState::new_active(owner, AcceptedFieldKind::Nat64)
1183 .expect("active identity state should admit");
1184 let retired = active.retire().expect("active state should retire");
1185 let mut owners = BTreeMap::new();
1186
1187 record_quick_identity_owner(&mut owners, "tests::first", &active)
1188 .expect("the first owner should admit");
1189 let error = record_quick_identity_owner(&mut owners, "tests::second", &retired)
1190 .expect_err("an active/retired owner collision must reject");
1191
1192 assert_eq!(error.class(), ErrorClass::Corruption);
1193 assert_eq!(error.origin(), ErrorOrigin::Identity);
1194 }
1195}