DataContext: attribute-level change tracking and merge hardening#5446
Open
knstvk wants to merge 14 commits into
Open
DataContext: attribute-level change tracking and merge hardening#5446knstvk wants to merge 14 commits into
knstvk wants to merge 14 commits into
Conversation
On a root non-fresh merge of a fuller copy onto a pre-existing managed instance, the copy loops read isLoaded from a stale CachingLoadedPropertiesInfo and skipped newly available values; the end-of-merge reset then made those attributes report loaded while holding null, so save wrote null over real database values. Reset the destination's loaded-state cache before the copy loops (resetLoadedInfoBeforeCopy in mergeState) so copy decisions recompute from the unioned fetch group and apply the values. Remove mergeFromChild's now-redundant manual pre-merge reset. Add two DataContextMergeTest regressions.
#4907) mergeFromChild now marks the merged composition child's owner chain in a dedicated compositionModifiedOwners set, consulted by isModified so the reopen gate keeps the in-context instance instead of reloading a stale copy. Owners are kept out of modifiedInstances, so they are not persisted and the composition-save contract holds. Fixes #4907 half (ii).
…arrower merge Generalize the increment-03 cold reset to the fresh path: widen isColdResetTarget from `isRoot && !isFresh` to `(isRoot || isFresh)` so a fresh merge onto a pre-existing caching instance recomputes loaded-state from the unioned fetch group instead of copying the narrower source's cache. A genuinely fetched attribute the narrower source omits no longer reverts to unloaded. Safe because mergeFetchGroups always unions, never narrows. Fuzz I2 83->0, I1 held at 0.
…d injected edits (#4071, #1258) Replace the disableListeners boolean with a mergeDepth counter, a recorded mergeApplied set of the merge's own (entity, attribute) writes, and a deferred ChangeEvent queue flushed when the outermost merge unwinds. - #4071: a property edit merged into an already-managed instance now fires a ChangeEvent (before: the whole merge ran suppressed). - #1258: a change fired during a merge that the merge did not apply is a listener-injected user edit and is now tracked, not dropped. Clear mergeApplied before the flush so a throwing flush listener cannot leak stale entries into the next merge. Pinned by four DataContextMergePolicyTest cases; fuzz gate a pure no-regression net.
…ntext identity map
…1409) StandardDetailView.hasUnsavedChanges() trusted the modifiedAfterOpen latch, which stays true after an edit is reverted to its baseline. After the latch gate, require the DataContext modified set to be non-empty, so reverting all edits clears the prompt while a pending new entity is still reported. The latch and the save-notification path are unchanged.
DataContextInvariantFuzzTest was a measurement harness that always passed and only printed a report. Add a tiered gate in the then: block: the closed invariants (I1, I2, I3, I6, I7, exceptions) must stay at 0 at any scenario count, and the known residuals are held to upper bounds (I4-ref-not-in-context <= 90, I4-ref-not-identical <= 64, I5 <= 9), enforced only at the canonical config (200 scenarios, default seed) so exploratory runs check only the zeros. A residual below its bound keeps the gate green and logs a note; the nightly diff still catches improvements. The failure message distinguishes a real regression from fuzz-config drift. Add a comment to the sales test entities (Order, OrderLine, Customer, Address) noting that their attribute set feeds the fuzz baseline and a structural change requires re-baselining. Slow-only (-PincludeSlowTests=true), unchanged. Verified green at the baseline: zeros hold, I4 = 90/64 and I5 = 9, exactly at the bounds.
knstvk
marked this pull request as ready for review
July 21, 2026 08:54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch reworks
DataContextImplchange tracking and merge so that unsaveduser edits are no longer lost or spuriously reported. Tracking becomes
per-attribute (with baselines) instead of per-entity, the merge now protects
dirty attributes from being overwritten, and several long-standing merge and
loaded-state defects are fixed. Targets the 3.1 line.
What changed
Attribute-level change tracking. A new internal
DataContextChangeTrackerrecords, per managed instance, which attributes the user changed and the
baseline each change is measured against (scalar value, reference id, embedded
dotted path, or an order-insensitive membership bag for collections).
Reverting an attribute to its baseline un-dirties it; when the last dirty
attribute of an entity reverts, the entity leaves
getModified()(unless keptvia
setModified(entity, true)).Merge conflict rule: user edits win over incoming state. For a dirty
attribute on an already-managed instance, a non-fresh merge skips the copy and
a fresh merge keeps the user value but rebaselines it (un-dirtying only if the
incoming value is now equal). Clean attributes copy as before. Reloads and
stale copies no longer clobber unsaved edits.
Loaded-state correctness and durability. The merge resets a pre-existing
managed instance's loaded-state cache before its copy loops (recomputed from
the unioned fetch group) instead of copying the source's cache, so a save no
longer writes null over a loaded value. A value that becomes present because it
was set or merge-installed is now marked loaded, and set/fetched loaded-state
stays durable across a fresh narrower merge.
Event model split. Merge-triggered
ChangeEvents are deferred and firedwhen the outermost merge unwinds; an edit injected by a listener during a merge
is now recognized and tracked instead of being swallowed. Replaces the old
disableListenersboolean.Composition and child→parent saves.
DataContextInternal.mergeFromChildlets a child context's edits win over the parent and unions the child's dirty
attributes into the parent's tracker. A child save marks the composition owner
chain modified (reopen protection only, not persisted).
Identity map on merge-transplant. A merge that brings a
reference/collection loaded onto an instance the context tracked unfetched now
merges the reached nodes into the context instead of transplanting source
copies.
New public API
DataContext.getModifiedAttributes(Object entity)— default method returningthe names of the entity's modified, not-yet-saved attributes.
io.jmix.flowui.datacontext.diagnostics(DEBUG):reports every dirty/revert/rebaseline transition, merge-skips of dirty
attributes, and a once-per-instance notice when a read-only
NoopDataContextreceives a
merge()call.isModified,getModified,setModified,clearChanges, andhasChangeskeep their signatures;
isModifiedmembership is now exact.Fixed issues
ChangeEventTests
New
data_componentssuites cover the tracker, the merge conflict policy,diagnostics, and loaded-state behavior, plus
StandardDetailViewcases for theunsaved-changes prompt. A
DataContextInvariantFuzzTestgenerates randomload/merge/edit scenarios and enforces a tiered regression gate over the merge
invariants (slow test, run with
-PincludeSlowTests=true).