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