From d2c05c17bc58cdbedfb0a702b2858c52aff2eedf Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 1 Nov 2026 22:22:07 +0100 Subject: api: PUT changes only what differs --- src/document/store.rs | 171 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 148 insertions(+), 23 deletions(-) (limited to 'src/document/store.rs') diff --git a/src/document/store.rs b/src/document/store.rs index 45d46cd..c2c0622 100644 --- a/src/document/store.rs +++ b/src/document/store.rs @@ -542,8 +542,9 @@ pub(crate) fn read_candidates( .collect() } -/// Writes (replaces) the value at `path`, creating intermediate maps. Within a list, -/// the segment is an index (`list_index`): an item there is replaced, one at the end +/// Writes the value at `path`, creating intermediate maps: afterwards it reads as +/// `value`, what was there changed only where it differs (`update_key`). Within a list, +/// the segment is an index (`list_index`): an item there is overwritten, one at the end /// appended; past the end, or anything but an index, names nothing (`NotFound`). pub(crate) fn write_path( doc: &mut impl Transactable, @@ -552,12 +553,7 @@ pub(crate) fn write_path( ) -> Result<(), StoreError> { let (leaf, parents) = path.split_last().ok_or(StoreError::NotFound)?; match parent_for_write(doc, parents)? { - Parent::Maps(maps) => { - // Replace semantics: clear the key in every merged parent, or an old map - // would merge into the new value. - delete_in_all(doc, &maps, leaf)?; - json_to_automerge(doc, &maps[0], leaf, value)?; - } + Parent::Maps(maps) => update_key(doc, &maps, leaf, value)?, Parent::List(list) => { let index = list_index(leaf).ok_or(StoreError::NotFound)?; let length = doc.length(&list); @@ -565,14 +561,149 @@ pub(crate) fn write_path( return Err(StoreError::NotFound); } if index < length { - doc.delete(&list, index)?; + update_item(doc, &list, index, value)?; + } else { + write_value(doc, &list, Slot::Index(index), value)?; } - write_value(doc, &list, Slot::Index(index), value)?; } } Ok(()) } +// A write makes its path read as the value written, changing only what differs: a map +// written over a map keeps its keys that stay (and what is inside them), and gets +// those it lacks and loses those the value doesn't have; a list written over a list +// does the same item by item, by position; a scalar equal to the one there isn't +// written again. So a write of a whole map, concurrent with one inside it elsewhere, +// keeps the other unless it changes the same key. Where the path holds concurrent +// values (a conflict), or one of another kind, it is written anew: the write ends the +// conflict. + +/// What `value` would be stored as, if it is a scalar. +fn json_scalar(value: &JsonValue) -> Result, StoreError> { + Ok(Some(match value { + JsonValue::String(value) => ScalarValue::Str(value.as_str().into()), + JsonValue::Bool(value) => ScalarValue::Boolean(*value), + JsonValue::Number(value) => { + if let Some(value) = value.as_i64() { + ScalarValue::Int(value) + } else if let Some(value) = value.as_u64() { + ScalarValue::Uint(value) + } else if let Some(value) = value.as_f64() { + ScalarValue::F64(value) + } else { + return Err(anyhow!("Unsupported JSON number").into()); + } + } + JsonValue::Null => ScalarValue::Null, + JsonValue::Object(_) | JsonValue::Array(_) => return Ok(None), + })) +} + +/// What updating a place to a value comes to, given what it holds (all its concurrent +/// values). +enum Update { + /// Nothing: the same scalar is there. + Same, + /// A map there, the only value: its keys updated. + Map(ObjId), + /// A list there, the only value: its items updated. + List(ObjId), + /// Written anew. + Anew, +} + +fn update_of(holds: &[(Value<'static>, ObjId)], value: &JsonValue) -> Result { + let [(held, id)] = holds else { + return Ok(Update::Anew); + }; + Ok(match (held, value) { + (Value::Object(ObjType::Map), JsonValue::Object(_)) => Update::Map(id.clone()), + (Value::Object(ObjType::List), JsonValue::Array(_)) => Update::List(id.clone()), + (Value::Scalar(held), _) if json_scalar(value)?.as_ref() == Some(held.as_ref()) => { + Update::Same + } + _ => Update::Anew, + }) +} + +/// Updates `key` of the merged maps `maps` to `value`. +fn update_key( + doc: &mut impl Transactable, + maps: &[ObjId], + key: &str, + value: &JsonValue, +) -> Result<(), StoreError> { + match update_of(&candidates(doc, maps, key)?, value)? { + Update::Same => Ok(()), + Update::Map(map) => update_map(doc, &map, value), + Update::List(list) => update_list(doc, &list, value), + Update::Anew => { + // Cleared in every merged map, or an old map would merge into the new value. + delete_in_all(doc, maps, key)?; + json_to_automerge(doc, &maps[0], key, value) + } + } +} + +/// Updates the item at `index` of `list` (which has it) to `value`. +fn update_item( + doc: &mut impl Transactable, + list: &ObjId, + index: usize, + value: &JsonValue, +) -> Result<(), StoreError> { + match update_of(&candidates(doc, std::slice::from_ref(list), index)?, value)? { + Update::Same => Ok(()), + Update::Map(map) => update_map(doc, &map, value), + Update::List(inner) => update_list(doc, &inner, value), + Update::Anew => write_value(doc, list, Slot::At(index), value), + } +} + +fn update_map( + doc: &mut impl Transactable, + map: &ObjId, + value: &JsonValue, +) -> Result<(), StoreError> { + let JsonValue::Object(object) = value else { + unreachable!("a map is updated to an object"); + }; + let maps = std::slice::from_ref(map); + // The keys it loses first, then those it keeps or gets, each in key order. + for key in doc.keys(map).collect::>() { + if !object.contains_key(&key) { + doc.delete(map, key.as_str())?; + } + } + for (key, value) in object { + update_key(doc, maps, key, value)?; + } + Ok(()) +} + +fn update_list( + doc: &mut impl Transactable, + list: &ObjId, + value: &JsonValue, +) -> Result<(), StoreError> { + let JsonValue::Array(items) = value else { + unreachable!("a list is updated to an array"); + }; + let length = doc.length(list); + for (index, item) in items.iter().enumerate() { + if index < length { + update_item(doc, list, index, item)?; + } else { + write_value(doc, list, Slot::Index(index), item)?; + } + } + for index in (items.len()..length).rev() { + doc.delete(list, index)?; + } + Ok(()) +} + /// What a write's last segment goes into. enum Parent { /// Merged maps (`[0]` takes new keys). @@ -868,6 +999,8 @@ enum Slot<'a> { Key(&'a str), /// An index in a list. The value is inserted here, not overwritten. Index(usize), + /// An index in a list whose item is overwritten. + At(usize), } fn put_object( @@ -879,6 +1012,7 @@ fn put_object( Ok(match slot { Slot::Key(key) => doc.put_object(parent, key, obj_type)?, Slot::Index(index) => doc.insert_object(parent, index, obj_type)?, + Slot::At(index) => doc.put_object(parent, index, obj_type)?, }) } @@ -891,6 +1025,7 @@ fn put_scalar( match slot { Slot::Key(key) => doc.put(parent, key, value)?, Slot::Index(index) => doc.insert(parent, index, value)?, + Slot::At(index) => doc.put(parent, index, value)?, } Ok(()) } @@ -914,20 +1049,10 @@ fn write_value( write_value(doc, &id, Slot::Index(index), item)?; } } - JsonValue::String(value) => put_scalar(doc, parent, slot, value.as_str())?, - JsonValue::Bool(value) => put_scalar(doc, parent, slot, *value)?, - JsonValue::Number(value) => { - if let Some(value) = value.as_i64() { - put_scalar(doc, parent, slot, value)?; - } else if let Some(value) = value.as_u64() { - put_scalar(doc, parent, slot, value)?; - } else if let Some(value) = value.as_f64() { - put_scalar(doc, parent, slot, value)?; - } else { - return Err(anyhow!("Unsupported JSON number").into()); - } + scalar => { + let scalar = json_scalar(scalar)?.expect("neither an object nor an array"); + put_scalar(doc, parent, slot, scalar)?; } - JsonValue::Null => put_scalar(doc, parent, slot, ScalarValue::Null)?, } Ok(()) -- cgit v1.3.1