use std::fmt;
use crate::{
db::{
commit::CommitSchemaFingerprint,
schema::{
PersistedSchemaSnapshot, SchemaVersion, accepted_schema_admission_fingerprint,
accepted_schema_admission_fingerprint_method_version,
},
},
error::InternalError,
};
use super::{
SchemaTransitionRejection, SchemaTransitionRejectionDetail,
SchemaTransitionRejectionDetailCode, SchemaTransitionRejectionKind,
};
#[cfg_attr(test, derive(Debug))]
#[derive(Clone, Copy, Eq, PartialEq)]
pub(in crate::db::schema) struct SchemaAdmissionIdentity {
pub(super) schema_version: SchemaVersion,
pub(super) fingerprint_method_version: u8,
pub(super) schema_fingerprint: CommitSchemaFingerprint,
}
impl SchemaAdmissionIdentity {
fn from_snapshot(snapshot: &PersistedSchemaSnapshot) -> Result<Self, InternalError> {
Ok(Self {
schema_version: snapshot.version(),
fingerprint_method_version: accepted_schema_admission_fingerprint_method_version(),
schema_fingerprint: accepted_schema_admission_fingerprint(snapshot)?,
})
}
}
#[cfg_attr(test, derive(Debug))]
#[derive(Clone, Copy, Eq, PartialEq)]
pub(in crate::db::schema) struct SchemaAdmissionIdentityComparison {
pub(super) stored: SchemaAdmissionIdentity,
pub(super) candidate: SchemaAdmissionIdentity,
}
impl SchemaAdmissionIdentityComparison {
pub(in crate::db::schema) fn from_snapshots(
stored: &PersistedSchemaSnapshot,
candidate: &PersistedSchemaSnapshot,
) -> Result<Self, InternalError> {
Ok(Self {
stored: SchemaAdmissionIdentity::from_snapshot(stored)?,
candidate: SchemaAdmissionIdentity::from_snapshot(candidate)?,
})
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub(in crate::db::schema) enum SchemaAdmissionRejectionReason {
EmptyVersionBump,
FingerprintMethodMismatch,
MissingVersionBump,
VersionGap,
VersionRollback,
}
impl SchemaAdmissionRejectionReason {
const fn code(self) -> u8 {
match self {
Self::EmptyVersionBump => 1,
Self::FingerprintMethodMismatch => 2,
Self::MissingVersionBump => 3,
Self::VersionGap => 4,
Self::VersionRollback => 5,
}
}
}
impl fmt::Debug for SchemaAdmissionRejectionReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let code = self.code();
write!(f, "{code}")
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub(in crate::db::schema) struct SchemaAdmissionRejectionClassification {
pub(super) reason: SchemaAdmissionRejectionReason,
pub(super) expected_next: Option<u32>,
}
impl SchemaAdmissionRejectionClassification {
const fn new(reason: SchemaAdmissionRejectionReason, expected_next: Option<u32>) -> Self {
Self {
reason,
expected_next,
}
}
pub(in crate::db::schema) const fn reason(self) -> SchemaAdmissionRejectionReason {
self.reason
}
pub(in crate::db::schema) const fn expected_next(self) -> Option<u32> {
self.expected_next
}
}
impl fmt::Debug for SchemaAdmissionRejectionClassification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let reason = self.reason.code();
let expected_next = self.expected_next.unwrap_or_default();
write!(f, "{reason}:{expected_next}")
}
}
pub(in crate::db::schema) fn schema_admission_rejection(
comparison: SchemaAdmissionIdentityComparison,
) -> Option<SchemaTransitionRejection> {
let classification = classify_schema_admission_rejection(comparison)?;
Some(SchemaTransitionRejection::new(
SchemaTransitionRejectionKind::SchemaVersion,
SchemaTransitionRejectionDetail::new(SchemaTransitionRejectionDetailCode::SchemaAdmission),
Some(classification),
))
}
pub(super) fn classify_schema_admission_rejection(
comparison: SchemaAdmissionIdentityComparison,
) -> Option<SchemaAdmissionRejectionClassification> {
if comparison.stored.fingerprint_method_version
!= comparison.candidate.fingerprint_method_version
{
return Some(SchemaAdmissionRejectionClassification::new(
SchemaAdmissionRejectionReason::FingerprintMethodMismatch,
None,
));
}
let stored_version = comparison.stored.schema_version.get();
let candidate_version = comparison.candidate.schema_version.get();
let same_fingerprint =
comparison.stored.schema_fingerprint == comparison.candidate.schema_fingerprint;
if candidate_version < stored_version {
return Some(SchemaAdmissionRejectionClassification::new(
SchemaAdmissionRejectionReason::VersionRollback,
None,
));
}
if stored_version == candidate_version {
return (!same_fingerprint).then(|| {
SchemaAdmissionRejectionClassification::new(
SchemaAdmissionRejectionReason::MissingVersionBump,
None,
)
});
}
if same_fingerprint {
return Some(SchemaAdmissionRejectionClassification::new(
SchemaAdmissionRejectionReason::EmptyVersionBump,
None,
));
}
let expected_next = stored_version.saturating_add(1);
if candidate_version == expected_next {
None
} else {
Some(SchemaAdmissionRejectionClassification::new(
SchemaAdmissionRejectionReason::VersionGap,
Some(expected_next),
))
}
}