Commit 9a486c7
fix: Deep-merge nested data documents in Engine::add_data (microsoft#760)
* Deep-merge nested data documents in Engine::add_data
add_data previously performed a shallow merge: adding a nested object under a key that already existed either replaced the whole subtree or errored on a spurious conflict, instead of merging the trees. This makes Engine::add_data (and the shared Value::merge) recurse into nested objects so keys from both sides are preserved, matching OPA's data-document merge semantics. Nested sets are unioned as a regorus extension (OPA data is JSON and has no sets). Genuine leaf conflicts (same path, two different scalar values) still error; equal values remain a no-op, which the shared rule-evaluation path relies on. Adds tests for object deep-merge, set union, leaf/type conflicts, and interaction with the 'with data.x' modifier.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs(value): clarify Value::merge conflict wording
Copilot review on microsoft#760 noted the doc comment called non-mergeable variants 'non-container values', which is misleading since arrays are containers yet still conflict unless equal. Reword to describe a conflict as any differing pair that is not both objects or both sets (e.g. unequal scalars or arrays).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* perf(value): avoid deep-cloning RHS set during merge union
When unioning sets in Value::merge, the RHS set is often shared: the object arm recurses via existing.merge(v.clone()), which bumps the incoming set's Rc refcount. The old Rc::make_mut(new) then structurally deep-cloned the entire RHS BTreeSet just to drain it via append and immediately discard the copy.
Move the elements out when the RHS set is uniquely owned, and otherwise clone only the per-element Rc handles into the destination. The union result is identical (BTreeSet dedups), but no throwaway set is allocated on the nested-merge path exercised by add_data deep-merge.
Addresses a Copilot review comment on microsoft#760.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(engine): make add_data atomic on merge conflict
Now that Value::merge recurses, a conflict in a later nested key was reported only after earlier keys of the same document had already been written into the live init_data, leaving the engine partially mutated on a rejected add_data.
Add a read-only Value::check_mergeable that mirrors merge's conflict rule (objects deep-merge, sets union, equal values no-op, anything else conflicts) and run it in add_data before merging. On conflict nothing is mutated, so add_data is all-or-nothing. The check allocates nothing and never copies the data spine, preserving merge's in-place uniquely-owned fast path (no candidate copy of the data document).
Adds regression tests for a partial object-leaf conflict and a partial set-union conflict. Reported by a maintainer on microsoft#760.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* test(engine): add array atomicity regression for add_data
Arrays are atomic leaves, so a differing array at a shared path is a
conflict. The new key sorts before the conflicting array key, so a naive
in-place merge would leak the new key before hitting the conflict. This
test locks in that add_data rejects the whole call and leaves data
untouched.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix: make add_data atomic under allocator memory limits
On �llocator-memory-limits builds, Value::merge runs the limit check
*after* inserting each key, so an add_data whose merge trips the limit
mid-way left the data document partially mutated. check_mergeable only
models semantic conflicts, not limit failures, so the validate-then-merge
precheck couldn't cover this failure mode.
Use a build-split strategy in �dd_data:
- default builds: keep the zero-copy validate-then-merge fast path
(a conflict is the only way the merge can fail).
- allocator-memory-limits builds: merge into a candidate copy and commit
only on success, making both conflict and limit failures transactional.
Value is Rc/copy-on-write, so only touched subtrees are cloned.
check_mergeable is now cfg-gated to the default build to avoid dead code.
Tests (allocator-memory-limits build): add a partial-merge atomicity test
(limit trips mid-merge, data must be untouched) and a candidate-copy
conflict-atomicity test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix: separate strict rule-output merge from data-document deep-merge
microsoft#760 made Value::merge recursive so Engine::add_data deep-merges nested
data documents. But that same method also backs rule materialization,
where recursion is wrong: two rule definitions producing different
outputs for one path must conflict (OPA complete-rule semantics), not
silently combine.
Split the two behaviors:
- Value::merge is strict and shallow again (as pre-microsoft#760): a key on both
sides must be equal or it conflicts; used for rule outputs.
- Value::deep_merge is the recursive data-document merge behind add_data;
check_mergeable validates it up front without allocating, so the
default build merges in place instead of cloning a candidate.
Also fix zero-arg functions (f() := ...): route their materialization
through strict equality via a new RuleValueMerge selector, so disjoint
outputs ({a:1} vs {b:2}) conflict as OPA does while prefix scaffolding
(a.foo + a.bar) still combines.
Add a 14-case interpreter conformance matrix (multiple_outputs.yaml)
covering functions, static/dynamic partial objects, and ref-heads,
matched against OPA v1.2.0.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* perf(value): make deep_merge acquire mutable access lazily
deep_merge's object arm called Rc::make_mut on the target map up front,
cloning a shared map's spine even when the merge changed nothing (a
no-op subset re-add) or conflicted before any mutation. Decide each
incoming key from a read-only probe (skip / insert / recurse / conflict)
and take Rc::make_mut only when a key actually mutates, so no-op and
conflict merges leave shared maps untouched.
Behavior is unchanged: the equality short-circuit that previously ran
inside the recursive call now runs in the probe, and conflicts bail with
the same message. Add value tests asserting Rc::ptr_eq is preserved
across no-op subset, equal-nested-object, and first-key-conflict merges.
OPA conformance unchanged (3021 pass / 651 fail, byte-identical).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* feat(value): bound deep_merge recursion depth to prevent stack-overflow DoS
deep_merge and check_mergeable recursed unbounded on object/set nesting.
A Value built without serde_json's parse-time recursion limit (the Python
and Ruby native bindings, or programmatic construction) could therefore
drive add_data into a stack overflow -- an uncatchable abort that poisons
every engine in an FFI process.
Thread a depth counter through both functions and bail past MAX_MERGE_DEPTH
(128, matching serde_json's default) so over-deep data fails with a clean
Err. In the default build check_mergeable trips first, keeping add_data
atomic; the guard in deep_merge covers the allocator-memory-limits build
and any disjoint-then-overlapping merge.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs(changelog): note strict zero-arg function conflict and add_data depth limit
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Mark Birger <markbirger@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>1 parent 9838b25 commit 9a486c7
8 files changed
Lines changed: 1113 additions & 17 deletions
File tree
- src
- tests/interpreter
- value
- tests
- interpreter/cases/rule
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
9 | 18 | | |
10 | 19 | | |
11 | 20 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
434 | 434 | | |
435 | 435 | | |
436 | 436 | | |
437 | | - | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
438 | 444 | | |
439 | 445 | | |
440 | 446 | | |
| |||
453 | 459 | | |
454 | 460 | | |
455 | 461 | | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
456 | 466 | | |
457 | 467 | | |
458 | | - | |
| 468 | + | |
459 | 469 | | |
460 | 470 | | |
461 | 471 | | |
| |||
464 | 474 | | |
465 | 475 | | |
466 | 476 | | |
467 | | - | |
468 | | - | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
469 | 500 | | |
470 | 501 | | |
471 | 502 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
63 | 74 | | |
64 | 75 | | |
65 | 76 | | |
| |||
3408 | 3419 | | |
3409 | 3420 | | |
3410 | 3421 | | |
| 3422 | + | |
| 3423 | + | |
| 3424 | + | |
| 3425 | + | |
| 3426 | + | |
| 3427 | + | |
| 3428 | + | |
| 3429 | + | |
| 3430 | + | |
| 3431 | + | |
| 3432 | + | |
| 3433 | + | |
| 3434 | + | |
| 3435 | + | |
| 3436 | + | |
| 3437 | + | |
| 3438 | + | |
3411 | 3439 | | |
3412 | 3440 | | |
3413 | 3441 | | |
| |||
3663 | 3691 | | |
3664 | 3692 | | |
3665 | 3693 | | |
| 3694 | + | |
3666 | 3695 | | |
3667 | 3696 | | |
3668 | 3697 | | |
3669 | 3698 | | |
3670 | 3699 | | |
3671 | 3700 | | |
3672 | 3701 | | |
3673 | | - | |
| 3702 | + | |
| 3703 | + | |
| 3704 | + | |
| 3705 | + | |
3674 | 3706 | | |
3675 | 3707 | | |
3676 | 3708 | | |
| |||
3778 | 3810 | | |
3779 | 3811 | | |
3780 | 3812 | | |
3781 | | - | |
| 3813 | + | |
| 3814 | + | |
| 3815 | + | |
| 3816 | + | |
| 3817 | + | |
| 3818 | + | |
| 3819 | + | |
3782 | 3820 | | |
3783 | 3821 | | |
3784 | 3822 | | |
| |||
3790 | 3828 | | |
3791 | 3829 | | |
3792 | 3830 | | |
3793 | | - | |
| 3831 | + | |
| 3832 | + | |
| 3833 | + | |
| 3834 | + | |
| 3835 | + | |
| 3836 | + | |
| 3837 | + | |
3794 | 3838 | | |
3795 | 3839 | | |
3796 | 3840 | | |
| |||
4037 | 4081 | | |
4038 | 4082 | | |
4039 | 4083 | | |
| 4084 | + | |
4040 | 4085 | | |
4041 | 4086 | | |
4042 | 4087 | | |
| |||
0 commit comments