mirror of
https://github.com/Start9Labs/patch-db.git
synced 2026-03-26 18:31:53 +00:00
fix: revert from ciborium to serde_cbor_2, make apply_patches iterative
- ciborium's deserialize_str bug (#32) caused DB deserialization to fail silently, nuking the database value to null on compress - Switch to dr-bonez/cbor fork (serde_cbor_2) which has StreamDeserializer change_output_type support - Propagate deserialization errors instead of falling back to null - Convert apply_patches from recursion to iteration to prevent stack overflow on patches with many operations (e.g. 953 ops)
This commit is contained in:
@@ -438,66 +438,75 @@ fn apply_patches<'a>(
|
||||
patches: &'a [PatchOperation],
|
||||
undo: &mut Undo<'a>,
|
||||
) -> Result<(), PatchError> {
|
||||
let (patch, tail) = match patches.split_first() {
|
||||
None => return Ok(()),
|
||||
Some((patch, tail)) => (patch, tail),
|
||||
};
|
||||
|
||||
let res = match *patch {
|
||||
PatchOperation::Add(ref op) => {
|
||||
let prev = add(doc, &op.path, op.value.clone())?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
match prev {
|
||||
None => remove(doc, &op.path, true).unwrap(),
|
||||
Some(v) => add(doc, &op.path, v).unwrap().unwrap(),
|
||||
};
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
let initial_len = undo.0.len();
|
||||
for patch in patches {
|
||||
let res = match *patch {
|
||||
PatchOperation::Add(ref op) => {
|
||||
add(doc, &op.path, op.value.clone()).map(|prev| {
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
match prev {
|
||||
None => {
|
||||
remove(doc, &op.path, true).unwrap();
|
||||
}
|
||||
Some(v) => {
|
||||
add(doc, &op.path, v).unwrap().unwrap();
|
||||
}
|
||||
};
|
||||
}));
|
||||
})
|
||||
}
|
||||
PatchOperation::Remove(ref op) => {
|
||||
remove(doc, &op.path, false).map(|prev| {
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
assert!(add(doc, &op.path, prev).unwrap().is_none());
|
||||
}));
|
||||
})
|
||||
}
|
||||
PatchOperation::Replace(ref op) => {
|
||||
replace(doc, &op.path, op.value.clone()).map(|prev| {
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
replace(doc, &op.path, prev).unwrap();
|
||||
}));
|
||||
})
|
||||
}
|
||||
PatchOperation::Move(ref op) => {
|
||||
mov(doc, &op.from, &op.path, false).map(|prev| {
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
mov(doc, &op.path, &op.from, true).unwrap();
|
||||
if let Some(prev) = prev {
|
||||
assert!(add(doc, &op.path, prev).unwrap().is_none());
|
||||
}
|
||||
}));
|
||||
})
|
||||
}
|
||||
PatchOperation::Copy(ref op) => {
|
||||
copy(doc, &op.from, &op.path).map(|prev| {
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
match prev {
|
||||
None => {
|
||||
remove(doc, &op.path, true).unwrap();
|
||||
}
|
||||
Some(v) => {
|
||||
add(doc, &op.path, v).unwrap().unwrap();
|
||||
}
|
||||
};
|
||||
}));
|
||||
})
|
||||
}
|
||||
PatchOperation::Test(ref op) => {
|
||||
test(doc, &op.path, &op.value).map(|()| {
|
||||
undo.0.push(Box::new(move |_| ()));
|
||||
})
|
||||
}
|
||||
};
|
||||
if let Err(e) = res {
|
||||
while undo.0.len() > initial_len {
|
||||
undo.0.pop().unwrap()(doc);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
PatchOperation::Remove(ref op) => {
|
||||
let prev = remove(doc, &op.path, false)?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
assert!(add(doc, &op.path, prev).unwrap().is_none());
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
PatchOperation::Replace(ref op) => {
|
||||
let prev = replace(doc, &op.path, op.value.clone())?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
replace(doc, &op.path, prev).unwrap();
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
PatchOperation::Move(ref op) => {
|
||||
let prev = mov(doc, &op.from, &op.path, false)?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
mov(doc, &op.path, &op.from, true).unwrap();
|
||||
if let Some(prev) = prev {
|
||||
assert!(add(doc, &op.path, prev).unwrap().is_none());
|
||||
}
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
PatchOperation::Copy(ref op) => {
|
||||
let prev = copy(doc, &op.from, &op.path)?;
|
||||
undo.0.push(Box::new(move |doc| {
|
||||
match prev {
|
||||
None => remove(doc, &op.path, true).unwrap(),
|
||||
Some(v) => add(doc, &op.path, v).unwrap().unwrap(),
|
||||
};
|
||||
}));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
PatchOperation::Test(ref op) => {
|
||||
test(doc, &op.path, &op.value)?;
|
||||
undo.0.push(Box::new(move |_| ()));
|
||||
apply_patches(doc, tail, undo)
|
||||
}
|
||||
};
|
||||
if res.is_err() {
|
||||
undo.0.pop().unwrap()(doc);
|
||||
}
|
||||
res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Patch provided JSON document (given as `imbl_value::Value`) in place.
|
||||
|
||||
@@ -81,3 +81,41 @@ fn revert_tests() {
|
||||
fn merge_tests() {
|
||||
util::run_specs("specs/merge_tests.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_ops_no_stack_overflow() {
|
||||
let mut doc = json!({"items": {}});
|
||||
let ops: Vec<PatchOperation> = (0..10_000)
|
||||
.map(|i| {
|
||||
PatchOperation::Add(AddOperation {
|
||||
path: format!("/items/{i}").parse().unwrap(),
|
||||
value: json!(i),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let p = Patch(ops);
|
||||
patch(&mut doc, &p).unwrap();
|
||||
assert_eq!(doc["items"].as_object().unwrap().len(), 10_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_ops_undo_on_failure() {
|
||||
let original = json!({"items": {}});
|
||||
let mut doc = original.clone();
|
||||
let mut ops: Vec<PatchOperation> = (0..1000)
|
||||
.map(|i| {
|
||||
PatchOperation::Add(AddOperation {
|
||||
path: format!("/items/{i}").parse().unwrap(),
|
||||
value: json!(i),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// Final op fails: test against a wrong value at a valid path
|
||||
ops.push(PatchOperation::Test(TestOperation {
|
||||
path: "/items/0".parse().unwrap(),
|
||||
value: json!("wrong"),
|
||||
}));
|
||||
let p = Patch(ops);
|
||||
assert!(patch(&mut doc, &p).is_err());
|
||||
assert_eq!(doc, original, "document should be fully restored after failed patch");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user