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