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 validate_quick_integrity_control<C: CanisterKind>(
134 db: &crate::db::Db<C>,
135 plan: &AcceptedInspectionPlan,
136 incarnation: DatabaseIncarnationId,
137) -> Result<Vec<IntegrityFinding>, InternalError> {
138 let identity = plan.identity();
139 let source_store = db.store_handle(identity.store_path())?;
140 let relations = plan.relation_inspection();
141 let mut participating_stores =
142 BTreeMap::from([(identity.store_path().to_string(), source_store)]);
143 for relation in relations {
144 participating_stores
145 .entry(relation.target_store_path().to_string())
146 .or_insert_with(|| relation.target_store());
147 }
148
149 proof::validate_integrity_allocation_registry()?;
154 validate_quick_identity_control(db, incarnation)?;
155 let mut findings = Vec::new();
156 for (store_path, store) in &participating_stores {
157 if let Some(finding) = validate_quick_store_control(plan, store_path, *store)? {
158 findings.push(finding);
159 }
160 }
161 for ordinal in 0..plan.index_inspection().len() {
162 let _domain = plan
163 .index_inspection()
164 .domain(ordinal, identity.entity_tag())?;
165 }
166
167 Ok(findings)
168}
169
170fn validate_quick_identity_control<C: CanisterKind>(
171 db: &crate::db::Db<C>,
172 incarnation: DatabaseIncarnationId,
173) -> Result<(), InternalError> {
174 let mut stores = db.with_store_registry(|registry| registry.iter().collect::<Vec<_>>());
175 stores.sort_unstable_by_key(|(store_path, _)| *store_path);
176
177 let mut owners = BTreeMap::new();
178 let mut state_count = 0usize;
179 for (store_path, store) in stores {
180 let states = store.with_schema(|schema_store| {
181 schema_store.identity_state_inventory_for_integrity(incarnation)
182 })?;
183 state_count = state_count
184 .checked_add(states.len())
185 .ok_or_else(InternalError::identity_state_corruption)?;
186 if state_count > MAX_IDENTITY_STATE_RECORDS_PER_DATABASE {
187 return Err(InternalError::identity_state_corruption());
188 }
189
190 for state in states {
191 let owner = state.owner();
192 record_quick_identity_owner(&mut owners, store_path, &state)?;
193 if state.lifecycle() == IdentityStateLifecycle::Active {
194 let runtime_entity = db
195 .accepted_runtime_entity_for_tag(owner.entity_tag())
196 .map_err(|_| InternalError::identity_state_corruption())?;
197 if runtime_entity.store_path() != store_path {
198 return Err(InternalError::identity_state_corruption());
199 }
200 }
201 }
202 }
203
204 Ok(())
205}
206
207fn record_quick_identity_owner<'a>(
208 owners: &mut BTreeMap<(crate::types::EntityTag, crate::db::schema::FieldId), &'a str>,
209 store_path: &'a str,
210 state: &crate::db::schema::IdentityState,
211) -> Result<(), InternalError> {
212 let owner = state.owner();
213 let key = (owner.entity_tag(), owner.field_id());
214 if owners.insert(key, store_path).is_some() {
215 return Err(InternalError::identity_state_corruption());
216 }
217 Ok(())
218}
219
220fn validate_quick_store_control(
221 plan: &AcceptedInspectionPlan,
222 store_path: &str,
223 store: StoreHandle,
224) -> Result<Option<IntegrityFinding>, InternalError> {
225 let capabilities = store.storage_capabilities();
226 let allocations = store.allocation_identities();
227 match capabilities.storage_mode() {
228 StoreRuntimeStorageMode::Heap => {
229 if capabilities != StoreRuntimeStorageCapabilities::heap()
230 || allocations != StoreAllocationIdentities::absent()
231 || store.journal_tail_store().is_some()
232 {
233 return Err(InternalError::store_invariant());
234 }
235 Ok(None)
236 }
237 StoreRuntimeStorageMode::Journaled => {
238 if capabilities != StoreRuntimeStorageCapabilities::journaled()
239 || !allocations.matches_storage_capabilities(capabilities)
240 {
241 return Err(InternalError::store_invariant());
242 }
243 let journal = store
244 .journal_tail_store()
245 .ok_or_else(InternalError::store_invariant)?
246 .with_borrow(crate::db::journal::JournalTailStore::proof_identity)?;
247 if !journal.is_well_formed() {
248 return Ok(Some(quick_journal_control_finding(plan, store_path)));
249 }
250 Ok(None)
251 }
252 }
253}
254
255fn quick_journal_control_finding(
256 plan: &AcceptedInspectionPlan,
257 store_path: &str,
258) -> IntegrityFinding {
259 let error = InternalError::store_corruption();
260 IntegrityFinding {
261 diagnostic_code: error.diagnostic_code().error_code().raw(),
262 class: IntegrityFindingClass::Corruption,
263 severity: IntegritySeverity::Error,
264 kind: IntegrityFindingKind::JournalControlMismatch,
265 entity: IntegrityEntityIdentity::from_plan(plan),
266 store_path: store_path.to_string(),
267 phase: IntegrityPhase::QuickMetadata,
268 verifier_family: IntegrityVerifierFamily::JournalEnvelope,
269 physical_key: Vec::new(),
270 primary_key: None,
271 field_paths: Vec::new(),
272 value_path: None,
273 constraint_id: None,
274 constraint_name: None,
275 schema_index_id: None,
276 relation_id: None,
277 expected: Some("well-formed-journal-control".to_string()),
278 observed: Some("inconsistent-journal-control".to_string()),
279 }
280}
281
282fn relation_field_paths(plan: &AcceptedInspectionPlan, relation_id: u32) -> Vec<String> {
283 let snapshot = plan.snapshot().persisted_snapshot();
284 let Some(relation) = snapshot
285 .relations()
286 .iter()
287 .find(|relation| relation.id().get() == relation_id)
288 else {
289 return Vec::new();
290 };
291
292 relation
293 .local_field_ids()
294 .iter()
295 .filter_map(|field_id| {
296 snapshot
297 .fields()
298 .iter()
299 .find(|field| field.id() == *field_id)
300 .map(|field| field.name().to_string())
301 })
302 .collect()
303}
304
305const MAX_QUICK_RETURNED_FINDINGS: usize = 64;
306#[cfg(target_arch = "wasm32")]
307const DATABASE_INCARNATION_DOMAIN: &[u8] = b"icydb.database-incarnation.v1";
308static DATABASE_INCARNATION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
309
310#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
317pub struct DatabaseIncarnationId([u8; 16]);
318
319impl DatabaseIncarnationId {
320 pub(crate) fn try_from_bytes(bytes: [u8; 16]) -> Result<Self, InternalError> {
322 if bytes == [0; 16] {
323 return Err(InternalError::database_incarnation_invalid());
324 }
325
326 Ok(Self(bytes))
327 }
328
329 #[must_use]
331 pub const fn to_bytes(self) -> [u8; 16] {
332 self.0
333 }
334
335 fn generate() -> Result<Self, InternalError> {
336 let sequence = DATABASE_INCARNATION_SEQUENCE
337 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
338 current.checked_add(1)
339 })
340 .map_err(|_| InternalError::database_incarnation_generation_failed())?
341 .checked_add(1)
342 .ok_or_else(InternalError::database_incarnation_generation_failed)?;
343
344 #[cfg(not(target_arch = "wasm32"))]
345 let bytes = {
346 let mut bytes = [0_u8; 16];
347 getrandom::fill(&mut bytes)
348 .map_err(|_| InternalError::database_incarnation_generation_failed())?;
349 bytes
350 };
351
352 #[cfg(target_arch = "wasm32")]
353 let bytes = {
354 use sha2::{Digest, Sha256};
355
356 let mut hasher = Sha256::new();
357 hasher.update(DATABASE_INCARNATION_DOMAIN);
358 hasher.update(ic_cdk::api::canister_self().as_slice());
359 hasher.update(ic_cdk::api::time().to_be_bytes());
360 hasher.update(sequence.to_be_bytes());
361 let digest = hasher.finalize();
362 let mut bytes = [0_u8; 16];
363 bytes.copy_from_slice(&digest[..16]);
364 bytes
365 };
366
367 let _ = sequence;
368 Self::try_from_bytes(bytes)
369 }
370
371 #[cfg(test)]
372 pub(crate) const fn for_tests(fill: u8) -> Self {
373 let mut bytes = [fill; 16];
374 if fill == 0 {
375 bytes[15] = 1;
376 }
377 Self(bytes)
378 }
379}
380
381#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
383pub struct IntegrityEntityIdentity {
384 entity_tag: u64,
385 entity_path: String,
386 store_path: String,
387}
388
389impl IntegrityEntityIdentity {
390 fn from_plan(plan: &AcceptedInspectionPlan) -> Self {
391 Self::from_accepted_identity(plan.identity_ref())
392 }
393
394 pub(in crate::db) fn from_accepted_identity(
395 identity: &crate::db::schema::AcceptedCatalogIdentity,
396 ) -> Self {
397 Self {
398 entity_tag: identity.entity_tag().value(),
399 entity_path: identity.entity_path().to_string(),
400 store_path: identity.store_path().to_string(),
401 }
402 }
403
404 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
405 if self.entity_tag == 0
406 || self.entity_path.is_empty()
407 || self.entity_path.len() > MAX_INTEGRITY_PATH_BYTES
408 || self.store_path.is_empty()
409 || self.store_path.len() > MAX_INTEGRITY_PATH_BYTES
410 {
411 return Err(IntegrityJobError::InvalidEntityIdentity);
412 }
413 Ok(())
414 }
415
416 #[must_use]
418 pub const fn entity_tag(&self) -> u64 {
419 self.entity_tag
420 }
421
422 #[must_use]
424 pub const fn entity_path(&self) -> &str {
425 self.entity_path.as_str()
426 }
427
428 #[must_use]
430 pub const fn store_path(&self) -> &str {
431 self.store_path.as_str()
432 }
433}
434
435#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
437pub enum IntegrityAuthorityClass {
438 Corruption,
440 IncompatiblePersistedFormat,
442 InvariantViolation,
444 Unsupported,
446 Internal,
448}
449
450#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
452pub enum IntegrityFindingClass {
453 Corruption,
455 IncompatiblePersistedFormat,
457 ResourceLimited,
459}
460
461#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
464pub enum IntegrityFindingKind {
465 MalformedDataKey,
467
468 MalformedRow,
470
471 OversizedRow,
473
474 InvalidFieldValue,
476
477 PrimaryKeyMismatch,
479
480 InvalidIdentityValue,
482
483 IdentityHighWaterExceeded,
485
486 ConstraintViolation,
488
489 MissingIndexEntry,
491
492 DivergentIndexEntry,
494
495 MalformedIndexEntry,
497
498 OrphanIndexEntry,
500
501 DuplicateUniqueIndexKey,
503
504 MissingRelationTarget,
506
507 MissingReverseRelationEntry,
509
510 DivergentReverseRelationEntry,
512
513 MalformedReverseRelationEntry,
515
516 OrphanReverseRelationEntry,
518
519 MalformedJournalBatch,
521
522 JournalSequenceGap,
524
525 DuplicateJournalBatchIdentity,
527
528 JournalControlMismatch,
530}
531
532#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
535pub enum IntegrityPhase {
536 QuickMetadata,
538
539 Rows,
541
542 IndexEntries,
544
545 ReverseRelations,
547
548 JournalTails,
550
551 FinalProofVectorCheck,
553}
554
555#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
558pub enum IntegrityVerifierFamily {
559 DataKey,
561
562 RowEnvelope,
564
565 FieldValue,
567
568 PrimaryKey,
570
571 IdentityState,
573
574 ValidatedConstraints,
576
577 ForwardIndex,
579
580 IndexEntry,
582
583 UniqueIndex,
585
586 Relation,
588
589 ReverseRelationEntry,
591
592 JournalEnvelope,
594
595 JournalBatchIdentity,
597}
598
599#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
601pub enum IntegritySeverity {
602 Error,
604 Advisory,
606}
607
608#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
612pub struct IntegrityFinding {
613 diagnostic_code: u16,
614 class: IntegrityFindingClass,
615 severity: IntegritySeverity,
616 kind: IntegrityFindingKind,
617 entity: IntegrityEntityIdentity,
618 store_path: String,
619 phase: IntegrityPhase,
620 verifier_family: IntegrityVerifierFamily,
621 physical_key: Vec<u8>,
622 primary_key: Option<Vec<u8>>,
623 field_paths: Vec<String>,
624 value_path: Option<Box<ConstraintValuePath>>,
625 constraint_id: Option<u32>,
626 constraint_name: Option<String>,
627 schema_index_id: Option<u32>,
628 relation_id: Option<u32>,
629 expected: Option<String>,
630 observed: Option<String>,
631}
632
633impl IntegrityFinding {
634 #[must_use]
636 pub const fn diagnostic_code(&self) -> u16 {
637 self.diagnostic_code
638 }
639
640 #[must_use]
642 pub const fn class(&self) -> IntegrityFindingClass {
643 self.class
644 }
645
646 #[must_use]
648 pub const fn severity(&self) -> IntegritySeverity {
649 self.severity
650 }
651
652 #[must_use]
654 pub const fn kind(&self) -> IntegrityFindingKind {
655 self.kind
656 }
657
658 #[must_use]
660 pub const fn entity(&self) -> &IntegrityEntityIdentity {
661 &self.entity
662 }
663
664 #[must_use]
666 pub const fn store_path(&self) -> &str {
667 self.store_path.as_str()
668 }
669
670 #[must_use]
672 pub const fn phase(&self) -> IntegrityPhase {
673 self.phase
674 }
675
676 #[must_use]
678 pub const fn verifier_family(&self) -> IntegrityVerifierFamily {
679 self.verifier_family
680 }
681
682 #[must_use]
684 pub const fn physical_key(&self) -> &[u8] {
685 self.physical_key.as_slice()
686 }
687
688 #[must_use]
690 pub fn primary_key(&self) -> Option<&[u8]> {
691 self.primary_key.as_deref()
692 }
693
694 #[must_use]
696 pub const fn field_paths(&self) -> &[String] {
697 self.field_paths.as_slice()
698 }
699
700 #[must_use]
702 pub fn value_path(&self) -> Option<&ConstraintValuePath> {
703 self.value_path.as_deref()
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 pub(in crate::db) 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, Debug, Deserialize, Eq, PartialEq)]
798pub enum QuickIntegrityStatus {
799 CompleteClean,
801 CompleteWithFindings,
803 Uninspectable(IntegrityAuthorityDiagnostic),
805 ResourceLimited(IntegrityResourceDiagnostic),
807}
808
809#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
811pub struct QuickIntegrityResult {
812 entity: IntegrityEntityIdentity,
813 database_incarnation_id: DatabaseIncarnationId,
814 accepted_schema_version: u32,
815 accepted_schema_fingerprint: [u8; 16],
816 status: QuickIntegrityStatus,
817 total_findings: u64,
818 omitted_findings: u64,
819 findings: Vec<IntegrityFinding>,
820}
821
822impl QuickIntegrityResult {
823 #[must_use]
825 pub const fn entity(&self) -> &IntegrityEntityIdentity {
826 &self.entity
827 }
828
829 #[must_use]
831 pub const fn database_incarnation_id(&self) -> DatabaseIncarnationId {
832 self.database_incarnation_id
833 }
834
835 #[must_use]
837 pub const fn accepted_schema_version(&self) -> u32 {
838 self.accepted_schema_version
839 }
840
841 #[must_use]
843 pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
844 self.accepted_schema_fingerprint
845 }
846
847 #[must_use]
849 pub const fn status(&self) -> &QuickIntegrityStatus {
850 &self.status
851 }
852
853 #[must_use]
855 pub const fn total_findings(&self) -> u64 {
856 self.total_findings
857 }
858
859 #[must_use]
861 pub const fn omitted_findings(&self) -> u64 {
862 self.omitted_findings
863 }
864
865 #[must_use]
867 pub const fn findings(&self) -> &[IntegrityFinding] {
868 self.findings.as_slice()
869 }
870}
871
872struct QuickIntegrityAccumulator {
873 total_findings: u64,
874 findings: Vec<IntegrityFinding>,
875}
876
877impl QuickIntegrityAccumulator {
878 const fn new() -> Self {
879 Self {
880 total_findings: 0,
881 findings: Vec::new(),
882 }
883 }
884
885 fn record(&mut self, finding: IntegrityFinding) -> Result<(), IntegrityResourceDiagnostic> {
886 self.total_findings =
887 self.total_findings
888 .checked_add(1)
889 .ok_or(IntegrityResourceDiagnostic {
890 diagnostic_code: icydb_diagnostic_code::ErrorCode::RUNTIME_INTERNAL.raw(),
891 })?;
892 if self.findings.len() < MAX_QUICK_RETURNED_FINDINGS {
893 self.findings.push(finding);
894 }
895 Ok(())
896 }
897
898 fn complete(
899 self,
900 plan: &AcceptedInspectionPlan,
901 incarnation: DatabaseIncarnationId,
902 ) -> Result<QuickIntegrityResult, InternalError> {
903 let status = if self.total_findings == 0 {
904 QuickIntegrityStatus::CompleteClean
905 } else {
906 QuickIntegrityStatus::CompleteWithFindings
907 };
908 let omitted_findings = self.omitted_findings()?;
909 let identity = plan.identity();
910
911 Ok(QuickIntegrityResult {
912 entity: IntegrityEntityIdentity::from_plan(plan),
913 database_incarnation_id: incarnation,
914 accepted_schema_version: identity.accepted_schema_version().get(),
915 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
916 status,
917 total_findings: self.total_findings,
918 omitted_findings,
919 findings: self.findings,
920 })
921 }
922
923 fn resource_limited(
924 self,
925 plan: &AcceptedInspectionPlan,
926 incarnation: DatabaseIncarnationId,
927 diagnostic: IntegrityResourceDiagnostic,
928 ) -> Result<QuickIntegrityResult, InternalError> {
929 let omitted_findings = self.omitted_findings()?;
930 let identity = plan.identity();
931
932 Ok(QuickIntegrityResult {
933 entity: IntegrityEntityIdentity::from_plan(plan),
934 database_incarnation_id: incarnation,
935 accepted_schema_version: identity.accepted_schema_version().get(),
936 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
937 status: QuickIntegrityStatus::ResourceLimited(diagnostic),
938 total_findings: self.total_findings,
939 omitted_findings,
940 findings: self.findings,
941 })
942 }
943
944 fn omitted_findings(&self) -> Result<u64, InternalError> {
945 let returned = u64::try_from(self.findings.len()).map_err(|_| {
946 InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
947 })?;
948 self.total_findings.checked_sub(returned).ok_or_else(|| {
949 InternalError::classified(ErrorClass::InvariantViolation, ErrorOrigin::Response)
950 })
951 }
952}
953
954pub(in crate::db) fn uninspectable_quick_integrity(
955 identity: crate::db::schema::AcceptedCatalogIdentity,
956 incarnation: DatabaseIncarnationId,
957 error: &InternalError,
958) -> QuickIntegrityResult {
959 QuickIntegrityResult {
960 entity: IntegrityEntityIdentity::from_accepted_identity(&identity),
961 database_incarnation_id: incarnation,
962 accepted_schema_version: identity.accepted_schema_version().get(),
963 accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
964 status: QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(
965 error,
966 )),
967 total_findings: 0,
968 omitted_findings: 0,
969 findings: Vec::new(),
970 }
971}
972
973pub(in crate::db) fn execute_quick_integrity<C: CanisterKind>(
974 db: &crate::db::Db<C>,
975 plan: &AcceptedInspectionPlan,
976 incarnation: DatabaseIncarnationId,
977) -> Result<QuickIntegrityResult, InternalError> {
978 ensure_recovery_admitted(db)?;
979 let findings = match validate_quick_integrity_control(db, plan, incarnation) {
980 Ok(findings) => findings,
981 Err(error) => {
982 return Ok(uninspectable_quick_integrity(
983 plan.identity(),
984 incarnation,
985 &error,
986 ));
987 }
988 };
989 let mut accumulator = QuickIntegrityAccumulator::new();
990 for finding in findings {
991 if let Err(diagnostic) = accumulator.record(finding) {
992 return accumulator.resource_limited(plan, incarnation, diagnostic);
993 }
994 }
995
996 accumulator.complete(plan, incarnation)
997}
998
999pub(crate) fn generate_database_incarnation_id() -> Result<DatabaseIncarnationId, InternalError> {
1000 DatabaseIncarnationId::generate()
1001}
1002
1003pub(crate) fn generate_cursor_authentication_key() -> Result<[u8; 32], InternalError> {
1009 let first = <crate::types::Ulid as crate::types::GenerateKey>::generate()?;
1010 let second = <crate::types::Ulid as crate::types::GenerateKey>::generate()?;
1011 let mut bytes = [0_u8; 32];
1012 bytes[..16].copy_from_slice(&first.to_bytes());
1013 bytes[16..].copy_from_slice(&second.to_bytes());
1014 if bytes == [0; 32] {
1015 return Err(InternalError::database_incarnation_generation_failed());
1016 }
1017
1018 Ok(bytes)
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024 use crate::{
1025 db::schema::{FieldStorageDecode, LeafCodec, ScalarCodec},
1026 db::{
1027 commit::CommitSchemaFingerprint,
1028 schema::{
1029 AcceptedCatalogIdentity, AcceptedCompositeCatalog, AcceptedFieldKind,
1030 AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
1031 FieldId, IdentityState, IdentityStateOwner, PersistedFieldSnapshot,
1032 PersistedSchemaSnapshot, SchemaFieldSlot, SchemaInsertDefault, SchemaRowLayout,
1033 SchemaVersion, empty_accepted_enum_catalog_for_tests,
1034 },
1035 },
1036 types::EntityTag,
1037 };
1038
1039 fn plan() -> AcceptedInspectionPlan {
1040 let revision = AcceptedSchemaRevision::INITIAL;
1041 let identity = AcceptedCatalogIdentity::new(
1042 EntityTag::new(23),
1043 "tests::QuickEntity",
1044 "tests::QuickStore",
1045 revision,
1046 SchemaVersion::initial(),
1047 CommitSchemaFingerprint::from([0x44; 16]),
1048 );
1049 let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
1050 SchemaVersion::initial(),
1051 "tests::QuickEntity".to_string(),
1052 "QuickEntity".to_string(),
1053 FieldId::new(1),
1054 SchemaRowLayout::initial(vec![(FieldId::new(1), SchemaFieldSlot::new(0))]),
1055 vec![PersistedFieldSnapshot::new_initial(
1056 FieldId::new(1),
1057 "id".to_string(),
1058 SchemaFieldSlot::new(0),
1059 AcceptedFieldKind::Nat64,
1060 Vec::new(),
1061 false,
1062 SchemaInsertDefault::None,
1063 FieldStorageDecode::ByKind,
1064 LeafCodec::Scalar(ScalarCodec::Nat64),
1065 )],
1066 ));
1067 let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
1068 empty_accepted_enum_catalog_for_tests(),
1069 AcceptedCompositeCatalog::empty(),
1070 revision,
1071 );
1072
1073 AcceptedInspectionPlan::compile_relation_free_for_tests(identity, snapshot, value_catalog)
1074 .expect("accepted Quick plan should compile")
1075 }
1076
1077 fn finding(plan: &AcceptedInspectionPlan) -> IntegrityFinding {
1078 IntegrityFinding {
1079 diagnostic_code: icydb_diagnostic_code::ErrorCode::STORE_CORRUPTION.raw(),
1080 class: IntegrityFindingClass::Corruption,
1081 severity: IntegritySeverity::Error,
1082 kind: IntegrityFindingKind::MalformedRow,
1083 entity: IntegrityEntityIdentity::from_plan(plan),
1084 store_path: plan.identity().store_path().to_string(),
1085 phase: IntegrityPhase::Rows,
1086 verifier_family: IntegrityVerifierFamily::RowEnvelope,
1087 physical_key: vec![1],
1088 primary_key: None,
1089 field_paths: Vec::new(),
1090 value_path: None,
1091 constraint_id: None,
1092 constraint_name: None,
1093 schema_index_id: None,
1094 relation_id: None,
1095 expected: None,
1096 observed: None,
1097 }
1098 }
1099
1100 #[test]
1101 fn database_incarnation_rejects_zero_and_round_trips_current_bytes() {
1102 assert!(DatabaseIncarnationId::try_from_bytes([0; 16]).is_err());
1103
1104 let identity = DatabaseIncarnationId::for_tests(7);
1105 assert_eq!(
1106 DatabaseIncarnationId::try_from_bytes(identity.to_bytes())
1107 .expect("nonzero incarnation should decode"),
1108 identity,
1109 );
1110 }
1111
1112 #[test]
1113 fn integrity_finding_candid_preserves_targeted_constraint_path() {
1114 let plan = plan();
1115 let mut finding = finding(&plan);
1116 let path = ConstraintValuePath::new(vec![
1117 crate::error::ConstraintValuePathComponent::RootField { field_id: 1 },
1118 crate::error::ConstraintValuePathComponent::ListElement { index: 2 },
1119 ]);
1120 finding.kind = IntegrityFindingKind::ConstraintViolation;
1121 finding.value_path = Some(Box::new(path.clone()));
1122 finding.constraint_id = Some(7);
1123 finding.constraint_name = Some("nested_limit".to_string());
1124
1125 let bytes = candid::encode_one(&finding).expect("integrity finding should encode");
1126 let decoded: IntegrityFinding =
1127 candid::decode_one(&bytes).expect("integrity finding should decode");
1128 assert_eq!(decoded.value_path(), Some(&path));
1129 assert_eq!(decoded.constraint_id(), Some(7));
1130 assert_eq!(decoded.constraint_name(), Some("nested_limit"));
1131 }
1132
1133 #[test]
1134 fn quick_clean_result_binds_incarnation_and_accepted_plan_identity() {
1135 let plan = plan();
1136 let incarnation = DatabaseIncarnationId::for_tests(8);
1137 let result = QuickIntegrityAccumulator::new()
1138 .complete(&plan, incarnation)
1139 .expect("clean Quick accounting should remain valid");
1140
1141 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteClean);
1142 assert_eq!(result.database_incarnation_id(), incarnation);
1143 assert_eq!(result.accepted_schema_version(), 1);
1144 assert_eq!(result.accepted_schema_fingerprint(), [0x44; 16]);
1145 assert_eq!(result.total_findings(), 0);
1146 assert_eq!(result.omitted_findings(), 0);
1147 }
1148
1149 #[test]
1150 fn quick_findings_keep_a_bounded_prefix_and_exact_omitted_count() {
1151 let plan = plan();
1152 let mut accumulator = QuickIntegrityAccumulator::new();
1153 for _ in 0..=MAX_QUICK_RETURNED_FINDINGS {
1154 accumulator
1155 .record(finding(&plan))
1156 .expect("bounded test finding count should fit");
1157 }
1158 let result = accumulator
1159 .complete(&plan, DatabaseIncarnationId::for_tests(9))
1160 .expect("one-over-cap Quick accounting should remain valid");
1161
1162 assert_eq!(result.status(), &QuickIntegrityStatus::CompleteWithFindings,);
1163 assert_eq!(result.total_findings(), 65);
1164 assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1165 assert_eq!(result.omitted_findings(), 1);
1166 assert_eq!(
1167 result.total_findings(),
1168 result.findings().len() as u64 + result.omitted_findings(),
1169 );
1170 }
1171
1172 #[test]
1173 fn quick_findings_at_the_exact_returned_cap_have_no_omissions() {
1174 let plan = plan();
1175 let mut accumulator = QuickIntegrityAccumulator::new();
1176 for _ in 0..MAX_QUICK_RETURNED_FINDINGS {
1177 accumulator
1178 .record(finding(&plan))
1179 .expect("exact-cap finding count should fit");
1180 }
1181 let result = accumulator
1182 .complete(&plan, DatabaseIncarnationId::for_tests(10))
1183 .expect("exact-cap Quick accounting should remain valid");
1184
1185 assert_eq!(result.total_findings(), 64);
1186 assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
1187 assert_eq!(result.omitted_findings(), 0);
1188 }
1189
1190 #[test]
1191 fn quick_selected_authority_failure_is_not_a_clean_completion() {
1192 let plan = plan();
1193 let error = InternalError::accepted_row_constraint_program_corrupt();
1194 let result = uninspectable_quick_integrity(
1195 plan.identity(),
1196 DatabaseIncarnationId::for_tests(11),
1197 &error,
1198 );
1199
1200 assert!(matches!(
1201 result.status(),
1202 QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic {
1203 class: IntegrityAuthorityClass::Corruption,
1204 ..
1205 }),
1206 ));
1207 assert_eq!(result.total_findings(), 0);
1208 assert_eq!(result.omitted_findings(), 0);
1209 }
1210
1211 #[test]
1212 fn quick_identity_inventory_rejects_active_retired_owner_collision_first() {
1213 let incarnation = DatabaseIncarnationId::for_tests(12);
1214 let owner = IdentityStateOwner::try_new(incarnation, EntityTag::new(31), FieldId::new(1))
1215 .expect("identity owner should admit");
1216 let active = IdentityState::new_active(owner, AcceptedFieldKind::Nat64)
1217 .expect("active identity state should admit");
1218 let retired = active.retire().expect("active state should retire");
1219 let mut owners = BTreeMap::new();
1220
1221 record_quick_identity_owner(&mut owners, "tests::first", &active)
1222 .expect("the first owner should admit");
1223 let error = record_quick_identity_owner(&mut owners, "tests::second", &retired)
1224 .expect_err("an active/retired owner collision must reject");
1225
1226 assert_eq!(error.class(), ErrorClass::Corruption);
1227 assert_eq!(error.origin(), ErrorOrigin::Identity);
1228 }
1229}