grc_20/codec/
op.rs

1//! Operation encoding/decoding for GRC-20 binary format.
2//!
3//! Implements the wire format for operations (spec Section 6.4).
4
5use crate::codec::primitives::{Reader, Writer};
6use crate::codec::value::{decode_position, decode_property_value, validate_position};
7use crate::error::{DecodeError, EncodeError};
8use crate::limits::MAX_VALUES_PER_ENTITY;
9use crate::model::{
10    CreateEntity, CreateRelation, CreateValueRef, DataType, DeleteEntity, DeleteRelation,
11    DictionaryBuilder, Op, PropertyValue, RestoreEntity, RestoreRelation,
12    UnsetLanguage, UnsetValue, UnsetRelationField, UpdateEntity, UpdateRelation, WireDictionaries,
13};
14
15// Op type constants (grouped by lifecycle: Create, Update, Delete, Restore)
16const OP_CREATE_ENTITY: u8 = 1;
17const OP_UPDATE_ENTITY: u8 = 2;
18const OP_DELETE_ENTITY: u8 = 3;
19const OP_RESTORE_ENTITY: u8 = 4;
20const OP_CREATE_RELATION: u8 = 5;
21const OP_UPDATE_RELATION: u8 = 6;
22const OP_DELETE_RELATION: u8 = 7;
23const OP_RESTORE_RELATION: u8 = 8;
24const OP_CREATE_VALUE_REF: u8 = 9;
25
26// UpdateEntity flags
27const FLAG_HAS_SET_PROPERTIES: u8 = 0x01;
28const FLAG_HAS_UNSET_VALUES: u8 = 0x02;
29const UPDATE_ENTITY_RESERVED_MASK: u8 = 0xFC;
30
31// CreateRelation flags (bit order matches field order in spec Section 6.4)
32const FLAG_HAS_FROM_SPACE: u8 = 0x01;
33const FLAG_HAS_FROM_VERSION: u8 = 0x02;
34const FLAG_HAS_TO_SPACE: u8 = 0x04;
35const FLAG_HAS_TO_VERSION: u8 = 0x08;
36const FLAG_HAS_ENTITY: u8 = 0x10;
37const FLAG_HAS_POSITION: u8 = 0x20;
38const FLAG_FROM_IS_VALUE_REF: u8 = 0x40;
39const FLAG_TO_IS_VALUE_REF: u8 = 0x80;
40
41// CreateValueRef flags
42const FLAG_HAS_LANGUAGE: u8 = 0x01;
43const FLAG_HAS_SPACE: u8 = 0x02;
44const CREATE_VALUE_REF_RESERVED_MASK: u8 = 0xFC;
45
46// Context reference sentinel value (no context)
47const NO_CONTEXT_REF: u32 = 0xFFFFFFFF;
48
49// UpdateRelation set flags (bit order matches field order in spec Section 6.4)
50const UPDATE_SET_FROM_SPACE: u8 = 0x01;
51const UPDATE_SET_FROM_VERSION: u8 = 0x02;
52const UPDATE_SET_TO_SPACE: u8 = 0x04;
53const UPDATE_SET_TO_VERSION: u8 = 0x08;
54const UPDATE_SET_POSITION: u8 = 0x10;
55const UPDATE_SET_RESERVED_MASK: u8 = 0xE0;
56
57// UpdateRelation unset flags
58const UPDATE_UNSET_FROM_SPACE: u8 = 0x01;
59const UPDATE_UNSET_FROM_VERSION: u8 = 0x02;
60const UPDATE_UNSET_TO_SPACE: u8 = 0x04;
61const UPDATE_UNSET_TO_VERSION: u8 = 0x08;
62const UPDATE_UNSET_POSITION: u8 = 0x10;
63const UPDATE_UNSET_RESERVED_MASK: u8 = 0xE0;
64
65// =============================================================================
66// DECODING
67// =============================================================================
68
69/// Decodes an Op from the reader (zero-copy).
70pub fn decode_op<'a>(reader: &mut Reader<'a>, dicts: &WireDictionaries) -> Result<Op<'a>, DecodeError> {
71    let op_type = reader.read_byte("op_type")?;
72
73    match op_type {
74        OP_CREATE_ENTITY => decode_create_entity(reader, dicts),
75        OP_UPDATE_ENTITY => decode_update_entity(reader, dicts),
76        OP_DELETE_ENTITY => decode_delete_entity(reader, dicts),
77        OP_RESTORE_ENTITY => decode_restore_entity(reader, dicts),
78        OP_CREATE_RELATION => decode_create_relation(reader, dicts),
79        OP_UPDATE_RELATION => decode_update_relation(reader, dicts),
80        OP_DELETE_RELATION => decode_delete_relation(reader, dicts),
81        OP_RESTORE_RELATION => decode_restore_relation(reader, dicts),
82        OP_CREATE_VALUE_REF => decode_create_value_ref(reader, dicts),
83        _ => Err(DecodeError::InvalidOpType { op_type }),
84    }
85}
86
87fn decode_create_entity<'a>(
88    reader: &mut Reader<'a>,
89    dicts: &WireDictionaries,
90) -> Result<Op<'a>, DecodeError> {
91    let id = reader.read_id("entity_id")?;
92    let value_count = reader.read_varint("value_count")? as usize;
93
94    if value_count > MAX_VALUES_PER_ENTITY {
95        return Err(DecodeError::LengthExceedsLimit {
96            field: "values",
97            len: value_count,
98            max: MAX_VALUES_PER_ENTITY,
99        });
100    }
101
102    let mut values = Vec::with_capacity(value_count);
103    for _ in 0..value_count {
104        values.push(decode_property_value(reader, dicts)?);
105    }
106
107    // Read context_ref: 0xFFFFFFFF = no context, else index into contexts[]
108    let context_ref_raw = reader.read_varint("context_ref")? as u32;
109    let context = if context_ref_raw == NO_CONTEXT_REF {
110        None
111    } else {
112        let idx = context_ref_raw as usize;
113        Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
114            dict: "contexts",
115            index: idx,
116            size: dicts.contexts.len(),
117        })?.clone())
118    };
119
120    Ok(Op::CreateEntity(CreateEntity { id, values, context }))
121}
122
123fn decode_update_entity<'a>(
124    reader: &mut Reader<'a>,
125    dicts: &WireDictionaries,
126) -> Result<Op<'a>, DecodeError> {
127    let id_index = reader.read_varint("entity_id")? as usize;
128    if id_index >= dicts.objects.len() {
129        return Err(DecodeError::IndexOutOfBounds {
130            dict: "objects",
131            index: id_index,
132            size: dicts.objects.len(),
133        });
134    }
135    let id = dicts.objects[id_index];
136
137    let flags = reader.read_byte("update_flags")?;
138
139    // Check reserved bits
140    if flags & UPDATE_ENTITY_RESERVED_MASK != 0 {
141        return Err(DecodeError::ReservedBitsSet {
142            context: "UpdateEntity flags",
143        });
144    }
145
146    let mut update = UpdateEntity::new(id);
147
148    if flags & FLAG_HAS_SET_PROPERTIES != 0 {
149        let count = reader.read_varint("set_properties_count")? as usize;
150        if count > MAX_VALUES_PER_ENTITY {
151            return Err(DecodeError::LengthExceedsLimit {
152                field: "set_properties",
153                len: count,
154                max: MAX_VALUES_PER_ENTITY,
155            });
156        }
157        for _ in 0..count {
158            update.set_properties.push(decode_property_value(reader, dicts)?);
159        }
160    }
161
162    if flags & FLAG_HAS_UNSET_VALUES != 0 {
163        let count = reader.read_varint("unset_values_count")? as usize;
164        if count > MAX_VALUES_PER_ENTITY {
165            return Err(DecodeError::LengthExceedsLimit {
166                field: "unset_values",
167                len: count,
168                max: MAX_VALUES_PER_ENTITY,
169            });
170        }
171        for _ in 0..count {
172            let prop_index = reader.read_varint("property")? as usize;
173            if prop_index >= dicts.properties.len() {
174                return Err(DecodeError::IndexOutOfBounds {
175                    dict: "properties",
176                    index: prop_index,
177                    size: dicts.properties.len(),
178                });
179            }
180            let property = dicts.properties[prop_index].0;
181
182            // Language encoding: 0xFFFFFFFF = all, 0 = English, 1+ = specific language
183            let lang_value = reader.read_varint("unset.language")? as u32;
184            let language = if lang_value == 0xFFFFFFFF {
185                UnsetLanguage::All
186            } else if lang_value == 0 {
187                UnsetLanguage::English
188            } else {
189                let idx = (lang_value - 1) as usize;
190                if idx >= dicts.languages.len() {
191                    return Err(DecodeError::IndexOutOfBounds {
192                        dict: "languages",
193                        index: lang_value as usize,
194                        size: dicts.languages.len() + 1,
195                    });
196                }
197                UnsetLanguage::Specific(dicts.languages[idx])
198            };
199
200            update.unset_values.push(UnsetValue { property, language });
201        }
202    }
203
204    // Read context_ref: 0xFFFFFFFF = no context, else index into contexts[]
205    let context_ref_raw = reader.read_varint("context_ref")? as u32;
206    update.context = if context_ref_raw == NO_CONTEXT_REF {
207        None
208    } else {
209        let idx = context_ref_raw as usize;
210        Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
211            dict: "contexts",
212            index: idx,
213            size: dicts.contexts.len(),
214        })?.clone())
215    };
216
217    Ok(Op::UpdateEntity(update))
218}
219
220fn decode_delete_entity<'a>(
221    reader: &mut Reader<'a>,
222    dicts: &WireDictionaries,
223) -> Result<Op<'a>, DecodeError> {
224    let id_index = reader.read_varint("entity_id")? as usize;
225    if id_index >= dicts.objects.len() {
226        return Err(DecodeError::IndexOutOfBounds {
227            dict: "objects",
228            index: id_index,
229            size: dicts.objects.len(),
230        });
231    }
232    let id = dicts.objects[id_index];
233    Ok(Op::DeleteEntity(DeleteEntity { id }))
234}
235
236fn decode_restore_entity<'a>(
237    reader: &mut Reader<'a>,
238    dicts: &WireDictionaries,
239) -> Result<Op<'a>, DecodeError> {
240    let id_index = reader.read_varint("entity_id")? as usize;
241    if id_index >= dicts.objects.len() {
242        return Err(DecodeError::IndexOutOfBounds {
243            dict: "objects",
244            index: id_index,
245            size: dicts.objects.len(),
246        });
247    }
248    let id = dicts.objects[id_index];
249    Ok(Op::RestoreEntity(RestoreEntity { id }))
250}
251
252fn decode_create_relation<'a>(
253    reader: &mut Reader<'a>,
254    dicts: &WireDictionaries,
255) -> Result<Op<'a>, DecodeError> {
256    let id = reader.read_id("relation_id")?;
257
258    let type_index = reader.read_varint("relation_type")? as usize;
259    if type_index >= dicts.relation_types.len() {
260        return Err(DecodeError::IndexOutOfBounds {
261            dict: "relation_types",
262            index: type_index,
263            size: dicts.relation_types.len(),
264        });
265    }
266    let relation_type = dicts.relation_types[type_index];
267
268    let flags = reader.read_byte("relation_flags")?;
269    let from_is_value_ref = flags & FLAG_FROM_IS_VALUE_REF != 0;
270    let to_is_value_ref = flags & FLAG_TO_IS_VALUE_REF != 0;
271
272    // Read from endpoint: inline ID if value ref, otherwise ObjectRef
273    let from = if from_is_value_ref {
274        reader.read_id("from")?
275    } else {
276        let from_index = reader.read_varint("from")? as usize;
277        if from_index >= dicts.objects.len() {
278            return Err(DecodeError::IndexOutOfBounds {
279                dict: "objects",
280                index: from_index,
281                size: dicts.objects.len(),
282            });
283        }
284        dicts.objects[from_index]
285    };
286
287    // Read to endpoint: inline ID if value ref, otherwise ObjectRef
288    let to = if to_is_value_ref {
289        reader.read_id("to")?
290    } else {
291        let to_index = reader.read_varint("to")? as usize;
292        if to_index >= dicts.objects.len() {
293            return Err(DecodeError::IndexOutOfBounds {
294                dict: "objects",
295                index: to_index,
296                size: dicts.objects.len(),
297            });
298        }
299        dicts.objects[to_index]
300    };
301
302    // Read optional fields in spec order: from_space, from_version, to_space, to_version, entity, position
303    let from_space = if flags & FLAG_HAS_FROM_SPACE != 0 {
304        Some(reader.read_id("from_space")?)
305    } else {
306        None
307    };
308
309    let from_version = if flags & FLAG_HAS_FROM_VERSION != 0 {
310        Some(reader.read_id("from_version")?)
311    } else {
312        None
313    };
314
315    let to_space = if flags & FLAG_HAS_TO_SPACE != 0 {
316        Some(reader.read_id("to_space")?)
317    } else {
318        None
319    };
320
321    let to_version = if flags & FLAG_HAS_TO_VERSION != 0 {
322        Some(reader.read_id("to_version")?)
323    } else {
324        None
325    };
326
327    let entity = if flags & FLAG_HAS_ENTITY != 0 {
328        Some(reader.read_id("entity_id")?)
329    } else {
330        None
331    };
332
333    let position = if flags & FLAG_HAS_POSITION != 0 {
334        Some(decode_position(reader)?)
335    } else {
336        None
337    };
338
339    // Read context_ref: 0xFFFFFFFF = no context, else index into contexts[]
340    let context_ref_raw = reader.read_varint("context_ref")? as u32;
341    let context = if context_ref_raw == NO_CONTEXT_REF {
342        None
343    } else {
344        let idx = context_ref_raw as usize;
345        Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
346            dict: "contexts",
347            index: idx,
348            size: dicts.contexts.len(),
349        })?.clone())
350    };
351
352    Ok(Op::CreateRelation(CreateRelation {
353        id,
354        relation_type,
355        from,
356        from_is_value_ref,
357        to,
358        to_is_value_ref,
359        entity,
360        position,
361        from_space,
362        from_version,
363        to_space,
364        to_version,
365        context,
366    }))
367}
368
369fn decode_update_relation<'a>(
370    reader: &mut Reader<'a>,
371    dicts: &WireDictionaries,
372) -> Result<Op<'a>, DecodeError> {
373    let id_index = reader.read_varint("relation_id")? as usize;
374    if id_index >= dicts.objects.len() {
375        return Err(DecodeError::IndexOutOfBounds {
376            dict: "objects",
377            index: id_index,
378            size: dicts.objects.len(),
379        });
380    }
381    let id = dicts.objects[id_index];
382
383    let set_flags = reader.read_byte("set_flags")?;
384    let unset_flags = reader.read_byte("unset_flags")?;
385
386    // Check reserved bits
387    if set_flags & UPDATE_SET_RESERVED_MASK != 0 {
388        return Err(DecodeError::ReservedBitsSet {
389            context: "UpdateRelation set_flags",
390        });
391    }
392    if unset_flags & UPDATE_UNSET_RESERVED_MASK != 0 {
393        return Err(DecodeError::ReservedBitsSet {
394            context: "UpdateRelation unset_flags",
395        });
396    }
397
398    // Read set fields
399    let from_space = if set_flags & UPDATE_SET_FROM_SPACE != 0 {
400        Some(reader.read_id("from_space")?)
401    } else {
402        None
403    };
404
405    let from_version = if set_flags & UPDATE_SET_FROM_VERSION != 0 {
406        Some(reader.read_id("from_version")?)
407    } else {
408        None
409    };
410
411    let to_space = if set_flags & UPDATE_SET_TO_SPACE != 0 {
412        Some(reader.read_id("to_space")?)
413    } else {
414        None
415    };
416
417    let to_version = if set_flags & UPDATE_SET_TO_VERSION != 0 {
418        Some(reader.read_id("to_version")?)
419    } else {
420        None
421    };
422
423    let position = if set_flags & UPDATE_SET_POSITION != 0 {
424        Some(decode_position(reader)?)
425    } else {
426        None
427    };
428
429    // Build unset list
430    let mut unset = Vec::new();
431    if unset_flags & UPDATE_UNSET_FROM_SPACE != 0 {
432        unset.push(UnsetRelationField::FromSpace);
433    }
434    if unset_flags & UPDATE_UNSET_FROM_VERSION != 0 {
435        unset.push(UnsetRelationField::FromVersion);
436    }
437    if unset_flags & UPDATE_UNSET_TO_SPACE != 0 {
438        unset.push(UnsetRelationField::ToSpace);
439    }
440    if unset_flags & UPDATE_UNSET_TO_VERSION != 0 {
441        unset.push(UnsetRelationField::ToVersion);
442    }
443    if unset_flags & UPDATE_UNSET_POSITION != 0 {
444        unset.push(UnsetRelationField::Position);
445    }
446
447    // Read context_ref: 0xFFFFFFFF = no context, else index into contexts[]
448    let context_ref_raw = reader.read_varint("context_ref")? as u32;
449    let context = if context_ref_raw == NO_CONTEXT_REF {
450        None
451    } else {
452        let idx = context_ref_raw as usize;
453        Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
454            dict: "contexts",
455            index: idx,
456            size: dicts.contexts.len(),
457        })?.clone())
458    };
459
460    Ok(Op::UpdateRelation(UpdateRelation {
461        id,
462        from_space,
463        from_version,
464        to_space,
465        to_version,
466        position,
467        unset,
468        context,
469    }))
470}
471
472fn decode_delete_relation<'a>(
473    reader: &mut Reader<'a>,
474    dicts: &WireDictionaries,
475) -> Result<Op<'a>, DecodeError> {
476    let id_index = reader.read_varint("relation_id")? as usize;
477    if id_index >= dicts.objects.len() {
478        return Err(DecodeError::IndexOutOfBounds {
479            dict: "objects",
480            index: id_index,
481            size: dicts.objects.len(),
482        });
483    }
484    let id = dicts.objects[id_index];
485    Ok(Op::DeleteRelation(DeleteRelation { id }))
486}
487
488fn decode_restore_relation<'a>(
489    reader: &mut Reader<'a>,
490    dicts: &WireDictionaries,
491) -> Result<Op<'a>, DecodeError> {
492    let id_index = reader.read_varint("relation_id")? as usize;
493    if id_index >= dicts.objects.len() {
494        return Err(DecodeError::IndexOutOfBounds {
495            dict: "objects",
496            index: id_index,
497            size: dicts.objects.len(),
498        });
499    }
500    let id = dicts.objects[id_index];
501    Ok(Op::RestoreRelation(RestoreRelation { id }))
502}
503
504fn decode_create_value_ref<'a>(
505    reader: &mut Reader<'a>,
506    dicts: &WireDictionaries,
507) -> Result<Op<'a>, DecodeError> {
508    let id = reader.read_id("value_ref_id")?;
509
510    let entity_index = reader.read_varint("entity")? as usize;
511    if entity_index >= dicts.objects.len() {
512        return Err(DecodeError::IndexOutOfBounds {
513            dict: "objects",
514            index: entity_index,
515            size: dicts.objects.len(),
516        });
517    }
518    let entity = dicts.objects[entity_index];
519
520    let property_index = reader.read_varint("property")? as usize;
521    if property_index >= dicts.properties.len() {
522        return Err(DecodeError::IndexOutOfBounds {
523            dict: "properties",
524            index: property_index,
525            size: dicts.properties.len(),
526        });
527    }
528    let property = dicts.properties[property_index].0;
529    let data_type = dicts.properties[property_index].1;
530
531    let flags = reader.read_byte("value_ref_flags")?;
532
533    // Check reserved bits
534    if flags & CREATE_VALUE_REF_RESERVED_MASK != 0 {
535        return Err(DecodeError::ReservedBitsSet {
536            context: "CreateValueRef flags",
537        });
538    }
539
540    let language = if flags & FLAG_HAS_LANGUAGE != 0 {
541        // Validate: language is only allowed for TEXT properties
542        if data_type != DataType::Text {
543            return Err(DecodeError::MalformedEncoding {
544                context: "CreateValueRef has_language=1 but property DataType is not TEXT",
545            });
546        }
547        let lang_index = reader.read_varint("language")? as usize;
548        // Language index 0 = English, 1+ = language_ids[index-1]
549        if lang_index == 0 {
550            None // English (no explicit ID needed)
551        } else {
552            let idx = lang_index - 1;
553            if idx >= dicts.languages.len() {
554                return Err(DecodeError::IndexOutOfBounds {
555                    dict: "languages",
556                    index: lang_index,
557                    size: dicts.languages.len() + 1,
558                });
559            }
560            Some(dicts.languages[idx])
561        }
562    } else {
563        None
564    };
565
566    let space = if flags & FLAG_HAS_SPACE != 0 {
567        Some(reader.read_id("space")?)
568    } else {
569        None
570    };
571
572    Ok(Op::CreateValueRef(CreateValueRef {
573        id,
574        entity,
575        property,
576        language,
577        space,
578    }))
579}
580
581// =============================================================================
582// ENCODING
583// =============================================================================
584
585/// Encodes an Op to the writer.
586///
587/// Note: This function requires that the dictionary builder has already been
588/// populated with all IDs that will be referenced. Call `collect_op_ids` first.
589pub fn encode_op(
590    writer: &mut Writer,
591    op: &Op<'_>,
592    dict_builder: &mut DictionaryBuilder,
593    property_types: &rustc_hash::FxHashMap<crate::model::Id, DataType>,
594) -> Result<(), EncodeError> {
595    match op {
596        Op::CreateEntity(ce) => encode_create_entity(writer, ce, dict_builder, property_types),
597        Op::UpdateEntity(ue) => encode_update_entity(writer, ue, dict_builder, property_types),
598        Op::DeleteEntity(de) => encode_delete_entity(writer, de, dict_builder),
599        Op::RestoreEntity(re) => encode_restore_entity(writer, re, dict_builder),
600        Op::CreateRelation(cr) => encode_create_relation(writer, cr, dict_builder),
601        Op::UpdateRelation(ur) => encode_update_relation(writer, ur, dict_builder),
602        Op::DeleteRelation(dr) => encode_delete_relation(writer, dr, dict_builder),
603        Op::RestoreRelation(rr) => encode_restore_relation(writer, rr, dict_builder),
604        Op::CreateValueRef(cvr) => encode_create_value_ref(writer, cvr, dict_builder),
605    }
606}
607
608fn encode_create_entity(
609    writer: &mut Writer,
610    ce: &CreateEntity<'_>,
611    dict_builder: &mut DictionaryBuilder,
612    property_types: &rustc_hash::FxHashMap<crate::model::Id, DataType>,
613) -> Result<(), EncodeError> {
614    writer.write_byte(OP_CREATE_ENTITY);
615    writer.write_id(&ce.id);
616    writer.write_varint(ce.values.len() as u64);
617
618    for pv in &ce.values {
619        let data_type = property_types.get(&pv.property)
620            .copied()
621            .unwrap_or_else(|| pv.value.data_type());
622        encode_property_value(writer, pv, dict_builder, data_type)?;
623    }
624
625    // Write context_ref: 0xFFFFFFFF = no context, else index into contexts[]
626    let context_ref = match &ce.context {
627        Some(ctx) => dict_builder.add_context(ctx) as u32,
628        None => NO_CONTEXT_REF,
629    };
630    writer.write_varint(context_ref as u64);
631
632    Ok(())
633}
634
635fn encode_update_entity(
636    writer: &mut Writer,
637    ue: &UpdateEntity<'_>,
638    dict_builder: &mut DictionaryBuilder,
639    property_types: &rustc_hash::FxHashMap<crate::model::Id, DataType>,
640) -> Result<(), EncodeError> {
641    writer.write_byte(OP_UPDATE_ENTITY);
642
643    let id_index = dict_builder.add_object(ue.id);
644    writer.write_varint(id_index as u64);
645
646    let mut flags = 0u8;
647    if !ue.set_properties.is_empty() {
648        flags |= FLAG_HAS_SET_PROPERTIES;
649    }
650    if !ue.unset_values.is_empty() {
651        flags |= FLAG_HAS_UNSET_VALUES;
652    }
653    writer.write_byte(flags);
654
655    if !ue.set_properties.is_empty() {
656        writer.write_varint(ue.set_properties.len() as u64);
657        for pv in &ue.set_properties {
658            let data_type = property_types.get(&pv.property)
659                .copied()
660                .unwrap_or_else(|| pv.value.data_type());
661            encode_property_value(writer, pv, dict_builder, data_type)?;
662        }
663    }
664
665    if !ue.unset_values.is_empty() {
666        writer.write_varint(ue.unset_values.len() as u64);
667        for unset in &ue.unset_values {
668            // We need the data type to add to dictionary, use a placeholder
669            let idx = dict_builder.add_property(unset.property, DataType::Bool);
670            writer.write_varint(idx as u64);
671            // Language encoding: 0xFFFFFFFF = all, 0 = English, 1+ = specific language
672            let lang_value: u32 = match &unset.language {
673                UnsetLanguage::All => 0xFFFFFFFF,
674                UnsetLanguage::English => 0,
675                UnsetLanguage::Specific(lang_id) => {
676                    let lang_index = dict_builder.add_language(Some(*lang_id));
677                    lang_index as u32
678                }
679            };
680            writer.write_varint(lang_value as u64);
681        }
682    }
683
684    // Write context_ref: 0xFFFFFFFF = no context, else index into contexts[]
685    let context_ref = match &ue.context {
686        Some(ctx) => dict_builder.add_context(ctx) as u32,
687        None => NO_CONTEXT_REF,
688    };
689    writer.write_varint(context_ref as u64);
690
691    Ok(())
692}
693
694fn encode_delete_entity(
695    writer: &mut Writer,
696    de: &DeleteEntity,
697    dict_builder: &mut DictionaryBuilder,
698) -> Result<(), EncodeError> {
699    writer.write_byte(OP_DELETE_ENTITY);
700    let id_index = dict_builder.add_object(de.id);
701    writer.write_varint(id_index as u64);
702    Ok(())
703}
704
705fn encode_restore_entity(
706    writer: &mut Writer,
707    re: &RestoreEntity,
708    dict_builder: &mut DictionaryBuilder,
709) -> Result<(), EncodeError> {
710    writer.write_byte(OP_RESTORE_ENTITY);
711    let id_index = dict_builder.add_object(re.id);
712    writer.write_varint(id_index as u64);
713    Ok(())
714}
715
716fn encode_create_relation(
717    writer: &mut Writer,
718    cr: &CreateRelation<'_>,
719    dict_builder: &mut DictionaryBuilder,
720) -> Result<(), EncodeError> {
721    writer.write_byte(OP_CREATE_RELATION);
722    writer.write_id(&cr.id);
723
724    let type_index = dict_builder.add_relation_type(cr.relation_type);
725    writer.write_varint(type_index as u64);
726
727    // Build flags (bit order matches field order in spec Section 6.4)
728    let mut flags = 0u8;
729    if cr.from_space.is_some() {
730        flags |= FLAG_HAS_FROM_SPACE;
731    }
732    if cr.from_version.is_some() {
733        flags |= FLAG_HAS_FROM_VERSION;
734    }
735    if cr.to_space.is_some() {
736        flags |= FLAG_HAS_TO_SPACE;
737    }
738    if cr.to_version.is_some() {
739        flags |= FLAG_HAS_TO_VERSION;
740    }
741    if cr.entity.is_some() {
742        flags |= FLAG_HAS_ENTITY;
743    }
744    if cr.position.is_some() {
745        flags |= FLAG_HAS_POSITION;
746    }
747    if cr.from_is_value_ref {
748        flags |= FLAG_FROM_IS_VALUE_REF;
749    }
750    if cr.to_is_value_ref {
751        flags |= FLAG_TO_IS_VALUE_REF;
752    }
753    writer.write_byte(flags);
754
755    // Write from endpoint: inline ID if value ref, otherwise ObjectRef
756    if cr.from_is_value_ref {
757        writer.write_id(&cr.from);
758    } else {
759        let from_index = dict_builder.add_object(cr.from);
760        writer.write_varint(from_index as u64);
761    }
762
763    // Write to endpoint: inline ID if value ref, otherwise ObjectRef
764    if cr.to_is_value_ref {
765        writer.write_id(&cr.to);
766    } else {
767        let to_index = dict_builder.add_object(cr.to);
768        writer.write_varint(to_index as u64);
769    }
770
771    // Write optional fields in spec order: from_space, from_version, to_space, to_version, entity, position
772    if let Some(space) = &cr.from_space {
773        writer.write_id(space);
774    }
775
776    if let Some(version) = &cr.from_version {
777        writer.write_id(version);
778    }
779
780    if let Some(space) = &cr.to_space {
781        writer.write_id(space);
782    }
783
784    if let Some(version) = &cr.to_version {
785        writer.write_id(version);
786    }
787
788    if let Some(entity) = &cr.entity {
789        writer.write_id(entity);
790    }
791
792    if let Some(pos) = &cr.position {
793        validate_position(pos)?;
794        writer.write_string(pos);
795    }
796
797    // Write context_ref: 0xFFFFFFFF = no context, else index into contexts[]
798    let context_ref = match &cr.context {
799        Some(ctx) => dict_builder.add_context(ctx) as u32,
800        None => NO_CONTEXT_REF,
801    };
802    writer.write_varint(context_ref as u64);
803
804    Ok(())
805}
806
807fn encode_update_relation(
808    writer: &mut Writer,
809    ur: &UpdateRelation<'_>,
810    dict_builder: &mut DictionaryBuilder,
811) -> Result<(), EncodeError> {
812    writer.write_byte(OP_UPDATE_RELATION);
813
814    let id_index = dict_builder.add_object(ur.id);
815    writer.write_varint(id_index as u64);
816
817    // Build set flags
818    let mut set_flags = 0u8;
819    if ur.from_space.is_some() {
820        set_flags |= UPDATE_SET_FROM_SPACE;
821    }
822    if ur.from_version.is_some() {
823        set_flags |= UPDATE_SET_FROM_VERSION;
824    }
825    if ur.to_space.is_some() {
826        set_flags |= UPDATE_SET_TO_SPACE;
827    }
828    if ur.to_version.is_some() {
829        set_flags |= UPDATE_SET_TO_VERSION;
830    }
831    if ur.position.is_some() {
832        set_flags |= UPDATE_SET_POSITION;
833    }
834    writer.write_byte(set_flags);
835
836    // Build unset flags
837    let mut unset_flags = 0u8;
838    for field in &ur.unset {
839        match field {
840            UnsetRelationField::FromSpace => unset_flags |= UPDATE_UNSET_FROM_SPACE,
841            UnsetRelationField::FromVersion => unset_flags |= UPDATE_UNSET_FROM_VERSION,
842            UnsetRelationField::ToSpace => unset_flags |= UPDATE_UNSET_TO_SPACE,
843            UnsetRelationField::ToVersion => unset_flags |= UPDATE_UNSET_TO_VERSION,
844            UnsetRelationField::Position => unset_flags |= UPDATE_UNSET_POSITION,
845        }
846    }
847    writer.write_byte(unset_flags);
848
849    // Write set fields in order
850    if let Some(space) = &ur.from_space {
851        writer.write_id(space);
852    }
853    if let Some(version) = &ur.from_version {
854        writer.write_id(version);
855    }
856    if let Some(space) = &ur.to_space {
857        writer.write_id(space);
858    }
859    if let Some(version) = &ur.to_version {
860        writer.write_id(version);
861    }
862    if let Some(pos) = &ur.position {
863        validate_position(pos)?;
864        writer.write_string(pos);
865    }
866
867    // Write context_ref: 0xFFFFFFFF = no context, else index into contexts[]
868    let context_ref = match &ur.context {
869        Some(ctx) => dict_builder.add_context(ctx) as u32,
870        None => NO_CONTEXT_REF,
871    };
872    writer.write_varint(context_ref as u64);
873
874    Ok(())
875}
876
877fn encode_delete_relation(
878    writer: &mut Writer,
879    dr: &DeleteRelation,
880    dict_builder: &mut DictionaryBuilder,
881) -> Result<(), EncodeError> {
882    writer.write_byte(OP_DELETE_RELATION);
883    let id_index = dict_builder.add_object(dr.id);
884    writer.write_varint(id_index as u64);
885    Ok(())
886}
887
888fn encode_restore_relation(
889    writer: &mut Writer,
890    rr: &RestoreRelation,
891    dict_builder: &mut DictionaryBuilder,
892) -> Result<(), EncodeError> {
893    writer.write_byte(OP_RESTORE_RELATION);
894    let id_index = dict_builder.add_object(rr.id);
895    writer.write_varint(id_index as u64);
896    Ok(())
897}
898
899fn encode_create_value_ref(
900    writer: &mut Writer,
901    cvr: &CreateValueRef,
902    dict_builder: &mut DictionaryBuilder,
903) -> Result<(), EncodeError> {
904    writer.write_byte(OP_CREATE_VALUE_REF);
905    writer.write_id(&cvr.id);
906
907    let entity_index = dict_builder.add_object(cvr.entity);
908    writer.write_varint(entity_index as u64);
909
910    // For CreateValueRef, we need to add the property to the dictionary.
911    // Use DataType::Text as a placeholder if language is present, otherwise Bool.
912    // The actual data type will be determined by the property's declaration elsewhere.
913    let data_type = if cvr.language.is_some() { DataType::Text } else { DataType::Bool };
914    let property_index = dict_builder.add_property(cvr.property, data_type);
915    writer.write_varint(property_index as u64);
916
917    let mut flags = 0u8;
918    if cvr.language.is_some() {
919        flags |= FLAG_HAS_LANGUAGE;
920    }
921    if cvr.space.is_some() {
922        flags |= FLAG_HAS_SPACE;
923    }
924    writer.write_byte(flags);
925
926    if let Some(lang_id) = cvr.language {
927        let lang_index = dict_builder.add_language(Some(lang_id));
928        writer.write_varint(lang_index as u64);
929    }
930
931    if let Some(space) = &cvr.space {
932        writer.write_id(space);
933    }
934
935    Ok(())
936}
937
938fn encode_property_value(
939    writer: &mut Writer,
940    pv: &PropertyValue<'_>,
941    dict_builder: &mut DictionaryBuilder,
942    data_type: DataType,
943) -> Result<(), EncodeError> {
944    let prop_index = dict_builder.add_property(pv.property, data_type);
945    writer.write_varint(prop_index as u64);
946    crate::codec::value::encode_value(writer, &pv.value, dict_builder)?;
947    Ok(())
948}
949
950#[cfg(test)]
951mod tests {
952    use std::borrow::Cow;
953
954    use super::*;
955    use crate::model::Value;
956
957    #[test]
958    fn test_create_entity_roundtrip() {
959        let op = Op::CreateEntity(CreateEntity {
960            id: [1u8; 16],
961            values: vec![PropertyValue {
962                property: [2u8; 16],
963                value: Value::Text {
964                    value: Cow::Owned("test".to_string()),
965                    language: None,
966                },
967            }],
968            context: None,
969        });
970
971        let mut dict_builder = DictionaryBuilder::new();
972        let mut property_types = rustc_hash::FxHashMap::default();
973        property_types.insert([2u8; 16], DataType::Text);
974
975        let mut writer = Writer::new();
976        encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
977
978        let dicts = dict_builder.build();
979        let mut reader = Reader::new(writer.as_bytes());
980        let decoded = decode_op(&mut reader, &dicts).unwrap();
981
982        // Compare by extracting values since Cow::Owned vs Cow::Borrowed
983        match (&op, &decoded) {
984            (Op::CreateEntity(e1), Op::CreateEntity(e2)) => {
985                assert_eq!(e1.id, e2.id);
986                assert_eq!(e1.values.len(), e2.values.len());
987                for (v1, v2) in e1.values.iter().zip(e2.values.iter()) {
988                    assert_eq!(v1.property, v2.property);
989                    match (&v1.value, &v2.value) {
990                        (Value::Text { value: s1, language: l1 }, Value::Text { value: s2, language: l2 }) => {
991                            assert_eq!(s1.as_ref(), s2.as_ref());
992                            assert_eq!(l1, l2);
993                        }
994                        _ => panic!("expected Text values"),
995                    }
996                }
997            }
998            _ => panic!("expected CreateEntity"),
999        }
1000    }
1001
1002    #[test]
1003    fn test_create_relation_roundtrip() {
1004        // Test with explicit entity
1005        let op = Op::CreateRelation(CreateRelation {
1006            id: [10u8; 16],
1007            relation_type: [1u8; 16],
1008            from: [2u8; 16],
1009            from_is_value_ref: false,
1010            to: [3u8; 16],
1011            to_is_value_ref: false,
1012            entity: Some([4u8; 16]),
1013            position: Some(Cow::Owned("abc".to_string())),
1014            from_space: None,
1015            from_version: None,
1016            to_space: None,
1017            to_version: None,
1018            context: None,
1019        });
1020
1021        let mut dict_builder = DictionaryBuilder::new();
1022        let property_types = rustc_hash::FxHashMap::default();
1023
1024        let mut writer = Writer::new();
1025        encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
1026
1027        let dicts = dict_builder.build();
1028        let mut reader = Reader::new(writer.as_bytes());
1029        let decoded = decode_op(&mut reader, &dicts).unwrap();
1030
1031        // Compare by extracting values
1032        match (&op, &decoded) {
1033            (Op::CreateRelation(r1), Op::CreateRelation(r2)) => {
1034                assert_eq!(r1.id, r2.id);
1035                assert_eq!(r1.relation_type, r2.relation_type);
1036                assert_eq!(r1.from, r2.from);
1037                assert_eq!(r1.from_is_value_ref, r2.from_is_value_ref);
1038                assert_eq!(r1.to, r2.to);
1039                assert_eq!(r1.to_is_value_ref, r2.to_is_value_ref);
1040                assert_eq!(r1.entity, r2.entity);
1041                match (&r1.position, &r2.position) {
1042                    (Some(p1), Some(p2)) => assert_eq!(p1.as_ref(), p2.as_ref()),
1043                    (None, None) => {}
1044                    _ => panic!("position mismatch"),
1045                }
1046            }
1047            _ => panic!("expected CreateRelation"),
1048        }
1049    }
1050
1051    #[test]
1052    fn test_create_relation_auto_entity_roundtrip() {
1053        // Test with auto-derived entity (entity = None)
1054        let op = Op::CreateRelation(CreateRelation {
1055            id: [10u8; 16],
1056            relation_type: [1u8; 16],
1057            from: [2u8; 16],
1058            from_is_value_ref: false,
1059            to: [3u8; 16],
1060            to_is_value_ref: false,
1061            entity: None,
1062            position: Some(Cow::Owned("abc".to_string())),
1063            from_space: None,
1064            from_version: None,
1065            to_space: None,
1066            to_version: None,
1067            context: None,
1068        });
1069
1070        let mut dict_builder = DictionaryBuilder::new();
1071        let property_types = rustc_hash::FxHashMap::default();
1072
1073        let mut writer = Writer::new();
1074        encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
1075
1076        let dicts = dict_builder.build();
1077        let mut reader = Reader::new(writer.as_bytes());
1078        let decoded = decode_op(&mut reader, &dicts).unwrap();
1079
1080        match (&op, &decoded) {
1081            (Op::CreateRelation(r1), Op::CreateRelation(r2)) => {
1082                assert_eq!(r1.id, r2.id);
1083                assert_eq!(r1.relation_type, r2.relation_type);
1084                assert_eq!(r1.from, r2.from);
1085                assert_eq!(r1.from_is_value_ref, r2.from_is_value_ref);
1086                assert_eq!(r1.to, r2.to);
1087                assert_eq!(r1.to_is_value_ref, r2.to_is_value_ref);
1088                assert_eq!(r1.entity, r2.entity);
1089                assert!(r1.entity.is_none());
1090                assert!(r2.entity.is_none());
1091            }
1092            _ => panic!("expected CreateRelation"),
1093        }
1094    }
1095
1096    #[test]
1097    fn test_create_relation_with_versions() {
1098        let op = Op::CreateRelation(CreateRelation {
1099            id: [10u8; 16],
1100            relation_type: [1u8; 16],
1101            from: [2u8; 16],
1102            from_is_value_ref: false,
1103            to: [3u8; 16],
1104            to_is_value_ref: false,
1105            entity: Some([4u8; 16]),
1106            position: Some(Cow::Owned("abc".to_string())),
1107            from_space: Some([5u8; 16]),
1108            from_version: Some([6u8; 16]),
1109            to_space: Some([7u8; 16]),
1110            to_version: Some([8u8; 16]),
1111            context: None,
1112        });
1113
1114        let mut dict_builder = DictionaryBuilder::new();
1115        let property_types = rustc_hash::FxHashMap::default();
1116
1117        let mut writer = Writer::new();
1118        encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
1119
1120        let dicts = dict_builder.build();
1121        let mut reader = Reader::new(writer.as_bytes());
1122        let decoded = decode_op(&mut reader, &dicts).unwrap();
1123
1124        match (&op, &decoded) {
1125            (Op::CreateRelation(r1), Op::CreateRelation(r2)) => {
1126                assert_eq!(r1.id, r2.id);
1127                assert_eq!(r1.relation_type, r2.relation_type);
1128                assert_eq!(r1.from, r2.from);
1129                assert_eq!(r1.from_is_value_ref, r2.from_is_value_ref);
1130                assert_eq!(r1.to, r2.to);
1131                assert_eq!(r1.to_is_value_ref, r2.to_is_value_ref);
1132                assert_eq!(r1.entity, r2.entity);
1133                assert_eq!(r1.from_space, r2.from_space);
1134                assert_eq!(r1.from_version, r2.from_version);
1135                assert_eq!(r1.to_space, r2.to_space);
1136                assert_eq!(r1.to_version, r2.to_version);
1137            }
1138            _ => panic!("expected CreateRelation"),
1139        }
1140    }
1141
1142    #[test]
1143    fn test_create_relation_with_value_ref_endpoint() {
1144        // Test with to endpoint being a value ref (inline ID)
1145        let op = Op::CreateRelation(CreateRelation {
1146            id: [10u8; 16],
1147            relation_type: [1u8; 16],
1148            from: [2u8; 16],
1149            from_is_value_ref: false,
1150            to: [99u8; 16], // Value ref ID
1151            to_is_value_ref: true,
1152            entity: None,
1153            position: None,
1154            from_space: None,
1155            from_version: None,
1156            to_space: None,
1157            to_version: None,
1158            context: None,
1159        });
1160
1161        let mut dict_builder = DictionaryBuilder::new();
1162        let property_types = rustc_hash::FxHashMap::default();
1163
1164        let mut writer = Writer::new();
1165        encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
1166
1167        let dicts = dict_builder.build();
1168        let mut reader = Reader::new(writer.as_bytes());
1169        let decoded = decode_op(&mut reader, &dicts).unwrap();
1170
1171        match (&op, &decoded) {
1172            (Op::CreateRelation(r1), Op::CreateRelation(r2)) => {
1173                assert_eq!(r1.id, r2.id);
1174                assert_eq!(r1.from, r2.from);
1175                assert_eq!(r1.from_is_value_ref, r2.from_is_value_ref);
1176                assert!(!r1.from_is_value_ref);
1177                assert_eq!(r1.to, r2.to);
1178                assert_eq!(r1.to_is_value_ref, r2.to_is_value_ref);
1179                assert!(r1.to_is_value_ref);
1180            }
1181            _ => panic!("expected CreateRelation"),
1182        }
1183    }
1184
1185    #[test]
1186    fn test_create_value_ref_roundtrip() {
1187        let op = Op::CreateValueRef(CreateValueRef {
1188            id: [1u8; 16],
1189            entity: [2u8; 16],
1190            property: [3u8; 16],
1191            language: None,
1192            space: None,
1193        });
1194
1195        let mut dict_builder = DictionaryBuilder::new();
1196        let property_types = rustc_hash::FxHashMap::default();
1197
1198        let mut writer = Writer::new();
1199        encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
1200
1201        let dicts = dict_builder.build();
1202        let mut reader = Reader::new(writer.as_bytes());
1203        let decoded = decode_op(&mut reader, &dicts).unwrap();
1204
1205        match (&op, &decoded) {
1206            (Op::CreateValueRef(v1), Op::CreateValueRef(v2)) => {
1207                assert_eq!(v1.id, v2.id);
1208                assert_eq!(v1.entity, v2.entity);
1209                assert_eq!(v1.property, v2.property);
1210                assert_eq!(v1.language, v2.language);
1211                assert_eq!(v1.space, v2.space);
1212            }
1213            _ => panic!("expected CreateValueRef"),
1214        }
1215    }
1216
1217    #[test]
1218    fn test_create_value_ref_with_language_and_space() {
1219        let op = Op::CreateValueRef(CreateValueRef {
1220            id: [1u8; 16],
1221            entity: [2u8; 16],
1222            property: [3u8; 16],
1223            language: Some([4u8; 16]),
1224            space: Some([5u8; 16]),
1225        });
1226
1227        let mut dict_builder = DictionaryBuilder::new();
1228        let property_types = rustc_hash::FxHashMap::default();
1229
1230        let mut writer = Writer::new();
1231        encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
1232
1233        let dicts = dict_builder.build();
1234        let mut reader = Reader::new(writer.as_bytes());
1235        let decoded = decode_op(&mut reader, &dicts).unwrap();
1236
1237        match (&op, &decoded) {
1238            (Op::CreateValueRef(v1), Op::CreateValueRef(v2)) => {
1239                assert_eq!(v1.id, v2.id);
1240                assert_eq!(v1.entity, v2.entity);
1241                assert_eq!(v1.property, v2.property);
1242                assert_eq!(v1.language, v2.language);
1243                assert_eq!(v1.space, v2.space);
1244            }
1245            _ => panic!("expected CreateValueRef"),
1246        }
1247    }
1248
1249    #[test]
1250    fn test_update_relation_roundtrip() {
1251        // Test with all set fields
1252        let op = Op::UpdateRelation(UpdateRelation {
1253            id: [1u8; 16],
1254            from_space: Some([2u8; 16]),
1255            from_version: Some([3u8; 16]),
1256            to_space: Some([4u8; 16]),
1257            to_version: Some([5u8; 16]),
1258            position: Some(Cow::Owned("xyz".to_string())),
1259            unset: vec![],
1260            context: None,
1261        });
1262
1263        let mut dict_builder = DictionaryBuilder::new();
1264        dict_builder.add_object([1u8; 16]); // Pre-add the relation ID
1265        let property_types = rustc_hash::FxHashMap::default();
1266
1267        let mut writer = Writer::new();
1268        encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
1269
1270        let dicts = dict_builder.build();
1271        let mut reader = Reader::new(writer.as_bytes());
1272        let decoded = decode_op(&mut reader, &dicts).unwrap();
1273
1274        match (&op, &decoded) {
1275            (Op::UpdateRelation(r1), Op::UpdateRelation(r2)) => {
1276                assert_eq!(r1.id, r2.id);
1277                assert_eq!(r1.from_space, r2.from_space);
1278                assert_eq!(r1.from_version, r2.from_version);
1279                assert_eq!(r1.to_space, r2.to_space);
1280                assert_eq!(r1.to_version, r2.to_version);
1281                match (&r1.position, &r2.position) {
1282                    (Some(p1), Some(p2)) => assert_eq!(p1.as_ref(), p2.as_ref()),
1283                    (None, None) => {}
1284                    _ => panic!("position mismatch"),
1285                }
1286                assert_eq!(r1.unset, r2.unset);
1287            }
1288            _ => panic!("expected UpdateRelation"),
1289        }
1290    }
1291
1292    #[test]
1293    fn test_update_relation_with_unset() {
1294        // Test with unset fields
1295        let op = Op::UpdateRelation(UpdateRelation {
1296            id: [1u8; 16],
1297            from_space: None,
1298            from_version: None,
1299            to_space: None,
1300            to_version: None,
1301            position: None,
1302            unset: vec![
1303                UnsetRelationField::FromSpace,
1304                UnsetRelationField::ToVersion,
1305                UnsetRelationField::Position,
1306            ],
1307            context: None,
1308        });
1309
1310        let mut dict_builder = DictionaryBuilder::new();
1311        dict_builder.add_object([1u8; 16]); // Pre-add the relation ID
1312        let property_types = rustc_hash::FxHashMap::default();
1313
1314        let mut writer = Writer::new();
1315        encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
1316
1317        let dicts = dict_builder.build();
1318        let mut reader = Reader::new(writer.as_bytes());
1319        let decoded = decode_op(&mut reader, &dicts).unwrap();
1320
1321        match (&op, &decoded) {
1322            (Op::UpdateRelation(r1), Op::UpdateRelation(r2)) => {
1323                assert_eq!(r1.id, r2.id);
1324                // Check that unset fields are preserved (order may differ due to bit decoding)
1325                assert_eq!(r1.unset.len(), r2.unset.len());
1326                for field in &r1.unset {
1327                    assert!(r2.unset.contains(field), "missing unset field: {:?}", field);
1328                }
1329            }
1330            _ => panic!("expected UpdateRelation"),
1331        }
1332    }
1333
1334}