use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::capability::{CapabilityId, CapabilitySet};
use crate::codec::{FormatVersion, from_canonical_json, to_canonical_json};
use crate::diagnostic::{Diagnostic, DiagnosticCategory};
use crate::fingerprint::Fingerprint as GenericFingerprint;
use crate::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
use crate::id::{AttributeId, TypeId, TypeKind};
use crate::schema_fingerprint::ManagedSemanticSchemaFingerprint;
pub const COPY_ATTRIBUTE_BACKFILL_CAPABILITY: &str = "migration.backfill.copy-attribute";
pub const MIGRATION_BACKFILL_FINGERPRINT_DOMAIN: &str = "typebridge.migration.backfill";
pub const MIGRATION_BACKFILL_CANONICALIZATION: &str = "typebridge.migration-backfill/v1";
pub const MAX_BACKFILL_BATCH_ROWS: u32 = 10_000;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BackfillValueTransform {
Identity,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BackfillConflictPolicy {
SkipEqualRejectDifferent,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BackfillPostcondition {
SourceValueCopiedExactly,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BackfillReverseProgram {
RemoveEqualCopiedDestination,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct BackfillPartition {
batch_rows: u32,
stable_attribute: AttributeId,
}
impl BackfillPartition {
pub fn new(batch_rows: u32, stable_attribute: AttributeId) -> Result<Self, Diagnostic> {
if batch_rows == 0 || batch_rows > MAX_BACKFILL_BATCH_ROWS {
return Err(backfill_failure(
DiagnosticCategory::ResourceLimit,
"migration_backfill_batch_rows_out_of_range",
"backfill batch rows must be nonzero and within the common ceiling",
));
}
Ok(Self {
batch_rows,
stable_attribute,
})
}
pub const fn batch_rows(&self) -> u32 {
self.batch_rows
}
pub const fn stable_attribute(&self) -> &AttributeId {
&self.stable_attribute
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct AttributeBackfillPlan {
conflict: BackfillConflictPolicy,
destination: AttributeId,
format: FormatVersion,
managed_semantics: ManagedSemanticSchemaFingerprint,
owner: TypeId,
partition: BackfillPartition,
postcondition: BackfillPostcondition,
required_capabilities: CapabilitySet,
#[serde(skip_serializing_if = "Option::is_none")]
reverse: Option<BackfillReverseProgram>,
source: AttributeId,
transform: BackfillValueTransform,
}
impl AttributeBackfillPlan {
pub fn new(
owner: TypeId,
source: AttributeId,
destination: AttributeId,
partition: BackfillPartition,
managed_semantics: ManagedSemanticSchemaFingerprint,
reverse: Option<BackfillReverseProgram>,
) -> Result<Self, Diagnostic> {
if !matches!(owner.kind(), TypeKind::Entity | TypeKind::Relation) {
return Err(backfill_failure(
DiagnosticCategory::InvalidContract,
"migration_backfill_owner_kind_invalid",
"backfill owner must be an entity or relation type",
));
}
if source == destination {
return Err(backfill_failure(
DiagnosticCategory::InvalidContract,
"migration_backfill_same_attribute",
"backfill source and destination attributes must differ",
));
}
let mut required_capabilities = CapabilitySet::new();
required_capabilities.insert(
CapabilityId::new(COPY_ATTRIBUTE_BACKFILL_CAPABILITY)
.expect("the fixed backfill capability is canonical"),
);
Ok(Self {
conflict: BackfillConflictPolicy::SkipEqualRejectDifferent,
destination,
format: FormatVersion::V1,
managed_semantics,
owner,
partition,
postcondition: BackfillPostcondition::SourceValueCopiedExactly,
required_capabilities,
reverse,
source,
transform: BackfillValueTransform::Identity,
})
}
pub const fn owner(&self) -> &TypeId {
&self.owner
}
pub const fn source(&self) -> &AttributeId {
&self.source
}
pub const fn destination(&self) -> &AttributeId {
&self.destination
}
pub const fn partition(&self) -> &BackfillPartition {
&self.partition
}
pub const fn managed_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
&self.managed_semantics
}
pub const fn conflict(&self) -> BackfillConflictPolicy {
self.conflict
}
pub const fn transform(&self) -> BackfillValueTransform {
self.transform
}
pub const fn postcondition(&self) -> BackfillPostcondition {
self.postcondition
}
pub const fn reverse(&self) -> Option<BackfillReverseProgram> {
self.reverse
}
pub const fn required_capabilities(&self) -> &CapabilitySet {
&self.required_capabilities
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
to_canonical_json(self)
}
pub fn fingerprint(&self) -> Result<Fingerprint, Diagnostic> {
Ok(Fingerprint::compute(
FingerprintDomain::new(MIGRATION_BACKFILL_FINGERPRINT_DOMAIN)?,
CanonicalizationVersion::new(MIGRATION_BACKFILL_CANONICALIZATION)?,
self.managed_semantics
.as_fingerprint()
.semantic_profile()
.cloned(),
&self.canonical_bytes()?,
))
}
}
pub fn decode_attribute_backfill_plan(bytes: &[u8]) -> Result<AttributeBackfillPlan, Diagnostic> {
let candidate = from_canonical_json::<AttributeBackfillCandidate>(bytes)?;
let format = match candidate.format {
1 => FormatVersion::V1,
_ => {
return Err(backfill_failure(
DiagnosticCategory::InvalidContract,
"migration_backfill_format_unsupported",
"backfill plan format is not supported",
));
}
};
let _ = format;
let semantics =
ManagedSemanticSchemaFingerprint::from_wire(from_canonical_json::<GenericFingerprint>(
&to_canonical_json(&candidate.managed_semantics)?,
)?)?;
let reverse = match candidate.reverse.as_deref() {
None => None,
Some("remove_equal_copied_destination") => {
Some(BackfillReverseProgram::RemoveEqualCopiedDestination)
}
Some(_) => {
return Err(backfill_failure(
DiagnosticCategory::InvalidContract,
"migration_backfill_reverse_unsupported",
"backfill reverse program is not supported",
));
}
};
let plan = AttributeBackfillPlan::new(
candidate.owner,
candidate.source,
candidate.destination,
BackfillPartition::new(
candidate.partition.batch_rows,
candidate.partition.stable_attribute,
)?,
semantics,
reverse,
)?;
if plan.canonical_bytes()? != bytes {
return Err(backfill_failure(
DiagnosticCategory::Integrity,
"migration_backfill_contract_mismatch",
"backfill plan claims differ from constructor-derived canonical claims",
));
}
Ok(plan)
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct AttributeBackfillCandidate {
conflict: String,
destination: AttributeId,
format: u32,
managed_semantics: Value,
owner: TypeId,
partition: BackfillPartitionCandidate,
postcondition: String,
required_capabilities: CapabilitySet,
#[serde(default)]
reverse: Option<String>,
source: AttributeId,
transform: String,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct BackfillPartitionCandidate {
batch_rows: u32,
stable_attribute: AttributeId,
}
fn backfill_failure(
category: DiagnosticCategory,
code: &'static str,
message: &'static str,
) -> Diagnostic {
Diagnostic::stable(category, code, message)
}