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