Skip to main content

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