Skip to main content

type_bridge_contract/
migration_backfill.rs

1//! Closed binding-neutral data plans used by canonical migration steps.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::capability::{CapabilityId, CapabilitySet};
7use crate::codec::{FormatVersion, from_canonical_json, to_canonical_json};
8use crate::diagnostic::{Diagnostic, DiagnosticCategory};
9use crate::fingerprint::Fingerprint as GenericFingerprint;
10use crate::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
11use crate::id::{AttributeId, TypeId, TypeKind};
12use crate::schema_fingerprint::ManagedSemanticSchemaFingerprint;
13
14/// Capability required to execute the first closed copy-attribute backfill.
15pub const COPY_ATTRIBUTE_BACKFILL_CAPABILITY: &str = "migration.backfill.copy-attribute";
16/// Fingerprint domain for a canonical binding-neutral backfill plan.
17pub const MIGRATION_BACKFILL_FINGERPRINT_DOMAIN: &str = "typebridge.migration.backfill";
18/// Canonicalization identity for the first backfill-plan wire.
19pub const MIGRATION_BACKFILL_CANONICALIZATION: &str = "typebridge.migration-backfill/v1";
20/// Maximum rows admitted into one deterministic backfill transaction group.
21pub const MAX_BACKFILL_BATCH_ROWS: u32 = 10_000;
22
23/// Closed value transformation applied while copying an attribute.
24#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum BackfillValueTransform {
27    /// Copy the exact canonical source value without coercion.
28    Identity,
29}
30
31/// Closed conflict behavior for a destination value that already exists.
32#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum BackfillConflictPolicy {
35    /// Skip an equal value and reject a different value before reporting success.
36    SkipEqualRejectDifferent,
37}
38
39/// Required terminal predicate for the first backfill plan.
40#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
41#[serde(rename_all = "snake_case")]
42pub enum BackfillPostcondition {
43    /// Every selected source value has one equal destination value.
44    SourceValueCopiedExactly,
45}
46
47/// Optional independently verifiable reverse program.
48#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
49#[serde(rename_all = "snake_case")]
50pub enum BackfillReverseProgram {
51    /// Remove only a destination value that still equals its source value.
52    RemoveEqualCopiedDestination,
53}
54
55/// A deterministic partition contract bound to a stable historical attribute.
56#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
57pub struct BackfillPartition {
58    batch_rows: u32,
59    stable_attribute: AttributeId,
60}
61
62impl BackfillPartition {
63    /// Construct a bounded deterministic partition contract.
64    pub fn new(batch_rows: u32, stable_attribute: AttributeId) -> Result<Self, Diagnostic> {
65        if batch_rows == 0 || batch_rows > MAX_BACKFILL_BATCH_ROWS {
66            return Err(backfill_failure(
67                DiagnosticCategory::ResourceLimit,
68                "migration_backfill_batch_rows_out_of_range",
69                "backfill batch rows must be nonzero and within the common ceiling",
70            ));
71        }
72        Ok(Self {
73            batch_rows,
74            stable_attribute,
75        })
76    }
77
78    /// Return the maximum rows in one transaction group.
79    pub const fn batch_rows(&self) -> u32 {
80        self.batch_rows
81    }
82
83    /// Return the exact historical attribute used for stable partition order.
84    pub const fn stable_attribute(&self) -> &AttributeId {
85        &self.stable_attribute
86    }
87}
88
89/// First closed binding-neutral data plan: copy one historical attribute.
90#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
91pub struct AttributeBackfillPlan {
92    conflict: BackfillConflictPolicy,
93    destination: AttributeId,
94    format: FormatVersion,
95    managed_semantics: ManagedSemanticSchemaFingerprint,
96    owner: TypeId,
97    partition: BackfillPartition,
98    postcondition: BackfillPostcondition,
99    required_capabilities: CapabilitySet,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    reverse: Option<BackfillReverseProgram>,
102    source: AttributeId,
103    transform: BackfillValueTransform,
104}
105
106impl AttributeBackfillPlan {
107    /// Construct and validate an exact copy-attribute data plan.
108    pub fn new(
109        owner: TypeId,
110        source: AttributeId,
111        destination: AttributeId,
112        partition: BackfillPartition,
113        managed_semantics: ManagedSemanticSchemaFingerprint,
114        reverse: Option<BackfillReverseProgram>,
115    ) -> Result<Self, Diagnostic> {
116        if !matches!(owner.kind(), TypeKind::Entity | TypeKind::Relation) {
117            return Err(backfill_failure(
118                DiagnosticCategory::InvalidContract,
119                "migration_backfill_owner_kind_invalid",
120                "backfill owner must be an entity or relation type",
121            ));
122        }
123        if source == destination {
124            return Err(backfill_failure(
125                DiagnosticCategory::InvalidContract,
126                "migration_backfill_same_attribute",
127                "backfill source and destination attributes must differ",
128            ));
129        }
130        let mut required_capabilities = CapabilitySet::new();
131        required_capabilities.insert(
132            CapabilityId::new(COPY_ATTRIBUTE_BACKFILL_CAPABILITY)
133                .expect("the fixed backfill capability is canonical"),
134        );
135        Ok(Self {
136            conflict: BackfillConflictPolicy::SkipEqualRejectDifferent,
137            destination,
138            format: FormatVersion::V1,
139            managed_semantics,
140            owner,
141            partition,
142            postcondition: BackfillPostcondition::SourceValueCopiedExactly,
143            required_capabilities,
144            reverse,
145            source,
146            transform: BackfillValueTransform::Identity,
147        })
148    }
149
150    /// Return the historical owner type selected by this plan.
151    pub const fn owner(&self) -> &TypeId {
152        &self.owner
153    }
154
155    /// Return the historical source attribute.
156    pub const fn source(&self) -> &AttributeId {
157        &self.source
158    }
159
160    /// Return the historical destination attribute.
161    pub const fn destination(&self) -> &AttributeId {
162        &self.destination
163    }
164
165    /// Return deterministic transaction partitioning.
166    pub const fn partition(&self) -> &BackfillPartition {
167        &self.partition
168    }
169
170    /// Return the exact historical intermediate-schema fingerprint.
171    pub const fn managed_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
172        &self.managed_semantics
173    }
174
175    /// Return the fixed conflict policy.
176    pub const fn conflict(&self) -> BackfillConflictPolicy {
177        self.conflict
178    }
179
180    /// Return the fixed value transformation.
181    pub const fn transform(&self) -> BackfillValueTransform {
182        self.transform
183    }
184
185    /// Return the required terminal postcondition.
186    pub const fn postcondition(&self) -> BackfillPostcondition {
187        self.postcondition
188    }
189
190    /// Return the optional checked reverse program.
191    pub const fn reverse(&self) -> Option<BackfillReverseProgram> {
192        self.reverse
193    }
194
195    /// Return capabilities derived from the closed plan variant.
196    pub const fn required_capabilities(&self) -> &CapabilitySet {
197        &self.required_capabilities
198    }
199
200    /// Return exact canonical plan bytes.
201    pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
202        to_canonical_json(self)
203    }
204
205    /// Compute the domain-separated plan fingerprint.
206    pub fn fingerprint(&self) -> Result<Fingerprint, Diagnostic> {
207        Ok(Fingerprint::compute(
208            FingerprintDomain::new(MIGRATION_BACKFILL_FINGERPRINT_DOMAIN)?,
209            CanonicalizationVersion::new(MIGRATION_BACKFILL_CANONICALIZATION)?,
210            self.managed_semantics
211                .as_fingerprint()
212                .semantic_profile()
213                .cloned(),
214            &self.canonical_bytes()?,
215        ))
216    }
217}
218
219/// Decode exact canonical bytes and rebuild every derived backfill claim.
220pub fn decode_attribute_backfill_plan(bytes: &[u8]) -> Result<AttributeBackfillPlan, Diagnostic> {
221    let candidate = from_canonical_json::<AttributeBackfillCandidate>(bytes)?;
222    let format = match candidate.format {
223        1 => FormatVersion::V1,
224        _ => {
225            return Err(backfill_failure(
226                DiagnosticCategory::InvalidContract,
227                "migration_backfill_format_unsupported",
228                "backfill plan format is not supported",
229            ));
230        }
231    };
232    let _ = format;
233    let semantics =
234        ManagedSemanticSchemaFingerprint::from_wire(from_canonical_json::<GenericFingerprint>(
235            &to_canonical_json(&candidate.managed_semantics)?,
236        )?)?;
237    let reverse = match candidate.reverse.as_deref() {
238        None => None,
239        Some("remove_equal_copied_destination") => {
240            Some(BackfillReverseProgram::RemoveEqualCopiedDestination)
241        }
242        Some(_) => {
243            return Err(backfill_failure(
244                DiagnosticCategory::InvalidContract,
245                "migration_backfill_reverse_unsupported",
246                "backfill reverse program is not supported",
247            ));
248        }
249    };
250    let plan = AttributeBackfillPlan::new(
251        candidate.owner,
252        candidate.source,
253        candidate.destination,
254        BackfillPartition::new(
255            candidate.partition.batch_rows,
256            candidate.partition.stable_attribute,
257        )?,
258        semantics,
259        reverse,
260    )?;
261    if plan.canonical_bytes()? != bytes {
262        return Err(backfill_failure(
263            DiagnosticCategory::Integrity,
264            "migration_backfill_contract_mismatch",
265            "backfill plan claims differ from constructor-derived canonical claims",
266        ));
267    }
268    Ok(plan)
269}
270
271#[derive(Deserialize, Serialize)]
272#[serde(deny_unknown_fields)]
273struct AttributeBackfillCandidate {
274    conflict: String,
275    destination: AttributeId,
276    format: u32,
277    managed_semantics: Value,
278    owner: TypeId,
279    partition: BackfillPartitionCandidate,
280    postcondition: String,
281    required_capabilities: CapabilitySet,
282    #[serde(default)]
283    reverse: Option<String>,
284    source: AttributeId,
285    transform: String,
286}
287
288#[derive(Deserialize, Serialize)]
289#[serde(deny_unknown_fields)]
290struct BackfillPartitionCandidate {
291    batch_rows: u32,
292    stable_attribute: AttributeId,
293}
294
295fn backfill_failure(
296    category: DiagnosticCategory,
297    code: &'static str,
298    message: &'static str,
299) -> Diagnostic {
300    Diagnostic::stable(category, code, message)
301}