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