1use crate::db::{
7 codec::hex::decode_hex_nibble,
8 encode_hex_lower,
9 integrity::{
10 DatabaseIncarnationId, IntegrityAuthorityDiagnostic, IntegrityEntityIdentity,
11 IntegrityFinding, IntegrityFindingKind, IntegrityPhase, IntegrityProofVector,
12 IntegrityResourceDiagnostic, IntegrityVerifierFamily, MAX_INTEGRITY_PATH_BYTES,
13 PhysicalUnitCheckpoint,
14 },
15 journal::JournalInspectionCheckpoint,
16 schema::MAX_ACCEPTED_TARGET_PATH_COMPONENTS,
17};
18use crate::error::{ConstraintValuePath, ConstraintValuePathComponent, InternalError};
19
20use icydb_diagnostic_code::{DiagnosticDetail, RuntimeBoundaryCode};
21use serde::Deserialize;
22
23pub(in crate::db) const MAX_INTEGRITY_OWNER_BYTES: usize = 256;
24pub(in crate::db) const MAX_INTEGRITY_SUBMISSION_KEY_BYTES: usize = 256;
25pub(super) const MAX_INTEGRITY_RECEIPT_FINDINGS: usize = 64;
26pub(in crate::db) const MAX_INTEGRITY_IN_PROGRESS_PAGES: u64 = u64::MAX - 1;
27
28#[derive(
31 serde::Serialize, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd,
32)]
33pub struct IntegrityJobId([u8; 32]);
34
35impl IntegrityJobId {
36 pub fn try_from_bytes(bytes: [u8; 32]) -> Result<Self, IntegrityJobError> {
43 if bytes == [0; 32] {
44 return Err(IntegrityJobError::CorruptProgressRecord);
45 }
46 Ok(Self(bytes))
47 }
48
49 pub fn try_from_hex(value: &str) -> Result<Self, IntegrityJobError> {
56 if value.len() != 64 {
57 return Err(IntegrityJobError::InvalidJobId);
58 }
59
60 let mut bytes = [0_u8; 32];
61 for (index, pair) in value.as_bytes().as_chunks::<2>().0.iter().enumerate() {
62 let high = decode_hex_nibble(pair[0]).ok_or(IntegrityJobError::InvalidJobId)?;
63 let low = decode_hex_nibble(pair[1]).ok_or(IntegrityJobError::InvalidJobId)?;
64 bytes[index] = (high << 4) | low;
65 }
66 if bytes == [0; 32] {
67 return Err(IntegrityJobError::InvalidJobId);
68 }
69
70 Ok(Self(bytes))
71 }
72
73 #[must_use]
75 pub const fn to_bytes(self) -> [u8; 32] {
76 self.0
77 }
78
79 #[must_use]
81 pub fn to_hex(self) -> String {
82 encode_hex_lower(&self.0)
83 }
84
85 pub(in crate::db) fn validate(self) -> Result<(), IntegrityJobError> {
87 if self.0 == [0; 32] {
88 return Err(IntegrityJobError::InvalidJobId);
89 }
90 Ok(())
91 }
92}
93
94#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
97pub struct IntegrityJobOwner(String);
98
99impl IntegrityJobOwner {
100 pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
107 let value = value.into();
108 if value.is_empty() || value.len() > MAX_INTEGRITY_OWNER_BYTES {
109 return Err(IntegrityJobError::InvalidOwner);
110 }
111 Ok(Self(value))
112 }
113
114 #[must_use]
116 pub const fn as_str(&self) -> &str {
117 self.0.as_str()
118 }
119
120 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
122 if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_OWNER_BYTES {
123 return Err(IntegrityJobError::InvalidOwner);
124 }
125 Ok(())
126 }
127}
128
129#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
132pub struct IntegritySubmissionKey(String);
133
134impl IntegritySubmissionKey {
135 pub fn new(value: impl Into<String>) -> Result<Self, IntegrityJobError> {
142 let value = value.into();
143 if value.is_empty() || value.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
144 return Err(IntegrityJobError::InvalidSubmissionKey);
145 }
146 Ok(Self(value))
147 }
148
149 #[must_use]
151 pub const fn as_str(&self) -> &str {
152 self.0.as_str()
153 }
154
155 pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
157 if self.0.is_empty() || self.0.len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES {
158 return Err(IntegrityJobError::InvalidSubmissionKey);
159 }
160 Ok(())
161 }
162}
163
164#[derive(Clone, Debug, Eq, PartialEq)]
166pub(in crate::db) enum IntegrityCheckpoint {
167 QuickMetadata,
169 Rows(PhysicalUnitCheckpoint),
171 Index {
173 ordinal: u32,
174 checkpoint: PhysicalUnitCheckpoint,
175 },
176 ReverseRelation {
178 ordinal: u32,
179 checkpoint: PhysicalUnitCheckpoint,
180 },
181 Journal {
183 store_ordinal: u32,
184 checkpoint: JournalInspectionCheckpoint,
185 },
186 FinalProof,
188}
189
190impl IntegrityCheckpoint {
191 #[must_use]
193 pub(in crate::db) const fn phase(&self) -> IntegrityPhase {
194 match self {
195 Self::QuickMetadata => IntegrityPhase::QuickMetadata,
196 Self::Rows(_) => IntegrityPhase::Rows,
197 Self::Index { .. } => IntegrityPhase::IndexEntries,
198 Self::ReverseRelation { .. } => IntegrityPhase::ReverseRelations,
199 Self::Journal { .. } => IntegrityPhase::JournalTails,
200 Self::FinalProof => IntegrityPhase::FinalProofVectorCheck,
201 }
202 }
203}
204
205#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
208pub enum IntegrityPendingTerminal {
209 Expired,
211 Aborted,
213}
214
215#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
218pub enum IntegrityTerminalOutcome {
219 DeepCompleteClean,
221 DeepCompleteWithFindings,
223 Invalidated,
225 Uninspectable(IntegrityAuthorityDiagnostic),
227 ResourceLimited(IntegrityResourceDiagnostic),
229 Expired,
231 Aborted,
233}
234
235impl IntegrityTerminalOutcome {
236 pub(in crate::db::integrity) fn from_internal(error: &InternalError) -> Self {
240 if matches!(
241 error.diagnostic().detail(),
242 Some(DiagnosticDetail::RuntimeBoundary {
243 boundary: RuntimeBoundaryCode::ExecutionBudgetExceeded
244 | RuntimeBoundaryCode::PageUnitTooLarge,
245 })
246 ) {
247 return Self::ResourceLimited(IntegrityResourceDiagnostic {
248 diagnostic_code: error.diagnostic_code().error_code().raw(),
249 });
250 }
251
252 Self::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(error))
253 }
254}
255
256#[derive(Clone, Debug, Eq, PartialEq)]
258pub(in crate::db) enum IntegrityJobState {
259 InProgress,
261 TerminalPending(IntegrityPendingTerminal),
263 Terminal {
265 outcome: IntegrityTerminalOutcome,
266 receipt_acknowledged: bool,
267 },
268}
269
270#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
273pub enum DeepIntegrityPageStatus {
274 InProgress,
276 Terminal(IntegrityTerminalOutcome),
278}
279
280#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
283pub struct DeepIntegrityPage {
284 pub(super) job_id: IntegrityJobId,
285 pub(super) page_sequence: u64,
286 pub(super) phase: IntegrityPhase,
287 pub(super) status: DeepIntegrityPageStatus,
288 pub(super) pages_completed: u64,
289 pub(super) findings_seen: u64,
290 pub(super) findings: Vec<IntegrityFinding>,
291 pub(super) blocked_verifier_families: Vec<IntegrityVerifierFamily>,
292}
293
294impl DeepIntegrityPage {
295 #[must_use]
297 pub const fn job_id(&self) -> IntegrityJobId {
298 self.job_id
299 }
300
301 #[must_use]
303 pub const fn page_sequence(&self) -> u64 {
304 self.page_sequence
305 }
306
307 #[must_use]
309 pub const fn phase(&self) -> IntegrityPhase {
310 self.phase
311 }
312
313 #[must_use]
315 pub const fn status(&self) -> &DeepIntegrityPageStatus {
316 &self.status
317 }
318
319 #[must_use]
321 pub const fn pages_completed(&self) -> u64 {
322 self.pages_completed
323 }
324
325 #[must_use]
327 pub const fn findings_seen(&self) -> u64 {
328 self.findings_seen
329 }
330
331 #[must_use]
333 pub const fn findings(&self) -> &[IntegrityFinding] {
334 self.findings.as_slice()
335 }
336
337 #[must_use]
339 pub const fn blocked_verifier_families(&self) -> &[IntegrityVerifierFamily] {
340 self.blocked_verifier_families.as_slice()
341 }
342}
343
344#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
347pub enum IntegrityAbortStatus {
348 TerminationPending(IntegrityPendingTerminal),
350 Terminal(IntegrityTerminalOutcome),
352}
353
354#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
357pub struct IntegrityAbortReceipt {
358 pub(super) job_id: IntegrityJobId,
359 pub(super) page_sequence: u64,
360 pub(super) status: IntegrityAbortStatus,
361}
362
363impl IntegrityAbortReceipt {
364 #[must_use]
366 pub const fn job_id(&self) -> IntegrityJobId {
367 self.job_id
368 }
369
370 #[must_use]
372 pub const fn page_sequence(&self) -> u64 {
373 self.page_sequence
374 }
375
376 #[must_use]
378 pub const fn status(&self) -> &IntegrityAbortStatus {
379 &self.status
380 }
381}
382
383#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
386pub enum IntegrityJobReceipt {
387 Page(DeepIntegrityPage),
389 Abort(IntegrityAbortReceipt),
391}
392
393impl IntegrityJobReceipt {
394 #[must_use]
396 pub const fn job_id(&self) -> IntegrityJobId {
397 match self {
398 Self::Page(page) => page.job_id,
399 Self::Abort(receipt) => receipt.job_id,
400 }
401 }
402
403 #[must_use]
405 pub const fn page_sequence(&self) -> u64 {
406 match self {
407 Self::Page(page) => page.page_sequence,
408 Self::Abort(receipt) => receipt.page_sequence,
409 }
410 }
411}
412
413#[derive(Clone, Copy, Debug, Eq, PartialEq)]
415pub(in crate::db) enum IntegrityReceiptReplayKey {
416 Start,
418 Continue { acknowledged_sequence: u64 },
420}
421
422#[derive(Clone, Debug, Eq, PartialEq)]
424pub(in crate::db) struct IntegrityReceiptEnvelope {
425 pub(super) replay_key: IntegrityReceiptReplayKey,
426 pub(super) receipt: IntegrityJobReceipt,
427}
428
429#[derive(Clone, Debug, Eq, PartialEq)]
431pub(in crate::db) struct IntegrityJob {
432 pub(super) id: IntegrityJobId,
433 pub(super) database_incarnation_id: DatabaseIncarnationId,
434 pub(super) owner: IntegrityJobOwner,
435 pub(super) submission_key: IntegritySubmissionKey,
436 pub(super) entity: IntegrityEntityIdentity,
437 pub(super) accepted_schema_version: u32,
438 pub(super) accepted_schema_fingerprint: [u8; 16],
439 pub(super) inspection_plan_fingerprint: [u8; 32],
440 pub(super) checkpoint: IntegrityCheckpoint,
441 pub(super) captured_proof_vector: IntegrityProofVector,
442 pub(super) state: IntegrityJobState,
443 pub(super) lease_deadline_nanos: u64,
444 pub(super) findings_seen: u64,
445 pub(super) pages_completed: u64,
446 pub(super) blocked_verifier_families: Vec<IntegrityVerifierFamily>,
447 pub(super) last_receipt: IntegrityReceiptEnvelope,
448}
449
450impl IntegrityJob {
451 pub(super) fn validate(&self) -> Result<(), IntegrityJobError> {
453 if self.id != self.last_receipt.receipt.job_id()
454 || self.database_incarnation_id != self.captured_proof_vector.database_incarnation_id()
455 || self.accepted_schema_version != self.captured_proof_vector.accepted_schema_version()
456 || self.accepted_schema_fingerprint
457 != self.captured_proof_vector.accepted_schema_fingerprint()
458 || self.inspection_plan_fingerprint
459 != self.captured_proof_vector.inspection_plan_fingerprint()
460 || self.entity.entity_path().is_empty()
461 || self.entity.entity_path().len() > MAX_INTEGRITY_PATH_BYTES
462 || self.entity.store_path().is_empty()
463 || self.entity.store_path().len() > MAX_INTEGRITY_PATH_BYTES
464 || self.entity.entity_tag() == 0
465 || self.owner.as_str().is_empty()
466 || self.owner.as_str().len() > MAX_INTEGRITY_OWNER_BYTES
467 || self.submission_key.as_str().is_empty()
468 || self.submission_key.as_str().len() > MAX_INTEGRITY_SUBMISSION_KEY_BYTES
469 || self.accepted_schema_version == 0
470 || self.lease_deadline_nanos == 0
471 || matches!(
472 self.state,
473 IntegrityJobState::InProgress | IntegrityJobState::TerminalPending(_)
474 ) && self.pages_completed > MAX_INTEGRITY_IN_PROGRESS_PAGES
475 || !strictly_sorted_unique(&self.blocked_verifier_families)
476 || self.captured_proof_vector.validate().is_err()
477 || !self.checkpoint_is_well_formed()
478 {
479 return Err(IntegrityJobError::CorruptProgressRecord);
480 }
481
482 let receipt_matches_state = match (&self.state, &self.last_receipt.receipt) {
483 (
484 IntegrityJobState::InProgress | IntegrityJobState::TerminalPending(_),
485 IntegrityJobReceipt::Page(page),
486 ) => {
487 page.status == DeepIntegrityPageStatus::InProgress
488 && page.phase == self.checkpoint.phase()
489 && self.page_matches_counters(page)
490 }
491 (IntegrityJobState::Terminal { outcome, .. }, IntegrityJobReceipt::Page(page)) => {
492 page.status == DeepIntegrityPageStatus::Terminal(outcome.clone())
493 && page.phase == self.checkpoint.phase()
494 && !matches!(
495 outcome,
496 IntegrityTerminalOutcome::Expired | IntegrityTerminalOutcome::Aborted
497 )
498 && self.page_matches_counters(page)
499 }
500 (IntegrityJobState::Terminal { outcome, .. }, IntegrityJobReceipt::Abort(receipt)) => {
501 receipt.status == IntegrityAbortStatus::Terminal(outcome.clone())
502 && matches!(
503 outcome,
504 IntegrityTerminalOutcome::Expired | IntegrityTerminalOutcome::Aborted
505 )
506 }
507 _ => false,
508 };
509 if !receipt_matches_state
510 || self.last_receipt.receipt.page_sequence() != self.pages_completed
511 || !self.replay_key_matches_receipt()
512 || !self.terminal_outcome_matches_counts()
513 {
514 return Err(IntegrityJobError::CorruptProgressRecord);
515 }
516
517 Ok(())
518 }
519
520 fn page_matches_counters(&self, page: &DeepIntegrityPage) -> bool {
521 page.pages_completed == self.pages_completed
522 && page.findings_seen == self.findings_seen
523 && page.findings.len() <= MAX_INTEGRITY_RECEIPT_FINDINGS
524 && u64::try_from(page.findings.len()).is_ok_and(|count| count <= self.findings_seen)
525 && page.findings.iter().all(|finding| {
526 finding.value_path().is_none_or(|path| {
527 finding.kind() == IntegrityFindingKind::ConstraintViolation
528 && finding.constraint_id().is_some()
529 && finding.constraint_name().is_some()
530 && constraint_value_path_is_well_formed(path)
531 })
532 })
533 && page.blocked_verifier_families == self.blocked_verifier_families
534 }
535
536 fn replay_key_matches_receipt(&self) -> bool {
537 match self.last_receipt.replay_key {
538 IntegrityReceiptReplayKey::Start => {
539 self.pages_completed == 0
540 && self.last_receipt.receipt.page_sequence() == 0
541 && matches!(
542 &self.last_receipt.receipt,
543 IntegrityJobReceipt::Page(DeepIntegrityPage {
544 status: DeepIntegrityPageStatus::InProgress,
545 ..
546 })
547 )
548 }
549 IntegrityReceiptReplayKey::Continue {
550 acknowledged_sequence,
551 } => acknowledged_sequence
552 .checked_add(1)
553 .is_some_and(|sequence| sequence == self.last_receipt.receipt.page_sequence()),
554 }
555 }
556
557 const fn terminal_outcome_matches_counts(&self) -> bool {
558 match &self.state {
559 IntegrityJobState::Terminal {
560 outcome: IntegrityTerminalOutcome::DeepCompleteClean,
561 ..
562 } => self.findings_seen == 0 && self.blocked_verifier_families.is_empty(),
563 IntegrityJobState::Terminal {
564 outcome: IntegrityTerminalOutcome::DeepCompleteWithFindings,
565 ..
566 } => self.findings_seen > 0 || !self.blocked_verifier_families.is_empty(),
567 _ => true,
568 }
569 }
570
571 fn checkpoint_is_well_formed(&self) -> bool {
572 match &self.checkpoint {
573 IntegrityCheckpoint::Rows(checkpoint) => row_checkpoint_is_well_formed(checkpoint),
574 IntegrityCheckpoint::Index {
575 ordinal,
576 checkpoint,
577 } => {
578 usize::try_from(*ordinal).is_ok_and(|ordinal| {
579 ordinal < self.captured_proof_vector.index_generation_count()
580 }) && index_checkpoint_is_well_formed(checkpoint)
581 }
582 IntegrityCheckpoint::ReverseRelation {
583 ordinal,
584 checkpoint,
585 } => {
586 usize::try_from(*ordinal).is_ok_and(|ordinal| {
587 ordinal < self.captured_proof_vector.relation_generation_count()
588 }) && reverse_checkpoint_is_well_formed(checkpoint)
589 }
590 IntegrityCheckpoint::Journal {
591 store_ordinal,
592 checkpoint,
593 } => usize::try_from(*store_ordinal)
594 .ok()
595 .and_then(|ordinal| self.captured_proof_vector.stores().get(ordinal))
596 .is_some_and(|proof| {
597 let (fold_sequence, next_append_sequence) = proof.journal_interval();
598 journal_checkpoint_is_well_formed(
599 checkpoint,
600 fold_sequence,
601 next_append_sequence,
602 )
603 }),
604 IntegrityCheckpoint::QuickMetadata | IntegrityCheckpoint::FinalProof => true,
605 }
606 }
607}
608
609fn constraint_value_path_is_well_formed(path: &ConstraintValuePath) -> bool {
610 let Some((first, remaining)) = path.components().split_first() else {
611 return false;
612 };
613 path.components().len() <= MAX_ACCEPTED_TARGET_PATH_COMPONENTS
614 && matches!(
615 first,
616 ConstraintValuePathComponent::RootField { field_id } if *field_id != 0
617 )
618 && remaining.iter().all(|component| match component {
619 ConstraintValuePathComponent::RootField { .. } => false,
620 ConstraintValuePathComponent::RecordMember {
621 composite_type_id,
622 member_id,
623 } => *composite_type_id != 0 && *member_id != 0,
624 ConstraintValuePathComponent::TupleElement {
625 composite_type_id, ..
626 }
627 | ConstraintValuePathComponent::Newtype { composite_type_id } => {
628 *composite_type_id != 0
629 }
630 ConstraintValuePathComponent::EnumVariant {
631 enum_type_id,
632 variant_id,
633 } => *enum_type_id != 0 && *variant_id != 0,
634 ConstraintValuePathComponent::ListElement { .. }
635 | ConstraintValuePathComponent::SetElement { .. }
636 | ConstraintValuePathComponent::MapEntryKey { .. }
637 | ConstraintValuePathComponent::MapEntryValue { .. } => true,
638 })
639}
640
641fn row_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
642 checkpoint.raw_data_key().is_ok()
643 && !matches!(
644 checkpoint,
645 PhysicalUnitCheckpoint::Within {
646 verifier_family: IntegrityVerifierFamily::IndexEntry
647 | IntegrityVerifierFamily::UniqueIndex
648 | IntegrityVerifierFamily::ReverseRelationEntry
649 | IntegrityVerifierFamily::JournalEnvelope
650 | IntegrityVerifierFamily::JournalBatchIdentity,
651 ..
652 }
653 )
654}
655
656fn index_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
657 checkpoint.raw_index_key().is_ok()
658 && !matches!(
659 checkpoint,
660 PhysicalUnitCheckpoint::Within {
661 verifier_family: IntegrityVerifierFamily::DataKey
662 | IntegrityVerifierFamily::RowEnvelope
663 | IntegrityVerifierFamily::FieldValue
664 | IntegrityVerifierFamily::PrimaryKey
665 | IntegrityVerifierFamily::IdentityState
666 | IntegrityVerifierFamily::ValidatedConstraints
667 | IntegrityVerifierFamily::ForwardIndex
668 | IntegrityVerifierFamily::Relation
669 | IntegrityVerifierFamily::ReverseRelationEntry
670 | IntegrityVerifierFamily::JournalEnvelope
671 | IntegrityVerifierFamily::JournalBatchIdentity,
672 ..
673 }
674 )
675}
676
677fn reverse_checkpoint_is_well_formed(checkpoint: &PhysicalUnitCheckpoint) -> bool {
678 checkpoint.raw_index_key().is_ok()
679 && !matches!(
680 checkpoint,
681 PhysicalUnitCheckpoint::Within {
682 verifier_family: IntegrityVerifierFamily::DataKey
683 | IntegrityVerifierFamily::RowEnvelope
684 | IntegrityVerifierFamily::FieldValue
685 | IntegrityVerifierFamily::PrimaryKey
686 | IntegrityVerifierFamily::IdentityState
687 | IntegrityVerifierFamily::ValidatedConstraints
688 | IntegrityVerifierFamily::ForwardIndex
689 | IntegrityVerifierFamily::Relation
690 | IntegrityVerifierFamily::IndexEntry
691 | IntegrityVerifierFamily::UniqueIndex
692 | IntegrityVerifierFamily::JournalEnvelope
693 | IntegrityVerifierFamily::JournalBatchIdentity,
694 ..
695 }
696 )
697}
698
699const fn journal_checkpoint_is_well_formed(
700 checkpoint: &JournalInspectionCheckpoint,
701 fold_sequence: u64,
702 next_append_sequence: u64,
703) -> bool {
704 match checkpoint {
705 JournalInspectionCheckpoint::BeforeFirst => true,
706 JournalInspectionCheckpoint::BeforeBatch { sequence } => {
707 *sequence > fold_sequence && *sequence < next_append_sequence
708 }
709 JournalInspectionCheckpoint::CheckingBatchIdentity {
710 sequence,
711 next_prior_sequence,
712 ..
713 } => {
714 *sequence > fold_sequence
715 && *sequence < next_append_sequence
716 && *next_prior_sequence > fold_sequence
717 && *next_prior_sequence < *sequence
718 }
719 JournalInspectionCheckpoint::AfterBatch { sequence } => {
720 *sequence >= fold_sequence && *sequence < next_append_sequence
721 }
722 }
723}
724
725fn strictly_sorted_unique(values: &[IntegrityVerifierFamily]) -> bool {
726 values.windows(2).all(|pair| pair[0] < pair[1])
727}
728
729#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
732pub enum IntegrityJobError {
733 CapacityExceeded,
735
736 CorruptProgressHeader,
738
739 CorruptProgressRecord,
741
742 CounterExhausted,
744
745 EntityIdentityMismatch,
747
748 IncompatibleProgressFormat,
750
751 Internal,
753
754 InvalidEntityIdentity,
756
757 InvalidJobId,
759
760 InvalidOwner,
762
763 InvalidSubmissionKey,
765
766 JobIncarnationMismatch,
768
769 JobNotFound,
771
772 JobOwnerMismatch,
774
775 StaleAcknowledgement,
777
778 StartInvalidated,
780
781 SubmissionAlreadyAdvanced,
783
784 SubmissionConflict,
786}
787
788#[derive(Debug)]
791pub enum IntegrityDeepError {
792 Internal(crate::error::InternalError),
794
795 Job(IntegrityJobError),
797
798 Uninspectable(IntegrityAuthorityDiagnostic),
800}
801
802impl IntegrityDeepError {
803 pub(in crate::db) fn from_plan_load(error: InternalError) -> Self {
805 match IntegrityTerminalOutcome::from_internal(&error) {
806 IntegrityTerminalOutcome::Uninspectable(diagnostic) => Self::Uninspectable(diagnostic),
807 _ => Self::Internal(error),
810 }
811 }
812}
813
814impl From<IntegrityJobError> for IntegrityDeepError {
815 fn from(error: IntegrityJobError) -> Self {
816 Self::Job(error)
817 }
818}
819
820impl From<crate::error::InternalError> for IntegrityDeepError {
821 fn from(error: crate::error::InternalError) -> Self {
822 Self::Internal(error)
823 }
824}
825
826#[cfg(test)]
827mod tests {
828 use super::*;
829 use crate::error::{ErrorClass, ErrorOrigin};
830 use icydb_diagnostic_code::DiagnosticExecutionBudgetResource as Resource;
831
832 #[test]
833 fn inspection_resource_failures_keep_their_typed_terminal_and_wire_code() {
834 use crate::db::{
835 resource::budget::MaintenanceConstructionBudget,
836 resource::construction::ConstructionBudget,
837 };
838 let work = MaintenanceConstructionBudget::with_limit_for_tests(Resource::TemporaryBytes, 0);
839 for error in [
840 work.charge(Resource::TemporaryBytes, 1).unwrap_err(),
841 InternalError::relation_budget_exceeded(Resource::NestedValueSteps, 10, 11),
842 InternalError::relation_budget_exceeded(Resource::TemporaryBytes, 10, 11),
843 InternalError::page_unit_too_large(Resource::TemporaryBytes, 10, 11),
844 ] {
845 let expected = error.diagnostic();
846 let outcome = IntegrityTerminalOutcome::from_internal(&error);
847 let IntegrityTerminalOutcome::ResourceLimited(diagnostic) = &outcome else {
848 panic!("resource exhaustion must not become an authority failure");
849 };
850 assert_eq!(
851 diagnostic.diagnostic_code(),
852 error.diagnostic_code().error_code().raw(),
853 );
854 let bytes = icydb_schema::wire::encode_one(&outcome).expect("terminal should encode");
855 let decoded: IntegrityTerminalOutcome =
856 icydb_schema::wire::decode_one(&bytes).expect("terminal should decode");
857 assert_eq!(decoded, outcome);
858 let IntegrityDeepError::Internal(error) = IntegrityDeepError::from_plan_load(error)
859 else {
860 panic!("plan-load exhaustion must retain its operational error");
861 };
862 assert_eq!(error.diagnostic(), expected);
863 }
864 }
865
866 #[test]
867 fn inspection_authority_failures_are_not_classified_as_exhaustion() {
868 for class in [
869 ErrorClass::Corruption,
870 ErrorClass::IncompatiblePersistedFormat,
871 ErrorClass::InvariantViolation,
872 ErrorClass::Unsupported,
873 ErrorClass::NotFound,
874 ErrorClass::Conflict,
875 ErrorClass::Internal,
876 ] {
877 let error = InternalError::classified(class, ErrorOrigin::Store);
878 assert_eq!(
879 IntegrityTerminalOutcome::from_internal(&error),
880 IntegrityTerminalOutcome::Uninspectable(
881 IntegrityAuthorityDiagnostic::from_internal(&error),
882 ),
883 );
884 let expected = IntegrityAuthorityDiagnostic::from_internal(&error);
885 let IntegrityDeepError::Uninspectable(diagnostic) =
886 IntegrityDeepError::from_plan_load(error)
887 else {
888 panic!("invalid authority keeps its established plan-load classification");
889 };
890 assert_eq!(diagnostic, expected);
891 }
892 }
893
894 #[test]
895 fn public_job_inputs_revalidate_after_wire_decode() {
896 let owner_bytes = icydb_schema::wire::encode_one(IntegrityJobOwner(String::new()))
897 .expect("owner should encode");
898 let owner: IntegrityJobOwner =
899 icydb_schema::wire::decode_one(&owner_bytes).expect("owner should decode");
900 assert_eq!(owner.validate(), Err(IntegrityJobError::InvalidOwner));
901
902 let submission_bytes =
903 icydb_schema::wire::encode_one(IntegritySubmissionKey(String::new()))
904 .expect("submission key should encode");
905 let submission: IntegritySubmissionKey = icydb_schema::wire::decode_one(&submission_bytes)
906 .expect("submission key should decode");
907 assert_eq!(
908 submission.validate(),
909 Err(IntegrityJobError::InvalidSubmissionKey),
910 );
911
912 let job_id_bytes =
913 icydb_schema::wire::encode_one(IntegrityJobId([0; 32])).expect("job id should encode");
914 let job_id: IntegrityJobId =
915 icydb_schema::wire::decode_one(&job_id_bytes).expect("job id should decode");
916 assert_eq!(job_id.validate(), Err(IntegrityJobError::InvalidJobId),);
917 }
918
919 #[test]
920 fn persisted_constraint_value_paths_require_current_accepted_id_shape() {
921 let valid = ConstraintValuePath::new(vec![
922 ConstraintValuePathComponent::RootField { field_id: 1 },
923 ConstraintValuePathComponent::RecordMember {
924 composite_type_id: 2,
925 member_id: 3,
926 },
927 ConstraintValuePathComponent::ListElement { index: 0 },
928 ]);
929 assert!(constraint_value_path_is_well_formed(&valid));
930
931 for malformed in [
932 ConstraintValuePath::new(Vec::new()),
933 ConstraintValuePath::new(vec![ConstraintValuePathComponent::RootField {
934 field_id: 0,
935 }]),
936 ConstraintValuePath::new(vec![
937 ConstraintValuePathComponent::RootField { field_id: 1 },
938 ConstraintValuePathComponent::RootField { field_id: 2 },
939 ]),
940 ConstraintValuePath::new(vec![
941 ConstraintValuePathComponent::RootField { field_id: 1 },
942 ConstraintValuePathComponent::Newtype {
943 composite_type_id: 0,
944 },
945 ]),
946 ] {
947 assert!(!constraint_value_path_is_well_formed(&malformed));
948 }
949 }
950
951 #[test]
952 fn public_job_id_hex_round_trip_is_exact_and_fail_closed() {
953 let mut bytes = [0_u8; 32];
954 bytes[0] = 0x01;
955 bytes[31] = 0xfe;
956 let job_id = IntegrityJobId::try_from_bytes(bytes).expect("job id should admit");
957 let encoded = job_id.to_hex();
958
959 assert_eq!(encoded.len(), 64);
960 assert_eq!(IntegrityJobId::try_from_hex(encoded.as_str()), Ok(job_id),);
961 assert_eq!(
962 IntegrityJobId::try_from_hex(encoded.to_uppercase().as_str()),
963 Ok(job_id),
964 );
965 for malformed in [
966 "",
967 "01",
968 "0000000000000000000000000000000000000000000000000000000000000000",
969 "g001000000000000000000000000000000000000000000000000000000000000",
970 ] {
971 assert_eq!(
972 IntegrityJobId::try_from_hex(malformed),
973 Err(IntegrityJobError::InvalidJobId),
974 );
975 }
976 }
977
978 #[test]
979 fn persisted_checkpoint_families_stay_phase_owned() {
980 let journal_in_row = PhysicalUnitCheckpoint::Within {
981 physical_key: vec![1],
982 verifier_family: IntegrityVerifierFamily::JournalEnvelope,
983 ordinal: 0,
984 };
985 let row_in_index = PhysicalUnitCheckpoint::Within {
986 physical_key: vec![1],
987 verifier_family: IntegrityVerifierFamily::FieldValue,
988 ordinal: 0,
989 };
990 let reverse_in_reverse = PhysicalUnitCheckpoint::Within {
991 physical_key: vec![1],
992 verifier_family: IntegrityVerifierFamily::ReverseRelationEntry,
993 ordinal: 0,
994 };
995
996 assert!(!row_checkpoint_is_well_formed(&journal_in_row));
997 assert!(!index_checkpoint_is_well_formed(&row_in_index));
998 assert!(reverse_checkpoint_is_well_formed(&reverse_in_reverse));
999 }
1000
1001 #[test]
1002 fn persisted_journal_checkpoint_cannot_skip_the_captured_tail_interval() {
1003 assert!(journal_checkpoint_is_well_formed(
1004 &JournalInspectionCheckpoint::BeforeFirst,
1005 4,
1006 8,
1007 ));
1008 assert!(journal_checkpoint_is_well_formed(
1009 &JournalInspectionCheckpoint::BeforeBatch { sequence: 7 },
1010 4,
1011 8,
1012 ));
1013 assert!(journal_checkpoint_is_well_formed(
1014 &JournalInspectionCheckpoint::AfterBatch { sequence: 4 },
1015 4,
1016 8,
1017 ));
1018 assert!(!journal_checkpoint_is_well_formed(
1019 &JournalInspectionCheckpoint::BeforeBatch { sequence: 4 },
1020 4,
1021 8,
1022 ));
1023 assert!(!journal_checkpoint_is_well_formed(
1024 &JournalInspectionCheckpoint::AfterBatch { sequence: 8 },
1025 4,
1026 8,
1027 ));
1028 assert!(!journal_checkpoint_is_well_formed(
1029 &JournalInspectionCheckpoint::CheckingBatchIdentity {
1030 sequence: 7,
1031 batch_id: [1; 16],
1032 next_prior_sequence: 4,
1033 },
1034 4,
1035 8,
1036 ));
1037 }
1038}