mod deep;
mod derived;
mod job;
mod progress_store;
mod proof;
mod row;
use crate::{
db::{
commit::{database_control_proof_identity, database_incarnation_id, ensure_recovered},
registry::{
StoreAllocationIdentities, StoreHandle, StoreRuntimeStorageCapabilities,
StoreRuntimeStorageMode,
},
schema::AcceptedInspectionPlan,
},
entity::EntityKind,
error::{ErrorClass, InternalError},
traits::{CanisterKind, Path},
};
use candid::CandidType;
use serde::Deserialize;
use std::{
collections::BTreeMap,
sync::atomic::{AtomicU64, Ordering},
};
#[cfg(all(test, feature = "sql"))]
pub(in crate::db) use deep::run_integrity_retention_page_for_tests;
pub(in crate::db) use deep::{
abort_deep_integrity_job, continue_deep_integrity_job, run_next_integrity_retention_page,
start_deep_integrity_job,
};
#[cfg(all(test, feature = "sql"))]
pub(in crate::db) use deep::{
reset_integrity_retention_cursor_for_tests, run_next_integrity_retention_page_for_tests,
};
#[cfg(test)]
pub(in crate::db) use derived::DerivedIntegrityPage;
pub(in crate::db) use derived::{
DerivedInspectionLimits, execute_index_integrity_page, execute_reverse_integrity_page,
};
pub use job::{
DeepIntegrityPage, DeepIntegrityPageStatus, IntegrityAbortReceipt, IntegrityAbortStatus,
IntegrityDeepError, IntegrityJobError, IntegrityJobId, IntegrityJobOwner, IntegrityJobReceipt,
IntegrityPendingTerminal, IntegritySubmissionKey, IntegrityTerminalOutcome,
};
pub(in crate::db) use job::{
IntegrityCheckpoint, IntegrityJob, IntegrityJobState, IntegrityReceiptEnvelope,
IntegrityReceiptReplayKey, MAX_INTEGRITY_IN_PROGRESS_PAGES,
};
#[cfg(all(test, feature = "sql"))]
pub(in crate::db) use progress_store::{
clear_progress_store_for_tests, corrupt_progress_job_for_tests,
progress_job_encoded_len_for_tests, set_progress_job_lease_deadline_for_tests,
};
pub(in crate::db) use proof::{IntegrityProofVector, capture_integrity_proof_vector};
#[cfg(test)]
pub(in crate::db) use row::RowIntegrityPage;
pub(in crate::db) use row::{
PhysicalUnitCheckpoint, RowInspectionLimits, execute_row_integrity_page,
};
pub(in crate::db) const MAX_INTEGRITY_PATH_BYTES: usize = 4 * 1024;
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum IntegrityCheckRequest {
Quick {
entity: IntegrityEntityIdentity,
},
DeepStart {
entity: IntegrityEntityIdentity,
submission_key: IntegritySubmissionKey,
},
DeepContinue {
job_id: IntegrityJobId,
acknowledged_sequence: u64,
},
DeepAbort {
job_id: IntegrityJobId,
},
}
impl IntegrityCheckRequest {
#[must_use]
pub fn quick<E: EntityKind>() -> Self {
Self::Quick {
entity: IntegrityEntityIdentity::for_entity::<E>(),
}
}
#[must_use]
pub fn deep_start<E: EntityKind>(submission_key: IntegritySubmissionKey) -> Self {
Self::DeepStart {
entity: IntegrityEntityIdentity::for_entity::<E>(),
submission_key,
}
}
#[must_use]
pub const fn deep_continue(job_id: IntegrityJobId, acknowledged_sequence: u64) -> Self {
Self::DeepContinue {
job_id,
acknowledged_sequence,
}
}
#[must_use]
pub const fn deep_abort(job_id: IntegrityJobId) -> Self {
Self::DeepAbort { job_id }
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum IntegrityCheckResult {
Quick(QuickIntegrityResult),
Deep(IntegrityJobReceipt),
}
fn validate_quick_integrity_control<C: CanisterKind>(
db: &crate::db::Db<C>,
plan: &AcceptedInspectionPlan,
) -> Result<Vec<IntegrityFinding>, InternalError> {
let identity = plan.identity();
let source_store = db.store_handle(identity.store_path())?;
let relations = plan.relation_inspection();
let mut participating_stores =
BTreeMap::from([(identity.store_path().to_string(), source_store)]);
for relation in relations {
participating_stores
.entry(relation.target_store_path().to_string())
.or_insert_with(|| relation.target_store());
}
let _database_control = database_control_proof_identity()?;
proof::validate_integrity_allocation_registry()?;
let mut findings = Vec::new();
for (store_path, store) in &participating_stores {
if let Some(finding) = validate_quick_store_control(plan, store_path, *store)? {
findings.push(finding);
}
}
for ordinal in 0..plan.index_inspection().len() {
let _domain = plan
.index_inspection()
.domain(ordinal, identity.entity_tag())?;
}
Ok(findings)
}
fn validate_quick_store_control(
plan: &AcceptedInspectionPlan,
store_path: &str,
store: StoreHandle,
) -> Result<Option<IntegrityFinding>, InternalError> {
let capabilities = store.storage_capabilities();
let allocations = store.allocation_identities();
match capabilities.storage_mode() {
StoreRuntimeStorageMode::Heap => {
if capabilities != StoreRuntimeStorageCapabilities::heap()
|| allocations != StoreAllocationIdentities::absent()
|| store.journal_tail_store().is_some()
{
return Err(InternalError::store_invariant());
}
Ok(None)
}
StoreRuntimeStorageMode::Journaled => {
if capabilities != StoreRuntimeStorageCapabilities::journaled()
|| !allocations.matches_storage_capabilities(capabilities)
{
return Err(InternalError::store_invariant());
}
let journal = store
.journal_tail_store()
.ok_or_else(InternalError::store_invariant)?
.with_borrow(crate::db::journal::JournalTailStore::proof_identity)?;
if !journal.is_well_formed() {
return Ok(Some(quick_journal_control_finding(plan, store_path)));
}
Ok(None)
}
}
}
fn quick_journal_control_finding(
plan: &AcceptedInspectionPlan,
store_path: &str,
) -> IntegrityFinding {
let error = InternalError::store_corruption();
IntegrityFinding {
diagnostic_code: error.diagnostic_code().error_code().raw(),
class: IntegrityFindingClass::Corruption,
severity: IntegritySeverity::Error,
kind: IntegrityFindingKind::JournalControlMismatch,
entity: IntegrityEntityIdentity::from_plan(plan),
store_path: store_path.to_string(),
phase: IntegrityPhase::QuickMetadata,
verifier_family: IntegrityVerifierFamily::JournalEnvelope,
physical_key: Vec::new(),
primary_key: None,
field_paths: Vec::new(),
constraint_id: None,
constraint_name: None,
schema_index_id: None,
relation_id: None,
expected: Some("well-formed-journal-control".to_string()),
observed: Some("inconsistent-journal-control".to_string()),
}
}
fn relation_field_paths(plan: &AcceptedInspectionPlan, relation_id: u32) -> Vec<String> {
let snapshot = plan.snapshot().persisted_snapshot();
let Some(relation) = snapshot
.relations()
.iter()
.find(|relation| relation.id().get() == relation_id)
else {
return Vec::new();
};
relation
.local_field_ids()
.iter()
.filter_map(|field_id| {
snapshot
.fields()
.iter()
.find(|field| field.id() == *field_id)
.map(|field| field.name().to_string())
})
.collect()
}
const MAX_QUICK_RETURNED_FINDINGS: usize = 64;
#[cfg(target_arch = "wasm32")]
const DATABASE_INCARNATION_DOMAIN: &[u8] = b"icydb.database-incarnation.v1";
static DATABASE_INCARNATION_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
pub struct DatabaseIncarnationId([u8; 16]);
impl DatabaseIncarnationId {
pub(crate) fn try_from_bytes(bytes: [u8; 16]) -> Result<Self, InternalError> {
if bytes == [0; 16] {
return Err(InternalError::database_incarnation_invalid());
}
Ok(Self(bytes))
}
#[must_use]
pub const fn to_bytes(self) -> [u8; 16] {
self.0
}
fn generate() -> Result<Self, InternalError> {
let sequence = DATABASE_INCARNATION_SEQUENCE
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
current.checked_add(1)
})
.map_err(|_| InternalError::database_incarnation_generation_failed())?
.checked_add(1)
.ok_or_else(InternalError::database_incarnation_generation_failed)?;
#[cfg(not(target_arch = "wasm32"))]
let bytes = {
let mut bytes = [0_u8; 16];
getrandom::fill(&mut bytes)
.map_err(|_| InternalError::database_incarnation_generation_failed())?;
bytes
};
#[cfg(target_arch = "wasm32")]
let bytes = {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(DATABASE_INCARNATION_DOMAIN);
hasher.update(ic_cdk::api::canister_self().as_slice());
hasher.update(ic_cdk::api::time().to_be_bytes());
hasher.update(sequence.to_be_bytes());
let digest = hasher.finalize();
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&digest[..16]);
bytes
};
let _ = sequence;
Self::try_from_bytes(bytes)
}
#[cfg(test)]
pub(crate) const fn for_tests(fill: u8) -> Self {
let mut bytes = [fill; 16];
if fill == 0 {
bytes[15] = 1;
}
Self(bytes)
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct IntegrityEntityIdentity {
entity_tag: u64,
entity_path: String,
store_path: String,
}
impl IntegrityEntityIdentity {
fn from_plan(plan: &AcceptedInspectionPlan) -> Self {
Self::from_accepted_identity(plan.identity())
}
pub(in crate::db) fn from_accepted_identity(
identity: crate::db::schema::AcceptedCatalogIdentity,
) -> Self {
Self {
entity_tag: identity.entity_tag().value(),
entity_path: identity.entity_path().to_string(),
store_path: identity.store_path().to_string(),
}
}
#[cfg(feature = "sql")]
pub(in crate::db) fn from_runtime_selector(
entity_tag: u64,
entity_path: &str,
store_path: &str,
) -> Self {
Self {
entity_tag,
entity_path: entity_path.to_string(),
store_path: store_path.to_string(),
}
}
#[must_use]
pub fn for_entity<E: EntityKind>() -> Self {
Self {
entity_tag: E::ENTITY_TAG.value(),
entity_path: <E as Path>::PATH.to_string(),
store_path: <E::Store as Path>::PATH.to_string(),
}
}
pub(in crate::db) const fn validate(&self) -> Result<(), IntegrityJobError> {
if self.entity_tag == 0
|| self.entity_path.is_empty()
|| self.entity_path.len() > MAX_INTEGRITY_PATH_BYTES
|| self.store_path.is_empty()
|| self.store_path.len() > MAX_INTEGRITY_PATH_BYTES
{
return Err(IntegrityJobError::InvalidEntityIdentity);
}
Ok(())
}
#[must_use]
pub const fn entity_tag(&self) -> u64 {
self.entity_tag
}
#[must_use]
pub const fn entity_path(&self) -> &str {
self.entity_path.as_str()
}
#[must_use]
pub const fn store_path(&self) -> &str {
self.store_path.as_str()
}
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum IntegrityAuthorityClass {
Corruption,
IncompatiblePersistedFormat,
InvariantViolation,
Unsupported,
Internal,
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum IntegrityFindingClass {
Corruption,
IncompatiblePersistedFormat,
ResourceLimited,
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum IntegrityFindingKind {
MalformedDataKey,
MalformedRow,
OversizedRow,
InvalidFieldValue,
PrimaryKeyMismatch,
ConstraintViolation,
MissingIndexEntry,
DivergentIndexEntry,
MalformedIndexEntry,
OrphanIndexEntry,
DuplicateUniqueIndexKey,
MissingRelationTarget,
MissingReverseRelationEntry,
DivergentReverseRelationEntry,
MalformedReverseRelationEntry,
OrphanReverseRelationEntry,
MalformedJournalBatch,
JournalSequenceGap,
DuplicateJournalBatchIdentity,
JournalControlMismatch,
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum IntegrityPhase {
QuickMetadata,
Rows,
IndexEntries,
ReverseRelations,
JournalTails,
FinalProofVectorCheck,
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum IntegrityVerifierFamily {
DataKey,
RowEnvelope,
FieldValue,
PrimaryKey,
ValidatedConstraints,
ForwardIndex,
IndexEntry,
UniqueIndex,
Relation,
ReverseRelationEntry,
JournalEnvelope,
JournalBatchIdentity,
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum IntegritySeverity {
Error,
Advisory,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct IntegrityFinding {
diagnostic_code: u16,
class: IntegrityFindingClass,
severity: IntegritySeverity,
kind: IntegrityFindingKind,
entity: IntegrityEntityIdentity,
store_path: String,
phase: IntegrityPhase,
verifier_family: IntegrityVerifierFamily,
physical_key: Vec<u8>,
primary_key: Option<Vec<u8>>,
field_paths: Vec<String>,
constraint_id: Option<u32>,
constraint_name: Option<String>,
schema_index_id: Option<u32>,
relation_id: Option<u32>,
expected: Option<String>,
observed: Option<String>,
}
impl IntegrityFinding {
#[must_use]
pub const fn diagnostic_code(&self) -> u16 {
self.diagnostic_code
}
#[must_use]
pub const fn class(&self) -> IntegrityFindingClass {
self.class
}
#[must_use]
pub const fn severity(&self) -> IntegritySeverity {
self.severity
}
#[must_use]
pub const fn kind(&self) -> IntegrityFindingKind {
self.kind
}
#[must_use]
pub const fn entity(&self) -> &IntegrityEntityIdentity {
&self.entity
}
#[must_use]
pub const fn store_path(&self) -> &str {
self.store_path.as_str()
}
#[must_use]
pub const fn phase(&self) -> IntegrityPhase {
self.phase
}
#[must_use]
pub const fn verifier_family(&self) -> IntegrityVerifierFamily {
self.verifier_family
}
#[must_use]
pub const fn physical_key(&self) -> &[u8] {
self.physical_key.as_slice()
}
#[must_use]
pub fn primary_key(&self) -> Option<&[u8]> {
self.primary_key.as_deref()
}
#[must_use]
pub const fn field_paths(&self) -> &[String] {
self.field_paths.as_slice()
}
#[must_use]
pub const fn constraint_id(&self) -> Option<u32> {
self.constraint_id
}
#[must_use]
pub fn constraint_name(&self) -> Option<&str> {
self.constraint_name.as_deref()
}
#[must_use]
pub const fn schema_index_id(&self) -> Option<u32> {
self.schema_index_id
}
#[must_use]
pub const fn relation_id(&self) -> Option<u32> {
self.relation_id
}
#[must_use]
pub fn expected(&self) -> Option<&str> {
self.expected.as_deref()
}
#[must_use]
pub fn observed(&self) -> Option<&str> {
self.observed.as_deref()
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct IntegrityAuthorityDiagnostic {
diagnostic_code: u16,
class: IntegrityAuthorityClass,
}
impl IntegrityAuthorityDiagnostic {
fn from_internal(error: &InternalError) -> Self {
let class = match error.class {
ErrorClass::Corruption => IntegrityAuthorityClass::Corruption,
ErrorClass::IncompatiblePersistedFormat => {
IntegrityAuthorityClass::IncompatiblePersistedFormat
}
ErrorClass::InvariantViolation => IntegrityAuthorityClass::InvariantViolation,
ErrorClass::Unsupported | ErrorClass::NotFound | ErrorClass::Conflict => {
IntegrityAuthorityClass::Unsupported
}
ErrorClass::Internal => IntegrityAuthorityClass::Internal,
};
Self {
diagnostic_code: error.diagnostic_code().error_code().raw(),
class,
}
}
#[must_use]
pub const fn diagnostic_code(&self) -> u16 {
self.diagnostic_code
}
#[must_use]
pub const fn class(&self) -> IntegrityAuthorityClass {
self.class
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct IntegrityResourceDiagnostic {
diagnostic_code: u16,
}
impl IntegrityResourceDiagnostic {
#[must_use]
pub const fn diagnostic_code(&self) -> u16 {
self.diagnostic_code
}
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub enum IntegrityPhysicalContainer {
Rows,
IndexEntries,
ReverseRelations,
JournalTails,
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct StorageTraversalCorruption {
diagnostic_code: u16,
store_path: String,
container: IntegrityPhysicalContainer,
phase: IntegrityPhase,
last_verified_physical_key: Option<Vec<u8>>,
}
impl StorageTraversalCorruption {
#[must_use]
pub const fn diagnostic_code(&self) -> u16 {
self.diagnostic_code
}
#[must_use]
pub const fn store_path(&self) -> &str {
self.store_path.as_str()
}
#[must_use]
pub const fn container(&self) -> IntegrityPhysicalContainer {
self.container
}
#[must_use]
pub const fn phase(&self) -> IntegrityPhase {
self.phase
}
#[must_use]
pub fn last_verified_physical_key(&self) -> Option<&[u8]> {
self.last_verified_physical_key.as_deref()
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum QuickIntegrityStatus {
CompleteClean,
CompleteWithFindings,
Uninspectable(IntegrityAuthorityDiagnostic),
UninspectableStorage(StorageTraversalCorruption),
ResourceLimited(IntegrityResourceDiagnostic),
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct QuickIntegrityResult {
entity: IntegrityEntityIdentity,
database_incarnation_id: DatabaseIncarnationId,
accepted_schema_version: u32,
accepted_schema_fingerprint: [u8; 16],
status: QuickIntegrityStatus,
total_findings: u64,
omitted_findings: u64,
findings: Vec<IntegrityFinding>,
}
impl QuickIntegrityResult {
#[must_use]
pub const fn entity(&self) -> &IntegrityEntityIdentity {
&self.entity
}
#[must_use]
pub const fn database_incarnation_id(&self) -> DatabaseIncarnationId {
self.database_incarnation_id
}
#[must_use]
pub const fn accepted_schema_version(&self) -> u32 {
self.accepted_schema_version
}
#[must_use]
pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
self.accepted_schema_fingerprint
}
#[must_use]
pub const fn status(&self) -> &QuickIntegrityStatus {
&self.status
}
#[must_use]
pub const fn total_findings(&self) -> u64 {
self.total_findings
}
#[must_use]
pub const fn omitted_findings(&self) -> u64 {
self.omitted_findings
}
#[must_use]
pub const fn findings(&self) -> &[IntegrityFinding] {
self.findings.as_slice()
}
}
struct QuickIntegrityAccumulator {
total_findings: u64,
findings: Vec<IntegrityFinding>,
}
impl QuickIntegrityAccumulator {
const fn new() -> Self {
Self {
total_findings: 0,
findings: Vec::new(),
}
}
fn record(&mut self, finding: IntegrityFinding) -> Result<(), IntegrityResourceDiagnostic> {
self.total_findings =
self.total_findings
.checked_add(1)
.ok_or(IntegrityResourceDiagnostic {
diagnostic_code: icydb_diagnostic_code::ErrorCode::RUNTIME_INTERNAL.raw(),
})?;
if self.findings.len() < MAX_QUICK_RETURNED_FINDINGS {
self.findings.push(finding);
}
Ok(())
}
fn complete(
self,
plan: &AcceptedInspectionPlan,
incarnation: DatabaseIncarnationId,
) -> QuickIntegrityResult {
let status = if self.total_findings == 0 {
QuickIntegrityStatus::CompleteClean
} else {
QuickIntegrityStatus::CompleteWithFindings
};
let omitted_findings = self
.total_findings
.saturating_sub(self.findings.len() as u64);
let identity = plan.identity();
QuickIntegrityResult {
entity: IntegrityEntityIdentity::from_plan(plan),
database_incarnation_id: incarnation,
accepted_schema_version: identity.accepted_schema_version().get(),
accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
status,
total_findings: self.total_findings,
omitted_findings,
findings: self.findings,
}
}
fn resource_limited(
self,
plan: &AcceptedInspectionPlan,
incarnation: DatabaseIncarnationId,
diagnostic: IntegrityResourceDiagnostic,
) -> QuickIntegrityResult {
let omitted_findings = self
.total_findings
.saturating_sub(self.findings.len() as u64);
let identity = plan.identity();
QuickIntegrityResult {
entity: IntegrityEntityIdentity::from_plan(plan),
database_incarnation_id: incarnation,
accepted_schema_version: identity.accepted_schema_version().get(),
accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
status: QuickIntegrityStatus::ResourceLimited(diagnostic),
total_findings: self.total_findings,
omitted_findings,
findings: self.findings,
}
}
}
pub(in crate::db) fn uninspectable_quick_integrity(
identity: crate::db::schema::AcceptedCatalogIdentity,
incarnation: DatabaseIncarnationId,
error: &InternalError,
) -> QuickIntegrityResult {
QuickIntegrityResult {
entity: IntegrityEntityIdentity::from_accepted_identity(identity),
database_incarnation_id: incarnation,
accepted_schema_version: identity.accepted_schema_version().get(),
accepted_schema_fingerprint: identity.accepted_schema_fingerprint(),
status: QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic::from_internal(
error,
)),
total_findings: 0,
omitted_findings: 0,
findings: Vec::new(),
}
}
pub(in crate::db) fn execute_quick_integrity<C: CanisterKind>(
db: &crate::db::Db<C>,
plan: &AcceptedInspectionPlan,
) -> Result<QuickIntegrityResult, InternalError> {
ensure_recovered(db)?;
let incarnation = database_incarnation_id()?;
let findings = match validate_quick_integrity_control(db, plan) {
Ok(findings) => findings,
Err(error) => {
return Ok(uninspectable_quick_integrity(
plan.identity(),
incarnation,
&error,
));
}
};
let mut accumulator = QuickIntegrityAccumulator::new();
for finding in findings {
if let Err(diagnostic) = accumulator.record(finding) {
return Ok(accumulator.resource_limited(plan, incarnation, diagnostic));
}
}
Ok(accumulator.complete(plan, incarnation))
}
pub(crate) fn generate_database_incarnation_id() -> Result<DatabaseIncarnationId, InternalError> {
DatabaseIncarnationId::generate()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
db::{
commit::CommitSchemaFingerprint,
schema::{
AcceptedCatalogIdentity, AcceptedCompositeCatalog, AcceptedFieldKind,
AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
FieldId, PersistedFieldSnapshot, PersistedSchemaSnapshot, SchemaFieldSlot,
SchemaInsertDefault, SchemaRowLayout, SchemaVersion,
enum_catalog::build_initial_accepted_enum_catalog,
},
},
model::field::{FieldStorageDecode, LeafCodec, ScalarCodec},
types::EntityTag,
};
fn plan() -> AcceptedInspectionPlan {
let revision = AcceptedSchemaRevision::INITIAL;
let identity = AcceptedCatalogIdentity::new(
EntityTag::new(23),
"tests::QuickEntity",
"tests::QuickStore",
revision,
SchemaVersion::initial(),
CommitSchemaFingerprint::from([0x44; 16]),
);
let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
SchemaVersion::initial(),
"tests::QuickEntity".to_string(),
"QuickEntity".to_string(),
FieldId::new(1),
SchemaRowLayout::initial(vec![(FieldId::new(1), SchemaFieldSlot::new(0))]),
vec![PersistedFieldSnapshot::new_initial(
FieldId::new(1),
"id".to_string(),
SchemaFieldSlot::new(0),
AcceptedFieldKind::Nat64,
Vec::new(),
false,
SchemaInsertDefault::None,
FieldStorageDecode::ByKind,
LeafCodec::Scalar(ScalarCodec::Nat64),
)],
));
let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
build_initial_accepted_enum_catalog(&[])
.expect("empty accepted enum catalog should build"),
AcceptedCompositeCatalog::empty(),
revision,
);
AcceptedInspectionPlan::compile_relation_free_for_tests(identity, snapshot, value_catalog)
.expect("accepted Quick plan should compile")
}
fn finding(plan: &AcceptedInspectionPlan) -> IntegrityFinding {
IntegrityFinding {
diagnostic_code: icydb_diagnostic_code::ErrorCode::STORE_CORRUPTION.raw(),
class: IntegrityFindingClass::Corruption,
severity: IntegritySeverity::Error,
kind: IntegrityFindingKind::MalformedRow,
entity: IntegrityEntityIdentity::from_plan(plan),
store_path: plan.identity().store_path().to_string(),
phase: IntegrityPhase::Rows,
verifier_family: IntegrityVerifierFamily::RowEnvelope,
physical_key: vec![1],
primary_key: None,
field_paths: Vec::new(),
constraint_id: None,
constraint_name: None,
schema_index_id: None,
relation_id: None,
expected: None,
observed: None,
}
}
#[test]
fn database_incarnation_rejects_zero_and_round_trips_current_bytes() {
assert!(DatabaseIncarnationId::try_from_bytes([0; 16]).is_err());
let identity = DatabaseIncarnationId::for_tests(7);
assert_eq!(
DatabaseIncarnationId::try_from_bytes(identity.to_bytes())
.expect("nonzero incarnation should decode"),
identity,
);
}
#[test]
fn quick_clean_result_binds_incarnation_and_accepted_plan_identity() {
let plan = plan();
let incarnation = DatabaseIncarnationId::for_tests(8);
let result = QuickIntegrityAccumulator::new().complete(&plan, incarnation);
assert_eq!(result.status(), &QuickIntegrityStatus::CompleteClean);
assert_eq!(result.database_incarnation_id(), incarnation);
assert_eq!(result.accepted_schema_version(), 1);
assert_eq!(result.accepted_schema_fingerprint(), [0x44; 16]);
assert_eq!(result.total_findings(), 0);
assert_eq!(result.omitted_findings(), 0);
}
#[test]
fn quick_findings_keep_a_bounded_prefix_and_exact_omitted_count() {
let plan = plan();
let mut accumulator = QuickIntegrityAccumulator::new();
for _ in 0..=MAX_QUICK_RETURNED_FINDINGS {
accumulator
.record(finding(&plan))
.expect("bounded test finding count should fit");
}
let result = accumulator.complete(&plan, DatabaseIncarnationId::for_tests(9));
assert_eq!(result.status(), &QuickIntegrityStatus::CompleteWithFindings,);
assert_eq!(result.total_findings(), 65);
assert_eq!(result.findings().len(), MAX_QUICK_RETURNED_FINDINGS);
assert_eq!(result.omitted_findings(), 1);
assert_eq!(
result.total_findings(),
result.findings().len() as u64 + result.omitted_findings(),
);
}
#[test]
fn quick_selected_authority_failure_is_not_a_clean_completion() {
let plan = plan();
let error = InternalError::accepted_row_constraint_program_corrupt();
let result = uninspectable_quick_integrity(
plan.identity(),
DatabaseIncarnationId::for_tests(10),
&error,
);
assert!(matches!(
result.status(),
QuickIntegrityStatus::Uninspectable(IntegrityAuthorityDiagnostic {
class: IntegrityAuthorityClass::Corruption,
..
}),
));
assert_eq!(result.total_findings(), 0);
assert_eq!(result.omitted_findings(), 0);
}
}