1use minicbor::{Decoder, Encoder};
2
3use crate::{BlobHash, MigrationId, SchemaDescriptor, SchemaError};
4
5const MAGIC: &str = "iroh-db";
6const FORMAT_MAJOR: u16 = 1;
7const FORMAT_MINOR: u16 = 0;
8const STAGE_KIND: u8 = 4;
9const ACTIVATION_KIND: u8 = 5;
10const MAX_RECORD_ID: usize = 1_024;
11const MAX_TARGET_RECORD: usize = 16 * 1024 * 1024;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct SchemaStage {
16 migration_id: MigrationId,
17 from: SchemaDescriptor,
18 to: SchemaDescriptor,
19 source_record_id: Vec<u8>,
20 source_digest: BlobHash,
21 target_record: Vec<u8>,
22}
23
24impl SchemaStage {
25 pub fn new(
27 migration_id: MigrationId,
28 from: SchemaDescriptor,
29 to: SchemaDescriptor,
30 source_record_id: Vec<u8>,
31 source_digest: BlobHash,
32 target_record: Vec<u8>,
33 ) -> Result<Self, MigrationCodecError> {
34 validate_transition(&from, &to)?;
35 if source_record_id.len() > MAX_RECORD_ID {
36 return Err(MigrationCodecError::RecordIdTooLarge(
37 source_record_id.len(),
38 ));
39 }
40 if target_record.len() > MAX_TARGET_RECORD {
41 return Err(MigrationCodecError::TargetRecordTooLarge(
42 target_record.len(),
43 ));
44 }
45 Ok(Self {
46 migration_id,
47 from,
48 to,
49 source_record_id,
50 source_digest,
51 target_record,
52 })
53 }
54
55 pub const fn migration_id(&self) -> MigrationId {
57 self.migration_id
58 }
59
60 pub const fn from(&self) -> &SchemaDescriptor {
62 &self.from
63 }
64
65 pub const fn to(&self) -> &SchemaDescriptor {
67 &self.to
68 }
69
70 pub fn source_record_id(&self) -> &[u8] {
72 &self.source_record_id
73 }
74
75 pub const fn source_digest(&self) -> BlobHash {
77 self.source_digest
78 }
79
80 pub fn target_record(&self) -> &[u8] {
82 &self.target_record
83 }
84
85 pub fn encode_canonical(&self) -> Vec<u8> {
87 let mut encoder = Encoder::new(Vec::new());
88 encode_stage(&mut encoder, self).expect("encoding into Vec cannot fail");
89 encoder.into_writer()
90 }
91
92 pub fn decode_canonical(bytes: &[u8]) -> Result<Self, MigrationCodecError> {
94 let decoded = decode_stage(bytes)?;
95 if decoded.encode_canonical() != bytes {
96 return Err(MigrationCodecError::NonCanonicalEncoding);
97 }
98 Ok(decoded)
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct SchemaActivation {
105 migration_id: MigrationId,
106 from: SchemaDescriptor,
107 to: SchemaDescriptor,
108 staged_records: u64,
109 staging_digest: BlobHash,
110}
111
112impl SchemaActivation {
113 pub fn new(
115 migration_id: MigrationId,
116 from: SchemaDescriptor,
117 to: SchemaDescriptor,
118 staged_records: u64,
119 staging_digest: BlobHash,
120 ) -> Result<Self, MigrationCodecError> {
121 validate_transition(&from, &to)?;
122 Ok(Self {
123 migration_id,
124 from,
125 to,
126 staged_records,
127 staging_digest,
128 })
129 }
130
131 pub const fn migration_id(&self) -> MigrationId {
133 self.migration_id
134 }
135
136 pub const fn from(&self) -> &SchemaDescriptor {
138 &self.from
139 }
140
141 pub const fn to(&self) -> &SchemaDescriptor {
143 &self.to
144 }
145
146 pub const fn staged_records(&self) -> u64 {
148 self.staged_records
149 }
150
151 pub const fn staging_digest(&self) -> BlobHash {
153 self.staging_digest
154 }
155
156 pub fn encode_canonical(&self) -> Vec<u8> {
158 let mut encoder = Encoder::new(Vec::new());
159 encode_activation(&mut encoder, self).expect("encoding into Vec cannot fail");
160 encoder.into_writer()
161 }
162
163 pub fn decode_canonical(bytes: &[u8]) -> Result<Self, MigrationCodecError> {
165 let decoded = decode_activation(bytes)?;
166 if decoded.encode_canonical() != bytes {
167 return Err(MigrationCodecError::NonCanonicalEncoding);
168 }
169 Ok(decoded)
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
175pub enum MigrationCodecError {
176 #[error("migration schemas belong to different collections")]
178 CollectionMismatch,
179 #[error("migration versions must be adjacent: {from} -> {to}")]
181 NonAdjacentVersion { from: u32, to: u32 },
182 #[error("migration changes the primary-key contract")]
184 PrimaryKeyMismatch,
185 #[error("staging set mixes migration identities or schema transitions")]
187 MixedStagingSet,
188 #[error("staging set contains a duplicate source record")]
190 DuplicateSourceRecord,
191 #[error("migration record id is too large: {0} bytes")]
193 RecordIdTooLarge(usize),
194 #[error("migration target record is too large: {0} bytes")]
196 TargetRecordTooLarge(usize),
197 #[error(transparent)]
199 Schema(#[from] SchemaError),
200 #[error("unsupported migration object format")]
202 UnsupportedFormat,
203 #[error("invalid migration encoding: {0}")]
205 InvalidEncoding(String),
206 #[error("migration encoding is not canonical")]
208 NonCanonicalEncoding,
209}
210
211pub fn staging_digest(stages: &[SchemaStage]) -> Result<BlobHash, MigrationCodecError> {
213 let mut ordered: Vec<_> = stages.iter().collect();
214 ordered.sort_unstable_by(|left, right| left.source_record_id.cmp(&right.source_record_id));
215 if ordered
216 .windows(2)
217 .any(|pair| pair[0].source_record_id.as_slice() == pair[1].source_record_id.as_slice())
218 {
219 return Err(MigrationCodecError::DuplicateSourceRecord);
220 }
221 if let Some(first) = ordered.first()
222 && ordered.iter().any(|stage| {
223 stage.migration_id != first.migration_id
224 || stage.from.schema_id() != first.from.schema_id()
225 || stage.to.schema_id() != first.to.schema_id()
226 })
227 {
228 return Err(MigrationCodecError::MixedStagingSet);
229 }
230 let mut hasher = blake3::Hasher::new();
231 hasher.update(b"iroh-db/schema-staging/v1");
232 hasher.update(&(ordered.len() as u64).to_be_bytes());
233 for stage in ordered {
234 let bytes = stage.encode_canonical();
235 hasher.update(&(bytes.len() as u64).to_be_bytes());
236 hasher.update(&bytes);
237 }
238 Ok(BlobHash::from_bytes(*hasher.finalize().as_bytes()))
239}
240
241fn validate_transition(
242 from: &SchemaDescriptor,
243 to: &SchemaDescriptor,
244) -> Result<(), MigrationCodecError> {
245 if from.collection_id() != to.collection_id() {
246 return Err(MigrationCodecError::CollectionMismatch);
247 }
248 if from.version().checked_add(1) != Some(to.version()) {
249 return Err(MigrationCodecError::NonAdjacentVersion {
250 from: from.version(),
251 to: to.version(),
252 });
253 }
254 let from_id = from.fields().iter().find(|field| field.is_record_id());
255 let to_id = to.fields().iter().find(|field| field.is_record_id());
256 if from_id.map(|field| (field.id(), field.crdt(), field.value_type()))
257 != to_id.map(|field| (field.id(), field.crdt(), field.value_type()))
258 {
259 return Err(MigrationCodecError::PrimaryKeyMismatch);
260 }
261 Ok(())
262}
263
264fn encode_prefix(
265 encoder: &mut Encoder<Vec<u8>>,
266 map_len: u64,
267 kind: u8,
268 migration_id: MigrationId,
269 from: &SchemaDescriptor,
270 to: &SchemaDescriptor,
271) -> Result<(), minicbor::encode::Error<std::convert::Infallible>> {
272 encoder.map(map_len)?;
273 encoder.u8(0)?.str(MAGIC)?;
274 encoder.u8(1)?.u16(FORMAT_MAJOR)?;
275 encoder.u8(2)?.u16(FORMAT_MINOR)?;
276 encoder.u8(3)?.u8(kind)?;
277 encoder.u8(4)?.bytes(migration_id.as_bytes())?;
278 encoder.u8(5)?.bytes(&from.encode_canonical())?;
279 encoder.u8(6)?.bytes(&to.encode_canonical())?;
280 Ok(())
281}
282
283fn encode_stage(
284 encoder: &mut Encoder<Vec<u8>>,
285 stage: &SchemaStage,
286) -> Result<(), minicbor::encode::Error<std::convert::Infallible>> {
287 encode_prefix(
288 encoder,
289 10,
290 STAGE_KIND,
291 stage.migration_id,
292 &stage.from,
293 &stage.to,
294 )?;
295 encoder.u8(7)?.bytes(&stage.source_record_id)?;
296 encoder.u8(8)?.bytes(stage.source_digest.as_bytes())?;
297 encoder.u8(9)?.bytes(&stage.target_record)?;
298 Ok(())
299}
300
301fn encode_activation(
302 encoder: &mut Encoder<Vec<u8>>,
303 activation: &SchemaActivation,
304) -> Result<(), minicbor::encode::Error<std::convert::Infallible>> {
305 encode_prefix(
306 encoder,
307 9,
308 ACTIVATION_KIND,
309 activation.migration_id,
310 &activation.from,
311 &activation.to,
312 )?;
313 encoder.u8(7)?.u64(activation.staged_records)?;
314 encoder.u8(8)?.bytes(activation.staging_digest.as_bytes())?;
315 Ok(())
316}
317
318fn decode_prefix(
319 decoder: &mut Decoder<'_>,
320 map_len: u64,
321 kind: u8,
322) -> Result<(MigrationId, SchemaDescriptor, SchemaDescriptor), MigrationCodecError> {
323 expect_map_len(decoder, map_len)?;
324 expect_key(decoder, 0)?;
325 if decoder.str().map_err(decode_error)? != MAGIC {
326 return Err(MigrationCodecError::UnsupportedFormat);
327 }
328 expect_key(decoder, 1)?;
329 if decoder.u16().map_err(decode_error)? != FORMAT_MAJOR {
330 return Err(MigrationCodecError::UnsupportedFormat);
331 }
332 expect_key(decoder, 2)?;
333 if decoder.u16().map_err(decode_error)? != FORMAT_MINOR {
334 return Err(MigrationCodecError::UnsupportedFormat);
335 }
336 expect_key(decoder, 3)?;
337 if decoder.u8().map_err(decode_error)? != kind {
338 return Err(MigrationCodecError::UnsupportedFormat);
339 }
340 expect_key(decoder, 4)?;
341 let migration_id = MigrationId::from_bytes(fixed_bytes(decoder, "migration id")?);
342 expect_key(decoder, 5)?;
343 let from = SchemaDescriptor::decode_canonical(decoder.bytes().map_err(decode_error)?)?;
344 expect_key(decoder, 6)?;
345 let to = SchemaDescriptor::decode_canonical(decoder.bytes().map_err(decode_error)?)?;
346 Ok((migration_id, from, to))
347}
348
349fn decode_stage(bytes: &[u8]) -> Result<SchemaStage, MigrationCodecError> {
350 let mut decoder = Decoder::new(bytes);
351 let (migration_id, from, to) = decode_prefix(&mut decoder, 10, STAGE_KIND)?;
352 expect_key(&mut decoder, 7)?;
353 let source_record_id = decoder.bytes().map_err(decode_error)?.to_vec();
354 expect_key(&mut decoder, 8)?;
355 let source_digest = BlobHash::from_bytes(fixed_bytes(&mut decoder, "source digest")?);
356 expect_key(&mut decoder, 9)?;
357 let target_record = decoder.bytes().map_err(decode_error)?.to_vec();
358 if decoder.position() != bytes.len() {
359 return Err(MigrationCodecError::InvalidEncoding("trailing data".into()));
360 }
361 SchemaStage::new(
362 migration_id,
363 from,
364 to,
365 source_record_id,
366 source_digest,
367 target_record,
368 )
369}
370
371fn decode_activation(bytes: &[u8]) -> Result<SchemaActivation, MigrationCodecError> {
372 let mut decoder = Decoder::new(bytes);
373 let (migration_id, from, to) = decode_prefix(&mut decoder, 9, ACTIVATION_KIND)?;
374 expect_key(&mut decoder, 7)?;
375 let staged_records = decoder.u64().map_err(decode_error)?;
376 expect_key(&mut decoder, 8)?;
377 let staging_digest = BlobHash::from_bytes(fixed_bytes(&mut decoder, "staging digest")?);
378 if decoder.position() != bytes.len() {
379 return Err(MigrationCodecError::InvalidEncoding("trailing data".into()));
380 }
381 SchemaActivation::new(migration_id, from, to, staged_records, staging_digest)
382}
383
384fn fixed_bytes(decoder: &mut Decoder<'_>, label: &str) -> Result<[u8; 32], MigrationCodecError> {
385 decoder
386 .bytes()
387 .map_err(decode_error)?
388 .try_into()
389 .map_err(|_| MigrationCodecError::InvalidEncoding(format!("{label} must be 32 bytes")))
390}
391
392fn expect_map_len(decoder: &mut Decoder<'_>, expected: u64) -> Result<(), MigrationCodecError> {
393 let actual = decoder
394 .map()
395 .map_err(decode_error)?
396 .ok_or_else(|| MigrationCodecError::InvalidEncoding("indefinite map".into()))?;
397 if actual != expected {
398 return Err(MigrationCodecError::InvalidEncoding(format!(
399 "expected map length {expected}, got {actual}"
400 )));
401 }
402 Ok(())
403}
404
405fn expect_key(decoder: &mut Decoder<'_>, expected: u8) -> Result<(), MigrationCodecError> {
406 let actual = decoder.u8().map_err(decode_error)?;
407 if actual != expected {
408 return Err(MigrationCodecError::InvalidEncoding(format!(
409 "expected key {expected}, got {actual}"
410 )));
411 }
412 Ok(())
413}
414
415#[allow(clippy::needless_pass_by_value)]
416fn decode_error(error: minicbor::decode::Error) -> MigrationCodecError {
417 MigrationCodecError::InvalidEncoding(error.to_string())
418}