1use crate::hashing::HashDigest;
8use crate::type_schema::TypeSchemaRegistry;
9use sha2::{Digest, Sha256};
10use shape_value::NanTag;
11use shape_value::ValueWord;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15#[derive(Debug, Clone)]
27pub struct Delta {
28 pub changed: HashMap<String, ValueWord>,
30 pub removed: Vec<String>,
32}
33
34impl Delta {
35 pub fn empty() -> Self {
37 Self {
38 changed: HashMap::new(),
39 removed: Vec::new(),
40 }
41 }
42
43 pub fn is_empty(&self) -> bool {
45 self.changed.is_empty() && self.removed.is_empty()
46 }
47
48 pub fn change_count(&self) -> usize {
50 self.changed.len() + self.removed.len()
51 }
52
53 pub fn patch(
70 &self,
71 base: &ValueWord,
72 schemas: &TypeSchemaRegistry,
73 ) -> (ValueWord, Vec<String>) {
74 let mut rejected = Vec::new();
75 let validated = self.validated_delta(&mut rejected);
76 let result = patch_value(base, &validated, schemas);
77 (result, rejected)
78 }
79
80 fn validated_delta(&self, rejected: &mut Vec<String>) -> Delta {
83 let mut valid = Delta::empty();
84
85 for (path, value) in &self.changed {
86 if is_valid_delta_path(path) {
87 valid.changed.insert(path.clone(), value.clone());
88 } else {
89 rejected.push(path.clone());
90 }
91 }
92
93 for path in &self.removed {
94 if is_valid_delta_path(path) {
95 valid.removed.push(path.clone());
96 } else {
97 rejected.push(path.clone());
98 }
99 }
100
101 valid
102 }
103}
104
105fn is_valid_delta_path(path: &str) -> bool {
111 if path == "." {
113 return true;
114 }
115
116 if path.is_empty() {
117 return false;
118 }
119
120 if path.starts_with('[') {
122 return true;
123 }
124
125 if path.starts_with('.') || path.ends_with('.') {
127 return false;
128 }
129
130 if path.contains("..") {
132 return false;
133 }
134
135 true
136}
137
138pub fn content_hash_value(value: &ValueWord, schemas: &TypeSchemaRegistry) -> HashDigest {
148 let mut hasher = Sha256::new();
149 hash_value_into(&mut hasher, value, schemas);
150 let result = hasher.finalize();
151 let hex_str = result.iter().fold(String::with_capacity(64), |mut acc, b| {
152 use std::fmt::Write;
153 let _ = write!(acc, "{:02x}", b);
154 acc
155 });
156 HashDigest::from_hex(&hex_str)
157}
158
159fn hash_value_into(hasher: &mut Sha256, value: &ValueWord, schemas: &TypeSchemaRegistry) {
160 match value.tag() {
161 NanTag::F64 => {
162 hasher.update(b"f64:");
163 if let Some(f) = value.as_f64() {
164 hasher.update(f.to_le_bytes());
165 }
166 }
167 NanTag::I48 => {
168 hasher.update(b"i48:");
169 if let Some(i) = value.as_i64() {
170 hasher.update(i.to_le_bytes());
171 }
172 }
173 NanTag::Bool => {
174 hasher.update(b"bool:");
175 if let Some(b) = value.as_bool() {
176 hasher.update(if b { &[1u8] } else { &[0u8] });
177 }
178 }
179 NanTag::None => {
180 hasher.update(b"none");
181 }
182 NanTag::Unit => {
183 hasher.update(b"unit");
184 }
185 NanTag::Function => {
186 hasher.update(b"fn:");
187 hasher.update(value.raw_bits().to_le_bytes());
188 }
189 NanTag::ModuleFunction => {
190 hasher.update(b"modfn:");
191 hasher.update(value.raw_bits().to_le_bytes());
192 }
193 NanTag::Ref => {
194 hasher.update(b"ref:");
195 hasher.update(value.raw_bits().to_le_bytes());
196 }
197 NanTag::Heap => {
198 if let Some(s) = value.as_str() {
200 hasher.update(b"str:");
201 hasher.update((s.len() as u64).to_le_bytes());
202 hasher.update(s.as_bytes());
203 } else if let Some(view) = value.as_any_array() {
204 hasher.update(b"arr:");
205 hasher.update((view.len() as u64).to_le_bytes());
206 let arr = view.to_generic();
207 for elem in arr.iter() {
208 hash_value_into(hasher, elem, schemas);
209 }
210 } else if let Some((schema_id, slots, heap_mask)) = value.as_typed_object() {
211 hasher.update(b"obj:");
212 hasher.update(schema_id.to_le_bytes());
213 for (i, slot) in slots.iter().enumerate() {
214 let is_heap = (heap_mask >> i) & 1 == 1;
215 if is_heap {
216 let nb = slot.as_heap_nb();
217 hash_value_into(hasher, &nb, schemas);
218 } else {
219 hasher.update(b"slot:");
220 hasher.update(slot.raw().to_le_bytes());
221 }
222 }
223 } else {
224 hasher.update(b"heap:");
226 hasher.update(value.raw_bits().to_le_bytes());
227 }
228 }
229 }
230}
231
232pub fn diff_values(old: &ValueWord, new: &ValueWord, schemas: &TypeSchemaRegistry) -> Delta {
242 let mut delta = Delta::empty();
243 diff_recursive(old, new, "", schemas, &mut delta);
244 delta
245}
246
247fn make_path(prefix: &str, suffix: &str) -> String {
248 if prefix.is_empty() {
249 suffix.to_string()
250 } else {
251 format!("{}.{}", prefix, suffix)
252 }
253}
254
255fn root_path(prefix: &str) -> String {
256 if prefix.is_empty() {
257 ".".to_string()
258 } else {
259 prefix.to_string()
260 }
261}
262
263fn diff_recursive(
264 old: &ValueWord,
265 new: &ValueWord,
266 prefix: &str,
267 schemas: &TypeSchemaRegistry,
268 delta: &mut Delta,
269) {
270 if old.raw_bits() == new.raw_bits() {
272 return;
273 }
274
275 if old.tag() != new.tag() {
277 delta.changed.insert(root_path(prefix), new.clone());
278 return;
279 }
280
281 match old.tag() {
282 NanTag::Heap => {
283 if let (Some((old_sid, old_slots, old_hm)), Some((new_sid, new_slots, new_hm))) =
285 (old.as_typed_object(), new.as_typed_object())
286 {
287 if old_sid == new_sid {
288 let schema = schemas.get_by_id(old_sid as u32);
289 let min_len = old_slots.len().min(new_slots.len());
290
291 for i in 0..min_len {
292 let field_name = schema
293 .and_then(|s| s.fields.get(i).map(|f| f.name.as_str()))
294 .unwrap_or("?");
295 let field_path = make_path(prefix, field_name);
296
297 let old_is_heap = (old_hm >> i) & 1 == 1;
298 let new_is_heap = (new_hm >> i) & 1 == 1;
299
300 if old_is_heap && new_is_heap {
301 let old_nb = old_slots[i].as_heap_nb();
302 let new_nb = new_slots[i].as_heap_nb();
303 diff_recursive(&old_nb, &new_nb, &field_path, schemas, delta);
304 } else if old_slots[i].raw() != new_slots[i].raw()
305 || old_is_heap != new_is_heap
306 {
307 if new_is_heap {
309 delta.changed.insert(field_path, new_slots[i].as_heap_nb());
310 } else {
311 delta.changed.insert(field_path, unsafe {
312 ValueWord::clone_from_bits(new_slots[i].raw())
313 });
314 }
315 }
316 }
317
318 for i in old_slots.len()..new_slots.len() {
320 let field_name = schema
321 .and_then(|s| s.fields.get(i).map(|f| f.name.as_str()))
322 .unwrap_or("?");
323 let field_path = make_path(prefix, field_name);
324 let is_heap = (new_hm >> i) & 1 == 1;
325 if is_heap {
326 delta.changed.insert(field_path, new_slots[i].as_heap_nb());
327 } else {
328 delta.changed.insert(field_path, unsafe {
329 ValueWord::clone_from_bits(new_slots[i].raw())
330 });
331 }
332 }
333
334 for i in new_slots.len()..old_slots.len() {
336 let field_name = schema
337 .and_then(|s| s.fields.get(i).map(|f| f.name.as_str()))
338 .unwrap_or("?");
339 delta.removed.push(make_path(prefix, field_name));
340 }
341 return;
342 }
343 delta.changed.insert(root_path(prefix), new.clone());
345 return;
346 }
347
348 if let (Some(old_view), Some(new_view)) = (old.as_any_array(), new.as_any_array()) {
350 let old_arr = old_view.to_generic();
351 let new_arr = new_view.to_generic();
352 let min_len = old_arr.len().min(new_arr.len());
353
354 for i in 0..min_len {
355 let idx_path = if prefix.is_empty() {
356 format!("[{}]", i)
357 } else {
358 format!("{}.[{}]", prefix, i)
359 };
360 diff_recursive(&old_arr[i], &new_arr[i], &idx_path, schemas, delta);
361 }
362
363 for i in min_len..new_arr.len() {
364 let idx_path = if prefix.is_empty() {
365 format!("[{}]", i)
366 } else {
367 format!("{}.[{}]", prefix, i)
368 };
369 delta.changed.insert(idx_path, new_arr[i].clone());
370 }
371
372 for i in min_len..old_arr.len() {
373 let idx_path = if prefix.is_empty() {
374 format!("[{}]", i)
375 } else {
376 format!("{}.[{}]", prefix, i)
377 };
378 delta.removed.push(idx_path);
379 }
380 return;
381 }
382
383 if let (Some(old_data), Some(new_data)) =
385 (old.as_hashmap_data(), new.as_hashmap_data())
386 {
387 diff_hashmap(old_data, new_data, prefix, schemas, delta);
388 return;
389 }
390
391 if let (Some(old_s), Some(new_s)) = (old.as_str(), new.as_str()) {
393 if old_s != new_s {
394 delta.changed.insert(root_path(prefix), new.clone());
395 }
396 return;
397 }
398
399 delta.changed.insert(root_path(prefix), new.clone());
401 }
402
403 _ => {
404 delta.changed.insert(root_path(prefix), new.clone());
406 }
407 }
408}
409
410fn diff_hashmap(
420 old_data: &shape_value::HashMapData,
421 new_data: &shape_value::HashMapData,
422 prefix: &str,
423 schemas: &TypeSchemaRegistry,
424 delta: &mut Delta,
425) {
426 for (new_idx, new_key) in new_data.keys.iter().enumerate() {
429 let key_label = format_map_key(new_key);
430 let key_path = make_path(prefix, &key_label);
431
432 match old_data.find_key(new_key) {
433 Some(old_idx) => {
434 diff_recursive(
436 &old_data.values[old_idx],
437 &new_data.values[new_idx],
438 &key_path,
439 schemas,
440 delta,
441 );
442 }
443 None => {
444 delta
446 .changed
447 .insert(key_path, new_data.values[new_idx].clone());
448 }
449 }
450 }
451
452 for old_key in &old_data.keys {
454 if new_data.find_key(old_key).is_none() {
455 let key_label = format_map_key(old_key);
456 let key_path = make_path(prefix, &key_label);
457 delta.removed.push(key_path);
458 }
459 }
460}
461
462fn format_map_key(key: &ValueWord) -> String {
468 if let Some(s) = key.as_str() {
469 s.to_string()
470 } else if let Some(i) = key.as_i64() {
471 format!("{{{}}}", i)
472 } else if let Some(f) = key.as_f64() {
473 format!("{{{}}}", f)
474 } else if let Some(b) = key.as_bool() {
475 format!("{{{}}}", b)
476 } else {
477 format!("{{0x{:x}}}", key.raw_bits())
478 }
479}
480
481pub fn patch_value(base: &ValueWord, delta: &Delta, schemas: &TypeSchemaRegistry) -> ValueWord {
491 if delta.is_empty() {
492 return base.clone();
493 }
494
495 if let Some(root_val) = delta.changed.get(".") {
497 return root_val.clone();
498 }
499
500 if let Some((schema_id, slots, heap_mask)) = base.as_typed_object() {
502 let schema = schemas.get_by_id(schema_id as u32);
503 if let Some(schema) = schema {
504 let mut direct_changes: HashMap<String, ValueWord> = HashMap::new();
506 let mut nested_changes: HashMap<String, Delta> = HashMap::new();
507
508 for (path, value) in &delta.changed {
509 if let Some(dot_pos) = path.find('.') {
510 let top = &path[..dot_pos];
511 let rest = &path[dot_pos + 1..];
512 nested_changes
513 .entry(top.to_string())
514 .or_insert_with(Delta::empty)
515 .changed
516 .insert(rest.to_string(), value.clone());
517 } else {
518 direct_changes.insert(path.clone(), value.clone());
519 }
520 }
521
522 let mut _direct_removals: Vec<String> = Vec::new();
527 let mut nested_removals: HashMap<String, Delta> = HashMap::new();
528
529 for path in &delta.removed {
530 if let Some(dot_pos) = path.find('.') {
531 let top = &path[..dot_pos];
532 let rest = &path[dot_pos + 1..];
533 nested_removals
534 .entry(top.to_string())
535 .or_insert_with(Delta::empty)
536 .removed
537 .push(rest.to_string());
538 } else {
539 _direct_removals.push(path.clone());
540 }
541 }
542
543 for (top, mut removal_delta) in nested_removals {
545 let entry = nested_changes.entry(top).or_insert_with(Delta::empty);
546 entry.removed.append(&mut removal_delta.removed);
547 }
548
549 let mut new_slots: Vec<shape_value::ValueSlot> = Vec::with_capacity(slots.len());
551 for (i, slot) in slots.iter().enumerate() {
552 let is_heap = (heap_mask >> i) & 1 == 1;
553 if is_heap {
554 new_slots.push(unsafe { slot.clone_heap() });
555 } else {
556 new_slots.push(shape_value::ValueSlot::from_raw(slot.raw()));
557 }
558 }
559 let mut new_heap_mask = heap_mask;
560
561 for (path, new_val) in &direct_changes {
563 if let Some(field_idx_u16) = schema.field_index(path) {
564 let field_idx = field_idx_u16 as usize;
565 if field_idx < new_slots.len() {
566 if (new_heap_mask >> field_idx) & 1 == 1 {
568 unsafe {
569 new_slots[field_idx].drop_heap();
570 }
571 }
572
573 if new_val.is_heap() {
574 if let Some(hv) = new_val.as_heap_ref() {
575 new_slots[field_idx] =
576 shape_value::ValueSlot::from_heap(hv.clone());
577 new_heap_mask |= 1u64 << field_idx;
578 }
579 } else if let Some(f) = new_val.as_f64() {
580 new_slots[field_idx] = shape_value::ValueSlot::from_number(f);
581 new_heap_mask &= !(1u64 << field_idx);
582 } else if let Some(i) = new_val.as_i64() {
583 new_slots[field_idx] = shape_value::ValueSlot::from_int(i);
584 new_heap_mask &= !(1u64 << field_idx);
585 } else if let Some(b) = new_val.as_bool() {
586 new_slots[field_idx] = shape_value::ValueSlot::from_bool(b);
587 new_heap_mask &= !(1u64 << field_idx);
588 }
589 }
590 }
591 }
592
593 for (top_field, sub_delta) in &nested_changes {
595 if let Some(field_idx_u16) = schema.field_index(top_field) {
596 let field_idx = field_idx_u16 as usize;
597 if field_idx < new_slots.len() {
598 let is_heap = (new_heap_mask >> field_idx) & 1 == 1;
600 if is_heap {
601 let current_val = new_slots[field_idx].as_heap_nb();
602 let patched = patch_value(¤t_val, sub_delta, schemas);
604
605 unsafe {
607 new_slots[field_idx].drop_heap();
608 }
609
610 if patched.is_heap() {
612 if let Some(hv) = patched.as_heap_ref() {
613 new_slots[field_idx] =
614 shape_value::ValueSlot::from_heap(hv.clone());
615 new_heap_mask |= 1u64 << field_idx;
616 }
617 } else if let Some(f) = patched.as_f64() {
618 new_slots[field_idx] = shape_value::ValueSlot::from_number(f);
619 new_heap_mask &= !(1u64 << field_idx);
620 } else if let Some(i) = patched.as_i64() {
621 new_slots[field_idx] = shape_value::ValueSlot::from_int(i);
622 new_heap_mask &= !(1u64 << field_idx);
623 } else if let Some(b) = patched.as_bool() {
624 new_slots[field_idx] = shape_value::ValueSlot::from_bool(b);
625 new_heap_mask &= !(1u64 << field_idx);
626 }
627 }
628 }
629 }
630 }
631
632 use shape_value::HeapValue;
633 return ValueWord::from_heap_value(HeapValue::TypedObject {
634 schema_id,
635 slots: new_slots.into_boxed_slice(),
636 heap_mask: new_heap_mask,
637 });
638 }
639 }
640
641 if let Some(view) = base.as_any_array() {
643 let arr = view.to_generic();
644 let mut new_arr: Vec<ValueWord> = arr.to_vec();
645
646 let mut removal_indices: Vec<usize> = delta
648 .removed
649 .iter()
650 .filter_map(|path| parse_array_index(path))
651 .collect();
652 removal_indices.sort_unstable();
653 removal_indices.reverse();
654 for idx in removal_indices {
655 if idx < new_arr.len() {
656 new_arr.remove(idx);
657 }
658 }
659
660 for (path, new_val) in &delta.changed {
662 if let Some(idx) = parse_array_index(path) {
663 if idx < new_arr.len() {
664 new_arr[idx] = new_val.clone();
665 } else {
666 while new_arr.len() < idx {
667 new_arr.push(ValueWord::none());
668 }
669 new_arr.push(new_val.clone());
670 }
671 }
672 }
673
674 return ValueWord::from_array(Arc::new(new_arr));
675 }
676
677 if let Some(data) = base.as_hashmap_data() {
679 let mut new_keys = data.keys.clone();
680 let mut new_values = data.values.clone();
681
682 for path in &delta.removed {
684 let remove_idx = new_keys
686 .iter()
687 .position(|k| format_map_key(k) == *path);
688 if let Some(idx) = remove_idx {
689 new_keys.remove(idx);
690 new_values.remove(idx);
691 }
692 }
693
694 for (path, new_val) in &delta.changed {
696 let existing_idx = new_keys
699 .iter()
700 .position(|k| format_map_key(k) == *path);
701 if let Some(idx) = existing_idx {
702 new_values[idx] = new_val.clone();
703 } else {
704 new_keys.push(ValueWord::from_string(Arc::new(path.clone())));
706 new_values.push(new_val.clone());
707 }
708 }
709
710 return ValueWord::from_hashmap_pairs(new_keys, new_values);
711 }
712
713 base.clone()
715}
716
717fn parse_array_index(path: &str) -> Option<usize> {
719 let part = path.rsplit('.').next().unwrap_or(path);
720 if part.starts_with('[') && part.ends_with(']') {
721 part[1..part.len() - 1].parse().ok()
722 } else {
723 None
724 }
725}
726
727#[cfg(test)]
732mod tests {
733 use super::*;
734
735 #[test]
736 fn test_empty_delta() {
737 let delta = Delta::empty();
738 assert!(delta.is_empty());
739 assert_eq!(delta.change_count(), 0);
740 }
741
742 #[test]
743 fn test_diff_identical_primitives() {
744 let schemas = TypeSchemaRegistry::new();
745 let a = ValueWord::from_f64(42.0);
746 let b = ValueWord::from_f64(42.0);
747 let delta = diff_values(&a, &b, &schemas);
748 assert!(delta.is_empty());
749 }
750
751 #[test]
752 fn test_diff_different_primitives() {
753 let schemas = TypeSchemaRegistry::new();
754 let a = ValueWord::from_f64(42.0);
755 let b = ValueWord::from_f64(99.0);
756 let delta = diff_values(&a, &b, &schemas);
757 assert!(!delta.is_empty());
758 assert_eq!(delta.change_count(), 1);
759 assert!(delta.changed.contains_key("."));
760 }
761
762 #[test]
763 fn test_diff_arrays_same() {
764 let schemas = TypeSchemaRegistry::new();
765 let a = ValueWord::from_array(Arc::new(vec![
766 ValueWord::from_f64(1.0),
767 ValueWord::from_f64(2.0),
768 ]));
769 let b = ValueWord::from_array(Arc::new(vec![
770 ValueWord::from_f64(1.0),
771 ValueWord::from_f64(2.0),
772 ]));
773 let delta = diff_values(&a, &b, &schemas);
774 assert!(delta.is_empty());
776 }
777
778 #[test]
779 fn test_diff_arrays_element_changed() {
780 let schemas = TypeSchemaRegistry::new();
781 let a = ValueWord::from_array(Arc::new(vec![
782 ValueWord::from_f64(1.0),
783 ValueWord::from_f64(2.0),
784 ]));
785 let b = ValueWord::from_array(Arc::new(vec![
786 ValueWord::from_f64(1.0),
787 ValueWord::from_f64(99.0),
788 ]));
789 let delta = diff_values(&a, &b, &schemas);
790 assert_eq!(delta.change_count(), 1);
791 assert!(delta.changed.contains_key("[1]"));
792 }
793
794 #[test]
795 fn test_diff_arrays_element_added() {
796 let schemas = TypeSchemaRegistry::new();
797 let a = ValueWord::from_array(Arc::new(vec![ValueWord::from_f64(1.0)]));
798 let b = ValueWord::from_array(Arc::new(vec![
799 ValueWord::from_f64(1.0),
800 ValueWord::from_f64(2.0),
801 ]));
802 let delta = diff_values(&a, &b, &schemas);
803 assert_eq!(delta.changed.len(), 1);
804 assert!(delta.changed.contains_key("[1]"));
805 }
806
807 #[test]
808 fn test_diff_arrays_element_removed() {
809 let schemas = TypeSchemaRegistry::new();
810 let a = ValueWord::from_array(Arc::new(vec![
811 ValueWord::from_f64(1.0),
812 ValueWord::from_f64(2.0),
813 ]));
814 let b = ValueWord::from_array(Arc::new(vec![ValueWord::from_f64(1.0)]));
815 let delta = diff_values(&a, &b, &schemas);
816 assert_eq!(delta.removed.len(), 1);
817 assert!(delta.removed.contains(&"[1]".to_string()));
818 }
819
820 #[test]
821 fn test_patch_root_replacement() {
822 let schemas = TypeSchemaRegistry::new();
823 let base = ValueWord::from_f64(42.0);
824 let mut delta = Delta::empty();
825 delta
826 .changed
827 .insert(".".to_string(), ValueWord::from_f64(99.0));
828
829 let result = patch_value(&base, &delta, &schemas);
830 assert_eq!(result.as_f64(), Some(99.0));
831 }
832
833 #[test]
834 fn test_patch_array_element() {
835 let schemas = TypeSchemaRegistry::new();
836 let base = ValueWord::from_array(Arc::new(vec![
837 ValueWord::from_f64(1.0),
838 ValueWord::from_f64(2.0),
839 ]));
840 let mut delta = Delta::empty();
841 delta
842 .changed
843 .insert("[1]".to_string(), ValueWord::from_f64(99.0));
844
845 let result = patch_value(&base, &delta, &schemas);
846 let arr = result.as_any_array().unwrap().to_generic();
847 assert_eq!(arr[0].as_f64(), Some(1.0));
848 assert_eq!(arr[1].as_f64(), Some(99.0));
849 }
850
851 #[test]
852 fn test_parse_array_index() {
853 assert_eq!(parse_array_index("[0]"), Some(0));
854 assert_eq!(parse_array_index("[42]"), Some(42));
855 assert_eq!(parse_array_index("prefix.[3]"), Some(3));
856 assert_eq!(parse_array_index("notindex"), None);
857 }
858
859 #[test]
860 fn test_content_hash_deterministic() {
861 let schemas = TypeSchemaRegistry::new();
862 let v1 = ValueWord::from_f64(42.0);
863 let v2 = ValueWord::from_f64(42.0);
864 assert_eq!(
865 content_hash_value(&v1, &schemas),
866 content_hash_value(&v2, &schemas)
867 );
868 }
869
870 #[test]
871 fn test_content_hash_different() {
872 let schemas = TypeSchemaRegistry::new();
873 let v1 = ValueWord::from_f64(42.0);
874 let v2 = ValueWord::from_f64(99.0);
875 assert_ne!(
876 content_hash_value(&v1, &schemas),
877 content_hash_value(&v2, &schemas)
878 );
879 }
880
881 #[test]
882 fn test_nested_typed_object_diff_and_patch() {
883 use crate::type_schema::TypeSchemaBuilder;
884 use shape_value::{HeapValue, ValueSlot};
885
886 let mut schemas = TypeSchemaRegistry::new();
887
888 let inner_id = TypeSchemaBuilder::new("Inner")
890 .f64_field("x")
891 .f64_field("y")
892 .register(&mut schemas);
893
894 let outer_id = TypeSchemaBuilder::new("Outer")
896 .string_field("name")
897 .object_field("inner", "Inner")
898 .f64_field("score")
899 .register(&mut schemas);
900
901 let inner_old = ValueWord::from_heap_value(HeapValue::TypedObject {
903 schema_id: inner_id as u64,
904 slots: vec![
905 ValueSlot::from_number(1.0), ValueSlot::from_number(2.0), ]
908 .into_boxed_slice(),
909 heap_mask: 0,
910 });
911
912 let inner_new = ValueWord::from_heap_value(HeapValue::TypedObject {
913 schema_id: inner_id as u64,
914 slots: vec![
915 ValueSlot::from_number(1.0), ValueSlot::from_number(99.0), ]
918 .into_boxed_slice(),
919 heap_mask: 0,
920 });
921
922 let name_val = Arc::new("test".to_string());
924 let old_outer = ValueWord::from_heap_value(HeapValue::TypedObject {
925 schema_id: outer_id as u64,
926 slots: vec![
927 ValueSlot::from_heap(HeapValue::String(name_val.clone())), ValueSlot::from_heap(inner_old.as_heap_ref().unwrap().clone()), ValueSlot::from_number(10.0), ]
931 .into_boxed_slice(),
932 heap_mask: 0b011, });
934
935 let new_outer = ValueWord::from_heap_value(HeapValue::TypedObject {
936 schema_id: outer_id as u64,
937 slots: vec![
938 ValueSlot::from_heap(HeapValue::String(name_val.clone())), ValueSlot::from_heap(inner_new.as_heap_ref().unwrap().clone()), ValueSlot::from_number(10.0), ]
942 .into_boxed_slice(),
943 heap_mask: 0b011,
944 });
945
946 let delta = diff_values(&old_outer, &new_outer, &schemas);
948 assert!(!delta.is_empty(), "delta should not be empty");
949 assert!(
950 delta.changed.contains_key("inner.y"),
951 "delta should contain 'inner.y', got keys: {:?}",
952 delta.changed.keys().collect::<Vec<_>>()
953 );
954 assert_eq!(delta.change_count(), 1, "only inner.y should have changed");
955
956 let patched = patch_value(&old_outer, &delta, &schemas);
958
959 let (patched_sid, patched_slots, patched_hm) = patched
961 .as_typed_object()
962 .expect("patched should be a TypedObject");
963 assert_eq!(patched_sid, outer_id as u64);
964
965 assert_eq!(patched_hm & 1, 1, "slot 0 should be heap");
967 let patched_name = patched_slots[0].as_heap_nb();
968 assert_eq!(patched_name.as_str().unwrap(), "test");
969
970 assert_eq!(
972 f64::from_bits(patched_slots[2].raw()),
973 10.0,
974 "score should be 10.0"
975 );
976
977 assert_eq!((patched_hm >> 1) & 1, 1, "slot 1 should be heap");
979 let patched_inner = patched_slots[1].as_heap_nb();
980 let (inner_sid, inner_slots, _inner_hm) = patched_inner
981 .as_typed_object()
982 .expect("inner should be a TypedObject");
983 assert_eq!(inner_sid, inner_id as u64);
984 assert_eq!(
985 f64::from_bits(inner_slots[0].raw()),
986 1.0,
987 "inner.x should be 1.0"
988 );
989 assert_eq!(
990 f64::from_bits(inner_slots[1].raw()),
991 99.0,
992 "inner.y should be 99.0"
993 );
994 }
995
996 #[test]
997 fn test_patch_direct_fields_still_work() {
998 use crate::type_schema::TypeSchemaBuilder;
999 use shape_value::{HeapValue, ValueSlot};
1000
1001 let mut schemas = TypeSchemaRegistry::new();
1002
1003 let schema_id = TypeSchemaBuilder::new("Simple")
1004 .f64_field("a")
1005 .f64_field("b")
1006 .register(&mut schemas);
1007
1008 let base = ValueWord::from_heap_value(HeapValue::TypedObject {
1009 schema_id: schema_id as u64,
1010 slots: vec![ValueSlot::from_number(1.0), ValueSlot::from_number(2.0)]
1011 .into_boxed_slice(),
1012 heap_mask: 0,
1013 });
1014
1015 let mut delta = Delta::empty();
1017 delta
1018 .changed
1019 .insert("b".to_string(), ValueWord::from_f64(42.0));
1020
1021 let patched = patch_value(&base, &delta, &schemas);
1022 let (_sid, slots, _hm) = patched.as_typed_object().unwrap();
1023 assert_eq!(f64::from_bits(slots[0].raw()), 1.0, "a unchanged");
1024 assert_eq!(f64::from_bits(slots[1].raw()), 42.0, "b patched to 42.0");
1025 }
1026
1027 #[test]
1028 fn test_nested_patch_mixed_direct_and_dotted() {
1029 use crate::type_schema::TypeSchemaBuilder;
1030 use shape_value::{HeapValue, ValueSlot};
1031
1032 let mut schemas = TypeSchemaRegistry::new();
1033
1034 let inner_id = TypeSchemaBuilder::new("MixedInner")
1035 .f64_field("val")
1036 .register(&mut schemas);
1037
1038 let outer_id = TypeSchemaBuilder::new("MixedOuter")
1039 .f64_field("score")
1040 .object_field("nested", "MixedInner")
1041 .register(&mut schemas);
1042
1043 let inner_obj = ValueWord::from_heap_value(HeapValue::TypedObject {
1044 schema_id: inner_id as u64,
1045 slots: vec![ValueSlot::from_number(5.0)].into_boxed_slice(),
1046 heap_mask: 0,
1047 });
1048
1049 let base = ValueWord::from_heap_value(HeapValue::TypedObject {
1050 schema_id: outer_id as u64,
1051 slots: vec![
1052 ValueSlot::from_number(100.0),
1053 ValueSlot::from_heap(inner_obj.as_heap_ref().unwrap().clone()),
1054 ]
1055 .into_boxed_slice(),
1056 heap_mask: 0b10, });
1058
1059 let mut delta = Delta::empty();
1061 delta
1062 .changed
1063 .insert("score".to_string(), ValueWord::from_f64(200.0));
1064 delta
1065 .changed
1066 .insert("nested.val".to_string(), ValueWord::from_f64(77.0));
1067
1068 let patched = patch_value(&base, &delta, &schemas);
1069 let (_sid, slots, hm) = patched.as_typed_object().unwrap();
1070
1071 assert_eq!(
1073 f64::from_bits(slots[0].raw()),
1074 200.0,
1075 "score should be 200.0"
1076 );
1077
1078 assert_eq!((hm >> 1) & 1, 1, "slot 1 should be heap");
1080 let patched_inner = slots[1].as_heap_nb();
1081 let (_inner_sid, inner_slots, _) = patched_inner.as_typed_object().unwrap();
1082 assert_eq!(
1083 f64::from_bits(inner_slots[0].raw()),
1084 77.0,
1085 "nested.val should be 77.0"
1086 );
1087 }
1088
1089 #[test]
1092 fn test_diff_hashmaps_identical() {
1093 let schemas = TypeSchemaRegistry::new();
1094 let a = ValueWord::from_hashmap_pairs(
1095 vec![
1096 ValueWord::from_string(Arc::new("x".to_string())),
1097 ValueWord::from_string(Arc::new("y".to_string())),
1098 ],
1099 vec![ValueWord::from_f64(1.0), ValueWord::from_f64(2.0)],
1100 );
1101 let b = ValueWord::from_hashmap_pairs(
1102 vec![
1103 ValueWord::from_string(Arc::new("x".to_string())),
1104 ValueWord::from_string(Arc::new("y".to_string())),
1105 ],
1106 vec![ValueWord::from_f64(1.0), ValueWord::from_f64(2.0)],
1107 );
1108 let delta = diff_values(&a, &b, &schemas);
1109 assert!(delta.is_empty(), "identical hashmaps should produce empty delta");
1110 }
1111
1112 #[test]
1113 fn test_diff_hashmaps_value_changed() {
1114 let schemas = TypeSchemaRegistry::new();
1115 let a = ValueWord::from_hashmap_pairs(
1116 vec![
1117 ValueWord::from_string(Arc::new("x".to_string())),
1118 ValueWord::from_string(Arc::new("y".to_string())),
1119 ],
1120 vec![ValueWord::from_f64(1.0), ValueWord::from_f64(2.0)],
1121 );
1122 let b = ValueWord::from_hashmap_pairs(
1123 vec![
1124 ValueWord::from_string(Arc::new("x".to_string())),
1125 ValueWord::from_string(Arc::new("y".to_string())),
1126 ],
1127 vec![ValueWord::from_f64(1.0), ValueWord::from_f64(99.0)],
1128 );
1129 let delta = diff_values(&a, &b, &schemas);
1130 assert_eq!(delta.change_count(), 1);
1131 assert!(delta.changed.contains_key("y"));
1132 }
1133
1134 #[test]
1135 fn test_diff_hashmaps_key_added() {
1136 let schemas = TypeSchemaRegistry::new();
1137 let a = ValueWord::from_hashmap_pairs(
1138 vec![ValueWord::from_string(Arc::new("x".to_string()))],
1139 vec![ValueWord::from_f64(1.0)],
1140 );
1141 let b = ValueWord::from_hashmap_pairs(
1142 vec![
1143 ValueWord::from_string(Arc::new("x".to_string())),
1144 ValueWord::from_string(Arc::new("y".to_string())),
1145 ],
1146 vec![ValueWord::from_f64(1.0), ValueWord::from_f64(2.0)],
1147 );
1148 let delta = diff_values(&a, &b, &schemas);
1149 assert_eq!(delta.changed.len(), 1);
1150 assert!(delta.changed.contains_key("y"));
1151 assert!(delta.removed.is_empty());
1152 }
1153
1154 #[test]
1155 fn test_diff_hashmaps_key_removed() {
1156 let schemas = TypeSchemaRegistry::new();
1157 let a = ValueWord::from_hashmap_pairs(
1158 vec![
1159 ValueWord::from_string(Arc::new("x".to_string())),
1160 ValueWord::from_string(Arc::new("y".to_string())),
1161 ],
1162 vec![ValueWord::from_f64(1.0), ValueWord::from_f64(2.0)],
1163 );
1164 let b = ValueWord::from_hashmap_pairs(
1165 vec![ValueWord::from_string(Arc::new("x".to_string()))],
1166 vec![ValueWord::from_f64(1.0)],
1167 );
1168 let delta = diff_values(&a, &b, &schemas);
1169 assert!(delta.changed.is_empty());
1170 assert_eq!(delta.removed.len(), 1);
1171 assert!(delta.removed.contains(&"y".to_string()));
1172 }
1173
1174 #[test]
1175 fn test_diff_hashmaps_symmetric_difference() {
1176 let schemas = TypeSchemaRegistry::new();
1178 let a = ValueWord::from_hashmap_pairs(
1179 vec![
1180 ValueWord::from_string(Arc::new("a".to_string())),
1181 ValueWord::from_string(Arc::new("b".to_string())),
1182 ValueWord::from_string(Arc::new("c".to_string())),
1183 ],
1184 vec![
1185 ValueWord::from_f64(1.0),
1186 ValueWord::from_f64(2.0),
1187 ValueWord::from_f64(3.0),
1188 ],
1189 );
1190 let b = ValueWord::from_hashmap_pairs(
1191 vec![
1192 ValueWord::from_string(Arc::new("b".to_string())),
1193 ValueWord::from_string(Arc::new("c".to_string())),
1194 ValueWord::from_string(Arc::new("d".to_string())),
1195 ],
1196 vec![
1197 ValueWord::from_f64(2.0),
1198 ValueWord::from_f64(3.0),
1199 ValueWord::from_f64(4.0),
1200 ],
1201 );
1202 let delta = diff_values(&a, &b, &schemas);
1203 assert_eq!(delta.removed.len(), 1);
1205 assert!(delta.removed.contains(&"a".to_string()));
1206 assert_eq!(delta.changed.len(), 1);
1207 assert!(delta.changed.contains_key("d"));
1208 }
1209
1210 #[test]
1211 fn test_diff_hashmap_with_integer_keys() {
1212 let schemas = TypeSchemaRegistry::new();
1213 let a = ValueWord::from_hashmap_pairs(
1214 vec![ValueWord::from_i64(1), ValueWord::from_i64(2)],
1215 vec![
1216 ValueWord::from_string(Arc::new("one".to_string())),
1217 ValueWord::from_string(Arc::new("two".to_string())),
1218 ],
1219 );
1220 let b = ValueWord::from_hashmap_pairs(
1221 vec![ValueWord::from_i64(1), ValueWord::from_i64(2)],
1222 vec![
1223 ValueWord::from_string(Arc::new("one".to_string())),
1224 ValueWord::from_string(Arc::new("TWO".to_string())),
1225 ],
1226 );
1227 let delta = diff_values(&a, &b, &schemas);
1228 assert_eq!(delta.change_count(), 1);
1229 assert!(delta.changed.contains_key("{2}"));
1231 }
1232
1233 #[test]
1234 fn test_patch_hashmap_add_entry() {
1235 let schemas = TypeSchemaRegistry::new();
1236 let base = ValueWord::from_hashmap_pairs(
1237 vec![ValueWord::from_string(Arc::new("x".to_string()))],
1238 vec![ValueWord::from_f64(1.0)],
1239 );
1240 let mut delta = Delta::empty();
1241 delta
1242 .changed
1243 .insert("y".to_string(), ValueWord::from_f64(2.0));
1244
1245 let patched = patch_value(&base, &delta, &schemas);
1246 let data = patched.as_hashmap_data().expect("should be hashmap");
1247 assert_eq!(data.keys.len(), 2);
1248 }
1249
1250 #[test]
1251 fn test_patch_hashmap_remove_entry() {
1252 let schemas = TypeSchemaRegistry::new();
1253 let base = ValueWord::from_hashmap_pairs(
1254 vec![
1255 ValueWord::from_string(Arc::new("x".to_string())),
1256 ValueWord::from_string(Arc::new("y".to_string())),
1257 ],
1258 vec![ValueWord::from_f64(1.0), ValueWord::from_f64(2.0)],
1259 );
1260 let mut delta = Delta::empty();
1261 delta.removed.push("y".to_string());
1262
1263 let patched = patch_value(&base, &delta, &schemas);
1264 let data = patched.as_hashmap_data().expect("should be hashmap");
1265 assert_eq!(data.keys.len(), 1);
1266 assert!(data.find_key(&ValueWord::from_string(Arc::new("x".to_string()))).is_some());
1267 }
1268
1269 #[test]
1272 fn test_diff_nested_arrays_recursive() {
1273 let schemas = TypeSchemaRegistry::new();
1274 let inner1_old = ValueWord::from_array(Arc::new(vec![
1276 ValueWord::from_f64(1.0),
1277 ValueWord::from_f64(2.0),
1278 ]));
1279 let inner2 = ValueWord::from_array(Arc::new(vec![
1280 ValueWord::from_f64(3.0),
1281 ValueWord::from_f64(4.0),
1282 ]));
1283 let a = ValueWord::from_array(Arc::new(vec![inner1_old, inner2.clone()]));
1284
1285 let inner1_new = ValueWord::from_array(Arc::new(vec![
1287 ValueWord::from_f64(1.0),
1288 ValueWord::from_f64(99.0),
1289 ]));
1290 let b = ValueWord::from_array(Arc::new(vec![inner1_new, inner2]));
1291
1292 let delta = diff_values(&a, &b, &schemas);
1293 assert_eq!(delta.change_count(), 1, "only one element changed");
1295 assert!(
1296 delta.changed.contains_key("[0].[1]"),
1297 "should have path [0].[1], got keys: {:?}",
1298 delta.changed.keys().collect::<Vec<_>>()
1299 );
1300 }
1301
1302 #[test]
1303 fn test_diff_nested_array_with_object_elements() {
1304 use crate::type_schema::TypeSchemaBuilder;
1305 use shape_value::{HeapValue, ValueSlot};
1306
1307 let mut schemas = TypeSchemaRegistry::new();
1308 let point_id = TypeSchemaBuilder::new("Point")
1309 .f64_field("x")
1310 .f64_field("y")
1311 .register(&mut schemas);
1312
1313 let mk_point = |x: f64, y: f64| {
1314 ValueWord::from_heap_value(HeapValue::TypedObject {
1315 schema_id: point_id as u64,
1316 slots: vec![ValueSlot::from_number(x), ValueSlot::from_number(y)]
1317 .into_boxed_slice(),
1318 heap_mask: 0,
1319 })
1320 };
1321
1322 let a = ValueWord::from_array(Arc::new(vec![mk_point(1.0, 2.0), mk_point(3.0, 4.0)]));
1323 let b = ValueWord::from_array(Arc::new(vec![mk_point(1.0, 2.0), mk_point(3.0, 99.0)]));
1324
1325 let delta = diff_values(&a, &b, &schemas);
1326 assert_eq!(delta.change_count(), 1);
1328 assert!(
1329 delta.changed.contains_key("[1].y"),
1330 "should have path [1].y, got keys: {:?}",
1331 delta.changed.keys().collect::<Vec<_>>()
1332 );
1333 }
1334
1335 #[test]
1336 fn test_diff_hashmap_nested_value_recursive() {
1337 let schemas = TypeSchemaRegistry::new();
1340
1341 let old_arr = ValueWord::from_array(Arc::new(vec![
1342 ValueWord::from_f64(1.0),
1343 ValueWord::from_f64(2.0),
1344 ]));
1345 let new_arr = ValueWord::from_array(Arc::new(vec![
1346 ValueWord::from_f64(1.0),
1347 ValueWord::from_f64(99.0),
1348 ]));
1349
1350 let a = ValueWord::from_hashmap_pairs(
1351 vec![ValueWord::from_string(Arc::new("data".to_string()))],
1352 vec![old_arr],
1353 );
1354 let b = ValueWord::from_hashmap_pairs(
1355 vec![ValueWord::from_string(Arc::new("data".to_string()))],
1356 vec![new_arr],
1357 );
1358 let delta = diff_values(&a, &b, &schemas);
1359 assert_eq!(delta.change_count(), 1);
1361 assert!(
1362 delta.changed.contains_key("data.[1]"),
1363 "should have path data.[1], got keys: {:?}",
1364 delta.changed.keys().collect::<Vec<_>>()
1365 );
1366 }
1367
1368 #[test]
1371 fn test_is_valid_delta_path_root() {
1372 assert!(super::is_valid_delta_path("."));
1373 }
1374
1375 #[test]
1376 fn test_is_valid_delta_path_simple_field() {
1377 assert!(super::is_valid_delta_path("name"));
1378 assert!(super::is_valid_delta_path("field_name"));
1379 }
1380
1381 #[test]
1382 fn test_is_valid_delta_path_dotted() {
1383 assert!(super::is_valid_delta_path("a.b.c"));
1384 assert!(super::is_valid_delta_path("inner.field"));
1385 }
1386
1387 #[test]
1388 fn test_is_valid_delta_path_array_index() {
1389 assert!(super::is_valid_delta_path("[0]"));
1390 assert!(super::is_valid_delta_path("[42]"));
1391 }
1392
1393 #[test]
1394 fn test_is_valid_delta_path_rejects_empty() {
1395 assert!(!super::is_valid_delta_path(""));
1396 }
1397
1398 #[test]
1399 fn test_is_valid_delta_path_rejects_leading_dot() {
1400 assert!(!super::is_valid_delta_path(".field"));
1401 }
1402
1403 #[test]
1404 fn test_is_valid_delta_path_rejects_trailing_dot() {
1405 assert!(!super::is_valid_delta_path("field."));
1406 }
1407
1408 #[test]
1409 fn test_is_valid_delta_path_rejects_empty_segment() {
1410 assert!(!super::is_valid_delta_path("a..b"));
1411 }
1412
1413 #[test]
1416 fn test_delta_patch_valid_paths() {
1417 let schemas = TypeSchemaRegistry::new();
1418 let base = ValueWord::from_f64(42.0);
1419 let mut delta = Delta::empty();
1420 delta
1421 .changed
1422 .insert(".".to_string(), ValueWord::from_f64(99.0));
1423
1424 let (result, rejected) = delta.patch(&base, &schemas);
1425 assert!(rejected.is_empty());
1426 assert_eq!(result.as_f64(), Some(99.0));
1427 }
1428
1429 #[test]
1430 fn test_delta_patch_rejects_invalid_paths() {
1431 let schemas = TypeSchemaRegistry::new();
1432 let base = ValueWord::from_f64(42.0);
1433 let mut delta = Delta::empty();
1434 delta
1436 .changed
1437 .insert(".".to_string(), ValueWord::from_f64(99.0));
1438 delta
1440 .changed
1441 .insert("".to_string(), ValueWord::from_f64(1.0));
1442 delta
1443 .changed
1444 .insert("a..b".to_string(), ValueWord::from_f64(2.0));
1445 delta.removed.push(".trailing.".to_string());
1446
1447 let (result, rejected) = delta.patch(&base, &schemas);
1448 assert_eq!(rejected.len(), 3);
1449 assert!(rejected.contains(&"".to_string()));
1450 assert!(rejected.contains(&"a..b".to_string()));
1451 assert!(rejected.contains(&".trailing.".to_string()));
1452 assert_eq!(result.as_f64(), Some(99.0));
1454 }
1455}