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