grc_20/model/
edit.rs

1//! Edit structure for batched operations.
2//!
3//! Edits are standalone patches containing a batch of ops with metadata.
4
5use std::borrow::Cow;
6
7use rustc_hash::FxHashMap;
8
9use crate::codec::primitives::Writer;
10use crate::model::{DataType, Id, Op};
11
12/// An edge in a context path (spec Section 4.5).
13///
14/// Represents a step in the path from the root entity to the changed entity.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct ContextEdge {
17    /// The relation type ID for this edge (e.g., BLOCKS_ID).
18    pub type_id: Id,
19    /// The target entity ID at this edge.
20    pub to_entity_id: Id,
21}
22
23/// Context metadata for grouping changes (spec Section 4.5).
24///
25/// Provides the path from a root entity to the changed entity,
26/// enabling context-aware change grouping (e.g., grouping block changes
27/// under their parent entity).
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct Context {
30    /// The root entity for this context.
31    pub root_id: Id,
32    /// Path from root to the changed entity.
33    pub edges: Vec<ContextEdge>,
34}
35
36/// A batch of operations with metadata (spec Section 4.1).
37///
38/// Edits are standalone patches. They contain no parent references;
39/// ordering is provided by on-chain governance.
40#[derive(Debug, Clone, PartialEq)]
41pub struct Edit<'a> {
42    /// The edit's unique identifier.
43    pub id: Id,
44    /// Optional human-readable name.
45    pub name: Cow<'a, str>,
46    /// Author entity IDs.
47    pub authors: Vec<Id>,
48    /// Creation timestamp (metadata only, not used for conflict resolution).
49    pub created_at: i64,
50    /// Operations in this edit.
51    pub ops: Vec<Op<'a>>,
52}
53
54impl<'a> Edit<'a> {
55    /// Creates a new empty edit with the given ID.
56    pub fn new(id: Id) -> Self {
57        Self {
58            id,
59            name: Cow::Borrowed(""),
60            authors: Vec::new(),
61            created_at: 0,
62            ops: Vec::new(),
63        }
64    }
65
66    /// Creates a new empty edit with the given ID and name.
67    pub fn with_name(id: Id, name: impl Into<Cow<'a, str>>) -> Self {
68        Self {
69            id,
70            name: name.into(),
71            authors: Vec::new(),
72            created_at: 0,
73            ops: Vec::new(),
74        }
75    }
76}
77
78/// Wire-format dictionaries for encoding/decoding.
79///
80/// These dictionaries map between full IDs and compact indices
81/// within an edit.
82#[derive(Debug, Clone, Default)]
83pub struct WireDictionaries {
84    /// Properties dictionary: (ID, DataType) pairs.
85    pub properties: Vec<(Id, DataType)>,
86    /// Relation type IDs.
87    pub relation_types: Vec<Id>,
88    /// Language entity IDs for localized TEXT values.
89    pub languages: Vec<Id>,
90    /// Unit entity IDs for numerical values.
91    pub units: Vec<Id>,
92    /// Object IDs (entities and relations).
93    pub objects: Vec<Id>,
94    /// Context IDs (root_ids and edge to_entity_ids) - used during encoding/decoding.
95    pub context_ids: Vec<Id>,
96    /// Decoded contexts array - used by op decoders to resolve context_ref to Context.
97    pub contexts: Vec<Context>,
98}
99
100impl WireDictionaries {
101    /// Creates empty dictionaries.
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Looks up a property ID by index.
107    pub fn get_property(&self, index: usize) -> Option<&(Id, DataType)> {
108        self.properties.get(index)
109    }
110
111    /// Looks up a relation type ID by index.
112    pub fn get_relation_type(&self, index: usize) -> Option<&Id> {
113        self.relation_types.get(index)
114    }
115
116    /// Looks up a language ID by index.
117    ///
118    /// Index 0 means default (no language), returns None.
119    /// Index 1+ maps to languages[index-1].
120    pub fn get_language(&self, index: usize) -> Option<&Id> {
121        if index == 0 {
122            None
123        } else {
124            self.languages.get(index - 1)
125        }
126    }
127
128    /// Looks up a unit ID by index.
129    ///
130    /// Index 0 means no unit, returns None.
131    /// Index 1+ maps to units[index-1].
132    pub fn get_unit(&self, index: usize) -> Option<&Id> {
133        if index == 0 {
134            None
135        } else {
136            self.units.get(index - 1)
137        }
138    }
139
140    /// Looks up an object ID by index.
141    pub fn get_object(&self, index: usize) -> Option<&Id> {
142        self.objects.get(index)
143    }
144
145    /// Looks up a context ID by index.
146    pub fn get_context_id(&self, index: usize) -> Option<&Id> {
147        self.context_ids.get(index)
148    }
149
150    /// Looks up a context by index.
151    pub fn get_context(&self, index: usize) -> Option<&Context> {
152        self.contexts.get(index)
153    }
154}
155
156/// Builder for constructing wire dictionaries during encoding.
157///
158/// Uses FxHashMap for faster hashing of 16-byte IDs.
159#[derive(Debug, Clone, Default)]
160pub struct DictionaryBuilder {
161    properties: Vec<(Id, DataType)>,
162    property_indices: FxHashMap<Id, usize>,
163    relation_types: Vec<Id>,
164    relation_type_indices: FxHashMap<Id, usize>,
165    languages: Vec<Id>,
166    language_indices: FxHashMap<Id, usize>,
167    units: Vec<Id>,
168    unit_indices: FxHashMap<Id, usize>,
169    objects: Vec<Id>,
170    object_indices: FxHashMap<Id, usize>,
171    context_ids: Vec<Id>,
172    context_id_indices: FxHashMap<Id, usize>,
173    contexts: Vec<Context>,
174    context_indices: FxHashMap<Context, usize>,
175}
176
177impl DictionaryBuilder {
178    /// Creates a new empty builder.
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    /// Creates a new builder with pre-allocated capacity.
184    ///
185    /// `estimated_ops` is used to estimate dictionary sizes:
186    /// - properties: ~estimated_ops / 4 (entities average ~4 properties)
187    /// - relation_types: ~estimated_ops / 20 (fewer unique relation types)
188    /// - languages: 4 (typically few languages per edit)
189    /// - units: 4 (typically few units per edit)
190    /// - objects: ~estimated_ops / 2 (many ops reference existing objects)
191    /// - context_ids: 8 (typically few context IDs per edit)
192    /// - contexts: 4 (typically few unique contexts per edit)
193    pub fn with_capacity(estimated_ops: usize) -> Self {
194        let prop_cap = estimated_ops / 4 + 1;
195        let rel_cap = estimated_ops / 20 + 1;
196        let lang_cap = 4;
197        let unit_cap = 4;
198        let obj_cap = estimated_ops / 2 + 1;
199        let ctx_id_cap = 8;
200        let ctx_cap = 4;
201
202        Self {
203            properties: Vec::with_capacity(prop_cap),
204            property_indices: FxHashMap::with_capacity_and_hasher(prop_cap, Default::default()),
205            relation_types: Vec::with_capacity(rel_cap),
206            relation_type_indices: FxHashMap::with_capacity_and_hasher(rel_cap, Default::default()),
207            languages: Vec::with_capacity(lang_cap),
208            language_indices: FxHashMap::with_capacity_and_hasher(lang_cap, Default::default()),
209            units: Vec::with_capacity(unit_cap),
210            unit_indices: FxHashMap::with_capacity_and_hasher(unit_cap, Default::default()),
211            objects: Vec::with_capacity(obj_cap),
212            object_indices: FxHashMap::with_capacity_and_hasher(obj_cap, Default::default()),
213            context_ids: Vec::with_capacity(ctx_id_cap),
214            context_id_indices: FxHashMap::with_capacity_and_hasher(ctx_id_cap, Default::default()),
215            contexts: Vec::with_capacity(ctx_cap),
216            context_indices: FxHashMap::with_capacity_and_hasher(ctx_cap, Default::default()),
217        }
218    }
219
220    /// Adds or gets the index for a property.
221    pub fn add_property(&mut self, id: Id, data_type: DataType) -> usize {
222        if let Some(&idx) = self.property_indices.get(&id) {
223            idx
224        } else {
225            let idx = self.properties.len();
226            self.properties.push((id, data_type));
227            self.property_indices.insert(id, idx);
228            idx
229        }
230    }
231
232    /// Adds or gets the index for a relation type.
233    pub fn add_relation_type(&mut self, id: Id) -> usize {
234        if let Some(&idx) = self.relation_type_indices.get(&id) {
235            idx
236        } else {
237            let idx = self.relation_types.len();
238            self.relation_types.push(id);
239            self.relation_type_indices.insert(id, idx);
240            idx
241        }
242    }
243
244    /// Adds or gets the index for a language.
245    ///
246    /// Returns 0 for default (no language), 1+ for actual languages.
247    pub fn add_language(&mut self, id: Option<Id>) -> usize {
248        match id {
249            None => 0,
250            Some(lang_id) => {
251                if let Some(&idx) = self.language_indices.get(&lang_id) {
252                    idx + 1
253                } else {
254                    let idx = self.languages.len();
255                    self.languages.push(lang_id);
256                    self.language_indices.insert(lang_id, idx);
257                    idx + 1
258                }
259            }
260        }
261    }
262
263    /// Adds or gets the index for a unit.
264    ///
265    /// Returns 0 for no unit, 1+ for actual units.
266    pub fn add_unit(&mut self, id: Option<Id>) -> usize {
267        match id {
268            None => 0,
269            Some(unit_id) => {
270                if let Some(&idx) = self.unit_indices.get(&unit_id) {
271                    idx + 1
272                } else {
273                    let idx = self.units.len();
274                    self.units.push(unit_id);
275                    self.unit_indices.insert(unit_id, idx);
276                    idx + 1
277                }
278            }
279        }
280    }
281
282    /// Adds or gets the index for an object.
283    pub fn add_object(&mut self, id: Id) -> usize {
284        if let Some(&idx) = self.object_indices.get(&id) {
285            idx
286        } else {
287            let idx = self.objects.len();
288            self.objects.push(id);
289            self.object_indices.insert(id, idx);
290            idx
291        }
292    }
293
294    /// Adds or gets the index for a context ID.
295    pub fn add_context_id(&mut self, id: Id) -> usize {
296        if let Some(&idx) = self.context_id_indices.get(&id) {
297            idx
298        } else {
299            let idx = self.context_ids.len();
300            self.context_ids.push(id);
301            self.context_id_indices.insert(id, idx);
302            idx
303        }
304    }
305
306    /// Adds or gets the index for a context.
307    ///
308    /// If the context is new, registers all its IDs to the appropriate dictionaries:
309    /// - root_id and edge.to_entity_id go to context_ids dictionary
310    /// - edge.type_id goes to relation_types dictionary (it's a RelationTypeRef)
311    /// Returns the index into the contexts array.
312    pub fn add_context(&mut self, context: &Context) -> usize {
313        if let Some(&idx) = self.context_indices.get(context) {
314            idx
315        } else {
316            // Register all IDs in the context to appropriate dictionaries
317            self.add_context_id(context.root_id);
318            for edge in &context.edges {
319                // type_id is a relation type, not a context ID
320                self.add_relation_type(edge.type_id);
321                self.add_context_id(edge.to_entity_id);
322            }
323
324            // Add context to contexts array
325            let idx = self.contexts.len();
326            self.contexts.push(context.clone());
327            self.context_indices.insert(context.clone(), idx);
328            idx
329        }
330    }
331
332    /// Gets the index for an existing context (for encoding).
333    pub fn get_context_index(&self, context: &Context) -> Option<usize> {
334        self.context_indices.get(context).copied()
335    }
336
337    /// Builds the final wire dictionaries (consumes the builder).
338    pub fn build(self) -> WireDictionaries {
339        WireDictionaries {
340            properties: self.properties,
341            relation_types: self.relation_types,
342            languages: self.languages,
343            units: self.units,
344            objects: self.objects,
345            context_ids: self.context_ids,
346            contexts: self.contexts,
347        }
348    }
349
350    /// Returns a reference to wire dictionaries without consuming the builder.
351    /// This allows continued use of the builder for encoding while having the dictionaries.
352    pub fn as_wire_dicts(&self) -> WireDictionaries {
353        WireDictionaries {
354            properties: self.properties.clone(),
355            relation_types: self.relation_types.clone(),
356            languages: self.languages.clone(),
357            units: self.units.clone(),
358            objects: self.objects.clone(),
359            context_ids: self.context_ids.clone(),
360            contexts: self.contexts.clone(),
361        }
362    }
363
364    /// Gets the index for an existing property (for encoding).
365    pub fn get_property_index(&self, id: &Id) -> Option<usize> {
366        self.property_indices.get(id).copied()
367    }
368
369    /// Gets the index for an existing relation type (for encoding).
370    pub fn get_relation_type_index(&self, id: &Id) -> Option<usize> {
371        self.relation_type_indices.get(id).copied()
372    }
373
374    /// Gets the index for an existing language (for encoding).
375    /// Returns 0 for None, 1+ for existing languages.
376    pub fn get_language_index(&self, id: Option<&Id>) -> Option<usize> {
377        match id {
378            None => Some(0),
379            Some(lang_id) => self.language_indices.get(lang_id).map(|idx| idx + 1),
380        }
381    }
382
383    /// Gets the index for an existing object (for encoding).
384    pub fn get_object_index(&self, id: &Id) -> Option<usize> {
385        self.object_indices.get(id).copied()
386    }
387
388    /// Gets the index for an existing context ID (for encoding).
389    pub fn get_context_id_index(&self, id: &Id) -> Option<usize> {
390        self.context_id_indices.get(id).copied()
391    }
392
393    /// Writes the dictionaries directly to a writer (avoids cloning).
394    pub fn write_dictionaries(&self, writer: &mut Writer) {
395        // Properties: count + (id, data_type) pairs
396        writer.write_varint(self.properties.len() as u64);
397        for (id, data_type) in &self.properties {
398            writer.write_id(id);
399            writer.write_byte(*data_type as u8);
400        }
401
402        // Relation types
403        writer.write_id_vec(&self.relation_types);
404
405        // Languages
406        writer.write_id_vec(&self.languages);
407
408        // Units
409        writer.write_id_vec(&self.units);
410
411        // Objects
412        writer.write_id_vec(&self.objects);
413
414        // Context IDs
415        writer.write_id_vec(&self.context_ids);
416    }
417
418    /// Writes the contexts array to the writer.
419    ///
420    /// Each context is encoded as:
421    /// - root_id: varint (index into context_ids)
422    /// - edge_count: varint
423    /// - edges: for each edge: type_id (RelationTypeRef), to_entity_id (ContextRef)
424    pub fn write_contexts(&self, writer: &mut Writer) {
425        writer.write_varint(self.contexts.len() as u64);
426        for ctx in &self.contexts {
427            // Root ID as context_id index
428            let root_idx = self.context_id_indices.get(&ctx.root_id)
429                .copied()
430                .expect("context root_id must be in context_ids dictionary");
431            writer.write_varint(root_idx as u64);
432
433            // Edges
434            writer.write_varint(ctx.edges.len() as u64);
435            for edge in &ctx.edges {
436                // type_id is a RelationTypeRef (index into relation_types dictionary)
437                let type_idx = self.relation_type_indices.get(&edge.type_id)
438                    .copied()
439                    .expect("context edge type_id must be in relation_types dictionary");
440                // to_entity_id is a ContextRef (index into context_ids dictionary)
441                let to_idx = self.context_id_indices.get(&edge.to_entity_id)
442                    .copied()
443                    .expect("context edge to_entity_id must be in context_ids dictionary");
444                writer.write_varint(type_idx as u64);
445                writer.write_varint(to_idx as u64);
446            }
447        }
448    }
449
450    /// Converts this builder into a sorted canonical form.
451    ///
452    /// All dictionaries are sorted by ID bytes (lexicographic order),
453    /// and the index maps are rebuilt to reflect the new ordering.
454    ///
455    /// This is used for canonical encoding to ensure deterministic output.
456    pub fn into_sorted(self) -> Self {
457        // Sort properties by ID
458        let mut properties = self.properties;
459        properties.sort_by(|a, b| a.0.cmp(&b.0));
460        let property_indices: FxHashMap<Id, usize> = properties
461            .iter()
462            .enumerate()
463            .map(|(i, (id, _))| (*id, i))
464            .collect();
465
466        // Sort relation types by ID
467        let mut relation_types = self.relation_types;
468        relation_types.sort();
469        let relation_type_indices: FxHashMap<Id, usize> = relation_types
470            .iter()
471            .enumerate()
472            .map(|(i, id)| (*id, i))
473            .collect();
474
475        // Sort languages by ID
476        let mut languages = self.languages;
477        languages.sort();
478        let language_indices: FxHashMap<Id, usize> = languages
479            .iter()
480            .enumerate()
481            .map(|(i, id)| (*id, i))
482            .collect();
483
484        // Sort units by ID
485        let mut units = self.units;
486        units.sort();
487        let unit_indices: FxHashMap<Id, usize> = units
488            .iter()
489            .enumerate()
490            .map(|(i, id)| (*id, i))
491            .collect();
492
493        // Sort objects by ID
494        let mut objects = self.objects;
495        objects.sort();
496        let object_indices: FxHashMap<Id, usize> = objects
497            .iter()
498            .enumerate()
499            .map(|(i, id)| (*id, i))
500            .collect();
501
502        // Sort context IDs by ID
503        let mut context_ids = self.context_ids;
504        context_ids.sort();
505        let context_id_indices: FxHashMap<Id, usize> = context_ids
506            .iter()
507            .enumerate()
508            .map(|(i, id)| (*id, i))
509            .collect();
510
511        // Sort contexts by root_id, then by edges (canonically)
512        let mut contexts = self.contexts;
513        contexts.sort_by(|a, b| {
514            // First compare by root_id
515            match a.root_id.cmp(&b.root_id) {
516                std::cmp::Ordering::Equal => {
517                    // Then compare edges lexicographically
518                    let a_edges: Vec<_> = a.edges.iter().map(|e| (e.type_id, e.to_entity_id)).collect();
519                    let b_edges: Vec<_> = b.edges.iter().map(|e| (e.type_id, e.to_entity_id)).collect();
520                    a_edges.cmp(&b_edges)
521                }
522                other => other,
523            }
524        });
525        let context_indices: FxHashMap<Context, usize> = contexts
526            .iter()
527            .enumerate()
528            .map(|(i, ctx)| (ctx.clone(), i))
529            .collect();
530
531        Self {
532            properties,
533            property_indices,
534            relation_types,
535            relation_type_indices,
536            languages,
537            language_indices,
538            units,
539            unit_indices,
540            objects,
541            object_indices,
542            context_ids,
543            context_id_indices,
544            contexts,
545            context_indices,
546        }
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553
554    #[test]
555    fn test_edit_new() {
556        let id = [1u8; 16];
557        let edit = Edit::new(id);
558        assert_eq!(edit.id, id);
559        assert!(edit.name.is_empty());
560        assert!(edit.authors.is_empty());
561        assert!(edit.ops.is_empty());
562    }
563
564    #[test]
565    fn test_dictionary_builder() {
566        let mut builder = DictionaryBuilder::new();
567
568        let prop1 = [1u8; 16];
569        let prop2 = [2u8; 16];
570
571        // First add returns 0
572        assert_eq!(builder.add_property(prop1, DataType::Text), 0);
573        // Second add of same ID returns same index
574        assert_eq!(builder.add_property(prop1, DataType::Text), 0);
575        // Different ID gets new index
576        assert_eq!(builder.add_property(prop2, DataType::Int64), 1);
577
578        let dicts = builder.build();
579        assert_eq!(dicts.properties.len(), 2);
580        assert_eq!(dicts.properties[0], (prop1, DataType::Text));
581        assert_eq!(dicts.properties[1], (prop2, DataType::Int64));
582    }
583
584    #[test]
585    fn test_language_indexing() {
586        let mut builder = DictionaryBuilder::new();
587
588        let lang1 = [10u8; 16];
589        let lang2 = [20u8; 16];
590
591        // None returns 0
592        assert_eq!(builder.add_language(None), 0);
593        // First language returns 1
594        assert_eq!(builder.add_language(Some(lang1)), 1);
595        // Same language returns same index
596        assert_eq!(builder.add_language(Some(lang1)), 1);
597        // Different language returns 2
598        assert_eq!(builder.add_language(Some(lang2)), 2);
599
600        let dicts = builder.build();
601        assert_eq!(dicts.languages.len(), 2);
602
603        // get_language(0) returns None (default)
604        assert!(dicts.get_language(0).is_none());
605        // get_language(1) returns lang1
606        assert_eq!(dicts.get_language(1), Some(&lang1));
607        // get_language(2) returns lang2
608        assert_eq!(dicts.get_language(2), Some(&lang2));
609    }
610}