1use 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
14pub const COPY_ATTRIBUTE_BACKFILL_CAPABILITY: &str = "migration.backfill.copy-attribute";
16pub const MIGRATION_BACKFILL_FINGERPRINT_DOMAIN: &str = "typebridge.migration.backfill";
18pub const MIGRATION_BACKFILL_CANONICALIZATION: &str = "typebridge.migration-backfill/v1";
20pub const MAX_BACKFILL_BATCH_ROWS: u32 = 10_000;
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum BackfillValueTransform {
27 Identity,
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum BackfillConflictPolicy {
35 SkipEqualRejectDifferent,
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
41#[serde(rename_all = "snake_case")]
42pub enum BackfillPostcondition {
43 SourceValueCopiedExactly,
45}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
49#[serde(rename_all = "snake_case")]
50pub enum BackfillReverseProgram {
51 RemoveEqualCopiedDestination,
53}
54
55#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
57pub struct BackfillPartition {
58 batch_rows: u32,
59 stable_attribute: AttributeId,
60}
61
62impl BackfillPartition {
63 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 pub const fn batch_rows(&self) -> u32 {
80 self.batch_rows
81 }
82
83 pub const fn stable_attribute(&self) -> &AttributeId {
85 &self.stable_attribute
86 }
87}
88
89#[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 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 pub const fn owner(&self) -> &TypeId {
152 &self.owner
153 }
154
155 pub const fn source(&self) -> &AttributeId {
157 &self.source
158 }
159
160 pub const fn destination(&self) -> &AttributeId {
162 &self.destination
163 }
164
165 pub const fn partition(&self) -> &BackfillPartition {
167 &self.partition
168 }
169
170 pub const fn managed_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
172 &self.managed_semantics
173 }
174
175 pub const fn conflict(&self) -> BackfillConflictPolicy {
177 self.conflict
178 }
179
180 pub const fn transform(&self) -> BackfillValueTransform {
182 self.transform
183 }
184
185 pub const fn postcondition(&self) -> BackfillPostcondition {
187 self.postcondition
188 }
189
190 pub const fn reverse(&self) -> Option<BackfillReverseProgram> {
192 self.reverse
193 }
194
195 pub const fn required_capabilities(&self) -> &CapabilitySet {
197 &self.required_capabilities
198 }
199
200 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
202 to_canonical_json(self)
203 }
204
205 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
219pub 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}