aboutsummaryrefslogtreecommitdiffstats
path: root/src/document/store.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-01 22:22:07 +0100
committertv <tv@krebsco.de>2026-11-01 22:22:07 +0100
commitd2c05c17bc58cdbedfb0a702b2858c52aff2eedf (patch)
treeaf2b484ac84b259aa7e9bccc5222698b9ba15b81 /src/document/store.rs
parent743fe1a098d93b62beba30fe0764dc86891be2df (diff)
api: PUT changes only what differs
Diffstat (limited to 'src/document/store.rs')
-rw-r--r--src/document/store.rs171
1 files changed, 148 insertions, 23 deletions
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<Option<ScalarValue>, 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<Update, StoreError> {
+ 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::<Vec<_>>() {
+ 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(())